软件 I2C:用 GPIO 敲出一条总线
软件 I2C:用 GPIO 敲出一条总线
2026-09-16 实测验证:洋桃 1 号开发板(STM32F103C8T6),100_SOFT_I2C_OLED 模块。PB6/PB7 两个 GPIO 位敲 I2C 时序点亮 OLED,Keil 命令行编译、CubeProgrammer CLI SWD 烧录,Download verified successfully。
#一、硬件 I2C vs 软件 I2C:先讲清为什么"自己敲"
洋桃例程的 Basic\i2c\ 目录里并存着两套 I2C 实现,正好是教科书级的对照组:
| 对比项 | i2c.c(硬件 I2C 外设) | io_i2c.c(软件 I2C,GPIO 位敲) |
|---|---|---|
| 实现方式 | 芯片内置 I2C 外设,配置寄存器后硬件自动收发 | 两个普通 GPIO,软件手动拉高拉低模拟时序 |
| 引脚自由度 | 固定在少数复用引脚(I2C1 = PB6/PB7) | 任意 GPIO 都能当 I2C 用 |
| CPU 占用 | 低:外设自己跑,CPU 等中断/标志位 | 高:每个时钟沿都要 CPU 亲手拨 |
| 稳定性 | F103 的硬件 I2C 出名地易翻车(卡死 BUSY、Stick 错误等历史遗留问题) | 时序完全可控,出问题好查好改 |
结论很直接:F103 上宁可牺牲一点 CPU,也要换时序的完全可控。本模块用的就是软件 I2C——引脚 SCL=PB6、SDA=PB7(与硬件 I2C1 复用相同引脚,硬件坏了随时可以无缝切到软件方案)。
#二、I2C 时序三规则
软件 I2C 的全部工作,就是用代码维护三条总线规则:
- SCL 低电平时,SDA 才允许变化——数据在时钟的低电平段"换挡";
- SCL 高电平期间,SDA 必须保持稳定——这是接收方的采样窗口,高电平段数据动了就会被当成起始/停止信号,总线直接乱套;
- START 与 STOP 是仅有的两个例外:
- START:SCL 高电平时,SDA 从高 → 低(总线上的所有设备都会注意到"要开始说话了");
- STOP:SCL 高电平时,SDA 从低 → 高("说完了,总线释放")。
所有普通数据位的时序都是规则 1、2 的循环,唯独起止信号借"高电平段跳变"这个例外表达特殊含义——这就是 I2C 协议的骨架。
#三、应答位:每 8 位之后的第 9 个时钟
I2C 以 8 位为一个传输单元,每个字节之后跟第 9 个时钟的应答位(ACK):
- 发送方在这第 9 个时钟把 SDA 交出来(主机切 SDA 为输入);
- 接收方把 SDA 拉低表示"收到了"(ACK);SDA 保持高就是 NACK;
- 主机看完应答再继续下一字节或发 STOP。
一句话:I2C 的可靠不靠校验码,就靠这"每说 8 位停下来听一耳朵"的机制。
#四、方向切换宏:一根 SDA 两种身份
SDA 要在"输出"和"输入"之间反复切换(发数据时输出、听应答时输入),io_i2c.h 里用两个宏直接改 CRL 寄存器(PB7 是 CRL 的最高 4 位):
c/* SDA 切输出:推挽输出,50MHz */
#define VL2_SDA_OUT() { GPIOB->CRL &= 0X0FFFFFFF; GPIOB->CRL |= 3u << 28; }
/* SDA 切输入:上拉输入 */
#define VL2_SDA_IN() { GPIOB->CRL &= 0X0FFFFFFF; GPIOB->CRL |= 8u << 28; }
3u << 28:MODE=11(50MHz)+ CNF=00(通用推挽输出);8u << 28:MODE=00(输入)+ CNF=10(上拉输入);- 先
&= 0X0FFFFFFF清掉低半字节的旧配置,再|=写入新配置——读-改-写的标准手法。
#五、为什么可以不加 2.2K 上拉
标准 I2C 规范要求 SDA/SCL 开漏输出 + 外部上拉电阻(常见 2.2K~4.7K):开漏只能拉低不能拉高,高电平靠电阻"回"上去。
本驱动的做法不同:推挽输出主动驱动高、低两个电平,高电平由引脚自己顶上去,不需要电阻回高,所以两根线上不加 2.2K 上拉也能跑。
代价也要说清:推挽是硬驱动,若总线上两个设备同时一个推高一个拉低,就是电源对地的电流冲突。所以这个"免上拉"方案只适合单主机 + 严格的方向切换场景(任何时候只有一个推挽在驱动总线)——本例一块 MCU 点一块 OLED,完全够用;要挂多从设备、多主机,老老实实回开漏 + 上拉的标准接法。
#六、速度:4?s 一拍,约 100kHz
io_i2c.h 里 VL2_SPEED = 2,对应每个时序步进之间 delay_us(4)——半个时钟沿 4?s,一个时钟周期约 8~10?s,折算下来约 100kHz,正好是 I2C 标准模式(Standard-mode)的速率档。
调速规律:值越小 → 每拍延时越短 → 总线越快,但留给信号稳定的时间越少,越不稳。调这个参数就是"速度 vs 余量"的权衡,杜洋给的经验值 2 是稳定优先的选择。
#七、固件验证链:从改一行码到 Download verified successfully
这次改造顺手把"改码 → 确认进固件 → 烧录验证"整条链走成了流水线,每一步都有客观证据:
- Edit 改码:修改 io_i2c 相关代码(GBK 编码源码,编辑时保编码);
- PowerShell Select-String 验证改动落盘:替换/修改后立即把文件读回来搜关键行,确认新代码真的在文件里——注意本站的 Grep 工具对 GBK 文件会报
stream did not contain valid UTF-8,改用 PowerShell 的 Select-String 按 GBK 语义处理; - Keil 命令行编译:
D:\Keil_v5\UV4\UV4.exe -b 工程.uvproj -j0 -o build.log,检查退出码 exit 0、日志 0 Error 0 Warning; - axf 搜串确认:在生成的 .axf 二进制里以 ASCII 搜刚改的字符串——搜到了才证明新代码真的进了固件(hex 文件里搜不到是正常的,hex 是十六进制编码的记录文本,明文根本不在里面);
- hex 拷暂存目录:拷到纯 ASCII 路径,规避工具链对中文路径的不兼容;
- CubeProgrammer CLI 烧录:
powershellSTM32_Programmer_CLI -c port=SWD -w 固件.hex -v -rst
-v 校验、-rst 烧完复位,看到 Download verified successfully 即为完整闭环。
这条链的价值和《定时器与中断》里的教训一脉相承:补丁要验证落盘、烧录要验证固件、别在 hex 里搜明文——每一步都拿证据说话。
#八、小结
- 软件 I2C = 两个 GPIO + 三条时序规则 + 第 9 个时钟听应答,牺牲 CPU 换引脚自由与时序可控;
- F103 硬件 I2C 出名易翻车,PB6/PB7 软件实现反而是更稳的选择;
- 推挽主动驱动可以省掉上拉电阻,但只适合单主机 + 严格方向切换;
- VL2_SPEED=2(每步 4?s)约 100kHz 标准档,参数越小越快越不稳;
- 这条软件 I2C 总线点亮的第一块屏,就是《OLED 显示与汉字取模:让"韶华实验室"上屏》里那五个 16×16 汉字。
#九、相关阅读
- OLED 字库与中文点阵——软件 I2C 的上层应用
- 定时器与中断:1 秒心跳与五阶段状态机
- 硬件 CRC 单元:一次实验看懂循环冗余校验
#十、更新日志
| 日期 | 类型 | 更新内容 |
|---|---|---|
| 2026-09-16 | 新增 | 首次建档:软硬 I2C 对照、时序三规则、方向切换宏、免上拉原理、速度参数与固件验证链 |