2026/9/9 10:35:36

F28388D以太网通信实战:从EMAC到LWIP移植全记录

F28388D以太网通信实战:从EMAC到LWIP移植全记录 去年接手一个电机测试台的上位机监控改造客户提了一连串要求实时看电流波形、转速、绕组温度还要能远程下发参数。UART方案虽然稳但那点带宽抓波形是真不够用以太网几乎成了唯一选项。当时手头正好有块YXDSP-F28388D开发板就决定直接在这上面把Ethernet通信做透——从最底层的EMAC硬件寄存器到LWIP协议栈的完整移植全程踩了不少坑。这篇记录就把整个链路写清楚包括硬件管脚配置、PHY芯片调试、LWIP接口适配和“Ping不通”这类问题的完整排查思路。项目做完再回头看最大的感受是这套板子跑以太网性能边界和坑位基本都在本文里了。这篇内容适合几类人看一是准备在C2000系列特别是F28388D上做网络通信的嵌入式工程师二是做协议栈移植但总被底层接口卡住的同学三是电气/自动化背景、想快速搞懂以太网链路组成和调试手法的朋友。如果你想直接看排错链路跳到第4节就有完整流程想从零复现整个项目建议顺着读。1. 为什么新项目会把网络通信押在YXDSP-F28388D上1.1 串口带宽卡了项目进度才把目光转向Ethernet先说说实际场景。客户需要在工控机上实时显示电机运行曲线原来用的是USB转串口标称115200波特率除去协议帧头、校验和应答开销实际有效载荷只有不到8KB/s。而一个基本的诊断协议要同时上传电流、电压、转速、温度四路数据每路128点波形每帧约600字节更新频率还要求100Hz串口无论如何都喂不动。这时候第一反应是换USB高速方案或者走CAN。但USB在工业现场离得远就得加长线缆、隔离成本高CAN带宽也只有1Mbps级别对波形上传依然不理想。以太网的优势很明显物理层带宽100Mbps实际能跑出几十Mbps的有效吞吐线缆传输距离100米起步对上位机来说TCPUDP甚至HTTP都是现成协议栈不用自己折腾帧格式和错误重传。决定用以太网之后芯片选型就成了下一个问题。市面上能跑以太网的MCU不少但在这个项目里DSP侧要同时做三路电机控制控制周期50微秒所以必须保留一个高性能实时内核。YXDSP-F28388D就是这么进候选名单的。1.2 F28388D的双核架构里Ethernet模块到底挂在哪个核上F28388D很有意思它不是一颗纯DSP而是一颗异构双核处理器一边是C28x DSP核主频200MHz负责实时控制另一边是Arm Cortex-M4核主频200MHz负责通信和系统管理。两个核可以独立跑不同程序中间通过IPC核间通信和共享RAM交换数据。Ethernet外设挂在M4核的总线上EMAC、MDIO这些寄存器都从M4的内存映射地址访问。这一点很关键别想着在C28x核上直接操作EMAC寄存器C28x那个地址空间根本没有外设映射。所以实际方案里LWIP协议栈全部跑在M4核上C28x只负责算控制算法然后把结果通过共享内存丢给M4由M4封装成网络包发出去。这个架构天然适合“控制通信”分离的场景。C28x的反应速度毫秒级不被打断M4就算被网络中断淹没也不会影响控制环。反过来如果你拿一颗单核MCU同时做电机控制和TCP/IP协议栈协议栈一次处理十几个包的堆栈消耗和中断延迟就足够让控制周期超时。1.3 EMAC、PHY、网络变压器的分工硬件链路先看明白第一次接触以太网硬件的人容易把EMAC和PHY搞混。我习惯用一个粗暴的类比EMAC是“网卡控制器”它管理MAC地址、以太网帧的封包解包、CRC校验、DMA传输PHY是“物理层翻译器”负责把并行的数字信号调制成网线上的差分模拟信号同时处理链路协商、速率检测网络变压器则承担电气隔离和共模干扰抑制网线上那两根差分线经过它之后才能安全地接进PHY。YXDSP-F28388D开发板上的PHY芯片型号用的是国产IP101GRI接口方式是RMII。RMII比MII省一大半引脚MII需要14根数据线RMII只要9根左右TXD0/TXD1、TX_EN、RXD0/RXD1、CRS_DV、MDC/MDIO外加一个50MHz参考时钟。对F28388D这种引脚紧张的封裝来说用RMII几乎是必然选择代价是每个时钟沿采2bit吞吐和MII一样都是100Mbps。有个细节必须在画板阶段就注意RMII模式下的50MHz参考时钟可以从MAC侧主动生成给PHY也可以由PHY反馈给MAC。板级设计手册上通常建议用“MAC提供时钟”的方式保证两边同源我实际测下来这种方式最容易稳定。如果你拿到的板子不是这种时钟方案SDK启动时会卡在PHY link up上这个后文排查章节会展开。2. EMAC硬件初始化从寄存器到DMA描述符的关键细节2.1 管数据的EMAC和管配置的MDIO先分清角色EMAC模块内部有两条独立通路数据通路和配置通路。数据通路上跑的是从网线收下来的帧、要发出去的帧配置通路则通过MDIO接口去访问PHY芯片的寄存器。MDIO全称“Management Data Input/Output”只有两根线MDC时钟和MDIO数据。MAC通过这个口读写PHY寄存器比如让PHY做速率自动协商、读取链路状态、获取PHY芯片型号。初始化的第一步往往就是读PHY的寄存器0和1一个是控制寄存器一个是状态寄存器读到的值能直接判断PHY活着没活。F28388D的EMAC在TI的SDK里已经被封装了一层驱动但为了搞清楚机制我还是手动操作了一遍关键寄存器。EMAC模块有一套自己的寄存器基址通过syscfg工具生成地址映射启动代码里重点配置三个寄存器段MAC控制寄存器管帧格式、流控、全双工、RX/TX控制寄存器管发送接收使能、以及MDIO控制寄存器管PHY访问时序。如果哪一步写错表现不是初始化报错而是PHY完全不应答。对于想从零移植的同学我的建议是别上来就啃手册里几百页寄存器表先把SDK例程里EMAC初始化那几百行C代码对照寄存器手册走通一遍搞清楚每个寄存器位是干什么的再去做裁剪会高效得多。2.2 MAC地址、速率与双工初始化之前就要定死的参数MAC地址是全网设备身份的唯一标识一共6字节。F28388D的EMAC寄存器里有专门的MAC地址寄存器高位字和低位字把6字节地址拆开写入即可。但有个坑MAC地址的最高字节最右一位是组播/单播标志实际使用时这一位必须为0。很多板子出厂时Flash里写的MAC地址可能带前缀我在调试中就遇到过一次因为MAC高字节第0位为1导致板子把所有组播帧都当成发往自己的包协议栈收进来一堆垃圾数据。速率和双工模式建议直接采用自动协商。PHY上电后默认会发起自动协商LMAC侧也配置成自动协商模式让网线对端的交换机或PC网卡去匹配。如果硬编码成100M强制全双工遇到对端设备不支持或者协商失败现象就是Link灯常亮但一个包都通不了。有一类特别隐蔽的问题RMII模式下自动协商和实际数据收发用的时钟必须统一。有些PHY在自动协商期间用的是内部25MHz晶振协商完成后切换到外部50MHz参考时钟如果切换没完成寄存器里已经显示Link Up但收发一个包都没有。这个只能在PHY寄存器里看具体状态位我一般会读PHY的0x11寄存器MII状态指示寄存器确认真正的连接状态。2.3 DMA描述符与收发缓冲区LWIP能跑起来的基础EMAC的数据收发不是CPU直接搬字节而是通过DMA完成的。DMA工作前需要构建一套描述符表每个描述符告诉DMA缓冲区地址、缓冲区长度、状态标志、下一个描述符指针。F28388D的EMAC描述符是环形链表结构初始化完成后DMA会按顺序遍历描述符把收到的数据写进缓冲区或者把要发的数据从缓冲区取走。这套描述符机制里最容易被新手写错的是两个字段一是Buffer Offset字段硬件要求必须设置为0或1表示从缓冲区偏移多少字节才开始有效数据一般配置为0二是Ownership位这是硬件和软件交接的“钥匙”——硬件写入数据时持有该位软件处理完数据后要把它归还给硬件。如果忘了归还DMA会认为这个描述符仍在忙一直不写入新包现象就是接收中断只触发几次后就再也不来了。另一个容易忽略的是描述符和缓冲区的内存对齐。F28388D的EMAC要求描述符按4字节对齐、缓冲区建议按缓存行或64字节对齐。现代编译器一般默认4字节对齐但如果你用结构体数组分配缓冲区加上#pragmapack语句很容易破坏对齐规则出问题时很难查。2.4 中断路由为什么我选择把以太网中断交给CM4处理EMAC有两类中断源一类是收发完成中断另一类是PHY链路状态变化中断通过MDIO上报。中断路由到CPU这一步F28388D提供了很大的灵活性因为有两个核。我最终选择把所有以太网中断统一挂到CM4核的NVIC上。理由不复杂LWIP协议栈在CM4上跑它需要及时响应事件如果让C28x读网络中断再IPC通知CM4一轮核间通信的延迟和上下文切换在这个应用场景里完全是额外开销。更重要的是C28x要盯控制周期网络突发帧会让它在中断里待太久控制任务容易错过截止时间。初始化时还要特别注意EMAC使能时会自动触发一次伪中断Pending Interrupt。SDK例程里的写法是“先无效化全局中断再使能硬件最后清一次中断标志”这个顺序不能乱否则一开机没理由地进一次处理函数对调试很不友好。3. LWIP协议栈移植接口适配比拷源码难十倍3.1 基于TI SDK的LWIP例程起步避免重复造轮子LWIP是一个开源TCP/IP协议栈源码本身已经非常成熟网上能找到从ARM7到RISC-V各种平台的移植教程。第一次接触的人容易有一个误解把LWIP源码包整个丢进工程、配置include路径就能跑起来。真实情况和这差了十万八千里——真正花时间的不是编译协议栈而是把协议栈和具体网卡硬件之间的接口填好。TI的C2000 SDK里专门为F28388D准备了enet_lwip例程它把底层EMAC驱动和上层LWIP都串好了。我的做法是先把例程完整编译、烧录、ping通然后再一点一点改成自己的业务逻辑。不建议直接新建空工程开始手写因为TI的syscfg配置文件里藏了很多芯片地址映射和时钟树细节手工搞非常容易漏。编译工程时用CCS的默认优化等级通常-O2就行LWIP对C语言特性要求不高不需要特殊编译选项。但如果你的工程里同时跑了FreeRTOS就得关注LWIP是否开启OS支持和系统锁机制这决定你调用LWIP API的线程安全方式。3.2 六个必须封装的底层接口netif与EMAC之间的桥LWIP与硬件之间通过一个netif结构体交互。理解netif之前我先说一下LWIP的分层设计最上层是应用调用socket或netconn API中间是TCP/IP协议栈核心管理报文、连接表最下层是网卡抽象层也就是netif。移植的核心任务就是实现netif结构体里声明的几个函数指针。按我这次移植的经验需要重点处理的接口有六个low_level_init初始化EMAC和PHY创建描述符和缓冲区设置netif的MAC地址注册中断。这个在开机系统初始化阶段调用一次。low_level_output把LWIP要发送的数据一个或多个pbuf拆散映射到DMA可读的缓冲区里交给发送描述符最后触发发送。如果pbuf是链式结构一个网络包可能占多个不连续内存块需要把每个块都配置进发送描述符的“分片”字段。low_level_input从接收描述符里取数据包装成LWIP的pbuf形式并返回给协议栈。这里能优化成零拷贝也就是描述符缓冲区不复制直接把pbuf指向描述符缓冲区处理完后再把缓冲区还给DMA。ethernetif_input这是中断处理函数通常在ISR里调用tcpip_input当使用tcpip_thread时或ethernet_input当无OS直接轮询时。F28388D裸机例程用的是后者它会把收到的以太网帧递给上层协议解析。ethernetif_link_callbackPHY链路状态变化时的回调LWIP要求链路变化及时通告netif_set_link_up/down这样TCP连接才能在断网时快速感知。ethernetif_rx_irq清中断标志并调用input函数注意不能在中断里做大量内存拷贝否则实时性全毁。3.3 lwipopts.h参数裁剪内存只有这么多必须精打细算LWIP的内存策略直接影响性能和稳定性lwipopts.h里那些宏不是随便填的。F28388D的RAM总容量虽然不小但C28x和M4两部分还有控制程序占用LWIP最多分到几百KB所以要精打细算。我在项目里用的关键参数如下宏定义值说明NO_SYS1裸机运行不上操作系统简化线程锁MEM_SIZE128KB协议栈堆大小供TCP控制块、路由表等使用PBUF_POOL_SIZE32接收缓冲区池数量需要在内存占用和抗突发流量间平衡PBUF_POOL_BUFSIZE1518足够容纳整个MTU帧150014头FCSTCP_MSS1460一个TCP分段最大长度MTU-IP头-TCP头TCP_WND8 * TCP_MSSTCP接收窗口128KB级别足够跑满百兆LWIP_DHCP0固定IP工业现场不做DHCPLWIP_AUTOIP0同上避免冲突耗时LWIP_STATS0释放统计代码省ROMNO_SYS模式最关键。裸机环境下LWIP没有自己的任务调度你必须在主循环里定时调用sys_check_timeouts()来处理TCP超时重传、ARP老化这些事务。如果这个函数长期不调用TCP连接建立到一半就卡死因为SYN重传定时器永远不跑。3.4 内存管理与PBUF机制让数据在协议栈里转起来LWIP的数据缓冲叫pbuf有三种类型PBUF_RAM放在堆里数据在内存中连续、PBUF_ROM指向只读数据、PBUF_POOL从接收池中分配可以链式连接。以太网接收方向最常用PBUF_POOL。DMA把帧数据写进预分配缓冲区然后low_level_input把它封装成一个pool类型的pbuf协议栈解析时如果数据跨多个pbuf节点会通过p-next链式访问。需要注意的是DMA写入的数据包含完整以太网帧头目的MAC、源MAC、类型域而LWIP的ethernet_input正好通过帧头里的类型域判断是IP包、ARP包还是VLAN包所以不用自己在驱动层剥头。发送方向我用PBUF_RAM。应用层调用netconn_write或socket发送时LWIP会从堆里分配一段连续内存一次次填入TCP头、IP头、以太网头最后交给low_level_output。这里的误区是一旦调用low_level_output不能认为协议栈把这个包的所有权交给你了驱动得在硬件发送完成中断里释放它。如果发送完成中断被屏蔽太久描述符和pbuf都会泄漏。4. 实测调通Ping不通时我是怎么一步步排查的4.1 第一轮排查MDIO能读到PHY寄存器基本盘就稳了我把程序烧进去信心满满地打开命令行输入ping结果是“请求超时”。先从最基本的硬件链路查起。第一步是在初始化代码里加一段MDIO读测试直接读PHY的寄存器0BMCR控制寄存器和寄存器1BMSR状态寄存器。串口打印出来PHY ID: 0x0083 BMCR: 0x1000 BMSR: 0x7849PHY ID读出来是0x0083说明MDIO链路通了EMAC与PHY之间的管理通道没问题。BMSR的低位表示支持能力0x7849里有自动协商完成位这说明PHY的协商已经完成了。网络变压器、RJ45、PHY供电、MDIO这些物理环节在电学上都是通的。如果这一步读不到任何值排查方向是MDC时钟分频对不对EMAC的MDIO时钟不能超过2.5MHz、PHY地址对不对一般硬件上通过引脚上下拉决定F28388D平台默认0x00或0x01、PHY复位引脚有没有被拉高。4.2 第二轮排查发送路径用Wireshark验证比盲猜快得多MDIO能读到寄存器只说明PHY还活着不代表数据收发没问题。下一步验证发送路径。我在DSP代码里写了这样一段测试构造一个UDP广播包目的MAC为FF:FF:FF:FF:FF:FF目的IP为255.255.255.255填充好UDP头、IP头、以太网头调用low_level_output接口丢给DMA发送。PC端打开Wireshark抓包看能不能抓到。第一次测试Wireshark什么都收不到。这说明MAC到PHY方向要么没发出去要么发出的电平不对。我当时的排查很直接用示波器看PHY的TXD0/TXD1和TX_EN引脚发现TX_EN是正常的10MHz周期性信号但FF:FF的MAC地址在RMII上表现为2bit信号模式示波器看得不太直观。后来定位到问题在发送描述符的Flag字段——把发送缓冲区长度写错了。EMAC的发送描述符中长度字段是包含CRC的但并没有要求驱动填CRC如果长度多填了4字节PHY会把CRC也算进以太网帧尾交换机直接把这包当坏帧丢弃。把长度改为实际有效载荷后重新抓包Wireshark立刻弹出了完整的UDP广播包。4.3 接收方向最隐蔽的坑RMII参考时钟与中断丢失发送通了继续验证接收。PC上执行ping命令发ARP请求DSP进入主循环后我在low_level_input里放了一个全局标志串口打印标志位。结果串口一直不打印。也就是说EMAC的接收中断压根没触发或者触发了但没进我的处理函数。查硬件的第一个点是PHY的RXD0/RXD1和CRS_DV引脚示波器一量数据确实有进来。再查PHY的寄存器发现RXN/RX状态是正常的。最后盯着RMII参考时钟看了很久。这个50MHz时钟如果停在低电平等异常状态PHY和MAC两侧都会处于“假死”状态。示波器直接戳时钟输入引脚发现它是由MAC侧提供的但波形幅度只有1.6V左右。原本应该3.3V方波的时钟变成了一堆毛刺。查原理图发现这个开发板在参考时钟线上串联了一颗磁珠磁珠对高速时钟影响很大换成0欧电阻后时钟恢复正常收包立刻通了。这个坑提醒我RMII模式下接口速度高任何串阻、磁珠、长走线都可能让质量差的时钟失效。如果在自己的板上用RMII参考时钟线上少加奇奇怪怪的元件严格按照PHY手册的参考设计来。4.4 能进中断却回不了包PBUF和缓存一致性导致的幽灵问题时钟问题解决后DSP能收到ARP请求了但PC端依然收不到ARP回复。这时候问题已经从物理层转移到了软件层。我在中断里加了打印确认收到了完整以太网帧MAC地址和ARP请求解析都对。但回复包构造完之后调用low_level_outputPC抓包仍然没有。把打印加到发送函数里发送函数返回成功但Wireshark没包。最后把怀疑点放在数据缓冲区上F28388D的DMA在写入内存后CPU直接读取该缓冲区时可能读到缓存里的旧值。虽然M4核默认没有开数据缓存但编译器的优化和内存预取也可能导致类似现象。我在读取描述符状态位和缓冲区内容前加了一层内存屏障问题立刻消失。这类问题在单核小系统里不常见一旦出现会非常难查因为它不报错、不异常、只是数据是旧的。我的建议是描述符和DMA缓冲区尽量放在非缓存的RAM段比如F28388D可配置的CLA直连RAM或专门的DMA RAM如果必须用共享内存那每次读写前都要确保同步。4.5 最终验证从Ping通到TCP回环的完整数据记录修完上面几个问题ping开始有响应了。最初的延迟如下64 bytes from 192.168.1.10: icmp_seq1 ttl64 time0.3 ms 64 bytes from 192.168.1.10: icmp_seq2 ttl64 time0.2 ms 64 bytes from 192.168.1.10: icmp_seq3 ttl64 time0.2 ms这时基本可以确认协议栈通了。接着做一个TCP回环测试板子上起一个TCP server监听端口5000PC用网络调试助手连接发送一串数据板子原样返回。连续发送10万字节接收端完整收到无丢包无乱序。在测试中有一项我很在意PC端随机拔掉网线再插回板子能否自动恢复连接。结果第一次恢复就花了接近30秒因为LWIP的TCP超时重传默认是5次累计要很长时间才能感知到链路断了。后来通过PHY链路中断事件触发netif_set_link_down再通知TCP连接关闭恢复时间降到1秒以内。5. 跑通之后的事吞吐量优化与工业场景工程化5.1 吞吐量优化从能用到好用差在描述符和中断设计ping通只能说是功能验证对实际业务的吞吐量要求还需要专门优化。首先是描述符数量。接收描述符默认8个在突发流量面前很容易瞬间耗尽DMA没地方放下一帧数据就直接丢弃。我把接收描述符扩到32个内存占用也就增加几十KB但抗突发能力提升明显。紧接着是中断处理策略每个包都进一次中断在100Mbps满载下会出现“中断风暴”。我做了一个简单的中断聚合接收中断触发后在当前处理函数里循环读状态寄存器把所有已完成的接收包都取走再清中断重新使能。实测这一项把CPU占用从60%降到25%左右。然后是发送路径的优化。LWIP默认在发送完成中断里释放pbuf但如果发送频率很高中断太频繁。我改成了发送完成描述符轮询模式发送中断只置一个标志位主循环里轮询检查当标志有效时批量释放完成包。虽然发送实时性略降但对这种监控场景完全够用。最终在百兆网络里UDP单向吞吐实测到了94MbpsTCP单向吞吐约80Mbps考虑到协议帧头开销基本接近线速。5.2 与C28x实时核的数据流转共享内存和IPC的正确姿势业务上C28x核每秒要往上位机推送4000帧波形数据。如果让C28x通过IPC逐包发给M4再组帧IPC带来的延迟会挤占控制周期。最终设计是这样一个环形缓冲区C28x把控制结果和波形包按固定结构体写入共享RAM写完后通过IPC发一个轻量级通知给M4M4收到通知后直接从共享RAM读数据套上以太网帧发往上位机。共享RAM区域用CMD文件固定地址双方都放在不缓存段结构体按4字节对齐。这里有个容易踩的性能问题两个核同时访问同一块共享RAM时总线仲裁会引入等待周期。实测下来访问冲突对C28x控制周期的影响在100ns以内对200MHz主频的DSP来说占不到一个完整周期完全可以接受。5.3 工业场景必须考虑的稳定性掉线重连、看门狗、唯一MAC实验室里跑通是一回事现场能用是另一回事。项目中我还做了三项加固掉线重连不再依赖TCP超时。PHY链路中断上位机一直在ping但TCP连接断了之后重连逻辑要么在服务器端尽快关闭要么在客户端主动侦测。MCU侧作为TCP server链路断开时主动监听到netif_down事件然后关闭所有监听连接并重新进入listen状态这样客户端重连基本是秒级完成。看门狗喂狗和网络状态绑在一起。做法是在主循环里维护一个通信心跳计数每次收到有效数据或定时器超时都更新看门狗只在下一次喂狗窗口打开时才清零。这样如果协议栈死锁或者主循环卡死看门狗能立刻复位而不至于让系统挂死无人知。MAC地址不写死。开发板出厂预留的一串序列号我把它转成6字节MAC地址高位字节填充厂商OUI固定值低三位字节用序列号低24位。这样批量生产时每块板子都有唯一地址不会出现广播包冲突。5.4 我的最终参数建议与交付形态把最终lwipopts.h的关键配置单列在下面这份配置在128KB LWIP堆空间下能跑百兆线速适合多数监控类产品配置项推荐值备注TCP_MSS1460配合MTU 1500TCP_SND_BUF16 * TCP_MSS发送缓冲够大避免写阻塞TCP_WND16 * TCP_MSS接收窗口百兆下很关键PBUF_POOL_SIZE32收发抗突发MEM_SIZE128KB若资源紧张可降64KB但TCP并发数会降LWIP_STATS0省ROM省CPU链路事件回调打开必须用于掉线重连通过这套方案YXDSP-F28388D开发板最终跑成了“实时控制以太网监控”一体化装置。C28x干核心控制M4负责通信两者各司其职。如果后续要更高速率的同步通信可以考虑在同样硬件上引入EtherCAT或TSN方案但对目前主流的实时监控、参数下发、固件升级需求这套TCP/IP链路已经完全够用。