ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32F407+LAN8720以太网开发:CubeMX配置与LwIP实战详解

STM32F407+LAN8720以太网开发:CubeMX配置与LwIP实战详解 上个月帮一个客户做工业网关的原型验证电路板上画了STM32F407 LAN8720A 网络变压器软件用的是STM32CubeMX自动生成框架 FreeRTOS LwIP。我原本以为这种组合已经被网上写烂了真正动手才发现从CubeMX的某个参数填错导致ping不通到PHY读寄存器全是0xFF再到UDP数据莫名其妙收不到每一步都有能卡住半天的细节。这篇文章就把整个流程重新捋一遍CubeMX的ETH和LWIP配置、FreeRTOS任务的搭建、UDP/TCP收发代码怎么写、以及我在实际调试中踩过的那几个坑。适合刚接触STM32以太网开发、想用F407 LAN8720快速跑通网络通信的读者只要你手里有一块F407开发板和一块LAN8720模块跟着做基本能复现。我用的是CubeMX 6.9 STM32CubeF4固件包1.27.0LwIP版本为2.1.2FreeRTOS接口选择CMSIS_V2。不同版本界面可能略有差异但核心配置项是通用的。1. 开工前的连线梳理F407与LAN8720的RMII信号1.1 为什么选择LAN8720A而非DP83848F407的以太网MAC支持两种接口MII和RMII。MII需要16根数据线RMII只要10根左右F407上再接RMII更现实。PHY芯片的选择上LAN8720A是最常见、最便宜、资料最多的一款10/100M自适应RMII接口3.3V供电内部LDO输出1.2V核心电压外围电路非常简洁。有人会拿DP83848来做对比它也是一款经典PHY但需要注意DP83848的PHY地址默认是1LAN8720A的PHY地址默认是0。这个地址差异是很多人拿网上例程直接套用后MDIO读不到寄存器、PHY始终无法link的首要原因。我后面会再强调这一点。另外LAN8720A的RMII时钟有两种方式——外部50MHz直接输入或者25MHz晶振让PHY内部PLL倍频到50MHz。现在市面上几乎所有LAN8720模块都采用了“板载25MHz晶振 REF_CLK输出50MHz给MCU”的方式这让MCU侧的时钟设计变得非常简单。1.2 RMII十个信号逐一说明RMII模式下的信号不算多但每一个都有讲究信号名MCU引脚方向说明ETH_RMII_REF_CLKPA1输入50MHz参考时钟由LAN8720的REFOUT引脚提供ETH_RMII_CRS_DVPA7输入载波侦听/数据有效接PHY的CRS_DVETH_RMII_RXD0PC4输入接收数据位0ETH_RMII_RXD1PC5输入接收数据位1ETH_RMII_TX_ENPG11输出发送使能ETH_RMII_TXD0PG13输出发送数据位0ETH_RMII_TXD1PG14输出发送数据位1ETH_MDCPC1输出MDIO管理时钟ETH_MDIOPA2双向MDIO管理数据LAN8720 INT/nINT可选输入LAN8720中断脚一般不用悬空或接GPIO注意RMII标准中REF_CLK必须由MAC侧提供或由外部时钟源同时供给MAC和PHY但LAN8720的REFOUT方案是PHY自己产生50MHz输出给MAC这在模块设计上已经把时钟方向反过来了所以PA1在CubeMX中是作为输入信号存在的不需要在MCU里配置任何PLL。还有一个容易搞错的地方MCU的TXD0/TXD1必须接PHY的TXD0/TXD1RXD0/RXD1接PHY的RXD0/RXD1发对端别把TXD和RXD交叉理解成RS232那种概念这里不是交叉的。1.3 50MHz REF_CLK的两条路线与PA8占用提示虽然模块方案已经固定了“PHY给MCU供50MHz”的路线但还是有必要讲清楚另一种方案因为网上不少人会问“能不能直接用MCU的MCO输出50MHz给LAN8720”。理论上可以但F407的MCO1引脚PA8输出50MHz存在一个现实问题要实现50MHz输出PLL参数需要配到SYSCLK200MHz再四分频而F407最高主频只有168MHz超频带来的稳定性问题不值得冒。有人用25MHz外部晶振直通MCO输出25MHz给PHY这又要求PHY改配置不是默认晶振模式。另外很多F407板子的PA8引脚已经被USB的VBUS检测占用了热词里那句“stm32f407 pa8 vbus typec”说的就是这种冲突。如果板子上PA8连接了Type-C座的VBUS检测你又在CubeMX里把PA8配成MCO输出两个功能直接打架。所以我的建议很简单买现成的LAN8720模块25MHz晶振在模块上REF_CLK输出接PA1全程不需要碰PA8。1.4 模块供电和复位引脚的处理LAN8720A本身是3.3V供电模块上通常有LDO或直接3.3V输入注意模块的电源纹波最好在VDD处加一个10uF钽电容和100nF陶瓷电容组合这个细节在长时间大数据量传输时能明显降低偶发丢包。复位引脚nRST有两种接法一种是RC上电复位电阻10K上拉、电容1uF下拉到地另一种是用MCU的GPIO控制软件里先拉低再拉高完成复位。我强烈推荐后者。因为RC复位的延时不太可控如果PHY还没完全稳定MCU就开始初始化会读到异常寄存器值。用GPIO复位就简单了上电后GPIO拉低100ms再拉高然后延时200ms等PHY内部校准完成再接下去做MDIO读写百试百灵。2. CubeMX配置逐项拆解ETH、LWIP、FreeRTOS一次填对2.1 时钟树怎么配168MHz主频和ETH时钟的关系F407以太网MAC挂在AHB1总线上使用HCLK作为时钟源。比较常见的配置是外部8MHz晶振PLL倍频到168MHzAHB1168MHzAPB284MHz。以太网DMA和MAC都使用这个时钟不需要单独为ETH生成特殊时钟。在CubeMX的Clock Configuration页面里确认以下几点HSE选中Crystal/Ceramic ResonatorPLL Source选择HSESYSCLK设为168MHzAHB Prescaler设为1即HCLK168MAPB1 Prescaler为4得到42MAPB2 Prescaler为2得到84METH外设使用的是HCLK也就是168MHz只要AHB不分频就能满足。2.2 ETH参数设置PHY Address0是第一个坎进入Pinout Configuration左侧Categories展开Connectivity选择ETHMode选择RMII此时GPIO会自动分配PA1、PA2、PA7、PC1、PC4、PC5、PG11、PG13、PG14检查无误Parameter Settings里有两个重点一个是PHY Address另一个是MAC AddressPHY Address一定要填0。LAN8720A模块上的PHYAD0引脚默认是LED2/nINTSEL引脚通过strap电阻下拉到地所以PHY地址为0x00。如果你从DP83848的例程改过来那边填的是1不改就等着MDIO通信失败。MAC Address填一个在本地网络里唯一的地址就行比如 00:80:E1:00:00:01只要不和局域网内其他设备冲突即可。新版CubeMX里ETH参数中有一个关于PHY的标签页如果你购买了ST官方或第三方PHY驱动包可以在这里选但LAN8720不在官方预置列表里保持默认即可后续通过HAL库通用MDIO接口访问。2.3 LWIP内存与协议参数默认值不一定够用在左侧Middleware and Software Packs里找到LWIPMode改为Enabled。这里有几个关键选项配置项推荐值原因NO_SYSDisabled让LwIP跑在FreeRTOS之上使用tcpip_thread和netconn APIDHCPDisabled测试阶段使用静态IP更可控IP_ADDRESS192.168.1.10根据自己的局域网网段调整NETMASK255.255.255.0标准C类掩码GATEWAY192.168.1.1可选同网段直连时无所谓往下翻到Memory Settings这是最容易忽视的部分。默认的MEM_SIZE只有1600字节PBUF_POOL_SIZE只有8个这对一个简单的ping测试勉强够用但一旦UDP/TCP跑起来netconn API内部要分配很多控制块内存不够直接表现为创建连接失败、收包卡死、数据回显异常。我在实际项目中一般这样设置MEM_SIZE 65536这是LwIP内部堆的大小所有动态分配的内存都从这里出PBUF_POOL_SIZE 32每个PBUF大约1518字节32个差不多48KB足够处理多个并发连接PBUF_POOL_BUFSIZE 1518保持以太网MTU大小TCP_MSS 1460标准以太网MSSTCP_SND_BUF 65536TCP发送缓冲区TCP_WND 65536TCP接收窗口这些值直接决定了LwIP能支撑多少并发数据量。F407有192KB RAM留给LwIP一百多KB不算奢侈但如果你的工程还有大量其他任务和缓冲要按自己实际需求折中。2.4 FreeRTOS任务规划与内存分配在Middleware and Software Packs选择FREERTOSInterface选择CMSIS_V2这样任务创建、信号量、消息队列都有统一的CMSIS函数封装。我建议在建工程时就把三个任务规划好任务名优先级栈大小功能defaultTaskosPriorityNormal512字可以放状态指示灯闪烁等辅助逻辑udp_taskosPriorityAboveNormal1024字UDP收发交互tcp_taskosPriorityAboveNormal2048字TCP服务器/客户端交互TCP任务栈给到2048个字是因为netconn API的TCP接收路径上会有较深的调用栈再叠加上LwIP内部函数调用栈给小了容易莫名其妙HardFault。UDP栈1024个字一般够如果同时还跑mbedTLS或JSON解析需要再加。里面还有一个Memory Management默认就是heap_4保持默认即可。2.5 生成代码前的完整检查清单我每次生成代码前会过一遍这个清单避免生成后反复返工[ ] RCC里HSE已勾选并且外部晶振频率与实际板子一致常见8M或25M[ ] 时钟树SYSCLK显示168MHz没有红色报错[ ] ETH ModeRMIIPHY Address0[ ] LWIP开启NO_SYSDisabled静态IP配置好[ ] LWIP内存参数已调整PBUF_POOL_SIZE≥32[ ] FREERTOS接口为CMSIS_V2三个任务已添加到Tasks配置[ ] 如果使用ETH中断方式NVIC里ETH全局中断已打开优先级设为5或6不要太高[ ] 工程名和保存路径不要包含中文或空格确认无误后点击Generate Code生成完先别急着写业务代码编译一次保证空工程能过。这一步能过滤掉很多编译器路径、芯片选型的问题。3. 代码生成之后手工要做的三处修补3.1 启动文件里的堆大小必须加大CubeMX生成的启动文件startup_stm32f407xx.s里默认Heap_Size只有0x200512字节这对FreeRTOS来说太小了。任务栈全部从FreeRTOS自己的堆分配而这个堆正是由启动文件里的Heap_Size决定的heap_4.c会从这个区域开辟内存池。如果忘了改现象很经典编译下载后系统只跑默认任务udp_task和tcp_task压根不执行用调试器看osThreadNew返回值是NULL或者在任务创建那一刻HardFault。打开startup_stm32f407xx.s找到这一行Heap_Size EQU 0x200改成Heap_Size EQU 0x4000Stack_Size建议保持默认或加大到0x800因为main函数真正跑业务逻辑前用的还是MSP栈太小会出奇怪问题。改保存后重新编译FreeRTOS的内存池就有16KB可用了。如果三个任务栈加内部对象超过这个数继续调大到0x8000也行F407的内存足够。3.2 PHY复位与读寄存器验证先过自检再写业务生成完代码第一步不要急着写UDP、TCP逻辑而是先验证PHY链路是否正常。打开main.c在MX_ETH_Init之后加一个延时并读取PHY的ID寄存器确认MDIO总线通信正常。我的做法是在main.c初始化ETH之后加一个自定义函数void MX_ETH_Phy_Check(void) { uint32_t id1 0, id2 0; uint32_t bsr 0; HAL_GPIO_WritePin(LAN8720_RST_GPIO_Port, LAN8720_RST_Pin, GPIO_PIN_RESET); HAL_Delay(100); HAL_GPIO_WritePin(LAN8720_RST_GPIO_Port, LAN8720_RST_Pin, GPIO_PIN_SET); HAL_Delay(200); HAL_ETH_ReadPHYRegister(heth, 2, id1); HAL_ETH_ReadPHYRegister(heth, 3, id2); HAL_ETH_ReadPHYRegister(heth, 1, bsr); printf(PHY ID: 0x%04X 0x%04X, BSR: 0x%04X\r\n, id1, id2, bsr); }LAN8720A的PHY ID读取结果应该为0x0007和0xC0F1如果读出来全是0x0000或0xFFFF说明PHY地址配置错误或MDIO线路有问题。BSR寄存器bit2为1表示链路已连接插上网线后这一位才是1。这一步等于给整个网络通信做了“底层自检”省下后面业务调试无数时间。3.3 中断优先级、调度与LWIP线程的关系ETH中断在CubeMX默认生成代码里服务于网卡DMA的中断回调。如果NVIC里ETH中断优先级设置不当会出现一种非常隐蔽的问题网卡偶尔能收到数据但系统里tcpip_thread无法被正常唤醒或者一进中断就卡死。原因是FreeRTOS要求中断服务程序里调用FromISR结尾的API时中断优先级必须低于configMAX_SYSCALL_INTERRUPT_PRIORITY。在CMSIS_V2的默认配置里这个值通常映射为5如果使用4位优先级分组。所以我一般在NVIC设置里把ETH全局中断优先级设为5或6保证不高于FreeRTOS允许的范围。另外CubeMX生成代码里自动创建了ethernetif_input线程、tcpip_thread线程这两个线程一个负责从网卡DMA搬运报文一个负责LwIP协议栈处理不要手动删除或随意降低它们的优先级。默认情况下它们都能正常工作你只负责在业务任务里通过netconn API收发数据。3.4 首测字段用读写PHY寄存器验证底层链路第3.2节已经写了读PHY ID的方法再补充一个更实际的测试利用PHY的Basic Control Register0x00做软复位然后读Basic Status Register0x01观察自协商状态和链路状态。uint32_t bcr 0; uint32_t bsr 0; HAL_ETH_ReadPHYRegister(heth, 0, bcr); bcr | 0x8000; // 软件复位 HAL_ETH_WritePHYRegister(heth, 0, bcr); HAL_Delay(50); HAL_ETH_ReadPHYRegister(heth, 1, bsr); // 判断链路是否建立 if (bsr 0x0004) { printf(Link is UP\r\n); } else { printf(Link is DOWN\r\n); }插上网线管用拔掉网线Bit2变为0这种打点测试最可靠。LAN8720模块上也有LED指示灯它的亮灭状态同样反映链路和活动信息可以作为辅助判断。4. UDP和TCP业务代码从回声到双向收发4.1 在FreeRTOS中创建网络任务CubeMX的Tasks配置里建好任务后对应函数骨架会生成在freertos.c中。默认的例子里你只需要在三个任务函数里填充内容。需要注意进入任务函数后最好加一个无限循环配合osDelay或vTaskDelay避免任务占满CPU导致其他低优先级任务饿死。我习惯在任务开头先打印一句话确认任务已启动比如void udp_task(void *argument) { printf(UDP task started\r\n); // ... for (;;) { vTaskDelay(pdMS_TO_TICKS(2)); } }这里的printf如果走串口重定向到USART1记得CubeMX里把USART1也配置好对调试验证非常有用。没有串口打印的话用LED翻转也行但网络故障排查时串口几乎是必需品。4.2 UDP收发端口绑定、接收回调与netbuf使用UDP在netconn API里的模型很清晰创建netconn绑定本地端口然后循环等待接收数据。下面是一个完整的UDP回声服务器收到什么就原样发回实测在F407 LAN8720上稳定运行。void udp_task(void *argument) { struct netconn *conn; struct netbuf *buf; err_t err; void *data; u16_t len; conn netconn_new(NETCONN_UDP); if (conn NULL) { printf(UDP netconn create failed\r\n); vTaskDelete(NULL); return; } err netconn_bind(conn, IP_ADDR_ANY, 8080); if (err ! ERR_OK) { printf(UDP bind failed: %d\r\n, err); netconn_delete(conn); vTaskDelete(NULL); return; } printf(UDP echo server listening on port 8080\r\n); for (;;) { err netconn_recv(conn, buf); if (err ERR_OK buf ! NULL) { netbuf_data(buf, data, len); // 把数据原样回发给对端 netconn_connect(conn, netbuf_getaddr(buf), netbuf_getport(buf)); netconn_send(conn, buf); printf(UDP recv %d bytes, echo back\r\n, len); netbuf_delete(buf); } vTaskDelay(pdMS_TO_TICKS(2)); } }几个关键点netconn_recv是阻塞的UDP没数据时会一直挂起这正好配合FreeRTOS调度不会白白占CPU收到数据后netbuf_data拿到的是数据指针和长度不要直接存指针因为netbuf_delete之后数据就没了回显前先调用netconn_connect把对端地址和端口绑定到连接上这样后续netconn_send才知道发往哪里netbuf_delete务必调用否则内存泄漏跑上几个小时内存耗尽系统就卡死了4.3 TCP收发connect/accept、recv/send的配合TCP比UDP复杂的地方在于连接管理。netconn API里服务端的模式是new bind listen accept recv/write。下面是一个单线程TCP回声服务器一次只处理一个客户端连接对功能验证足够了。void tcp_task(void *argument) { struct netconn *server, *client; struct netbuf *buf; err_t err; void *data; u16_t len; server netconn_new(NETCONN_TCP); if (server NULL) { printf(TCP netconn create failed\r\n); vTaskDelete(NULL); return; } netconn_bind(server, IP_ADDR_ANY, 8080); netconn_listen(server); printf(TCP echo server listening on port 8080\r\n); for (;;) { err netconn_accept(server, client); if (err ! ERR_OK) { continue; } printf(TCP client connected\r\n); while ((err netconn_recv(client, buf)) ERR_OK) { netbuf_data(buf, data, len); netconn_write(client, data, len, NETCONN_COPY); netbuf_delete(buf); } netconn_close(client); netconn_delete(client); printf(TCP client disconnected\r\n); } }这里有两个容易掉进去的坑。第一个是netconn_accept之前忘记netconn_listen或者listen之后不检查返回值导致accept一直返回错误。我在调试时遇到过检查代码才发现listen返回值没处理。第二个是发送数据时第三个参数apiflags。我这里用了NETCONN_COPY表示netconn_write内部会拷贝一份数据到LwIP的发送缓冲区函数返回后你的data指针可以随意覆盖。如果不加这个标志LwIP会直接引用你的data指针函数返回后数据可能还没真正发出内容就被改了出现随机丢包。除非你清楚自己在做什么否则TCP发送一律加NETCONN_COPY。4.4 发送方向定时主动上报数据怎么写除了被动接收很多场景需要MCU主动每隔一段时间上报数据。以UDP为例可以单独写一个udp_client任务定时向PC端发送数据帧void udp_client_task(void *argument) { struct netconn *conn; struct netbuf *txbuf; void *data; err_t err; ip_addr_t remote; conn netconn_new(NETCONN_UDP); if (conn NULL) { vTaskDelete(NULL); return; } netconn_bind(conn, IP_ADDR_ANY, 0); // 本地随机端口 IP4_ADDR(remote, 192, 168, 1, 100); // PC端IP uint16_t remote_port 9000; printf(UDP client task started, send to 192.168.1.100:9000\r\n); for (;;) { txbuf netbuf_new(); if (txbuf ! NULL) { data netbuf_alloc(txbuf, 64); if (data ! NULL) { snprintf(data, 64, heartbeat %lu, (unsigned long)osKernelGetTickCount()); netbuf_setaddr(txbuf, remote, remote_port); err netconn_send(conn, txbuf); if (err ! ERR_OK) { printf(UDP send failed: %d\r\n, err); } } netbuf_delete(txbuf); } vTaskDelay(pdMS_TO_TICKS(1000)); } }这里面netbuf_alloc分配了数据空间netbuf_setaddr设置目标地址netconn_send发出后立刻netbuf_delete释放。务必要delete否则每秒钟泄漏一个netbuf不用多久内存池就会耗尽。TCP主动连接对端也一样用netconn_new netconn_connect netconn_write netconn_close netconn_delete如果远端没开服务器connect会返回错误要做超时重连逻辑不能让任务一上来就卡死。4.5 内存与pbuf释放的注意事项LwIP的所有内存都来自内部堆和内存池一旦泄漏现象是运行一段时间后网络功能逐渐失效但MCU其他任务还正常。排查起来非常隐蔽。我给自己定了三条铁律规则一netbuf来自netconn_recv用完必须netbuf_delete规则二netbuf_alloc出来的数据用完必须netbuf_delete规则三netconn_recv返回ERR_OK不代表buf一定有数据要先判NULL再操作至于LwIP内部API比如raw API里pbuf_free(p)的次数必须和pbuf_alloc的次数一致netconn封装层已经帮你处理了大部分但如果你自己写了raw回调函数就要格外小心pbuf引用计数。上面示例用的都是netconn API更安全也更适合RTOS环境。另外如果调试中发现netconn_create和netconn_bind偶尔返回ERR_MEM先把PBUF_POOL_SIZE和MEM_SIZE调大再检查是不是有netbuf泄漏。用CubeMX生成的ETH LWIP工程内存出现瓶颈的概率远大于CPU。5. 联调、抓包与五个经典问题5.1 PC端联调步骤与常用工具把开发板的网口用网线直连电脑网口或者都接到同一个路由器/交换机上。PC端把本地网卡的IPv4地址改成静态192.168.1.100子网掩码255.255.255.0网关可以不填。板子这边静态IP为192.168.1.10。联调顺序建议从底层到上层打开串口终端看板子启动后PHY ID和Link状态是否正常在PC上ping 192.168.1.10观察通断用网络调试助手建一个UDP Socket本地端口9000目标IP 192.168.1.10目标端口8080发一条消息看板子是否原样回显再建一个TCP Client连接到192.168.1.10:8080发消息验证TCP回显网络调试助手推荐NetAssist或者SocketTool它们支持UDP和TCP两种模式也可以同时开两个实例分别测。串口工具方面XCOM和SSCOM都行记得固件里重定向printf到串口。5.2 用Wireshark看到的三种典型报错如果业务层测试卡住打开Wireshark抓包往往一眼就能定位问题。第一种是只有ARP广播没有ICMP响应。这种情况说明板子在网络层是活的但对ICMP请求没有应答。检查LwIP里LWIP_ICMP是否使能有的精简配置为了省内存把ICMP关掉了ping自然不通。第二种是TCP三次握手完成但数据段没有ACK或乱序。多半是发送缓冲区太小TCP_WND和TCP_SND_BUF设置不匹配导致对端窗口为0双方僵持。此时把CubeMX里TCP_WND和TCP_SND_BUF同步调大两边重新上电。第三种是完全抓不到板子的任何报文。这时候问题不在LwIP业务层而是网络底层——PHY没link或者MDIO通信有问题。回过去看BSR寄存器的link位再看LED指示灯多数是网线接触不良或PHY复位没完成。5.3 问题排查速查表现象可能原因排查/解决PHY ID读回0x0000/0xFFFFMDIO线接反PHY地址错误检查MDC/MDIO接线PHY Address改为0读PHY寄存器正常但Link bit为0网线/变压器/RJ45座损坏对端未开启换一根网线确认对端设备已连接ping不通Wireshark抓不到报文PHY没配置成功检查PHY复位时序看LAN8720指示灯ping第一次超时第二次通过ARP缓存未建立PHY自协商耗时属正常现象启动后等待2秒再测试TCP能连接但收发一段时间卡死内存池不足或pbuf泄漏加大MEM_SIZE、PBUF_POOL_SIZE检查netbuf_delete任务创建失败串口无输出FreeRTOS堆大小不够修改启动文件Heap_Size为0x4000或更大5.4 下一步优化方向基础收发通了以后可以朝几个方向继续挖。一个是性能调优。当前用netconn API底层有数据拷贝发生。如果要做高吞吐转发考虑改用raw API在tcp_recv回调里直接用pbuf操作避免netbuf和用户缓冲区的两趟拷贝。F407 LAN8720跑TCP理论能到90Mbps左右实际大包传输到50-70Mbps是常见水平。另一个是安全与可靠性。LwIP自带DHCP客户端CubeMX里把DHCP选项打开就能改成自动获取IP。如果板子要长期运行加上看门狗网络任务异常时重启网络栈而不是整个系统复位。还有一个扩展点是上层的协议栈。LwIP之上可以挂mbedTLS做MQTT/TLS加密通信F407的168MHz主频跑AES-128-CBC大概能到几十MBps级别做轻量IoT网关完全够用。不过这些都属于“打通网络之后”的下一步了先把UDP/TCP收发跑顺后面的路自然就宽了。我在实际项目中的体会是F407 FreeRTOS LwIP这套组合最大的瓶颈从来不是芯片性能而是配置细节。CubeMX把80%的框架搭好了剩下20%的坑都集中在PHY地址、内存大小、时钟方案和任务栈这几件事上。你把这几个点一次配好整个开发过程会顺畅非常多。最后再分享一个小技巧把PHY寄存器读写的验证函数留在工程里每次改硬件配置后先跑一遍确认“底层通没通”再动上层逻辑这能帮你省下至少两天的排查时间。
返回列表