1、添加驱动文件
话不多说,接着上一讲,我们已经成功创建了设备节点,那接下来就是编写驱动了。如果不知道怎么把添加的驱动源码编译进内核的可以参考我这篇文章,手把手教你如何将自己添加的源码编译进内核
在我们项目代码的/kernel/driver/input目录下创建temperature目录并添加驱动文件shtc3.c shtc3.h,添加Makefile和Kconfig。
编写框架前我们要知道的几个概念
i2c_driver 表示i2c驱动
i2c_client 表示i2c设备
i2c_adapter 表示i2c适配器
i2c总线驱动编写的大体思路是什么?理解下面这两句话至关重要
- 上一篇我们编写了设备树节点,设备树节点里详细描述了shtc3这个i2c设备的地址、compatible属性、设备类型、中断是否使能、延时等状态,这一步相当与初始化了i2c_client中的device成员,这些信息都可以通过i2c_client->device.device_node里面去获取
- 编写驱动文件,在驱动文件中,我们会创建并初始化一个i2c_driver结构体,然后把i2c_driver这个结构体放到i2c_add_driver函数中去注册,i2c_add_driver中会拿着i2c_driver这个结构体中的driver下的of_match_table去遍历设备树中的设备信息,所以我们只要把of_match_table 中的compatible属性定义成和设备树中的compatible保持一致,那么就会匹配成功,匹配成功则会执行i2c_driver中probe对应的函数。(这一步中,匹配两个compatible属性,匹配成功就调用probe函数,这些工作都是i2c总线帮我们做的,我们要做的就是dts中设备树节点信息编写正确,驱动文件中,i2c_driver结构体以及各个成员变量初始化正确,然后把i2c_driver送个i2c_add_driver函数,让它去注册就完事了)
2、开始编写I2C驱动框架
先搭一个i2c驱动注册的框架
static int shtc3_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
printk("into shtc3_probe\n");
return 0;
}
//传统匹配方法
static const struct i2c_device_id shtc3_id[] = {
{ "tmp_shtc3", 0 },
{ }
};
//设备树匹配方法
static struct of_device_id shtc3_dt_id[] = {
{ .compatible = "tmp_shtc3" },//此处的compatible 要和设备树节点中的compatible名称一致
{ }
};
static struct i2c_driver shtc3_i2c_driver = {
.probe = shtc3_probe,//i2c总线中设备和驱动匹配成功则调用此函数
.id_table = shtc3_id,//传统匹配方法
.driver = {
.name = "shtc3",
.of_match_table = of_match_ptr(shtc3_dt_id),//设备树匹配
},
};
static int __init shtc3_init(void) {
return i2c_add_driver(&shtc3_i2c_driver);
}
static void __exit shtc3__exit(void) {
return i2c_del_driver(&shtc3_i2c_driver);
}
module_init(shtc3_init);
module_exit(shtc3__exit);
MODULE_AUTHOR("xpc");
MODULE_DESCRIPTION("Sensirion SHTC3 humidity and temperature sensor driver");
MODULE_LICENSE("GPL");
i2c_add_driver中完成了驱动的注册,驱动注册完成后会遍历设备树中的设备,如有设备的compatible属性与该驱动匹配,就会执行驱动的probe函数。
验证:
编译烧录后启动板子,如果设备和驱动能匹配成功,则会进入到i2c_driver 结构体的probe函数。printk(“into shtc3_probe\n”);这句便会打印出来。
3、完善probe函数
这一步便是驱动开发的核心,也是初学者最难开始的地方,怎么初始化、操控寄存器、获取底层数据都是在这一步中完成的。
这里怎么开头,最好的方法就是站在巨人的肩膀上,看看前辈都是怎么做的。其实在Linux驱动源码中已经有一个现成的shtc1.c的温湿度传感器驱动文件,我们使用的shtc3是它的升级款,所以驱动直接仿照shtc1.c来做就好了,我们来一句句解析代码
/* commands (high precision mode) */
static const unsigned char shtc1_cmd_measure_blocking_hpm[] = { 0x7C, 0xA2 };
static const unsigned char shtc1_cmd_measure_nonblocking_hpm[] = { 0x78, 0x66 };
/* commands (low precision mode) */
static const unsigned char shtc1_cmd_measure_blocking_lpm[] = { 0x64, 0x58 };
static const unsigned char shtc1_cmd_measure_nonblocking_lpm[] = { 0x60, 0x9c };
/* command for reading the ID register */
static const unsigned char shtc1_cmd_read_id_reg[] = { 0xef, 0xc8 };
/* constants for reading the ID register */
#define SHTC1_ID 0x07
#define SHTC1_ID_REG_MASK 0x1f
/* delays for non-blocking i2c commands, both in us */
#define SHTC1_NONBLOCKING_WAIT_TIME_HPM 14400
#define SHTC1_NONBLOCKING_WAIT_TIME_LPM 1000
#define SHTC1_CMD_LENGTH 2
#define SHTC1_RESPONSE_LENGTH 6
struct shtc1_platform_data {
bool blocking_io;
bool high_precision;
};
struct shtc1_data {
struct i2c_client *client;
struct mutex update_lock;//驱动要用到的互斥锁
bool valid;
unsigned long last_updated; /* in jiffies */
const unsigned char *command;//存放以哪种模式读取温湿度数据的命令
unsigned int nonblocking_wait_time; //非阻塞模式的等待时间
struct shtc1_platform_data setup;//存放时钟是否使能以及数据读取模式
int temperature; //存放温度数据
int humidity; //存放湿度数据
};
//根据模式设置筛选出具体读取数据的方式
static void shtc1_select_command(struct shtc1_data *data)
{
if (data->setup.high_precision) {
data->command = data->setup.blocking_io ?
shtc1_cmd_measure_blocking_hpm :
shtc1_cmd_measure_nonblocking_hpm;
data->nonblocking_wait_time = SHTC1_NONBLOCKING_WAIT_TIME_HPM;
} else {
data->command = data->setup.blocking_io ?
shtc1_cmd_measure_blocking_lpm :
shtc1_cmd_measure_nonblocking_lpm;
data->nonblocking_wait_time = SHTC1_NONBLOCKING_WAIT_TIME_LPM;
}
}
static int shtc1_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
int ret;
char id_reg[2];
struct shtc1_data *data;//一般一个驱动都会专门定义一个结构体去存放它的各种数据,详见上
struct device *hwmon_dev;//定义一个要注册的设备
struct i2c_adapter *adap = client->adapter;//i2c适配器,i2c驱动和i2c设备之间的通信需要它
struct device *dev = &client->dev;//devm_kzalloc第一个参数所需
/* 这一句貌似是绝大部分i2c驱动都会做的一步,检查i2c_adapter是否正常,我们照做就行了 */
if (!i2c_check_functionality(adap, I2C_FUNC_I2C)) {
dev_err(dev, "plain i2c transactions not supported\n");
return -ENODEV;
}
/* 由数据手册可知,有一个efc8寄存器用于验证传感器是否能正常沟通,所以我们这里读取该寄存器 */
ret = i2c_master_send(client, shtc1_cmd_read_id_reg, SHTC1_CMD_LENGTH);
if (ret != SHTC1_CMD_LENGTH) {
dev_err(dev, "could not send read_id_reg command: %d\n", ret);
return ret < 0 ? ret : -ENODEV;
}
/* 读取完,再接收数据,进行验证 */
ret = i2c_master_recv(client, id_reg, sizeof(id_reg));
if (ret != sizeof(id_reg)) {
dev_err(dev, "could not read ID register: %d\n", ret);
return -ENODEV;
}
if ((id_reg[1] & SHTC1_ID_REG_MASK) != SHTC1_ID) {
dev_err(dev, "ID register doesn't match\n");
return -ENODEV;
}
/* 为设备申请内存空间 */
data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
/* 初始化 shtc1_data 结构体 */
data->setup.blocking_io = false;//时钟不使能
data->setup.high_precision = true;//正常模式
data->client = client;
if (client->dev.platform_data)
data->setup = *(struct shtc1_platform_data *)dev->platform_data;
shtc1_select_command(data);//根据以上模式确认读取数据的指令
mutex_init(&data->update_lock);
/* 注册hwmon子系统,成功则会有一个设备挂在hwmon下 */
hwmon_dev = devm_hwmon_device_register_with_groups(dev,
client->name,
data,
shtc1_groups);
if (IS_ERR(hwmon_dev))
dev_dbg(dev, "unable to register hwmon device\n");
return PTR_ERR_OR_ZERO(hwmon_dev);
}
到这里probe函数中的内容已经完成一大半,剩余的工作就是完成sysfs操作节点的创建
4、sysfs操作节点的创建
在devm_hwmon_device_register_with_groups函数中,有一个参数我们是没有初始化的shtc1_groups,它便是负责创建设备操作节点的。
这里要重点看三个宏的定义
- #define ATTRIBUTE_GROUPS(_name)
static const struct attribute_group _name##_group = {
.attrs = _name##_attrs,
};
所以ATTRIBUTE_GROUPS(shtc1);其实就是
static const struct attribute_group shtc1_group =
{.attrs = shtc1_attrs,}; - #define DEVICE_ATTR_RO(name)
struct device_attribute dev_attr##_name = __ATTR_RO(_name) - #define __ATTR_RO(_name) {
.attr = { .name = __stringify(_name), .mode = S_IRUGO },
.show = _name##_show,
}
由2和3可得,static DEVICE_ATTR_RO(temp1_input);就是把temp1_input_show作为.show的参数,
当应用使用cat命令操作这个节点时,便会调用这个函数
static DEVICE_ATTR_RO(humidity1_input);同理,因为这个驱动只需要读取数据所以用DEVICE_ATTR_RO这个宏,如果要写入数据则用DEVICE_ATTR_RW,并且要多实现一个store的函数,供echo命令使用
static ssize_t temp1_input_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
printk("temp1_input_show\n");
return 0;
}
static ssize_t humidity1_input_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
printk("humidity1_input_show\n");
return 0;
}
/*
*因为温湿度驱动上层只需要读取节点,所以用DEVICE_ATTR_RO(xxx)只需要实现xxx_show的函数
*当上层使用cat命令操作时
*便会调用这个函数,如果上层还需要对节点写入数据,则使用DEVICE_ATTR_RW(xxx),
*并再定义一个xxx_store的函数
*来对应上层的echo命令
*/
static DEVICE_ATTR_RO(temp1_input);
static DEVICE_ATTR_RO(humidity1_input);
static struct attribute *shtc1_attrs[] = {
&dev_attr_temp1_input.attr,//此处dev_attr_temp1_input.attr便是由上面的宏DEVICE_ATTR_RO(temp1_input定义
&dev_attr_humidity1_input.attr,
NULL
};
ATTRIBUTE_GROUPS(shtc1);//此处便是shtc1_groups声明的地方,查看ATTRIBUTE_GROUPS的定义展开来如下
/*
static const struct attribute_group shtc1_group =
{.attrs = shtc1_attrs,}; //这里的shtc1_attrs便是上面定义的struct attribute结构体
*/
写到这里,底层数据获取已经快接近尾声了,先编译烧录进板子,看看目前为止的编码是否有问题,我们开发过程中要走一截验一截,否则一股脑全部干完,出现问题了很难定位。
当我们在板子上能看到这个节点,并且cat这个节点能出现对应的打印,说明驱动是通的,接下来,我们只要在这个函数中获取温度和湿度的数据,返回就ok了
5、实现节点操作函数
最后一步是底层数据获取,也是最难的一步。
i2c协议中使用i2c_master_send发送指令,i2c_master_recv接收数据,这两个接口也是i2c核心代码已经实现好了,我们只要保证参数的正确性
详细见代码注释,由下往上分析
static int shtc1_update_values(struct i2c_client *client,
struct shtc1_data *data,
char *buf, int bufsize)
{
//发送读取数据指令,这里的data->command由前面可知是0x7866,长度是2字节
int ret = i2c_master_send(client, data->command, SHTC1_CMD_LENGTH);
if (ret != SHTC1_CMD_LENGTH) {
dev_err(&client->dev, "failed to send command: %d\n", ret);
return ret < 0 ? ret : -EIO;
}
/*
* In blocking mode (clock stretching mode) the I2C bus
* is blocked for other traffic, thus the call to i2c_master_recv()
* will wait until the data is ready. For non blocking mode, we
* have to wait ourselves.
*/
//非阻塞模式,我们得自己等
if (!data->setup.blocking_io)
usleep_range(data->nonblocking_wait_time,
data->nonblocking_wait_time + 1000);
//接收数据,保存在buf中
ret = i2c_master_recv(client, buf, bufsize);
if (ret != bufsize) {
dev_err(&client->dev, "failed to read values: %d\n", ret);
return ret < 0 ? ret : -EIO;
}
return 0;
}
static struct shtc1_data *shtc1_update_client(struct device *dev)
{
struct shtc1_data *data = dev_get_drvdata(dev);//获取dev中的driver_data
struct i2c_client *client = data->client;
unsigned char buf[SHTC1_RESPONSE_LENGTH];//用来保存获取的数据
int val;
int ret = 0;
mutex_lock(&data->update_lock);//因为会频繁获取数据,所以需要加锁
//因为我们是设置的时钟拉伸不使能的模式,所以每次获取数据时有一个间隔时间
if (time_after(jiffies, data->last_updated + HZ / 10) || !data->valid) {
ret = shtc1_update_values(client, data, buf, sizeof(buf));//获取数据的函数
if (ret)
goto out;
/*
* From datasheet:
* T = -45 + 175 * ST / 2^16
* RH = 100 * SRH / 2^16
*
* Adapted for integer fixed point (3 digit) arithmetic.
*/
//这里温度的数据处理要特别注意,因为Linux内核不支持浮点运算,
//所以想要多保留小数点后几位需要特别处理
//我们保留小数点后三位,所以放大1000倍,正常来说,根据公式
//会这样写((175000 * val) >> 16) - 45000;
//但是会有问题,val是一个5位数得值,所以175000*val超过了int数据类型范围了
//所以这里直接把175000 >> 3后,再乘以val,然后再 >> 13,减去45000
//就得到这个了((21875 * val) >> 13) - 45000
//湿度也是做同样处理,先把100000 >> 3后,再做处理
val = be16_to_cpup((__be16 *)buf);
data->temperature = ((21875 * val) >> 13) - 45000;
val = be16_to_cpup((__be16 *)(buf + 3));
data->humidity = ((12500 * val) >> 13);
data->last_updated = jiffies;
data->valid = true;
}
out:
mutex_unlock(&data->update_lock);
//返回shtc1_data结构体
return ret == 0 ? data : ERR_PTR(ret);
}
static ssize_t temp1_input_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct shtc1_data *data = shtc1_update_client(dev);
if (IS_ERR(data))
return PTR_ERR(data);
return sprintf(buf, "%d\n", data->temperature);//int转换成char
}
tatic ssize_t humidity1_input_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct shtc1_data *data = shtc1_update_client(dev);
if (IS_ERR(data))
return PTR_ERR(data);
return sprintf(buf, "%d\n", data->humidity);//int转换成char
}
编译、烧录、验证,
温度:27.279 ℃
相对湿度:59.495 %RH
到这里Linux驱动开发算是完成了,本来如果只是给传感器写个驱动并提供能读取温湿度数据的节点,那倒是比较轻松的事。
但如果你是做的andoird平台的温湿度驱动开发,安卓上层应用的同事还会要求我们按照安卓标准的流程来,这样他们就能通过注册一个服务直接读取传感器事件数据了。这样做的好处就是第三方的应用也能正常读取温湿度的数据并展示,那么得把这个驱动注在input子系统下,而不是hwmon子系统,后面则还有hal层的工作。
这里我不再继续赘述了,如果有读者需要,我再更新。
看到这儿了,如果有帮助的话,就点个赞吧~