I2C驱动框架解析
代码入口
代码入口其实挺好找,基本上芯片对I2C驱动代码提供会以以下名称编写,drv_i2c.c(硬件I2C驱动)或drv_soft_i2c.c(软件I2C套在硬件I2C驱动接口上的实现,我个人的观点,如果不支持硬件I2C,其实根本没必要专门写一套软件I2C的驱动,原因是,软件I2C本质上就是gpio操作加延时处理,完全就是业务逻辑的事情,不应该放在驱动层实现)。这里我们主要看硬件I2C,因此直接查看硬件I2C驱动文件。在驱动文件中,我们找到么一个入口:
struct i2c_handle { struct rt_i2c_bus_device bus; char bus_name[RT_NAME_MAX]; }; static struct i2c_handle ra_i2cs[] = { ... } static const struct rt_i2c_bus_device_ops i2c_ops = { .master_xfer = ra_i2c_mst_xfer, .slave_xfer = RT_NULL, .i2c_bus_control = RT_NULL }; int xxxx(void) { fsp_err_t err = FSP_SUCCESS; for (rt_uint32_t i = 0; i < sizeof(ra_i2cs) / sizeof(ra_i2cs[0]); i++) { // I2C接口初始化 i2cs[i].bus.ops = &i2c_ops; i2cs[i].bus.priv = 0; // 注册I2C设备 rt_i2c_bus_device_register(&i2cs[i].bus, i2cs[i].bus_name); } return 0; } INIT_DEVICE_EXPORT(xxxx);
其中:rt_i2c_bus_device_register即为我们所需要找到的I2C框架的入口位置。
I2C驱动注册函数解析
找到了设备级的I2C设备注册入口,我们便开始逐层分析注册接口。
#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 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; } rt_err_t rt_i2c_bus_device_register(struct rt_i2c_bus_device *bus, const char *bus_name) { rt_err_t res = RT_EOK; // 注册设备锁,用于不同线程调用I2C设备时的互斥操作 rt_mutex_init(&bus->lock, "i2c_bus_lock", RT_IPC_FLAG_PRIO); if (bus->timeout == 0) bus->timeout = RT_TICK_PER_SECOND; // 注册I2C设备 res = rt_i2c_bus_device_device_init(bus, bus_name); LOG_I("I2C bus [%s] registered", bus_name); #ifdef RT_USING_DM if (!res) { i2c_bus_scan_clients(bus); } #endif return res; }
完整代码一贴出来,我们便可以发现,所谓的I2C总线设备,实际上只是在posix标准框架上标准化了一套I2C接口框架,这个框架没有init,open和close。只有read,write和control。而这个注册最核心的部分,恰恰是rtt设备框架中的驱动注册入口rt_device_register。
应用获取I2C入口
既然I2C设备是通过rt_device_register注册的,因此应用端寻找I2C设备,也只需要调用rt_device_find即可,若存在,则返回设备操作句柄,若不存在,则返回null。
但是呢,实际上,rtt还封装了一层接口。
struct rt_i2c_bus_device *rt_i2c_bus_device_find(const char *bus_name) { struct rt_i2c_bus_device *bus; rt_device_t dev = rt_device_find(bus_name); if (dev == RT_NULL || dev->type != RT_Device_Class_I2CBUS) { LOG_E("I2C bus %s not exist", bus_name); return RT_NULL; } bus = (struct rt_i2c_bus_device *)dev->user_data; return bus; }
从接口实现上看,其实可以看出来了,加了个判断,判断获取到的设备的设备类类型是否是I2C设备
I2C读写函数解析
由RTT官方的I2C总线设备使用手册可知,I2C读写入口可以分为I2C读写入口,I2C读入口和I2C写入口
I2C 读写入口
rt_ssize_t rt_i2c_transfer(struct rt_i2c_bus_device *bus, struct rt_i2c_msg msgs[], rt_uint32_t num) { rt_ssize_t ret; rt_err_t err; if (bus->ops->master_xfer) { #ifdef RT_I2C_DEBUG for (ret = 0; ret < num; ret++) { LOG_D("msgs[%d] %c, addr=0x%02x, len=%d", ret, (msgs[ret].flags & RT_I2C_RD) ? 'R' : 'W', msgs[ret].addr, msgs[ret].len); } #endif err = rt_mutex_take(&bus->lock, RT_WAITING_FOREVER); if (err != RT_EOK) { return (rt_ssize_t)err; } ret = bus->ops->master_xfer(bus, msgs, num); err = rt_mutex_release(&bus->lock); if (err != RT_EOK) { return (rt_ssize_t)err; } return ret; } else { LOG_E("I2C bus operation not supported"); return -RT_EINVAL; } }
看到这,我们可以大致知道这些信息,驱动需要实现xfer函数,而xfer函数的作用需要实现读写操作。因此我们拿一份简化驱动实现来看。
static rt_ssize_t i2c_mst_xfer(struct rt_i2c_bus_device *bus, struct rt_i2c_msg msgs[], rt_uint32_t num) { rt_size_t i; RT_ASSERT(bus != RT_NULL); for (i = 0; i < num; i++) { if (msg[i].flags & RT_I2C_NO_START) { // 设置是否需要起始位 } if (msg[i].flags & RT_I2C_ADDR_10BIT) { // 设置成10bit地址工作模式 } else { // 设置成8bit地址工作模式 } if (msg[i].flags & RT_I2C_RD) { // 读操作,读失败跳出 } else { // 写操作,写失败跳出 } } return (rt_ssize_t)i; }
实际上驱动的实现也是按照此思路实现的,根据flags的标记做对应功能的实现,根据传入的数据量决定读写多少数据。
I2C 写入口
rt_ssize_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_ssize_t ret; struct rt_i2c_msg msg; msg.addr = addr; msg.flags = flags; msg.len = count; msg.buf = (rt_uint8_t *)buf; ret = rt_i2c_transfer(bus, &msg, 1); return (ret == 1) ? count : ret; }
从数据上结构上看,写入口仅仅是多开放了一种实现方式,将struct rt_i2c_msg放在函数内部实现,应用是现实不再需要维护这么个结构体,仅仅需要将读写参数以变量的形式传递至rt_i2c_master_send即可
I2C读入口
rt_ssize_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_ssize_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 == 1) ? count : ret; }
读入口,从功能上看,和写入口一致,仅仅是将struct rt_i2c_msg封装,另外,把读标记的操作方式固化下来。
看完这几个入口,其实我们已经可以看出,RTT的I2C框架,目前仅支持主模式工作的I2C,并不支持I2C从模式。另外,还有一个疑问,I2C设备不是注册了几个标准事件接口(i2c_bus_device_read, i2c_bus_device_write, i2c_bus_device_control)吗,怎么感觉完全没用上?实际上,i2c_bus_device_read 和 i2c_bus_device_write这两个接口,从使用上来说,就是调用rtt文档中暴露的 rt_i2c_master_recv 和 rt_i2c_master_send接口,其实现如下:
static rt_ssize_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; RT_ASSERT(bus != RT_NULL); RT_ASSERT(buffer != RT_NULL); LOG_D("I2C bus dev [%s] reading %u bytes.", dev->parent.name, count); addr = pos & 0xffff; flags = (pos >> 16) & 0xffff; return rt_i2c_master_recv(bus, addr, flags, (rt_uint8_t *)buffer, count); } static rt_ssize_t i2c_bus_device_write(rt_device_t dev, rt_off_t pos, const 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; RT_ASSERT(bus != RT_NULL); RT_ASSERT(buffer != RT_NULL); LOG_D("I2C bus dev [%s] writing %u bytes.", dev->parent.name, count); addr = pos & 0xffff; flags = (pos >> 16) & 0xffff; return rt_i2c_master_send(bus, addr, flags, (const rt_uint8_t *)buffer, count); }
I2C控制接口
至于i2c_bus_device_control接口,我们可以通过代码层级来看
rt_err_t rt_i2c_control(struct rt_i2c_bus_device *bus, int cmd, void *args) { rt_err_t ret; if(bus->ops->i2c_bus_control) { ret = bus->ops->i2c_bus_control(bus, cmd, args); return ret; } else { LOG_E("I2C bus operation not supported"); return -RT_EINVAL; } } 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_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: return rt_i2c_control(bus, cmd, args); } return RT_EOK; }
可以看出,在i2c_dev.c这一层,标准化了I2C常用格式的控制逻辑,并将这部分数据在读写I2C时传递至驱动实现。另外,该接口也预留了驱动自定义实现接口的部分,方便各家在实际应用中自定义一些内部参数控制入口。
总结
至此,我们基本上了理清了RTT的硬件I2C框架结构,其设计上的分层逻辑如下:
对于用于应用来说,最好的使用方式是直接调用设备驱动暴露的接口或者是i2s_dev.c中暴露的接口。而对于板载驱动来说,最好的方式是在驱动中调用i2c_core.c中暴露的接口来实现功能。这样便可实现代码的逻辑分层,便于后期代码迭代维护。