STM32软件模拟I2C从机的实现方法 您所在的位置:网站首页 软件IIC STM32软件模拟I2C从机的实现方法

STM32软件模拟I2C从机的实现方法

2023-07-15 06:47| 来源: 网络整理| 查看: 265

 1.1 前言

在使用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)编译器:arm-none-eabi-gcc (GNU Arm Embedded Toolchain 10-2020-q4-major) 10.2.1 20201103 (release)硬件开发板:STM32F429I-DISCO本文对应的例程代码链接如下。 

        STM32软件模拟I2C从机的例程代码-单片机文档类资源-CSDN下载

1.3 软件模拟I2C从机实现方法

这里结合开发板STM32F429I-DISCO上的STM32F429ZI的单片机来演示软件模拟I2C从机的实现方法。

I2C通信的时序图如下图1所示。

​ 图1 I2C通信时序图

I2C通信的时序中关键的几个点如下。

START和ReSTART信号:用于标识I2C通信的开始,时序特点是SCL为高电平的时候,SDA从高电平变成低电平。STOP信号:用于标识I2C通信的结束,时序特点是SCL为高电平的时候,SDA从低电平变成高电平。应答信号:I2C通信每传输完8个比特的数据位后,紧接着需要传输应答标志位,当该位为0时,是ACK应答信号,该位为1时,是NACK无应答信号。应答信号在SCL的第9个时钟周期的位置。数据采集时刻:I2C通信的数据在SCL的上升沿进行采集确认,所以在SCL的高电平期间,数据必须保持不变,防止数据采集出错。当然,START信号和STOP信号的时序在SCL高电平期间是特殊情况,具有专门的含义。数据更新时刻:I2C通信的数据更新需要在SCL为低电平的时候进行。

由于各个关键点基本都发生在SCL或SDA的上升沿或者下降沿的地方,所以可以将用于模拟I2C通信引脚的GPIO口配置成边沿中断,这样就可以通过中断实时抓取边沿信号,并在中断中进行及时的数据处理。使用GPIO的边沿中断来模拟I2C从机的好处是可以实时获取到START和STOP信号,I2C主机发过来的数据可以通过中断得到及时处理,而且程序主流程无需关心模拟I2C从机的相关处理,可以处理其他事务。

因为是I2C从机,所以SCL引脚直接固定成输入引脚即可,而SDA信号由于是双向的,所以需要根据I2C通信中的各个状态来设置输入或输出方向。另外,由于GPIO中断只在GPIO配置成输入时才会产生,所以默认情况下,SDA必须设置成输入引脚。

程序的具体设计思路如下。

将SCL和SDA引脚设置成GPIO的边沿中断模式,默认为输入引脚。I2C通信状态机设置成默认的IDLE状态。SCL的中断用于处理数据的收发,SDA的中断只用于START/ReSTART/STOP这些特殊信号的判断。SDA引脚中断处理思路:发生下降沿中断,并且SCL为高电平,则收到START信号,状态机更新成START状态;发生上升沿中断,并且SCL为高电平,则收到STOP信号,紧接着I2C通信就应该处于空闲状态,所以这里直接将状态机设置成IDLE状态。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通信相关的宏定义的声明。对应的头文件代码如下。

#ifndef __SW_SLAVE_I2C_H_ #define __SW_SLAVE_I2C_H_ #ifdef __cplusplus extern "C" { #endif #include "stm32f4xx_hal.h" #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 #define SET_SCL_DIR(Temp, InOut) \ Temp = SW_SLAVE_SCL_PRT->MODER; \ Temp &= ~(GPIO_MODER_MODER6); \ Temp |= ((InOut & GPIO_MODE_MSK) MODER = temp; #define SET_SDA_DIR(Temp, InOut) \ Temp = SW_SLAVE_SDA_PRT->MODER; \ Temp &= ~(GPIO_MODER_MODER7); \ Temp |= ((InOut & GPIO_MODE_MSK) MODER = Temp; #define CLR_SDA_PIN() (SW_SLAVE_SDA_PRT->BSRR = SW_SLAVE_SDA_PIN BSRR = SW_SLAVE_SDA_PIN) 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; void InitSwSlaveI2C(void); void I2cGpioIsr(void); void CheckSwSlaveI2cTimeout(void); #ifdef __cplusplus } #endif #endif /* __SW_TIMER_H_ */

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从机的所有功能。代码如下。其中EXTI9_5_IRQHandler为STM32外部line9-5中断的入口函数,在该入口函数中调用模拟I2C从机的GPIO口中断处理函数I2cGpioIsr()。

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)) { SwSlaveI2C.State = I2C_STA_NACK; } break; } } } else if(__HAL_GPIO_EXTI_GET_IT(SW_SLAVE_SDA_PIN) != RESET) { __HAL_GPIO_EXTI_CLEAR_IT(SW_SLAVE_SDA_PIN); if((SW_SLAVE_SDA_PRT->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


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

      专题文章
        CopyRight 2018-2019 实验室设备网 版权所有