介绍
本项目是利用GPIO模拟I2C的从机
网上常见的是模拟I2C主机
本项目是作为一个两个单片机之间低速通信的用法
协议介绍请看,传送门
从机
功能
实现I2C从机端读写寄存器
编程思路
I2C的从机实现比起主机来麻烦一些
因为SCL的时序是由主机发送,从机需要响应
注意:整个过程不考虑应答码
思路是检测SCL与SDA的边沿(上升沿和下降沿)中断
在SDA的边沿检测SCL的电平,如果SCL为高电平,则根据协议开始(SDA上升沿),或结束(SDA下降沿)I2C通信,在SCL低电平则无需动作
在SCL上升沿是检测来自SDA的数据(来自主机),SCL下降沿通过SDA发送数据(发给主机)
整体使用状态机的思想:
- 在SDA上升沿,SCL高电平时进入空闲态(0),之后转入准备态(1)
- 在SCL下降沿时,清空中间变量的数据,转入器件地址解码(2)
- 在之后的8个上升沿是数据,第9个上升沿是应答吗,这里用于状态切换,如果是写入器件地址(最低位为0)则转移为寄存器读取态(3),如果是读取器件地址(最低位为1)则转换为数据发送态(5)
- 如果寄存器读取态(3)则同样在SCL上升沿动作,读取8个上升沿的数据,当作寄存器地址,并转移为数据读取态(4)
- 数据读取态(4),还是在SCL上升沿动作,读取数据放入缓冲区即可
- 这边说一下器件地址是读取器件地址时寄存器地址的获取手段,因为发送寄存器地址是按照写入器件地址发送的,之后需要再发送一次读取器件地址,这时又触发了一次起始信号,因此可以即在第一次发送写入器件地址并转移到寄存器读取态(3)读取寄存器地址后被打断了,重新进入了 准备态(1)->器件地址解码(2)->数据发送态(5)
- 数据发送态(5),此态是在SCL下降沿动作的(在SCL低电平时改变SDA),按顺序依次发送数据即可
- 结束后进入空闲态,等待下次触发
HAL设置
需要设置两个GPIO的上升沿和下降沿中断
如下图,设置为边沿中断,上拉
程序
GPIO基本输出函数
#define I2C_Address 0x54
#define I2C_SCL_GPIOx GPIOA
#define I2C_SCL_Pin GPIO_PIN_0
#define I2C_SDA_GPIOx GPIOA
#define I2C_SDA_Pin GPIO_PIN_1
/**
* @brief 一段延迟
* @param 无
* @return 无
* @author HZ12138
* @date 2022-07-27 08:53:30
*/
void I2C_Delay(void)
{
int z = 0xff;
while (z--)
;
}
/**
* @brief 写SDA
* @param H_L:高低电平
* @return 无
* @author HZ12138
* @date 2022-10-21 18:07:18
*/
void I2C_Write_SDA(GPIO_PinState H_L)
{
HAL_GPIO_WritePin(I2C_SDA_GPIOx, I2C_SDA_Pin, H_L);
}
/**
* @brief 写SCL
* @param H_L:高低电平
* @return 无
* @author HZ12138
* @date 2022-10-21 18:07:40
*/
void I2C_Write_SCL(GPIO_PinState H_L)
{
HAL_GPIO_WritePin(I2C_SCL_GPIOx, I2C_SCL_Pin, H_L);
}
/**
* @brief 读取SDA
* @param 无
* @return SDA的状态
* @author HZ12138
* @date 2022-10-21 18:07:56
*/
uint16_t I2C_Read_SDA(void)
{
return HAL_GPIO_ReadPin(I2C_SDA_GPIOx, I2C_SDA_Pin);
}
/**
* @brief 读取SCL
* @param 无
* @return SDA的状态
* @author HZ12138
* @date 2022-10-21 18:07:56
*/
uint16_t I2C_Read_SCL(void)
{
return HAL_GPIO_ReadPin(I2C_SCL_GPIOx, I2C_SCL_Pin);
}
切换GPIO模式
首先我们分析一下GPIO的模式
SCL一直保持边沿中断即可
但SDA需要在空闲和主机写(从机接收数据)状态保持边沿中断,而在主机读(从机发送数据)在边沿中断和开漏上拉输出状态切换
为了让通信速率不算太低,此处切换需要较高速率,不建议使用HAL函数
这边选择直接操作寄存器来实现
具体请参考这篇博客,传送门
#define I2C_SDA_Pinx 1 // GPIO_PIN_x 写x
/**
* @brief 设置SDA为中断模式
* @param 无
* @return 无
* @author HZ12138
* @date 2022-12-29 19:56:29
*/
void I2C_Slave_Set_SDA_IT(void)
{
I2C_SDA_GPIOx->MODER &= ~(3 << (I2C_SDA_Pinx * 2));
I2C_SDA_GPIOx->MODER |= 0 << I2C_SDA_Pinx * 2;
}
/**
* @brief 设置SDA为开漏上拉输出
* @param 无
* @return 无
* @author HZ12138
* @date 2022-12-29 19:56:54
*/
void I2C_Slave_Set_SDA_Out(void)
{
I2C_SDA_GPIOx->MODER &= ~(3 << (I2C_SDA_Pinx * 2));
I2C_SDA_GPIOx->MODER |= 1 << I2C_SDA_Pinx * 2;
}
SDA和SCL边沿服务函数
本函数主要用于区分上升和下降沿
/**
* @brief 在SCL中断服务函数中调用
* @param 无
* @return 无
* @author HZ12138
* @date 2022-12-29 21:28:32
*/
void I2C_Slave_IRQ_SCL(void)
{
if (I2C_Read_SCL() == GPIO_PIN_SET)
{ // 上升沿
I2C_Slave_IRQ_SCL_Rising();
}
else
{ // 下降沿
I2C_Slave_IRQ_SCL_Falling();
}
}
/**
* @brief 在SDA中断服务函数中调用
* @param 无
* @return 无
* @author HZ12138
* @date 2022-12-29 21:28:32
*/
void I2C_Slave_IRQ_SDA(void)
{
if (I2C_Slave_SDA_IRQ_EN == 1)
{
if (I2C_Read_SCL() == GPIO_PIN_SET)
{
if (I2C_Read_SDA() == GPIO_PIN_SET)
{ // SDA上升沿
// 完成态
I2C_Slave_Ins = 0; // 到空闲态
I2C_Slave_Set_SDA_IT();
}
else
{ // SDA下降沿
I2C_Slave_Set_SDA_IT();
if (I2C_Slave_Ins == 0) // 空闲态
I2C_Slave_Ins = 1; // 到准备态
else
{
I2C_Slave_Ins = 1;
}
}
}
}
}
SCL上升沿服务函数
本函数是核心之一
根据编程思路一节的内容来编写上升沿服务函数
/**
* @brief SCL上升沿服务函数
* @param 无
* @return 无
* @author HZ12138
* @date 2022-12-29 21:29:43
*/
void I2C_Slave_IRQ_SCL_Rising(void)
{ // SCL上升沿
switch (I2C_Slave_Ins)
{
case 2: // 器件地址解码
I2C_Slave_zj <<= 1;
I2C_Slave_zj |= I2C_Read_SDA();
I2C_Slave_num++;
if (I2C_Slave_num == 8) // 数据码
{
I2C_Slave_Add = I2C_Slave_zj;
I2C_Slave_zj = 0;
}
else if (I2C_Slave_num == 9) // 应答码
{
I2C_Slave_num = 0;
if (I2C_Slave_Add == (I2C_Address & 0xfe))
I2C_Slave_Ins = 3; // 到寄存器地址读取态
else if (I2C_Slave_Add == (I2C_Address | 0x01))
{
I2C_Write_SDA(GPIO_PIN_SET);
I2C_Slave_Set_SDA_Out();
I2C_Slave_Ins = 5;
I2C_Slave_zj = 0xaa;
I2C_Slave_num = 0;
I2C_Slave_SDA_IRQ_EN = 0;
}
}
break;
case 3: // 寄存器地址读取
I2C_Slave_zj <<= 1;
I2C_Slave_zj |= I2C_Read_SDA();
I2C_Slave_num++;
if (I2C_Slave_num == 8) // 数据码
{
Reg_Add = I2C_Slave_zj;
}
else if (I2C_Slave_num == 9) // 应答码
{
I2C_Slave_Ins = 4; // 数据读取
I2C_Slave_zj = 0;
I2C_Slave_num = 0;
}
break;
case 4: // 数据读取(主机写)
I2C_Slave_zj <<= 1;
I2C_Slave_zj |= I2C_Read_SDA();
I2C_Slave_num++;
if (I2C_Slave_num == 9) // 应答码
{
I2C_Slave_zj = 0;
I2C_Slave_num = 0;
}
break;
default:
break;
}
}
SCL上升沿服务函数
本函数是核心之一
根据编程思路一节的内容来编写下降沿服务函数
/**
* @brief SCL下降沿服务函数
* @param 无
* @return 无
* @author HZ12138
* @date 2022-12-29 21:29:43
*/
void I2C_Slave_IRQ_SCL_Falling(void)
{ // SCL下降沿
switch (I2C_Slave_Ins)
{
case 1: // 准备态
I2C_Slave_zj = 0;
I2C_Slave_num = 0;
I2C_Slave_Ins = 2; // 到器件地址解码
break;
case 5: // 数据发送(主机读)
if (I2C_Slave_zj & 0x80)
I2C_Write_SDA(GPIO_PIN_SET);
else
I2C_Write_SDA(GPIO_PIN_RESET);
I2C_Slave_zj <<= 1;
I2C_Slave_num++;
if (I2C_Slave_num == 9) // 应答码
{
I2C_Slave_num = 0;
I2C_Write_SDA(GPIO_PIN_SET);
I2C_Slave_Set_SDA_IT();
I2C_Slave_SDA_IRQ_EN = 1;
I2C_Slave_Ins = 0;
}
break;
default:
break;
}
}
成品
I2C_Write_SDA(GPIO_PIN_SET);
else
I2C_Write_SDA(GPIO_PIN_RESET);
I2C_Slave_zj <<= 1;
I2C_Slave_num++;
if (I2C_Slave_num == 9) // 应答码
{
I2C_Slave_num = 0;
I2C_Write_SDA(GPIO_PIN_SET);
I2C_Slave_Set_SDA_IT();
I2C_Slave_SDA_IRQ_EN = 1;
I2C_Slave_Ins = 0;
}
break;
default:
break;
}
}
# 成品
[GitHub](https://github.com/HZ1213825/HAL_STM32F4_IIC)