简介:这份资源面向从事嵌入式网络驱动开发的工程师与学习者,聚焦DM9000A以太网控制器在VxWorks实时操作系统下的驱动实现,帮助解决硬件寄存器操作、数据收发与中断处理等核心问题。压缩包共2个文件,包含1个c源码与1个txt说明文档,整体约18KB,源码对应驱动主体逻辑,说明文档则用于交代接口与使用背景。已有140人学习下载,可作为入门VxWorks网卡驱动或移植参考的轻量素材。读者可从中了解DM9000A的初始化流程、发送与接收数据路径、中断服务例程设计思路,以及内核态驱动如何与硬件RAM缓冲配合降低CPU负担,进而对照自身平台完成适配与调试,提升网络通信模块的开发效率。
1. DM9000A 在 VxWorks 下的驱动移植:从网卡不识别到 ping 通的第一公里
手上有一块工控板,CPU 是 ARM9,网络口用的是 DM9000A,BSP 里默认的网卡驱动跑不起来,ifconfig看不到设备,串口一直打印dm9000: probe failed。这种场景在嵌入式网络开发里太常见了——DM9000A 这颗芯片便宜、供货稳、接口简单,很多国产工控板和电力终端都在用,但 VxWorks 的 BSP 往往只带一个通用模板,寄存器基址、中断号、位宽模式对不上,网卡就是死活不认。这篇笔记就围绕 DM9000A 在 VxWorks 下的驱动适配展开,把芯片初始化、寄存器配置、中断挂接、收发环搭建这条链路拆开讲清楚。适合正在做 BSP 移植、调试以太网口、或者接手了一块网口不通的板子的嵌入式工程师。读完你至少能判断:是硬件没焊好、时序不对,还是驱动层寄存器写错了。
2. DM9000A 的寄存器模型与 VxWorks 驱动挂接点
2.1 先搞清楚 DM9000A 的 INDEX/DATA 双端口访问机制
DM9000A 和很多以太网控制器不一样,它没有把寄存器直接铺在地址空间上,而是用两个物理地址做窗口:一个 INDEX 端口写寄存器编号,一个 DATA 端口读写该编号对应的数据。CPU 侧看到的就是两个地址,比如基址0x88000000对应 INDEX,0x88000004对应 DATA。每次访问某个寄存器,都要先写 INDEX,再读写 DATA,这个顺序不能反,中间也不能被中断打断,否则寄存器窗口就串了。
在 VxWorks 里,这个基址通常由 BSP 的sysPhysMemDesc[]或sysLib.c里的地址映射决定。你需要确认两件事:一是物理地址到虚拟地址的映射有没有做,二是映射后的虚拟地址是不是按 4 字节对齐。很多板子跑不起来,就是因为基址写成了0x88000000,但实际硬件译码出来是0x88000000起始的 8 字节窗口,DATA 偏移应该是+4,结果写成了+2,读出来的全是 0xFFFF。
/* dm9000a.h - 寄存器索引定义,只列关键几个 */ #define DM9000A_NCR 0x00 /* 网络控制寄存器 */ #define DM9000A_NSR 0x01 /* 网络状态寄存器 */ #define DM9000A_TCR 0x02 /* 发送控制寄存器 */ #define DM9000A_RCR 0x03 /* 接收控制寄存器 */ #define DM9000A_ISR 0x0E /* 中断状态寄存器 */ #define DM9000A_IMR 0x0F /* 中断屏蔽寄存器 */ #define DM9000A_RCR 0x03 #define DM9000A_PAR 0x10 /* 物理地址寄存器起始,共6字节 */ #define DM9000A_MAR 0x16 /* 组播地址寄存器起始,共8字节 */上面这段是寄存器索引的宏定义,实际写驱动时你至少要把 NCR、NSR、ISR、IMR、PAR 这几个用对。NCR 的 bit0 是 RXEN,bit1 是 TXEN,初始化时先关掉收发,配好其他寄存器再打开。NSR 的 bit6 是 LINK_STAT,用来判断网线插没插。ISR 是中断状态,读一次会自动清,但有些位需要写 1 清除,这个在数据手册里写得很细,别凭感觉写。
2.2 VxWorks 网络驱动框架里 DM9000A 该挂在哪一层
VxWorks 的网卡驱动通常走END(Enhanced Network Driver)框架,也就是muxDevLoad/muxDevStart那一套。你需要实现一个END_OBJ结构,填好start、stop、send、ioctl、muxBind这些回调。DM9000A 的驱动一般放在target/src/drv/end/下面,编译进 BSP 或者做成可加载模块。
关键挂接点有三个:第一是dm9000aLoad(),负责探测芯片、读 ID、分配END_OBJ;第二是dm9000aStart(),负责初始化寄存器、挂中断、启动收发;第三是dm9000aSend(),把上层协议栈下来的mBlk链拆成帧,写进 DM9000A 的发送 FIFO。中断服务程序dm9000aIsr()里读 ISR,判断是收还是发完成,然后调用netJobAdd把实际处理推到任务级,别在 ISR 里做太多事。
/* 探测芯片:读 VID 和 PID,DM9000A 固定为 0x0A46 / 0x9000 */ static UINT16 dm9000aReadReg(UINT32 base, UINT8 reg) { sysOutByte(base, reg); /* 先写 INDEX */ return sysInWord(base + 4); /* 再读 DATA,注意偏移 */ } static STATUS dm9000aProbe(UINT32 base) { UINT16 vid = dm9000aReadReg(base, 0x28); /* VID 低16位 */ UINT16 pid = dm9000aReadReg(base, 0x2A); /* PID 低16位 */ if (vid != 0x0A46 || pid != 0x9000) { logMsg("dm9000a: probe failed, vid=0x%04X pid=0x%04X\n", vid, pid, 2,3,4,5,6); return ERROR; } return OK; }这段探测代码里,sysOutByte和sysInWord是 VxWorks 的 IO 访问原语,具体用哪个取决于你的总线位宽。DM9000A 支持 8 位和 16 位模式,16 位模式下 DATA 端口按字访问,8 位模式下按字节访问。如果你读出来的 VID 是 0xFFFF 或者 0x0000,先查硬件复位和片选,再查 INDEX/DATA 偏移。VID 和 PID 对不上,后面所有寄存器操作都是白费。
3. 从复位到 ping 通:DM9000A 初始化与收发环搭建
3.1 上电复位、PHY 自协商与 MAC 地址写入
DM9000A 上电后需要一次软复位,写 NCR 的 bit0 为 1,然后等至少 10us 再清掉。复位完成后,先读 NSR 确认 PHY 状态,如果 LINK_STAT 没置起来,说明网线没插或者 PHY 自协商没完成。自协商一般不用手动干预,DM9000A 内部 PHY 会自动跑,但有些板子为了省成本用了外部 PHY,那就得通过 SMI 接口去配,这个后面避坑章节会讲。
MAC 地址写入 PAR 寄存器,从 0x10 到 0x15 共 6 个字节。注意 DM9000A 的 PAR 是字节序敏感的,先写低字节还是高字节取决于你的 CPU 大小端。ARM 一般小端,MAC 地址00:11:22:33:44:55写进去的顺序是 0x55、0x44、0x33、0x22、0x11、0x00。写反了能 ping 通自己,但和外部设备通不了,因为源 MAC 错了。
static void dm9000aInitMac(UINT32 base, UINT8 *mac) { int i; for (i = 0; i < 6; i++) { dm9000aWriteReg(base, DM9000A_PAR + i, mac[5 - i]); /* 小端反序 */ } /* 使能收发 */ dm9000aWriteReg(base, DM9000A_NCR, 0x03); /* RXEN | TXEN */ }这段初始化里,dm9000aWriteReg就是先写 INDEX 再写 DATA。MAC 地址通常从 BSP 的sysLib.c或者 flash 里读,别硬编码在驱动里,否则批量生产时每块板子 MAC 都一样,网络里会冲突。写完后读回来校验一遍,读不到就说明寄存器访问时序有问题。
3.2 中断挂接与 ISR 里的收发分流
DM9000A 的中断引脚接到 CPU 的哪个 GPIO 或外部中断线,这个在 BSP 的sysIntr.c里配。VxWorks 下用intConnect挂 ISR,中断号从 BSP 的hwint.h里拿。ISR 里第一件事读 ISR 寄存器,判断是 RX 中断还是 TX 中断。RX 中断置位说明有帧进来了,TX 中断置位说明上一帧发完了。
ISR 里不要直接读 FIFO,因为 DM9000A 的接收 FIFO 读操作需要连续进行,中间被打断会丢数据。正确做法是在 ISR 里记一个标志,然后netJobAdd把dm9000aRxJob推到网络任务里执行。dm9000aRxJob里读 MRCMDX 寄存器判断有没有新帧,有的话读帧头、读长度、读数据,最后封装成mBlk交给netJobAdd上层。
static void dm9000aIsr(void *arg) { UINT32 base = (UINT32)arg; UINT8 isr = dm9000aReadReg(base, DM9000A_ISR); if (isr & 0x01) { /* RX 中断 */ netJobAdd((FUNCPTR)dm9000aRxJob, base, 0,0,0,0,0); } if (isr & 0x02) { /* TX 中断 */ /* 释放已发送的 mBlk,唤醒等待的发送任务 */ dm9000aTxComplete(base); } /* 写 1 清除中断状态,具体位看数据手册 */ dm9000aWriteReg(base, DM9000A_ISR, isr); }ISR 里读 ISR 后要写回清除,但有些位是读清,有些是写清,DM9000A 的 ISR bit0 到 bit3 基本都是写 1 清除。如果你发现中断一直触发,先查是不是没清干净。另外中断触发方式要配成边沿触发还是电平触发,这个和硬件设计有关,配错了要么丢中断,要么中断风暴。
3.3 发送路径:mBlk 到 FIFO 的搬运与超时处理
VxWorks 协议栈下来的数据是mBlk链,发送时要先算总长度,然后写 TCR 的 bit0 为 1 启动发送,接着把数据按字或字节写进 MWCMD 寄存器。DM9000A 的发送 FIFO 是 2KB 左右,超过这个长度的帧要分片,但以太网帧最大 1518 字节,一般不会超。写完后等 TX 中断,或者轮询 NSR 的 bit2(TX 完成标志)。
发送超时是常见问题。如果 TX 中断一直不来,先查 TCR 有没有使能,再查网线是不是没插。DM9000A 在链路断开时会自动停止发送,但驱动层不一定知道,所以发送函数里最好加一个超时计数器,超时后返回 ERROR,让上层协议栈重传。
static STATUS dm9000aSend(END_OBJ *pEnd, M_BLK_ID pMblk) { UINT32 base = DM9000A_BASE; UINT16 len = pMblk->mBlkHdr.mLen; UINT8 *data = mtod(pMblk, UINT8 *); int i; /* 等上一帧发完,超时 100us */ for (i = 0; i < 100; i++) { if (dm9000aReadReg(base, DM9000A_NSR) & 0x04) break; sysUsDelay(1); } if (i == 100) return ERROR; /* 写长度和启动发送 */ dm9000aWriteReg(base, DM9000A_TXPLL, len & 0xFF); dm9000aWriteReg(base, DM9000A_TXPLH, (len >> 8) & 0xFF); dm9000aWriteReg(base, DM9000A_TCR, 0x01); /* 搬运数据到 FIFO */ for (i = 0; i < len; i++) { sysOutByte(base + 4, data[i]); /* 8位模式 */ } return OK; }这段发送代码里,TXPLL和TXPLH是发送长度低字节和高字节寄存器,写反了长度就错了。数据搬运用 8 位模式还是 16 位模式,取决于你的硬件连线,16 位模式下用sysOutWord,速度更快但要求数据长度是偶数,奇数长度要补一个字节。发送完成后别忘了释放mBlk,否则内存泄漏,跑几个小时就挂了。
4. DM9000A 驱动调试避坑:从 ping 不通到稳定收发的排查清单
4.1 坑一:VID/PID 读出来是 0xFFFF,但硬件量了电压正常
现象是dm9000aProbe里读 VID 返回 0xFFFF,用万用表量 DM9000A 的 VDD 和 VDDIO 都正常,片选信号也有。原因通常是 INDEX/DATA 的地址偏移搞错了,或者总线位宽配成了 8 位但硬件是 16 位。DM9000A 的 DATA 端口在 16 位模式下偏移是+4,8 位模式下偏移是+2,这个在数据手册的“Host Interface”章节有明确表格。解决方法是先用示波器抓一次读写时序,确认 CMD 引脚(也就是 INDEX/DATA 选择)在地址译码时有没有正确翻转,然后对照 BSP 里的sysPhysMemDesc确认映射长度至少 8 字节。
4.2 坑二:能收不能发,或者能发不能收
现象是ifconfig能看到设备,ping自己通,但 ping 外部设备不通。原因可能是 MAC 地址写反了,或者 RCR/TCR 的使能位没配对。DM9000A 的 RCR bit0 是 RXEN,TCR bit0 是 TXEN,但 RCR 还有 bit1 是 PROMISC,bit2 是 RUNT,这些位配错了会导致帧被过滤掉。解决方法是先把 RCR 写成 0x3F 全开,TCR 写成 0x01,确认能通之后再逐位收紧。另外检查 PAR 写入顺序,小端 CPU 下 MAC 地址要反序写,写完后读回来对比。
4.3 坑三:中断进不去,或者进去一次就再也不来了
现象是网卡初始化完,ping有发送但收不到回复,串口没有中断打印。原因可能是中断号配错了,或者 ISR 里没清中断状态。VxWorks 下用intConnect挂 ISR 时,中断号要从 BSP 的hwint.h里拿,不能随便写。ISR 里读 ISR 寄存器后必须写回清除,DM9000A 的 ISR bit0 到 bit3 是写 1 清除,bit4 到 bit7 是只读。如果只读不写,中断会一直触发,系统卡死。解决方法是 ISR 里先读 ISR,处理完后把读到的值写回去,但注意不要误清只读位。
4.4 坑四:大包丢帧,小包正常
现象是ping -l 1400不通,ping -l 32通。原因通常是接收 FIFO 读时序不对,或者mBlk分配的长度不够。DM9000A 的接收 FIFO 读操作要求连续,中间不能插入其他寄存器访问,否则 FIFO 指针会乱。解决方法是把接收处理放到任务级,用netJobAdd推一个 job,在 job 里一次性读完帧头和帧数据。另外检查mBlk的mBlkHdr.mLen有没有被截断,VxWorks 默认的mBlk簇大小可能是 128 或 256,大包需要mBlk链,别只读第一个mBlk。
4.5 坑五:跑一段时间后网卡死掉,重启就好
现象是系统跑几个小时或几天后,网络不通,ifconfig看统计有大量错误。原因可能是发送完成中断丢了,导致发送队列满,或者接收mBlk没释放,内存耗尽。解决方法是加看门狗,定期检查 NSR 的 TX 完成位和接收队列长度,发现异常就复位 DM9000A 的 NCR 重新初始化。另外在dm9000aSend里加超时,超时后主动读一次 ISR 清中断,别死等。
5. 用 NSR 和 ISR 做在线诊断:一个不重启就能定位网卡状态的技巧
调试 DM9000A 最难受的是板子在现场,不能随便重启,网络断了只能猜。我后来养成了一个习惯:在驱动里加一个dm9000aDiag函数,通过ioctl暴露出来,上层用一个小工具就能读。这个函数不复杂,就是读几个关键寄存器,然后按位解析成可读状态。比如 NSR 的 bit6 是链路状态,bit5 是速度,bit2 是发送完成,ISR 的 bit0 是接收中断挂起。把这些位打出来,基本能判断是物理层断了、还是驱动层卡了。
/* 诊断函数:通过 ioctl 的 SIOCGIFMEDIA 或自定义命令调用 */ void dm9000aDiag(UINT32 base) { UINT8 nsr = dm9000aReadReg(base, DM9000A_NSR); UINT8 isr = dm9000aReadReg(base, DM9000A_ISR); UINT8 rcr = dm9000aReadReg(base, DM9000A_RCR); logMsg("NSR=0x%02X LINK=%d SPEED=%d TXDONE=%d\n", nsr, (nsr>>6)&1, (nsr>>5)&1, (nsr>>2)&1, 4,5,6); logMsg("ISR=0x%02X RX=%d TX=%d\n", isr, isr&1, (isr>>1)&1, 3,4,5,6); logMsg("RCR=0x%02X RXEN=%d PROMISC=%d\n", rcr, rcr&1, (rcr>>1)&1, 4,5,6); }这个诊断函数我一般放在ioctl的SIOCGIFMEDIA分支里,或者自定义一个SIOCGDM9000命令。上层用ifconfig或者自己写个小程序调一下,就能看到当前网卡状态。有一次现场反馈网络时通时断,我让他们跑这个诊断,发现 LINK 位一直在跳,最后查出来是网线水晶头氧化了,换线就好了。如果没有这个诊断,可能就要换板子、换驱动,折腾好几天。
还有一个技巧是统计寄存器。DM9000A 内部有接收错误计数和发送错误计数,虽然不精确,但能看出趋势。如果接收错误计数一直涨,先查 RCR 的 RUNT 位和 CRC 位,可能是对端设备发的小包或者坏包。如果发送错误计数涨,查 TCR 的 CRC 使能位和填充位。这些统计不用频繁读,一分钟读一次就够,写个定时器任务,把值打到串口或者存到 flash 里,出问题时翻日志。
最后说一个我踩过的坑:DM9000A 的 ISR 寄存器读一次会自动清一些位,但有些位必须写 1 清。我一开始只读不写,结果中断触发一次后就再也不来了,因为 ISR 的 RX 位一直是 1,中断控制器认为是电平触发,一直报。后来改成读完后把读到的值写回去,但注意只写那些写 1 清的位,只读位写 0 就行。这个细节数据手册里写得很清楚,但英文手册翻起来费劲,容易漏。希望帮到你。
本文还有配套的精品资源,点击获取