IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架

文章目录

  • 一、IIC协议简介
    • 1.1 IIC总线简介
    • 1.2 硬件IIC与软件模拟IIC
  • 二、IIC设备对象管理
    • 2.1 IIC设备驱动框架层
  • 三、IIC应用示例之AHT10
    • 3.1 AHT10温湿度传感器简介
    • 3.2 IIC设备应用示例
  • 四、Sensor框架原理及示例
    • 4.1 Sensor设备描述
    • 4.2 Sensor设备访问接口
    • 4.3 AHT10移植到Sensor框架
    • 4.4 Sensor框架应用示例
  • 更多文章:

一、IIC协议简介

1.1 IIC总线简介

I2C(Inter Integrated Circuit)总线是 PHILIPS 公司开发的一种半双工、双向二线制同步串行总线。I2C 总线传输数据时只需两根信号线,一根是双向数据线 SDA(serial data),另一根是双向时钟线 SCL(serial clock)。SPI 总线有两根线分别用于主从设备之间接收数据和发送数据,而 I2C 总线只使用一根线进行数据收发。

I2C 和 SPI 一样以主从的方式工作,不同于 SPI 一主多从的结构,它允许同时有多个主设备存在,每个连接到总线上的器件都有唯一的地址,主设备启动数据传输并产生时钟信号,从设备被主设备寻址(SPI通过CS片选引脚选择目标从设备,IIC通过发送从设备地址以寻址方式选择目标从设备),同一时刻只允许有一个主设备。如下图所示:
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第1张图片
SDA 线上的数据必须在时钟的高电平周期保持稳定,数据线的高或低电平状态只有在 SCL 线的时钟信号是低电平时才能改变。换言之,SCL为高电平时表示有效数据,SDA为高电平表示“1”,低电平表示“0”;SCL为低电平时表示无效数据,此时SDA会进行电平切换,为下次数据表示做准备。下图为数据有效性的时序图:
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第2张图片
I2C 总线主要的数据传输格式如下图所示:
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第3张图片
当总线空闲时,SDA 和 SCL 都处于高电平状态,当主机要和某个从机通讯时,会先发送一个开始条件,然后发送从机地址和读写控制位,接下来传输数据(主机发送或者接收数据),数据传输结束时主机会发送停止条件。传输的每个字节为8位,高位在前,低位在后。数据传输过程中的不同名词详解如下所示:

  • 开始条件: SCL 为高电平时,主机将 SDA 拉低,表示数据传输即将开始,总线在开始条件后处于busy的状态,在停止条件的某段时间后,总线才再次处于空闲状态,下图为开始和停止条件的信号产生时序图:
    IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第4张图片
  • 从机地址: 主机发送的第一个字节为从机地址,高 7 位为地址,最低位为 R/W 读写控制位,1 表示读操作,0 表示写操作。一般从机地址有 7 位地址模式和 10 位地址模式两种,如果是 10 位地址模式,第一个字节的头 7 位 是 11110XX 的组合,其中最后两位(XX)是 10 位地址的两个最高位,第二个字节为 10 位从机地址的剩下8位,如下图所示:
    IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第5张图片
  • 应答信号: 每传输完成一个字节的数据,接收方就需要回复一个 ACK(acknowledge)。写数据时由从机发送ACK,读数据时由主机发送 ACK。数据接收方收到传输的一个字节数据后,需要给出响应,此时处在第九个时钟,发送端释放SDA线控制权,将SDA电平拉高,由接收方控制。若希望继续,则给出“应答(ACK)”信号,即SDA为低电平;反之给出“非应答(NACK)”信号,即SDA为高电平,应答信号产生的时序图如下:
    IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第6张图片
  • 数据: 从机地址发送完后可能会发送一些指令,依从机而定,然后开始传输数据,由主机或者从机发送,每个数据为 8 位,数据的字节数没有限制;
  • 重复开始条件: 在一次通信过程中,主机可能需要和不同的从机传输数据或者需要切换读写操作时,主机可以再发送一个开始条件;
  • 停止条件: 在 SDA 为低电平时,主机将 SCL 拉高并保持高电平,然后在将 SDA 拉高,表示传输结束。

1.2 硬件IIC与软件模拟IIC

在正点原子的教程中说,STM32的硬件IIC设计比较复杂,而且稳定性不佳(貌似是ST为了规避飞利浦IIC的版权问题),所以在CPU资源不紧张的情况下,很多人一般会选择GPIO模拟I2C。硬件IIC与软件模拟IIC有何区别呢?

  • 硬件IIC:跟之前介绍的SPI外设与USART外设类似,物理层有专门的电路支持,IIC引脚自然也是专用的,借助芯片厂商提供的固件库函数实现对IIC外设寄存器的访问,工作效率较高;
  • 软件模拟IIC:物理层借助GPIO外设,并不使用固件库的IIC函数访问IIC寄存器,可以根据需要配置模拟IIC通信的GPIO引脚,协议层需要自己实现,而且软件模拟IIC工作效率比硬件IIC低不少。

STM32平台由于软件模拟IIC比较常用,且方便移植,RT-Thread的IIC设备驱动也是使用的软件模拟方式实现的,所以下面就不介绍硬件IIC的功能框图、IIC固件库等内容了(自然也不需要CubeMX配置IIC外设了),下面开始介绍RT-Thread IIC驱动框架的实现。

二、IIC设备对象管理

介绍IIC设备对象管理前,再展示下RT-Thread I / O设备模型框架,按照模型框架一层层解析:
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第7张图片

2.1 IIC设备驱动框架层

  • IIC总线控制块

