背景
前面分析硬件I2C框架时,提到了个人不认可在方案不支持硬件I2C时,通过在驱动层用硬件I2C的接口实现软件I2C的逻辑。刚好看RTT框架层中,有软件I2C的对应实现,因此开始分析此接口,以期在后续使用中直接使用该接口。
功能实现分析
软件I2C注册
在查看I2C设备注册入口时,偶然发现组件层也有调用I2C设备注册入口,很好奇为何会有此入口,便顺势查看了这个入口的调用关系:
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_PRIO); if (bus->timeout == 0) bus->timeout = RT_TICK_PER_SECOND; 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; } static const struct rt_i2c_bus_device_ops i2c_bit_bus_ops = { i2c_bit_xfer, RT_NULL, RT_NULL }; 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); } /* I2C initialization function */ int rt_soft_i2c_init(void) { int err = RT_EOK; struct rt_soft_i2c *obj; int i; for(i = 0; i < sizeof(i2c_bus_obj) / sizeof(i2c_bus_obj[0]); i++) { struct soft_i2c_config *cfg = &i2c_cfg[i]; pin_init(cfg); obj = &i2c_bus_obj[i]; obj->ops = soft_i2c_ops; obj->ops.data = cfg; obj->i2c_bus.priv = &obj->ops; obj->ops.delay_us = cfg->timing_delay; obj->ops.timeout = cfg->timing_timeout; if(rt_i2c_bit_add_bus(&obj->i2c_bus, cfg->bus_name) == RT_EOK) { i2c_bus_unlock(cfg); LOG_D("Software simulation %s init done" ", SCL pin: 0x%02X, SDA pin: 0x%02X" , cfg->bus_name , cfg->scl_pin , cfg->sda_pin ); } else { err++; LOG_E("Software simulation %s init fail" ", SCL pin: 0x%02X, SDA pin: 0x%02X" , cfg->bus_name , cfg->scl_pin , cfg->sda_pin ); } } return err; } INIT_PREV_EXPORT(rt_soft_i2c_init);
当反查到函数INIT_PREV_EXPORT(rt_soft_i2c_init);时,已经可以通过函数名确认了,这个实现是软件I2C的实现,再进一步的查,可以发现以下信息:
if GetDepend('RT_USING_SOFT_I2C'): src = src + ['soft_i2c.c']
也就是说,只要在.config中使能宏RT_USING_SOFT_I2C便可启用该功能,而此功能的选现在组件的Kconfig中有维护。
if RT_USING_I2C config RT_I2C_DEBUG bool "Use I2C debug message" default n config RT_USING_I2C_BITOPS bool "Use GPIO to simulate I2C" default y if RT_USING_I2C_BITOPS config RT_I2C_BITOPS_DEBUG bool "Use simulate I2C debug message" default n endif config RT_USING_SOFT_I2C bool "Use GPIO to soft simulate I2C" default n select RT_USING_PIN select RT_USING_I2C_BITOPS if RT_USING_SOFT_I2C config RT_USING_SOFT_I2C1 bool "Enable I2C1 Bus (software simulation)" default y if RT_USING_SOFT_I2C1 config RT_SOFT_I2C1_SCL_PIN int "SCL pin number" range 0 32767 default 1 config RT_SOFT_I2C1_SDA_PIN int "SDA pin number" range 0 32767 default 2 config RT_SOFT_I2C1_BUS_NAME string "Bus name" default "i2c1" config RT_SOFT_I2C1_TIMING_DELAY int "Timing delay (us)" range 0 32767 default 10 config RT_SOFT_I2C1_TIMING_TIMEOUT int "Timing timeout (tick)" range 0 32767 default 10 endif config RT_USING_SOFT_I2C2 bool "Enable I2C2 Bus (software simulation)" default n if RT_USING_SOFT_I2C2 config RT_SOFT_I2C2_SCL_PIN int "SCL pin number" range 0 32767 default 3 config RT_SOFT_I2C2_SDA_PIN int "SDA pin number" range 0 32767 default 4 config RT_SOFT_I2C2_BUS_NAME string "Bus name" default "i2c2" config RT_SOFT_I2C2_TIMING_DELAY int "Timing delay (us)" range 0 32767 default 10 config RT_SOFT_I2C2_TIMING_TIMEOUT int "Timing timeout (tick)" range 0 32767 default 10 endif config RT_USING_SOFT_I2C3 bool "Enable I2C3 Bus (software simulation)" default n if RT_USING_SOFT_I2C3 config RT_SOFT_I2C3_SCL_PIN int "SCL pin number" range 0 32767 default 5 config RT_SOFT_I2C3_SDA_PIN int "SDA pin number" range 0 32767 default 6 config RT_SOFT_I2C3_BUS_NAME string "Bus name" default "i2c3" config RT_SOFT_I2C3_TIMING_DELAY int "Timing delay (us)" range 0 32767 default 10 config RT_SOFT_I2C3_TIMING_TIMEOUT int "Timing timeout (tick)" range 0 32767 default 10 endif config RT_USING_SOFT_I2C4 bool "Enable I2C4 Bus (software simulation)" default n if RT_USING_SOFT_I2C4 config RT_SOFT_I2C4_SCL_PIN int "SCL pin number" range 0 32767 default 7 config RT_SOFT_I2C4_SDA_PIN int "SDA pin number" range 0 32767 default 8 config RT_SOFT_I2C4_BUS_NAME string "Bus name" default "i2c4" config RT_SOFT_I2C4_TIMING_DELAY int "Timing delay (us)" range 0 32767 default 10 config RT_SOFT_I2C4_TIMING_TIMEOUT int "Timing timeout (tick)" range 0 32767 default 10 endif config RT_USING_SOFT_I2C5 bool "Enable I2C5 Bus (software simulation)" default n if RT_USING_SOFT_I2C5 config RT_SOFT_I2C5_SCL_PIN int "SCL pin number" range 0 32767 default 9 config RT_SOFT_I2C5_SDA_PIN int "SDA pin number" range 0 32767 default 10 config RT_SOFT_I2C5_BUS_NAME string "Bus name" default "i2c5" config RT_SOFT_I2C5_TIMING_DELAY int "Timing delay (us)" range 0 32767 default 10 config RT_SOFT_I2C5_TIMING_TIMEOUT int "Timing timeout (tick)" range 0 32767 default 10 endif config RT_USING_SOFT_I2C6 bool "Enable I2C6 Bus (software simulation)" default n if RT_USING_SOFT_I2C6 config RT_SOFT_I2C6_SCL_PIN int "SCL pin number" range 0 32767 default 11 config RT_SOFT_I2C6_SDA_PIN int "SDA pin number" range 0 32767 default 12 config RT_SOFT_I2C6_BUS_NAME string "Bus name" default "i2c6" config RT_SOFT_I2C6_TIMING_DELAY int "Timing delay (us)" range 0 32767 default 10 config RT_SOFT_I2C6_TIMING_TIMEOUT int "Timing timeout (tick)" range 0 32767 default 10 endif config RT_USING_SOFT_I2C7 bool "Enable I2C7 Bus (software simulation)" default n if RT_USING_SOFT_I2C7 config RT_SOFT_I2C7_SCL_PIN int "SCL pin number" range 0 32767 default 13 config RT_SOFT_I2C7_SDA_PIN int "SDA pin number" range 0 32767 default 14 config RT_SOFT_I2C7_BUS_NAME string "Bus name" default "i2c7" config RT_SOFT_I2C7_TIMING_DELAY int "Timing delay (us)" range 0 32767 default 10 config RT_SOFT_I2C7_TIMING_TIMEOUT int "Timing timeout (tick)" range 0 32767 default 10 endif config RT_USING_SOFT_I2C8 bool "Enable I2C8 Bus (software simulation)" default n if RT_USING_SOFT_I2C8 config RT_SOFT_I2C8_SCL_PIN int "SCL pin number" range 0 32767 default 15 config RT_SOFT_I2C8_SDA_PIN int "SDA pin number" range 0 32767 default 16 config RT_SOFT_I2C8_BUS_NAME string "Bus name" default "i2c8" config RT_SOFT_I2C8_TIMING_DELAY int "Timing delay (us)" range 0 32767 default 10 config RT_SOFT_I2C8_TIMING_TIMEOUT int "Timing timeout (tick)" range 0 32767 default 10 endif endif endif
此Kconfig路径为: ./components/drivers/Kconfig
而从注册函数的实现上看,我们可以看出,本质上RTT所实现的软件I2C和在驱动层套用硬件I2C框架实现软件I2C的思路是一致的,唯一的区别是,把这套实现挪至组件层,那所有不支持硬件I2C的器件都能直接实现软件I2C,而不需要再去单独写一套软件I2C的实现。而既然弄清楚了软件I2C的注册过程,其实软件I2C的使用方法也很清晰了,各层的调用逻辑和硬件I2C逻辑一致,唯一的区别可能就是软件I2C的设备名与硬件I2C的设备名一致。
软件I2C实现逻辑
软件I2C的实现,无非就是实现以下几个接口:
SetSCL
SetSDA
GetSDA
Delay
在这个接口的基础上,封装以下几个接口:
Start
Stop
ReadAck
SendBit
需要注意的是:ReadAck需要一个超时机制,以防止从机无响应而死等。
最后基于这些接口上,实现以下接口:
Read
Write
而交给应用层的接口,其实仅仅是把Read和Write接口以某种形式暴露给上层使用。
我们可以按照这么个思路去套用,看RTT的软件I2C框架是否与该思路一致:
最底层接口
// 最底层的IO操作定义 // 至于ops对应的接口定义,在 rt_soft_i2c_init有定义,具体可见文件soft_i2c.c #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); } // 设置输出高低电平 #define SDA_L(ops) SET_SDA(ops, 0) #define SDA_H(ops) SET_SDA(ops, 1) #define SCL_L(ops) SET_SCL(ops, 0) static rt_err_t SCL_H(struct rt_i2c_bit_ops *ops) { rt_tick_t start; SET_SCL(ops, 1); if (!ops->get_scl) goto done; start = rt_tick_get(); while (!GET_SCL(ops)) // 由于在框架里,IO口被初始化成开漏输出,因此置高需要等待外部拉高,因此需要等待外部实际变高 { if ((rt_tick_get() - start) > ops->timeout) return -RT_ETIMEOUT; i2c_delay(ops); } #ifdef RT_I2C_BITOPS_DEBUG if (rt_tick_get() != start) { LOG_D("wait %ld tick for SCL line to go high", rt_tick_get() - start); } #endif done: i2c_delay(ops); return RT_EOK; }
中间层接口
static void i2c_start(struct rt_i2c_bit_ops *ops) { // 起始信号,SDA低电平后SCL低电平 #ifdef RT_I2C_BITOPS_DEBUG if (ops->get_scl && !GET_SCL(ops)) { LOG_E("I2C bus error, SCL line low"); } if (ops->get_sda && !GET_SDA(ops)) { LOG_E("I2C bus error, SDA line low"); } #endif SDA_L(ops); i2c_delay(ops); SCL_L(ops); } static void i2c_restart(struct rt_i2c_bit_ops *ops) { // I2C复位操作, SDA和SCL全高后再发出起始波形,其实起始波形可以复用i2c_start SDA_H(ops); SCL_H(ops); i2c_delay(ops); SDA_L(ops); i2c_delay(ops); SCL_L(ops); } static void i2c_stop(struct rt_i2c_bit_ops *ops) { //I2C停止波形,由于I2C通信的最后一个波形是等待从机拉低,因此若未等到从机拉低时,需主动拉低保证波形符合标准,因此先拉低SDA后,再拉高SCL,最后拉高SDA,保证波形为停止波形。 SDA_L(ops); i2c_delay(ops); SCL_H(ops); i2c_delay(ops); SDA_H(ops); i2c_delay2(ops); } rt_inline rt_bool_t i2c_waitack(struct rt_i2c_bit_ops *ops) { //等待ACK信号,在8bit数据发送完毕后,从机端若收到信息,则需要在第九个时钟位置拉低SDA // 主机端读到高电平判定为NACK,低电平判定位ACK // 这个函数并没有SDA输入输出态转换的操作,原因是在soft_i2c.c的pin_init中,框架直接将IO口设置为开漏输出, // 也就是说,这个管脚硬件需接上拉,而管脚的输出低电平,本质上是将接口引到地上,输出高电平,本质上是变成输入高阻态 // 另外,为何在拉高SCL的时候去读SDA,这操作没看明白,个人理解实际波形为低电平时去读取 rt_bool_t ack; SDA_H(ops); i2c_delay(ops); if (SCL_H(ops) < 0) { LOG_W("wait ack timeout"); return -RT_ETIMEOUT; } ack = !GET_SDA(ops); /* ACK : SDA pin is pulled low */ LOG_D("%s", ack ? "ACK" : "NACK"); SCL_L(ops); return ack; } static rt_int32_t i2c_writeb(struct rt_i2c_bus_device *bus, rt_uint8_t data) { // 写一个字节,由低到高的顺序写完8bit后,拉低SCL再去读SDA rt_int32_t i; rt_uint8_t bit; struct rt_i2c_bit_ops *ops = (struct rt_i2c_bit_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) { LOG_D("i2c_writeb: 0x%02x, " "wait scl pin high timeout at bit %d", data, i); 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) {//读取一个字节的数据,拉高SDA后,由低到高的顺序读取8bit rt_uint8_t i; rt_uint8_t data = 0; struct rt_i2c_bit_ops *ops = (struct rt_i2c_bit_ops *)bus->priv; SDA_H(ops); i2c_delay(ops); for (i = 0; i < 8; i++) { data <<= 1; if (SCL_H(ops) < 0) { LOG_D("i2c_readb: wait scl pin high " "timeout at bit %d", 7 - i); return -RT_ETIMEOUT; } if (GET_SDA(ops)) data |= 1; SCL_L(ops); i2c_delay2(ops); } return data; }
Read Write接口
static rt_ssize_t i2c_send_bytes(struct rt_i2c_bus_device *bus, struct rt_i2c_msg *msg) { // 写msg->len个字节 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) { LOG_D("send bytes: NACK."); return 0; } else { LOG_E("send bytes: error %d", ret); return ret; } } return bytes; } static rt_err_t i2c_send_ack_or_nack(struct rt_i2c_bus_device *bus, int ack) { // 连续读时,需要在读完一个字节时读取一次ack标记,若标记为ack,则继续读,若标记为nack,则停止读取,因此有这函数的实现 struct rt_i2c_bit_ops *ops = (struct rt_i2c_bit_ops *)bus->priv; if (ack) SET_SDA(ops, 0); i2c_delay(ops); if (SCL_H(ops) < 0) { LOG_E("ACK or NACK timeout."); return -RT_ETIMEOUT; } SCL_L(ops); return RT_EOK; } static rt_ssize_t i2c_recv_bytes(struct rt_i2c_bus_device *bus, struct rt_i2c_msg *msg) { // 连续读入口,每读完一个字节便读取一下ack状态,并决定是否继续读写 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 --; LOG_D("recieve bytes: 0x%02x, %s", val, (flags & RT_I2C_NO_READ_ACK) ? "(No ACK/NACK)" : (count ? "ACK" : "NACK")); if (!(flags & RT_I2C_NO_READ_ACK)) { val = i2c_send_ack_or_nack(bus, count); if (val < 0) return val; } } return bytes; } static rt_int32_t i2c_send_address(struct rt_i2c_bus_device *bus, rt_uint8_t addr, rt_int32_t retries) { // 正常I2C读取的时候,地址发送失败后不会再去尝试 // 但rtt的框架中,增加了这个操作,发现读地址失败后,尝试多次读取,读取次数由retries决定 struct rt_i2c_bit_ops *ops = (struct rt_i2c_bit_ops *)bus->priv; rt_int32_t i; rt_err_t ret = 0; for (i = 0; i <= retries; i++) { ret = i2c_writeb(bus, addr); if (ret == 1 || i == retries) break; LOG_D("send stop condition"); i2c_stop(ops); i2c_delay2(ops); LOG_D("send start condition"); i2c_start(ops); } return ret; } static rt_err_t i2c_bit_send_address(struct rt_i2c_bus_device *bus, struct rt_i2c_msg *msg) { rt_uint16_t flags = msg->flags; rt_uint16_t ignore_nack = msg->flags & RT_I2C_IGNORE_NACK; struct rt_i2c_bit_ops *ops = (struct rt_i2c_bit_ops *)bus->priv; rt_uint8_t addr1, addr2; rt_int32_t retries; rt_err_t ret; retries = ignore_nack ? 0 : bus->retries; if (flags & RT_I2C_ADDR_10BIT) { // 发送10位地址的方式,本质上是发送两个字节的数据,没数据的位默认置1,10bit模式,如果是读,则后面补了个读的操作 addr1 = 0xf0 | ((msg->addr >> 7) & 0x06); addr2 = msg->addr & 0xff; LOG_D("addr1: %d, addr2: %d", addr1, addr2); ret = i2c_send_address(bus, addr1, retries); if ((ret != 1) && !ignore_nack) { LOG_W("NACK: sending first addr"); return -RT_EIO; } ret = i2c_writeb(bus, addr2); if ((ret != 1) && !ignore_nack) { LOG_W("NACK: sending second addr"); return -RT_EIO; } if (flags & RT_I2C_RD) { LOG_D("send repeated start condition"); i2c_restart(ops); addr1 |= 0x01; ret = i2c_send_address(bus, addr1, retries); if ((ret != 1) && !ignore_nack) { LOG_E("NACK: sending repeated addr"); return -RT_EIO; } } } else { //7bit操作,如果是读,则直接把读写位标记了,然后发出地址 /* 7-bit addr */ addr1 = msg->addr << 1; if (flags & RT_I2C_RD) addr1 |= 1; ret = i2c_send_address(bus, addr1, retries); if ((ret != 1) && !ignore_nack) return -RT_EIO; } return RT_EOK; } static rt_ssize_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 = (struct rt_i2c_bit_ops *)bus->priv; rt_int32_t ret; rt_uint32_t i; rt_uint16_t ignore_nack; if((ops->i2c_pin_init_flag == RT_FALSE) && (ops->pin_init != RT_NULL)) { // 如果模拟I2C从未被使用过,则使用前初始化io口并置已初始化标记,前提是接口被注册 // 不过很可惜,这个接口并未在soft_i2c.c中定义,而是在rt_soft_i2c_init调用时直接初始化了 ops->pin_init(); ops->i2c_pin_init_flag = RT_TRUE; } if (num == 0) return 0; 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); } else { LOG_D("send start condition"); i2c_start(ops); } ret = i2c_bit_send_address(bus, msg); if ((ret != RT_EOK) && !ignore_nack) { LOG_D("receive NACK from device addr 0x%02x msg %d", msgs[i].addr, i); goto out; } } if (msg->flags & RT_I2C_RD) //如果是读,则调用读接口,否则调用写接口 { ret = i2c_recv_bytes(bus, msg); if (ret >= 1) { LOG_D("read %d byte%s", 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) { LOG_D("write %d byte%s", ret, ret == 1 ? "" : "s"); } if (ret < msg->len) { if (ret >= 0) ret = -RT_ERROR; goto out; } } } ret = i; out: if (!(msg->flags & RT_I2C_NO_STOP)) //如果操作完毕后存在停止命令,则发送停止波形 { LOG_D("send stop condition"); i2c_stop(ops); } return ret; }
从实现思路上看,RTT的软件I2C实现思路与通用做法基本一致,其中 i2c-bit-ops.c封装了通用思路中的顶层和中间层,而 soft_i2c.c则实现了最基础的底层接口。
总结
分析了那么多,其实还漏了一个最关键的点,如何使用软件I2C,这里简要说一下步骤:
a.命令行执行menuconfig后,去component目录下找到driver层,在此层找到软件I2C开关并使能
b.使能软件I2C时,需配置对应的SDA和SCL对应的gpio管脚编号,根据以往的经验,各家对GPIO编号的规则都有差异,但最终都是转换成十六进制数以符合RTT的gpio框架,因此此处仅需将对应管脚的编号换算成对应的十进制数写进去即可
c.设置好延时时间,这个延时时间其实就是对应的软件I2C的总线速度
d.scons --target=mdk5 mdk5也可以是其他平台,这里只是举个例子
e.打开对应工程,编写I2C使用代
另外,RTT的软件I2C框架默认芯片的GPIO支持开漏模式,因此使用RTT的软件I2C,需要gpio驱动实现开漏模式。实际上,不支持开漏模式的GPIO也可以用作模拟I2C,但操作上会稍微复杂一些,且需要修改RTT的模拟I2C框架。此外若实现了不支持开漏模式的模拟I2C,外部I2C主设备便不能并入监控设备状态了。