展芯片詳解:從I2C原理到STM32實(shí)戰(zhàn)驅(qū)動(dòng))
做單片機(jī)或者嵌入式開(kāi)發(fā)總會(huì)有那么一天你的GPIO不夠用了。板子上一堆傳感器、按鍵、LED、撥碼開(kāi)關(guān)都要接MCU引腳就那么幾十個(gè)還要留出一堆給SPI Flash、顯示屏、電機(jī)驅(qū)動(dòng)。我印象特別深的一次一個(gè)項(xiàng)目硬件已經(jīng)打樣了結(jié)果產(chǎn)品經(jīng)理臨時(shí)說(shuō)要加8個(gè)按鍵和幾個(gè)狀態(tài)燈MCU引腳一個(gè)都不剩。當(dāng)時(shí)唯一的出路就是掛一顆PCA9555搞定一切軟件上根本沒(méi)動(dòng)太多硬件上就多了一顆芯片和兩根線。這篇文章就把PCA9555這顆I2C GPIO擴(kuò)展芯片講透從內(nèi)部原理、通信機(jī)制到裸機(jī)驅(qū)動(dòng)、Linux驅(qū)動(dòng)再結(jié)合STM32WBA65這顆新出的高性能BLE芯片做實(shí)戰(zhàn)演示。如果你也遇到過(guò)引腳告急、或者想弄明白I2C通信到底怎么工作這篇文章可以直接當(dāng)參考手冊(cè)用。1. PCA9555到底是什么從管腳到寄存器逐個(gè)啃PCA9555是NXP原Philips出品的一款經(jīng)典I2C接口GPIO擴(kuò)展芯片24腳封裝提供16路雙向IO。市面上還有PCA9535、PCF8575等類似型號(hào)但PCA9555有個(gè)關(guān)鍵區(qū)別——它支持中斷輸出并且內(nèi)部有極性反轉(zhuǎn)寄存器用起來(lái)靈活很多。1.1 管腳功能與地址配置先看管腳布局。PCA9555的電源范圍是2.3V到5.5V對(duì)3.3V和5V系統(tǒng)都很友好IO口輸出電流能達(dá)到25mA灌電流直接驅(qū)動(dòng)LED完全沒(méi)問(wèn)題不需要再加驅(qū)動(dòng)三極管。關(guān)鍵管腳分幾類電源與地VCC、GND旁邊要放0.1uF去耦電容我習(xí)慣再加一個(gè)10uF電解電容電源更穩(wěn)。I2C接口SCL、SDA標(biāo)準(zhǔn)I2C引腳開(kāi)漏結(jié)構(gòu)。中斷輸出INT低電平有效。當(dāng)輸入引腳狀態(tài)發(fā)生變化時(shí)INT會(huì)拉低MCU可以通過(guò)這個(gè)引腳感知外部事件不用一直輪詢I2C總線。這個(gè)功能特別適合按鍵矩陣不然你只能不停讀I2CCPU占用高不說(shuō)總線也容易被占滿。地址選擇A0、A1、A2這三個(gè)腳決定芯片的I2C從機(jī)地址懸空時(shí)內(nèi)部下拉為低電平。IO口P0組P0.0~P0.7和P1組P1.0~P1.7共16路。地址計(jì)算規(guī)則是這樣的PCA9555的固定地址頭是0100即0x40的bit7~bit4加上A2、A1、A0組成的三個(gè)地址位最后一位是讀寫(xiě)位。所以7位設(shè)備地址范圍是0x20~0x27對(duì)應(yīng)到8位讀寫(xiě)地址就是0x40~0x4F。具體對(duì)應(yīng)關(guān)系是A2A1A07位設(shè)備地址寫(xiě)地址(8位)讀地址(8位)0000x200x400x410010x210x420x431110x270x4E0x4F這就是為什么一片I2C總線上最多能掛8顆PCA9555理論上可以擴(kuò)展出128個(gè)IO當(dāng)然實(shí)際還要考慮總線上拉電容和地址沖突。注意很多國(guó)產(chǎn)兼容芯片比如TCA9555地址完全一樣但有些便宜芯片的地址映射可能不同用之前一定要看對(duì)應(yīng)型號(hào)的數(shù)據(jù)手冊(cè)別想當(dāng)然。1.2 寄存器地圖六個(gè)寄存器搞定所有操作PCA9555之所以用起來(lái)簡(jiǎn)單是因?yàn)樗阉泄δ芏加成涞搅?個(gè)寄存器上通過(guò)I2C讀寫(xiě)這些寄存器就能完成全部配置和數(shù)據(jù)操作。寄存器名稱偏移地址(0/1代表P0/P1)功能說(shuō)明上電默認(rèn)值輸入端口寄存器0x00 / 0x01讀取IO引腳當(dāng)前電平只讀輸出端口寄存器0x02 / 0x03設(shè)置IO輸出電平0xFF極性反轉(zhuǎn)寄存器0x04 / 0x05反轉(zhuǎn)輸入極性0x00配置寄存器0x06 / 0x070輸出1輸入0xFF這里有個(gè)很重要的細(xì)節(jié)上電默認(rèn)值是所有引腳都是輸入模式配置寄存器默認(rèn)0xFF輸出寄存器默認(rèn)0xFF。這意味著如果你要配置某個(gè)引腳為輸出并輸出低電平順序必須是先把配置寄存器對(duì)應(yīng)位設(shè)成0輸出模式再設(shè)輸出寄存器為0。如果反過(guò)來(lái)可能會(huì)產(chǎn)生一個(gè)短暫的高電平毛刺在驅(qū)動(dòng)MOS管、繼電器的時(shí)候容易造成誤動(dòng)作。極性反轉(zhuǎn)寄存器用得不多但有時(shí)候能救命。假如你接了一排按鍵按下時(shí)引腳變低松開(kāi)時(shí)變高你希望讀到的值是按下為1就可以把對(duì)應(yīng)位設(shè)置成1這樣輸入寄存器讀出來(lái)的結(jié)果就變成邏輯取反了。省去在軟件里做異或操作的麻煩少幾行代碼多一分清晰。2. I2C通信機(jī)制搞懂時(shí)序才能寫(xiě)好驅(qū)動(dòng)PCA9555掛在I2C總線上I2C通信的核心就是兩根線SCL時(shí)鐘線和SDA數(shù)據(jù)線都是開(kāi)漏輸出必須接上拉電阻到VCC。很多新手在這上面栽跟頭我后面會(huì)詳細(xì)說(shuō)。2.1 地址幀和數(shù)據(jù)幀長(zhǎng)什么樣I2C通信以起始條件START開(kāi)始SCL為高時(shí)SDA從高跳變到低。結(jié)束條件STOP相反SCL為高時(shí)SDA從低跳變到高。起始條件之后發(fā)送的是設(shè)備地址幀一共8位7位設(shè)備地址 1位讀寫(xiě)位0表示寫(xiě)1表示讀。以PCA9555地址0x20為例寫(xiě)操作時(shí)發(fā)送的字節(jié)是0100 00000x40讀操作是0100 00010x41。發(fā)送完地址幀后從設(shè)備會(huì)回一個(gè)ACK位SDA拉低。如果等不到ACK說(shuō)明設(shè)備不存在、地址不對(duì)或者硬件有問(wèn)題。數(shù)據(jù)幀的格式取決于你要訪問(wèn)寄存器。寫(xiě)寄存器時(shí)先發(fā)寄存器偏移地址比如0x06對(duì)應(yīng)配置寄存器再發(fā)數(shù)據(jù)。注意PCA9555支持自動(dòng)地址自增連續(xù)寫(xiě)多個(gè)字節(jié)時(shí)不用重復(fù)發(fā)送寄存器地址。但讀寄存器時(shí)有兩種方式當(dāng)前地址讀直接發(fā)讀地址讀取上次操作位置的數(shù)據(jù)。隨機(jī)地址讀先用寫(xiě)操作發(fā)送寄存器偏移地址產(chǎn)生一個(gè)重復(fù)起始條件Repeated START再切到讀模式。我強(qiáng)烈建議大家用隨機(jī)地址讀邏輯清晰也方便讀連續(xù)寄存器。我曾經(jīng)見(jiàn)過(guò)同行用當(dāng)前地址讀結(jié)果因?yàn)樯弦淮尾僮魑恢貌淮_定讀回來(lái)的數(shù)據(jù)位置老是錯(cuò)排查了半天。I2C的時(shí)鐘頻率支持100kHz標(biāo)準(zhǔn)模式、400kHz快速模式PCA9555本身還支持1MHz高速模式。默認(rèn)用400kHz就行總線長(zhǎng)度短、負(fù)載輕的時(shí)候完全沒(méi)問(wèn)題。2.2 為什么I2C必須用開(kāi)漏輸出加上拉電阻這個(gè)問(wèn)題是網(wǎng)上被問(wèn)爆的也是面試嵌入式崗位的高頻題。I2C協(xié)議從設(shè)計(jì)上就要求支持多設(shè)備共享總線同一時(shí)刻可能有多個(gè)設(shè)備都要往總線上發(fā)數(shù)據(jù)。如果用推挽輸出Push-Pull兩個(gè)設(shè)備同時(shí)一個(gè)輸出高、一個(gè)輸出低就會(huì)直接懟在一起。輸出高的那個(gè)引腳要通過(guò)低阻抗路徑灌入另一個(gè)設(shè)備輕則邏輯錯(cuò)誤重則燒管子。開(kāi)漏輸出的結(jié)構(gòu)就相當(dāng)于一個(gè)MOS管的漏極只能主動(dòng)拉低輸出0拉高是完全靠外部上拉電阻完成的。這樣有兩個(gè)好處線與特性任何一個(gè)設(shè)備輸出低總線就是低電平天然實(shí)現(xiàn)了多設(shè)備通信的仲裁。電平轉(zhuǎn)換方便上拉電阻接3.3V各設(shè)備即使工作在不同電壓域比如一個(gè)5V的設(shè)備和3.3V的MCU由于開(kāi)漏結(jié)構(gòu)不主動(dòng)輸出高總線電平由3.3V決定天然兼容。生活化理解就是I2C總線像一條公交專線所有設(shè)備都在同一個(gè)站臺(tái)上車誰(shuí)要發(fā)數(shù)據(jù)就把車按住拉低沒(méi)人按的時(shí)候車自然停在高位上拉。2.3 上拉電阻怎么算要弄清兩個(gè)邊界上拉電阻阻值不能隨便選我之前踩過(guò)坑用了1kΩ的電阻結(jié)果總線負(fù)載太重拉低時(shí)電流太大通信不穩(wěn)定用過(guò)100kΩ的上升沿太緩信號(hào)變形高速模式下直接不通。選擇上拉電阻要注意兩個(gè)極端條件最小值限制設(shè)備輸出低電平時(shí)VCC經(jīng)過(guò)上拉電阻流到設(shè)備內(nèi)部MOS管灌電流不能超過(guò)設(shè)備允許的最大值一般為3mA~5mA。按3.3V電源、3mA灌電流來(lái)算Rmin 3.3V / 3mA ≈ 1.1kΩ所以低于1kΩ就容易出問(wèn)題。最大值限制總線上所有設(shè)備的引腳電容加上走線分布電容會(huì)形成一個(gè)RC充電回路。如果電阻太大上升沿時(shí)間常數(shù)過(guò)大在400kHz快速模式下信號(hào)拉不上去導(dǎo)致通信失敗。I2C規(guī)范要求快速模式下上升時(shí)間不超過(guò)300ns假設(shè)總線電容約200pF那么 Rmax ≈ 300ns / 0.9 × 200pF ≈ 1.7kΩ。綜合來(lái)看3.3V系統(tǒng)、400kHz單主設(shè)備、總線上不超過(guò)3~4個(gè)設(shè)備2.2kΩ到4.7kΩ是合理范圍??偩€越長(zhǎng)、設(shè)備越多阻值適當(dāng)小一些。我自己最常用的組合是4.7kΩ配100k模式2.2kΩ配400k模式基本沒(méi)翻過(guò)車。3. 驅(qū)動(dòng)開(kāi)發(fā)實(shí)戰(zhàn)從裸機(jī)到Linux用戶態(tài)PCA9555的驅(qū)動(dòng)寫(xiě)法分幾個(gè)層次最底層是操作I2C控制器或者模擬I2C時(shí)序往上才是PCA9555寄存器讀寫(xiě)邏輯。這里講兩種最常見(jiàn)的實(shí)戰(zhàn)場(chǎng)景。3.1 裸機(jī)環(huán)境GPIO模擬I2C的實(shí)現(xiàn)思路很多國(guó)產(chǎn)單片機(jī)沒(méi)有硬件I2C或者I2C外設(shè)用起來(lái)別扭狀態(tài)機(jī)復(fù)雜、出錯(cuò)不好排查我就直接用GPIO模擬。模擬I2C只要注意時(shí)序細(xì)節(jié)可靠性和硬件I2C沒(méi)差別。核心代碼分成幾層// 引腳宏定義按實(shí)際硬件修改 #define I2C_SCL_PIN GPIO_PIN_6 #define I2C_SDA_PIN GPIO_PIN_7 #define I2C_GPIO_PORT GPIOB #define SCL_HIGH() HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_SET) #define SCL_LOW() HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_RESET) #define SDA_HIGH() HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_SET) #define SDA_LOW() HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_RESET) #define SDA_READ() HAL_GPIO_ReadPin(I2C_GPIO_PORT, I2C_SDA_PIN) // 起始條件SCL高時(shí)SDA由高變低 static void i2c_start(void) { SDA_HIGH(); SCL_HIGH(); delay_us(5); SDA_LOW(); delay_us(5); SCL_LOW(); } // 停止條件SCL高時(shí)SDA由低變高 static void i2c_stop(void) { SDA_LOW(); SCL_HIGH(); delay_us(5); SDA_HIGH(); delay_us(5); } // 發(fā)送一字節(jié)返回ACK狀態(tài) static uint8_t i2c_write_byte(uint8_t data) { uint8_t i; for (i 0; i 8; i) { if (data 0x80) { SDA_HIGH(); } else { SDA_LOW(); } data 1; delay_us(3); SCL_HIGH(); delay_us(5); SCL_LOW(); delay_us(3); } // 釋放SDA等待從設(shè)備ACK SDA_HIGH(); delay_us(3); SCL_HIGH(); delay_us(3); uint8_t ack SDA_READ(); // 低電平表示ACK SCL_LOW(); return ack 0 ? 0 : 1; }注意SDA方向的切換。發(fā)送數(shù)據(jù)時(shí)SDA要配置為輸出模式等發(fā)送完釋放總線時(shí)要把引腳切回輸入模式或者寫(xiě)1讓開(kāi)漏輸出釋放否則你拉高了引腳從設(shè)備想拉低都拉不動(dòng)。PCA9555的寫(xiě)操作封裝如下void pca9555_write_reg(uint8_t dev_addr, uint8_t reg, uint8_t data) { i2c_start(); i2c_write_byte(dev_addr 1); // 寫(xiě)地址 i2c_write_byte(reg); // 寄存器偏移 i2c_write_byte(data); // 數(shù)據(jù) i2c_stop(); }讀操作要用到重復(fù)起始條件uint8_t pca9555_read_reg(uint8_t dev_addr, uint8_t reg) { uint8_t data; i2c_start(); i2c_write_byte(dev_addr 1); // 寫(xiě)地址先指定寄存器 i2c_write_byte(reg); i2c_start(); // 重復(fù)起始條件 i2c_write_byte((dev_addr 1) | 0x01); // 讀地址 data i2c_read_byte(); // 讀數(shù)據(jù) i2c_stop(); return data; }讀字節(jié)時(shí)要特別注意最后一個(gè)字節(jié)要回NACK給設(shè)備告訴它“不用繼續(xù)傳了”然后發(fā)停止條件。很多新手在這里漏掉NACK導(dǎo)致多讀了一字節(jié)或者總線狀態(tài)錯(cuò)亂。3.2 Linux環(huán)境用i2c-dev在用戶態(tài)直接讀寫(xiě)在嵌入式Linux里用PCA9555有兩條路一是寫(xiě)內(nèi)核驅(qū)動(dòng)注冊(cè)成gpio_chip這樣PCA9555的引腳就能像原生GPIO那樣被/sys/class/gpio或libgpiod使用二是直接留出I2C總線設(shè)備節(jié)點(diǎn)/dev/i2c-x應(yīng)用層用ioctl讀寫(xiě)寄存器這種方法快速驗(yàn)證硬件特別方便。對(duì)于快速驗(yàn)證我推薦第二種方案核心流程#include stdio.h #include fcntl.h #include linux/i2c-dev.h #include sys/ioctl.h int pca9555_write_reg(int fd, uint8_t reg, uint8_t data) { uint8_t buf[2] { reg, data }; if (write(fd, buf, 2) ! 2) { perror(write error); return -1; } return 0; } int pca9555_read_reg(int fd, uint8_t reg, uint8_t *data) { if (write(fd, reg, 1) ! 1) { perror(write reg error); return -1; } if (read(fd, data, 1) ! 1) { perror(read error); return -1; } return 0; } int main(int argc, char **argv) { int fd open(/dev/i2c-1, O_RDWR); if (fd 0) { perror(open i2c); return -1; } // 設(shè)置從機(jī)地址0x20為PCA9555基地址 if (ioctl(fd, I2C_SLAVE, 0x20) 0) { perror(i2c slave); return -1; } // 配置P0全部為輸出 pca9555_write_reg(fd, 0x06, 0x00); // P0輸出 1010 1010 pca9555_write_reg(fd, 0x02, 0xAA); uint8_t val; pca9555_read_reg(fd, 0x00, val); printf(Input P0 0x%02X\n, val); close(fd); return 0; }編譯命令gcc -o pca_test pca_test.c。然后運(yùn)行前確認(rèn)I2C設(shè)備節(jié)點(diǎn)存在并且當(dāng)前用戶有權(quán)限操作可能需要加sudo。Linux內(nèi)核驅(qū)動(dòng)的方式也很值得研究。內(nèi)核里其實(shí)已經(jīng)有現(xiàn)成的pca953x驅(qū)動(dòng)支持PCA9555在設(shè)備樹(shù)里配置好節(jié)點(diǎn)內(nèi)核會(huì)自動(dòng)注冊(cè)成GPIO控制器然后你在應(yīng)用層就能直接操作了。設(shè)備樹(shù)節(jié)點(diǎn)大致是這樣i2c1 { pca9555: pca955520 { compatible nxp,pca9555; reg 0x20; gpio-controller; #gpio-cells 2; interrupt-parent gpio1; interrupts 3 IRQ_TYPE_LEVEL_LOW; }; };這樣 Linux 里就能用gpiod_get或者命令行工具直接操作擴(kuò)展IO。用libgpiod命令測(cè)試gpioset gpiochip0 01 # 把PCA9555的P0.0置高 gpioget gpiochip0 5 # 讀P0.5電平4. 實(shí)戰(zhàn)落地用STM32WBA65連接PCA9555STM32WBA65是ST新一代藍(lán)牙低功耗芯片主打高性能BLE和豐富的外設(shè)資源但它的GPIO數(shù)量相比傳統(tǒng)大容量系列還是偏緊的畢竟射頻天線和射頻前端也占了PIN。用PCA9555來(lái)做外設(shè)擴(kuò)展算是處理MCU引腳不足的標(biāo)準(zhǔn)操作。4.1 硬件連接與初始化接線特別簡(jiǎn)單PCA9555的SCL、SDA分別接到STM32WBA65的對(duì)應(yīng)I2C引腳我用的是I2C1的外設(shè)PB6/PB7然后SCL、SDA各接一個(gè)4.7kΩ上拉電阻到3.3V。A0~A2全部接地INT引腳接到MCU的一個(gè)外部中斷輸入引腳比如PA0用于按鍵事件喚醒。關(guān)鍵初始化代碼void MX_I2C1_Init(void) { I2C_HandleTypeDef hi2c1; hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 400000; // 400kHz快速模式 hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 0; hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 0; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; HAL_I2C_Init(hi2c1); }順便說(shuō)一下GPIO配置I2C引腳要配置為復(fù)用開(kāi)漏模式同時(shí)開(kāi)啟內(nèi)部上拉或者外部上拉。如果你已經(jīng)接了外部上拉電阻內(nèi)部上拉開(kāi)不開(kāi)都行但如果沒(méi)接外部電阻就一定要開(kāi)內(nèi)部上拉不然通信上電后就是死路一條。另外PCA9555的INT引腳要配置成輸入上拉模式因?yàn)镮NT是開(kāi)漏輸出靠外部上拉拉高拉低表示有事件。4.2 核心讀寫(xiě)示例配置PCA9555的P0口全部為輸出用于控制8個(gè)LEDvoid pca9555_config_output(void) { uint8_t data 0x00; // 0輸出模式 HAL_I2C_Mem_Write(hi2c1, 0x40, 0x06, I2C_MEMADD_SIZE_8BIT, data, 1, 100); }注意HAL庫(kù)的HAL_I2C_Mem_Write內(nèi)部已經(jīng)包含了發(fā)寄存器地址寫(xiě)數(shù)據(jù)的完整流程第三個(gè)參數(shù)就是寄存器偏移地址傳0x06就是寫(xiě)配置寄存器P0口。設(shè)備地址傳0x40是8位寫(xiě)地址模式7位地址0x20左移一位HAL庫(kù)的地址參數(shù)直接傳8位形式別搞混??刂芁ED流水燈void led_flow(void) { uint8_t pattern 0x01; for (int i 0; i 8; i) { HAL_I2C_Mem_Write(hi2c1, 0x40, 0x02, I2C_MEMADD_SIZE_8BIT, pattern, 1, 100); pattern 1; HAL_Delay(200); } }按鍵檢測(cè)用中斷方式void EXTI0_IRQHandler(void) { if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) ! RESET) { uint8_t input_val; // 讀取P0輸入寄存器假設(shè)按鍵接在P0.0-P0.3 HAL_I2C_Mem_Read(hi2c1, 0x41, 0x00, I2C_MEMADD_SIZE_8BIT, input_val, 1, 100); // 處理按鍵值把高四位對(duì)應(yīng)的按鍵狀態(tài)通過(guò)串口上報(bào) char msg[16]; snprintf(msg, sizeof(msg), Key:0x%X, input_val 0x0F); HAL_UART_Transmit(huart1, (uint8_t*)msg, strlen(msg), 100); __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0); } }這里有個(gè)細(xì)節(jié)讀取輸入寄存器時(shí)設(shè)備地址傳0x41也就是讀地址這和寫(xiě)地址是差一位的。HAL庫(kù)的函數(shù)設(shè)計(jì)里DevAddress參數(shù)是完整的8位地址所以讀操作你要自己或上0x01。5. 踩坑實(shí)錄與排查技巧沒(méi)有不翻車的嵌入式開(kāi)發(fā)PCA9555的坑我也踩了不少。把最典型的幾個(gè)問(wèn)題和排查方法總結(jié)一下希望能幫你節(jié)省半天時(shí)間。5.1 常見(jiàn)問(wèn)題速查表現(xiàn)象可能原因排查方法I2C讀寫(xiě)無(wú)ACK地址錯(cuò)誤、芯片沒(méi)供電、SDA上拉缺失用邏輯分析儀抓起始條件和設(shè)備地址確認(rèn)發(fā)送的從機(jī)地址是否符合A0~A2配置SDA線一直是低某設(shè)備拉死總線、從機(jī)沒(méi)釋放SDA逐個(gè)斷開(kāi)從設(shè)備查找兇手或者把SDA引腳砍掉后重新上電看是否恢復(fù)高電平高速模式下通信不穩(wěn)定上拉電阻太大、總線電容過(guò)大、從設(shè)備不支持換2.2kΩ上拉降低頻率到100k縮短總線走線長(zhǎng)度輸出寄存器寫(xiě)入但I(xiàn)O不變配置寄存器還是輸入模式寫(xiě)0x06/0x07配置寄存器將對(duì)應(yīng)位設(shè)為0輸出中斷一直觸發(fā)輸入引腳懸空、極性反轉(zhuǎn)位沒(méi)關(guān)把輸入引腳配置成內(nèi)部上拉或下拉確保輸入電平確定讀回的數(shù)據(jù)不對(duì)讀地址寫(xiě)錯(cuò)、忘了重復(fù)起始條件參照3.1的隨機(jī)地址讀流程最好先用邏輯分析儀確認(rèn)時(shí)序5.2 邏輯分析儀實(shí)測(cè)心得調(diào)試I2C設(shè)備邏輯分析儀是神器操作也很簡(jiǎn)單把SDA通道接到邏輯分析儀的CH0SCL接到CH1共地設(shè)置采樣率10MHz以上然后解碼設(shè)置里選I2C協(xié)議填入7位設(shè)備地址0x20。抓波形的時(shí)候重點(diǎn)看幾個(gè)東西起始條件是否干凈SDA在SCL高電平跳變到低這一段上不能有毛刺。地址字節(jié)的bit順序是否正確地址是MSB先發(fā)第一個(gè)字節(jié)如果是0x40二進(jìn)制是0100 0000。ACK位是否正常9個(gè)時(shí)鐘周期中第9個(gè)周期SDA應(yīng)該被從機(jī)拉低如果一直是高說(shuō)明從機(jī)沒(méi)響應(yīng)。重復(fù)起始條件是否被正確識(shí)別讀操作時(shí)如果沒(méi)有重復(fù)起始直接拉低SDA/拉高SCL很多從機(jī)會(huì)不認(rèn)。我這幾年實(shí)際調(diào)試的經(jīng)驗(yàn)是90%的I2C問(wèn)題都出在硬件連接和時(shí)序不規(guī)范上軟件邏輯反而是次要的。常見(jiàn)的總線卡死就是某個(gè)設(shè)備沒(méi)釋放SDA導(dǎo)致的。這時(shí)候最快的處理就是把SDA、SCL的GPIO先復(fù)位配置連續(xù)切換十幾個(gè)時(shí)鐘信號(hào)把總線上的設(shè)備狀態(tài)機(jī)重置一遍很多問(wèn)題就迎刃而解了。6. 擴(kuò)展應(yīng)用與幾個(gè)實(shí)用技巧PCA9555不只是擴(kuò)個(gè)LED、按鍵這么簡(jiǎn)單我用的比較多的是三塊場(chǎng)景帶中斷的按鍵矩陣16路IO接16個(gè)按鍵每個(gè)按鍵按下都觸發(fā)INT中斷MCU通過(guò)一次I2C讀取就拿到全部按鍵狀態(tài)比起獨(dú)立按鍵省引腳不說(shuō)還不用輪詢功耗能壓得很低適合電池供電的可穿戴設(shè)備。STM32WBA65主打低功耗BLE配合帶中斷擴(kuò)展IO是最佳搭檔。產(chǎn)品配置撥碼開(kāi)關(guān)很多工控設(shè)備需要設(shè)備地址、波特率等參數(shù)撥碼設(shè)置PCA9555讀回來(lái)的值直接對(duì)應(yīng)配置表省去出廠燒錄環(huán)節(jié)。LED驅(qū)動(dòng)與指示燈復(fù)用PCA9555每個(gè)IO可以輸出25mA電流直接驅(qū)動(dòng)LED綽綽有余。還可以利用極性反轉(zhuǎn)寄存器做LED閃爍的邏輯反轉(zhuǎn)省去在MCU里做狀態(tài)翻轉(zhuǎn)。再說(shuō)一個(gè)技巧如果你用的是STM32的HAL庫(kù)I2C的通信過(guò)程有超時(shí)參數(shù)務(wù)必設(shè)一個(gè)合理的超時(shí)值。我習(xí)慣設(shè)100ms這樣當(dāng)從設(shè)備異常時(shí)MCU不會(huì)永遠(yuǎn)卡死在等待ACK的死循環(huán)里系統(tǒng)能及時(shí)報(bào)錯(cuò)和恢復(fù)。個(gè)人經(jīng)驗(yàn)里再補(bǔ)充一個(gè)PCA9555的IO上電瞬間是高電平輸出寄存器默認(rèn)0xFF如果你用這個(gè)IO控制繼電器或者PMOS開(kāi)關(guān)上電瞬間負(fù)載會(huì)被打開(kāi)這可能帶來(lái)安全問(wèn)題。解決方法是讓PCA9555先處于復(fù)位狀態(tài)如果有復(fù)位引腳或者在配置階段盡快把所有IO輸出置低再切換成輸出模式。如果硬件上做不到至少要在電路設(shè)計(jì)時(shí)考慮上電瞬間的行為。最后分享一個(gè)調(diào)試小技巧裸機(jī)環(huán)境下如果I2C怎么都讀不到數(shù)據(jù)先用萬(wàn)用表量一下SCL和SDA的電平正??臻e時(shí)都是高電平。如果量到某個(gè)引腳是0.8V左右不穩(wěn)定的中間電平那大概率是上拉電阻沒(méi)焊好或者焊反了。別問(wèn)我怎么知道這個(gè)細(xì)節(jié)的——我當(dāng)年在PCA9555上花了整整一下午最后發(fā)現(xiàn)是飛線斷裂導(dǎo)致SDA懸空電平漂在中間邏輯分析儀看起來(lái)全是亂碼。先把電源、上拉、焊接這些基礎(chǔ)項(xiàng)排查完再動(dòng)軟件你會(huì)少走很多彎路。