1.1 前言在使用i2c通信时,一般会用到软件模拟i2c。目前网络上能搜索到的软件模拟i2c一般都是模拟i2c主机,很少有模拟i2c从机的例程。由于i2c主机在进行数据收发时,有明确的可预见性,也就是主机明确知道什么时候要进行数据的收发操作,而且i2c的同步时钟信号也是由主机产生的,所以实现起来相对来说比较简单。而i2c从机的通信受制于主机,即什么时候需要进行数据的收发都是由主机发起的,数据收发的发起时机具有随机性,所以实现方法不能参照软件模拟i2c主机那样使用单纯的软件查询状态的方法。由于实际使用时,mcu的固件还会执行其他的操作,所以如果单纯使用软件查询的方法来判断i2c通信的起始信号不太现实。这里提供一种软件模拟i2c从机的实现方法,考虑使用gpio中断的方法来及时接收i2c通信的起始信号,并进行数据的收发。
1.2 测试平台这里使用的开发环境和相关硬件如下。
操作系统:ubuntu 20.04.2 lts x86_64(使用uname -a命令查看)集成开发环境(ide):eclipse ide for embedded c/c++ developers,version: 2021-06 (4.20.0)硬件开发板:stm32f429i-disco本文对应的例程代码链接如下。https://download.csdn.net/download/goodrenze/85272480
1.3 软件模拟i2c从机实现方法这里结合开发板stm32f429i-disco上的stm32f429zi的单片机来演示软件模拟i2c从机的实现方法。
i2c通信的时序图如下图1所示。
图1 i2c通信时序图
i2c通信的时序中关键的几个点如下。
1)start和restart信号:用于标识i2c通信的开始,时序特点是scl为高电平的时候,sda从高电平变成低电平。
2)stop信号:用于标识i2c通信的结束,时序特点是scl为高电平的时候,sda从低电平变成高电平。
3)应答信号:i2c通信每传输完8个比特的数据位后,紧接着需要传输应答标志位,当该位为0时,是ack应答信号,该位为1时,是nack无应答信号。应答信号在scl的第9个时钟周期的位置。
4)数据采集时刻:i2c通信的数据在scl的上升沿进行采集确认,所以在scl的高电平期间,数据必须保持不变,防止数据采集出错。当然,start信号和stop信号的时序在scl高电平期间是特殊情况,具有专门的含义。
5)数据更新时刻:i2c通信的数据更新需要在scl为低电平的时候进行。
通过以上几个关键点,软件模拟i2c从机的基本思路就有了。由于各个关键点基本都发生在scl或sda的上升沿或者下降沿的地方,所以可以将用于模拟i2c通信引脚的gpio口配置成边沿中断,这样就可以通过中断实时抓取边沿信号,并在中断中进行及时的数据处理。使用gpio的边沿中断来模拟i2c从机的好处是可以实时获取到start和stop信号,i2c主机发过来的数据可以通过中断得到及时处理,而且程序主流程无需关心模拟i2c从机的相关处理,可以处理其他事务。
因为是i2c从机,所以scl引脚直接固定成输入引脚即可,而sda信号由于是双向的,所以需要根据i2c通信中的各个状态来设置输入或输出方向。另外,由于gpio中断只在gpio配置成输入时才会产生,所以默认情况下,sda必须设置成输入引脚。
程序的具体设计思路如下。
1)将scl和sda引脚设置成gpio的边沿中断模式,默认为输入引脚。i2c通信状态机设置成默认的idle状态。scl的中断用于处理数据的收发,sda的中断只用于start/restart/stop这些特殊信号的判断。
2)sda引脚中断处理思路:发生下降沿中断,并且scl为高电平,则收到start信号,状态机更新成start状态;发生上升沿中断,并且scl为高电平,则收到stop信号,紧接着i2c通信就应该处于空闲状态,所以这里直接将状态机设置成idle状态。
3)scl引脚中断处理思路:
a. 发生下降沿中断时
a1. 如果状态机为start状态,则i2c通信正式开始,准备开始接收设备地址,状态机更新成data状态。
a2. 如果状态机为data状态,scl下降沿计数小于8时,如果是主机读取数据,则更新sda的位数据输出。scl下降沿计数等于8时,进入应答阶段,状态机更新成ack状态;如果是主机写入数据,并且是设备地址数据,则判断设备地址是否匹配,如果设备地址匹配,则将sda设置成输出,并输出ack信号,否则如果地址不匹配,则sda保持为输入状态,不输出ack信号;如果是主机读取数据,将sda设置成输入,准备接收主机的应答信号。
a3. 如果状态机为ack状态,这时应答信号已经传输完毕,状态机更新成data状态,准备继续接收或发送数据。如果是主机写入数据,将sda设置成输入,继续接收后续数据;如果是主机读取数据,将sda设置成输出,继续发送后续数据。
a4. 如果状态机为nack状态,说明紧接着i2c通信将停止或重新启动,准备接收stop或者restart信号,所以需要将sda设置成输入。此时状态机状态保持不变。
b. 发生上升沿中断时
b1. 如果状态机为data状态,i2c通信处于数据阶段,如果是主机写入数据,则采集主机通过sda发送过来的位数据。
b2. 如果状态机为ack状态,i2c通信处于应答阶段,如果是主机读取数据,则采集主机的应答信号,如果主机应答信号为1,说明主机发送了nack的应答,状态机需要更新成nack状态,准备接收停止或重新启动信号。
1.4 软件模拟i2c从机的代码实现根据上面的程序思路,可以开始进行程序代码的设计,步骤如下。
1)设计i2c从机通信对应的结构体,i2c通信状态定义,i2c通信相关的宏定义的声明。部分代码如下。
// ...#define sw_slave_addr 0xa2#define sw_slave_scl_clk_en() __hal_rcc_gpiob_clk_enable()#define sw_slave_sda_clk_en() __hal_rcc_gpiob_clk_enable()#define sw_slave_scl_prt gpiob#define sw_slave_scl_pin gpio_pin_6#define sw_slave_sda_prt gpiob#define sw_slave_sda_pin gpio_pin_7#define gpio_mode_msk 0x00000003u#define i2c_sta_idle 0#define i2c_sta_start 1#define i2c_sta_data 2#define i2c_sta_ack 3#define i2c_sta_nack 4#define i2c_sta_stop 5#define i2c_read 1#define i2c_write 0#define gpio_dir_in 0#define gpio_dir_out 1// ...typedef struct _swslavei2c_t{ uint8_t state; // i2c通信状态 uint8_t rw; // i2c读写标志:0-写,1-读 uint8_t sclfallcnt; // scl下降沿计数 uint8_t flag; // i2c状态标志,bit0:0-地址无效,1-地址匹配 uint32_t startms; // i2c通信起始时间,单位ms,用于判断通信是否超时 uint8_t* rxbuf; // 指向接收缓冲区的指针 uint8_t* txbuf; // 指向发送缓冲区的指针 uint8_t rxidx; // 接收缓冲区数据写入索引,最大值255 uint8_t txidx; // 发送缓冲区数据读取索引,最大值255}swslavei2c_t;extern swslavei2c_t swslavei2c;// ...2)i2c通信引脚scl/sda对应的gpio的初始化。这里使用pb6/pb7引脚。代码如下。
void initswslavei2c(void){ gpio_inittypedef gpio_initstructure; /* enable i2c gpio clock */ sw_slave_scl_clk_en(); sw_slave_sda_clk_en(); /* configure scl gpio pin */ gpio_initstructure.pin = sw_slave_scl_pin; gpio_initstructure.mode = gpio_mode_output_od; gpio_initstructure.pull = gpio_pullup; gpio_initstructure.speed = gpio_speed_fast; hal_gpio_init(sw_slave_scl_prt, &gpio_initstructure); /* configure sda gpio pin */ gpio_initstructure.pin = sw_slave_sda_pin; hal_gpio_init(sw_slave_sda_prt, &gpio_initstructure); /* configure scl gpio pin as input interruption with pull up */ gpio_initstructure.pin = sw_slave_scl_pin; gpio_initstructure.mode = gpio_mode_it_rising_falling; hal_gpio_init(sw_slave_scl_prt, &gpio_initstructure); /* configure sda gpio pin as input interruption with pull up */ gpio_initstructure.pin = sw_slave_sda_pin; hal_gpio_init(sw_slave_sda_prt, &gpio_initstructure); /* enable and set exti line9_5 interrupt to the highest priority */ hal_nvic_setpriority(exti9_5_irqn, 0, 0); hal_nvic_enableirq(exti9_5_irqn);}3)由于scl/sda引脚被设置成中断引脚,需要实现gpio的中断处理函数。中断处理函数中已经包含了软件模拟i2c从机的所有功能。代码如下。
void exti9_5_irqhandler(void){ i2cgpioisr();}void i2cgpioisr(void){ uint32_t temp; // 处理scl的上下沿中断 if(__hal_gpio_exti_get_it(sw_slave_scl_pin) != reset) { __hal_gpio_exti_clear_it(sw_slave_scl_pin); // 更新通信起始时间 swslavei2c.startms = hal_gettick(); // scl的下降沿事件处理,此时需要更新要传输的数据 if((sw_slave_scl_prt->idr & sw_slave_scl_pin) == (uint32_t)gpio_pin_reset) { switch(swslavei2c.state) { case i2c_sta_start: // 起始信号的下降沿,初始化相关参数并转到接收比特数据状态 swslavei2c.sclfallcnt = 0; swslavei2c.rxidx = 0; swslavei2c.txidx = 0; swslavei2c.flag = 0; // 默认地址不匹配 swslavei2c.rxbuf[swslavei2c.rxidx] = 0; swslavei2c.rw = i2c_write; // 第1字节为设备地址,一定是写入 swslavei2c.state = i2c_sta_data; break; case i2c_sta_data: swslavei2c.sclfallcnt++; if(8 > swslavei2c.sclfallcnt) { // 如果是主机读取数据,则在scl低电平时更新比特数据 if(swslavei2c.rw == i2c_read) { if(swslavei2c.txbuf[swslavei2c.txidx] & (1 < swslavei2c.sclfallcnt)) { if(sw_slave_sda_prt->idr & sw_slave_sda_pin) { swslavei2c.rxbuf[swslavei2c.rxidx] |= (1
idr & sw_slave_sda_pin) == (uint32_t)gpio_pin_reset) { // scl为高电平时,sda从高变低,说明是起始信号 if(sw_slave_scl_prt->idr & sw_slave_scl_pin) { swslavei2c.state = i2c_sta_start; } } else { // scl为高电平时,sda从低变高,说明是停止信号,一次i2c通信结束,直接将状态设置成空闲 if(sw_slave_scl_prt->idr & sw_slave_scl_pin) { swslavei2c.state = i2c_sta_idle; } } }}4)为了确保模拟i2c从机通信的可靠性,额外设计了i2c通信超时处理函数。在i2c通信进行的过程中,如果通信出现了中断,则通过超时判断来重置i2c从机状态,确保出现通信异常时可以从异常状态中自动恢复。该函数需要在主流程中调用。代码如下。
void checkswslavei2ctimeout(void){ uint32_t timems, timecurms; if(swslavei2c.state != i2c_sta_idle) { timecurms = hal_gettick(); if(timecurms >= swslavei2c.startms) { timems = timecurms - swslavei2c.startms; } else { timems = ~(swslavei2c.startms - timecurms) + 1; } if(500 <= timems) { // i2c通信超时的话,重置状态机,并把sda设置成输入 swslavei2c.state = i2c_sta_idle; set_sda_dir(timems, gpio_dir_in); } }}5)软件模拟i2c从机相关功能验证代码。这里需要借助stm32的另外一个i2c主机进行配合测试。这里将pf0/pf1对应的引脚配置成i2c主机,主机直接使用stm32的硬件i2c实现。pf0/pf1分别和pb7/pb6连接,然后验证数据收发的正确性。具体代码参见上面的工程链接。这里只展示最终的测试结果数据。如下图所示。
软件模拟i2c从机状态
i2c主机发送数据
软件模拟i2c从机接收数据
图2 软件模拟i2c从机数据接收验证结果
软件模拟i2c从机状态
软件模拟i2c从机发送数据
i2c主机接收数据
图3 软件模拟i2c从机数据发送验证结果
1.5 软件模拟i2c从机的注意事项本例程中,对于400kbps速率的i2c通信,在进行代码编译链接时,需要使用-ofast的优化方式,以提高中断处理函数的执行速度,使程序能正确执行。如果使用默认的无优化配置,会造成程序无法正确运行。
对于主频比较低的mcu,使用这里提供的软件模拟i2c从机进行i2c通信时,建议使用100kpbs以下的通信速率,并且注意使用可以提高代码执行速度的代码优化配置。
另外,建议将用于模拟sda/scl的gpio引脚中断优先级设置成最高,以便能及时响应i2c通信时序的中断。
金升阳推出66-160VDC输入铁路电源 输出功率可达400W
物联网技术研究与应用案例分析
大数据及人工智能时代的3项关键技术
利用电分析和纳米尺度表征方法证明锂电镀形貌和电流密度高度相关
什么叫断路器失灵保护 断路器失灵保护是什么
软件模拟I2C从机的实现方法及注意事项
新能源汽车废旧动力电池精细拆解设备工艺
TOP204单片开关电源IC的原理及应用
我国学者研究发现现存最古老天文观测仪器
分析当前我国整车电磁兼容的主要技术要求及当前标准存在的问题
简单了解单晶硅片基本的加工过程
打入工业互联网,5G需要99.9999%的确定性
vivoX23幻彩版体验 同级别产品难寻敌手
蒸汽相氟化氢对晶片清洗和氧化物刻蚀的表征
PLC存储器的5种型号及特点介绍
电子式高分子湿度传感器的结构与原理
混合动力汽车各工况分析 (上)
三电平逆变器IGBT驱动电路电磁兼容研究
电动车的电池安装注意事项
2021年彩电行业利好不断,海外市场全面增长