STM32F103 I2C寄存器级实战:绕开HAL阻塞与PB6/PB7引脚冲突
2026/9/13 9:55:07 网站建设 项目流程

简介:本资源是一份面向STM32嵌入式开发初学者与中级工程师的I2C通信实践代码包,聚焦于解决原厂示例在实际项目中兼容性差、配置冗余、调试困难等常见痛点。压缩包共3个文件(1个C源码、1个头文件、1个说明文本),总大小仅3KB,轻量易集成;其中I2C.c实现GPIO初始化、I2C外设配置、主模式读写函数及中断响应逻辑,I2C.h定义设备地址、状态枚举与接口原型,zxur.txt则详述作者对原厂代码的优化点与实测注意事项,涵盖PB6/PB7引脚配置、时钟分频设置、应答失败恢复机制等关键细节。已有172人学习下载,适合快速掌握STM32硬件I2C驱动开发全流程,尤其适用于传感器通信、EEPROM读写、多设备总线调试等典型物联网场景,可直接移植到Keil或STM32CubeIDE工程中验证使用。

1. 这不是“抄个例程就能跑”的I2C——STM32_I2C压缩包里藏着三处关键修复,绕开HAL库阻塞陷阱和PB6/PB7引脚复用冲突

你手头这个stm32_i2c.rar压缩包,表面看只是三个文件:I2C.cI2C.hzxur.txt,但实际它解决的是STM32F103系列在真实硬件上跑I2C时最常卡死的三个硬伤——GPIO复用配置遗漏、ACK检测逻辑缺陷、以及SCL低电平超时未判别。很多开发者照着ST官方HAL例程改完代码,接上0.96寸OLED或AT24C02 EEPROM后,HAL_I2C_Master_Transmit()永远返回HAL_BUSY,根本不是时序没调对,而是底层状态机在I2C_FLAG_BUSY被误判为总线占用(实为从机未应答导致SCL被拉低锁死)。这个包里的I2C.c不依赖HAL,纯寄存器操作,且所有函数都带超时计数(非SysTick,而是独立while循环+__NOP()防优化),zxur.txt明确记录了“将原厂例程中I2C_CheckEvent(I2C_EVENT_MASTER_MODE_SELECT)移至I2C_GenerateSTART()之后”这一关键调整。它适合正在调试温湿度传感器(如SHT30)、EEPROM或I2C OLED屏的嵌入式工程师,尤其当你发现示波器上SCL波形在第9个时钟沿后突然停住、SDA保持高阻态时——这包里的实现能立刻定位到I2C_WaitEvent()里缺失的I2C_FLAG_AF轮询分支。


2. GPIO与I2C外设寄存器级初始化:为什么PB6/PB7必须配置为开漏+上拉,且时钟使能顺序不能颠倒

2.1 引脚复用配置的四个不可省略步骤

STM32F103的I2C1默认映射到PB6(SCL)和PB7(SDA),但直接设置GPIO_Mode_Out_OD是错误的起点。正确流程需严格按序执行:

  1. 使能GPIOB和AFIO时钟(RCC_APB2ENR)
  2. 重映射I2C1到PB6/PB7(AFIO_MAPR寄存器bit10=1)
  3. 配置PB6/PB7为开漏输出+上拉(GPIOB_CRL寄存器)
  4. 使能I2C1时钟(RCC_APB1ENR bit21=1)

提示:若跳过第2步,I2C1仍默认使用PB8/PB9,此时PB6/PB7即使配置为开漏也不会响应I2C事件。zxur.txt中明确标注:“F103C8T6最小系统板必须写AFIO_MAPR |= 0x0400”。

2.1.1 关键寄存器配置代码(I2C.c片段)
// 1. 使能GPIOB和AFIO时钟 RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN; // 2. 重映射I2C1到PB6/PB7 AFIO->MAPR |= AFIO_MAPR_I2C1_REMAP; // bit10置1 // 3. 配置PB6/PB7:开漏输出,50MHz,上拉 GPIOB->CRH &= ~(0xFF << 8); // 清除PB6/PB7原有配置(CRH[15:8]) GPIOB->CRH |= (0x88 << 8); // 0x88 = 10001000b → CNF6[1:0]=10(开漏), MODE6[1:0]=11(50MHz) GPIOB->ODR |= (1<<6) | (1<<7); // PB6/PB7上拉使能 // 4. 使能I2C1时钟 RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
  • GPIOB->CRH |= (0x88 << 8)0x88的二进制为10001000,对应PB6(高4位)和PB7(低4位):
    • CNF6[1:0]=10表示开漏模式(Open-Drain)
    • MODE6[1:0]=11表示最大输出速度50MHz(满足标准模式100kHz需求)
  • GPIOB->ODR |= (1<<6) | (1<<7)必须显式设置的上拉,因为I2C物理层要求SDA/SCL通过外部或内部上拉电阻维持高电平,开漏输出本身无法主动拉高。

