三、I2C客户端驱动 —— htu21d
前言
和bmp280一样,使用i2c驱动注册。需要注意的是,设备reset需要15ms,可以使用读出来的第三个字节作为CRC8校验码,进行数据校验。另外,温湿度计算公式如果使用小数,可能编译器默认的参数不支持,可以将小数转化为整数进行计算。
另外,htu21d的i2c支持两种模式,hold master模式支持SCL stretching,更加方便我们主机端读取数据,设备会在数据ADC采集完成之前,控制SCL,不让master继续后续操作,再数据完成采集后,再释放SCL,master才进行后续读取操作。另外一个模式,需要我们自己去轮询读取,指导读取到有ACK的字节,才算htu21d数据准备完毕。但是hold master有一个比较严重的问题,如果SCL stretching期间,htu21d掉电的话,master i2c bus会被占用,导致后续都没办法再继续操作了。
CRC-8
需要确定CRC使用的多项式,多项式的计算公式是依据每一项的指数排序,存在指数的项依次用1填充一个字节的一位,没有的项用0填充。比如下面的多项式:
x^8 + x^5 + x^4 + 1
那么x8 应该填充第8位,x5 应该填充第5位,依次类推,最后的+1,应该是 x0 ,最终得到的数是 100110001
,这时得到一个9位数的值,因为只需要一个字节,因此去掉第8位,得到 00110001
(0x31)。
crc值会有一个默认初始值,htu21d要求的crc初始值是0。将crc值依次与数据的每个字节异或得到新的crc。在异或了一个字节得到新crc后,需要将当时的crc值向左移位,当最高位为1时,则将当前移位后的值与上述计算的多项式得到的值进行异或后赋给crc,如果最高位不为1,则将移位后的值赋给crc。一个字节8位都移位完成后,crc又与下一个字节异或,继续上面的操作,直到最后一个字节完成,就得到了最终的crc值。
/* x^8 + x^5 + x^4 + 1: 100110001 (00110001 -> 0x31) */
static u8 crc8_calcu(u8 *data, size_t len)
{
/* init value for htu21d: 0x0 */
u8 crc = 0x0;
int i, j;
for (i = 0; i < len; i++) {
crc ^= data[i];
for (j = 0; j < 8; j++) {
crc = (crc & 0x80) ? ((crc << 1) ^ 0x31) : (crc << 1);
}
}
return crc;
}
设备属性
在提供温湿度接口时,我们这里使用最简单的sysfs接口提供温湿度属性值,或者也可以使用字符设备驱动来完成。。一般温湿度传感器是使用iio子系统或者input子系统来提供用户接口。后续做到相应章节再实现iio和input子系统方式的接口。
总线,设备,驱动,类等内核对象都是拥有属性接口的。可以直接属性定义的宏,定义内核某个对象的属性借口,实现属性的读写功能。读属性为show,写属性为store。设备的属性接口使用以下宏进行定义:
#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
static DEVICE_ATTR(temperature, 0644, bmp280_temperature_show,
bmp280_temperature_store);
定义属性的名字,sysfs文件节点的权限,读写方法。当属性只读或者只写时,建议不要使用这个接口,可能回到只系统报oops。比如我不支持写,那么我给store传参会是NULL。insmod的时候回报一堆错误警告及堆栈信息出来。
定义完成后,使用 device_create_file
函数创建文件节点。
只读或者只写使用以下宏:
#define DEVICE_ATTR_RO(_name) \
struct device_attribute dev_attr_##_name = __ATTR_RO(_name)
#define DEVICE_ATTR_WO(_name) \
struct device_attribute dev_attr_##_name = __ATTR_WO(_name)
在用户空间,将生成 temperature 和 humidity 两个属性节点,权限只读。可以使用cat读取设备节点,将分别读取到传感器的温度数据和湿度数据。如果支持写权限的话,可以使用echo命令进行写入。这里两个节点是以字符串的格式进行输出的,如果属性节点是输出二进制数据的,可以使用xxd命令进行读取。
设备树配置
&i2c4 {
status = "okay";
htu21d: htu21d@40 {
status = "okay";
compatible = "se,htu21d";
reg = <0x40>;
};
};
这里使用驱动和设备的匹配方式使用设备树风格的方式,会和驱动中 of_device_id
列表中的 compatible 字段进行匹配。可以尝试修改设备树中的compatible属性,设备树风格匹配将失败,继而使用 i2c_device_id 方式进行匹配,这个时候 i2c_device_id 匹配将匹配设备节点的名称,即 "htu21d"
。
驱动代码
#include "asm-generic/errno-base.h"
#include "linux/device.h"
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/delay.h>
struct htu21d_dev {
struct i2c_client *client;
s32 temperature;
u32 humidity;
};
/* x^8 + x^5 + x^4 + 1: 100110001 (00110001 -> 0x31) */
static u8 crc8_calcu(u8 *data, size_t len)
{
/* init value for htu21d: 0x0 */
u8 crc = 0x0;
int i, j;
for (i = 0; i < len; i++) {
crc ^= data[i];
for (j = 0; j < 8; j++) {
crc = (crc & 0x80) ? ((crc << 1) ^ 0x31) : (crc << 1);
}
}
return crc;
}
static int htu21d_i2c_read_bytes(struct i2c_client *client, u16 chip, u8 reg,
u8 *data, u8 size)
{
struct i2c_msg msg[] = {
{
.flags = 0,
.addr = client->addr,
.buf = ®,
.len = 1,
},
{
.flags = I2C_M_RD | I2C_M_STOP,
.addr = client->addr,
.buf = data,
.len = size,
},
};
if (i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg)) != ARRAY_SIZE(msg)) {
dev_err(&client->dev, "i2c read error, chip=0x%x, reg=0x%x\n",
client->addr, reg);
return -EREMOTEIO;
}
return 0;
}
static int htu21d_reset(struct htu21d_dev *htu21d)
{
struct i2c_client *client = htu21d->client;
u8 val = 0xFE;
struct i2c_msg msg[] = {
{
.flags = I2C_M_STOP,
.addr = client->addr,
.buf = &val,
.len = 1,
},
};
int ret;
ret = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg));
if (ret != ARRAY_SIZE(msg)) {
dev_err(&client->dev, "i2c write reg error, chip=0x%02x, reg=0x%02x, ret=%d\n",
client->addr, val, ret);
return -EREMOTEIO;
}
/* soft reset take at least 15ms */
msleep(15);
return 0;
}
static int htu21d_config(struct htu21d_dev *htu21d)
{
struct i2c_client *client = htu21d->client;
u8 data[] = {0xE6, 0x01};
struct i2c_msg msg[] = {
{
.flags = I2C_M_STOP,
.addr = client->addr,
.buf = (unsigned char *)&data,
.len = ARRAY_SIZE(data),
},
};
int ret = 0;
ret = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg));
if (ret != ARRAY_SIZE(msg)) {
dev_err(&client->dev, "i2c write reg error, chip=0x%02x, reg=0x%02x, ret=%d\n",
client->addr, data[0], ret);
return -EREMOTEIO;
}
return 0;
}
/* actual value T = temp / 100 */
static int htu21d_read_temperature(struct htu21d_dev *htu21d, s32 *temp)
{
u8 data[3] = {0};
struct i2c_client *client = htu21d->client;
int ret = 0;
u32 s_temp;
u8 crc;
*temp = 0;
/* hold master */
ret = htu21d_i2c_read_bytes(client, client->addr, 0xE3, data, ARRAY_SIZE(data));
if (ret) {
return ret;
}
crc = crc8_calcu(data, 2);
if (crc != data[2]) {
dev_err(&client->dev, "temp data crc error\n");
return -EINVAL;
}
s_temp = (data[0] << 8) | data[1];
*temp = -4685 + 17572 * s_temp / (1 << 16);
return 0;
}
/* actual value H = hum / 100 */
static int htu21d_read_humidity(struct htu21d_dev *htu21d, u32 *hum)
{
u8 data[3] = {0};
struct i2c_client *client = htu21d->client;
int ret = 0;
u32 s_rh;
u8 crc;
*hum = 0;
/* hold master */
ret = htu21d_i2c_read_bytes(client, client->addr, 0xE5, data, ARRAY_SIZE(data));
if (ret) {
return ret;
}
crc = crc8_calcu(data, 2);
if (crc != data[2]) {
dev_err(&client->dev, "humi data crc error\n");
return -EINVAL;
}
s_rh = (data[0] << 8)| data[1];
*hum = -600 + 12500 * s_rh / (1 << 16);
return 0;
}
static ssize_t temperature_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
struct i2c_client *client = container_of(dev, struct i2c_client, dev);
struct htu21d_dev *htu21d = i2c_get_clientdata(client);
htu21d_read_temperature(htu21d, &htu21d->temperature);
return sprintf(buf, "%d\n", htu21d->temperature);
}
static DEVICE_ATTR_RO(temperature);
static ssize_t humidity_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
struct i2c_client *client = container_of(dev, struct i2c_client, dev);
struct htu21d_dev *htu21d = i2c_get_clientdata(client);
htu21d_read_humidity(htu21d, &htu21d->humidity);
return sprintf(buf, "%u\n", htu21d->humidity);
}
static DEVICE_ATTR_RO(humidity);
static int htu21d_init(struct htu21d_dev *htu21d)
{
int ret = 0;
ret = htu21d_reset(htu21d);
if (ret) {
return ret;
}
ret = htu21d_config(htu21d);
if (ret) {
return ret;
}
return 0;
}
static int htu21d_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct htu21d_dev *htu21d;
int ret = 0;
if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
dev_err(&client->dev, "i2c_check_functionality not support I2C_FUNC_I2C\n");
return -EIO;
}
htu21d = devm_kzalloc(&client->dev, sizeof(*htu21d), GFP_KERNEL);
if (IS_ERR(htu21d)) {
dev_err(&client->dev, "kzalloc error: %ld\n", -PTR_ERR(htu21d));
return PTR_ERR(htu21d);
}
i2c_set_clientdata(client, htu21d);
htu21d->client = client;
ret = htu21d_init(htu21d);
if (ret) {
dev_err(&client->dev, "htu21d init error: %d\n", -ret);
return ret;
}
device_create_file(&client->dev, &dev_attr_temperature);
device_create_file(&client->dev, &dev_attr_humidity);
dev_info(&client->dev, "htu21d probe success\n");
return 0;
}
static int htu21d_remove(struct i2c_client *client)
{
i2c_set_clientdata(client, NULL);
device_remove_file(&client->dev, &dev_attr_temperature);
device_remove_file(&client->dev, &dev_attr_humidity);
dev_info(&client->dev, "htu21d remove\n");
return 0;
}
static struct i2c_device_id htu21d_id_table[] = {
{"htu21d", 0},
{ },
};
MODULE_DEVICE_TABLE(i2c, htu21d_id_table);
static struct of_device_id htu21d_of_match[] = {
{.compatible = "se,htu21d"},
{ },
};
MODULE_DEVICE_TABLE(of, htu21d_of_match);
static struct i2c_driver htu21d_driver = {
.probe = htu21d_probe,
.remove = htu21d_remove,
.driver = {
.owner = THIS_MODULE,
.name = "htu21d_drv",
.of_match_table = of_match_ptr(htu21d_of_match),
},
.id_table = htu21d_id_table,
};
module_i2c_driver(htu21d_driver);
MODULE_AUTHOR("duapple <duapple2@gmail.com>");
MODULE_LICENSE("GPL");