开始做这个驱动的时候我才发现...我对设备树的了解真的是皮毛....调这玩意调了我两天多,真是一把辛酸泪,虽然关键代码就这么几行....
首先这颗传感器我不多说,就是一个普通的I2C总线的温湿度传感器.主要来说说它跟MCU的交互过程,这应该是所有驱动都最在意的地方.
传感器的规格书里有一页简单的伪代码.
AAC·2021-03-12·4567 次阅读
开始做这个驱动的时候我才发现...我对设备树的了解真的是皮毛....调这玩意调了我两天多,真是一把辛酸泪,虽然关键代码就这么几行....
首先这颗传感器我不多说,就是一个普通的I2C总线的温湿度传感器.主要来说说它跟MCU的交互过程,这应该是所有驱动都最在意的地方.
传感器的规格书里有一页简单的伪代码.
从伪代码里可以看出来传感器的I2C设备地址是0x44,以及读取的方式是先写一个0xFD的命令到传感器,然后等待一段时间,再读六个字节,就是温湿度数据,然后做一定的转换后就能得出温湿度值.
还有一些其他的命令,但是不太重要,在这边一并贴出来.
然后是跟这个驱动本身功能实现有关的一些zephyr的API.
毫秒级延时,这个最简单,类似于FreeRTOS里的vTaskDelay(pdMS_TO_TICKS())的感觉,让当前线程等待指定ms的时间,期间RTOS会有调度,CPU会去执行其他线程.
/**
* @brief Put the current thread to sleep.
*
* This routine puts the current thread to sleep for @a duration milliseconds.
*
* @param ms Number of milliseconds to sleep.
*
* @return Zero if the requested time has elapsed or the number of milliseconds
* left to sleep, if thread was woken up by \ref k_wakeup call.
*/
static inline int32_t k_msleep(int32_t ms)
{
return k_sleep(Z_TIMEOUT_MS(ms));
}
另外还有一个是微秒级的延时.
void k_busy_wait(uint32_t usec_to_wait)
注意,这个函数不会触发任务调度,可以理解为是一种for(){nop();}的感觉.
还有就是I2C的API了.
I2C写数据的函数是这个:
/*
* Derived i2c APIs -- all implemented in terms of i2c_transfer()
*/
/**
* @brief Write a set amount of data to an I2C device.
*
* This routine writes a set amount of data synchronously.
*
* @param dev Pointer to the device structure for the driver instance.
* @param buf Memory pool from which the data is transferred.
* @param num_bytes Number of bytes to write.
* @param addr Address to the target I2C device for writing.
*
* @retval 0 If successful.
* @retval -EIO General input / output error.
*/
static inline int i2c_write(const struct device *dev, const uint8_t *buf,
uint32_t num_bytes, uint16_t addr)
这个是读的函数:
/**
* @brief Read a set amount of data from an I2C device.
*
* This routine reads a set amount of data synchronously.
*
* @param dev Pointer to the device structure for the driver instance.
* @param buf Memory pool that stores the retrieved data.
* @param num_bytes Number of bytes to read.
* @param addr Address of the I2C device being read.
*
* @retval 0 If successful.
* @retval -EIO General input / output error.
*/
static inline int i2c_read(const struct device *dev, uint8_t *buf,
uint32_t num_bytes, uint16_t addr)
另外还有一种写读的方式,也经常用到,就是I2C发了一个开始信号,然后写一个字节或者两个字节,一般都是寄存器地址或者命令字节之类的,然后不发停止信号,直接再发一次开始信号,然后读xx字节,然后才发停止信号,这种时候适用于这个函数:
/**
* @brief Write then read data from an I2C device.
*
* This supports the common operation "this is what I want", "now give
* it to me" transaction pair through a combined write-then-read bus
* transaction.
*
* @param dev Pointer to the device structure for the driver instance
* @param addr Address of the I2C device
* @param write_buf Pointer to the data to be written
* @param num_write Number of bytes to write
* @param read_buf Pointer to storage for read data
* @param num_read Number of bytes to read
*
* @retval 0 if successful
* @retval negative on error.
*/
static inline int i2c_write_read(const struct device *dev, uint16_t addr,
const void *write_buf, size_t num_write,
void *read_buf, size_t num_read)
其实这些函数都是inline的,声明下面就是他们的实现过程,看上去应该是类似于将每次调用打包成一次事务结构体,然后由i2c_transfer()去做处理(那个写读的是打包了两个事务,一次写一次读,写的那个事务专门定义了没停止信号).
开始写传感器的驱动,首先是写命令的函数:
/*******************************************************************************
* Function Name : shtv4_write_command
* Description : 写命令
* Input : const struct device* dev
* Input : uint8_t cmd
* Output : None
* Return : qp_status_t
*******************************************************************************/
qp_status_t shtv4_write_command(const struct device* dev, uint8_t cmd)
{
uint8_t tx_buf = cmd;
return (i2c_write(shtv4_i2c_device(dev), &tx_buf, 1,shtv4_i2c_address(dev))<0?QP_ERROR:QP_OK);
}
然后是读温湿度数据的函数:
/*******************************************************************************
* Function Name : temp_humi_read_data
* Description : 读取温湿度数据
* Input : uint8_t * ucDataBuffer
* Input : uint8_t uclen
* Output : None
* Return : qp_status_t
*******************************************************************************/
qp_status_t temp_humi_read_data(const struct device* dev,uint8_t* ucDataBuffer, uint8_t uclen)
{
struct shtv4_data* data = dev->data;
const struct device* i2c = shtv4_i2c_device(dev);
uint8_t address = shtv4_i2c_address(dev);
if (i2c_read(i2c, ucDataBuffer, uclen, address) < 0)
{
return QP_ERROR;
}
return QP_OK;
}
计算实际的温度湿度值的这些我就不贴了,上面的代码里有一些很奇怪的结构体还有一些很奇怪的函数调用,比如shtv4_i2c_device()与shtv4_i2c_address().这两个我在下面会说.
驱动与设备树部分可以参考官网文档的这部分(其实是这部分附近的内容,内容很多,还有点分散): https://docs.zephyrproject.org/latest/reference/drivers/index.html
然后,根据这份文档(https://docs.zephyrproject.org/latest/guides/dts/howtos.html)里的流程来创建设备驱动对外的接口,在这里,我选择了他上面说的选项2(Option 2),使用节点标签来创建设备(create devices using node labels).
先改一下.overlay文件:
&uart0 {
tx-pin = < 20 >;
rx-pin = < 22 >;
};
&i2c0 { /* SDA P0.26, SCL P0.27, ALERT P1.10 */
sda-pin = < 0x0d >;
scl-pin = < 0x0f >;
temp_humi_sensor0:shtv4@44 {
status = "okay";
compatible = "sensirion,shtv4";
reg = <0x44>;
label = "SHTV4";
};
};
另外再改个prj.conf文件,增加两个定义:
CONFIG_I2C=y
CONFIG_GPIO=y
重新cmake一下,生成的zephyr.dts的I2C0部分就是这样了.
i2c0: arduino_i2c: i2c@40003000 {
#address-cells = < 0x1 >;
#size-cells = < 0x0 >;
reg = < 0x40003000 0x1000 >;
clock-frequency = < 0x186a0 >;
interrupts = < 0x3 0x1 >;
status = "okay";
label = "I2C_0";
compatible = "nordic,nrf-twi";
sda-pin = < 0x1a >;
scl-pin = < 0x1b >;
sht3xd@44 {
compatible = "sensirion,sht3xd";
reg = < 0x44 >;
label = "SHT3XD";
alert-gpios = < &gpio1 0xa 0x0 >;
};
};
声明几个自定义的结构体类型,待会儿会用到.
struct shtv4_config
{
char* bus_name;
uint8_t base_address;
};
struct shtv4_data
{
const struct device* dev;
const struct device* bus;
uint16_t t_sample;
uint16_t rh_sample;
};
然后就是一堆定义跟一堆宏了
static const struct sensor_driver_api shtv4_driver_api = {
.sample_fetch = shtv4_sample_fetch,
.channel_get = shtv4_channel_get,
};
#define CREATE_SHTV4_DEVICE(idx) \
struct shtv4_data shtv4_driver_data##idx; \
static const struct shtv4_config shtv4_cfg##idx = { \
.bus_name = DT_LABEL(DT_PARENT(DT_NODELABEL(temp_humi_sensor##idx))),\
.base_address = DT_REG_ADDR_BY_IDX(DT_NODELABEL(temp_humi_sensor##idx),0),\
};\
DEVICE_DT_DEFINE(DT_NODELABEL(temp_humi_sensor ## idx),\
shtv4_init,\
device_pm_control_nop,\
&shtv4_driver_data##idx,\
&shtv4_cfg##idx,\
POST_KERNEL,\
CONFIG_SENSOR_INIT_PRIORITY,\
&shtv4_driver_api);
CREATE_SHTV4_DEVICE(0)
一个一个来,先是shtv4_driver_api结构体,这个结构体里有两个函数的指针,是这个驱动对外的两个唯二的功能,函数内的实现细节待会儿再谈:
shtv4_sample_fetch:触发温湿度传感器转换并从传感器中读取温湿度数据
shtv4_channel_get:以及获取传感器数据(从MCU RAM里(之前从传感器读取的数据的缓存))
然后就是比较关键的CREATE_SHTV4_DEVICE宏,这个宏会当成一个函数用,所以他的内容比较多,里面首先是定义了一些结构体变量,有shtv4_data跟shtv4_config的.
然后给shtv4_config里的bus_name成员赋值,这个用于告诉驱动这个传感器是被挂在哪个I2C控制器下的,是I2C0还是I2C1,或者有其他更多的I2C控制器.
还给base_address赋值了,这个可能比较好理解,是I2C从设备(传感器)的I2C地址,在这里SHT40的从地址是0x44,这些数据都是从设备树里拿到的.
然后就是DEVICE_DT_DEFINE定义并初始化设备了.他的参数大部分都是上面出现过的内容以及一些固定的宏或者枚举,但是有一个是shtv4_init函数,这个是上面没有的,他的内容是这样的:
static int shtv4_init(const struct device* dev)
{
struct shtv4_data* data = dev->data;
const struct shtv4_config* cfg = dev->config;
const struct device* i2c = device_get_binding(cfg->bus_name);
if (i2c == NULL)
{
LOG_DBG("Failed to get pointer to %s device!",cfg->bus_name);
return -EINVAL;
}
data->bus = i2c;
LOG_DBG("get pointer to %s device!",cfg->bus_name);
if (!cfg->base_address)
{
LOG_DBG("No I2C address");
return -EINVAL;
}
LOG_DBG("I2C address:%d",cfg->base_address);
data->dev = dev;
/* clear status register */
if (shtv4_write_command(dev, SHTV4_SOFT_RESET) < 0)
{
LOG_DBG("Failed to clear status register!");
return -EIO;
}
k_msleep(1);
return 0;
}
可以看到,很简单的一个初始化的函数.判断一些指针是否为空值,以及软件复位一下传感器.
另外之前一直有出现,但是不知道实现的两个函数shtv4_i2c_address()跟shtv4_i2c_device(),其实他们是这样的:
static uint8_t shtv4_i2c_address(const struct device* dev)
{
const struct shtv4_config* dcp = dev->config;
return dcp->base_address;
}
static const struct device* shtv4_i2c_device(const struct device* dev)
{
const struct shtv4_data* ddp = dev->data;
return ddp->bus;
}
这样看就简单多了,其实就是获取CREATE_SHTV4_DEVICE里定义的那个shtv4_config里的base_address跟...呃....dev->data暂时没细研究.
然后是shtv4_sample_fetch跟shtv4_channel_get的实现:
/*******************************************************************************
* Function Name : shtv4_sample_fetch
* Description : 从传感器获取数据
* Input : const struct device* dev
* Input : enum sensor_channel chan
* Output : None
* Return : int
*******************************************************************************/
static int shtv4_sample_fetch(const struct device* dev, enum sensor_channel chan)
{
qp_status_t ucRet = QP_OK;
uint8_t ucReadData[6];
struct shtv4_data* data = dev->data;
const struct device* i2c = shtv4_i2c_device(dev);
uint8_t address = shtv4_i2c_address(dev);
__ASSERT_NO_MSG(chan == SENSOR_CHAN_ALL);
ucRet = shtv4_write_command(dev,SHTV4_GET_TEMP_HUMI_HIGH_PRECISION);
// 等待10ms
k_msleep(10);
//获取温湿度数据
if (ucRet == QP_OK)ucRet = shtv4_read_temp_humi_data(dev,ucReadData);
if (ucRet == QP_OK)
{
return shtv4_temp_humi_data_convert(ucReadData, &(data->t_sample), &(data->rh_sample));
}
else
{
return -EIO;
}
return 0;
}
/*******************************************************************************
* Function Name : shtv4_channel_get
* Description : 获取数据
* Input : const struct device* dev
* Input : enum sensor_channel chan
* Output : struct sensor_value* val
* Return : int
*******************************************************************************/
static int shtv4_channel_get(const struct device* dev,enum sensor_channel chan,struct sensor_value* val)
{
const struct shtv4_data* data = dev->data;
uint64_t tmp;
if (chan == SENSOR_CHAN_AMBIENT_TEMP)
{
val->val1 = data->t_sample/100;
val->val2 = ((data->t_sample % 100)/10)*100000;
}
else if (chan == SENSOR_CHAN_HUMIDITY)
{
val->val1 = data->rh_sample / 100;
val->val2 = ((data->rh_sample % 100) / 10) * 100000;
}
else
{
return -ENOTSUP;
}
return 0;
}
关于shtv4_channel_get返回数据sensor_value* val,可以看下sensor_value的结构体的格式,从普通温湿度需要稍微转换一下才行.
然后在main()函数里用:
先获取一下驱动的结构体指针:
const struct device* dev = device_get_binding(DT_LABEL(DT_NODELABEL(temp_humi_sensor0)));
然后在while循环里这样读取温湿度:
for (;;) {
struct sensor_value temp, hum;
int rc;
rc = sensor_sample_fetch(dev);
if(rc!=0)LOG_ERR("sensor_sample_fetch error\n");
if (rc == 0) {
rc = sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP,
&temp);
}
if (rc == 0) {
rc = sensor_channel_get(dev, SENSOR_CHAN_HUMIDITY,
&hum);
}
if (rc != 0) {
printf("SHTV4: failed: %d\n", rc);
break;
}
printf("SHTV4: %.2f Cel ; %0.2f %%RH\n",
sensor_value_to_double(&temp),
sensor_value_to_double(&hum));
k_sleep(K_MSEC(2000));
}
然后编译一下,烧录看效果:
相关教程以及资料可以参考这边的官方资料:https://devzone.nordicsemi.com/nordic/nrf-connect-sdk-guides/b/getting-started/posts/nrf-connect-sdk-tutorial---part-3-ncs-v1-4-0
Comments | NOTHING