linux EC驱动书写指南

驱动书写指南系列会提供另一个角度的驱动分析,linux内核把各驱动共同的部分抽象出来,做在一起称为框架。就比如说文件系统,linux内核定义好了文件系统中最通用的打开文件、读写文件等公共接口,但是并没有实现函数。这些定义好的接口,可以认为是框架。等到了真正的文件系统实现的时候 ,才会填充这些open、read等函数。对于实现文件系统的程序员来说,就是填充框架外的其他内容,一般都是和硬件相关性比较大。

power supply core介绍

在本文中,主要介绍怎么注册自己的ec驱动。ec驱动的框架部分,power supply都是实现过了,这里有一个介绍power supply core中,蜗窝科技有一个文章介绍power supply core的,这是连接:http://www.wowotech.net/pm_subsystem/psy_class_overview.html

在上文的 介绍里补充一点内容,power supply 硬件属性分别是什么意思,在写ec驱动的途中,一大部分时间花在研究这几个属性分别是描述什么的以及我需要什么属性,大部分还是蜗窝科技写的对几个属性的解释,能找到的中文资料非常非常少,还是挺值得记录一下的。

enum power_supply_property {
    /* Properties of type `int' */
    POWER_SUPPLY_PROP_STATUS = 0,
    POWER_SUPPLY_PROP_CHARGE_TYPE,
    POWER_SUPPLY_PROP_HEALTH,
    POWER_SUPPLY_PROP_PRESENT,
    POWER_SUPPLY_PROP_ONLINE,
    POWER_SUPPLY_PROP_AUTHENTIC,
    POWER_SUPPLY_PROP_TECHNOLOGY,
    POWER_SUPPLY_PROP_CYCLE_COUNT,
    POWER_SUPPLY_PROP_VOLTAGE_MAX,
    POWER_SUPPLY_PROP_VOLTAGE_MIN,
    POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN,
    POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN,
    POWER_SUPPLY_PROP_VOLTAGE_NOW,
    POWER_SUPPLY_PROP_VOLTAGE_AVG,
    POWER_SUPPLY_PROP_VOLTAGE_OCV,
    POWER_SUPPLY_PROP_VOLTAGE_BOOT,
    POWER_SUPPLY_PROP_CURRENT_MAX,
    POWER_SUPPLY_PROP_CURRENT_NOW,
    POWER_SUPPLY_PROP_CURRENT_AVG,
    POWER_SUPPLY_PROP_CURRENT_BOOT,
    POWER_SUPPLY_PROP_POWER_NOW,
    POWER_SUPPLY_PROP_POWER_AVG,
    POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN,
    POWER_SUPPLY_PROP_CHARGE_EMPTY_DESIGN,
    POWER_SUPPLY_PROP_CHARGE_FULL,
    POWER_SUPPLY_PROP_CHARGE_EMPTY,
    POWER_SUPPLY_PROP_CHARGE_NOW,
    POWER_SUPPLY_PROP_CHARGE_AVG,
    POWER_SUPPLY_PROP_CHARGE_COUNTER,
    POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT,
    POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX,
    POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE,
    POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX,
    POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT,
    POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT_MAX,
    POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT,
    POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN,
    POWER_SUPPLY_PROP_ENERGY_EMPTY_DESIGN,
    POWER_SUPPLY_PROP_ENERGY_FULL,
    POWER_SUPPLY_PROP_ENERGY_EMPTY,
    POWER_SUPPLY_PROP_ENERGY_NOW,
    POWER_SUPPLY_PROP_ENERGY_AVG,
    POWER_SUPPLY_PROP_CAPACITY, /* in percents! */
    POWER_SUPPLY_PROP_CAPACITY_ALERT_MIN, /* in percents! */
    POWER_SUPPLY_PROP_CAPACITY_ALERT_MAX, /* in percents! */
    POWER_SUPPLY_PROP_CAPACITY_LEVEL,
    POWER_SUPPLY_PROP_TEMP,
    POWER_SUPPLY_PROP_TEMP_MAX,
    POWER_SUPPLY_PROP_TEMP_MIN,
    POWER_SUPPLY_PROP_TEMP_ALERT_MIN,
    POWER_SUPPLY_PROP_TEMP_ALERT_MAX,
    POWER_SUPPLY_PROP_TEMP_AMBIENT,
    POWER_SUPPLY_PROP_TEMP_AMBIENT_ALERT_MIN,
    POWER_SUPPLY_PROP_TEMP_AMBIENT_ALERT_MAX,
    POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW,
    POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG,
    POWER_SUPPLY_PROP_TIME_TO_FULL_NOW,
    POWER_SUPPLY_PROP_TIME_TO_FULL_AVG,
    POWER_SUPPLY_PROP_TYPE, /* use power_supply.type instead */
    POWER_SUPPLY_PROP_USB_TYPE,
    POWER_SUPPLY_PROP_SCOPE,
    POWER_SUPPLY_PROP_PRECHARGE_CURRENT,
    POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT,
    POWER_SUPPLY_PROP_CALIBRATE,
    /* Properties of type `const char *' */
    POWER_SUPPLY_PROP_MODEL_NAME,
    POWER_SUPPLY_PROP_MANUFACTURER,
    POWER_SUPPLY_PROP_SERIAL_NUMBER,
};

  • status: 表示电池状态,充电中、放电、未充电和电池电量已满
  • charge type:表示充电类型,快充、点滴式充电器(trikle charge)
  • health:表示电池健康属性,主要用来表示电池健康、过热、损坏、过压等
  • present:表示电池或者适配器是否存在
  • online:表示电池或者适配器是否在线,有时候可能电池存在但是并没有接线。有的ec芯片这两个状态都是可以读到的。
  • authentic:真实参数,暂时在内核里没发现哪个ec驱动使用了这个属性。这个属性是为了给用户看,移动设备的适配器 或者电池是不是非标准、不合格的。
  • technology:表示电池采用的技术,lion(锂离子电池)、nimh(镍氢电池)、lipo(锂聚合物电池)、nicd(镍铬电池)、limn(锰酸锂电池)
  • voltage:电压参数,框架定义了最大、最小、设计最大、设计最小等,框架定义的单位是μV
  • current:电流参数,框架定义了最大、目前和平均等,放电为负值,单位是μA,
  • power:电流参数,和current不同的是,这个电流参数是智能电池导出的。智能电池使用功率作为工作单位和充电放电测量单位,所以不适合再导出在current属性里了。
  • charge:容量参数,框架定义了设计满、设计空、充满、空等等,框架定义的很多设计值都是用来计算电池的健康属性的,比如说设计满容量是3000,但是目前充满只有1500了,那就说明电池的健康系数是50%。只是举个例子,实际上电池健康系数不可能用仅仅一个属性来计算,单位是μAh
  • energy:功率参数,用来计算功耗,单位是μWh
  • capacity:容量百分比,常被桌面用来显示电池图标选中后,出现的提示框里显示电池容量多少。
  • temp:温度参数,注意这个问题是电池温度,并不是cpu温度,单位是摄氏度
  • time:时间参数,充满时间、放空时间等等,单位是s

