方案:從硬件連接到TCP/UDP實(shí)戰(zhàn))
簡介這是一份面向嵌入式開發(fā)者的STM32H7與W5500以太網(wǎng)驅(qū)動移植學(xué)習(xí)資源適用于工業(yè)控制、物聯(lián)網(wǎng)節(jié)點(diǎn)及需要穩(wěn)定網(wǎng)絡(luò)連接的實(shí)時通信場景。W5500集成以太網(wǎng)MAC/PHY與TCP/IP協(xié)議棧通過SPI總線與STM32H7對接資源圍繞驅(qū)動移植展開涵蓋SPI底層驅(qū)動、W5500初始化與網(wǎng)絡(luò)參數(shù)配置、Socket創(chuàng)建、數(shù)據(jù)收發(fā)函數(shù)、客戶端與服務(wù)器模式示例以及相關(guān)頭文件和工程配置??蓭椭斫饧拇嫫髯x寫、TCP三次握手與四次揮手、中斷響應(yīng)和DMA傳輸優(yōu)化等關(guān)鍵知識點(diǎn)。壓縮包為rar格式大小約21.4MB上游未提供文件清單具體文件數(shù)及類型分布暫不明確實(shí)際內(nèi)容以包內(nèi)代碼工程為準(zhǔn)。目前已有1406人學(xué)習(xí)適合承擔(dān)STM32H7網(wǎng)絡(luò)功能開發(fā)任務(wù)、希望快速上手W5500的中級嵌入式開發(fā)者。1. 為什么是 STM32H7 W5500從一塊網(wǎng)卡芯片聊起做嵌入式聯(lián)網(wǎng)的人遲早會遇到一個尷尬場景STM32H7 這顆芯片本身自帶 MAC但想要讓它穩(wěn)定上網(wǎng)還得外掛一顆 PHY、移植 TCP/IP 協(xié)議棧、調(diào)整內(nèi)存池稍微復(fù)雜一點(diǎn)的 UDP/TCP 收發(fā)就能把人折騰到半夜。前兩年做一個數(shù)據(jù)采集設(shè)備甲方要求把傳感器數(shù)據(jù)通過以太網(wǎng)上報我在 STM32H7 上跑了 LwIP第一版光內(nèi)存崩潰就調(diào)了三天。后來換成外掛 W5500把協(xié)議棧從 CPU 上整個剝掉三天內(nèi)從畫板到 ping 通這個組合就成了我后續(xù)做聯(lián)網(wǎng)設(shè)備的默認(rèn)起手式。STM32H7 以太網(wǎng) W5500 程序.rar 這類工程之所以被反復(fù)下載是因?yàn)樗@開了「MCU 上跑協(xié)議?!惯@條最陡的學(xué)習(xí)曲線。W5500 是一顆帶硬件 TCP/IP 協(xié)議棧的以太網(wǎng)芯片MCU 只通過 SPI 讀寫寄存器ARP、IP、ICMP、TCP、UDP 都在芯片內(nèi)部完成。適合誰適合把大量精力放在業(yè)務(wù)邏輯和穩(wěn)定性的開發(fā)者——工業(yè)數(shù)據(jù)采集、設(shè)備遠(yuǎn)程監(jiān)控、串口轉(zhuǎn)以太網(wǎng)網(wǎng)關(guān)這類場景要的不是炫技而是能長時間跑在產(chǎn)線上的可靠性。讀完這篇你會清楚整套方案的原理、接線、移植步驟和常見坑點(diǎn)。2. W5500 與 STM32H7 的硬件鏈路SPI 接口與電平匹配2.1 硬件協(xié)議棧的工作原理誰在替你處理 TCP/IP 報文W5500 內(nèi)部集成了 MAC 和 PHY并在此基礎(chǔ)上用邏輯電路實(shí)現(xiàn)了完整的 TCP/IP 協(xié)議棧。MCU 需要做的不是解析報文而是通過 SPI 接口讀寫 W5500 內(nèi)部的寄存器與數(shù)據(jù)緩沖區(qū)。發(fā)數(shù)據(jù)時把應(yīng)用數(shù)據(jù)寫進(jìn) W5500 的 TX Buffer然后發(fā)一條 SEND 命令收數(shù)據(jù)時檢測到接收事件后從 RX Buffer 把數(shù)據(jù)讀出來。TCP 的三次握手、重傳計時、ACK 應(yīng)答、分片重組這些邏輯在芯片內(nèi)部完成不消耗 MCU 的 Flash 和 RAM。STM32H7 在這個方案里的角色被簡化成了「搬數(shù)據(jù)的人」。SPI 的時鐘頻率、FIFO 深度、DMA 能力都會影響吞吐量但即便不用 DMA、純輪詢寫 SPI在 20MHz 的 SPI 時鐘下也能輕松跑出幾 Mbps 的 TCP 吞吐量足夠覆蓋絕大多數(shù)工業(yè)數(shù)據(jù)采集場景。和 STM32H7 自帶 MAC 外部 PHY 的方案相比W5500 的優(yōu)勢不是性能而是確定性和開發(fā)效率——省去了 LwIP 的內(nèi)存池調(diào)優(yōu)也省去了協(xié)議棧對 Cortex-M7 緩存一致性的特殊處理。2.2 最小硬件連接七根線加一個復(fù)位別小看中斷線W5500 與 MCU 的通信最少只需要七根線四根 SPI、一根片選、一根復(fù)位、一根中斷。注意這里的 SPI 只能做主模式且片選必須用 GPIO 軟件控制不能用 STM32H7 的硬件 NSS。W5500 引腳功能STM32H7 示例引腳說明SCSSPI 片選PB7GPIO 輸出低有效整個幀傳輸期間保持拉低SCLKSPI 時鐘PF7SPI5_SCK速率建議 20MHz 以下MOSI主出從入PF9SPI5_MOSIMISO主入從出PF8SPI5_MISO注意上拉RST復(fù)位PC6低有效需保證上電復(fù)位時序INT中斷輸出PC7下降沿有效建議接外部中斷輸入3.3V / GND電源3.3V / GND就近放 10uF 0.1uF 去耦電容引腳號以你手頭開發(fā)板的原理圖為準(zhǔn)這里給的是 STM32H743 的 SPI5 默認(rèn)復(fù)用關(guān)系。有一個細(xì)節(jié)容易被忽略W5500 的 MISO 引腳在 SPI 通信空閑時是高阻態(tài)如果 STM32H7 的 SPI 配置了硬件去拉低 MISO會導(dǎo)致第一個字節(jié)讀到錯誤數(shù)據(jù)。最省心的做法是在 MISO 上加一個 4.7kΩ 上拉電阻或者在 STM32CubeMX 里把 MISO 配置為 GPIO 輸入上拉模式。2.3 上電時序與復(fù)位引腳W5500 不認(rèn) SPI 的常見第一個原因很多人拿到工程直接下載發(fā)現(xiàn) SPI 讀寫寄存器全是 0xFF第一反應(yīng)是 SPI 配置錯了實(shí)際上大概率是復(fù)位時序沒滿足。W5500 要求 VCC 穩(wěn)定之后RST 引腳保持低電平至少 500us才能完成內(nèi)部 PHY 校準(zhǔn)和寄存器初始化。如果 MCU 的復(fù)位引腳和 W5500 的 RST 直接連在一起或者 RST 懸空沒有上拉上電瞬間可能出現(xiàn)不可預(yù)測的初始化狀態(tài)。常見做法是給 RST 引腳加一個 10kΩ 上拉電阻和 0.1uF 對地電容形成硬件 RC 上電復(fù)位。同時軟件上在初始化 SPI 之前先把 RST 拉低、延時 10ms、再拉高、再延時 100ms等 W5500 內(nèi)部邏輯完全穩(wěn)定后再訪問寄存器。判斷 W5500 是否就緒有一個標(biāo)準(zhǔn)動作讀版本寄存器正確值應(yīng)當(dāng)是 0x04。如果讀回來是 0xFF 或 0x00先查復(fù)位時序再查 SPI 的極性和相位這比盲目調(diào)代碼高效得多。3. 把 .rar 里的工程跑起來從解壓到 ping 通3.1 工程里真正要關(guān)心的文件驅(qū)動庫、平臺適配與業(yè)務(wù)代碼解壓一份 STM32H7 W5500 的 .rar 工程目錄往往混合著 HAL 庫、驅(qū)動中間層和用戶代碼。不要去翻每一行先找三類文件W5500 協(xié)議棧驅(qū)動、平臺適配層、網(wǎng)絡(luò)配置頭文件。最常見的是這套結(jié)構(gòu)w5500.c / w5500.h 負(fù)責(zé)寄存器讀寫socket.c / socket.h 負(fù)責(zé)封裝 Socket APIwizchip_conf.c 里是協(xié)議棧入口還有你自己寫的 spi_stm32h7.c 用來對接 STM32H7 的 SPI 外設(shè)。需要改的核心只有平臺適配層和網(wǎng)絡(luò)參數(shù)配置。協(xié)議棧驅(qū)動是標(biāo)準(zhǔn)的不要動。你自己寫的 SPI 讀寫函數(shù)需要滿足驅(qū)動庫要求的三個原語CS 控制、SPI 單字節(jié)收發(fā)、可能還有 SPI 高速連續(xù)讀寫。驅(qū)動庫在讀寫 RX/TX Buffer 時會用你提供的原語一次搬幾十到幾百字節(jié)如果你的 SPI 讀寫函數(shù)里面每個字節(jié)都重新配置一次時鐘速度會慢得離譜。我一般會把 SPI 初始化放在一個單獨(dú)的函數(shù)里只做一次然后通過一個純裸的收發(fā)函數(shù)供驅(qū)動調(diào)用。3.2 修改平臺適配層SPI 時鐘極性與 W5500 幀格式W5500 支持 SPI Mode 0CPOL0, CPHA0和 Mode 3CPOL1, CPHA1絕大多數(shù)官方例程默認(rèn) Mode 0。你用 STM32CubeMX 配置 SPI 時選擇低速主模式、8 位數(shù)據(jù)長度、MSB First極性選 Low相位選 1Edge片選選 Software就能和 W5500 對上。/* stm32h7_w5500_spi.c */ #include main.h static SPI_HandleTypeDef hspi_w5500; void w5500_spi_init(void) { hspi_w5500.Instance SPI5; hspi_w5500.Init.Mode SPI_MODE_MASTER; hspi_w5500.Init.Direction SPI_DIRECTION_2LINES; hspi_w5500.Init.DataSize SPI_DATA_SIZE_8BIT; hspi_w5500.Init.CLKPolarity SPI_POLARITY_LOW; /* CPOL 0 */ hspi_w5500.Init.CLKPhase SPI_PHASE_1EDGE; /* CPHA 0 */ hspi_w5500.Init.NSS SPI_NSS_SOFT; /* CS 用 GPIO 控制 */ hspi_w5500.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_16; /* 約 13.5MHz 216MHz */ hspi_w5500.Init.FirstBit SPI_FIRSTBIT_MSB; if (HAL_SPI_Init(hspi_w5500) ! HAL_OK) { while (1); /* 初始化失敗直接卡住便于定位 */ } } static void spi_tx_byte(uint8_t data) { HAL_SPI_Transmit(hspi_w5500, data, 1, HAL_MAX_DELAY); } static uint8_t spi_rx_byte(void) { uint8_t rx 0xFF; HAL_SPI_Receive(hspi_w5500, rx, 1, HAL_MAX_DELAY); return rx; }這里每個字節(jié)都調(diào)用一次 HAL_SPI_Transmit 其實(shí)不是最優(yōu)的但勝在直觀可靠。HAL_SPI_Transmit 在 H7 上每次調(diào)用有固定的狀態(tài)機(jī)開銷實(shí)測在 13.5MHz 下達(dá)到 3Mbps 左右的吞吐量沒有問題。W5500 的 SPI 幀由三部分組成2 字節(jié)地址段、1 字節(jié)控制段、N 字節(jié)數(shù)據(jù)段。地址段指定寄存器偏移控制段里的 bit5 表示讀還是寫bit4-3 選擇訪問的塊——通用寄存器、Socket 寄存器、TX Buffer 或 RX Bufferbit2-0 設(shè)為 000 表示可變數(shù)據(jù)長度模式。以下是我在工程里實(shí)際使用的幀讀寫函數(shù)可以直接替換驅(qū)動庫底層的 SPI 原語/* W5500 片選控制換到你的工程時把引腳宏替換成實(shí)際定義的 GPIO */ #define W5500_CS_LOW() HAL_GPIO_WritePin(GPIOC, GPIO_PIN_6, GPIO_PIN_RESET) #define W5500_CS_HIGH() HAL_GPIO_WritePin(GPIOC, GPIO_PIN_6, GPIO_PIN_SET) void w5500_read_frame(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t ctrl (((addr 16) 0x03) 3) | (1 5); W5500_CS_LOW(); spi_tx_byte((uint8_t)(addr 8)); spi_tx_byte((uint8_t)(addr 0xFF)); spi_tx_byte(ctrl); /* 讀指令 塊選擇 變長模式 */ while (len--) { *buf spi_rx_byte(); } W5500_CS_HIGH(); } void w5500_write_frame(uint32_t addr, uint8_t *buf, uint16_t len) { uint8_t ctrl (((addr 16) 0x03) 3) | (0 5); W5500_CS_LOW(); spi_tx_byte((uint8_t)(addr 8)); spi_tx_byte((uint8_t)(addr 0xFF)); spi_tx_byte(ctrl); while (len--) { spi_tx_byte(*buf); } W5500_CS_HIGH(); }注意 ctrl 字節(jié)的低三位固定為 000這是 VDM 變長數(shù)據(jù)模式。塊選擇位來自 addr 的高位這是在驅(qū)動庫里約定好的 32 位地址編碼規(guī)則。片選必須在整個幀傳輸期間保持低電平不能每個字節(jié)都拉一次 CS否則 W5500 會認(rèn)為每次都是新的事務(wù)。如果你發(fā)現(xiàn)讀回來的數(shù)據(jù)只有第一個字節(jié)正確、后續(xù)全是 0xFF九成是片選時序問題。3.3 網(wǎng)絡(luò)參數(shù)初始化MAC、IP、掩碼、網(wǎng)關(guān)缺一不可把 W5500 驅(qū)動跑起來之后下一步是配置網(wǎng)絡(luò)參數(shù)。官方驅(qū)動的做法是先給每個 Socket 分配 TX/RX 緩沖區(qū)大小再寫入 MAC、IP、掩碼和網(wǎng)關(guān)。注意 MAC 地址不要用示例里的默認(rèn)值也不要和局域網(wǎng)里其他設(shè)備沖突否則會出現(xiàn)間歇性 ARP 失敗。這里給出最小初始化代碼/* 網(wǎng)絡(luò)參數(shù)結(jié)構(gòu)體 */ wiz_NetInfo g_net_info { .mac {0x02, 0x00, 0x00, 0x12, 0x34, 0x56}, .ip {192, 168, 1, 233}, .sn {255, 255, 255, 0}, .gw {192, 168, 1, 1}, .dns {192, 168, 1, 1}, .dhcp NETINFO_STATIC }; static uint8_t g_tx_buf[16 * 1024]; static uint8_t g_rx_buf[16 * 1024]; void network_init(void) { uint8_t txsize[8] {16, 0, 0, 0, 0, 0, 0, 0}; /* Socket0 獨(dú)占 16KB TX */ uint8_t rxsize[8] {16, 0, 0, 0, 0, 0, 0, 0}; /* Socket0 獨(dú)占 16KB RX */ /* 復(fù)位時序拉低保持 10ms再拉高等待內(nèi)部邏輯就緒 */ HAL_GPIO_WritePin(GPIOC, GPIO_PIN_5, GPIO_PIN_RESET); HAL_Delay(10); HAL_GPIO_WritePin(GPIOC, GPIO_PIN_5, GPIO_PIN_SET); HAL_Delay(100); wizchip_init(txsize, rxsize, g_tx_buf, g_rx_buf); wizchip_setnetinfo(g_net_info); /* 讀回來核對避免結(jié)構(gòu)體字節(jié)序?qū)е碌刂穼懛?*/ wiz_NetInfo verify; wizchip_getnetinfo(verify); }txsize 數(shù)組的單位是 KB16 表示 16KB。W5500 芯片內(nèi)部總共有 32KB 緩沖16KB 給 TX、16KB 給 RX所以 g_tx_buf 和 g_rx_buf 各分配 16KB。如果后續(xù)要開多個 Socket需要按比例拆分這些緩沖區(qū)每個 Socket 都要有獨(dú)立的 TX 和 RX。wizchip_init 的參數(shù)順序是 txsize、rxsize、txbuf、rxbuf很多人第一次寫反導(dǎo)致后續(xù) socket() 調(diào)用直接返回錯誤這一行值得反復(fù)核對。完成以上兩步后編譯下載PC 端配置同一網(wǎng)段的靜態(tài) IP比如 192.168.1.100ping 192.168.1.233。能通說明硬件鏈路、SPI、寄存器收發(fā)都正常ping 不通不要急著看代碼先用示波器量 SCLK 引腳有沒有時鐘輸出再量 RST 引腳電平是否被 W5500 拉低。硬件問題占了首輪調(diào)試的七成。4. 關(guān)鍵參數(shù)與調(diào)試技巧從 ping 通到 TCP/UDP 收發(fā)4.1 Socket 初始化與監(jiān)聽TCP 服務(wù)端連不上的常見原因W5500 的 Socket 操作流程和 BSD Socket 非常接近先 socket() 創(chuàng)建連接設(shè)置協(xié)議模式和本地端口然后 listen() 或 connect()。對 TCP 服務(wù)端來說最容易翻車的點(diǎn)是 socket() 返回成功就直接 while 循環(huán)里寫 send/recv沒有檢查 Socket 狀態(tài)。W5500 不會自己進(jìn)入監(jiān)聽狀態(tài)你必須顯式調(diào)用 listen()并且在每次 Accept 之前確認(rèn)狀態(tài)寄存器已經(jīng)是 SOCK_ESTABLISHED。#define TCP_SOCKET 0 void tcp_server_task(void) { if (socket(TCP_SOCKET, Sn_MR_TCP, 6000, 0) ! SOCK_OK) { return; /* Socket 打開失敗多半是緩沖區(qū)分配不足 */ } listen(TCP_SOCKET); while (1) { switch (getSn_SR(TCP_SOCKET)) { case SOCK_ESTABLISHED: { uint8_t buf[1024]; int16_t len recv(TCP_SOCKET, buf, sizeof(buf)); if (len 0) { send(TCP_SOCKET, buf, len); /* 回顯 */ } break; } case SOCK_CLOSE_WAIT: /* 對端關(guān)閉必須主動 disconnect 再 close否則 Socket 卡死 */ disconnect(TCP_SOCKET); close(TCP_SOCKET); break; case SOCK_CLOSED: /* Socket 被釋放后重新監(jiān)聽 */ socket(TCP_SOCKET, Sn_MR_TCP, 6000, 0); listen(TCP_SOCKET); break; default: break; } } }socket() 的第三個參數(shù) 6000 是本地端口第四個參數(shù) 0 是標(biāo)志位通常不用。這里的狀態(tài)機(jī)是我在工程里一直沿用的寫法核心思想是每個 case 都要有對應(yīng)的處置動作不能只處理 SOCK_ESTABLISHED。如果 TCP 客戶端連不上先讀一下 getSn_SR() 返回什么值一直停在 SOCK_INIT 說明 listen 沒生效停在 SOCK_CLOSED 說明 Socket 被斷開后沒有重新初始化。端口被占用時 socket() 本身就會失敗這時檢查是不是有上一個連接沒有 close。4.2 UDP 收發(fā)不需要握手但要注意 IP 與端口的來源UDP 的 Socket API 更簡單因?yàn)椴恍枰?listen 和 accept。調(diào)用 socket() 指定 Sn_MR_UDP 和端口后直接用 recvfrom 和 sendto。一個容易忽略的點(diǎn)sendto 必須傳入對端的 IP 和端口這個信息只能從 recvfrom 返回的參數(shù)里拿不能自己編。如果你的設(shè)備需要主動向服務(wù)器發(fā)送數(shù)據(jù)目標(biāo) IP 和端口寫死沒問題如果是應(yīng)答式協(xié)議請一定保存 recvfrom 回調(diào)里返回的 remote_ip 和 remote_port。#define UDP_SOCKET 1 void udp_echo_task(void) { uint8_t buf[512]; uint8_t remote_ip[4]; uint16_t remote_port; if (socket(UDP_SOCKET, Sn_MR_UDP, 6001, 0) ! SOCK_OK) { return; } while (1) { int16_t len recvfrom(UDP_SOCKET, buf, sizeof(buf), remote_ip, remote_port); if (len 0) { sendto(UDP_SOCKET, buf, (uint16_t)len, remote_ip, remote_port); } } }recvfrom 的返回值是收到的數(shù)據(jù)長度失敗時返回 SOCKERR_SOCKINVALID 這類負(fù)數(shù)。注意 remote_ip 是一個 4 字節(jié)數(shù)組remote_port 是 16 位無符號整數(shù)這兩個參數(shù)在 recvfrom 里是輸出型參數(shù)。UDP 模式下如果長時間沒有任何數(shù)據(jù)Socket 會保持打開狀態(tài)不需要像 TCP 那樣處理斷開事件所以上面的代碼可以一直輪詢。實(shí)測這個回顯邏輯在 13.5MHz SPI 下 UDP 吞吐量約 4Mbps遠(yuǎn)低于網(wǎng)絡(luò)帶寬瓶頸但做輕量級設(shè)備控制足夠。4.3 中斷與輪詢的選擇什么時候用回調(diào)什么時候用查詢W5500 有一個 INT 輸出引腳當(dāng) Socket 發(fā)生接收事件、連接建立、斷開等狀態(tài)變化時INT 會拉低。兩種處理方式各有適用場景中斷方式響應(yīng)快但你在回調(diào)函數(shù)里不能做耗時操作否則會阻塞其他中斷輪詢方式實(shí)現(xiàn)簡單但 CPU 需要頻繁讀寄存器。我的習(xí)慣是單 Socket 且數(shù)據(jù)流量不高的場景直接用 while 循環(huán)輪詢 Socket 狀態(tài)代碼簡單不容易出錯多 Socket 或者對響應(yīng)延遲有要求的場景把 INT 接到外部中斷中斷服務(wù)函數(shù)里只置一個標(biāo)志位實(shí)際收發(fā)放到主循環(huán)里做。判斷中斷源的方式是讀 Sn_IR 寄存器比如 Sn_IR_RECV 表示有數(shù)據(jù)到達(dá)處理完后寫回該寄存器清除標(biāo)志。如果不清除INT 引腳會一直保持低電平后續(xù)中斷再也進(jìn)不來。這個清標(biāo)志的操作必須放在讀取數(shù)據(jù)之前還是之后我的做法是先讀數(shù)據(jù)再清標(biāo)志因?yàn)榍逋陿?biāo)志后如果又有新數(shù)據(jù)到達(dá)會重新置位。如果先清標(biāo)志再讀數(shù)據(jù)數(shù)據(jù)讀走之前新數(shù)據(jù)到了標(biāo)志位會被置上但你可能讀漏一次。順序反了就是偶發(fā)丟包的來源。5. STM32H7 W5500 避坑指南5 條值得記錄的踩坑實(shí)錄5.1 現(xiàn)象能 ping 通但 TCP 連接總是超時現(xiàn)象是最典型的PC 能 ping 通開發(fā)板但用網(wǎng)絡(luò)調(diào)試助手連接 TCP 端口時一直沒有回應(yīng)。原因ping 走的是 ICMP 協(xié)議W5500 硬件協(xié)議棧會自動回包但這并不代表你的 Socket 已經(jīng)被正確配置。應(yīng)用層沒有調(diào)用 socket()、listen()或者 socket 打開失敗后沒有做任何處理都會導(dǎo)致端口沒有任何響應(yīng)。解決先在初始化代碼里檢查 socket() 的返回值確保等于 SOCK_OK然后打印 getSn_SR() 的狀態(tài)值如果一直是 SOCK_INIT 說明 listen() 沒有生效。另外一個常見問題socket() 里填的端口號是主機(jī)字節(jié)序還是網(wǎng)絡(luò)字節(jié)序在 W5500 驅(qū)動里端口號直接傳 uint16 即可不要手動調(diào)用 htons否則本來監(jiān)聽 6000 端口實(shí)際卻變成了 0x1770 反轉(zhuǎn)后的值PC 端怎么連都連不上。5.2 現(xiàn)象SPI 讀回全是 0xFF版本寄存器讀到 0xFF現(xiàn)象調(diào)用 wizchip_init 之前的讀版本操作返回 0xFF或者讀 MAC 地址全空。原因SPI 的極性和相位與 W5500 不匹配尤其很多人移植時直接復(fù)制了之前驅(qū)動 OLED 或 Flash 的 SPI 配置。W5500 支持 Mode 0 和 Mode 3但如果你用的是 Mode 1 或 Mode 2數(shù)據(jù)會在錯誤的邊沿被采樣讀回來的全是 1。解決確認(rèn) STM32CubeMX 里 SPI 設(shè)置為 CPOLLow、CPHA1Edge即 Mode 0。如果你在別的芯片上跑過 W5500 且當(dāng)時用的是 Mode 3那也要和現(xiàn)在的配置對齊不要混用。驗(yàn)證手段永遠(yuǎn)先讀 VERSION 寄存器讀到 0x04 再繼續(xù)往下調(diào)這是最省時間的判斷標(biāo)準(zhǔn)。5.3 現(xiàn)象數(shù)據(jù)偶發(fā)錯亂TCP 連接頻繁斷開現(xiàn)象設(shè)備長時間運(yùn)行后收到的數(shù)據(jù)出現(xiàn)字節(jié)錯位甚至 TCP 連接直接斷開重連。原因多半是中斷處理函數(shù)里做了過重的工作。W5500 的 INT 引腳下降沿觸發(fā)后如果你在中斷里直接調(diào)用 recv() 并且 recv() 內(nèi)部又通過 SPI 讀取大量數(shù)據(jù)整個 SPI 傳輸期間其他中斷會被阻塞此時又有新數(shù)據(jù)到達(dá)導(dǎo)致 INT 引腳拉低但標(biāo)志位還沒清除事件就丟了。解決中斷服務(wù)函數(shù)里只做一件事——置一個 volatile 標(biāo)志位主循環(huán)檢測到標(biāo)志后調(diào)用數(shù)據(jù)處理函數(shù)處理完畢再清除 Sn_IR。另一個隱蔽的坑是 recv() 讀完之后沒有正確回寫 Sn_RX_RD 寄存器導(dǎo)致 W5500 認(rèn)為數(shù)據(jù)沒被讀走緩沖區(qū)被占滿后停止接收表現(xiàn)就是連接還在但數(shù)據(jù)不再更新。建議讀完一段數(shù)據(jù)后立即調(diào)用 setSn_RX_RD() 和 command(Sn_CR_RECV) 收尾。5.4 現(xiàn)象換一塊開發(fā)板就失聯(lián)甚至第一次上電就失敗現(xiàn)象同一個程序在這塊板子上跑得好好的換一塊同型號板子就不通或者重新上電后進(jìn)入不可用狀態(tài)。原因W5500 的 RST 引腳懸空或者被板上的電容影響導(dǎo)致復(fù)位信號不完整。如果 RST 引腳沒接上拉電阻MCU 引腳默認(rèn)輸入狀態(tài)時電平不確定W5500 可能一直處于復(fù)位模式。解決確認(rèn) RST 引腳接了 10kΩ 上拉到 3.3V并并聯(lián) 0.1uF 電容到地。軟件上在 SPI 初始化之前執(zhí)行一次硬復(fù)位拉低 10ms拉高再等 100ms。如果你用的板子把 W5500 的 RST 接到了 MCU 的復(fù)位引腳上那么 MCU 每次復(fù)位都會帶動 W5500 復(fù)位這沒問題但要確保 MCU 的復(fù)位時間足夠長否則 W5500 還沒完成內(nèi)部初始化MCU 就開始訪問 SPI 了。5.5 現(xiàn)象ARP 解析偶爾失敗ping 時而通時而不通現(xiàn)象設(shè)備剛上電時 ping 不通等幾秒又通了或者局域網(wǎng)內(nèi)多臺設(shè)備同時出現(xiàn)時ARP 表現(xiàn)不穩(wěn)定。原因MAC 地址沖突。很多人下載的示例工程里 MAC 地址是寫死的同一個值比如 0x00 0x08 0xDC 開頭的那串兩臺設(shè)備在同一局域網(wǎng)就會出現(xiàn) ARP 緩存互相覆蓋表現(xiàn)就是時通時斷。解決為每臺設(shè)備生成唯一 MAC。STM32H7 內(nèi)部有 96 位唯一 IDUID取低 3 字節(jié)填充到 MAC 地址的末三位前三位保持廠商前綴不變。這樣即使幾十臺設(shè)備同時接入同一個交換機(jī)MAC 沖突的概率也極低。順帶提醒改 MAC 后需要重新上電才生效W5500 只在初始化時把 MAC 寫入 SHAR 寄存器。6. 進(jìn)階玩法把 W5500 從能用用到順手W5500 的入門玩法是數(shù)據(jù)回顯但真正決定這個方案價值的是三件事多 Socket 的緩沖區(qū)切割、DHCP 自動獲取 IP、以及把 W5500 做成固件升級通道。多 Socket 的緩沖區(qū)分配很容易理解W5500 總共有 16KB TX 和 16KB RX你需要通過 txsize 和 rxsize 數(shù)組把它們分配給 8 個 Socket。以下是我常用的分配策略Socket 數(shù)量TX 大小RX 大小適用場景1 個16KB16KB大包傳輸TCP 高速上傳2 個8KB / 8KB8KB / 8KB一個 TCP 一個 UDP4 個4KB / 4KB / 4KB / 4KB同左多路傳感器上報8 個2KB / 2KB ...同左長時間保持多個 TCP 連接注意一個 Socket 的 TX 和 RX 大小可以不同但芯片正常工作時不建議出現(xiàn) 0KB 的情況否則對應(yīng) Socket 打開會失敗。分配完緩沖區(qū)后記得重新調(diào)用 wizchip_init并且不能在程序運(yùn)行中途隨意改只能復(fù)位后重新初始化。DHCP 功能可以省去手動配置靜態(tài) IP 的麻煩。驅(qū)動庫里提供了 DHCP 客戶端初始化時把 g_net_info 的 dhcp 字段設(shè)為 NETINFO_DHCP然后周期調(diào)用 DHCP_run() 直到分配成功。實(shí)際項(xiàng)目里我一般給設(shè)備一個默認(rèn)靜態(tài) IP同時保留 DHCP 模式開關(guān)通過撥碼或配置項(xiàng)切換這樣既能在開發(fā)時快速調(diào)試又能在現(xiàn)場免配置部署。我最近做的一個項(xiàng)目中還把 W5500 的 TCP 服務(wù)端接到了 IAP 升級流程里設(shè)備啟動時進(jìn)入一個短暫窗口PC 端通過 TCP 連接把 bin 文件分幀發(fā)給設(shè)備設(shè)備收到后寫入外部 SPI Flash全部完成后跳轉(zhuǎn)到應(yīng)用區(qū)實(shí)現(xiàn)遠(yuǎn)程升級。W5500 在這里比 MCU 自帶 MAC 方案有天然優(yōu)勢——Bootloader 不需要引入?yún)f(xié)議棧只依賴一個 Socket API 就能實(shí)現(xiàn)文件接收代碼量少風(fēng)險低。這個方向適合有產(chǎn)品化需求的開發(fā)者重點(diǎn)投入一個是批量的設(shè)備離線升級另一個是設(shè)備日志遠(yuǎn)程抓取兩者的技術(shù)底座都是這套 Socket API。如果非要說一條血淚經(jīng)驗(yàn)?zāi)蔷褪窍劝?VERSION 寄存器讀出來再寫業(yè)務(wù)代碼先 ping 通再寫收發(fā)邏輯先調(diào)好狀態(tài)機(jī)再開優(yōu)化。這十幾年做以太網(wǎng)設(shè)備的經(jīng)驗(yàn)告訴我W5500 的坑基本都在硬件時序和初始化順序上軟件本身反而異常穩(wěn)定。按照上面的步驟走你也能少走那些我已經(jīng)替你們蹚過的彎路希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取