驅(qū)動(dòng)移植與TCP通信實(shí)戰(zhàn))
簡(jiǎn)介這是一份基于STM32F407VE的W5500以太網(wǎng)通信工程采用HAL庫(kù)并集成WIZnet最新驅(qū)動(dòng)面向嵌入式開(kāi)發(fā)者及物聯(lián)網(wǎng)、工業(yè)控制場(chǎng)景已配置好時(shí)鐘、GPIO、SPI、中斷與協(xié)議?;A(chǔ)可直接編譯運(yùn)行以驗(yàn)證TCP/UDP通信也可作為應(yīng)用起點(diǎn)。壓縮包共206個(gè)文件、約8.22MB以66個(gè)頭文件與46個(gè)C源文件為主體涵蓋uvprojx/uvoptx工程配置、ioc圖形化設(shè)置、hex/axf固件、sct鏈接腳本及清理編譯中間文件的bat腳本目錄按Hardware、Internet、W5500、Sys等模塊劃分并含utility.c/h工具函數(shù)與board_config.h硬件定義。另有README說(shuō)明、License授權(quán)、CMSIS與RTE組件管理及JLink調(diào)試配置已有40人學(xué)習(xí)適合需快速搭建W5500應(yīng)用、減少驅(qū)動(dòng)移植與工程配置成本的開(kāi)發(fā)者。1. 用STM32F407VE硬接W5500到底比其他方案省在哪如果你正在做一個(gè)需要聯(lián)網(wǎng)上報(bào)數(shù)據(jù)的設(shè)備比如工業(yè)采集器、智能網(wǎng)關(guān)或者遠(yuǎn)程控制板手里又恰好選中了STM32F407VE那么以太網(wǎng)通信大概率會(huì)在兩條路線之間糾結(jié)用LwIP軟件協(xié)議棧還是用W5500這種硬件協(xié)議棧芯片。F407VE本身不帶PHY要用內(nèi)部MAC就得外掛PHY芯片再配LwIP光回調(diào)、信號(hào)量、內(nèi)存池就能折騰掉一大半開(kāi)發(fā)周期而W5500把TCP/IP協(xié)議棧做進(jìn)了芯片內(nèi)部MCU端只需要通過(guò)HAL庫(kù)配一路SPI和幾個(gè)GPIO協(xié)議棧的收發(fā)、重傳、分片全交給硬件去干。這篇筆記講的就是這條落地路線用HAL庫(kù)把W5500官方最新驅(qū)動(dòng)跑在F407VE上從接線、CubeMX配置到驅(qū)動(dòng)移植和TCP收發(fā)再到那些最容易翻車的現(xiàn)場(chǎng)一次講清楚。適合手里有F407VE開(kāi)發(fā)板或自繪板、想快速把以太網(wǎng)跑進(jìn)量產(chǎn)節(jié)奏的工程師。2. W5500和STM32F407VE的連接方式與CubeMX配置SPI、復(fù)位、中斷三個(gè)關(guān)鍵點(diǎn)W5500對(duì)外接口很干凈一路SPI從機(jī)、一個(gè)片選、一個(gè)復(fù)位、一個(gè)中斷輸出。真正決定通信能不能穩(wěn)定的就這三組信號(hào)。這一章先講物理接線再講CubeMX里SPI1、復(fù)位腳和外部中斷的配置每一步都會(huì)說(shuō)清楚為什么這么設(shè)。2.1 引腳連接表五個(gè)信號(hào)一個(gè)都不能錯(cuò)常見(jiàn)做法是讓W(xué)5500掛在SPI1上因?yàn)镾PI1掛在APB2上時(shí)鐘源最高能到84MHz配置后實(shí)際SPI時(shí)鐘還有上限后續(xù)如果想把SPI時(shí)鐘提上去SPI1的裕量比SPI2和SPI3都大。下面這張表是我常用的一組引腳分配W5500信號(hào)W5500引腳STM32F407VE引腳說(shuō)明SCLKSCLKPA5 (SPI1_SCK)SPI時(shí)鐘空閑低電平MOSIMOSIPA7 (SPI1_MOSI)主機(jī)輸出接W5500的MOSIMISOMISOPA6 (SPI1_MISO)主機(jī)輸入接W5500的MISOSCSSCSnPA4片選低有效軟件控制RSTNRSTnPB0復(fù)位低有效普通推挽輸出INTNINTnPA0中斷輸出低有效下降沿觸發(fā)接線時(shí)有個(gè)容易忽略的點(diǎn)W5500的供電和IO電平都是3.3VF407VE的IO雖然部分引腳兼容5V但既然都是3.3V系統(tǒng)不要偷懶把W5500模塊插到5V供電的轉(zhuǎn)換座上。模塊上如果自帶電平轉(zhuǎn)換另說(shuō)裸芯片或最小系統(tǒng)板必須統(tǒng)一3.3V。另外SCSn和INTn這兩個(gè)腳在W5500內(nèi)部都有上拉但這不意味著可以不接。SCSn必須由MCU明確控制INTn必須進(jìn)MCU的外部中斷否則驅(qū)動(dòng)狀態(tài)更新只能靠輪詢實(shí)時(shí)性和CPU占用都吃虧。2.2 CubeMX里SPI1配置模式0、10MHz以內(nèi)、軟件片選在CubeMX里選擇STM32F407VE后先把SPI1打開(kāi)Mode設(shè)為Full-Duplex MasterNSS信號(hào)設(shè)為Disable片選完全交給普通GPIO控制。W5500的SPI時(shí)序支持模式0和模式3工程里絕大多數(shù)例程用模式0也就是CPOL0、CPHA0。如果配置成模式1或模式2讀出來(lái)的寄存器大概率是0xFF后面排查時(shí)會(huì)非常迷惑。SPI時(shí)鐘先別急著拉滿。W5500手冊(cè)上給的SCLK上限在不同參考資料里說(shuō)法不一致保險(xiǎn)的做法是先設(shè)到10MHz以內(nèi)跑通功能再一點(diǎn)點(diǎn)往上探。CubeMX里SPI1的波特率預(yù)設(shè)為10.5MHz84MHz除以8分頻這個(gè)值作為起步很合適。參數(shù)表如下配置項(xiàng)取值說(shuō)明ModeFull-Duplex Master全雙工主機(jī)NSSDisable軟件控制片選Data Size8 BitsW5500寄存器都是8位寬First BitMSB First與W5500幀格式一致CPOLHighNo設(shè)Low空閑時(shí)鐘為低CPHA1 Edge模式0第一個(gè)邊沿采樣Baud Rate84MHz / 8 10.5MHz調(diào)通后再提速有個(gè)隱藏坑點(diǎn)CubeMX生成代碼后SPI的片選引腳雖然在圖形界面里掛到了PA4但如果你勾選了硬件NSSSPI外設(shè)會(huì)在傳輸過(guò)程中自動(dòng)拉低NSS造成和軟件片選打架。最好把PA4配成普通推挽輸出初始化代碼里默認(rèn)輸出高電平片選的拉低和抬升全部在驅(qū)動(dòng)移植層手動(dòng)完成。2.3 復(fù)位與中斷引腳的初始化上電時(shí)序和EXTI優(yōu)先級(jí)PB0作為復(fù)位輸出腳CubeMX里配成GPIO_Output默認(rèn)輸出高電平。W5500在上電后需要一次完整的復(fù)位脈沖拉低、保持、拉高、等待穩(wěn)定。很多人只拉低幾微秒就釋放結(jié)果芯片還沒(méi)完成內(nèi)部初始化后續(xù)讀寄存器全失敗。我一般拉低不少于1ms拉高后延時(shí)100ms再訪問(wèn)寄存器雖然手冊(cè)上給的復(fù)位時(shí)間沒(méi)有這么夸張但工程上多等一點(diǎn)換穩(wěn)定性是劃算的。PA0作為外部中斷輸入CubeMX里配成GPIO_EXTI0觸發(fā)方式選Falling Edge因?yàn)閃5500的INTn是低有效輸出有中斷事件時(shí)拉低。外部中斷的優(yōu)先級(jí)要根據(jù)你的系統(tǒng)來(lái)定常見(jiàn)做法是比SysTick低、比主循環(huán)邏輯高在NVIC里設(shè)搶占優(yōu)先級(jí)為5、子優(yōu)先級(jí)為0這類中段數(shù)值避免中斷里做的輕量處理影響關(guān)鍵實(shí)時(shí)任務(wù)。外部中斷回調(diào)里應(yīng)當(dāng)只置標(biāo)志位和處理狀態(tài)記錄不要直接在中斷里操作SPI。原因很直接HAL庫(kù)的SPI收發(fā)是阻塞式的如果主循環(huán)同時(shí)也在操作SPI總線中斷和主循環(huán)會(huì)穿插訪問(wèn)W5500輕則數(shù)據(jù)錯(cuò)位重則卡死。真要在中斷里清W5500事件標(biāo)志也必須通過(guò)下面要講的臨界區(qū)保護(hù)來(lái)做。3. 移植W5500官方最新驅(qū)動(dòng)把VFS層接到HAL庫(kù)SPI驅(qū)動(dòng)移植是這條路線里最核心的一步。W5500官方驅(qū)動(dòng)歷史上幾經(jīng)改版但移植思路一直沒(méi)變芯片驅(qū)動(dòng)層負(fù)責(zé)拼地址幀和控制字節(jié)底層IO函數(shù)需要你按實(shí)際MCU去實(shí)現(xiàn)。HAL庫(kù)環(huán)境下這些IO函數(shù)就是包一層HAL_SPI_TransmitReceive再配合片選和臨界區(qū)回調(diào)。3.1 驅(qū)動(dòng)文件怎么拷、接口層要補(bǔ)什么從官方驅(qū)動(dòng)庫(kù)里拷貝文件時(shí)核心是Ethernet目錄下的那組源文件wizchip適配層、W5500芯片驅(qū)動(dòng)層和socket接口層。這三個(gè)部分是一套完整的依賴關(guān)系少一個(gè)都編譯不過(guò)所以直接整個(gè)目錄拷進(jìn)工程再按需保留其他內(nèi)容。socket接口層之上還有Internet目錄里面是DNS、DHCP這些上層應(yīng)用協(xié)議。手頭工程如果只需要靜態(tài)IP直連先不拷這些減少干擾項(xiàng)。等以太網(wǎng)通信跑通后再按需加入DHCP??酵旰笠a(bǔ)的就是底層移植文件通常命名為w5500_port.c之類。它需要向驅(qū)動(dòng)注冊(cè)三樣?xùn)|西片選控制函數(shù)、SPI單字節(jié)讀寫(xiě)函數(shù)、臨界區(qū)進(jìn)入退出函數(shù)。新版驅(qū)動(dòng)通過(guò)注冊(cè)回調(diào)的方式掛接老版本則直接改寫(xiě)底層宏定義具體接口名因版本而異但要做的事情就這三件。3.2 移植層代碼片選、SPI收發(fā)、臨界區(qū)三個(gè)回調(diào)下面這段是移植層的核心邏輯對(duì)應(yīng)W5500芯片對(duì)SPI時(shí)序的全部要求/* 片選低有效 */ static void w5500_cs_select(void) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); } static void w5500_cs_deselect(void) { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); } /* 單字節(jié)讀寫(xiě)SPI是全雙工收和發(fā)要同時(shí)進(jìn)行 */ static uint8_t w5500_spi_readbyte(void) { uint8_t tx 0xFF; uint8_t rx 0; HAL_SPI_TransmitReceive(hspi1, tx, rx, 1, HAL_MAX_DELAY); return rx; } static void w5500_spi_writebyte(uint8_t data) { uint8_t rx 0; HAL_SPI_TransmitReceive(hspi1, data, rx, 1, HAL_MAX_DELAY); } /* 臨界區(qū)保護(hù)裸機(jī)環(huán)境可以簡(jiǎn)單關(guān)中斷 */ static void w5500_cris_enter(void) { __disable_irq(); } static void w5500_cris_exit(void) { __enable_irq(); } /* 注冊(cè)到驅(qū)動(dòng) */ void w5500_port_init(void) { reg_wizchip_cs_cbfunc(w5500_cs_select, w5500_cs_deselect); reg_wizchip_cris_cbfunc(w5500_cris_enter, w5500_cris_exit); reg_wizchip_spi_cbfunc(w5500_spi_readbyte, w5500_spi_writebyte); }注意這里用的是HAL_SPI_TransmitReceive而不是分開(kāi)的Transmit或Receive原因是SPI本質(zhì)上是移位交換主機(jī)發(fā)送一個(gè)字節(jié)的同時(shí)必然收到一個(gè)字節(jié)。如果用HAL_SPI_Receive去收數(shù)據(jù)主機(jī)端依然要產(chǎn)生時(shí)鐘雖然HAL庫(kù)能生成時(shí)鐘但多一層封裝不如直接用收發(fā)一體清晰。片選函數(shù)里沒(méi)有做延時(shí)。有些工程師擔(dān)心CS拉低后W5500還沒(méi)準(zhǔn)備好于是在select和第一個(gè)字節(jié)之間插入微秒級(jí)延時(shí)這是多余的。只要SPI時(shí)鐘頻率不過(guò)高CS拉低到采樣之間由SPI硬件時(shí)序保證足夠。真遇到數(shù)據(jù)錯(cuò)位先去查SPI模式而不是亂加延時(shí)。臨界區(qū)在裸機(jī)環(huán)境直接關(guān)中斷即可。如果跑RTOS這里應(yīng)該換成互斥量或調(diào)度鎖否則中斷回調(diào)里操作wizchip相關(guān)寄存器時(shí)會(huì)發(fā)生上下文競(jìng)爭(zhēng)。3.3 驅(qū)動(dòng)初始化順序先復(fù)位芯片再校驗(yàn)版本和網(wǎng)關(guān)拷完驅(qū)動(dòng)、寫(xiě)完移植層接下來(lái)是初始化順序。這個(gè)順序錯(cuò)一步都會(huì)讓后續(xù)操作表現(xiàn)得很詭異尤其是先初始化網(wǎng)絡(luò)參數(shù)再?gòu)?fù)位芯片的做法會(huì)把W5500內(nèi)部寄存器清掉調(diào)試時(shí)以為是代碼問(wèn)題其實(shí)是順序問(wèn)題。uint8_t ver 0; /* 1. 硬件復(fù)位W5500 */ HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, GPIO_PIN_RESET); HAL_Delay(10); HAL_GPIO_WritePin(RST_GPIO_Port, RST_Pin, GPIO_PIN_SET); HAL_Delay(100); /* 2. 注冊(cè)IO回調(diào) */ w5500_port_init(); /* 3. 讀取版本寄存器確認(rèn)SPI鏈路已通 */ if (ctlwizchip(CW_GET_VERSION, ver) ! 0) { /* 讀版本失敗多半是SPI或復(fù)位問(wèn)題 */ } if (ver ! 0x04) { /* W5500版本號(hào)固定為0x04讀到別的值說(shuō)明鏈路不對(duì) */ } /* 4. 初始化wizchip內(nèi)部狀態(tài) */ ctlwizchip(CW_INIT_WIZCHIP); /* 5. 配置網(wǎng)絡(luò)參數(shù)靜態(tài)IP起步 */ wiz_NetInfo netinfo { .mac {0x02, 0x00, 0x00, 0x00, 0x00, 0x01}, .ip {192, 168, 1, 20}, .sn {255, 255, 255, 0}, .gw {192, 168, 1, 1}, .dns {192, 168, 1, 1}, .dhcp 0, }; ctlwizchip(CW_SET_NETINFO, netinfo);版本寄存器讀到0x04這一步是整個(gè)移植的分水嶺。如果這一步能通過(guò)說(shuō)明SPI時(shí)序、片選、復(fù)位都正常后面的socket操作都是在調(diào)參數(shù)如果這一步讀不出來(lái)先別急著查業(yè)務(wù)代碼回去對(duì)著第5章的避坑清單查物理層和配置層。DHCP在第一步調(diào)通時(shí)不建議開(kāi)因?yàn)檗k公室里不一定每臺(tái)路由器都有DHCP服務(wù)或者DHCP響應(yīng)慢會(huì)干擾你判斷到底是驅(qū)動(dòng)問(wèn)題還是網(wǎng)絡(luò)環(huán)境問(wèn)題。靜態(tài)IP直連同網(wǎng)段PC是最干凈的最小驗(yàn)證環(huán)境。4. 跑通第一路TCP客戶端、服務(wù)器最小代碼與狀態(tài)機(jī)驅(qū)動(dòng)初始化完成后W5500相當(dāng)于一個(gè)具備完整TCP/IP能力的網(wǎng)絡(luò)設(shè)備剩下的工作就是在8個(gè)socket上執(zhí)行open、connect、listen、send、recv。這一章分別寫(xiě)出TCP客戶端和TCP服務(wù)器的標(biāo)準(zhǔn)流程再講清中斷狀態(tài)機(jī)怎么處理和主循環(huán)配合。4.1 TCP客戶端連接服務(wù)器并收發(fā)數(shù)據(jù)先看看客戶端側(cè)的最小代碼。假設(shè)目標(biāo)服務(wù)器是192.168.1.100:8080本地端口用6000uint8_t sn 0; int8_t ret; /* 分配socket0的收發(fā)緩沖區(qū)每個(gè)2KB */ setSn_RXBUF(sn, 0x02); setSn_TXBUF(sn, 0x02); /* 打開(kāi)socket指定TCP協(xié)議和本地端口 */ ret socket(sn, Sn_MR_TCP, 6000, 0); if (ret ! sn) { /* socket分配失敗可能是sn已被占用 */ } /* 連接服務(wù)器 */ uint8_t server_ip[4] {192, 168, 1, 100}; uint16_t server_port 8080; ret connect(sn, server_ip, server_port); if (ret ! SOCK_OK) { /* 連接失敗檢查服務(wù)器是否可達(dá)、端口是否開(kāi)放 */ } /* 連接成功后進(jìn)入主循環(huán) */ while (1) { uint16_t len getSn_RX_RSR(sn); if (len 0) { uint8_t buf[1460]; int16_t rlen recv(sn, buf, len sizeof(buf) ? sizeof(buf) : len); if (rlen 0) { /* 收多少回多少做回顯測(cè)試 */ send(sn, buf, rlen); } } /* 主循環(huán)里還要定期檢查連接狀態(tài)見(jiàn)4.3節(jié) */ }setSn_RXBUF(sn, 0x02)里的0x02表示2KBW5500每個(gè)socket的發(fā)送和接收緩沖可獨(dú)立設(shè)置為1KB到16KB總共加起來(lái)32KB。調(diào)大收發(fā)緩沖能提升吞吐但會(huì)擠占其他socket的空間。8個(gè)socket全部使用默認(rèn)2KB配置在簡(jiǎn)單場(chǎng)景下完全夠用。recv函數(shù)返回的是實(shí)際接收字節(jié)數(shù)如果返回值是個(gè)負(fù)數(shù)去查錯(cuò)誤碼定義常見(jiàn)的是SOCK_BUSY意思是socket正在忙需要稍后重試。newdriver里有拿返回值當(dāng)長(zhǎng)度用的坑記得先判斷正負(fù)再處理緩沖區(qū)。4.2 TCP服務(wù)器監(jiān)聽(tīng)端口并處理新連接服務(wù)器側(cè)流程差異不大只是把connect換成listen然后查狀態(tài)寄存器確認(rèn)是否進(jìn)入監(jiān)聽(tīng)uint8_t sn 0; setSn_RXBUF(sn, 0x04); setSn_TXBUF(sn, 0x04); if (socket(sn, Sn_MR_TCP, 6000, 0) ! sn) { /* socket打開(kāi)失敗 */ } listen(sn); if (getSn_SR(sn) ! SOCK_LISTEN) { /* 沒(méi)有進(jìn)入監(jiān)聽(tīng)狀態(tài)檢查波特率和socket狀態(tài) */ } while (1) { /* 有客戶端連上來(lái)時(shí)Sn_SR變成SOCK_ESTABLISHED */ if (getSn_SR(sn) SOCK_ESTABLISHED) { uint16_t len getSn_RX_RSR(sn); if (len 0) { /* 與客戶端收發(fā)數(shù)據(jù) */ } } }W5500在服務(wù)器模式下沒(méi)有顯式的accept函數(shù)??蛻舳诉B接建立后Sn_SR自動(dòng)從SOCK_LISTEN變成SOCK_ESTABLISHED同時(shí)產(chǎn)生Sn_IR_CON中斷。應(yīng)用層只需要持續(xù)查詢狀態(tài)或者依賴中斷事件來(lái)感知新連接。有個(gè)邊界要提前知道W5500每個(gè)socket在監(jiān)聽(tīng)模式下同一時(shí)刻只能接受一個(gè)TCP連接。第二個(gè)客戶端連上來(lái)時(shí)W5500不會(huì)主動(dòng)處理表現(xiàn)為連接不上或超時(shí)。需要多連接時(shí)要么一個(gè)socket只服務(wù)一路客戶端要么改用多socket監(jiān)聽(tīng)不同端口這個(gè)坑在第5章還會(huì)細(xì)說(shuō)。4.3 中斷處理SIR/Sn_IR按位解析輪到誰(shuí)就服務(wù)誰(shuí)W5500的中斷源分兩級(jí)全局中斷寄存器SIR指示哪個(gè)socket有事件Sn_IR指示具體是什么事件。外部中斷回調(diào)里做的事件解析如下void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin INT_Pin) { uint8_t sir getSIR(); for (uint8_t i 0; i 8; i) { if (sir (1 i)) { uint8_t ir getSn_IR(i); if (ir Sn_IR_CON) { g_event[i] | EVT_CONNECT; } if (ir Sn_IR_RECV) { g_event[i] | EVT_RECV; } if (ir Sn_IR_DISCON) { g_event[i] | EVT_DISCON; } setSn_IR(i, ir); /* 寫(xiě)1清除讀到的ir原樣寫(xiě)回 */ } } setSIR(sir); } }中斷回調(diào)里只置應(yīng)用層的g_event標(biāo)志不直接調(diào)用recv或send。主循環(huán)檢測(cè)到標(biāo)志后再去操作socket這樣避免SPI總線并發(fā)訪問(wèn)。同時(shí)也能保證中斷處理的時(shí)間非常短不會(huì)拖累系統(tǒng)實(shí)時(shí)性。很多新手的疑問(wèn)是初始化時(shí)INT引腳已經(jīng)觸發(fā)了大量的中斷事件主循環(huán)還沒(méi)來(lái)得及處理flags會(huì)不會(huì)重復(fù)累積W5500的中斷標(biāo)志寄存器是邊沿型事件讀后如果不清除就會(huì)一直保持如果清了就不會(huì)重觸發(fā)。這里的關(guān)鍵是主循環(huán)處理完數(shù)據(jù)后再去清一次Sn_IR。如果主循環(huán)還沒(méi)處理完就把中斷清了事件就丟了如果處理完不清下次中斷不會(huì)再觸發(fā)。常見(jiàn)做法是中斷回調(diào)里清一次主循環(huán)里處理完業(yè)務(wù)后再主動(dòng)讀一次狀態(tài)寄存器確認(rèn)沒(méi)有新的pending。5. W5500移植最容易翻車的5個(gè)現(xiàn)場(chǎng)現(xiàn)象、原因、解決這一章是拿真金白銀換來(lái)的經(jīng)驗(yàn)。以下5個(gè)問(wèn)題在論壇和實(shí)際項(xiàng)目里反復(fù)出現(xiàn)每條都按現(xiàn)象、原因、解決的順序拆開(kāi)講排查時(shí)可以直接對(duì)號(hào)入座。5.1 讀版本寄存器返回0xFFSPI模式或時(shí)鐘先查讀VERSIONR寄存器一直得到0xFF是最常見(jiàn)的首發(fā)癥狀。出現(xiàn)這個(gè)現(xiàn)象時(shí)程序往往已經(jīng)正確配置了SPI、也注冊(cè)了回調(diào)但就是讀不到芯片的應(yīng)答。原因分三類一是SPI模式配錯(cuò)W5500要求模式0或模式3不少人用了模式2CPOL1、CPHA0數(shù)據(jù)在錯(cuò)誤的邊沿被采樣二是片選控制失效CS引腳沒(méi)用軟件GPIO控制、而是被SPI硬件NSS接管導(dǎo)致CS時(shí)序錯(cuò)亂三是SPI時(shí)鐘過(guò)快超出W5500的實(shí)際承受范圍尤其在長(zhǎng)走線或模塊供電不穩(wěn)時(shí)更容易出錯(cuò)。解決方法是先降速排查。把SPI波特率降到2MHz左右確認(rèn)模式為CPOL0、CPHA0片選用普通GPIO拉低不放再讀版本寄存器。如果還是0xFF用示波器抓SCLK和CS的時(shí)序看CS是否在傳輸期間穩(wěn)定拉低、SCLk是否有毛刺。這一步能排除九成的問(wèn)題。5.2 復(fù)位后立刻訪問(wèn)寄存器不成功復(fù)位時(shí)序不夠現(xiàn)象是代碼流程看著完全正常但第一次讀版本寄存器失敗復(fù)位一次后又能成功或者上電后必須手動(dòng)按一下復(fù)位鍵才能通信。原因是W5500內(nèi)部在復(fù)位釋放后需要時(shí)間完成自檢和PHY初始化。復(fù)位拉低時(shí)間太短、釋放后等待時(shí)間不足芯片還沒(méi)準(zhǔn)備好就訪問(wèn)寄存器自然會(huì)失敗。解決方法是拉低RSTn至少10ms釋放后延時(shí)100ms再執(zhí)行任何SPI訪問(wèn)。如果板子上W5500的電源濾波電容偏大上電時(shí)間會(huì)更長(zhǎng)延時(shí)要相應(yīng)加大。這個(gè)延時(shí)只出現(xiàn)在初始化階段不影響運(yùn)行時(shí)的響應(yīng)速度。5.3 中斷標(biāo)志清不掉讀到的ir原樣寫(xiě)回不是寫(xiě)0現(xiàn)象是調(diào)試時(shí)發(fā)現(xiàn)中斷回調(diào)反復(fù)被觸發(fā)即使在回調(diào)里執(zhí)行了setSn_IR(sn, 0)中斷依然不斷進(jìn)來(lái)仿佛永遠(yuǎn)清不掉。原因W5500的中斷標(biāo)志寄存器是寫(xiě)1清除W1C機(jī)制。寫(xiě)0沒(méi)有任何效果只有把對(duì)應(yīng)位寫(xiě)1才會(huì)清除該中斷標(biāo)志。很多從其他芯片轉(zhuǎn)過(guò)來(lái)的工程師習(xí)慣寫(xiě)0清標(biāo)志在W5500這里就失效了。解決方法是把讀出來(lái)的ir值原樣寫(xiě)回setSn_IR(i, ir);比如讀到的ir是0x04Sn_IR_RECV就寫(xiě)回0x04。如果想清除多個(gè)事件把要清除的位按位或起來(lái)再寫(xiě)。全局中斷SIR寄存器同樣是寫(xiě)1清除不要寫(xiě)0。5.4 TCP服務(wù)器只能接一個(gè)連接W5500硬件就這么設(shè)計(jì)現(xiàn)象開(kāi)發(fā)板上跑著TCP server第一個(gè)手機(jī)或PC連上來(lái)收發(fā)正常第二個(gè)設(shè)備再連接時(shí)一直連不上抓包發(fā)現(xiàn)服務(wù)端完全沒(méi)有響應(yīng)。原因W5500的每個(gè)socket在同一時(shí)刻只維護(hù)一個(gè)TCP連接。監(jiān)聽(tīng)狀態(tài)下一旦有客戶端連入Sn_SR就從SOCK_LISTEN變成SOCK_ESTABLISHED芯片不會(huì)再處理新的SYN包。這是硬件TCP/IP協(xié)議棧的邊界不是代碼bug。解決辦法分兩種一是產(chǎn)品本身只需要單連接比如一臺(tái)設(shè)備對(duì)一臺(tái)配置終端那就接受這個(gè)限制在文檔里寫(xiě)清楚二是需要多連接就在同一端口開(kāi)放多個(gè)socket輪詢監(jiān)聽(tīng)或者改走UDP。注意同一個(gè)端口號(hào)和不同socket綁定是可行的多個(gè)socket都執(zhí)行l(wèi)isten同一端口由硬件負(fù)載均衡但實(shí)現(xiàn)復(fù)雜度會(huì)上升一般場(chǎng)景不如直接換UDP或加一層應(yīng)用層轉(zhuǎn)發(fā)。5.5 收發(fā)一段時(shí)間后丟包接收緩沖被讀慢或緩沖區(qū)分配不合理現(xiàn)象TCP通信剛連上時(shí)收發(fā)都正常持續(xù)跑幾分鐘后開(kāi)始丟包或者吞吐量驟降重啟后又恢復(fù)過(guò)陣子再犯。原因W5500的接收緩沖區(qū)是共享的如果某個(gè)socket的接收緩沖太小應(yīng)用層讀取不及時(shí)硬件FIFO滿了之后新的數(shù)據(jù)包會(huì)被直接丟棄。TCP協(xié)議棧不會(huì)無(wú)限制地重傳超過(guò)重傳次數(shù)后連接就會(huì)被斷開(kāi)。解決方法是先從軟件上保證recv的調(diào)度頻率。中斷置位后主循環(huán)要盡快讀取數(shù)據(jù)不要在recv之前插入大延時(shí)。再把接收緩沖區(qū)調(diào)大比如setSn_RXBUF(sn, 0x08)設(shè)為8KB同時(shí)觀察getSn_RX_RSR的返回值是否持續(xù)增長(zhǎng)。如果增長(zhǎng)到接近緩沖區(qū)上限后掉回0說(shuō)明發(fā)生了溢出丟包。還有一種隱蔽情況recv的len參數(shù)直接用了getSn_RX_RSR的返回值而這個(gè)返回值可能大于你定義的buf長(zhǎng)度導(dǎo)致緩沖區(qū)溢出。標(biāo)準(zhǔn)寫(xiě)法是取RSR和buf容量的較小值防止棧被寫(xiě)穿。6. 收尾技巧用UDP回顯拉通全鏈路再談SPI提速TCP鏈路調(diào)通后我習(xí)慣再用一個(gè)UDP回顯做交叉驗(yàn)證因?yàn)閁DP無(wú)連接、無(wú)重傳能直接暴露最底層的收發(fā)路徑是否完整。如果UDP回顯都穩(wěn)定那么TCP出現(xiàn)問(wèn)題時(shí)就可以把嫌疑范圍縮小到驅(qū)動(dòng)參數(shù)或網(wǎng)絡(luò)環(huán)境而不是SPI物理鏈路。6.1 一個(gè)UDP socket測(cè)通整條收發(fā)路徑UDP不需要connect和listen打開(kāi)socket后直接收發(fā)uint8_t sn 1; socket(sn, Sn_MR_UDP, 9000, 0); uint8_t buf[1460]; while (1) { uint16_t len getSn_RX_RSR(sn); if (len 0) { uint8_t peer_ip[4]; uint16_t peer_port; int16_t rlen recvfrom(sn, buf, len, peer_ip, peer_port); if (rlen 0) { sendto(sn, buf, rlen, peer_ip, peer_port); } } }用PC上的網(wǎng)絡(luò)調(diào)試助手向板子的IP:9000發(fā)一串?dāng)?shù)據(jù)板子原樣回傳能連續(xù)跑幾百個(gè)包不丟說(shuō)明SPI鏈路、W5500緩沖管理、中斷機(jī)制都正常。UDP測(cè)試的另一個(gè)價(jià)值是排除TCP狀態(tài)機(jī)干擾——如果UDP正常而TCP異常問(wèn)題多半出在socket狀態(tài)轉(zhuǎn)換或網(wǎng)絡(luò)參數(shù)上而不是底層硬件。6.2 提高SPI時(shí)鐘的驗(yàn)證方法和判斷標(biāo)準(zhǔn)鏈路穩(wěn)定后可以試著提速SPI。每次在CubeMX里把波特率上調(diào)一檔比如從10.5MHz提到16MHz、21MHz重新編譯燒錄然后跑一遍UDP回顯壓力測(cè)試。判斷標(biāo)準(zhǔn)是連續(xù)跑10分鐘不出現(xiàn)0xFF、不丟包、中斷響應(yīng)正常。提速度有個(gè)前置條件檢查PCB走線。如果W5500模塊是通過(guò)杜邦線連到開(kāi)發(fā)板的線長(zhǎng)超過(guò)10cm高速SPI會(huì)因反射和串?dāng)_變得非常不可靠此時(shí)強(qiáng)行提速屬于玄學(xué)調(diào)參。自繪板上W5500的SCLK走線要做等長(zhǎng)處理盡量短遠(yuǎn)離晶振和電感。提速后如果出現(xiàn)偶發(fā)丟包先別急著降速試著在片選函數(shù)里加一個(gè)SPI速率相關(guān)的空操作延時(shí)。雖然我之前說(shuō)不要亂加延時(shí)但在高SPI頻率下CS拉低后立即產(chǎn)生第一個(gè)時(shí)鐘邊沿如果W5500從機(jī)還需要一點(diǎn)建立時(shí)間加一個(gè)極短的__NOP()可能有幫助。這個(gè)要根據(jù)示波器實(shí)測(cè)決定不要盲調(diào)。以我自己的經(jīng)驗(yàn)SPI時(shí)鐘穩(wěn)定運(yùn)行在16MHz左右是一個(gè)甜點(diǎn)區(qū)功耗不高且CPU占用率遠(yuǎn)低于純軟件協(xié)議棧。超過(guò)21MHz后收益衰減明顯排查成本和硬件要求反而直線上升。所以日常做的產(chǎn)品我一般鎖死在16MHz省下的時(shí)間拿去完善應(yīng)用協(xié)議更值得。最后說(shuō)一個(gè)習(xí)慣W5500工程做完后我會(huì)把讀版本寄存器、連接服務(wù)器、UDP回顯這三個(gè)節(jié)點(diǎn)用串口打日志輸出每個(gè)節(jié)點(diǎn)一個(gè)固定前綴比如[W5500][VER]、[W5500][TCP]。每次燒錄新板卡不用接調(diào)試器光看串口就知道問(wèn)題出在物理層、驅(qū)動(dòng)層還是網(wǎng)絡(luò)層。這個(gè)習(xí)慣幫我省掉了大量反復(fù)插拔調(diào)試器的時(shí)間希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取