在 STM32 上实现 IIC 的 IO 口模拟(bit-banging),核心是通过 GPIO 口模拟 IIC 通信的时序逻辑(SCL 时钟线和 SDA 数据线的高低电平变化)。
以下是具体实现方案:
一、硬件基础
IIC 通信需要两根线:
- SCL:时钟线(双向,由主机控制)
- SDA:数据线(双向,主机 / 从机均可控制)
硬件要求:
- 选择 STM32 的两个 GPIO 口(例如 PA6=SCL,PA7=SDA)
- 必须外接上拉电阻(通常 4.7KΩ),因为 IIC 总线通过线与逻辑工作(低电平有效,高电平由上拉维持)
- GPIO 配置为开漏输出模式(OD),允许外部设备拉低电平,同时支持输入模式切换(读取 SDA 状态)
二、软件实现核心
模拟 IIC 需要实现以下基础时序函数:
- 起始信号(Start)
- 停止信号(Stop)
- 发送一个字节(SendByte)
- 接收一个字节(ReadByte)
- 发送应答(Ack)/ 非应答(Nack)
1. 引脚定义与初始化
首先定义 SCL 和 SDA 的引脚,并初始化 GPIO 为开漏输出模式。
头文件(iic.h):
#ifndef __IIC_H
#define __IIC_H
#include "stm32f1xx_hal.h" // 根据实际型号修改
// 引脚定义(可根据实际硬件修改)
#define IIC_SCL_PORT GPIOA
#define IIC_SDA_PORT GPIOA
#define IIC_SCL_PIN GPIO_PIN_6
#define IIC_SDA_PIN GPIO_PIN_7
// 宏定义:设置SCL/SDA高低电平
#define IIC_SCL_HIGH HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_SET)
#define IIC_SCL_LOW HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_RESET)
#define IIC_SDA_HIGH HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, GPIO_PIN_SET)
#define IIC_SDA_LOW HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, GPIO_PIN_RESET)
// 读取SDA电平(需先切换为输入模式)
#define IIC_SDA_READ HAL_GPIO_ReadPin(IIC_SDA_PORT, IIC_SDA_PIN)
// 函数声明
void IIC_Init(void); // 初始化IIC引脚
void IIC_Start(void); // 发送起始信号
void IIC_Stop(void); // 发送停止信号
uint8_t IIC_SendByte(uint8_t data); // 发送一个字节,返回应答状态
uint8_t IIC_ReadByte(uint8_t ack); // 接收一个字节,ack=1发送应答,0发送非应答
#endif
2. 核心时序实现
源文件(iic.c):#include "iic.h"
// 延时函数(关键!需根据IIC速率调整,标准模式100KHz需约5us延时)
static void IIC_Delay(void) {
uint8_t i;
// 循环次数根据MCU主频调整(示例:STM32F1 72MHz下约5us)
for (i = 0; i < 10; i++);
}
// 初始化IIC引脚(开漏输出,初始高电平)
void IIC_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIO时钟(根据实际端口修改)
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置SCL和SDA为开漏输出
GPIO_InitStruct.Pin = IIC_SCL_PIN | IIC_SDA_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // 外部上拉,内部不使能
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(IIC_SCL_PORT, &GPIO_InitStruct);
// 初始化为高电平(空闲状态)
IIC_SCL_HIGH;
IIC_SDA_HIGH;
}
// 发送起始信号:SCL高电平时,SDA从高→低
void IIC_Start(void) {
IIC_SDA_HIGH; // 先确保SDA为高
IIC_SCL_HIGH;
IIC_Delay();
IIC_SDA_LOW; // SDA拉低,产生起始信号
IIC_Delay();
IIC_SCL_LOW; // 拉低SCL,准备发送数据
}
// 发送停止信号:SCL高电平时,SDA从低→高
void IIC_Stop(void) {
IIC_SDA_LOW; // 先确保SDA为低
IIC_SCL_HIGH;
IIC_Delay();
IIC_SDA_HIGH; // SDA拉高,产生停止信号
IIC_Delay();
}
// 发送一个字节,返回0表示接收应答,1表示无应答
uint8_t IIC_SendByte(uint8_t data) {
uint8_t i, ack;
for (i = 0; i < 8; i++) { // 逐位发送(高位先送)
// SCL低电平时,准备数据
IIC_SCL_LOW;
IIC_Delay();
// 设置SDA电平(发送当前位)
if (data & 0x80) IIC_SDA_HIGH;
else IIC_SDA_LOW;
data <<= 1; // 移位准备下一位
IIC_Delay();
// SCL拉高,从机读取数据
IIC_SCL_HIGH;
IIC_Delay();
}
// 接收应答位(第9个时钟)
IIC_SCL_LOW; // 拉低SCL,准备读取应答
IIC_SDA_HIGH; // 释放SDA(由从机控制)
IIC_Delay();
IIC_SCL_HIGH; // SCL拉高,读取应答
ack = IIC_SDA_READ; // 若ack=0表示从机应答,1表示非应答
IIC_Delay();
IIC_SCL_LOW; // 拉低SCL,结束应答阶段
return ack;
}
// 接收一个字节,ack=1发送应答,0发送非应答
uint8_t IIC_ReadByte(uint8_t ack) {
uint8_t i, data = 0;
IIC_SDA_HIGH; // 释放SDA,准备接收数据
for (i = 0; i < 8; i++) { // 逐位接收(高位先收)
IIC_SCL_LOW; // 拉低SCL,从机准备数据
IIC_Delay();
IIC_SCL_HIGH; // SCL拉高,读取数据
data <<= 1; // 移位
if (IIC_SDA_READ) data |= 0x01; // 读取当前位
IIC_Delay();
}
// 发送应答/非应答(第9个时钟)
IIC_SCL_LOW; // 拉低SCL,准备发送应答
if (ack) IIC_SDA_LOW; // 应答:拉低SDA
else IIC_SDA_HIGH; // 非应答:拉高SDA
IIC_Delay();
IIC_SCL_HIGH; // SCL拉高,从机读取应答
IIC_Delay();
IIC_SCL_LOW; // 拉低SCL,结束应答阶段
return data;
}
三、关键注意事项
- 延时精度:IIC_Delay()的时长直接影响通信速率(标准模式 100KHz 需时钟周期约 10us,快速模式 400KHz 需约 2.5us),需根据实际 MCU 主频调试。
- 引脚模式切换:若需频繁切换 SDA 为输入(如读取数据时),可在读取前将 GPIO 模式改为输入,读完后改回开漏输出(上述代码通过开漏输出 + 释放 SDA(拉高)实现输入功能,简化了模式切换)。
- 上拉电阻:必须外接,否则总线无法维持高电平,通信会失败。
- 应答处理:从机接收数据后需发送应答(ACK),主机接收完最后一个字节后通常发送非应答(NACK),然后发送停止信号。
四、应用示例(读取从机数据)
以读取某个 IIC 从机(地址 0xA0)的 0x00 寄存器为例:
// 读取从机数据
uint8_t IIC_ReadFromSlave(uint8_t slave_addr, uint8_t reg_addr, uint8_t *data, uint8_t len) {
IIC_Start();
// 发送从机写地址(bit0=0表示写)
if (IIC_SendByte(slave_addr & 0xFE)) {
IIC_Stop();
return 1; // 从机无应答
}
// 发送寄存器地址
if (IIC_SendByte(reg_addr)) {
IIC_Stop();
return 1;
}
// 重新发送起始信号,切换为读模式
IIC_Start();
// 发送从机读地址(bit0=1表示读)
if (IIC_SendByte(slave_addr | 0x01)) {
IIC_Stop();
return 1;
}
// 读取数据
for (uint8_t i = 0; i < len; i++) {
// 最后一个字节发送非应答,其余发送应答
data[i] = IIC_ReadByte((i != len - 1) ? 1 : 0);
}
IIC_Stop();
return 0; // 成功
}
通过上述实现,即可在 STM32 上通过 GPIO 模拟 IIC 通信,兼容绝大多数 IIC 设备(如传感器、EEPROM 等)。
核心是保证时序的准确性,需根据实际设备的时序要求调整延时函数。
1042

被折叠的 条评论
为什么被折叠?



