STM32的I2C死活不通,最后发现是内部上拉惹的祸

说出来有点丢人。上周调一块STM32F103的板子,I2C死活不通,示波器一挂,SCL波形跟心电图似的,SDA干脆一条直线。我当时第一反应——一定是初始化代码写错了。

改了一遍又一遍。HAL库的I2C初始化翻来覆去对了三四遍,时序参数从标准模式切到快速模式再切回来,GPIO配置也检查了,Open Drain没错,AFIO也开了。没用。

又怀疑是从机地址不对。拿逻辑分析仪抓,发现主机根本没发start信号。不对,发了,但从机没应答。再一看,SDA线上拉不到3.3V,卡在1.8V晃悠。这就很诡异了——板子上根本没焊外部上拉电阻,纯靠STM32内部的上拉撑着。

// 我当时GPIO初始化大概是这样的
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOB_CLK_ENABLE();

GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;  // PB6=SCL, PB7=SDA
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;          // 开漏输出
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

看着挺标准的对吧?问题就出在这儿——内部上拉没开。

等等,你可能要说:开漏输出配内部上拉?没搞错?

我之前也是这么想的。开漏嘛,外部拉电阻的活儿,跟内部上拉有啥关系。但ST的参考手册里写得很清楚:当GPIO配置为开漏输出时,内部上拉电阻仍然可以使能,而且某些引脚在开漏模式下,内部上拉弱到什么程度呢?弱到你挂一个从机就拉不动了。

F103的内部上拉电阻,参考手册上写的是典型值30kΩ到50kΩ之间,注意这只是典型值——有些引脚更离谱,都到70kΩ了。I2C标准模式要求上拉电阻一般在4.7kΩ左右,快速模式甚至要求更低。你算算,差了快一个数量级。

那为什么平时STM32的内部上拉用着好像也没出问题?因为大多数时候你操作的GPIO是推挽输出,推挽模式下内部上拉那点电流根本无所谓。但I2C是开漏,靠上拉电阻把线拉高。内部那三五十kΩ的电阻,总线电容稍微大一点,RC充电时间常数就上去了,上升沿变缓,从机就判断不了高电平。

解决办法?最稳妥的法子就是焊外部上拉电阻,两颗4.7kΩ上去世界清净,0805封装就行。要是板子已经做好了不方便改,就把GPIO配置改成这样:

GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Pull = GPIO_PULLUP;           // 开漏模式下也要开上拉!
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

加了这行 GPIO_PULLUP,波形直接恢复正常。但说实话,这招有前提条件——总线电容要小、从机最多一个、速率别超100kHz。你要是挂了两个以上的从机,或者想跑400kHz快速模式,还是乖乖焊外部电阻吧。内部上拉当个救急手段可以,长期用不靠谱。

说到这,我又想起来之前用STM32G0的时候踩过另一个坑。当时从F4平台迁移一个项目到G0,I2C部分直接把CubeMX生成的初始化代码拷过去。编译过了,I2C也能通信,但跑着跑着就丢数据。不是每次都能复现,有时候跑一两个小时才出现一次。

查了两天。G0和F4的I2C外设在架构上有区别——F4用的是传统I2C模块,Timing靠I2C_CCRI2C_TRISE两个寄存器配;G0用的是新的I2C模块,靠一个I2C_TIMINGR寄存器搞定。CubeMX生成的代码里,那个值在不同系列之间完全不通用。

/* I2C_TIMINGR 寄存器配置 (STM32G0, 100kHz) */
I2cHandle.Init.Timing = 0x00201D2B;

这串数字看着就像随便写的,但它其实是根据系统时钟、I2C速率和rise time算出来的。直接把F4的配置搬上来,表面能跑,实际时序已经偏了。最坑的是HAL的 HAL_I2C_Init 函数不校验timing值的合理性,你传个驴唇不对马嘴的值它也说ok,然后通信就在临界状态飘着。

还有个细节:STM32的I2C从机地址。我之前一直用7位地址,但HAL库的 HAL_I2C_Master_TransmitHAL_I2C_Slave_Receive 这些函数,地址参数到底传7位还是8位,不同版本的HAL库处理方式不一样。早期的HAL库要求左移一位后的地址(就是7位地址左移1位拼上R/W位),后来有些版本又改成了直接传原始7位地址。要是库版本升级了,这块也得跟着改。

回头看我那个板子,后来测了一下PCB布线,SDA和SCL走了差不多15cm还拐了两个直角、过了一个过孔。15cm在100kHz下倒不至于出问题,但加上弱上拉、加上过孔的寄生电容,拉不起来就完全不冤了。现在画板子凡是遇到I2C,我的原则就是:线能短则短,上拉电阻别省,从机超过一个直接上PCA9548做I2C MUX。踩过的坑多了自然就老实了。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