背景
前面分析硬件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主设备便不能并入监控设备状态了。
我要赚赏金
