I2C 硬件实现
I2C 初始化
#include "stm32f10x.h"
#define GPIO_Pin_SCL GPIO_Pin_6 //GPIOB_6 为 SCL
#define GPIO_Pin_SDA GPIO_Pin_7 //GPIOB_7 为 SDA
/**
* @brief I2C 初始化
* @params None
* @returns None
*/
void IIC_Init(void)
{
/* 开启外设时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
/* GPIO 配置 */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; //必须为复用开漏输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_SCL | GPIO_Pin_SDA;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOB, GPIO_Pin_SCL | GPIO_Pin_SDA);
/* 串口参数配置 */
I2C_InitTypeDef IIC_InitStructure;
IIC_InitStructure.I2C_Mode = I2C_Mode_I2C; //模式,选择为I2C模式
IIC_InitStructure.I2C_ClockSpeed = 50000; //时钟速度,选择为50KHz
IIC_InitStructure.I2C_Ack = I2C_Ack_Enable; //应答,选择使能
IIC_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; //时钟占空比,低电平:高电平 = 2:1
IIC_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //自身地址位数,选择7位,从机模式下才有效
IIC_InitStructure.I2C_OwnAddress1 = 0x00; //自身地址,从机模式下才有效
I2C_Init(I2C1, &IIC_InitStructure);
}
I2C 主模式
- 当主设备处于发送模式时,它会先发送起始位,然后是7位从设备地址加上读/写方向位(0表示写,1表示读),接着发送要写入的数据,最后以停止位结束。
- 当主设备处于接收模式时,主设备需要先发送起始位和从设备地址加写方向位,告诉从设备要读取其数据。然后,主设备需要切换到接收模式,等待从设备发送数据。为了实现这一切换,主设备需要再次发送一个起始位,但这次是从设备地址加读方向位,从而进入接收模式。
简单来说就是:主机想发就发,主机让从机发从机才能发
I2C 等待事件
/**
* @brief I2C 等待事件
* @params I2C_EVENT 同 I2C_CheckEvent
* @returns None
*/
void IIC_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
uint32_t Timeout = 10000; //给定超时计数时间
while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS) //等待指定事件
{
Timeout--;
if (Timeout == 0) //判断是否等待超时
{
/*超时的错误处理代码,可以添加到此处*/
break;
}
}
}
主机向从机指定寄存器发送数据
/* I2C 7 位主发送 */
/**
* @brief I2C 向指定从设备的指定寄存器发送一个字节数据
* @params
* @arg Address 七位从机地址
* @arg RegAddress 从机寄存器地址
* @arg Data 待发送数据
* @returns None
*/
void IIC_Send_Byte(uint8_t Address, uint8_t RegAddress, uint8_t const Data)
{
I2C_GenerateSTART(I2C1, ENABLE); //产生起始条件
IIC_WaitEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5
I2C_Send7bitAddress(I2C1, Address, I2C_Direction_Transmitter); //发送从机地址
IIC_WaitEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //等待EV6
I2C_SendData(I2C1, RegAddress); //向从机发送寄存器地址
IIC_WaitEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING); //等待EV8
I2C_SendData(I2C1, Data); //向从机寄存器发送数据
IIC_WaitEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //等待EV8_2
I2C_GenerateSTOP(I2C1, ENABLE); //产生停止条件
}
主机从从机指定寄存器读取数据
/* I2C 7 位主接收 */
/**
* @brief I2C 从指定从设备的指定寄存器读取一个字节数据
* @params
* @arg Address 七位从机地址
* @arg RegAddress 从机寄存器地址
* @returns 接收数据
*/
uint8_t IIC_Receive_Byte(uint8_t Address, uint8_t RegAddress)
{
I2C_GenerateSTART(I2C1, ENABLE); //产生起始条件
IIC_WaitEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5
I2C_Send7bitAddress(I2C1, Address, I2C_Direction_Transmitter); //发送从机地址,模式为发送
IIC_WaitEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); //等待EV6
I2C_SendData(I2C1, RegAddress); //向从机发送要读取的寄存器地址
IIC_WaitEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //等待EV8_2
I2C_GenerateSTART(I2C1, ENABLE); //产生重复起始条件
IIC_WaitEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5
I2C_Send7bitAddress(I2C1, Address, I2C_Direction_Receiver); //发送从机地址,模式为接收
IIC_WaitEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); //等待EV6
I2C_AcknowledgeConfig(I2C1, DISABLE); //提前关闭应答位
I2C_GenerateSTOP(I2C1, ENABLE); //提前产生停止条件
IIC_WaitEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED); //等待EV7
uint8_t Data = I2C_ReceiveData(I2C1); //接收数据
I2C_AcknowledgeConfig(I2C2, ENABLE); //将应答恢复使能,不影响读取多字节操作
return Data;
}
I2C 软件实现
GPIO 初始化
#include "stm32f10x.h"
/**
* @brief GPIO初始化
* @params None
* @returns None
*/
void MyIIC_Init(void)
{
/* 开启外设时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
/* GPIO 配置 */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //必须为开漏输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; //任意两个 GPIO_Pin 口
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
SCL、SDA 引脚配置
/**
* @brief 写 SCL 电平
* @params Value 写入 SCL 电平值(0 表示低电平,1 表示高电平)
* @returns None
*/
void MyIIC_W_SCL(uint8_t Value)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_6, (BitAction)Value);
/* 添加一定的延时时间,防止时序频率超过设备要求。
不同的 I2C 设备其最大支持的时钟频率是有限的。
如果使用的 I2C 时钟频率超过了所连接设备的最大支持频率,
那么这些设备可能无法正常工作,导致数据传输错误或失败。 */
}
/**
* @brief 写 SDA 电平
* @params Value 写入 SDA 电平值(0 表示低电平,1 表示高电平)
* @returns None
*/
void MyIIC_W_SDA(uint8_t Value)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_7, (BitAction)Value);
/* 添加一定的延时时间,防止时序频率超过设备要求。*/
}
/**
* @brief 读 SDA 电平
* @params None
* @returns SDA 电平值
*/
uint8_t MyIIC_R_SDA(void)
{
uint8_t Value = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7);
/* 添加一定的延时时间,防止时序频率超过设备要求。*/
return Value;
}
I2C 协议层
- 起始位:SCL高电平、SDA下降沿
void MyIIC_START(void)
{
/* 空闲状态下 SCL 和 SDA 均为高电平 */
MyIIC_W_SCL(SET);
MyIIC_W_SDA(SET);
MyIIC_W_SDA(RESET); //起始条件:SCL 高电平时 SDA 下降沿
MyIIC_W_SDA(RESET); //占用总线
}
- 终止位:SCL高电平、SDA上升沿
void MyIIC_STOP(void)
{
MyIIC_W_SDA(RESET);
MyIIC_W_SCL(SET);
MyIIC_W_SDA(SET); //终止条件:SCL 高电平时 SDA 上升沿
}
- 主机发送应答位
主机接收完一个字节后,在下一个时钟发送一位数据(0 表示应答,1 表示非应答)
/**
* @brief 主机向从机发送应答位(Ack 由主机控制 SDA 线发出)
* @params Ack 应答位 0 表示应答,1 表示非应答
* @returns None
*/
void Send_Ack(uint8_t Ack)
{
MyIIC_W_SDA(Ack); //主机把应答位数据放到 SDA 线
MyIIC_W_SCL(SET); //释放 SCL,从机在 SCL 高电平期间,读取应答位
MyIIC_W_SCL(RESET); //拉低 SCL,开始下一个数据传输
}
- 主机接收应答位
主机发送完一个字节后,在下一个时钟接收一位数据,判断从机是否应答(0 表示应答,1 表示非应答)
/**
* @brief 主机读取从机应答位(Ack 由从机控制 SDA 线发出)
* @params None
* @returns 应答位 0 表示应答,1 表示非应答
*/
uint8_t Receive_Ack(void)
{
uint8_t Ack; //定义应答位变量
MyIIC_W_SDA(SET); //接收前,主机先确保释放 SDA,避免干扰从机的数据发送
MyIIC_W_SCL(SET); //释放 SCL,主机将在 SCL 高电平期间读取 SDA
Ack = MyIIC_R_SDA(); //将应答位存储到变量里
MyIIC_W_SCL(RESET); //拉低 SCL,开始下一个数据传输
return Ack; //返回应答位
}
- 主机发送一个字节
/**
* @brief I2C 发送一个字节
* @params Byte 要发送的一个字节数据,范围:0x00~0xFF
* @returns None
*/
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++) //循环 8 次,主机依次发送数据的每一位
{
/* I2C协议规定先传输高位(MSB,即 Most Significant Bit),然后传输低位(LSB,即 Least Significant Bit)*/
MyIIC_W_SDA(Byte & (0x80 >> i)); //使用掩码的方式取出 Byte 的指定一位数据并写入到 SDA 线 (1000 0000b)
MyIIC_W_SCL(SET); //释放 SCL,从机在SCL高电平期间读取 SDA
MyIIC_W_SCL(RESET); //拉低 SCL,主机开始发送下一位数据
}
}
- 主机接收一个字节
/**
* @brief I2C 接收一个字节
* @params None
* @returns 接收到的一个字节数据,范围:0x00~0xFF
*/
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i, Byte = 0x00; //定义接收的数据,并赋初值 0x00
MyIIC_W_SDA(SET); //接收前,主机先确保释放 SDA,避免干扰从机的数据发送
for (i = 0; i < 8; i ++) //循环 8 次,主机依次接收数据的每一位
{
MyIIC_W_SCL(SET); //释放 SCL,主机机在 SCL 高电平期间读取 SDA
if (MyIIC_R_SDA() == 1){Byte |= (0x80 >> i);} //读取 SDA 数据,并存储到 Byte 变量
//当 SDA 为 1 时,置变量指定位为 1,当 SDA 为 0 时,不做处理,指定位为默认的初值 0
MyIIC_W_SCL(RESET); //拉低 SCL,从机在SCL低电平期间写入 SDA
}
return Byte;
}
欢迎批评指正!!!
参考文献:STM32F10xxx参考手册
协议层图片来源:STM32入门教程-2023版 细致讲解 中文字幕