Linux的I2C子系统资料遍地都是,但是单看资料是不会明白驱动如何写的,所以选择了一个简单的器件,针对该器件有目的的写一个驱动实现最基本的器件设置和数据读取。
SHT20是一个采用标准I2C协议通信的温湿度传感器,其精度和特性在本次学习中无需关注。驱动基于的开发板环境如下表所示:
环境 | 参数 |
---|---|
开发板型号 | 荣品RP-DV300B |
芯片型号 | Hi3516DV300 |
Kernel Version | 4.9.37 SMP |
SDK | Hi3516CV500_SDK_V2.0.1.0 |
linux4.9.37版本使用设备树描述板级连接,在 /linux-4.9.37-smp/arch/arm/boot/dts 目录下的 hi3516dv300-demb.dts 文件中添加节点。
这里需要注意的是设备I2C地址使用的是不包含读写位的7位地址,I2C的频率应当适配硬件,SHT20支持的最高SCL频率为0.4MHz,这在手册上可以找到。
很多资料都是这样修改设备树的,但是这样修改之后,编译内核时出现如下警告(对设备树不是很了解,所以放任不管),这个警告并不影响器件运作。
SHT20挂接在I2C总线上,但是从设备类型看,依旧属于字符设备。从整体来看,驱动程序使用 i2c_add_driver() 和 i2c_del_driver() 函数实现设备在I2C总线上的挂载和卸载,使用 cdev_init() 、 cdev_add() 和 cdev_del() 函数实现对字符设备的注册和注销。
I2C核心实现设备的匹配,执行 .probe() 和 .remove() ,在 .probe() 函数中进行字符设备注册,将文件操作结构体填充并绑定到注册的字符设备上。这样对字符设备的访问便会从文件读写函数,通过 i2c_transfer() 向I2C适配器提交 struct i2c_msg 结构体报文实现。
多说无益,来看伪代码:
static int sht2x_open(struct inode *inode,struct file *filp)
{
filp->private_data=st_dev.client;
/*此处初始化SHT2x器件(软复位)*/
return 0;
}
static int sht2x_read(struct file *filp,char __user *buf,size_t count,loff_t *f_pos)
{
struct i2c_client *client = (struct i2c_client *)filp->private_data;
/*对传感器数据进行读取*/
copy_to_user(...);
/*读回调返回值为驱动读到的有效数据长度*/
return count;
}
static int sht2x_release(struct inode *inode,struct file *filp)
{
return 0;
}
static struct file_operations sht2x_fops=
{
.owner =THIS_MODULE,
.read =sht2x_read,
.open =sht2x_open,
.release =sht2x_release,
};
static int sht2x_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
int ret;
/*注册字符设备*/
cdev_init(&st_dev.cdev,&sht2x_fops);
cdev_add(&st_dev.cdev, ...);
/*创建设备节点*/
class_create(THIS_MODULE,"sht2xclass");
device_create(dev_class,NULL,MKDEV(driver_major,0),NULL,"sht2x");
st_dev.client=client;
return 0;
FAIL:
return ret;
}
static int sht2x_remove(struct i2c_client *client)
{
/*删除字符设备*/
cdev_del(&st_dev.cdev);
/*删除节点*/
device_unregister(dev_class_device);
class_destroy(dev_class);
return 0;
}
/*设备树方式*/
static struct of_device_id sht2x_of_match[] = {
{ .compatible = "sensirion,sht2x", .data = NULL },
{}
};
static struct i2c_driver sht2x_driver =
{
.driver =
{
.name = "sht2x",
.owner = THIS_MODULE,
.of_match_table = sht2x_of_match,
},
.probe = sht2x_probe,
.remove = sht2x_remove,
.id_table = sht2x_id,
};
static int __init sht2x_init(void)
{
return i2c_add_driver(&sht2x_driver);
}
module_init(sht2x_init);
static void __exit sht2x_exit(void)
{
i2c_del_driver(&sht2x_driver);
}
module_exit(sht2x_exit);
代码删除了大部分内容,但是依然可以看出上面所说的过程。这个过程对于学习过字符设备驱动的人来说非常简单。
每一个 struct i2c_msg 结构体有自己的起始信号,在I2C适配器传输过程中,该结构体的数量取决于一次通信需要多少个起始信号。
I2C终止信号在一次 i2c_transfer() 函数调用完成后发送。在数据发送过程中,使用结构体的 .flag 标志位指定读写标志位。
static uint16_t i2c_read_reg(struct i2c_client *client, uint8_t reg, uint8_t *recv, uint32_t len)
{
struct i2c_msg msg[2];
/*首先使用IIC写模式传出要读取的寄存器地址*/
msg[0].addr = client->addr;
msg[0].flags = 0;//0表示写
msg[0].buf = ®
msg[0].len = 1;
/*然后使用IIC读模式读取从机传来的数据,具体长度由器件协议决定*/
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD;
msg[1].buf = recv;//读取的数据存放的缓冲区指针
msg[1].len = len;//要读取的长度由操作决定
/*提交适配器执行消息动作*/
ret = i2c_transfer(client->adapter, msg, 2);
}
SHT20的中英文手册网上都有,为了方便在文末提供下载链接。
由于只是实现最简单的温湿度读取功能,所以在驱动中只需要实现以下三个函数:
1.设备打开时进行软复位,软复位之后等待15ms
2.主机模式下的温度读取
3.主机模式下的湿度读取
这是因为用户寄存器复位之后的模式为最高分辨率,本次无需更改即可使用。
上面两个图是从手册上摘出的。软件中只需要按照对应的命令写入数据即可。驱动程序将在文末【本文资源】中给出下载链接,就不在正文中粘贴了。
编写中对数据进行读取时需要注意两个细节。一是对读出的原始温湿度数据进行的处理,虽然上图中标明了Data位的MSB到LSB之间一共占据了14位,但是对高低位进行合并的时候却是高位对其的,手册中这样写道:
在进行物理换算时,后两位状态位应置‘0’
另一点需要注意的是,linux驱动中对浮点计算有问题,所以传感器输出的温湿度将在用户应用程序中进行计算。
在测试中,使用海思自带的I2C总线控制驱动进行数据的传输,在默认情况下,数据传输将出现如下问题:
在测试应用中每一次数据读取会出现这样的提示:
hibvt-i2c 120b2000.i2c: wait rx no empty timeout, RIS: 0x10, SR: 0xa0000
经查,这个提示出现在 linux-4.9.37-smp/drivers/i2c/busses/i2c-hibvt.c 文件中:
这表示在等待50us*1024≈51ms没有收到数据后,对本次I2C读取判定超时,这与上面逻辑分析仪中测的的情况相符。由于SHT20在最高分辨率下温度的转换需要66ms(逻辑分析仪同样证实这一点),
所以修复上面的错误可以采用两种方式进行:降低传感器测量分辨率或者增大I2C读超时时间,本文采取后者。
正常对SHT20的温度读取波形如下所示(其中红点表示停止信号,显示过密起始信号隐去了):
最终读取的温湿度如图所示:
百度网盘链接: https://pan.baidu.com/s/1iOG4FrrvNUgLUctqSZn1Ag
提取码: aqwq
1.Linux驱动开发(十八):I2C驱动
————2020-6-13 @燕卫博————