rt-thread设备驱动模型-i2c驱动
1. 回顾
前面两章分别介绍了rt-thread设备驱动框架的实现原理,以及介绍了一个简单的看门狗驱动程序,用来加深对驱动框架的理解。看门狗驱动程序最终归纳成了下面这一张图:
rt-thread对看门狗设备进行了抽象,使用 rt_watchdog_device
结构体进行描述,这个结构体包含一个 rt_device
的设备对象,用于将该看门狗设备挂载到内核中的设备信息链表上。另外, rt_watchdog_device
还包含一套针对看门狗设备进行操作的方法 rt_watchdog_ops
,这些方法是需要驱动开发者实现。
驱动开发者定义好 rt_watchdog_device
,并且实现 rt_watchdog_ops
中的函数,就可以调用接口 rt_hw_watchdog_register
进行看门狗设备的注册。这个注册函数中会初始化 rt_device
中的 rt_device_ops
,然后调用 rt_device_register
将设备挂载到内核设备信息链表上。
应用层对设备进行操作的标准接口,如 rt_device_open
、 rt_device_read
、 rt_device_write
与 rt_device
中的 rt_device_ops
的函数一一对应。在看门狗设备框架中, rt_device_ops
中的函数会调用驱动开发者实现的 rt_watchdog_ops
中的函数,从而使整个调用流程形成了一个闭环。
rt-thread的看门狗驱动体现了rt-thread驱动框架的整体流程,其他设备的框架也是采用了相同的架构。下文将会介绍rt-thread中稍微复杂一点的i2c驱动。
; 2. i2c的使用
i2c是一种半双工同步通信方式,在硬件上包含两条线分别为时钟线SCL和数据线SDA。i2c总线上可以挂载多个从设备,每个从设备都有唯一的地址,主设备通过地址与指定的从设备进行通信。i2c硬件时序主要包含开始信号、从机地址、读写标志位、应答信号和停止信号,关于i2c具体的读写时序有很多资料可以参考,不是本文介绍的重点,在此不做介绍。
要介绍i2c的使用,首先要介绍一下i2c框架的收发数据的组织形式,熟悉linux i2c驱动的开发者对 i2c_msg
这个结构体应该不会感到陌生,i2c驱动框架会将需要发送的数据或者接收的数据封装成一个message进行发送和接收。rt-thread也采用相同的方法,message的数据结构为:
struct rt_i2c_msg
{
rt_uint16_t addr;
rt_uint16_t flags;
rt_uint16_t len;
rt_uint8_t *buf;
};
addr
为i2c设备地址、 flags
为读写标志、 len
为读写数据长度、 buf
为读写buffer指针,使用 rt_i2c_msg
将需要的读写数据封装起来,然后调用i2c的发送函数即可,rt-thread的发送函数为:
rt_size_t rt_i2c_transfer(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num)
这个函数有三个形参,其中 msgs
为i2c消息数组的地址, num
为消息数组成员的个数。第一个参数 bus
为 struct rt_i2c_bus_device
的指针,还记得前文说过设备都会备抽象成一个结构体对象来进行描述么, rt_i2c_bus_device
就是对i2c控制器设备的抽象。
struct rt_i2c_bus_device
{
struct rt_device parent;
const struct rt_i2c_bus_device_ops *ops;
rt_uint16_t flags;
struct rt_mutex lock;
rt_uint32_t timeout;
rt_uint32_t retries;
void *priv;
};
在此不对这个结构体进行详细的介绍,后面会进行说明,其实这个结构体与前文说的看门狗结构体 rt_watchdog_device
等同。举例,i2c读写eeprom使用流程如下:
#define EEPROM_I2CBUS_NAME "i2c1"
#define EEPROM_ADDR 0x50
struct rt_i2c_bus_device *i2c_bus;
i2c_bus = (struct rt_i2c_bus_device*)rt_device_find(EEPROM_I2CBUS_NAME);
rt_err_t eeprom_read(rt_uint8_t addr, rt_uint8_t *buf, rt_uint16_t size) {
struct rt_i2c_msg msg[2];
msg[0].addr = EEPROM_ADDR;
msg[0].flags = RT_I2C_WR;
msg[0].buf = &addr;
msg[0].len = 1;
msg[1].addr = EEPROM_ADDR;
msg[1].flags = RT_I2C_RD;
msg[1].buf = buf;
msg[1].len = size;
return rt_i2c_transfer(i2c_bus, msg, 2) == 2 ? RT_EOK : -RT_ERROR;
}
rt_err_t eeprom_write(rt_uint8_t addr, rt_uint8_t *buf, rt_uint16_t size) {
struct rt_i2c_msg msg[2];
msg[0].addr = EEPROM_ADDR;
msg[0].flags = RT_I2C_WR;
msg[0].buf = &addr;
msg[0].len = 1;
msg[1].addr = EEPROM_ADDR;
msg[1].flags = RT_I2C_WR | RT_I2C_NO_START;
msg[1].buf = buf;
msg[1].len = size;
return rt_i2c_transfer(i2c_bus, msg, 2) == 2 ? RT_EOK : -RT_ERROR;
}
对i2c设备进行操作的步骤与看门狗设备没有什么差别,第一步就是调用通用接口 rt_device_find
根据设备名查找 rt_i2c_bus_device
,然后用 rt_i2c_msg
封装要发送或者接收的数据,最后调用 rt_i2c_transfer
进行数据收发即可。所以,i2c驱动框架在应用层开发还是挺简单的,就是填充数据然后调用收发接口,下面将介绍i2c框架的具体实现。
3. i2c驱动框架
i2c驱动一般分为两个部分,一部分为i2c控制器驱动,另一部分为挂载在i2c总线上设备的驱动。在Linux i2c驱动框架中将i2c控制器抽象为 i2c_adapter
结构体,在这个结构体中包含i2c的收发函数,将挂载在i2c总线上的设备驱动抽象为 i2c_driver
,并且将i2c设备信息抽象为 i2c_client
。 i2c_driver
使用 i2c_adapter
的收发函数与i2c设备进行数据交互。在rt-thread的i2c驱动框架中,并没有使用这么多的结构体进行抽象,其框架相较Linux的i2c框架更为简单一些,使用 rt_i2c_bus_device
对i2c控制器进行抽象,并没有使用专门的数据结构对i2c设备进行抽象。
struct rt_i2c_bus_device
{
struct rt_device parent;
const struct rt_i2c_bus_device_ops *ops;
rt_uint16_t flags;
struct rt_mutex lock;
rt_uint32_t timeout;
rt_uint32_t retries;
void *priv;
};
rt_i2c_bus_device
是rt-thread用来描述i2c控制器的结构体,对i2c驱动框架的理解只需关注 rt_device parent
和 rt_i2c_bus_device_ops ops
成员即可。 parent
的作用相信有了前面几章的介绍应该不会感到陌生了,就是将i2c控制器设备挂载到内核的设备信息链表中进行统一的管理。 ops
成员就是i2c控制器的设备操作函数集合,实际就是i2c的数据收发函数:
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);
};
上面结构体就是i2c控制器操作函数,本文只关心 master_xfer
,该函数就是对i2c设备进行数据交互的核心函数。上面两个数据结构对i2c控制器进行了抽象,根据以往的经验,会提供一个注册函数向rt-thread内核注册设备对象,函数 rt_i2c_bit_add_bus
的作用就是这个。
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);
}
该函数的作用就是向rt-thread内核注册i2c设备内核对象,然后进行统一的管理。在进行注册之前会对 rt_i2c_bit_add_bus
和 rt_device
中的函数进行初始化,如上 rt_i2c_bus_device
中的 rt_i2c_bus_device_ops
就被赋值成 i2c_bit_bus_ops
:
static const struct rt_i2c_bus_device_ops i2c_bit_bus_ops =
{
i2c_bit_xfer,
RT_NULL,
RT_NULL
};
i2c_bit_xfer
就是实际的i2c控制器与i2c设备进行数据交互的函数,在此先不讲这函数怎么实现的。接着看 rt_i2c_bit_add_bus
的后面过程, rt_i2c_bit_add_bus
最后会调用 rt_i2c_bus_device_register
,该函数主要调用流程为:
rt_i2c_bus_device_register
rt_i2c_bus_device_device_init
就是调用了函数 rt_i2c_bus_device_device_init
, rt_i2c_bus_device_device_init
的调用流程为:
rt_i2c_bus_device_device_init
1. device = &bus->parent;
2. device->user_data = bus;
3. device->type = RT_Device_Class_I2CBUS;
4. 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;
5. rt_device_register(device, name, RT_DEVICE_FLAG_RDWR);
在函数 rt_i2c_bus_device_device_init
中,首先根据传入的 rt_i2c_bus_device
结构体获取到 rt_device
,然后将 rt_device
的用户私有数据设置为传入的 rt_i2c_bus_device
。最后,就是初始化 rt_device
中设备标准操作函数了,还记得前几篇章节文章中说过,设备标准操作函数与 rt_device
中的函数是一一对应的么,从这里就能看出i2c设备操作函数的对应关系为:
rt_device_read ---> i2c_bus_device_read
rt_device_write ---> i2c_bus_device_write
rt_device_control ---> i2c_bus_device_control
最后就调用了熟悉的设备对象注册函数 rt_device_register
,这个函数前面文章已经讲了多次,在此不再进行赘述了。
看到此处,可以引出一个问题:
rt_i2c_bus_device
中rt_i2c_bus_device_ops
函数集是被谁调用?
在前文讲 i2c的使用的时候,我们使用函数 rt_i2c_transfer
与设备进行交互,该函数定义为:
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;
}
}
上述函数对无用的部分进行了删减,可见 rt_i2c_transfer
会调用 rt_i2c_bus_device_ops
中的 master_xfer
函数,对i2c设备进行数据收发。使用函数 rt_i2c_transfer
是一种与设备进行交互的方式,其实还有另一种方式与i2c设备进行数据的通信。 rt_i2c_bus_device_device_init
中初始化了 rt_device
的 read
、 write
等函数,也就意味这可以使用 rt_device_read
、 rt_device_write
等函数与i2c设备进行通信,通过分析源代码来验证猜想。
static rt_size_t i2c_bus_device_read(rt_device_t dev,
rt_off_t pos,
void *buffer,
rt_size_t count)
{
rt_uint16_t addr;
rt_uint16_t flags;
struct rt_i2c_bus_device *bus = (struct rt_i2c_bus_device *)dev->user_data;
addr = pos & 0xffff;
flags = (pos >> 16) & 0xffff;
return rt_i2c_master_recv(bus, addr, flags, (rt_uint8_t *)buffer, count);
}
上述函数进行了部分删减, rt_device_read
函数最终会调用到 i2c_bus_device_read
,从这个函数中可以看出 i2c_bus_device_read
的第二个参数,即读写偏移量作为了i2c设备的地址,然后调用 rt_i2c_master_recv
。
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_RD;
msg.len = count;
msg.buf = buf;
ret = rt_i2c_transfer(bus, &msg, 1);
return (ret > 0) ? count : ret;
}
看到这儿是不是一切都清楚了,使用 rt_device_read
、 rt_device_write
函数与i2c设备进行通信的时候,会将收发的数据封装成 rt_i2c_msg
消息,最终通过函数 rt_i2c_transfer
进行数据的交互。所以,上面问题的答案就有两个:
rt_i2c_transfer
会调用rt_i2c_bus_device_ops
中的master_xfer
函数rt_device_read
、rt_device_write
等函数会调用rt_i2c_transfer
可见, master_xfer
函数就是i2c控制器与i2c设备进行数据交互的最底层函数了,在 rt_i2c_bus_device
注册中,该函数已经被初始化为 i2c_bit_xfer
,函数声明为:
static rt_size_t i2c_bit_xfer(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num)
对于这个函数需要实现的内容,在此应该能够大致猜测出来,就是根据i2c的协议实现相应的读写时序,这些时序包括开始信号、从机地址、读写标志位、应答信号和停止信号。至此,rt-thread的i2c驱动框架就已经介绍完了,驱动开发者需要根据不同的soc平台实现 i2c_bit_xfer
的i2c时序。stm32的bsp采用的是模拟i2c, i2c_bit_xfer
就是模拟i2c实现源码,可以自行分析。最后,以一张图进行总结,如下:
Original: https://blog.csdn.net/weixin_43249970/article/details/126506554
Author: 夏日清凉-thj
Title: 第三章 rt-thread设备驱动模型-i2c驱动
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/813367/
转载文章受原作者版权保护。转载请注明原作者出处!