最后还有几个字符串类型的属性,模块名称、生产商和序列号。

实现自己的ec驱动

定义psy设备

第一步是明确设备的充电类型,框架定义的充电类型有:


enum power_supply_type {
    POWER_SUPPLY_TYPE_UNKNOWN = 0,
    POWER_SUPPLY_TYPE_BATTERY,
    POWER_SUPPLY_TYPE_UPS,
    POWER_SUPPLY_TYPE_MAINS,
    POWER_SUPPLY_TYPE_USB, /* Standard Downstream Port */
    POWER_SUPPLY_TYPE_USB_DCP, /* Dedicated Charging Port */
    POWER_SUPPLY_TYPE_USB_CDP, /* Charging Downstream Port */
    POWER_SUPPLY_TYPE_USB_ACA, /* Accessory Charger Adapters */
    POWER_SUPPLY_TYPE_USB_TYPE_C, /* Type C Port */
    POWER_SUPPLY_TYPE_USB_PD, /* Power Delivery Port */
    POWER_SUPPLY_TYPE_USB_PD_DRP, /* PD Dual Role Port */
    POWER_SUPPLY_TYPE_APPLE_BRICK_ID, /* Apple Charging Method */
};  

一般来讲笔记本都是这两个充电类型:mains类型和battery类型,移动设备需要定义usb的充电类型。先定义两个最常见的psy设备:适配器和电池,适配器的充电类型是mains,电池是battery。