2.2 I2C外设核心参数计算:如何用公式反推TRISE和CCR值

I2C时钟分频器(CCR)和上升时间寄存器(TRISE)决定通信速率。标准模式(100kHz)下,公式为:

CCR = (PCLK1 / (2 × I2C_CLK)) - 1 TRISE = (PCLK1 / 1000000) + 1

假设PCLK1=36MHz(APB1总线),目标I2C_CLK=100kHz:

  • CCR = (36000000 / (2 × 100000)) - 1 = 179
  • TRISE = (36000000 / 1000000) + 1 = 37
2.2.1 初始化I2C1寄存器组
// 复位I2C1 I2C1->CR1 = 0x0000; // 先关闭 I2C1->CR2 = 0x0000; // 清除所有配置 // 设置时钟控制寄存器 I2C1->CCR = 0x00B3; // CCR=179 → 0xB3(16位,高8位为0) I2C1->TRISE = 0x0025; // TRISE=37 → 0x25 // 使能ACK、错误中断、事件中断 I2C1->CR1 = I2C_CR1_ACK | I2C_CR1_PE; // PE=1使能外设,ACK=1允许应答 // 配置自身地址(主模式下可忽略,但寄存器必须写0) I2C1->OAR1 = 0x0000;
  • I2C1->CR1 = I2C_CR1_ACK | I2C_CR1_PEI2C_CR1_ACK是关键:若未置位,从机发送数据时主机会拒绝应答,导致传输中断。
  • I2C1->OAR1 = 0x0000并非无意义——I2C外设在主模式下虽不使用自身地址,但OAR1寄存器若为非法值(如全1),某些芯片批次会触发总线错误中断。

2.3 为什么必须禁用JTAG?——I2C与SWD调试接口的引脚冲突真相

PB6/PB7在部分STM32F103子型号(如F103C8T6)上与JTAG的TMS/TDO复用。若调试器(ST-Link)保持JTAG连接,PB6/PB7会被强制为JTAG功能,I2C初始化失败。解决方案是SystemInit()后立即禁用JTAG,仅保留SWD

// 禁用JTAG,启用SWD(保留SWDIO/PB14和SWCLK/PB13) AFIO->MAPR &= ~AFIO_MAPR_JTAGDISABLE; // 清除JTAG使能位 AFIO->MAPR |= AFIO_MAPR_SWJ_CFG_JTAGDISABLE; // 010b → JTAG-DP关闭,SW-DP开启
  • AFIO_MAPR_SWJ_CFG_JTAGDISABLE对应010b,这是ST官方勘误表(DocID13996)明确要求的配置。
  • 若未执行此步,I2C1->CR1写入后读回可能为0,表明外设未真正使能。

3. 主机读写函数实现:从START信号生成到字节级ACK校验的完整时序控制

3.1 START条件生成与事件等待的原子性保障

I2C总线启动需在SCL高时SDA由高变低。I2C_GenerateSTART()函数必须确保该跳变发生在SCL稳定高电平之后,且后续事件检查不能被中断打断:

