一、引言
I2C(Inter - Integrated Circuit)是一种串行通信协议,常用于连接微控制器和各种外围设备,如传感器、EEPROM 等。在 STM32 开发中,除了使用硬件 I2C 接口,还可以通过软件模拟的方式实现 I2C 通信。软件 I2C 具有灵活性高、不受硬件资源限制等优点,适用于各种需要自定义通信时序的场景。本文将详细介绍 STM32 软件 I2C 读写的实现方法,并给出相应的代码示例。
二、I2C 协议基础
2.1 物理层
I2C 总线由两根线组成:串行数据线(SDA)和串行时钟线(SCL)。所有连接到 I2C 总线上的设备都通过这两根线进行通信,并且每个设备都有一个唯一的 7 位或 10 位地址。
2.2 协议层
I2C 通信基于主从模式,主设备负责发起通信并控制时钟信号,从设备根据主设备的指令进行响应。通信过程主要包括起始条件、地址帧、数据帧、应答信号和停止条件。
起始条件:SCL 为高电平时,SDA 由高电平变为低电平,表示通信开始。
地址帧:主设备发送从设备的地址,其中最低位表示读写方向(0 为写,1 为读)。
数据帧:主设备和从设备之间传输数据,每次传输一个字节。
应答信号:每传输一个字节后,接收方需要发送一个应答信号(ACK)或非应答信号(NACK)。
停止条件:SCL 为高电平时,SDA 由低电平变为高电平,表示通信结束。
三、STM32 软件 I2C 实现步骤
3.1 引脚初始化
选择两个 GPIO 引脚分别作为 SDA 和 SCL,并将它们初始化为开漏输出模式。
3.2 起始条件和停止条件的实现
通过控制 SDA 和 SCL 引脚的电平变化来产生起始条件和停止条件。
3.3 数据传输和应答信号处理
逐位发送和接收数据,并处理应答信号。
3.4 读写操作的实现
根据 I2C 协议,实现主设备对从设备的读写操作。
四、代码实现
4.1 引脚定义和初始化
#include "stm32f10x.h"
// SDA 和 SCL 引脚定义
#define I2C_SCL_PIN GPIO_Pin_6
#define I2C_SDA_PIN GPIO_Pin_7
#define I2C_GPIO_PORT GPIOB
// 引脚初始化函数
void I2C_GPIO_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// SCL 引脚初始化
GPIO_InitStructure.GPIO_Pin = I2C_SCL_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStructure);
// SDA 引脚初始化
GPIO_InitStructure.GPIO_Pin = I2C_SDA_PIN;
GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStructure);
// 初始状态 SCL 和 SDA 都为高电平
GPIO_SetBits(I2C_GPIO_PORT, I2C_SCL_PIN | I2C_SDA_PIN);
}
4.2 起始条件和停止条件的实现
// 产生起始条件
void I2C_Start(void) {
GPIO_SetBits(I2C_GPIO_PORT, I2C_SDA_PIN);
GPIO_SetBits(I2C_GPIO_PORT, I2C_SCL_PIN);
delay_us(5);
GPIO_ResetBits(I2C_GPIO_PORT, I2C_SDA_PIN);
delay_us(5);
GPIO_ResetBits(I2C_GPIO_PORT, I2C_SCL_PIN);
}
// 产生停止条件
void I2C_Stop(void) {
GPIO_ResetBits(I2C_GPIO_PORT, I2C_SDA_PIN);
GPIO_SetBits(I2C_GPIO_PORT, I2C_SCL_PIN);
delay_us(5);
GPIO_SetBits(I2C_GPIO_PORT, I2C_SDA_PIN);
delay_us(5);
}
4.3 数据传输和应答信号处理
// 发送一个字节数据
void I2C_SendByte(uint8_t byte) {
uint8_t i;
for (i = 0; i < 8; i++) {
if (byte & 0x80) {
GPIO_SetBits(I2C_GPIO_PORT, I2C_SDA_PIN);
} else {
GPIO_ResetBits(I2C_GPIO_PORT, I2C_SDA_PIN);
}
byte <<= 1;
delay_us(2);
GPIO_SetBits(I2C_GPIO_PORT, I2C_SCL_PIN);
delay_us(5);
GPIO_ResetBits(I2C_GPIO_PORT, I2C_SCL_PIN);
delay_us(2);
}
}
// 接收一个字节数据
uint8_t I2C_ReceiveByte(void) {
uint8_t i, byte = 0;
GPIO_InitTypeDef GPIO_InitStructure;
// 将 SDA 引脚设置为输入模式
GPIO_InitStructure.GPIO_Pin = I2C_SDA_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStructure);
for (i = 0; i < 8; i++) {
GPIO_SetBits(I2C_GPIO_PORT, I2C_SCL_PIN);
delay_us(5);
byte <<= 1;
if (GPIO_ReadInputDataBit(I2C_GPIO_PORT, I2C_SDA_PIN)) {
byte |= 0x01;
}
GPIO_ResetBits(I2C_GPIO_PORT, I2C_SCL_PIN);
delay_us(2);
}
// 将 SDA 引脚恢复为输出模式
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStructure);
return byte;
}
// 发送应答信号
void I2C_SendACK(void) {
GPIO_ResetBits(I2C_GPIO_PORT, I2C_SDA_PIN);
delay_us(2);
GPIO_SetBits(I2C_GPIO_PORT, I2C_SCL_PIN);
delay_us(5);
GPIO_ResetBits(I2C_GPIO_PORT, I2C_SCL_PIN);
delay_us(2);
GPIO_SetBits(I2C_GPIO_PORT, I2C_SDA_PIN);
}
// 发送非应答信号
void I2C_SendNACK(void) {
GPIO_SetBits(I2C_GPIO_PORT, I2C_SDA_PIN);
delay_us(2);
GPIO_SetBits(I2C_GPIO_PORT, I2C_SCL_PIN);
delay_us(5);
GPIO_ResetBits(I2C_GPIO_PORT, I2C_SCL_PIN);
delay_us(2);
}
// 等待应答信号
uint8_t I2C_WaitACK(void) {
uint8_t ack;
GPIO_InitTypeDef GPIO_InitStructure;
// 将 SDA 引脚设置为输入模式
GPIO_InitStructure.GPIO_Pin = I2C_SDA_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStructure);
GPIO_SetBits(I2C_GPIO_PORT, I2C_SCL_PIN);
delay_us(5);
ack = GPIO_ReadInputDataBit(I2C_GPIO_PORT, I2C_SDA_PIN);
GPIO_ResetBits(I2C_GPIO_PORT, I2C_SCL_PIN);
delay_us(2);
// 将 SDA 引脚恢复为输出模式
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStructure);
return ack;
}
4.4 读写操作的实现
// 写数据到从设备
uint8_t I2C_WriteData(uint8_t slaveAddr, uint8_t regAddr, uint8_t data) {
I2C_Start();
I2C_SendByte(slaveAddr << 1); // 发送从设备地址(写模式)
if (I2C_WaitACK()) {
I2C_Stop();
return 1;
}
I2C_SendByte(regAddr); // 发送寄存器地址
if (I2C_WaitACK()) {
I2C_Stop();
return 1;
}
I2C_SendByte(data); // 发送数据
if (I2C_WaitACK()) {
I2C_Stop();
return 1;
}
I2C_Stop();
return 0;
}
// 从从设备读取数据
uint8_t I2C_ReadData(uint8_t slaveAddr, uint8_t regAddr, uint8_t *data) {
I2C_Start();
I2C_SendByte(slaveAddr << 1); // 发送从设备地址(写模式)
if (I2C_WaitACK()) {
I2C_Stop();
return 1;
}
I2C_SendByte(regAddr); // 发送寄存器地址
if (I2C_WaitACK()) {
I2C_Stop();
return 1;
}
I2C_Start();
I2C_SendByte((slaveAddr << 1) | 0x01); // 发送从设备地址(读模式)
if (I2C_WaitACK()) {
I2C_Stop();
return 1;
}
*data = I2C_ReceiveByte();
I2C_SendNACK();
I2C_Stop();
return 0;
}
4.5 主函数示例
int main(void) {
uint8_t data;
I2C_GPIO_Init();
// 写数据到从设备
if (I2C_WriteData(0x50, 0x00, 0xAA) == 0) {
// 写成功
}
// 从从设备读取数据
if (I2C_ReadData(0x50, 0x00, &data) == 0) {
// 读成功
}
while (1) {
// 主循环
}
}
五、代码解释
引脚初始化:将 SDA 和 SCL 引脚初始化为开漏输出模式,确保在通信过程中可以正确控制引脚电平。
起始条件和停止条件:通过控制 SDA 和 SCL 引脚的电平变化,产生起始条件和停止条件,标志着通信的开始和结束。
数据传输和应答信号处理:I2C_SendByte 和 I2C_ReceiveByte 函数用于逐位发送和接收数据,I2C_SendACK、I2C_SendNACK 和 I2C_WaitACK 函数用于处理应答信号。
读写操作:I2C_WriteData 函数用于向从设备的指定寄存器写入数据,I2C_ReadData 函数用于从从设备的指定寄存器读取数据。
主函数:在主函数中调用 I2C_WriteData 和 I2C_ReadData 函数进行读写操作。
六、注意事项
延时函数:代码中使用了 delay_us 函数进行延时,需要根据实际情况实现该函数,以确保通信时序的正确性。
从设备地址:在进行读写操作时,需要根据从设备的实际地址进行设置。
引脚电平:在读取 SDA 引脚状态时,需要将其设置为输入模式;在发送数据时,需要将其恢复为输出模式。
通过以上代码和解释,我们可以实现 STM32 软件 I2C 的读写功能,满足不同应用场景的需求。
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/duierrorshuobu/article/details/145445441
|
|