简介:这份DM9000网卡驱动源码专为嵌入式开发者准备,覆盖DM9000A、DM9000B与DM9000E三款以太网控制器,适用于Linux内核驱动开发、裸机驱动移植及相关课程设计等场景。DM9000系列在工业控制与嵌入式设备中应用广泛,该驱动正可帮助解决不同芯片版本在初始化、寄存器配置和数据收发上的差异。压缩包共含2个文件,分别是一个C源文件与一个头文件,体积仅11KB,代码紧凑但功能完整,从PHY/MAC寄存器设置、DMA传输、中断处理到错误管理均有清晰实现,函数分层直观,方便阅读和移植。目前已有137人下载学习,说明其实用性获得一定认可。通过学习这份代码,开发者可以理解DM9000系列与CPU之间完整的交互流程,掌握设备驱动在内核中的挂载方式,也能对比A/B/E型号的配置差异;借鉴其模块化设计,可加快自身网卡驱动项目的开发调试,并在遇到网络不通、寄存器配置异常等问题时提供实际排查思路。
1. DM9000驱动代码:先弄清它解决什么,再看代码不迟
嵌入式板卡上接了一颗DM9000网卡,上电后ping不通,八成不是芯片坏了,而是驱动还没把寄存器摸熟。这份DM9000 driver代码(dm9000.h、dm9000.c)做的事情很直接:把DM9000A、DM9000B、DM9000E这颗集成MAC+PHY的以太网控制器,从数据手册里的寄存器表格翻译成可编译、可单步跟踪的C语言。它能解决的是板子接入10/100M以太网最核心的那一摊事:初始化、发数据、收数据、处理中断。
适合谁?两类人。一类是在裸机或FreeRTOS这类RTOS上做网络功能的,需要一份不依赖Linux内核的独立驱动;另一类是做Linux移植、想搞清楚DM9000在net_device之下怎么跟硬件打交道的。带着这两个诉求去读代码,会发现它其实只围绕几条固定的寄存器读写路径展开,摸清楚之后,改到任何平台上心里都有底。
2. 先摸芯片底细:DM9000A/B/E的寄存器地图与三处选型差异
2.1 内存映射IO和CMD引脚:访问DM9000的时序规矩
DM9000的寄存器访问方式和SPI、I2C接口的网卡不一样,它走内存映射接口,CPU用片选、读写信号直接访问一段地址空间。芯片内部寄存器不是每个独占一个地址,而是分成「索引」和「数据」两个口:先往地址端口写寄存器号,再从数据端口读写数据。这个访问时序靠CMD引脚区分,CMD为低时是地址周期,CMD为高时是数据周期。
硬件上,常见做法是把CMD接到地址总线某一根线上。比如基地址0x300,CMD接到A2,地址端口就是0x300,数据端口就是0x302。不按这个习惯接的板子也有,所以移植第一件事不是看代码,是看原理图里CMD到底挂了哪根线。先看这份驱动最底层的两个函数:
static inline u8 dm9000_read_reg(struct dm9000_priv *priv, int reg) { writeb(reg, priv->addr_port); /* 先写寄存器索引 */ return readb(priv->data_port); /* 再从数据端口读 */ } static inline void dm9000_write_reg(struct dm9000_priv *priv, int reg, u8 val) { writeb(reg, priv->addr_port); /* 写索引 */ writeb(val, priv->data_port); /* 写数据 */ }逻辑上就是前面说的两拍访问。写索引那一下,CMD处于地址周期,芯片把reg锁存到内部索引寄存器;第二拍CMD拉高,芯片知道接下来要访问数据了。reg取值范围0x00到0x7F,对应芯片内部的配置寄存器、状态寄存器和FIFO控制端口。
有两个细节值得注意。一是readb/writeb在Linux里由平台提供,在裸机里通常直接操作指针加volatile;二是对数据端口连续读写时,编译器可能把两次访问合并或重排,导致读到串位的数据。我一般在数据口读写之间加编译器屏障,或者确保data_port指针带volatile修饰。
提示:DM9000所有寄存器操作都走「索引+数据」两拍,任何一步直接访问寄存器绝对地址,读回来的值都和你预期的不一样。
2.2 初始化要碰的寄存器分组
DM9000寄存器看着多,其实驱动里高频使用的就十几个。分组整理之后,初始化顺序就清楚了。
| 分组 | 寄存器 | 作用 |
|---|---|---|
| 控制类 | NCR(0x00)、RCR(0x05)、TCR(0x06) | 软复位、接收使能、发送触发 |
| 状态类 | NSR(0x01)、ISR(0x02) | 发送完成、接收事件、溢出标志 |
| 中断控制 | IMR(0x03) | 屏蔽/使能PRX、PTX、RXOV |
| 地址类 | MAR0-5(0x10-0x15) | 本机MAC地址 |
| 数据通道 | MWCMD(0xF8)、MRCMD(0xFA)、MRCMDX(0xF9) | FIFO连续读写 |
| 长度寄存器 | TXPLL(0xFC)、TXPLH(0xFD) | 发送帧长度 |
初始化顺序一般是:软复位 -> 关中断 -> 清中断标志 -> 写MAC地址 -> 配置接收控制 -> 开中断。软复位这一步容易翻车,NCR的RST位写1之后,芯片内部要重新加载配置、复位PHY,如果复位后立刻去写寄存器,数据可能根本没写进去。常见做法是复位后延时几百微秒,再轮询确认复位完成。
void dm9000_init(struct dm9000_priv *priv) { /* 1. 软复位,NCR的RST位置1 */ dm9000_write_reg(priv, DM9000_NCR, NCR_RST); udelay(200); /* 2. 先关中断,防止复位后中断风暴 */ dm9000_write_reg(priv, DM9000_IMR, 0x00); /* 3. 清除所有挂起中断(ISR是写1清除) */ dm9000_write_reg(priv, DM9000_ISR, 0xFF); /* 4. 写MAC地址,MAR0对应MAC第一个字节 */ for (i = 0; i < 6; i++) dm9000_write_reg(priv, DM9000_MAR + i, priv->mac_addr[i]); /* 5. 使能接收,接收控制寄存器 */ dm9000_write_reg(priv, DM9000_RCR, RCR_RXEN); /* 6. 最后开中断,只开放收包和溢出 */ dm9000_write_reg(priv, DM9000_IMR, IMR_PRX | IMR_RXOV); }关键是第6步必须放在最后。如果一开始就把IMR打开,复位过程中ISR里累积的各种状态会直接触发中断,而这时候驱动还没准备好处理逻辑,容易误判成硬件故障。
2.3 DM9000A/B/E的型号差异落在这几个参数上
DM9000A、DM9000B、DM9000E是同一系列的不同版本,摘要里说得很清楚:A适合标准应用,B面向低功耗环境,E可能带额外特性或优化。对驱动开发来说,真正要关注的差异是功耗控制、封装和PHY寄存器表现。
| 型号 | 主要定位 | 驱动里需要关注的点 |
|---|---|---|
| DM9000A | 标准应用 | 寄存器访问时序最典型,大多数驱动以此为准 |
| DM9000B | 低功耗环境 | GPR寄存器相关,休眠/唤醒路径要单独处理 |
| DM9000E | 新特性/优化 | 初始化时序可能更短,PHY协商表现不同 |
驱动代码里区分型号,常见做法是编译宏或者平台数据结构里带一个model字段。比如GPR寄存器用于控制内部PHY供电和低功耗模式,DM9000B的板子如果没在休眠前把GPR配好,唤醒后网络会长时间ping不通。另一个隐藏差异是EEPROM。芯片上电后会尝试从外部EEPROM读MAC地址和配置,如果板子上根本没贴EEPROM,读出来的MAC全是0xFF,驱动必须在初始化时强制用平台数据覆盖写入MAR0-5。
3. 移植实战:把dm9000.c/h接进板子的五段关键代码
3.1 平台层要准备的四样东西
驱动本身和平台无关,但它依赖平台提供四个东西:基地址、中断号、寄存器访问函数、延时函数。在Linux里这四样来自platform_device或设备树,在裸机里就是你自己定义的一个结构体。我习惯先把平台相关的东西收敛到一个结构体里,后面改板子时只动这一处:
struct dm9000_priv { void __iomem *addr_port; /* 地址端口基址 */ void __iomem *data_port; /* 数据端口基址 */ int irq; /* 中断号 */ u8 mac_addr[6]; /* MAC地址 */ u8 model; /* DM9000A/B/E型号 */ };这个结构体是所有函数的第一个参数。addr_port和data_port的差值由CMD引脚的接法决定,前面说过常见偏移是2。irq在裸机上可能就是GPIO中断号,在Linux上是IRQ号。mac_addr的来源要明确,是从EEPROM读、从设备树读还是从板级文件读,直接决定初始化代码里要不要覆盖MAR寄存器。
3.2 初始化函数逐段拆解:从软复位到PHY就绪
初始化代码在2.2节已经贴了主体,这里补充一个容易忽略的点:软复位之后,PHY(物理层)也需要时间从复位状态恢复。DM9000的PHY和MAC集成在同一颗芯片里,但复位释放后PHY的协商不是瞬间完成的,尤其是插着网线的时候,从复位到link up可能要一两秒。
常见做法是初始化时先不管PHY状态,把寄存器配置好、中断打开,然后让上层协议栈去处理link事件。有些RTOS移植会在这里加一个轮询:循环读PHY状态寄存器,等到bit表明link up再返回,这个等待放在init里会让系统启动变慢,我一般不建议这么干,除非你的应用要求上电后立刻发包。
3.3 发送路径:MWCMD顺序写入与TX触发
发送路径是驱动里最讲究顺序的一段。发一包数据,先后要做四件事:确认上次发送完成、写帧长度、往MWCMD端口写数据、触发发送。顺序错了,轻则发不出去,重则把FIFO写乱。
int dm9000_send(struct dm9000_priv *priv, u8 *buf, int len) { int i; /* 等待上一次发送完成,检查NSR的TX1END位 */ while (!(dm9000_read_reg(priv, DM9000_NSR) & NSR_TX1END)) ; /* 写长度寄存器,先低字节后高字节 */ dm9000_write_reg(priv, DM9000_TXPLL, len & 0xFF); dm9000_write_reg(priv, DM9000_TXPLH, (len >> 8) & 0xFF); /* 指向MWCMD端口,连续写数据(16位模式按半字写) */ dm9000_write_reg(priv, DM9000_MWCMD, 0x00); for (i = 0; i < (len + 1) / 2; i++) writew(((u16 *)buf)[i], priv->data_port); /* TCR的TXREQ位置1,触发发送 */ dm9000_write_reg(priv, DM9000_TCR, TCR_TXREQ); return len; }NSR_TX1END是上次发送完成的标志,轮询它比定时器等更可靠。长度寄存器先写低字节再写高字节,这是DM9000固定的字节序,写反了帧长会变成一个大数,芯片会一直等数据。MWCMD是发送FIFO的写端口,往这个端口连续写数据,每写一次内部地址自动加一,不需要手动改索引寄存器。16位模式下按u16写,如果板子是8位总线,这行要改成按字节写,这也是驱动移植里最常见的改动点之一。
3.4 接收路径:ISR状态机与RX缓冲区循环
收包比发包麻烦,因为数据是异步进来的,驱动要么在中断里读,要么在轮询任务里读。DM9000接收FIFO的读取方式比较特殊,先通过MRCMDX读一个状态字节判断有没有包,再通过MRCMD连续读状态、长度和数据。
static void dm9000_rx(struct dm9000_priv *priv) { u8 status, lenl, lenh; int len, i; /* MRCMDX端口读入状态字,bit0为1表示FIFO里有包 */ status = readb(priv->data_port); if (!(status & 0x01)) return; /* 接着读状态字节和长度高低字节 */ status = readb(priv->data_port); lenl = readb(priv->data_port); lenh = readb(priv->data_port); len = (lenh << 8) | lenl; /* 按长度连续读数据到接收缓冲 */ for (i = 0; i < (len + 1) / 2; i++) ((u16 *)priv->rx_buf)[i] = readw(priv->data_port); }中断处理函数的职责是先读ISR,根据标志位决定调不调用dm9000_rx,处理完写ISR清除标志。
static irqreturn_t dm9000_interrupt(int irq, void *dev_id) { struct dm9000_priv *priv = dev_id; u8 isr = dm9000_read_reg(priv, DM9000_ISR); if (isr & ISR_PRX) /* 收包事件 */ dm9000_rx(priv); if (isr & ISR_RXOV) /* 溢出,清空FIFO恢复 */ dm9000_rx(priv); /* ISR写1清除本次已处理的中断 */ dm9000_write_reg(priv, DM9000_ISR, isr); return IRQ_HANDLED; }ISR是写1清除,这点极其容易踩坑,下面第4章专门说。读ISR拿到的值,哪些位置1就表示哪些事件发生,处理完后把这个值原样写回去,对应位就被清掉。如果写0,中断永远清不掉,驱动会一直进中断。
3.5 与Linux网络子系统的粘连点
如果是在Linux里用这份代码,不能直接照搬中断函数,得通过net_device_ops接进内核网络框架。对应关系是:dm9000_init对应ndo_open,dm9000_send对应ndo_start_xmit,中断函数在request_irq里注册。发送函数返回前要调用netif_wake_queue,否则协议栈以为网卡忙,后续包都积压在队列里。
还有一个点值得说,主线Linux内核本身已有一份dm9000.c,但那份代码带了大量内核框架绑定,读懂它反而费劲。这份独立驱动的好处是逻辑裸露,适合先跑通再往内核框架里套。实际项目里我见过不少人先用它做裸机验证,确认硬件没问题后再去改内核驱动,能省不少时间。
4. 避坑笔记:中断丢失、RX溢出与寄存器顺序的三个翻车现场
4.1 现象:发完一包数据,中断再也不来
发完数据后,期望收到发送完成中断,结果一次都不触发。查寄存器发现ISR里PTX位其实置1了,但中断始终没进来。
原因有两个。一是ISR没有写1清除,中断标志一直挂着,芯片认为中断还没被处理,不会触发下一次;二是初始化时IMR开启得太早,复位阶段的脏状态已经把中断霸占了,后续真正的事件反而被卡住。
解决:中断处理函数末尾,把从ISR读到的值原样写回去,并且确认初始化顺序是先把IMR全关、清ISR、配寄存器、最后开IMR。这个顺序问题我在两个项目上各踩过一次,每次都是查了一天最后发现是顺序问题。
4.2 现象:网络跑一会儿就卡死,RX溢出标志反复置位
长时间跑吞吐测试,网卡突然收不到包,读ISR发现RXOV位置1。清掉标志后能恢复,但过一会儿又卡。
原因是接收路径处理不过来。DM9000内部接收FIFO深度有限,中断处理函数里如果只读一包就退出,FIFO里的剩余帧会堆积,直到溢出。溢出之后芯片会停止接收新数据,表现为网络卡死。
解决:中断处理里做一个循环,反复调用dm9000_rx直到MRCMDX读出来的状态字bit0为0,也就是FIFO被清空。如果数据量特别大,还可以在循环里加一个次数上限,防止在中断里待太久。另外要检查IMR里RXOV对应的中断是否使能,溢出事件必须能进中断处理函数,才有机会清FIFO。
4.3 现象:读寄存器全是0xFF,写什么读回都是0xFF
驱动加载后dmesg打印出MDT、NCR等寄存器都是0xFF,MAC地址也全是FF,看起来芯片像没上电或者坏了。
原因基本是三个方向:寄存器访问时序不对、CMD引脚接错、或者GPIO模拟时序时没有留足延迟。最常见的是CMD引脚没接到预期的那根地址线,导致地址端口和数据端口实际指向同一个位置,索引永远写不进去。
解决:先确认原理图上CMD接到了哪根地址线,再对着代码改addr_port和data_port的偏移。如果是GPIO模拟访问,在写索引和读数据之间加一个小的延时,有些GPIO操作太快,芯片来不及锁存索引。这个问题的排查成本很高,我之前遇到过板子改版后CMD从A2挪到A3,驱动层完全没看出来,最后用逻辑分析仪看时序才定位到。
4.4 现象:MAC地址读出来全是FF,网络能起来但对端不认
ifconfig能看到eth0,但抓包发现源MAC是FF:FF:FF:FF:FF:FF,路由器直接把包丢了。
原因是板上没贴EEPROM。DM9000上电后会尝试从外部EEPROM读取MAC地址和配置,没有EEPROM时寄存器里读回来的就是全FF。芯片本身没有内置出厂MAC,这个地址必须由驱动从别的渠道拿。
解决:把MAC地址来源改成平台层。设备树里配local-mac-address,或者板级文件里写死一个由厂商分配的地址,初始化时在写MAR寄存器这一步覆盖掉。字节序要注意,MAR0对应MAC地址第一个字节,如果设备树里是按big-endian存的,移植时还要做字节反转,不然MAC又反着。
5. 验证与调试:环回测试、中断计数与吞吐量实测
5.1 PHY环回模式验证数据通路
拿到驱动第一件事不是接网线,而是先做环回测试。DM9000内部PHY支持loopback模式,设置PHY的BCR寄存器(PHY寄存器0)的loopback位,发送的帧会在PHY内部绕回来,不依赖外部网络环境。这一步能快速确认MAC、FIFO、中断这条通路是否完整。
/* 读取PHY寄存器0,置loopback位,再写回 */ u16 bcr = dm9000_phy_read(priv, 0x00); bcr |= 0x4000; /* BCR bit14 = loopback */ dm9000_phy_write(priv, 0x00, bcr); /* 发送一帧测试数据,启动发送 */ dm9000_send(priv, test_buf, test_len); /* 等待中断或轮询接收FIFO,看能否收到自己发的数据 */环回测试通过后,务必把loopback位清掉,恢复到正常模式,否则后面连网线怎么都ping不通。我踩过一次这个坑,环回测试完直接跑了应用,结果网络一直不通,查了半天才发现loopback没关。DM9000的PHY读写在驱动里走的是和访问MAC寄存器类似的索引端口方式,实现时要注意PHY访问的时序比MAC寄存器更慢,读完之后加一点延时再读数据,否则读回的值不稳定。
5.2 Linux下网卡调试三板斧
驱动接到Linux里之后,验证手段就丰富了。我的固定三件套是ifconfig、dmesg和iperf,分别对应状态、协商和吞吐。
| 验证项 | 命令/操作 | 预期结果 |
|---|---|---|
| 驱动加载状态 | dmesg | grep dm9000 | 看到寄存器读值正常,MAC地址正确 |
| 网卡UP和链路 | ifconfig eth0 up;dmesg查link | link up,速率10/100M自动协商 |
| 收发基本功能 | ping 对端地址 | 丢包率0,延迟正常 |
| 吞吐量 | iperf -c 对端IP | 100M下单向吞吐接近90M以上 |
如果ping丢包严重,优先看dmesg里有没RXOV或者CRC错误,这些直接指向FIFO溢出或PHY协商异常。吞吐量上不去,先看网线和对端网卡是不是协商成了10M全双工,再查驱动的发送路径有没有频繁等NSR_TX1END超时。发送路径的轮询延时直接影响吞吐,如果每次发包都要等上一次完成,吞吐会被压得很低,这时候要考虑用双缓冲指针或者TCR里发送完成中断来减少轮询开销。
6. 移植前的最后工序:我每次必过的五步代码自检
驱动代码写完后,编译通过不代表能用,我习惯过一次固定清单,每一条都是在不同项目上交过学费的。第一步,确认CMD引脚偏移和寄存器读回一致性,随便读一个NCR寄存器,对比原理图看值是否合理,这一步能过滤掉一大半硬件配置错误。第二步,检查ISR清除方式,全文搜ISR写入的地方,确认是写1清除而不是写0。第三步,检查发送长度字节序,TXPLL先写低字节,TXPLH后写高字节,这个顺序错了最隐蔽,因为单包小数据可能碰巧能发出去,大数据帧才会暴露。第四步,确认RX溢出处理不是只清标志,而是真的把FIFO清空,否则长时间跑业务必然卡死。第五步,核对MAC地址来源,没有EEPROM的板子必须走平台数据,并且确认字节序。
这套自检下来,最花时间的永远是第一步。硬件连线问题在代码层面看不出任何异常,所有的寄存器和中断逻辑都对,但芯片就是不工作。后来我养成了一个习惯,拿到新板子先把驱动里的寄存器读函数单独拿出来,写一个最简单的读寄存器循环,把0x00到0x7F的值全部dump出来,跟数据手册里的上电默认值对比,这个动作能在一小时内确认芯片是否真的被驱动起来了。
从那以后,每次移植DM9000驱动,我都强制先做寄存器dump,再做环回,最后才接网线联调。这个顺序帮我省下的调试时间,远比我最初学这些坑时付出的代价多。希望帮到你。
本文还有配套的精品资源,点击获取