YXDSP-F28388D以太网移植实战:EMAC驱动与LWIP协议栈接入详解
2026/9/8 15:02:07 网站建设 项目流程

拿到YXDSP-F28388D开发板那天,我原本的计划是先跑个电机控制样例,毕竟C2000系列在工业控制圈里的名声太响。但这块板子跟常见的C2000开发板不太一样,除了两个C28x DSP内核,还塞了一个Cortex-M7内核,外设列表里更躺着一个完整的Ethernet MAC模块。刚开始我也没太把这个网口当回事,直到项目需求里明确写了“设备需要和产线上位机通过TCP/IP完成状态上报和固件升级”,我才意识到,这次必须认认真真把EMAC硬件和LWIP协议栈从底层啃一遍。

这篇文章就把整个移植过程记录下来,从F28388D的EMAC外设特征、RMII接口和PHY芯片的硬件设计,到描述符驱动、MDIO读写、LWIP的lwipopts配置和netif接口对接,再到最后ping通、长时间传输验证的完整经历。如果你刚好也在折腾YXDSP-F28388D,或者用的是其他带以太网MAC的MCU,这篇文章里的思路和踩坑点应该能帮你省下好几天的排查时间。

1. 这块开发板的以太网外设,为什么值得单独写一篇

1.1 C28x与Cortex-M7双核架构下的网络外设位置

F28388D在TI C2000家族里算是非常“跨界”的存在。它内部有两个C28x DSP核心,负责实时控制、电机算法、采样处理这些重负载任务;同时还有一个Cortex-M7核心,主频和性能都不错。我的第一反应是:这芯片是不是在“不务正业”?一个单片机带以太网MAC,还要配一个M7核,怎么想都不像传统DSP该干的事。

但真正动手查手册之后才发现,这个设计特别聪明。C2000的核心强项是控制环路,而以太网协议栈、TCP/IP解析、文件传输这些事情非常吃通用处理能力,如果硬塞给C28x,反而会拖累控制实时性。让Cortex-M7去跑LWIP,C28x继续专注FOC或PFC算法,两者通过共享内存或IPC通信,这种分工在实际项目里非常高效。

YXDSP这块开发板做的事情,就是把这些外设资源全部引出来。板载的RJ45网口、PHY芯片、调试器接口,还有针对C2000扩展的排针,让我可以直接拿它做原型验证。对整个项目来说,这块板子承担的是“通信网关”角色:底层设备数据由C28x采集和运算,M7核处理网络协议,再把数据包送上以太网。

1.2 以太网接入给工业控制场景补上了哪块短板

以前做C2000项目,通信方式基本就是RS485、CAN、SPI/UART这些。它们各有各的优势,但放到产线远程监控这个场景里,短板很明显:CAN看着挺快,实际传大量日志或固件升级文件时,带宽不够;RS485虽然抗干扰好,但标准Modbus协议栈对大数据块传输和动态拓扑支持有限。

Ethernet的优势不用多说:带宽高、生态成熟、工具链丰富,上位机随便一个TCP/UDP客户端就能对接。F28388D这颗芯片直接内置以太网MAC,等于硬件上已经把口子留好了,软件上只需要把LWIP跑起来。对很多工业设备来说,有了以太网接口,就意味着可以脱离调试串口,直接用网络做远程配置、Web页面监控、OTA升级。这也是我为什么在这个项目里宁可花时间把EMAC底层一点一点调通,也要把网口真正用起来的原因。

2. 硬件链路线索:从F28388D的EMAC引脚到PHY芯片RMII接口

2.1 为什么选择RMII而不是MII

F28388D的EMAC模块同时支持MII和RMII两种外部接口。MII是经典的100M以太网接口,信号线比较多:TXD[3:0]、RXD[3:0]、TX_ER、RX_ER、CRS、COL,再加上时钟,总共十几根线;RMII则把数据线砍成TXD[1:0]、RXD[1:0],控制信号合并为TX_EN和CRS_DV,整体引脚数少了一半。