先看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协议数据帧中从机地址、各标志位、传输数据的起始地址及长度等内容。

  • 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设备驱动层实现。

  • 软件模拟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函数为例,对照其信号产生时序图看其实现代码如下:
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第8张图片

// 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为高电平。
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第9张图片
按照上面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;
}

三、IIC应用示例之AHT10

3.1 AHT10温湿度传感器简介

AHT10是一款高精度、完全校准、贴片封装的温湿度传感器,包括一个电容式感湿元件和一个高性能CMOS微处理器相连接。AHT10采用标准IIC通信方式,体积小、功耗低、响应快、性价比高。

该传感器输入电压范围为1.8V - 3.3V,测量温度与湿度的量程、精度如下表所示:
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第10张图片
AHT10接口定义及其在潘多拉开发板上与STM32L475芯片的连接原理图如下:
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第11张图片
从上图可知潘多拉开发板使用PC1与PD6来模拟IIC总线的SDA与SCL引脚和AHT10温湿度传感器进行通信。

AHT10上电后,MCU要通过IIC协议访问AHT10传感器,需要知道AHT10的设备地址及其支持的命令,查询AHT10 datasheet可知,AHT10的IIC设备地址为0x38,其支持的基本命令与返回状态位如下表所示:
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第12张图片
MCU通过IIC协议读取AHT10温湿度数据的IIC传输数据帧如下图所示:
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第13张图片
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第14张图片
通过AHT10触发测量数据并读取温湿度数据的IIC传输数据时序图可以编写AHT10的驱动函数(或访问接口函数),读取出的温湿度数据需要换算成更直观的格式,换算公式如下:
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第15张图片

3.2 IIC设备应用示例

这里既然采用软件模拟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中的配置界面如下:
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第16张图片
menuconfig保存配置后,模拟IIC的引脚就配置好了,RT-Thread中的I2C1设备就可以使用了(I2C初始化与注册过程被RT-Thread自动初始化组件调用执行,用户只需完成引脚配置与依赖宏使能即可)。

用户要读取AHT10的温湿度数据,还需要按照前面介绍的AHT10 IIC通讯命令进行,RT-Thread正好提供了AHT10的驱动函数,我们直接使用即可,在menuconfig中使能AHT10驱动的配置界面如下:
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第17张图片
AHT10软件包支持多次读取输出平均值,这里重点介绍AHT10对IIC设备的访问,所以就不使能average filter了,选择最新版本,保存配置。

在env界面执行pkgs --update从github下载aht10软件包到当前工程,命令执行结果如下:
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第18张图片
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,编译报错如下:
AHT10编译报错
提示找不到sensor.h文件,我们并没有使能SENSOR传感器框架,在menuconfig中使能SENSOR传感器框架(下文再介绍SENSOR框架工作原理),配置界面如下:
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第19张图片
保存配置后,在env中执行scons --target=mkd5重新生成工程,打开MDK工程文件project.uvprojx编译无报错,将程序烧录到我们的STM32L475潘多拉开发板中,运行结果如下:
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第20张图片
本示例工程源码下载地址:https://github.com/StreamAI/RT-Thread_Projects/tree/master/projects/stm32l475_device_sample

四、Sensor框架原理及示例

Sensor(传感器)是物联网重要的一部分,“Sensor 之于物联网”就相当于“眼睛之于人类”。人类如果没有了眼睛就看不到这大千的花花世界,对于物联网来说也是一样。

如今随着物联网的发展,已经有大量的 Sensor 被开发出来供开发者选择了,如:加速度计(Accelerometer)、磁力计(Magnetometer)、陀螺仪(Gyroscope)、气压计(Barometer/pressure)、湿度计(Humidometer)等。这些传感器,世界上的各大半导体厂商都有生产,虽然增加了市场的可选择性,同时也加大了应用程序开发的难度。因为不同的传感器厂商、不同的传感器都需要配套自己独有的驱动才能运转起来,这样在开发应用程序的时候就需要针对不同的传感器做适配,自然加大了开发难度。为了降低应用开发的难度,增加传感器驱动的可复用性,RT-Thread设计了 Sensor 设备。

Sensor 设备的作用是:为上层提供统一的操作接口(也即 I / O设备管理接口)。传感器设备一般具有如下特性:

  • 接口:标准 device 接口(open/close/read/control);
  • 工作模式:支持 轮询、中断、FIFO 三种模式;
  • 电源模式:支持 掉电、普通、低功耗、高功耗 四种模式

4.1 Sensor设备描述

既然要兼容市场大部分传感器,就要对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类传感器中的任意一类数据。

4.2 Sensor设备访问接口

先从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设备管理接口访问传感器设备了,接口函数在前面介绍过,这里不再详细介绍,只展示函数描述:
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第21张图片

4.3 AHT10移植到Sensor框架

前面介绍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的模数得小数部分。

4.4 Sensor框架应用示例

前面我们已经在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潘多拉开发板中,运行结果如下:
IOT-OS之RT-Thread(八)--- IIC设备对象管理与Sensor管理框架_第22张图片
sensor框架还为用户提供了部分finsh命令,方便用户通过console读取或调试传感器设备,在上图中也给出了sensor finsh命令运行示例。

本示例工程源码下载地址:https://github.com/StreamAI/RT-Thread_Projects/tree/master/projects/stm32l475_device_sample

更多文章:

  • 《RT-Thread Sample Project Source Code Based on STM32L475》
  • 《IOT-OS之RT-Thread(七)— I/O设备模型框架与PIN设备对象管理》
  • 《IOT-OS之RT-Thread(九)— SPI设备对象管理与SFUD管理框架》

你可能感兴趣的:(STM32,操作系统,流云的博客)