前言
在开始转Linux 驱动开发的时候,就计划着把约翰·马迪厄的Linux设备驱动开发看完,然后找一些器件把上面的驱动实验都做一下的。因为各种各样的原因,一直没有做这个事情。现在也算是做了一点项目,计划深入并复习一下。于是乎,专门开个专栏记录一下,适配各个器件驱动的过程,内核API使用,驱动开发注意事项,以及调试代码。方便自己使用的时候查看,也为正在学习的童鞋做一个分享。
Linux设备驱动开发并没有想象的那么难,设备主要分为控制通道和数据通道,数据通道主要是外设总线的驱动,这些都是由SOC厂商完成,比如MIPI。因此我们主要开发工作内容是设备的控制通道。大部分的设备在内核也是有驱动的,部分没有驱动的设备就需要我们自己开发。难点主要还是在对芯片的了解,芯片的使用上,主要工作量还是在阅读理解芯片手册上面。像实验用到的芯片,温湿度计,GPIO扩展芯片,ENC28J60等比较简单,芯片手册就几十页,复杂的像serdes芯片,成百上千页的内容,更是难以上手。因此,主要还是要静下心来,好好阅读理解datasheet,主要依赖的就是英文阅读能力。在反复的英文内容阅读,近两年来,datasheet的阅读流畅度已经是进步非常大了。
I2C Client Driver
驱动开发步骤
驱动注册
主要是将我们的驱动注册到I2C总线驱动上。I2C总线上挂了很多驱动和设备,总线会在有设备或者驱动注册时,进行匹配比较,将能够进行匹配的驱动和设备进行绑定。这样,在设备树种申明的设备就能使用我们开发好的驱动了。
I2C总线提供快速注册宏:
static struct i2c_driver bmp280_driver = {
.probe = bmp280_probe,
.remove = bmp280_remove,
.driver = {
.owner = THIS_MODULE,
.name = "bmp280_drv",
.of_match_table = of_match_ptr(bmp280_of_match),
},
.id_table = bmp280_id_table,
};
module_i2c_driver(bmp280_driver);
申明一个i2c_driver,然后填充我们必须支持的方法,probe,remove,如果驱动在整个申明周期都不需要卸载的话,甚至可以不需要remove。然后将这个驱动注册到I2C总线驱动上即可。使用 module_i2c_driver
宏能够减轻工作量,不需要再写init 和 exit,分别实现注册和取消注册。
设备树匹配
匹配有很多种机制,我们一般使用设备树匹配的方式,匹配驱动和设备。在 struct i2c_driver
结构种,of_match_table 是使用设备树的匹配方式,id_table 是老式的id匹配机制。可以在代码中同时使用,已兼容较新老版本的内核。设备是有设备树进行申明定义的,这里是通过 compatible 属性,进行匹配的,只要驱动的compatible 属性能和设备的compatible属性匹配上,那么驱动和设备就匹配成功了。
当使用 id_table 时,并不需要做什么,只要注册驱动,并在设备树中添加设备,并填写正确的compatible属性。当使用of_match_table时,在低版本的内核中,仍然需要定义id_table(也可仅定义id_table),否则,驱动检测到id_table是NULL,将返回-ENODEV错误导致probe不执行,比如4.14版本内核。在高版本的内核中,已经不存在这个问题了,可以仅使用of_match_table,如果检测到id_table是NULL,将使用of_match_table进行匹配。
driver方法填充
一般需要我们完成probe函数和remove函数的编写。尽量不要使用单例模式,由于可能驱动会被多个设备使用,尽量还是不要使用单例模式。动态分配内存,将实例保存到驱动的数据指针中。在使用时,调出来即可。一般是放到 struct device
结构中的: dev->driver_data
。I2C设备驱动,可以调用 i2c_set_clientdata
和 i2c_get_clientdata
API,保存和获取驱动实例。如果是虚拟平台设备驱动,可以使用 platform_set_drvdata
和 platform_get_drvdata
进行设置。
在probe中初始化和注册的调用,大多数都需要我们在remove接口中去去初始化和取消注册。需要支持remove的驱动,一定不要忘记做这个事情。因此,在我们申请和初始化时,尽量使用 devm_ 前缀风格的API,这些API支持在设备驱动移出时,自行管理。
完成函数实现后,填充到 struct i2c_driver
结构中去。
设备实例申明
最关键的,我们需要申明我们的设备结构。所有的方法填充和操作,都是围绕这个结构进行展开的,设备数据也将保存到这个结构种。另外,还需要将内核设备实例关联到我们自己的结构种,这样才能随时在两个结构之间进行切换,找到我们需要的设备实例数据,以及内核设备的数据及方法。
这里由于是I2C client设备,因此可以存放 struct i2c_client *
指针,然后通过i2c_set_clientdata 来反向绑定我们自己的设备实例。这将使得我们在内核定义的方法中,通过其提供的设备实例获得我们自己的数据结构。
开发注意事项
错误处理
保证使用内核标准的错误码和处理流程。主要使用 IS_ERR,PTR_ERR,ERR_PTR。
IS_ERR:判断API返回的指针是否是一个错误。这里指针会被设置为特殊值,而不是NULL,这样可以通过预定义的特殊值,指导返回的错误消息。
PTR_ERR: 将上述的包含错误信息的指针值,转化为long类型的值,方便我们对照errno。
ERR_PTR:我们自己在实现函数时,当返回值是指针时,我们可以通过这个将我们需要的错误信息放到指针值中进行返回。
错误码的定义,尽量要和错误的具体情况符合。让我看一眼,就能准确知道是什么类型的错误,不要随便返回与错误无关的错误码。错误码主要定义在以下几个文件中:
uapi/linux/errno.h
asm-generic/errno.h
asm-generic/errno-base.h
日志信息
推荐使用 dev_err 风格的错误日志输出API,好处是,我们能清楚的知道是那个设备和驱动报出来的错误信息。这里输出的前缀是 “driver_name device_name”,如果没有 driver_name,那么将输出 “bus_name device_name” 。日志中不需要添加换行符。pr_err 风格的API也是不需要添加换行符的。
BMP280驱动代码
设备树:
&i2c4 {
status = "okay";
bmp280: bmp280@76 {
status = "okay";
compatible = "bosch,bmp280";
reg = <0x76>;
};
}
注意:reg属性是必须的。
较新的内核中包含了BME280/BMP280传感器的驱动的。这里的驱动只是作为练习,实际项目中可以直接启动内核中的驱动。
#include "asm-generic/errno-base.h"
#include "asm-generic/errno.h"
#include "linux/device.h"
#include "linux/i2c.h"
#include "linux/kernel.h"
#include "linux/slab.h"
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/delay.h>
struct bmp280_compensation {
unsigned short dig_T1;
signed short dig_T2;
signed short dig_T3;
unsigned short dig_P1;
signed short dig_P2;
signed short dig_P3;
signed short dig_P4;
signed short dig_P5;
signed short dig_P6;
signed short dig_P7;
signed short dig_P8;
signed short dig_P9;
s32 t_fine;
};
struct bmp280_dev {
struct i2c_client *client;
struct bmp280_compensation compensation;
s32 adc_T;
s32 adc_P;
s32 temperature;
u32 pressure;
};
/* auto increase */
struct bmp280_i2c_reg_ctrl_r {
u8 reg;
u8 *val;
u16 len;
};
/* can't auto increase */
struct bmp280_i2c_reg_ctrl_w {
u8 reg;
u8 val;
};
static int bmp280_i2c_write_reg(struct i2c_client *client, u16 chip, u8 reg, u8 value)
{
u8 data[] = {
reg, value};
struct i2c_msg msg[] = {
{
.flags = I2C_M_STOP,
.addr = chip,
.buf = data,
.len = ARRAY_SIZE(data),
},
};
if (i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg)) < 0) {
dev_err(&client->dev, "i2c write reg error, chip=0x%02x, reg=0x%02x, val=0x%02x\n",
chip, reg, value);
}
return 0;
}
static int bmp280_i2c_read_reg(struct i2c_client *client, u16 chip, u8 reg, u8 *value)
{
struct i2c_msg msg[] = {
{
.flags = 0,
.addr = chip,
.buf = ®,
.len = 1,
},
{
.flags = I2C_M_RD | I2C_M_STOP,
.addr = chip,
.buf = value,
.len = 1,
},
};
if (i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg)) < 0) {
dev_err(&client->dev, "i2c read reg error, chip=0x%02x, reg=0x%02x",
chip, reg);
return -EIO;
}