我一开始差点选了MII,原因很朴素:MII是老前辈,资料多,TI手册里很多例子都用MII,照着写不容易出错。但看了原理图才发现,YXDSP这块板子引出的PHY接口就是RMII,而且RMII对实际项目有非常实际的好处:引脚占用少,PCB布线压力小。F28388D本身外设多,引脚复用紧张,把EMAC占用的引脚控制在RMII范围,剩下还能给电平转换、GPIO、PWM保持足够余量。

RMII有个硬性要求:参考时钟必须精确且稳定,频率是50MHz。这要求不像串口那样有点偏差也能忍,50MHz如果没有,或者抖动过大,数据就会偶发错包、丢包。设计上我强烈建议让PHY芯片自己输出50MHz时钟给MCU,而不是让MCU去给PHY供时钟。为什么?PHY靠近网口,时钟走线短,MCU更好控制时序,而且很多PHY芯片内置时钟电路,省掉一颗外部有源晶振。

修改后的文字(只显示本段内容,如果不够长请告诉我)

在F28388D手册里,EMAC外设的引脚复用寄存器需要把对应PINMUX配置成EMAC功能。RMII模式下,至少用到的信号就是这些:

信号方向作用
REF_CLK输入50MHz参考时钟,由PHY或外部时钟源提供
TX_EN输出发送使能
TXD[1:0]输出发送数据
CRS_DV输入载波侦听/数据有效
RXD[1:0]输入接收数据
MDC输出MDIO管理时钟
MDIO双向MDIO管理数据

如果你手头的开发板原理图走的是MII接口,那引脚更多,但只要软件里把EMAC模块配置成MII模式,一样能跑。重点是你要对应好板子上实际引出的PHY连接方式,硬件和软件必须保持一致。

2.2 PHY选型对比:LAN8720A和它的替代者

PHY芯片负责把MAC层的数字信号转换成模拟差分信号送到网线上。F28388D本身不带PHY,必须在板子上外接一颗。YXDSP这块开发板用的是LAN8720A,这颗芯片在工业模块和评估板里非常常见,我后来也对比过其他几颗:

PHY芯片接口RMII参考时钟特点
LAN8720ARMII支持外部50MHz或25MHz晶振封装小、便宜、资料多
DP83848MII/RMII25MHz晶振TI自家,工业级温度范围好
KSZ8081RMII支持外部50MHz或25MHz晶振功耗低,和LAN8720A接近
TJA1100车载百兆针对车载,不太适合通用开发

LAN8720A有个很实用的特性:它有两种时钟模式。常见做法是给PHY提供一颗25MHz晶振,PHY内部锁相环倍频到50MHz,然后通过CLKOUT引脚把50MHz时钟送出来,给MCU做REF_CLK。这样做的好处是板上只需要一颗普通晶振,成本低。另一种做法是外部直接输入50MHz差分或单端时钟给PHY,MCU侧自己负责生成参考时钟。我这次设计采用的是第一种,YXDSP开发板上PHY主控25MHz晶体,CLKOUT接F28388D的EMAC REF_CLK引脚,整体非常干净。

选PHY还要注意一个细节:PHY地址。LAN8720A的SMI地址由PHYAD0引脚电平决定,常见配置为0或1。板上如果PHYAD0接地,那么MDIO读取地址就是0;如果接上拉,就是1。调试的时候第一件事就是读PHY ID寄存器,确认这个地址对不对,不然后面全是白忙。

2.3 设计原理图时容易翻车的地方:时钟、复位和地平面

硬件问题很多时候在原理图阶段就能避免。我这次真正动手之前,仔细看了YXDSP开发板的原理图,有三个地方容易翻车,值得单独拿出来说。

第一是复位引脚。PHY芯片的NRST不能一直悬空,必须有明确的上电时序。很多MCU的EMAC模块都要求PHY复位释放后至少等待一段时间,才能开始MDIO访问。LAN8720A的复位低电平有效,一般需要RC电路,或者由MCU GPIO控制复位。如果GPIO默认状态刚好是低电平,就会把PHY一直按在复位里,MAC怎么配置都白搭。