static const struct power_supply_desc shiwen_ac_desc = {
    .name = "shiwen_ac",
    .type = POWER_SUPPLY_TYPE_MAINS,
    .properties = shiwen_power_ac_props,
    .num_properties = ARRAY_SIZE(shiwen_power_ac_props),
    .get_property = shiwen_power_get_ac_property,
};
static const struct power_supply_desc shiwen_bat_desc = {
    .name = "shiwen_battery",
    .type = POWER_SUPPLY_TYPE_BATTERY,
    .properties = shiwen_power_battery_props,
    .num_properties = ARRAY_SIZE(shiwen_power_battery_props),
    .get_property = shiwen_power_get_battery_property,
};

选择psy设备属性

定义好之后,再选择每个psy设备的硬件属性,这部分要看ec芯片提供了什么数据然后选择定义什么属性。上一节提到的属性是框架定义的全部属性,ec芯片不可能提供这全部的数据。这里写的示例,就简单选择几个上层需要的属性吧。适配器就一个是否在线属性,电池桌面需要的属性有电池存在标志、充满时间、放空时间、电池状态、电池容量百分比、电池容量等级、模块名、制造商。


static enum power_supply_property shiwen_power_ac_props[] = {
    POWER_SUPPLY_PROP_ONLINE,
};
static enum power_supply_property shiwen_power_battery_props[] = {
    POWER_SUPPLY_PROP_STATUS,
    POWER_SUPPLY_PROP_PRESENT,
    POWER_SUPPLY_PROP_CAPACITY, /* in percents! */
    POWER_SUPPLY_PROP_CAPACITY_LEVEL,
    POWER_SUPPLY_PROP_TIME_TO_EMPTY,
    POWER_SUPPLY_PROP_TIME_TO_FULL,
    POWER_SUPPLY_PROP_MODEL_NAME,
    POWER_SUPPLY_PROP_MANUFACTURER,
};

填充获取psy属性的方法

接着要告诉内核如何获取前面定义的psy属性,psy core留了一个get_property接口,需要驱动工程师把方法填充在接口里。这部分就完全和硬件相关了,不同的ec芯片,读取psy属性方法不同,比如说本文的示例代码里仅仅实现了一个固定值。