void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState) { if (NewState != DISABLE) { // 1. 确保SCL为高(读取I2C_SR2的BIT0) while (!(I2Cx->SR2 & I2C_SR2_TRA)); // 等待总线空闲(TRA=1表示发送器模式) // 2. 拉低SDA(先清零,再置位START位) I2Cx->CR1 |= I2C_CR1_START; // 3. 等待START事件(EV5) uint16_t timeout = 0xFFFF; while (!(I2Cx->SR1 & I2C_SR1_SB)) { // SB=1表示START已发送 if (--timeout == 0) return; // 超时退出 } } else { I2Cx->CR1 &= ~I2C_CR1_START; } }
  • I2Cx->SR2 & I2C_SR2_TRA检查TRA标志位(Transmitter Mode),而非简单轮询I2C_SR1_BUSY,因为BUSY在从机应答后可能仍为1,而TRA能准确反映主机是否进入发送状态。
  • timeout = 0xFFFF是经验阈值:在72MHz系统时钟下,100kHz I2C的SCL周期为10μs,0xFFFF ≈ 65535 × 10ns ≈ 655μs,足够覆盖最坏情况下的总线延迟。

3.2 字节写入函数中的ACK/NACK动态判定

标准I2C写操作中,主机在发送每个字节后需检测从机应答(ACK)。原厂例程常忽略NACK处理,导致后续字节发送失败。本包I2C_MasterWriteByte()强制校验:

uint8_t I2C_MasterWriteByte(I2C_TypeDef* I2Cx, uint8_t Data, uint8_t Addr, uint8_t IsLast) { // 发送地址(含R/W位=0) I2Cx->DR = (Addr << 1) & 0xFE; // 左移+清LSB(写模式) if (!I2C_WaitEvent(I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) return 1; // 发送数据字节 I2Cx->DR = Data; if (!I2C_WaitEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED)) return 1; // 关键:检查ACK(若IsLast=1,最后字节后发STOP;否则继续) if (IsLast) { I2Cx->CR1 |= I2C_CR1_STOP; // 发送STOP while (I2Cx->CR1 & I2C_CR1_STOP); // 等待STOP完成 } return 0; // 成功 } // I2C_WaitEvent()中新增ACK检测分支 uint8_t I2C_WaitEvent(I2C_TypeDef* I2Cx, uint16_t Event) { uint16_t timeout = 0xFFFF; while (1) { uint16_t sr1 = I2Cx->SR1; uint16_t sr2 = I2Cx->SR2; if (sr1 & I2C_SR1_AF) { // 检测到NACK(AF=1) I2Cx->SR1 &= ~I2C_SR1_AF; // 清除AF标志 return 0; // NACK返回失败 } if ((sr1 & Event) == Event) return 1; // 事件匹配 if (--timeout == 0) return 0; // 超时 } }
  • I2Cx->SR1 & I2C_SR1_AF唯一可靠检测NACK的方式。若仅靠I2C_EVENT_MASTER_BYTE_TRANSMITTED判断,当从机不响应时,该事件永不置位,函数无限等待。
  • IsLast参数控制STOP时机:向EEPROM写入多字节时,前N-1字节后不发STOP,保持总线占用;最后一字节后发STOP释放总线。

3.3 读取函数中的时钟延展与重复START处理

I2C读操作需在地址发送后插入重复START(Re-START),再发送读地址。常见错误是未在重复START前等待SCL释放:

uint8_t I2C_MasterReadByte(I2C_TypeDef* I2Cx, uint8_t* pData, uint8_t Addr, uint8_t IsLast) { // 第一步:发送写地址(选择寄存器) I2Cx->DR = (Addr << 1) & 0xFE; if (!I2C_WaitEvent(I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) return 1; // 第二步:生成重复START I2Cx->CR1 |= I2C_CR1_START; if (!I2C_WaitEvent(I2Cx, I2C_EVENT_MASTER_MODE_SELECT)) return 1; // EV6 // 第三步:发送读地址(R/W=1) I2Cx->DR = (Addr << 1) | 0x01; if (!I2C_WaitEvent(I2Cx, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)) return 1; // 第四步:读取数据 if (IsLast) { I2Cx->CR1 &= ~I2C_CR1_ACK; // 最后字节前清除ACK,通知从机这是最后一个字节 } uint16_t timeout = 0xFFFF; while (!(I2Cx->SR1 & I2C_SR1_RXNE)) { // RXNE=1表示数据接收完成 if (--timeout == 0) return 1; } *pData = I2Cx->DR; if (IsLast) { I2Cx->CR1 |= I2C_CR1_STOP; // 发送STOP } return 0; }
  • I2Cx->CR1 &= ~I2C_CR1_ACK必须在读取最后一个字节之前执行,否则从机会继续发送下一字节,导致主机缓冲区溢出。
  • I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED(EV7)标志位表明从机已准备好发送数据,此时才能安全读取I2Cx->DR

4. zxur.txt日志解析:三处原厂例程缺陷与对应修复方案

4.1 缺陷1:I2C_CheckEvent()I2C_GenerateSTART()前调用导致EV5丢失

原厂例程常在I2C_GenerateSTART()后立即调用I2C_CheckEvent(I2C_EVENT_MASTER_MODE_SELECT),但该事件(EV5)需在START信号发出后由硬件置位。若CheckEvent执行过快,SR1寄存器尚未更新,返回失败。zxur.txt记录:

“修改I2C_WaitEvent(),删除对I2C_SR2_BUSY的依赖,改用轮询I2C_SR1_SB(START Bit)。实测在72MHz系统下,原逻辑失败率37%,新逻辑100%通过。”

4.1.1 修复后的事件等待逻辑对比表
场景原厂逻辑本包修复逻辑效果
START后等待EV5while(!I2C_CheckEvent(EV5))while(!(I2C1->SR1 & I2C_SR1_SB))避免因编译器优化导致SR1读取延迟
ACK检测无显式AF轮询if(sr1 & I2C_SR1_AF) return 0NACK时立即返回,不卡死
STOP完成判断while(I2C1->CR1 & I2C_CR1_STOP)同左,但增加超时计数防止总线锁死时无限等待

4.2 缺陷2:PB6/PB7上拉电阻未启用导致SDA无法恢复高电平

zxur.txt指出:“在面包板测试中,未接外部4.7kΩ上拉电阻时,PB6/PB7配置为开漏但ODR未置位,SDA始终为低,I2C总线瘫痪”。修复方案已在2.1节详述,核心是GPIOB->ODR |= (1<<6)|(1<<7)

4.3 缺陷3:I2C时钟分频器CCR值计算错误引发时序违规

原厂例程对PCLK1=36MHz的计算采用CCR = PCLK1/(2*I2C_CLK),遗漏减1操作,导致实际速率为102.6kHz(超标)。zxur.txt提供修正公式并验证:

“使用DSO138示波器捕获SCL波形,CCR=179时周期9.8μs(102kHz),改为178后周期10.01μs(99.9kHz),符合I2C标准容差(±10%)”。

4.3.1 不同PCLK1频率下的推荐CCR值速查表
PCLK1频率目标速率CCR计算值实际写入值实测误差
36MHz100kHz1790xB2 (178)-0.1%
72MHz400kHz890x58 (88)-0.2%
24MHz100kHz1190x74 (116)+2.5%(需降低TRISE)
  • 表中“实际写入值”为CCR-1,因公式含-1项;TRISE需同步调整:TRISE = (PCLK1/1000000)+1,高速模式下可减小以加快上升沿。

5. 实战验证技巧:用逻辑分析仪抓取I2C波形,快速定位三类典型故障

5.1 故障1:START信号未生成——检查PB6/PB7电平与AFIO_MAPR

连接Saleae Logic或DSO138,通道1接PB6(SCL),通道2接PB7(SDA)。正常START应为:SCL高电平时SDA下降沿。若无此波形:

  • 用万用表测PB6/PB7对地电压,应为3.3V(上拉有效);若为0V,检查GPIOB->ODR是否置位。
  • 读取AFIO->MAPR寄存器,确认bit10=1(I2C1_REMAP=1);若为0,重写AFIO->MAPR |= 0x0400

5.2 故障2:地址发送后无ACK——聚焦从机供电与地址匹配

示波器捕获到地址字节(8位+1位R/W)后SDA保持高电平(无拉低),说明从机未应答:

  • 测从机VCC是否为3.3V,GND是否共地;
  • 查从机地址:AT24C02为0x50,SSD1306为0x78,注意7位地址左移后LSB为0(写)或1(读)
  • I2C_MasterWriteByte()Addr参数必须传7位地址(如0x50),函数内部自动左移。

5.3 故障3:读取数据时SDA持续低电平——诊断NACK与STOP缺失

波形显示地址发送后,SDA在第9个时钟沿(ACK位)被从机拉低,但后续无数据:

  • 检查I2C_MasterReadByte()IsLast参数是否正确传递;若读1字节却设IsLast=0,主机不会清除ACK,从机拒绝发送。
  • I2C1->SR1寄存器实时读取:若AF=1(bit10),则NACK发生,需检查从机状态或地址。
5.3.1 快速寄存器诊断命令(Keil MDK调试模式)
// 在调试窗口输入以下命令,实时查看状态 // 查看I2C1状态寄存器 > _mem32(0x40005400) // SR1地址 // 查看GPIOB输出数据寄存器 > _mem32(0x4001080C) // ODR地址 // 查看AFIO重映射寄存器 > _mem32(0x40010004) // MAPR地址
  • 0x40005400是I2C1_SR1地址,若返回值bit10=1,即AF=1,确认NACK。
  • 0x4001080C是GPIOB_ODR,若返回值bit6=0且bit7=0,则PB6/PB7未上拉。

注意:所有寄存器地址基于STM32F103参考手册(RM0008)Section 2.3,不可与F4/F7系列混淆。本包适配F103系列,若用于F407需修改RCC时钟使能位(APB1ENR bit21→bit22)及GPIO端口(GPIOB→GPIOB)。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询