stm32/comm/soft-i2c.md

软件 I2C:用 GPIO 敲出一条总线

知识库 · STM32 📅 初版 2026-09-16

软件 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 的全部工作,就是用代码维护三条总线规则:

  1. SCL 低电平时,SDA 才允许变化——数据在时钟的低电平段"换挡";
  2. SCL 高电平期间,SDA 必须保持稳定——这是接收方的采样窗口,高电平段数据动了就会被当成起始/停止信号,总线直接乱套;
  3. 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

这次改造顺手把"改码 → 确认进固件 → 烧录验证"整条链走成了流水线,每一步都有客观证据:

  1. Edit 改码:修改 io_i2c 相关代码(GBK 编码源码,编辑时保编码);
  2. PowerShell Select-String 验证改动落盘:替换/修改后立即把文件读回来搜关键行,确认新代码真的在文件里——注意本站的 Grep 工具对 GBK 文件会报 stream did not contain valid UTF-8,改用 PowerShell 的 Select-String 按 GBK 语义处理;
  3. Keil 命令行编译:D:\Keil_v5\UV4\UV4.exe -b 工程.uvproj -j0 -o build.log,检查退出码 exit 0、日志 0 Error 0 Warning;
  4. axf 搜串确认:在生成的 .axf 二进制里以 ASCII 搜刚改的字符串——搜到了才证明新代码真的进了固件(hex 文件里搜不到是正常的,hex 是十六进制编码的记录文本,明文根本不在里面);
  5. hex 拷暂存目录:拷到纯 ASCII 路径,规避工具链对中文路径的不兼容;
  6. 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 汉字。

#九、相关阅读

#十、更新日志

📝共 1 条记录
日期类型更新内容
2026-09-16新增首次建档:软硬 I2C 对照、时序三规则、方向切换宏、免上拉原理、速度参数与固件验证链