struct int shiwen_power_get_battery_property(struct power_supply *psy,
                       enum power_supply_property psp,
                       union power_supply_propval *val)
{
    case POWER_SUPPLY_PROP_ONLINE:
                val->intval = 1;                        //always assume ac online
                break;
    default:
            val->intval = -1;
            pr_err("property error\n");
}
struct int shiwen_power_get_battery_property(struct power_supply *psy,
                       enum power_supply_property psp,
                       union power_supply_propval *val)
{
    switch (psp) {
    case POWER_SUPPLY_PROP_MODEL_NAME:
                val->strval = "Shiwen example driver";
                break;
    case POWER_SUPPLY_PROP_MANUFACTURER:
                val->strval = "Shiwen example";
                 break;
    case POWER_SUPPLY_PROP_STATUS:
                val->intval = Charging;                                    //always assume battery charging
                break;
    case POWER_SUPPLY_PROP_PRESENT:
                val->intval = 1;                                                    //always assume battery present
                break;
    case POWER_SUPPLY_PROP_CAPACITY:
                val->intval = 100;                                                //always assume battery capacity 100%
                break;
    case POWER_SUPPLY_PROP_CAPACITY_LEVEL:
                val->intval = POWER_SUPPLY_CAPACITY_LEVEL_FULL; 
                break;
    case POWER_SUPPLY_PROP_TIME_TO_EMPTY:
                val->intval = 7200;                                                //always assume battery need 2 hours to empty
                break;
    case POWER_SUPPLY_PROP_TIME_TO_FULL:
                val->intval = 0;
                break;
    default:
                val->intval =-1;
                pr_err("property error\n");
                break;
}

到这一步,两个psy设备定义完成了。接下来只需要把注册设备,在sys下就能看到接口了。

注册设备

设备注册,psy core提供了接口,只需要调用接口并做好错误处理即可。

static int __init shiwen_power_init(void)
{
        shiwen_ac = power_supply_register(NULL, &shiwen_ac_desc, NULL);
        if (IS_ERR(shiwen_ac)) {
            pr_err("%s: failed to register\n",__func__, shiwen_ac_desc.name);
            ret = PTR_ERR(shiwen_ac);
            goto failed_ac;
          }
        shiwen_bat = power_supply_register(NULL, &shiwen_bat_desc, NULL);
        if (IS_ERR(shiwen_ac)) {
            pr_err("%s: failed to register %s\n", __func__, shiwen_bat_desc.name);
            ret = PTR_ERR(shiwen_bat);
            goto failed_bat;
        }
    return 0;
failed_bat:
    power_supply_unregister(shiwen_bat);
failed_ac:
    power_supply_unregister(shiwen_ac);
    return ret;
}

查看结果

到这里,非常简单的ec示例驱动注册就完成了,在sys下查看一下是否正确注册了设备,并且获取到正确的内容。

deepin@deepin-PC:~$ ls /sys/class/power_supply/
shiwen_ac shiwen_battery
deepin@deepin-PC:~$ 
deepin@deepin-PC:~$ ls /sys/class/power_supply/shiwen_ac/
online power subsystem type uevent
deepin@deepin-PC:~$ cat /sys/class/power_supply/shiwen_ac/uevent 
POWER_SUPPLY_NAME=shiwen_ac
POWER_SUPPLY_ONLINE=1
deepin@deepin-PC:~$ ls /sys/class/power_supply/shiwen_battery
capacity capacity_level  manufacturer mcu_time_effect model_name power present status subsystem time_to_empty_avg time_to_full_avg type uevent
deepin@deepin-PC:~$ cat /sys/class/power_supply/shiwen_battery/uevent 
POWER_SUPPLY_NAME=shiwen_battery
POWER_SUPPLY_STATUS=Charging
POWER_SUPPLY_PRESENT=1
POWER_SUPPLY_CAPACITY=100
POWER_SUPPLY_CAPACITY_LEVEL=POWER_SUPPLY_CAPACITY_LEVEL_FULL
POWER_SUPPLY_TIME_TO_EMPTY_AVG=7200
POWER_SUPPLY_TIME_TO_FULL_AVG=0
POWER_SUPPLY_MODEL_NAME=Shiwen example driver
POWER_SUPPLY_MANUFACTURER=Shiwen example

到这,就大功告成啦。sys下文件创建、sys下文件的读写都有sysfs框架和psy core框架帮我们实现。上文是一个非常非常简单的psy驱动实现流程,ec状态的改变驱动并没有关注,都是依赖上层daemon或者读取 sys下的文件来感知,对于用户来说相当于是轮询读取。实际上日常我们遇到的ec芯片都没有这么原始,ec事件基本上都是中断通知,所以一个完成的ec驱动还要在上面的流程里面加上中断的注册和处理代码。

增加中断处理

关注过我之前写过两个中断介绍文章的童鞋肯定知道驱动注册中断的第一步,肯定是实现中断处理函数。处理函数也和硬件有关系,有的ec芯片ac插拔事件和电池事件的中断是分开的,假设我们虚构的ec芯片只有ac和电池的插拔才能触发中断(看内核psy实现的芯片,这样的中断也是最常见的)。假设ac事件和电池事件共享155号中断(随意选的,没有任何理论依据),那么中断处理函数处理第一步肯定是确定中断源。接着根据中断源,更新一下设备状态即可。

static irqreturn_t shiwen_ec_interrupt(int irq, void *dev_id)
{
    int status;
    int source;
    source = SHIWEN_READ_INTSOURCE_REG();
    if (source == SHIWEN_BATTERY)
            power_supply_changed(shiwen_battery);
    else if(source == SHIWEN_AC)
            power_supply_changed(shiwen_ac);
    else
    	 //there are no psy event
        return IRQ_NONE;                                                   
    return IRQ_HANDLED;
}

更新整个psy设备状态的函数是psy core框架提供的,很好用吧。psy会根据参数传进去的设备,调用获取属性函数,更新psy设备属性。这个处理函数非常简单,因为ec芯片的中断就很简单。如果是比较复杂的ec芯片的话,可能中断源分的比较多,比如说电压变化、容量变化、电流变化等等都会有一种中断的产生。注意:为了避免读到的状态有问题,需要在中断产生后等一段时间再去读ec状态,所以中断处理函数一般都需要有延时读取ec状态。power_supply_changed函数内部实现是采用work queue方式读取状态的,我们自己实现的话,为了简单且保险,可以用delay work。假设说,需要实现一个电压变化中断的处理函数:

static irqreturn_t shiwen_voltage_interrupt(int irq, void *dev_id)
{
    int status;
    schedule_delayed_work(&ec_work, JIFFIES_NUM);
    return IRQ_HANDLED;
}

static void ec_work_func(struct work_struct *work)
{
    shiwen_update_voltage();
}

在 中断处理函数里面,仅有调用delay work内容,延时过了之后函数会被加载工作队列里,借此达到延时的目的。接下来指定一下延时工作进程需要执行的函数,这是工作进程要求的。这就是一个基本的读取某一个属性的中断函数实现,没实现真正电压读取是因为,电压读取是真正和硬件相关的,每款芯片都不一样,我也实在是虚构不出来了orz…

处理函数写完了之后,中断注册到内核里就可以使用啦。剩下的中断触发是设备的事,中断触发之后的感知是cpu的事,感知到中断之后在调到处理函数之前是内核中断子系统的事,前面中断的两篇文章介绍过内核已经做好了,驱动并不关心。中断的注册一般放在psy设备注册之后,修改过得psy设备注册代码如下:

static int __init shiwen_power_init(void)

{
        shiwen_ac = power_supply_register(NULL, &shiwen_ac_desc, NULL);
        ......
        shiwen_bat = power_supply_register(NULL, &shiwen_bat_desc, NULL);
        ......
          //shiwen_ac and shiwen_battery called shiwen_battery_ac
        ret =request_irq(155,  shiwen_ec_interrupt, IRQF_SHARED, shiwen_battery_ac);                  
        INIT_DELAYED_WORK(&ec_work, ec_work_func);
        return 0;
        ......
}

增加了中断注册个delay work初始化的代码(如果需要自己使用delay work读取某一个属性的时候),代码写到这里,一个带基本功能的ec驱动就做完了。看吧,还是很简单的吧。

如果电池芯片不支持中断呢?

对于虚拟的电池或者其他规范的电池芯片,硬件认为驱动只要读到正确的值就可以了,电池芯片把状态送到某个位置,任何电池事件并不会触发中断,包括适配器插拔和电池插拔。这时候就需要驱动工程师想个办法把电池驱动套到psy框架下了,最简单的办法肯定是——定时器。既然不支持中断,那我定时读取总可以了吧,虽然不能保证状态及时更新。
首先定义一个定时器和定时器超时处理函数:

struct timer_list power_timer;
#define TIMER_COUNT    60   /*设置超时时间为1分钟*/

void power_timer_handler(struct timer_list *unused)
{
       mod_timer(&power_timer, jiffies+HZ * TIMER_COUNT);
       power_supply_changed(shiwen_ac);
       power_supply_changed(shiwen_bat);
}

设置超时时间是60s,定时器超时处理函数就做了俩事,再次设置定时器超时时间和更新shiwen_ac和shiwen_bat状态。这里强调一点,尽量调用psy core提供的更新状态函数,而不要自己实现。因为这个函数的功能并不仅仅是调用get_properties接口,还有触发 内核uevent等,不要问为啥强调。orz…
最后在驱动入口函数加上定时器设置和 定时器修改超时时间等代码即可。

timer_setup(&power_timer, power_timer_handler, 0);
mod_timer(&power_timer, jiffies+HZ * TIMER_COUNT);

到这,就算设备没有电池,我也能写一个电池驱动出来!就这么霸道,快去试试看。

热心读者疑问解答

为什么需要ec芯片呢?不能做一个纯软件实现的吗?

先说结论:当然可以做,但不推荐做。对于ec芯片来说,他需要做的就是传递电池信息,这个信息包括:电池是否在线、电压值、电流值、容量温度事件等等,这些值难道不能模拟吗?当然不是,只要经过多次实验,得到一个放电曲线和充电曲线,在充电放电途中电压、电流、容量等值都可以通过计算得来。至于电池是否在线,可以写一个历程轮询查看,虽然代价有点大,也算是可以做。
为什么不推荐做呢?原因有下面几条:

  • 计算状态不准确。电池是个很精密的外设,想要仅仅通过数学计算得到每一个准确的状态过于困难。
  • 内核无法做硬件排障。通过计算得到一个相对靠谱的电池信息,都是建立在电池工作正常的情况下,万一发生电池过压、过热、过流等硬件故障呢?内核要怎么知道呢?电池温度如果超过80度,笔记本就很危险了。
  • 电池在线轮询例程是一个不小的软件开销,如果这个工作放在硬件来做,也是个性能的提升不是。

欢迎大家踊跃提问,一起交流!

你可能感兴趣的:(linux内核的一些事,驱动书写指南,linux内核模块)