Note 01 of 02: 笔记 01 / 02:I²C Communication and OLED DebuggingI²C 通信与 OLED 调试
Studying pull-ups, transfer timing and acknowledgment, then comparing software and peripheral I²C when troubleshooting an OLED.从上拉、电平、地址与应答,研究软件和硬件 I²C 的区别,以及 OLED 不显示时怎样定位问题。
The program compiled and flashed without complaint, and the OLED simply would not light; only after moving to another pair of ordinary GPIO pins with software-emulated I²C did it come on. That happened during the Tianjin internship, when I could not have said what separated the two kinds of I²C. Both end up talking over the same two lines, SDA and SCL, so where does the difference come from? The voltages on those two lines are the place to start.
程序编译、烧录都没问题,OLED 就是不亮,最后换了一组普通 GPIO、改用软件模拟 I²C 才亮起来。这是天津实习时碰上的事,那时我连两种 I²C 的区别都说不上来。两种实现最后都靠 SDA 和 SCL 这两根线通信,差别到底出在哪?先从这两根线上的电压看起。
I²C Open-Drain Outputs and Pull-UpsI²C 开漏输出与上拉电阻
I²C normally uses open-drain outputs. When the output transistor conducts, the device pulls the line low; when it switches off, the device lets go and a pull-up resistor brings the line back high. Controller and peripheral can both pull SDA low, and only after the sender lets go does the receiver get a chance to acknowledge. The NXP I²C specification explains the bus levels through exactly this shared connection. In software I²C, writing a high level should be read as releasing the pin, followed by reading it back to confirm the line has actually risen.
I²C 一般用开漏输出。输出管导通,器件把线拉低;关断,就把线放开,由上拉电阻把它拉回高电平。主控和外设都能拉低 SDA,发送方松了手,接收方才有机会应答。NXP 的 I²C 规范就是用这种共用连接来解释总线电平的。写软件 I²C 时,输出高电平要理解成松开引脚,然后还得读一下,确认线真的升上去了。
Seen that way, an assignment in the program connects to the circuit. Forcing SDA high with a push-pull output while the peripheral happens to be pulling it low makes the two fight over one wire. With open drain, the question becomes how fast the line rises once released. Modules, wiring and input pins all add capacitance, and the voltage has to charge up through the pull-up resistor.
这么一想,程序里的一个赋值就和电路接上了。用推挽输出硬把 SDA 推高,偏偏外设这时在拉低,两边就会抢同一根线。换成开漏,问题就变成松手以后线升得有多快。模块、连线、输入引脚都会添电容,电压要靠上拉电阻一点点充上去。
So a pull-up cannot be picked just because the value is familiar. TI's SLVA689 gives tr = 0.8473 × Rp × Cb for the approximate rise from 30% to 70% of the pull-up voltage. Assume 100 pF of bus capacitance and a 4.7 kΩ pull-up, and the rise comes to about 398 ns, inside the 1000 ns Standard-mode limit and over the 300 ns Fast-mode limit. However short the software delay becomes, the line charges no faster.
所以上拉电阻不能只按常用值随手挑一个。TI 的 SLVA689 给了从上拉电压 30% 升到 70% 的近似式 tr = 0.8473 × Rp × Cb。假设总线电容 100 pF、上拉 4.7 kΩ,算出来约 398 ns,在标准模式 1000 ns 的上限以内,却超出了快速模式 300 ns 的上限。软件里的延时改得再短,这根线也不会因此充得更快。
A smaller resistor speeds the rise and also raises the current while the line is held low. That current follows from the pull-up voltage, the low-level voltage and the resistance, and it has to stay within the device's sink capability. Two modules that each carry pull-ups put them in parallel once connected. Looking at the module circuits before wiring and measuring real levels afterwards finds problems faster than changing delays in the program again and again.
电阻减小,上升是快了,线被拉低时的电流也跟着变大。这个电流要按上拉电压、低电平电压和电阻算出来,再对照器件允许的灌电流。两块模块各自带着上拉,接到一起以后电阻还会并联。接线前看看模块电路,接好后量量实际电平,比在程序里一遍遍改延时更容易找到问题。
I²C Transfer SequenceI²C 传输流程
In a timing diagram, the first things I look for are START, the address and the acknowledgment right after it. TI's bus guide draws the sequence clearly: SDA falling while SCL is high is START, then come the address and direction bit, and every byte is followed by one more clock for the receiver to acknowledge. During ordinary data bits, SDA must hold still for the whole time SCL is high.
看时序图,我最先找的是 START、地址和紧跟着的应答。TI 的总线说明把这串动作画得很清楚:SCL 为高时,SDA 由高变低就是 START;接着发地址和读写位,每个字节后面再给一个时钟,让接收方应答。传普通数据位时,SCL 为高的这段时间里,SDA 必须稳住不动。
Take a hypothetical 7-bit address of 0x3C: the first byte of a write is (0x3C << 1) | 0 = 0x78. Whether an interface wants the 7-bit address or a byte with the direction bit already in place depends on that interface's own convention. Get it wrong and every number in the code looks familiar while the address on the wire has been shifted twice. It is a small sum worth doing whenever I read a driver interface.
拿一个假设的 7 位地址 0x3C 算一下,写操作发出去的第一个字节是 (0x3C << 1) | 0 = 0x78。接口要的是 7 位地址,还是已经拼好读写位的字节,得看它自己的约定。不然程序里的数字个个眼熟,线上的地址却可能被移了两次位。读驱动接口时,这笔账值得顺手算一遍。
Once the address is sent, the transmitter releases SDA. A receiver holding it low during the ninth clock means ACK; leaving it high means NACK. Seeing eight data pulses is not enough, then; the ninth clock, and who is driving SDA during it, matter just as much. With ACK in hand, the next checks are the initialization commands and display data. Between a byte being accepted and the right picture appearing on screen lies the display controller's own processing.
地址发完,发送方松开 SDA。接收方在第九个时钟把它拉低就是 ACK,一直是高就是 NACK。所以只看到八个数据脉冲还不够,得接着看第九个时钟上 SDA 被谁拉着。有了 ACK,再往下核对初始化命令和显示数据。字节被收下,到屏幕上出现想要的画面,中间还隔着显示控制器自己的处理。
Software and Hardware I²C软件与硬件 I²C
In software I²C the program itself schedules pin toggles, reads and waits. Every step is visible, and moving to other free GPIO pins is easy. The price is that the CPU has to babysit the timing, so an interrupt or some other job can stretch the gap between two operations. Supporting clock stretching also means reading SCL after releasing it to check that it really went high, instead of assuming a fixed delay was enough.
软件 I²C 由程序自己安排引脚翻转、读取和等待,步骤一目了然,换到别的可用 GPIO 也方便。代价是 CPU 得亲自盯着时序,一来中断或者有别的活,两次操作之间的间隔就可能被拉长。要支持时钟拉伸,还得在松开 SCL 以后读一下它是不是真变高了,不能等一个固定延时就当时钟已经到位。
Hardware I²C hands bit transfers to a dedicated on-chip peripheral, while software configures the pins, enables the peripheral clock, sets the rate, runs the transaction flow and handles status and errors. ST's F1 I²C driver notes list those setup steps, which makes it much clearer what the peripheral needs before it works. The bus rules are the same, pull-ups and signal quality included. When comparing the two implementations, wiring, pull-ups, rate and transmitted content are best held fixed, changing one condition at a time wherever possible.
硬件 I²C 把位传输交给片上的专用外设,软件负责配引脚、开外设时钟、设速率、安排事务流程,再处理状态和错误。ST 的 F1 系列 I²C 驱动说明把这些配置环节列得很全,对照着看,外设要做哪些准备就清楚多了。总线规则还是那一套,上拉和信号质量照样要达标。拿两种实现比较时,接线、上拉、速率和发送内容最好都固定住,尽量一次只动一个条件。
Back then I changed two things at once, the GPIO pins and the communication implementation. To dig further, I would first run the software version on the very pins the I²C peripheral uses, then switch those pins back to the peripheral, watching START, the address and ACK each time. Only a comparison like that separates pin configuration, electrical connection and driver handling, one layer at a time.
当时我其实同时改了两样东西,换了 GPIO,也换了通信实现。要接着往下查,我会在硬件 I²C 对应的那组引脚上先跑软件版本,再切回外设版本,分别看 START、地址和 ACK。这么一比,差别出在引脚配置、电气连接还是驱动处理上,才能一层层分开。
BUSY and Device ErrataBUSY 与芯片勘误
The STM32F103's I²C does have silicon issues worth checking. Section 2.8.7 of ST's ES096 errata describes SDA and SCL already sitting high while the internal analogue filters can stay stuck at low, so BUSY gets set and the controller cannot generate START. The documented recovery uses GPIO to produce prescribed level changes, reads the pins to confirm them, and then reconfigures the peripheral.
STM32F103 的 I²C 确实有值得查的芯片问题。ST 的 ES096 勘误 2.8.7 描述过这样一种情况:SDA 和 SCL 实际已经是高电平,内部的模拟滤波器却可能还停在低电平状态,BUSY 于是被置位,主机发不出 START。勘误给的恢复步骤,是先用 GPIO 产生规定的电平变化、读引脚确认,再重新配置外设。
What makes it interesting is that the pin levels and the chip's internal state can disagree. When checking BUSY, I would watch SDA and SCL alongside it to tell a line genuinely held low from two released lines with the inside still reporting busy, then let the silicon revision and the erratum's conditions decide the fix, rather than blaming every dark screen on the same cause. If that Tianjin screen were back on my desk, this erratum is the first thing I would check.
这个问题有意思的地方在于,引脚上的电平和芯片内部的状态可能对不上。查 BUSY 时,我会同时看 SDA 和 SCL,分清是线真的被拉低了,还是两根线早已松开、内部却还显示忙,再结合芯片版本和勘误的触发条件决定怎么处理,别把每一次黑屏都算到同一个原因头上。天津那块屏要是重新摆到桌上,这条勘误会是我最先排查的方向。
A dark screen can be traced section by section through power, bus levels, address acknowledgment and display data, and each section that becomes clear makes the next measurement better aimed. That is also what software I²C has been worth to me. After writing out pin release, clock waits and acknowledgment reads step by step myself, reading a hardware peripheral's status bits finally tells me what the program is actually waiting for.
屏幕不亮,可以顺着供电、总线电平、地址应答、显示数据一段段查,每看清一段,下一次测量就更有的放矢。软件 I²C 对我的价值也在这儿:自己把松开引脚、等时钟、读应答一步步写出来以后,再去读硬件外设的状态位,就知道程序到底在等什么了。