
简介这是面向嵌入式开发者的DSP28335网络接口驱动资源包解决在TI C2000系列DSP平台上驱动RTL8019以太网控制器、实现uIP协议栈通信的问题。适用于工业自动化、电机控制等需要轻量级网络通信的场景要求读者具备DSP外设编程、嵌入式网络及C语言基础。压缩包共33个文件包含21个头文件和12个C源文件覆盖uIP协议栈核心、ARP处理、RTL8019网卡驱动及相关配置代码结构清晰便于裁剪移植。已有247人学习下载开发者可从中获得完整的驱动框架和协议栈集成思路缩短网络通信功能开发周期。1. 为什么DSP28335没有片内MACuIP却成了网口驱动的默认起点先给结论TMS320F28335在以太网这件事上片内的SRAM、XINTF和控制逻辑都在唯独没有Ethernet MAC和PHY。所以“28335网口”的真正含义是外挂以太网控制器再用软件把协议栈跑起来uIP在这个场景里几乎成了默认选择因为它不需要操作系统一个裸机循环就能承载ARP、ICMP和TCP。DSP28335网口驱动程序要解决的不只是读寄存器、发数据而是如何把外设时序、中断/轮询方式和uIP的收发接口安全地捏在一起。这套思路同样适用于SPI接口的ENC28J60、并行接口的DM9000A。读这篇文章的人大概率正在CCS里面对ping不通、TCP反复重传或跑一段时间死机的问题下面直接进入可复现的做法。2. uIP在DSP28335上的移植边界字节序、内存段与uip-conf配置第一次把uIP搬上DSP28335时最容易做的错事是直接从网上拷一份uip-conf.h改两个IP地址就开始跑。等ping不通再去查往往发现不是寄存器写错而是协议栈配置和28335的内存模型没有对齐。先把uIP与DSP28335结合时的配置边界说清楚后面写网口驱动时才不会两头找原因。2.1 为什么选uIP而不是lwIPDSP28335主频150MHz内部RAM约34KB但实际可用的连续大块是L0-L7这些RAM区单块大小并不大。lwIP虽然功能全但光内存池和pbuf就足以把L0-L7吃光还要配合RTOS才能发挥价值。uIP是事件驱动协议栈只用两个全局缓冲区uip_buf放收发数据旁边再用uip_len记录长度这种模型和28335裸机环境非常匹配。常见做法是使用uIP 1.0或1.1的源码包把uip.c、uip_arp.c、uip_neighbor.c、uip_timer.c加入CCS工程。网络层接口就一个uip_input()周期性处理就一个uip_periodic()。它没有线程概念因此不需要额外移植锁这也让DSP28335网口驱动程序可以保持很薄的一层。2.2 uip-conf.h里最值得改的5个宏先看一个可复用的最小配置#define UIP_CONF_MAX_CONNECTIONS 8 #define UIP_CONF_MAX_LISTENPORTS 8 #define UIP_CONF_UDP 0 #define UIP_CONF_TCP 1 #define UIP_CONF_BYTE_ORDER UIP_LITTLE_ENDIAN #define UIP_CONF_LOGGING 0 #define UIP_CONF_BUFFER_SIZE 1514 #define UIP_CONF_TCP_MSS 1460 #define UIP_CONF_RECEIVE_WINDOW 2920这里不能只改IP地址就完事。UIP_CONF_BYTE_ORDER必须显式定义为UIP_LITTLE_ENDIAN因为TMS320F28335是32位内核内存按小端存放。如果不做这一步uIP在计算IP头校验和时会反序ARP包看起来正常但TCP三次握手永远无法完成数据发出去全是坏包。UIP_CONF_BUFFER_SIZE用于定义uip_buf的字节数1514对应标准以太网MTU 1500加14字节以太网头如果你的应用只需要控制命令可以缩小到600字节但TCP MSS也要同步降低否则对端发来的大包会被uIP直接丢弃。UIP_CONF_RECEIVE_WINDOW建议设成2 * UIP_CONF_TCP_MSS这样一个发送窗口内能放下两个TCP段吞吐量不至于太难看。UIP_CONF_UDP保持0是因为uIP 1.0的UDP实现比较简陋实际项目中若需UDP建议用Raw API自写或换别的方案。2.3 把uip_buf放到哪一块RAMDSP28335内部RAM分布在M0/M1、L0-L7等区域。uip_buf[1514]占用1.5KB如果整个协议栈加驱动代码占的内存不多放在L4_L5空间比较稳妥。常见做法是在CCS工程里用#pragma DATA_SECTION把缓冲放到自己划分的段#pragma DATA_SECTION(uip_buf, .uip_buffer) u8_t uip_buf[UIP_CONF_BUFFER_SIZE];同时在cmd文件里增加.uip_buffer : L4_RAM, PAGE 1这样做有两个好处一是明确大缓冲不与任务栈混在一起二是在CCS的Memory Browser里能看到整个uip_buf调试DSP28335网口驱动时可以直接观察数据包内容。uip_buf尽量不要放到外部ZONE6的扩展RAM上虽然28335的XINTF访问也不慢但每次收包都要跨总线会让驱动在高速网络下出现不可控时延。提示给uIP分配内存时把uip_buf单独放其它全局变量放在原.ebss。uIP启动时依赖uip_buf为零初始化段分开后可以避免其它模块越界写坏协议栈缓冲。2.4 与CCS优化等级配合的注意事项uIP代码面向8位MCU的写法较多CCS在-O3下有时会把uip_process循环展开得很夸张导致RAM栈占用暴涨。建议先把编译优化设为-O2确认驱动能跑通后再尝试更高优化。若程序在uip_input()调用后进入异常中断优先检查栈指针是否越界到uip_buf而不是怀疑DM9000。到这里协议栈的边界已经固定下来。下面进入DSP28335网口驱动最核心的硬件访问层也就是如何通过XINTF总线让28335和DM9000彼此听得懂。3. 28335网口驱动的硬件层XINTF与DM9000的读写引脚映射绝大多数DSP28335网口方案会选择DM9000系列作为MAC/PHY因为它的引脚少寄存器线性且非常容易在XINTF上模拟。下面用DM9000A作为对象把28335的XINTF当作普通总线写出一套可复用的底层驱动如果你用的是ENC28J60驱动实现思路也是一样的只是把XINTF换成SPI。3.1 地址线如何映射成DM9000的CMD引脚DM9000的数据线直接接DSP28335 XINTF的XD[15:0]读写控制线接XRD/XWE关键是DM9000的CMD引脚低电平时访问索引寄存器高电平时访问数据寄存器。常见做法是把28335的XA1接DM9000的CMD引脚这样当XA10时访问索引寄存器XA11时访问数据寄存器。如果DM9000挂在XINTF Zone2片选接XZCS2Zone2的地址基址是0x080000那么定义如下#define DM9000_INDEX_ADDR 0x080000u #define DM9000_DATA_ADDR 0x080002u #define DM9000_INDEX_REG (*(volatile Uint16 *)(DM9000_INDEX_ADDR)) #define DM9000_DATA_REG (*(volatile Uint16 *)(DM9000_DATA_ADDR))这里不要把数据地址写成0x080001那通常是8位总线接法。当XA1接CMD时0x080000对应CMD00x080002对应CMD1。volatile不能省否则CCS的编译器会认为两次写同一地址的程序是冗余把访问顺序优化掉。3.2 总线和端口的时序配置DM9000的读周期典型值要求最小200ns左右而28335的XINTF时钟在150MHz主频下一般设为SYSCLKOUT的整数分频。常见配置是把Zone2的建立时间设为1个XINTF时钟选通时间设为6个XINTF时钟保持时间设为1个XINTF时钟再打开16位数据总线模式。初始化代码大致如下void InitEmifForDm9000(void) { InitXintf(); XintfRegs.XTIMING2.bit.XWRTRAIL 1; XintfRegs.XTIMING2.bit.XWRACTIVE 6; XintfRegs.XTIMING2.bit.XWRLEAD 1; XintfRegs.XTIMING2.bit.XRDTRAIL 1; XintfRegs.XTIMING2.bit.XRDACTIVE 6; XintfRegs.XTIMING2.bit.XRDLEAD 1; XintfRegs.XTIMING2.bit.XSIZE 1; XintfRegs.XTIMING2.bit.X2TIME 1; }这些参数不是拍脑袋来的。XSIZE1表示外部总线为16位对应DM9000的16位数据模式X2TIME1表示定时器基于XTIMCLK/2会让每个等待状态的实际长度更长。如果DM9000偶尔出现读回0xFF或写不进寄存器先把XRDACTIVE和XWRACTIVE同时加2而不是调PHY配置。3.3 寄存器读写原语在写完XINTF后网口驱动的第一步是证明能读写DM9000寄存器。下面是最小的读写函数void dm9000_write_reg(Uint8 reg, Uint8 value) { DM9000_INDEX_REG reg; DM9000_DATA_REG value; } Uint8 dm9000_read_reg(Uint8 reg) { DM9000_INDEX_REG reg; return (Uint8)(DM9000_DATA_REG 0xFF); }逻辑说明每次操作先把寄存器编号写到INDEX地址然后对DATA地址读或写。DM9000的寄存器编号只有8位而数据总线是16位这里用Uint8截断即可。参数说明reg的取值范围从0x00到0xFF但实际用到的网管寄存器集中在0x00-0x1Fvalue写入时只保留低8位高8位填充0。读回时 0xFF是为了把16位数据寄存器里的高字节噪声挤掉这一步在后续读状态位时很有用。这里很容易犯的一个错误是读标志位时把INDEX和DATA地址弄反。DM9000的读写时序都是先写INDEX后读写DATA如果反过来读出的寄存器编号会成为前一次DATA的值表现为所有寄存器读出的内容都相同。3.4 初始化序列的五个要点DM9000上电后需要先软件复位再读PHY。可以这样写#define DM9000_NCR 0x00 #define DM9000_NSR 0x01 #define DM9000_RSR 0x02 #define DM9000_TSR 0x03 #define DM9000_TCR 0x04 #define DM9000_RCR 0x05 void dm9000_init(void) { Uint16 id; dm9000_write_reg(DM9000_NCR, 0x01); /* 软件复位 */ DELAY_US(20); dm9000_write_reg(DM9000_NCR, 0x00); /* 退出复位 */ dm9000_write_reg(DM9000_RSR, 0x01); /* 清除接收状态 */ dm9000_write_reg(DM9000_TSR, 0x01); /* 清除发送状态 */ id dm9000_read_reg(0x28) | (dm9000_read_reg(0x29) 8); if (id ! 0x900A) { /* 返回错误码让上层重试 */ } dm9000_write_reg(DM9000_RCR, 0x39); /* 接收单播/多播/广播包 */ dm9000_write_reg(DM9000_TCR, 0x00); /* 清发送请求位 */ }代码里的0x28/0x29是DM9000的PID寄存器正常读取是0x900A。失败时要回头查XINTF时序而不是继续往下初始化。RCR0x39表示使能接收开启广播和多播关闭休眠TCR0x00保证没有残留的发送请求。DELAY_US(20)在150MHz主频下约为20微秒足够DM9000完成内部复位。如果这里都读不对说明XINTF的读写时序或地址映射有问题不要再往uIP方向排查。4. 收发包路径uip_input、uip_periodic和DM9000的衔接方式有了底层寄存器的读写uIP和DSP28335网口驱动程序的接缝就开始聚焦了。uIP不是把网卡数据直接交给应用而是要求驱动在收到完整一帧时把数据放进uip_buf并调用入口函数每次调用后再根据uip_len决定要不要发送回应。把收包、发包、周期处理三件事写清楚。4.1 收包路径先把网卡数据搬到uip_bufDM9000收到包后会在内部SRAM里把包缓存好并置位接收状态寄存器。驱动层需要做的第一步是把DM9000内部的包数据读出来。Uint16 dm9000_read_packet(Uint8 *dst, Uint16 max_len) { Uint16 len; Uint16 i; len (Uint16)(dm9000_read_reg(0x0A) | (dm9000_read_reg(0x0B) 8)); if (len 0 || len max_len) { return 0; } DM9000_INDEX_REG 0x0C; /* MRCMDX, 读取当前包状态 */ if ((DM9000_DATA_REG 0x01) 0) { return 0; } DM9000_INDEX_REG 0x0C; for (i 0; i len; i) { dst[i] (Uint8)(DM9000_DATA_REG 0xFF); } dm9000_write_reg(DM9000_RSR, 0x01); /* 清除接收状态 */ return len; }这里从0x0A和0x0B读取16位包长度0x0A是低字节0x0B是高字节。MRCMDX地址是读取当前包状态连续读数据寄存器会把SRAM中的数据逐个取出。16位数据总线下读数据寄存器时低8位是有效字节因此全部掩成 0xFF。max_len参数应该传UIP_CONF_BUFFER_SIZE防止网卡上的超长帧把uip_buf写越界。4.2 把收包流程接到uip_input()在裸机主循环里轮询一个标志就可以实现DSP28335网口驱动的主处理。常见做法是用GPIO或定时器中断置一个net_packet_pending标志主函数检测到后执行如下代码extern u8_t *uip_buf; extern u16_t uip_len; void net_poll(void) { if (dm9000_is_packet_ready()) { uip_len dm9000_read_packet(uip_buf, UIP_CONF_BUFFER_SIZE); if (uip_len 0) { uip_input(); if (uip_len 0) { dm9000_send_packet(uip_buf, uip_len); } } } }这里有几个容易错的地方。先给uip_len赋接收长度再调uip_input()uIP内部会根据协议类型处理ARP和IP包uip_input()执行完后如果uip_len不为0说明协议栈需要驱动发送数据比如TCP ACK或ARP应答。此时驱动要把uip_buf中的uip_len字节通过DM9000发出不能用之前读包的长度。如果在这里直接判断固定的接收长度会漏掉大部分TCP互动。4.3 DM9000的发包函数怎么写DM9000的发包要先把要发送的数据写入SRAM然后设置TX长度寄存器最后把TCR的TXREQ位置1。转换成代码如下void dm9000_send_packet(Uint8 *src, Uint16 len) { Uint16 i; Uint32 timeout; dm9000_write_reg(0x04, 0x00); /* MWCMD, 写SRAM命令 */ for (i 0; i len; i) { DM9000_DATA_REG src[i]; } dm9000_write_reg(0xFC, len 0xFF); dm9000_write_reg(0xFD, (len 8) 0xFF); dm9000_write_reg(DM9000_TCR, 0x01); /* 置TXREQ触发发送 */ timeout 0; while ((dm9000_read_reg(DM9000_NSR) 0x01) 0) { if (timeout 10000) { dm9000_write_reg(DM9000_NSR, 0x01); break; } } }这里0x04是MWCMD内存写命令0xFC和0xFD分别是发送长度低字节和高字节0x01是DM9000_NSR位0是TX1END。while等待带超时保护超过一万次后清状态位跳出防止网口异常时驱动卡死。写MWCMD这个顺序不能颠倒否则会写入到错误的数据偏移。4.4 周期调用uip_periodic()不能漏TCP定时器、ARP老化都藏在uip_periodic()里。如果DSP28335网口驱动只处理收包而不周期调用TCP连接建立后很快会因超时被对端断开。常见做法是在定时器中断或主循环里每250ms调用一次static Uint32 last_tick 0; if (CpuTimer0Count - last_tick 250) { int i; for (i 0; i UIP_CONF_MAX_CONNECTIONS; i) { uip_periodic(i); if (uip_len 0) { dm9000_send_packet(uip_buf, uip_len); } } last_tick CpuTimer0Count; }uip_periodic(i)调用后同样要检查uip_len因为周期性超时可能触发TCP重传或ACK。这里的250ms只是示例如果应用对握手时间敏感可以改成100ms但频率越高28335的CPU占用越高。uIP的时钟基准实际是200Hz如果任务循环间隔超过5ms就要用uip_timer维护一个软定时器而不是简单依赖一次主循环。到这里一组收发接口已经能把底层驱动和uIP串起来了。接下来需要回答的是出了问题怎么看、怎么定位。5. 网口驱动的调试阶梯SCI统计、抓包对比与死机排错uIP程序写完后最常听到的问题是“ping不通”“偶尔能连上”“跑一天死了”。调试DSP28335网口驱动和调试普通串口不同不能靠printf打全程因为printf本身就会干扰时序。比较好的做法是分三层底层寄存器、协议栈连接、运行稳定性。下面按层给可操作的验证方法。5.1 用SCI加网口调试助手打出一个可见的状态窗口在28335主循环中加入一组静态计数每500ms从SCI打印一次。把DSP的SCIA和USB转串口连接PC端打开串口助手或网口调试助手就能看到驱动状态。void debug_report(void) { static Uint32 timebase; if (CpuTimer0Count - timebase 500) { return; } timebase CpuTimer0Count; printf(rx%u tx%u drop%u arp%u\n, net_stat.received, net_stat.sent, net_stat.dropped, net_stat.arp_recv); }这里的net_stat是自己定义的结构体在收包、发包、发送失败和ARP收到分支里递增。注意net_stat.received统计的是通过dm9000_read_packet读到的包数不是uIP处理成功的包数。当received持续增长而arp不动时问题在协议栈配置或字节序当received不增长时问题在DM9000初始化或XINTF时序。5.2 常见故障对照表现象定位方向先执行的检查完全ping不通DM9000寄存器读写读0x28/0x29是否为0x900A若不是则检查XINTF时序ping通但TCP连不上uIP配置确认UIP_CONF_BYTE_ORDERUIP_LITTLE_ENDIANTCP能连但数据乱码数据线极性检查DM9000_D0-D15与XD0-XD15是否一一对应不能错位跑几分钟后断连定时器频率检查uip_periodic是否每隔1s以上才调用收包正常但发不出去TX完成状态检查dm9000_send_packet里等待NSR发送完成的循环是否会卡死第一次Ping通第二次不通ARP缓存确认uip_arp定时器在周期任务里执行ARP表项会老化这张表里的每一项都对应一个可查证的故障源。比如“ping通但TCP连不上”最常见原因不是DM9000而是uIP的UIP_CONF_BYTE_ORDER没有设为little-endian如果TCP三次握手能看到SYN但看不到ACK基本就是这个宏的问题。还有一种隐蔽现象是“ARP正常但ICMP echo不回复”这种往往是uIP的ICMP处理需要UIP_CONF_ICMP支持很多精简版uIP默认关闭ICMP。5.3 用Wireshark抓包验证关键时序PC端用Wireshark抓DSP28335网口驱动的包首先要确保PC和DSP在同一个网段或开启混合模式。在DSP上只配置一个静态IP不配网关也不开DHCP方便过滤。抓包时观察三样东西DSP是否正确回复ARP请求Ping包是否能收到ICMP Echo ReplyTCP建立连接时是否连续发出SYN或复位包。如果Wireshark里看不到DSP的ARP应答而DSP的SCI打印显示ARP包已收到那么问题几乎在驱动发送路径上。可以先用一个简单的以太网帧发送测试比如在驱动初始化后主动发一个ARP请求这比直接跑TCP更容易隔离故障。5.4 网口驱动死机的通用排错法DSP28335网口驱动一旦死机往往不是协议栈问题而是中断和主循环竞争。常见做法是把网口驱动设计为轮询模式但如果在中断里调用uip_input()必须确认uIP不支持重入。主循环正在执行uip_input时来了网络中断再进一次uip_input两块代码同时改uip_buf结果就是内存错乱。推荐的做法是中断里只置一个volatile标志主循环检测标志后做处理。如果还不放心可以在主循环里关中断执行DINT;保护uip_input调用段执行完再EINT;。代价是网络中断延迟增加但DSP28335网口驱动最重要的不是吞吐量而是稳定性。6. 两个提高调试效率的小技巧回环自测与GPIO时延测量先看一个不接网线也能验证底层的方法DM9000的RCR寄存器bit3可以打开内部回环绕过PHY直接把要发送的数据环回到接收路径。初始化时把RCR写成0x33此时不接网线驱动也能收到自己发出的包。开启回环后可以用一个简单的自收发测试函数验证XINTF读写、DM9000 SRAM读写和uIP收包接口是否正常适合在产线或没有交换机的调试现场快速判定板级链路。第二个技巧是用GPIO翻转来测量单包处理时延而不是依赖计数器。在uip_input()调用前把GPIO32拉低调用后拉高用示波器测低电平脉冲宽度。这个宽度代表协议栈处理一个包消耗的时间一般应在50到200微秒之间。如果脉冲宽度超过1ms说明uIP的CPU开销太大需要检查是否开了不必要的调试打印或日志缓存如果脉冲宽度忽大忽小则要看主循环里是否还有别的长阻塞。这个方法也能帮助确认uip_periodic调用周期是否稳定因为当定时器偏慢时重传集中在某一次调用中波形会突然出现一个宽脉冲。配合这套调试阶梯交付前的最后一次验证可以这样做闭合回环确认自收发自通断开回环接交换机用串口助手观察DSP是否能持续输出rx/tx计数再用Wireshark验证PC与DSP之间的ARP和TCP握手是否干净。最后在发布固件前把UIP_CONF_LOGGING关掉并把uip_buf所在的.uip_buffer段地址记录下来方便以后对照崩溃转储。本文还有配套的精品资源点击获取