第二是时钟走线。REF_CLK是50MHz,虽然频率不算夸张,但它是所有收发时序的基准,走线尽量短,远离高频开关节点和电感。我见过有人把REF_CLK走在DC-DC电感旁边,结果连续丢包,重新布板才好。开发板虽然不需要你布线,但如果你之后自己出板子,这条一定要记住。

第三是地平面。网口RJ45座子外壳、变压器中心抽头、PHY芯片的地,处理不好会出现共模干扰,轻则ping值抖动,重则网络不通。正规设计一般是RJ45的金属外壳通过一个高压电容接到机壳地,信号地单点连接,PHY的模拟电源和数字电源用磁珠隔开。F28388D开发板已经有成熟设计,但如果你照着画板,这一块不要图省事。

3. 底层驱动动手:EMAC寄存器、描述符环和PHY自协商

3.1 最小初始化序列:时钟、复位、MAC地址和收发控制

软件层面的第一步,是把F28388D的EMAC模块从零初始化起来。我在TI官方例程基础上做了精简,去掉了调试打印,保留核心步骤。代码里的寄存器名可能因为SDK版本不同有差异,但逻辑大差不差。

void emac_init(void) { // 1. 使能EMAC外设时钟,并做外设复位 SYSCTL_peripheralReset(SYSCTL_PERIPH_EMAC); while (SYSCTL_peripheralResetStatus(SYSCTL_PERIPH_EMAC) != SYSCTL_STATUS_COMPLETE); SYSCTL_peripheralEnable(SYSCTL_PERIPH_EMAC); // 2. 关闭MAC发送和接收,清掉原有状态 EMACRegs.EMAC_CR.bit.TX_EN = 0; EMACRegs.EMAC_CR.bit.RX_EN = 0; // 3. 设置MAC地址,不能全零,否则会产生接收过滤问题 EMACRegs.EMAC_MAC_INDEX = 0; EMACRegs.EMAC_ADDR_LOW = 0x11223344; EMACRegs.EMAC_ADDR_HIGH = 0x00005566; // 4. 配置RMII模式 EMACRegs.EMAC_CR.bit.MII = 0; // 0表示RMII,1表示MII // 5. 描述符基地址将在下一步配置 // 6. 使能发送和接收 EMACRegs.EMAC_CR.bit.TX_EN = 1; EMACRegs.EMAC_CR.bit.RX_EN = 1; }

这段代码看起来简单,但里面藏着一个项目里很常见的坑:MAC地址不能全零。如果你的开发板没有预设MAC地址,程序里又刚好是全零初始化,接收方向会出现很诡异的现象——PHY明明link up,但收不到任何数据包,或者收到后被过滤掉。我当时排查了很久,最后把MAC地址改成板卡贴纸上的地址才正常。

还有一个容易被忽略的点:EMAC模块在使能收发之前,一定要先把DMA复位和描述符准备好。顺序错了,寄存器配置可能被后续的软复位冲掉,表现就是“我明明写了寄存器,怎么读出来还是0”。

3.2 发送/接收描述符环的设计

MAC把网络数据交给DMA搬运,而DMA本身不知道数据在哪,它依赖一个叫“描述符环”的数据结构。每个描述符包含控制字段、数据缓冲区指针、数据长度和状态标志。发送和接收各有一个环,我的做法是在内存里定义两个结构体数组。

#define EMAC_RX_BUF_NUM 4 #define EMAC_TX_BUF_NUM 4 typedef struct { uint32_t hwNext; uint32_t hwBufferPointer; uint16_t bufferLength; uint16_t flags; } emac_desc_t;

接收描述符在初始化时就要把缓冲区地址填进去,然后把OWNER位置成DMA所有。这样DMA收到数据后会直接写到缓冲区,再更新状态位。发送描述符则是软件先把数据填充到缓冲区,然后把描述符交给DMA,等DMA发送完成后检查状态位。

描述符数量不是越多越好。接收缓冲区我用了4个,正常100Mbps流量下足够;发送缓冲区也用4个。如果你要做大量UDP包突发,可以调大一些,但每个缓冲区都会吃掉RAM,嵌入式设备内存本来就紧张,要合理规划。

描述符本身必须满足对齐要求。F28388D的EMAC模块要求描述符按4字节或更高的边界对齐,缓冲区最好像我这样用__attribute__((aligned(32)))强制对齐。如果不做对齐,DMA访问描述符时会出错,表现是发送一直停在链头,不前进。

3.3 SMI/MDIO读写与PHY自协商

PHY芯片内部寄存器需要通过SMI接口访问。SMI有两根线:MDC时钟和MDIO数据。F28388D的EMAC模块里提供了MDIO控制器,你只需要配置分频系数,使MDC频率满足PHY芯片要求。LAN8720A的MDIO允许最高2.5MHz,如果你的系统时钟是100MHz,分频系数就要大于40。

uint16_t phy_read(uint8_t phy_addr, uint8_t reg) { // 配置MDC分频 EMACRegs.EMAC_MDIO_CONTROL.bit.CLK_DIV = 50; // 100MHz / 50 = 2MHz // 发起读命令 EMACRegs.EMAC_MDIO_ACCESS.bit.PHYAD = phy_addr; EMACRegs.EMAC_MDIO_ACCESS.bit.REGADR = reg; EMACRegs.EMAC_MDIO_ACCESS.bit.CTL = 0; // 0表示读操作 EMACRegs.EMAC_MDIO_ACCESS.bit.WRITE = 1; // MRDY 置位启动事务 EMACRegs.EMAC_MDIO_ACCESS.bit.MRDY = 1; // 等待忙状态结束 while (EMACRegs.EMAC_MDIO_ACCESS.bit.BUSY) ; return EMACRegs.EMAC_MDIO_DATA.bit.DATA; }

PHY自协商是Link Up的关键。上电后PHY会自动和交换机或PC网卡协商速度和工作模式,我们不需要太多干预,但软件需要去读状态寄存器,确认协商结果。LAN8720A的基本状态寄存器在Reg 1,bit1表示链接状态;如果为0,说明物理层没连上。

我在这个阶段遇到过一个问题:MDIO读回来的数据全是0xFFFF。当时第一反应是PHY地址错了,后来用万用表去量,才发现是PHY的复位引脚还拉着低电平,PHY根本没启动。所以调试MDIO之前,先确认PHY芯片电源、时钟和复位三件事,能省掉大量时间。

4. LWIP协议栈移植:从lwipopts到netif接口

4.1 版本和内存策略的选择

底层驱动跑通之后,就要把LWIP协议栈接进来了。我用的是LWIP 2.1.3版本,这个版本在嵌入式社区里比较成熟,坑少。相比老版本,2.1.x对内存池管理、TCP性能有了不少改进,而且TI官方例程大多也基于这个版本,跟F28388D的匹配度更高。

LWIP可以配置成无操作系统模式,也可以配置成带操作系统模式。F28388D的Cortex-M7内核很适合跑FreeRTOS,所以我选择了NO_SYS=0。这样LWIP自己会创建tcpip_thread,通过邮箱和信号量跟硬件驱动交互。

内存策略是移植中最容易出问题的环节。LWIP有两种内存分配方式:内存堆(mem_malloc)和内存池(memp)。我的建议是接收路径尽量使用PBUF_POOL,也就是预先分配好的固定大小缓冲区,这样在中断上下文里分配pbuf也不会产生不可控的等待。

下面是我使用的关键配置,经过长时间验证,没有出现内存耗尽问题:

#define MEM_SIZE (32 * 1024) // 内存堆大小 #define PBUF_POOL_SIZE 24 // pbuf池数量 #define PBUF_POOL_BUFSIZE 1520 // 单个缓冲区大小,能容纳一个完整以太网帧 #define TCP_MSS 1460 #define TCP_WND (4 * TCP_MSS) // 收窗口 #define TCP_SND_BUF (8 * TCP_MSS) // 发送缓冲 #define LWIP_DHCP 0 // 固定IP,暂时不跑DHCP #define LWIP_STATS 0 // 关闭统计,减少CPU开销

这里有件事必须提醒:PBUF_POOL_BUFSIZE不能小于1518。以太网最大帧是1518字节,加上VLAN标签会到1522。我看到的很多问题,都是因为缓冲区设置成了1500,结果收到较大帧时被截断,TCP校验一直过不去。宁可多给一点,不要刚好卡在临界值上。

4.2 sys_arch适配:信号量、互斥锁和时间基准

带操作系统的LWIP,需要实现一个sys_arch层。如果你用过FreeRTOS,你会发现LWIP本来就有对应的port,只是不同平台封装方式不太一样。F28388D上的实现思路是:信号量用FreeRTOS的SemaphoreHandle_t,互斥锁用MutexHandle_t,线程创建用xTaskCreate

信号量是LWIP和硬件驱动之间最核心的联系。接收中断里拿到网络数据,并不直接调用协议栈处理,而是通过tcpip_input把pbuf发给tcpip_thread。如果tcpip_input内部阻塞等待信号量,那中断上下文必须使用xSemaphoreGiveFromISR而不是加锁版本,否则会触发FreeRTOS的断言。

时间基准也别忘了。LWIP的TCP超时重传、ARP表老化都依赖系统时间。在F28388D上,我直接用Cortex-M7的SysTick来提供毫秒级时间戳,然后在sys_arch里实现:

u32_t sys_now(void) { return (u32_t)tick_ms; }

这个函数看起来简单,但在早期移植时我犯过一个错误:没有开SysTick中断,导致sys_now永远返回0,TCP连接迟迟建立不起来。如果你发现TCP三次握手一直卡在SYN_SENT,先确认sys_now是否在持续增长。

4.3 netif驱动的三个核心函数:init、output、input

LWIP通过struct netif来抽象网卡设备,我们需要注册一个网络接口,并提供初始化、发送和接收三个核心能力。

low_level_init主要工作是初始化EMAC硬件、填充netif的MAC地址、设置MTU为1500。MTU如果设得过大,超过底层缓冲区,会导致大包无法发送;设得过小,浪费协议栈能力。1500是标准值,配合PBUF_POOL_BUFSIZE=1520非常合适。

low_level_output负责把LWIP给出来的pbuf链表转成DMA能识别的连续数据。pbuf本质上可能是一个链表结构,每个节点只有一段数据,而DMA描述符往往只指向一个连续的地址。我采用的办法是:如果pbuf的整个数据量能拼成一个连续缓冲区,就直接填写描述符;否则先复制到一个静态发送缓冲区,再做DMA发送。

static err_t low_level_output(struct netif *netif, struct pbuf *p) { uint8_t *buf = &tx_buffer[0]; int len = 0; for (struct pbuf *q = p; q != NULL; q = q->next) { memcpy(buf + len, q->payload, q->len); len += q->len; } // 刷新数据缓存,保证DMA能读到最新数据 SCB_CleanDCache_by_Addr((uint32_t *)buf, len); // 绑定发送描述符,触发DMA发送 return emac_tx_start(buf, len); }

low_level_input则是在接收中断里调用,从接收描述符里拿到数据,用PBUF_RAM类型创建pbuf,拷贝数据后交给netif->input。这里有个关键点:在中断里尽量不要做耗时长的memcpy。幸好一个以太网帧最大1518字节,拷一次也就几个微秒,在可接受范围内。如果你想极致优化,可以直接让DMA缓冲区作为pbuf的payload,避免拷贝,但实现复杂度会高很多,我这次先选择了稳妥方案。

5. 联调记录:从ping不通到稳定传输

5.1 第一次ping和Wireshark抓包

所有代码写完、编译下载后,我直接在PC上用ping 192.168.1.100测试。第一次ping的结果和大家猜的一样,请求超时。我当时没有着急改代码,而是接上Wireshark抓包,仔细看的过程很关键。

Wireshark抓包会显示PC发出的ARP请求。如果PC的ARP请求能看到,但收不到开发板应答,那说明问题出在开发板的MAC接收路径或者ARP协议栈处理上。如果连开发板的网口link都没有起来,Wireshark上会看到PC网卡自己报“网络不可达”。那次我抓包后发现,ARP请求确实是发出去了,但迟迟没有回复,说明LMAC收中断已经跑了,但ARP包没被正确处理。

排查一圈后发现问题在MAC地址:我初始化的MAC地址跟板卡上实际配置的不一致,导致ARP缓存里记录的是另一个地址。把MAC地址修正后,ARP正常应答,ping也通了。

ping通那一刻,整个串口调试终端跳出“Reply from 192.168.1.100: bytes=32 time=1ms TTL=64”时,确实很有成就感。但对一个真实项目来说,ping通只能说明链路层通了,不能说明TCP/IP可靠。

5.2 CM7缓存一致性引发的隐蔽故障

有个问题,是ping通了之后才暴露出来的。刚开始,ping包偶尔会丢一两个,丢包率大概1%左右,表面上是偶发,但我觉得不对劲。TCP传大文件的时候,速度明显偏低,而且偶尔出现校验失败重传。

最终锁定是Cortex-M7的D-Cache和DMA的一致性冲突。CM7内核自带D-Cache,CPU访问内存时会先经过缓存,而DMA是直接从物理内存读写数据,两颗“大脑”对同一块内存的认知不一致,就会产生数据错乱。

接收方向:EMAC DMA把网线上收到的数据写进缓冲区,但CPU通过D-Cache去读时,读到的却是缓存里旧的脏数据,导致以太网帧内容错误;发送方向:CPU把报文写入发送缓冲区,DMA去读时又可能读到缓存中还没写回内存的数据,导致发出去的包是垃圾数据。

解决办法是发送之前做Cache Clean,接收之后做Cache Invalidate。我在代码里已经加了SCB_CleanDCache_by_Addr,后来又补上了接收路径:

// 接收完成中断里 SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buffer, len); pbuf_alloc(/* 分配PBUF_RAM */); memcpy(p->payload, rx_buffer, len); netif->input(p, netif);

加了Invalidate之后,丢包问题消失,TCP传输速率也明显稳定。这里有个小经验:如果F28388D你用的是Cortex-M7,并且开了D-Cache,最好把描述符数组和接收缓冲区都放进一个不被缓存覆盖的内存区域,或者在初始化时直接把D-Cache关掉。关掉D-Cache最简单,但牺牲性能;保留Cache加上手动Clean/Invalidate,性能更好,代价是代码逻辑要仔细。

5.3 长时间传输和TCP吞吐率观察

ping稳定后,我开始做长时间传输测试。测试方法是开发板作为TCP Server,PC作为客户端连接,开发板不断发送1KB的递增计数包,PC统计丢包和乱序。我让这个测试连续跑了两天,48小时里没有出现一次重连,计数包序号连续,说明TCP可靠性没有问题。

吞吐率方面,用TCP传输一个大文件,实际速率大概在3MB/s左右。这个数值对于100Mbps以太网来说不算高,但对于一颗以实时控制为主要任务的MCU来说,已经很够用。如果你想继续优化,可以从几个方向入手:增大TCP_SND_BUF、调整PBUF_POOL_SIZE、把接收中断改成轮询或NAPI模式,但通常会牺牲一些CPU时间,需要权衡。

还有一个细节:开发板作为TCP Server时,我设置的是静态IP,没有跑DHCP。工业环境里推荐固定IP,因为你要让整个产线设备在同一个网段,让上位机能够精确找到它。用DHCP虽然方便,但地址一旦变化,上位机软件就要做地址发现,工程复杂度上升很多。

6. 这次移植复盘:几个值得拿出来反复说的坑

6.1 PHY地址和SMI时钟是排查“link不起来”的第一站

如果网络不通,我现在的第一反应顺序是:先看PHY有没有起来,再看MDIO能不能正确读到PHY寄存器,最后才去查MAC配置。这个顺序是我踩过坑以后总结出来的。

PHY起没起来,最简单的方法就是读PHY寄存器。LAN8720A的PHY ID寄存器应该能读到标准值,如果读回来0xFFFF或者0x0000,先怀疑硬件:电源、时钟、复位。如果硬件没问题,再查SMI分频。有些开发板的系统时钟可能不是你以为的频率,导致MDIO超过2.5MHz上限,SMI通信不稳定,读出来的数据时对时错。

YXDSP这块板子默认PHY地址在原理图上能看到,但不同批次可能存在跳线差异。你代码里设置的phy_addr如果和硬件跳线不一致,MDIO永远读不到正确数据。所以我在驱动里做了一个小工具:初始化时从0到31扫描PHY地址,把所有能读到合法PHY ID的地址打印出来,这样就不用手动猜了。

6.2 中断上下文里的内存分配要非常克制

LWIP的接收路径绕不开中断,凡是中断里涉及的操作,都必须保持短小和行为可预测。我在移植时一开始直接在接收中断里调用了pbuf_alloc,用的是PBUF_POOL类型,因为该类型是从预分配池中取,速度很快,没问题。但如果用PBUF_RAM类型,内部会走mem_malloc,如果内存碎片化严重,分配时间会变得不稳定,可能直接影响中断响应。

另一个原则是中断里不要做TCP层处理。tcpip_thread就是专门用来处理协议栈的,你要做的只是把数据包交给它,然后立刻退出中断。如果自作聪明在中断里调用TCP处理函数,不仅容易崩,还会把整个系统的实时性拉低。我用FreeRTOS的时候,特别确认了tcpip_input可以从中断安全上下文调用,因为它内部使用的是sys_mbox_trypost这类不阻塞的接口。

6.3 如果重来一次,我会把代码拆成怎样的结构

这次移植从EMAC底层到LWIP接入,代码量不小。如果未来我还要在另一个项目里复用这套逻辑,我不会再把所有逻辑堆在一个大C文件里。我会分成这样几层:

  • emac_drv.c/h:负责F28388D EMAC寄存器初始化、描述符环管理、MAC地址设置。
  • phy_drv.c/h:负责PHY芯片SMI读写、复位、自协商状态机,隔离具体PHY型号。
  • netif_emac.c/h:作为LWIP和emac_drv之间的适配层,实现low_level_initlow_level_outputlow_level_input,并且处理Cache Clean/Invalidate。
  • lwipopts.hsys_arch.c:单独管理,不跟业务代码混在一起,这样升级LWIP版本时直接替换。

这种分层的好处是,PHY芯片换掉或者EMAC寄存器细节变化时,不需要把LWIP层全部重写,改一个文件就够了。我在这次项目后期,就曾经因为从LAN8720A换到另一颗PHY,只改了phy_drv.c里的寄存器操作,其他部分几乎没有动,验证了这种拆分的价值。

另外还有一个小建议:在工程里加入一个简易的MAC地址管理机制。开发板每块都有一个固定MAC地址,但批量生产时最好从配置区读取,而不是硬编码。否则两台设备同时接入同一个局域网,MAC冲突会让网络时好时坏,排查起来非常痛苦。这次我在代码里做了一个默认地址加一个可配置地址的结构,真正量产时可以通过Bootloader改写,算是提前做了一点准备。

整个项目做完,我再回头去看F28388D的以太网部分,反而觉得它没那么神秘。EMAC硬件摆在手册里,LWIP源码就在那里,真正难的是把硬件中断、DMA缓存、协议栈线程、PHY状态机这些分散的模块串成一个整体,让它们协同工作。这块开发板给我最大的帮助,是让我能够在真实硬件上反复验证链路和协议栈的每一个阶段,而不是停留在纸面配置。如果接下来你要在自己的板子上做网络功能,我建议也按照这个顺序:先把PHY调通,再跑裸机收发,最后接LWIP,一步一步来,反而最快。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询