I2C(Inter Integrated Circuit)总线是 PHILIPS 公司开发的一种半双工、双向二线制同步串行总线。I2C 总线传输数据时只需两根信号线,一根是双向数据线 SDA(serial data),另一根是双向时钟线 SCL(serial clock)。SPI 总线有两根线分别用于主从设备之间接收数据和发送数据,而 I2C 总线只使用一根线进行数据收发。
I2C 和 SPI 一样以主从的方式工作,不同于 SPI 一主多从的结构,它允许同时有多个主设备存在,每个连接到总线上的器件都有唯一的地址,主设备启动数据传输并产生时钟信号,从设备被主设备寻址(SPI通过CS片选引脚选择目标从设备,IIC通过发送从设备地址以寻址方式选择目标从设备),同一时刻只允许有一个主设备。如下图所示:
SDA 线上的数据必须在时钟的高电平周期保持稳定,数据线的高或低电平状态只有在 SCL 线的时钟信号是低电平时才能改变。换言之,SCL为高电平时表示有效数据,SDA为高电平表示“1”,低电平表示“0”;SCL为低电平时表示无效数据,此时SDA会进行电平切换,为下次数据表示做准备。下图为数据有效性的时序图:
I2C 总线主要的数据传输格式如下图所示:
当总线空闲时,SDA 和 SCL 都处于高电平状态,当主机要和某个从机通讯时,会先发送一个开始条件,然后发送从机地址和读写控制位,接下来传输数据(主机发送或者接收数据),数据传输结束时主机会发送停止条件。传输的每个字节为8位,高位在前,低位在后。数据传输过程中的不同名词详解如下所示:
在正点原子的教程中说,STM32的硬件IIC设计比较复杂,而且稳定性不佳(貌似是ST为了规避飞利浦IIC的版权问题),所以在CPU资源不紧张的情况下,很多人一般会选择GPIO模拟I2C。硬件IIC与软件模拟IIC有何区别呢?
STM32平台由于软件模拟IIC比较常用,且方便移植,RT-Thread的IIC设备驱动也是使用的软件模拟方式实现的,所以下面就不介绍硬件IIC的功能框图、IIC固件库等内容了(自然也不需要CubeMX配置IIC外设了),下面开始介绍RT-Thread IIC驱动框架的实现。
介绍IIC设备对象管理前,再展示下RT-Thread I / O设备模型框架,按照模型框架一层层解析:
先看IIC总线在驱动框架层是如何描述的:
// rt-thread-4.0.1\components\drivers\include\drivers\i2c.h
/*for i2c bus driver*/
struct rt_i2c_bus_device
{
struct rt_device parent;
const struct rt_i2c_bus_device_ops *ops;
rt_uint16_t flags;
rt_uint16_t addr;
struct rt_mutex lock;
rt_uint32_t timeout;
rt_uint32_t retries;
void *priv;
};
struct rt_i2c_bus_device_ops
{
rt_size_t (*master_xfer)(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num);
rt_size_t (*slave_xfer)(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num);
rt_err_t (*i2c_bus_control)(struct rt_i2c_bus_device *bus,
rt_uint32_t,
rt_uint32_t);
};
struct rt_i2c_msg
{
rt_uint16_t addr;
rt_uint16_t flags;
rt_uint16_t len;
rt_uint8_t *buf;
};
// flags
#define RT_I2C_WR 0x0000
#define RT_I2C_RD (1u << 0)
#define RT_I2C_ADDR_10BIT (1u << 2) /* this is a ten bit chip address */
#define RT_I2C_NO_START (1u << 4)
#define RT_I2C_IGNORE_NACK (1u << 5)
#define RT_I2C_NO_READ_ACK (1u << 6) /* when I2C reading, we do not ACK */
IIC总线访问IIC设备的操作函数主要有master_xfer、slave_xfer与i2c_bus_control三种,这几个访问函数由下面的IIC设备驱动层实现,这也是IIC协议软件模拟实现的重点。
IIC总线传输数据按照IIC协议数据帧格式,封装成结构体rt_i2c_msg,该结构体成员包含了IIC协议数据帧中从机地址、各标志位、传输数据的起始地址及长度等内容。
I/O设备管理层要想访问某设备,需要在下面的设备驱动层创建设备实例,并将该设备注册到I/O设备管理层,下面先看看IIC总线的创建与注册过程:
// rt-thread-4.0.1\components\drivers\i2c\i2c_core.c
rt_err_t rt_i2c_bus_device_register(struct rt_i2c_bus_device *bus,
const char *bus_name)
{
rt_err_t res = RT_EOK;
rt_mutex_init(&bus->lock, "i2c_bus_lock", RT_IPC_FLAG_FIFO);
if (bus->timeout == 0) bus->timeout = RT_TICK_PER_SECOND;
res = rt_i2c_bus_device_device_init(bus, bus_name);
i2c_dbg("I2C bus [%s] registered\n", bus_name);
return res;
}
// rt-thread-4.0.1\components\drivers\i2c\i2c_dev.c
rt_err_t rt_i2c_bus_device_device_init(struct rt_i2c_bus_device *bus,
const char *name)
{
struct rt_device *device;
RT_ASSERT(bus != RT_NULL);
device = &bus->parent;
device->user_data = bus;
/* set device type */
device->type = RT_Device_Class_I2CBUS;
/* initialize device interface */
#ifdef RT_USING_DEVICE_OPS
device->ops = &i2c_ops;
#else
device->init = RT_NULL;
device->open = RT_NULL;
device->close = RT_NULL;
device->read = i2c_bus_device_read;
device->write = i2c_bus_device_write;
device->control = i2c_bus_device_control;
#endif
/* register to device manager */
rt_device_register(device, name, RT_DEVICE_FLAG_RDWR);
return RT_EOK;
}
#ifdef RT_USING_DEVICE_OPS
const static struct rt_device_ops i2c_ops =
{
RT_NULL,
RT_NULL,
RT_NULL,
i2c_bus_device_read,
i2c_bus_device_write,
i2c_bus_device_control
};
#endif
static rt_size_t i2c_bus_device_read(rt_device_t dev,
rt_off_t pos,
void *buffer,
rt_size_t count)
{
......
struct rt_i2c_bus_device *bus = (struct rt_i2c_bus_device *)dev->user_data;
......
return rt_i2c_master_recv(bus, addr, flags, buffer, count);
}
static rt_size_t i2c_bus_device_write(rt_device_t dev,
rt_off_t pos,
const void *buffer,
rt_size_t count)
{
......
struct rt_i2c_bus_device *bus = (struct rt_i2c_bus_device *)dev->user_data;
......
return rt_i2c_master_send(bus, addr, flags, buffer, count);
}
static rt_err_t i2c_bus_device_control(rt_device_t dev,
int cmd,
void *args)
{
rt_err_t ret;
struct rt_i2c_priv_data *priv_data;
struct rt_i2c_bus_device *bus = (struct rt_i2c_bus_device *)dev->user_data;
RT_ASSERT(bus != RT_NULL);
switch (cmd)
{
/* set 10-bit addr mode */
case RT_I2C_DEV_CTRL_10BIT:
bus->flags |= RT_I2C_ADDR_10BIT;
break;
case RT_I2C_DEV_CTRL_ADDR:
bus->addr = *(rt_uint16_t *)args;
break;
case RT_I2C_DEV_CTRL_TIMEOUT:
bus->timeout = *(rt_uint32_t *)args;
break;
case RT_I2C_DEV_CTRL_RW:
priv_data = (struct rt_i2c_priv_data *)args;
ret = rt_i2c_transfer(bus, priv_data->msgs, priv_data->number);
if (ret < 0)
return -RT_EIO;
break;
default:
break;
}
return RT_EOK;
}
// rt-thread-4.0.1\components\drivers\include\drivers\i2c_dev.h
struct rt_i2c_priv_data
{
struct rt_i2c_msg *msgs;
rt_size_t number;
};
IIC驱动框架层向上层注册的操作函数集合i2c_ops最终通过调用rt_i2c_master_recv、rt_i2c_master_send与rt_i2c_transfer三个函数实现,IIC设备驱动层将这三个函数开放给用户了,用户除通过I / O设备统一访问接口访问IIC外,还可通过这三个函数直接访问IIC设备,这三个函数的实现代码如下:
// rt-thread-4.0.1\components\drivers\i2c\i2c_core.c
rt_size_t rt_i2c_transfer(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num)
{
rt_size_t ret;
if (bus->ops->master_xfer)
{
rt_mutex_take(&bus->lock, RT_WAITING_FOREVER);
ret = bus->ops->master_xfer(bus, msgs, num);
rt_mutex_release(&bus->lock);
return ret;
}
else
return 0;
}
rt_size_t rt_i2c_master_send(struct rt_i2c_bus_device *bus,
rt_uint16_t addr,
rt_uint16_t flags,
const rt_uint8_t *buf,
rt_uint32_t count)
{
rt_err_t ret;
struct rt_i2c_msg msg;
msg.addr = addr;
msg.flags = flags & RT_I2C_ADDR_10BIT;
msg.len = count;
msg.buf = (rt_uint8_t *)buf;
ret = rt_i2c_transfer(bus, &msg, 1);
return (ret > 0) ? count : ret;
}
rt_size_t rt_i2c_master_recv(struct rt_i2c_bus_device *bus,
rt_uint16_t addr,
rt_uint16_t flags,
rt_uint8_t *buf,
rt_uint32_t count)
{
rt_err_t ret;
struct rt_i2c_msg msg;
RT_ASSERT(bus != RT_NULL);
msg.addr = addr;
msg.flags = flags & RT_I2C_ADDR_10BIT;
msg.flags |= RT_I2C_RD;
msg.len = count;
msg.buf = buf;
ret = rt_i2c_transfer(bus, &msg, 1);
return (ret > 0) ? count : ret;
}
从上面的实现代码可以看出,rt_i2c_master_send与rt_i2c_master_recv最终是通过调用rt_i2c_transfer函数实现的,前两个函数对rt_i2c_transfer函数进行了再封装,不需要用户构造rt_i2c_msg结构体,调用更方便。
函数rt_i2c_transfer的实现最终是通过调用bus->ops->master_xfer函数来实现的,在前面介绍IIC总线设备控制块时介绍过,IIC总线操作函数集合rt_i2c_bus_device_ops需要IIC设备驱动层实现。
RT-Thread IIC设备采用软件模拟方式实现,通过GPIO设备模拟IIC通信协议实际上是通过控制GPIO引脚电平的置位操作实现的,STM32描述模拟IIC设备的数据结构如下:
// libraries\HAL_Drivers\drv_soft_i2c.h
/* stm32 i2c dirver class */
struct stm32_i2c
{
struct rt_i2c_bit_ops ops;
struct rt_i2c_bus_device i2c2_bus;
};
/* stm32 config class */
struct stm32_soft_i2c_config
{
rt_uint8_t scl;
rt_uint8_t sda;
const char *bus_name;
};
// rt-thread-4.0.1\components\drivers\include\drivers\i2c-bit-ops.h
struct rt_i2c_bit_ops
{
void *data; /* private data for lowlevel routines */
void (*set_sda)(void *data, rt_int32_t state);
void (*set_scl)(void *data, rt_int32_t state);
rt_int32_t (*get_sda)(void *data);
rt_int32_t (*get_scl)(void *data);
void (*udelay)(rt_uint32_t us);
rt_uint32_t delay_us; /* scl and sda line delay */
rt_uint32_t timeout; /* in tick */
};
结构体stm32_i2c相当于STM32 I2C设备驱动类,包含前面介绍的IIC总线设备rt_i2c_bus_device与IIC位操作函数集合rt_i2c_bit_ops,后者是软件模拟实现IIC通信协议的基础。
先看IIC设备的初始化与注册过程的实现代码:
// libraries\HAL_Drivers\drv_soft_i2c.c
/* I2C initialization function */
int rt_hw_i2c_init(void)
{
rt_size_t obj_num = sizeof(i2c_obj) / sizeof(struct stm32_i2c);
rt_err_t result;
for (int i = 0; i < obj_num; i++)
{
i2c_obj[i].ops = stm32_bit_ops_default;
i2c_obj[i].ops.data = (void*)&soft_i2c_config[i];
i2c_obj[i].i2c2_bus.priv = &i2c_obj[i].ops;
stm32_i2c_gpio_init(&i2c_obj[i]);
result = rt_i2c_bit_add_bus(&i2c_obj[i].i2c2_bus, soft_i2c_config[i].bus_name);
RT_ASSERT(result == RT_EOK);
stm32_i2c_bus_unlock(&soft_i2c_config[i]);
}
return RT_EOK;
}
INIT_BOARD_EXPORT(rt_hw_i2c_init);
static const struct rt_i2c_bit_ops stm32_bit_ops_default =
{
.data = RT_NULL,
.set_sda = stm32_set_sda,
.set_scl = stm32_set_scl,
.get_sda = stm32_get_sda,
.get_scl = stm32_get_scl,
.udelay = stm32_udelay,
.delay_us = 1,
.timeout = 100
};
static const struct stm32_soft_i2c_config soft_i2c_config[] =
{
#ifdef BSP_USING_I2C1
I2C1_BUS_CONFIG,
#endif
......
};
static struct stm32_i2c i2c_obj[sizeof(soft_i2c_config) / sizeof(soft_i2c_config[0])];
// libraries\HAL_Drivers\drv_soft_i2c.h
#ifdef BSP_USING_I2C1
#define I2C1_BUS_CONFIG \
{ \
.scl = BSP_I2C1_SCL_PIN, \
.sda = BSP_I2C1_SDA_PIN, \
.bus_name = "i2c1", \
}
#endif
......
IIC设备初始化被RT-Thread自动初始化组件INIT_BOARD_EXPORT调用,不需要用户手动调用IIC初始化函数,只需要进行必要的引脚配置即可。
从条件宏BSP_USING_I2C1与宏定义BSP_I2C1_SCL_PIN、BSP_I2C1_SDA_PIN可以看出,软件模拟IIC的GPIO引脚需要用户完成配置,可以在Kconfig文件中增加软件模拟IIC外设的配置项,实现通过menuconfig图形化配置软件模拟IIC外设的效果。
IIC设备初始化函数rt_hw_i2c_init中调用的stm32_i2c_gpio_init、stm32_bit_ops_default、stm32_i2c_bus_unlock都是通过pin设备接口函数实现的,前篇博客详细介绍过pin设备驱动的实现,这里就略去不谈了。这里重点介绍下被调用的rt_i2c_bit_add_bus函数的实现:
// rt-thread-4.0.1\components\drivers\i2c\i2c-bit-ops.c
rt_err_t rt_i2c_bit_add_bus(struct rt_i2c_bus_device *bus,
const char *bus_name)
{
bus->ops = &i2c_bit_bus_ops;
return rt_i2c_bus_device_register(bus, bus_name);
}
static const struct rt_i2c_bus_device_ops i2c_bit_bus_ops =
{
i2c_bit_xfer,
RT_NULL,
RT_NULL
};
static rt_size_t i2c_bit_xfer(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num)
{
struct rt_i2c_msg *msg;
struct rt_i2c_bit_ops *ops = bus->priv;
rt_int32_t i, ret;
rt_uint16_t ignore_nack;
bit_dbg("send start condition\n");
i2c_start(ops);
for (i = 0; i < num; i++)
{
msg = &msgs[i];
ignore_nack = msg->flags & RT_I2C_IGNORE_NACK;
if (!(msg->flags & RT_I2C_NO_START))
{
if (i)
{
i2c_restart(ops);
}
ret = i2c_bit_send_address(bus, msg);
if ((ret != RT_EOK) && !ignore_nack)
{
bit_dbg("receive NACK from device addr 0x%02x msg %d\n",
msgs[i].addr, i);
goto out;
}
}
if (msg->flags & RT_I2C_RD)
{
ret = i2c_recv_bytes(bus, msg);
if (ret >= 1)
bit_dbg("read %d byte%s\n", ret, ret == 1 ? "" : "s");
if (ret < msg->len)
{
if (ret >= 0)
ret = -RT_EIO;
goto out;
}
}
else
{
ret = i2c_send_bytes(bus, msg);
if (ret >= 1)
bit_dbg("write %d byte%s\n", ret, ret == 1 ? "" : "s");
if (ret < msg->len)
{
if (ret >= 0)
ret = -RT_ERROR;
goto out;
}
}
}
ret = i;
out:
bit_dbg("send stop condition\n");
i2c_stop(ops);
return ret;
}
在函数rt_i2c_bit_add_bus中通过调用rt_i2c_bus_device_register实现IIC总线设备的注册,同时也将IIC总线设备操作函数集合i2c_bit_bus_ops注册到上面的IIC设备驱动框架层。
RT-Thread只实现了i2c_bit_xfer这一个操作函数,通过i2c_bit_xfer函数实现代码可以看到其中调用的i2c_start、i2c_restart、i2c_bit_send_address、i2c_recv_bytes、i2c_send_bytes、i2c_stop函数便是软件模拟实现IIC驱动协议的主体。
前面通过pin设备实现的位操作函数集合rt_i2c_bit_ops是实现上述IIC协议函数的基础,IIC驱动协议的实现实际上是按照IIC协议要求的信号时序图设置SDA与SCL引脚电平变换及其间隔时间实现的。
下面先以简单的i2c_start与i2c_stop函数为例,对照其信号产生时序图看其实现代码如下:
// rt-thread-4.0.1\components\drivers\i2c\i2c-bit-ops.c
static void i2c_start(struct rt_i2c_bit_ops *ops)
{
SDA_L(ops);
i2c_delay(ops);
SCL_L(ops);
}
static void i2c_stop(struct rt_i2c_bit_ops *ops)
{
SDA_L(ops);
i2c_delay(ops);
SCL_H(ops);
i2c_delay(ops);
SDA_H(ops);
i2c_delay2(ops);
}
#define SDA_L(ops) SET_SDA(ops, 0)
#define SDA_H(ops) SET_SDA(ops, 1)
#define SCL_L(ops) SET_SCL(ops, 0)
#define SET_SDA(ops, val) ops->set_sda(ops->data, val)
#define SET_SCL(ops, val) ops->set_scl(ops->data, val)
#define GET_SDA(ops) ops->get_sda(ops->data)
#define GET_SCL(ops) ops->get_scl(ops->data)
rt_inline void i2c_delay(struct rt_i2c_bit_ops *ops)
{
ops->udelay((ops->delay_us + 1) >> 1);
}
rt_inline void i2c_delay2(struct rt_i2c_bit_ops *ops)
{
ops->udelay(ops->delay_us);
}
函数i2c_start与i2c_stop的实现完全是按照IIC协议要求的信号产生时序图设置SDA与SCL的电平进行的。
接下来看数据传输函数的实现,地址发送函数i2c_bit_send_address实际也是通过数据传输函数实现的,只是地址包含读写、7位/10位等标志位的设置。消息传输函数i2c_recv_bytes与i2c_send_bytes最终是通过位传输函数i2c_writeb与i2c_readb实现的。
从下面的IIC数据位传输与应答信号产生时序图可以看出,IIC写入数据时只能在SCL为低电平时改变SDA的电平,IIC读取数据时只能在SCL为高电平时读取SDA的电平状态才是有效信号(SDA高电平则表示数据‘1’,SDA低电平则表示数据‘0’),每写入一字节数据需要等待对方的应答/非应答信号,每读取一个字节数据需要向对方发送应答/非应答信号。
在传输一个字节数据后发送端释放SDA控制权,将SDA拉高并交由接收方控制,接收方若希望继续,则给出“应答(ACK)”信号,即SDA为低电平;反之给出“非应答(NACK)”信号,即SDA为高电平。
按照上面IIC数据位传输与应答信号产生的时序图,实现IIC协议中数据传输函数的代码如下:
// rt-thread-4.0.1\components\drivers\i2c\i2c-bit-ops.c
static rt_int32_t i2c_writeb(struct rt_i2c_bus_device *bus, rt_uint8_t data)
{
rt_int32_t i;
rt_uint8_t bit;
struct rt_i2c_bit_ops *ops = bus->priv;
for (i = 7; i >= 0; i--)
{
SCL_L(ops);
bit = (data >> i) & 1;
SET_SDA(ops, bit);
i2c_delay(ops);
if (SCL_H(ops) < 0)
return -RT_ETIMEOUT;
}
SCL_L(ops);
i2c_delay(ops);
return i2c_waitack(ops);
}
static rt_int32_t i2c_readb(struct rt_i2c_bus_device *bus)
{
rt_uint8_t i;
rt_uint8_t data = 0;
struct rt_i2c_bit_ops *ops = bus->priv;
SDA_H(ops);
i2c_delay(ops);
for (i = 0; i < 8; i++)
{
data <<= 1;
if (SCL_H(ops) < 0)
return -RT_ETIMEOUT;
if (GET_SDA(ops))
data |= 1;
SCL_L(ops);
i2c_delay2(ops);
}
return data;
}
rt_inline rt_bool_t i2c_waitack(struct rt_i2c_bit_ops *ops)
{
rt_bool_t ack;
SDA_H(ops);
i2c_delay(ops);
if (SCL_H(ops) < 0)
return -RT_ETIMEOUT;
ack = !GET_SDA(ops); /* ACK : SDA pin is pulled low */
bit_dbg("%s\n", ack ? "ACK" : "NACK");
SCL_L(ops);
return ack;
}
static rt_size_t i2c_send_bytes(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg *msg)
{
rt_int32_t ret;
rt_size_t bytes = 0;
const rt_uint8_t *ptr = msg->buf;
rt_int32_t count = msg->len;
rt_uint16_t ignore_nack = msg->flags & RT_I2C_IGNORE_NACK;
while (count > 0)
{
ret = i2c_writeb(bus, *ptr);
if ((ret > 0) || (ignore_nack && (ret == 0)))
{
count --;
ptr ++;
bytes ++;
}
else if (ret == 0)
{
i2c_dbg("send bytes: NACK.\n");
return 0;
}
else
{
i2c_dbg("send bytes: error %d\n", ret);
return ret;
}
}
return bytes;
}
static rt_err_t i2c_send_ack_or_nack(struct rt_i2c_bus_device *bus, int ack)
{
struct rt_i2c_bit_ops *ops = bus->priv;
if (ack)
SET_SDA(ops, 0);
i2c_delay(ops);
if (SCL_H(ops) < 0)
{
bit_dbg("ACK or NACK timeout\n");
return -RT_ETIMEOUT;
}
SCL_L(ops);
return RT_EOK;
}
static rt_size_t i2c_recv_bytes(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg *msg)
{
rt_int32_t val;
rt_int32_t bytes = 0; /* actual bytes */
rt_uint8_t *ptr = msg->buf;
rt_int32_t count = msg->len;
const rt_uint32_t flags = msg->flags;
while (count > 0)
{
val = i2c_readb(bus);
if (val >= 0)
{
*ptr = val;
bytes ++;
}
else
{
break;
}
ptr ++;
count --;
if (!(flags & RT_I2C_NO_READ_ACK))
{
val = i2c_send_ack_or_nack(bus, count);
if (val < 0)
return val;
}
}
return bytes;
}
AHT10是一款高精度、完全校准、贴片封装的温湿度传感器,包括一个电容式感湿元件和一个高性能CMOS微处理器相连接。AHT10采用标准IIC通信方式,体积小、功耗低、响应快、性价比高。
该传感器输入电压范围为1.8V - 3.3V,测量温度与湿度的量程、精度如下表所示:
AHT10接口定义及其在潘多拉开发板上与STM32L475芯片的连接原理图如下:
从上图可知潘多拉开发板使用PC1与PD6来模拟IIC总线的SDA与SCL引脚和AHT10温湿度传感器进行通信。
AHT10上电后,MCU要通过IIC协议访问AHT10传感器,需要知道AHT10的设备地址及其支持的命令,查询AHT10 datasheet可知,AHT10的IIC设备地址为0x38,其支持的基本命令与返回状态位如下表所示:
MCU通过IIC协议读取AHT10温湿度数据的IIC传输数据帧如下图所示:
通过AHT10触发测量数据并读取温湿度数据的IIC传输数据时序图可以编写AHT10的驱动函数(或访问接口函数),读取出的温湿度数据需要换算成更直观的格式,换算公式如下:
这里既然采用软件模拟IIC,RT-Thread pin设备并不需要在CubeMX中再配置GPIO引脚,因此可以直接跳过CubeMX配置。
软件模拟IIC需要配置模拟SDA与SCL使用的GPIO引脚号,这些可以通过宏定义配置,我们就在Kconfig内配置,然后在menuconfig中使能配置项。在.\board\Kconfig文件内已经有了I2C1配置项如下:
// projects\stm32l475_device_sample\board\Kconfig
......
menuconfig BSP_USING_I2C1
bool "Enable I2C1 BUS (software simulation)"
default n
select RT_USING_I2C
select RT_USING_I2C_BITOPS
select RT_USING_PIN
if BSP_USING_I2C1
config BSP_I2C1_SCL_PIN
int "i2c1 scl pin number"
range 1 176
default 15
config BSP_I2C1_SDA_PIN
int "I2C1 sda pin number"
range 1 176
default 16
endif
......
这个配置项也不需要修改,IIC SDA与SCL使用的GPIO引脚号在menuconfig中配置即可,首先查得PC1与PD6在RT-Thread PIN设备管理中的引脚号(libraries\HAL_Drivers\drv_gpio.c)分别为33和54,也即配置BSP_I2C1_SDA_PIN为33,BSP_I2C1_SCL_PIN为54,在menuconfig中的配置界面如下:
menuconfig保存配置后,模拟IIC的引脚就配置好了,RT-Thread中的I2C1设备就可以使用了(I2C初始化与注册过程被RT-Thread自动初始化组件调用执行,用户只需完成引脚配置与依赖宏使能即可)。
用户要读取AHT10的温湿度数据,还需要按照前面介绍的AHT10 IIC通讯命令进行,RT-Thread正好提供了AHT10的驱动函数,我们直接使用即可,在menuconfig中使能AHT10驱动的配置界面如下:
AHT10软件包支持多次读取输出平均值,这里重点介绍AHT10对IIC设备的访问,所以就不使能average filter了,选择最新版本,保存配置。
在env界面执行pkgs --update从github下载aht10软件包到当前工程,命令执行结果如下:
AHT10软件包下载到当前工程后,我们先看看其是怎么实现的,首先看AHT10设备控制块:
// projects\stm32l475_device_sample\packages\aht10-latest\aht10.h
struct aht10_device
{
struct rt_i2c_bus_device *i2c;
#ifdef AHT10_USING_SOFT_FILTER
filter_data_t temp_filter;
filter_data_t humi_filter;
rt_thread_t thread;
rt_uint32_t period; //sample period
#endif /* AHT10_USING_SOFT_FILTER */
rt_mutex_t lock;
};
typedef struct aht10_device *aht10_device_t;
由于我们没有使能AHT10_USING_SOFT_FILTER,先忽略与平均值过滤器相关的成员,结构体aht10_device只有两个成员:I2C总线设备句柄aht10_device.i2c与互斥锁aht10_device.lock。
接着看AHT10设备的初始化过程:
// projects\stm32l475_device_sample\packages\aht10-latest\aht10.c
/**
* This function initializes aht10 registered device driver
*
* @param dev the name of aht10 device
*
* @return the aht10 device.
*/
aht10_device_t aht10_init(const char *i2c_bus_name)
{
aht10_device_t dev;
RT_ASSERT(i2c_bus_name);
dev = rt_calloc(1, sizeof(struct aht10_device));
if (dev == RT_NULL)
{
LOG_E("Can't allocate memory for aht10 device on '%s' ", i2c_bus_name);
return RT_NULL;
}
dev->i2c = rt_i2c_bus_device_find(i2c_bus_name);
if (dev->i2c == RT_NULL)
{
LOG_E("Can't find aht10 device on '%s' ", i2c_bus_name);
rt_free(dev);
return RT_NULL;
}
dev->lock = rt_mutex_create("mutex_aht10", RT_IPC_FLAG_FIFO);
if (dev->lock == RT_NULL)
{
LOG_E("Can't create mutex for aht10 device on '%s' ", i2c_bus_name);
rt_free(dev);
return RT_NULL;
}
#ifdef AHT10_USING_SOFT_FILTER
......
#endif /* AHT10_USING_SOFT_FILTER */
sensor_init(dev);
return dev;
}
static rt_err_t sensor_init(aht10_device_t dev)
{
rt_uint8_t temp[2] = {0, 0};
write_reg(dev->i2c, AHT10_NORMAL_CMD, temp);
rt_thread_delay(rt_tick_from_millisecond(500)); //at least 300 ms
temp[0] = 0x08;
temp[1] = 0x00;
write_reg(dev->i2c, AHT10_CALIBRATION_CMD, temp); //go into calibration
rt_thread_delay(rt_tick_from_millisecond(450)); //at least 300 ms
return RT_EOK;
}
#define AHT10_ADDR 0x38 //connect GND
#define AHT10_CALIBRATION_CMD 0xE1 //calibration cmd for measuring
#define AHT10_NORMAL_CMD 0xA8 //normal cmd
#define AHT10_GET_DATA 0xAC //get data cmd
我们在使用AHT10前需要先手动调用aht10_init函数完成AHT10设备的初始化,其初始化过程实际上就是通过IIC总线向AHT10模块发送初始化命令0xE1,这在前面AHT10通讯命令中介绍过。
继续看读取AHT10温湿度数据的接口函数实现过程:
// projects\stm32l475_device_sample\packages\aht10-latest\aht10.c
/**
* This function reads temperature by aht10 sensor measurement
*
* @param dev the pointer of device driver structure
*
* @return the relative temperature converted to float data.
*/
float aht10_read_temperature(aht10_device_t dev)
{
#ifdef AHT10_USING_SOFT_FILTER
average_measurement(dev, &dev->temp_filter);
return dev->temp_filter.average;
#else
return read_hw_temperature(dev);
#endif /* AHT10_USING_SOFT_FILTER */
}
/**
* This function reads relative humidity by aht10 sensor measurement
*
* @param dev the pointer of device driver structure
*
* @return the relative humidity converted to float data.
*/
float aht10_read_humidity(aht10_device_t dev)
{
#ifdef AHT10_USING_SOFT_FILTER
average_measurement(dev, &dev->humi_filter);
return dev->humi_filter.average;
#else
return read_hw_humidity(dev);
#endif /* AHT10_USING_SOFT_FILTER */
}
static float read_hw_temperature(aht10_device_t dev)
{
rt_uint8_t temp[6];
float cur_temp = -50.0; //The data is error with missing measurement.
rt_err_t result;
RT_ASSERT(dev);
result = rt_mutex_take(dev->lock, RT_WAITING_FOREVER);
if (result == RT_EOK)
{
rt_uint8_t cmd[2] = {0, 0};
write_reg(dev->i2c, AHT10_GET_DATA, cmd); // sample data cmd
result = calibration_enabled(dev);
if (result != RT_EOK)
{
rt_thread_mdelay(1500);
sensor_init(dev); // reset sensor
LOG_E("The aht10 is under an abnormal status. Please try again");
}
else
{
read_regs(dev->i2c, 6, temp); // get data
/*sensor temperature converse to reality */
cur_temp = ((temp[3] & 0xf) << 16 | temp[4] << 8 | temp[5]) * 200.0 / (1 << 20) - 50;
}
}
else
{
LOG_E("The aht10 could not respond temperature measurement at this time. Please try again");
}
rt_mutex_release(dev->lock);
return cur_temp;
}
static float read_hw_humidity(aht10_device_t dev)
{
rt_uint8_t temp[6];
float cur_humi = 0.0; //The data is error with missing measurement.
rt_err_t result;
RT_ASSERT(dev);
result = rt_mutex_take(dev->lock, RT_WAITING_FOREVER);
if (result == RT_EOK)
{
rt_uint8_t cmd[2] = {0, 0};
write_reg(dev->i2c, AHT10_GET_DATA, cmd); // sample data cmd
result = calibration_enabled(dev);
if (result != RT_EOK)
{
rt_thread_mdelay(1500);
sensor_init(dev);
LOG_E("The aht10 is under an abnormal status. Please try again");
}
else
{
read_regs(dev->i2c, 6, temp); // get data
cur_humi = (temp[1] << 12 | temp[2] << 4 | (temp[3] & 0xf0) >> 4) * 100.0 / (1 << 20); //sensor humidity converse to reality
}
}
else
{
LOG_E("The aht10 could not respond temperature measurement at this time. Please try again");
}
rt_mutex_release(dev->lock);
return cur_humi;
}
static rt_err_t write_reg(struct rt_i2c_bus_device *bus, rt_uint8_t reg, rt_uint8_t *data)
{
rt_uint8_t buf[3];
buf[0] = reg; //cmd
buf[1] = data[0];
buf[2] = data[1];
if (rt_i2c_master_send(bus, AHT10_ADDR, 0, buf, 3) == 3)
return RT_EOK;
else
return -RT_ERROR;
}
static rt_err_t read_regs(struct rt_i2c_bus_device *bus, rt_uint8_t len, rt_uint8_t *buf)
{
struct rt_i2c_msg msgs;
msgs.addr = AHT10_ADDR;
msgs.flags = RT_I2C_RD;
msgs.buf = buf;
msgs.len = len;
if (rt_i2c_transfer(bus, &msgs, 1) == 1)
{
return RT_EOK;
}
else
{
return -RT_ERROR;
}
}
/*check calibration enable */
static rt_uint8_t calibration_enabled(aht10_device_t dev)
{
rt_uint8_t val = 0;
read_regs(dev->i2c, 1, &val);
if ((val & 0x68) == 0x08)
return RT_EOK;
else
return RT_ERROR;
}
读取AHT10温湿度数据,需要先发送触发测量数据的命令0xAC,再读取温湿度数据。读取温湿度数据前需要先确认AHT10是否已完成初始化,该过程通过函数calibration_enabled实现,原理是读取AHT10状态位,Bit[3]使能校准位是否被置位,若未校准则执行初始化函数,若已校准则读取温湿度数据,并按照AHT10温湿度信号换算公式处理读取的数据。
AHT10读取温湿度数据的过程中,需要通过IIC总线与AHT10通信,在函数write_reg与read_regs中分别调用了RT-Thread I2C设备接口函数rt_i2c_master_send与rt_i2c_transfer,从上面的代码中可以看出IIC设备的访问过程,即先通过rt_device_find发现I2C设备(函数rt_i2c_bus_device_find是对rt_device_find的再封装),然后就可以通过I2C设备驱动框架层或 I / O设备管理层的接口函数访问I2C设备了。
我们使用AHT10软件包提供的接口函数编写个读取AHT10温湿度数据的示例程序,先在.\applications目录下新建i2c_sample.c文件,在其中编辑读取AHT10温湿度数据的程序代码如下:
// projects\stm32l475_device_sample\applications\i2c_sample.c
#include "rtthread.h"
#include "rtdevice.h"
#include "aht10.h"
static void i2c_aht10_sample(void)
{
float humidity, temperature;
aht10_device_t aht10_dev;
const char *i2c_bus_name = "i2c1";
int count = 0;
aht10_dev = aht10_init(i2c_bus_name);
if(aht10_dev == RT_NULL)
rt_kprintf("The sensor initializes failed.\n");
while(++count < 10)
{
humidity = aht10_read_humidity(aht10_dev);
rt_kprintf("read aht10 sensor humidity : %d.%d %%\n", (int)humidity, (int)(humidity * 10) % 10);
temperature = aht10_read_temperature(aht10_dev);
if( temperature >= 0 )
rt_kprintf("read aht10 sensor temperature: %d.%d°C\n", (int)temperature, (int)(temperature * 10) % 10);
else
rt_kprintf("read aht10 sensor temperature: %d.%d°C\n", (int)temperature, (int)(-temperature * 10) % 10);
rt_thread_mdelay(1000);
}
}
MSH_CMD_EXPORT(i2c_aht10_sample, i2c aht10 sample);
在env环境下执行scons --target=mdk5命令生成MDK工程,打开生成的工程文件project.uvprojx,编译报错如下:
提示找不到sensor.h文件,我们并没有使能SENSOR传感器框架,在menuconfig中使能SENSOR传感器框架(下文再介绍SENSOR框架工作原理),配置界面如下:
保存配置后,在env中执行scons --target=mkd5重新生成工程,打开MDK工程文件project.uvprojx编译无报错,将程序烧录到我们的STM32L475潘多拉开发板中,运行结果如下:
本示例工程源码下载地址:https://github.com/StreamAI/RT-Thread_Projects/tree/master/projects/stm32l475_device_sample
Sensor(传感器)是物联网重要的一部分,“Sensor 之于物联网”就相当于“眼睛之于人类”。人类如果没有了眼睛就看不到这大千的花花世界,对于物联网来说也是一样。
如今随着物联网的发展,已经有大量的 Sensor 被开发出来供开发者选择了,如:加速度计(Accelerometer)、磁力计(Magnetometer)、陀螺仪(Gyroscope)、气压计(Barometer/pressure)、湿度计(Humidometer)等。这些传感器,世界上的各大半导体厂商都有生产,虽然增加了市场的可选择性,同时也加大了应用程序开发的难度。因为不同的传感器厂商、不同的传感器都需要配套自己独有的驱动才能运转起来,这样在开发应用程序的时候就需要针对不同的传感器做适配,自然加大了开发难度。为了降低应用开发的难度,增加传感器驱动的可复用性,RT-Thread设计了 Sensor 设备。
Sensor 设备的作用是:为上层提供统一的操作接口(也即 I / O设备管理接口)。传感器设备一般具有如下特性:
既然要兼容市场大部分传感器,就要对Sensor设备有恰当的描述,SENSOR框架描述传感器设备的数据结构如下:
// rt-thread-4.0.1\components\drivers\sensors\sensor.h
typedef struct rt_sensor_device *rt_sensor_t;
struct rt_sensor_device
{
struct rt_device parent; /* The standard device */
struct rt_sensor_info info; /* The sensor info data */
struct rt_sensor_config config; /* The sensor config data */
void *data_buf; /* The buf of the data received */
rt_size_t data_len; /* The size of the data received */
const struct rt_sensor_ops *ops; /* The sensor ops */
struct rt_sensor_module *module; /* The sensor module */
rt_err_t (*irq_handle)(rt_sensor_t sensor); /* Called when an interrupt is generated, registered by the driver */
};
struct rt_sensor_info
{
rt_uint8_t type; /* The sensor type */
rt_uint8_t vendor; /* Vendor of sensors */
const char *model; /* model name of sensor */
rt_uint8_t unit; /* unit of measurement */
rt_uint8_t intf_type; /* Communication interface type */
rt_int32_t range_max; /* maximum range of this sensor's value. unit is 'unit' */
rt_int32_t range_min; /* minimum range of this sensor's value. unit is 'unit' */
rt_uint32_t period_min; /* Minimum measurement period,unit:ms. zero = not a constant rate */
rt_uint8_t fifo_max;
};
struct rt_sensor_config
{
struct rt_sensor_intf intf; /* sensor interface config */
struct rt_device_pin_mode irq_pin; /* Interrupt pin, The purpose of this pin is to notification read data */
rt_uint8_t mode; /* sensor work mode */
rt_uint8_t power; /* sensor power mode */
rt_uint16_t odr; /* sensor out data rate */
rt_int32_t range; /* sensor range of measurement */
};
struct rt_sensor_intf
{
char *dev_name; /* The name of the communication device */
rt_uint8_t type; /* Communication interface type */
void *user_data; /* Private data for the sensor. ex. i2c addr,spi cs,control I/O */
};
struct rt_sensor_ops
{
rt_size_t (*fetch_data)(struct rt_sensor_device *sensor, void *buf, rt_size_t len);
rt_err_t (*control)(struct rt_sensor_device *sensor, int cmd, void *arg);
};
struct rt_sensor_module
{
rt_mutex_t lock; /* The module lock */
rt_sensor_t sen[RT_SENSOR_MODULE_MAX]; /* The module contains a list of sensors */
rt_uint8_t sen_num; /* Number of sensors contained in the module */
};
描述传感器设备的结构体rt_sensor_device继承自设备基类rt_device,成员包括:保存传感器信息数据的rt_sensor_info,保存传感器配置数据的rt_sensor_config,传感器接收数据基地址及长度,传感器操作函数集合rt_sensor_ops,拥有多个传感器的模组rt_sensor_module,传感器的中断处理函数指针irq_handle等,对传感器设备的描述还是挺完备的。
sensor配置结构体rt_sensor_config中包含了传感器接口rt_sensor_intf,中断引脚rt_device_pin_mode,工作模式mode,功耗模式power,传输速率odr,测量范围range等,该结构体需要用户根据具体的传感器型号配置。
Sensor的操作函数集合rt_sensor_ops中只有两个成员:获取传感器数据的接口函数fetch_data,传感器控制接口函数control。这两个函数需要具体的传感器在使用sensor框架时实现并注册到sensor框架中。
读取不同的Sensor,数据格式是不一样的,数据单位也是不一样的,为了将获取的传感器数据以更直观的形式展示,还需要专门的数据结构描述读取的传感器数据,描述sensor数据的数据结构如下:
// rt-thread-4.0.1\components\drivers\sensors\sensor.h
struct rt_sensor_data
{
rt_uint32_t timestamp; /* The timestamp when the data was received */
rt_uint8_t type; /* The sensor type of the data */
union
{
struct sensor_3_axis acce; /* Accelerometer. unit: mG */
struct sensor_3_axis gyro; /* Gyroscope. unit: mdps */
struct sensor_3_axis mag; /* Magnetometer. unit: mGauss */
rt_int32_t temp; /* Temperature. unit: dCelsius */
rt_int32_t humi; /* Relative humidity. unit: permillage */
rt_int32_t baro; /* Pressure. unit: pascal (Pa) */
rt_int32_t light; /* Light. unit: lux */
rt_int32_t proximity; /* Distance. unit: centimeters */
rt_int32_t hr; /* Heart rate. unit: bpm */
rt_int32_t tvoc; /* TVOC. unit: permillage */
rt_int32_t noise; /* Noise Loudness. unit: HZ */
rt_uint32_t step; /* Step sensor. unit: 1 */
rt_int32_t force; /* Force sensor. unit: mN */
} data;
};
/* 3-axis Data Type */
struct sensor_3_axis
{
rt_int32_t x;
rt_int32_t y;
rt_int32_t z;
};
结构体rt_sensor_data包含传感器类型type,读取数据的时间戳timestamp,读取到的数据data三个成员构成,其中data是一个共用体,可以保存这13类传感器中的任意一类数据。
先从sensor设备的创建与注册过程开始介绍,该过程的实现代码如下:
// rt-thread-4.0.1\components\drivers\sensors\sensor.c
/*
* sensor register
*/
int rt_hw_sensor_register(rt_sensor_t sensor,
const char *name,
rt_uint32_t flag,
void *data)
{
rt_int8_t result;
rt_device_t device;
RT_ASSERT(sensor != RT_NULL);
char *sensor_name = RT_NULL, *device_name = RT_NULL;
/* Add a type name for the sensor device */
sensor_name = sensor_name_str[sensor->info.type];
device_name = rt_calloc(1, rt_strlen(sensor_name) + 1 + rt_strlen(name));
if (device_name == RT_NULL)
{
LOG_E("device_name calloc failed!");
return -RT_ERROR;
}
rt_memcpy(device_name, sensor_name, rt_strlen(sensor_name) + 1);
strcat(device_name, name);
if (sensor->module != RT_NULL && sensor->module->lock == RT_NULL)
{
/* Create a mutex lock for the module */
sensor->module->lock = rt_mutex_create(name, RT_IPC_FLAG_FIFO);
if (sensor->module->lock == RT_NULL)
{
rt_free(device_name);
return -RT_ERROR;
}
}
device = &sensor->parent;
#ifdef RT_USING_DEVICE_OPS
device->ops = &rt_sensor_ops;
#else
device->init = rt_sensor_init;
device->open = rt_sensor_open;
device->close = rt_sensor_close;
device->read = rt_sensor_read;
device->write = RT_NULL;
device->control = rt_sensor_control;
#endif
device->type = RT_Device_Class_Sensor;
device->rx_indicate = RT_NULL;
device->tx_complete = RT_NULL;
device->user_data = data;
result = rt_device_register(device, device_name, flag | RT_DEVICE_FLAG_STANDALONE);
if (result != RT_EOK)
{
LOG_E("rt_sensor register err code: %d", result);
return result;
}
LOG_I("rt_sensor init success");
return RT_EOK;
}
static char *const sensor_name_str[] =
{
"none",
"acce_", /* Accelerometer */
"gyro_", /* Gyroscope */
"mag_", /* Magnetometer */
"temp_", /* Temperature */
"humi_", /* Relative Humidity */
"baro_", /* Barometer */
"li_", /* Ambient light */
"pr_", /* Proximity */
"hr_", /* Heart Rate */
"tvoc_", /* TVOC Level */
"noi_", /* Noise Loudness */
"step_" /* Step sensor */
"forc_" /* Force sensor */
};
#ifdef RT_USING_DEVICE_OPS
const static struct rt_device_ops rt_sensor_ops =
{
rt_sensor_init,
rt_sensor_open,
rt_sensor_close,
rt_sensor_read,
RT_NULL,
rt_sensor_control
};
#endif
传感器设备注册时,设备名自动加上了传感器类型前缀sensor_name_str,注册后的sensor设备若要访问也需要加上类型前缀,由于rt_device设备名最大长度RT_NAME_MAX默认为8,也即加上sensor type前缀后的设备名不应超过RT_NAME_MAX,若设备名长度不够可以将RT_NAME_MAX值设置大些,否则设备名会被自动截取。
Sensor框架向 I / O设备管理层注册的设备操作函数集合rt_sensor_ops是通过调用rt_sensor_ops来实现的,这里就不逐一展示实现代码了,需要注意的是从传感器读取的数据都是以rt_sensor_data为单位组织的,读取的数据长度则是rt_sensor_data数据的个数。
传感器设备向 I / O设备管理层注册后,就可以通过 I / O设备管理接口访问传感器设备了,接口函数在前面介绍过,这里不再详细介绍,只展示函数描述:
前面介绍AHT10软件包时使能了Sensor框架,说明最新版本的AHT10软件包支持Sensor框架,这倒是省去了我们的移植工作,下面看看移植实现过程。
先看AHT10传感器的初始化过程实现代码:
// bsp\stm32l475_week4\packages\aht10-latest\sensor_asair_aht10.c
int rt_hw_aht10_init(const char *name, struct rt_sensor_config *cfg)
{
rt_int8_t result;
rt_sensor_t sensor_temp = RT_NULL, sensor_humi = RT_NULL;
#ifdef PKG_USING_AHT10
/* temperature sensor register */
sensor_temp = rt_calloc(1, sizeof(struct rt_sensor_device));
if (sensor_temp == RT_NULL)
return -1;
sensor_temp->info.type = RT_SENSOR_CLASS_TEMP;
sensor_temp->info.vendor = RT_SENSOR_VENDOR_UNKNOWN;
sensor_temp->info.model = "aht10";
sensor_temp->info.unit = RT_SENSOR_UNIT_DCELSIUS;
sensor_temp->info.intf_type = RT_SENSOR_INTF_I2C;
sensor_temp->info.range_max = SENSOR_TEMP_RANGE_MAX;
sensor_temp->info.range_min = SENSOR_TEMP_RANGE_MIN;
sensor_temp->info.period_min = 5;
rt_memcpy(&sensor_temp->config, cfg, sizeof(struct rt_sensor_config));
sensor_temp->ops = &sensor_ops;
result = rt_hw_sensor_register(sensor_temp, name, RT_DEVICE_FLAG_RDONLY, RT_NULL);
if (result != RT_EOK)
{
LOG_E("device register err code: %d", result);
goto __exit;
}
/* humidity sensor register */
sensor_humi = rt_calloc(1, sizeof(struct rt_sensor_device));
if (sensor_humi == RT_NULL)
return -1;
sensor_humi->info.type = RT_SENSOR_CLASS_HUMI;
sensor_humi->info.vendor = RT_SENSOR_VENDOR_UNKNOWN;
sensor_humi->info.model = "aht10";
sensor_humi->info.unit = RT_SENSOR_UNIT_PERMILLAGE;
sensor_humi->info.intf_type = RT_SENSOR_INTF_I2C;
sensor_humi->info.range_max = SENSOR_HUMI_RANGE_MAX;
sensor_humi->info.range_min = SENSOR_HUMI_RANGE_MIN;
sensor_humi->info.period_min = 5;
rt_memcpy(&sensor_humi->config, cfg, sizeof(struct rt_sensor_config));
sensor_humi->ops = &sensor_ops;
result = rt_hw_sensor_register(sensor_humi, name, RT_DEVICE_FLAG_RDONLY, RT_NULL);
if (result != RT_EOK)
{
LOG_E("device register err code: %d", result);
goto __exit;
}
#endif
_aht10_init(&cfg->intf);
return RT_EOK;
__exit:
if (sensor_temp)
rt_free(sensor_temp);
if (sensor_humi)
rt_free(sensor_humi);
if (temp_humi_dev)
aht10_deinit(temp_humi_dev);
return -RT_ERROR;
}
static struct rt_sensor_ops sensor_ops =
{
aht10_fetch_data,
aht10_control
};
AHT10传感器包含温度传感器与湿度传感器两个类型,因此在初始化过程中分别创建并注册了两个sensor设备sensor_temp与sensor_humi。传感器对象rt_sensor_device通过动态分配内存空间,在初始化函数中完成了传感器信息rt_sensor_info的初始化,传感器配置rt_sensor_config则需要用户配置并以参数形式传入,最后将实现的传感器操作函数集合sensor_ops通过调用函数rt_hw_sensor_register注册到Sensor框架中。完成sensor设备注册后,调用前面介绍的aht10_init函数完成AHT10设备的初始化。
由于AHT10没有中断引脚,仅支持轮询访问方式,也没有功耗控制等复杂功能,因此其操作函数集合rt_sensor_ops实现比较简单,实现代码如下:
// bsp\stm32l475_week4\packages\aht10-latest\sensor_asair_aht10.c
static rt_size_t aht10_fetch_data(struct rt_sensor_device *sensor, void *buf, rt_size_t len)
{
RT_ASSERT(buf);
if (sensor->config.mode == RT_SENSOR_MODE_POLLING)
{
return _aht10_polling_get_data(sensor, buf);
}
else
return 0;
}
static rt_err_t aht10_control(struct rt_sensor_device *sensor, int cmd, void *args)
{
rt_err_t result = RT_EOK;
return result;
}
static rt_size_t _aht10_polling_get_data(rt_sensor_t sensor, struct rt_sensor_data *data)
{
float temperature_x10, humidity_x10;
if (sensor->info.type == RT_SENSOR_CLASS_TEMP)
{
temperature_x10 = 10 * aht10_read_temperature(temp_humi_dev);
data->data.temp = (rt_int32_t)temperature_x10;
data->timestamp = rt_sensor_get_ts();
}
else if (sensor->info.type == RT_SENSOR_CLASS_HUMI)
{
humidity_x10 = 10 * aht10_read_humidity(temp_humi_dev);
data->data.humi = (rt_int32_t)humidity_x10;
data->timestamp = rt_sensor_get_ts();
}
return 1;
}
AHT10获取传感器数据函数中,读取的温湿度值以十倍形式给出,主要是由于传感器数据结构体rt_sensor_data中的成员类型都为整数,要想精确到小数位需要另行处理,比如精确到一位小数,就把读取的值以10倍形式给出,用户对其除10得整数部分,对其取10的模数得小数部分。
前面我们已经在menuconfig中使能sensor框架了,所以这里直接在.\applications\i2c_sample.c文件中新增示例程序。
要访问AHT10设备,首先需要进行初始化,前面介绍AHT10初始化函数rt_hw_aht10_init时提到,需要我们自己配置rt_sensor_config,并将其以参数形式传入,编辑的初始化代码如下:
// projects\stm32l475_device_sample\applications\i2c_sample.c
#include "rtthread.h"
#include "rtdevice.h"
#include "sensor_asair_aht10.h"
#define AHT10_I2C_BUS_NAME "i2c1"
static int rt_hw_aht10_port(void)
{
struct rt_sensor_config cfg;
cfg.intf.dev_name = AHT10_I2C_BUS_NAME;
cfg.intf.type = RT_SENSOR_INTF_I2C;
cfg.intf.user_data = (void *)AHT10_I2C_ADDR;
cfg.mode = RT_SENSOR_MODE_POLLING;
rt_hw_aht10_init("aht10", &cfg);
return RT_EOK;
}
INIT_ENV_EXPORT(rt_hw_aht10_port);
上面使用了RT-Thread自动初始化机制,传感器配置项rt_sensor_config.intf.user_data对于IIC设备常用来保存其设备地址(对于SPI设备常用来保存其片选引脚)。
完成AHT10传感器设备初始化后,就可以使用 I / O设备管理接口访问该传感器了,访问AHT10传感器的示例代码如下:
// projects\stm32l475_device_sample\applications\i2c_sample.c
#define SENSOR_TEMP_NAME "temp_aht10"
#define SENSOR_HUMI_NAME "humi_aht10"
static void sensor_aht10_sample(void)
{
rt_device_t sensor_temp, sensor_humi;
struct rt_sensor_data sensor_data;
sensor_temp = rt_device_find(SENSOR_TEMP_NAME);
rt_device_open(sensor_temp, RT_DEVICE_FLAG_RDONLY);
if(rt_device_read(sensor_temp, 0, &sensor_data, 1) == 1)
{
rt_kprintf("read aht10 sensor temperature:%3d.%d°C, timestamp:%5d\n", sensor_data.data.temp / 10, sensor_data.data.temp % 10, sensor_data.timestamp);
}
rt_device_close(sensor_temp);
sensor_humi = rt_device_find(SENSOR_HUMI_NAME);
rt_device_open(sensor_humi, RT_DEVICE_FLAG_RDONLY);
if(rt_device_read(sensor_humi, 0, &sensor_data, 1) == 1)
{
rt_kprintf("read aht10 sensor humidity:%3d.%d%, timestamp:%5d\n", sensor_data.data.humi / 10, sensor_data.data.humi % 10, sensor_data.timestamp);
}
rt_device_close(sensor_temp);
}
MSH_CMD_EXPORT_ALIAS(sensor_aht10_sample, sensor_aht10, sensor aht10 sample);
示例程序编辑完成后保存,在env环境中执行scons --target=mdk5生成MDK工程文件,打开工程文件project.uvprojx编译无报错,将程序烧录到STM32L475潘多拉开发板中,运行结果如下:
sensor框架还为用户提供了部分finsh命令,方便用户通过console读取或调试传感器设备,在上图中也给出了sensor finsh命令运行示例。
本示例工程源码下载地址:https://github.com/StreamAI/RT-Thread_Projects/tree/master/projects/stm32l475_device_sample