STM32驱动DM9000以太网控制器:从时序到ping通全攻略
2026/9/21 1:36:22 网站建设 项目流程

简介:面向嵌入式开发与网络驱动初学者的单片机驱动DM9000E网卡芯片调试资料,重点弥补网上仅有Linux/WinCE驱动介绍、缺乏单片机底层DM9000示例的空白,适合正在学习ARM-Linux网络模块、或者需要在51/AVR等平台利用总线或IO模拟方式连接DM9000的开发者。内容依照实际操作流程展开:从DM9000E支持8/16/32位处理器模式的选择,到IOR、IOW、AEN、CMD、INT、RST及地址/数据引脚的连接,再到C驱动编写中容易踩坑的大端/小端格式、CMD与DATA端口的区分、寄存器读写和ARP协议实现细节,均有分步说明。包体为1个PDF文档,容量仅33KB,轻量便携,便于直接下载后对照调试。已有381人浏览学习。借助这份PDF,读者可快速掌握DM9000E初始化、索引/数据端口访问、总线时序模拟以及网络协议底层处理思路,为后续向Linux驱动移植打下坚实根基。

1. 为什么在单片机项目里死磕DM9000

搞嵌入式网络通信的朋友,对DM9000这颗芯片应该都不陌生。它是DAVICOM出品的一颗10/100M自适应以太网控制器,支持8位和16位两种总线模式,能和绝大多数MCU直接对接。我最早接触它是在一个基于STM32F103的远程数据采集项目里,当时需要把传感器数据通过以太网上传,主控资源又紧张,选来选去最后还是落了这颗芯片。

很多人可能会问,现在带MAC的MCU一抓一大把,为什么还要外挂DM9000?原因其实很实在:第一,很多工业级老方案的主控芯片根本没有以太网控制器,换主控等于整个硬件平台推倒重来,成本太高;第二,DM9000这颗芯片资料多、参考设计成熟、驱动代码网上也能找到,踩坑成本相对可控;第三,它支持自动协商、CRC校验、长线驱动这些功能,物理层和链路层的事情基本都包了,MCU只要做好数据搬运就行。

所以这篇博文适合谁看?正在做单片机以太网方案、被DM9000驱动搞得头疼的嵌入式开发者,尤其是手里握着51、STM32、NXP这类不带MAC控制器主控的朋友。我会把从硬件接线、寄存器读写、初始化流程到ping通全过程的细节都拆开讲一遍,包括我实际调试时踩过的坑和排查思路。读完你至少能有一个可以照着抄的驱动框架,而不是面对一堆datasheet无从下手。

2. 方案选型与整体设计思路

2.1 为什么选DM9000而不是其他方案

做网络方案之前,我其实对比过好几条路。一是直接用带MAC的单片机,比如STM32F407或者NXP的LPC1768,但当时项目定型的MCU是STM32F103,换芯片的连锁反应太大;二是用SPI接口的ENC28J60,省IO,但实测吞吐量一般,而且SPI中断处理在高负载下容易丢包;三是用DM9000,虽然要占用并行总线,但胜在驱动成熟、吞吐量高,和MCU之间只要处理好读写时序就行。

DM9000还有一个隐性优势:它内置了SRAM收发缓冲区,约16KB,可以缓解MCU侧内存紧张的问题。MCU只需要把要发送的数据包写入发送缓冲区,然后触发发送寄存器;接收时通过中断标志位判断有没有新包,再用读操作把接收缓冲区里的数据搬出来。整个数据通路非常清晰,特别适合裸机或者轻量级RTOS环境。

2.2 硬件接口设计要点

DM9000的控制接口本质上就是一个并行SRAM接口。地址线A0(有些参考设计叫CMD引脚)用于区分访问的是索引寄存器还是数据寄存器:低电平表示当前总线上的地址是寄存器索引,高电平表示读写的是寄存器数据。这个时序逻辑搞错,后面所有寄存器操作都会乱套。

我用的接法是16位数据总线模式,SD0~SD15接到MCU的PB0~PB15,A0接到PA8,片选CS接到NE1(STM32的Bank1区),读使能RD和写使能WE分别接到NOE和NWE,中断引脚INT接到PA15外部中断。硬件上还有一个小细节,芯片的EEPROM接口(EECS、EECK、EEDI、EEDO)最好预留出来,虽然我们一般不烧EEPROM,但留着能用软件配置默认MAC地址,调试起来方便很多。

注意:DM9000的复位引脚/RST是低电平有效,复位电路最好用RC复位加手动复位按钮,不要只靠MCU引脚控制,否则程序跑飞后想重新初始化芯片会非常被动。

2.3 为什么说驱动核心在时序

很多人第一次调DM9000,卡在第一步就动不了:读寄存器返回的全是0xFF或者0x00。这时候别急着怀疑芯片坏了,多半是时序没满足芯片手册要求。DM9000的总线访问时序里有几个关键参数:地址建立时间、数据建立时间、读写脉冲宽度。MCU的FSMC或者普通GPIO模拟时序,都要保证这些时间大于最小值。

如果用的是STM32的FSMC,配置起来相对简单,只要选好Bank区、设置好读写时序参数即可。但如果像我一样用普通GPIO模拟总线,就必须在每次读写操作里插入足够的延迟,保证每个信号的稳定时间。我后面会在调试章节详细展开这部分,因为这是整个驱动能否跑通的分水岭。

3. 驱动初始化:从寄存器说起

3.1 寄存器操作基础

DM9000的寄存器空间分两部分:索引寄存器和数据寄存器。访问流程很简单,先把想要访问的寄存器地址写到索引口(A0为低),然后通过数据口(A0为高)读写实际数据。因为这种设计,驱动里只要实现两个最底层的函数就能操作所有寄存器:

void dm9000_write_reg(uint8_t reg, uint8_t data); uint8_t dm9000_read_reg(uint8_t reg);

这两个函数内部怎么实现?如果是FSMC方式,直接对映射地址赋值或者取值就行,注意16位模式下次地址要按2字节对齐。如果是GPIO模拟方式,就需要按顺序拉高/拉低各个控制信号,中间插入延时。我项目里用的GPIO模拟,核心代码如下:

void dm9000_fastwrite(uint16_t addr, uint16_t data) { // 写寄存器地址 DM9000_CS_LOW(); DM9000_CMD_LOW(); set_data_lines(addr); DM9000_WE_LOW(); delay_us(1); DM9000_WE_HIGH(); // 写数据 DM9000_CMD_HIGH(); set_data_lines(data); DM9000_WE_LOW(); delay_us(1); DM9000_WE_HIGH(); DM9000_CS_HIGH(); }

这里有个容易栽的坑:设置数据线之后到拉低WE之间,必须留出足够的建立时间,否则芯片会采样到不稳定的电平。我一开始没加延时,结果奇偶地址的数据总是错位,折腾了半个下午。

3.2 芯片ID识别与自检

初始化芯片的第一步,永远是读芯片ID,判断总线通信是否正常。DM9000的芯片ID寄存器是0x00和0x01,读出来应该是0x90和0x0A,合起来是0x900A。这一步能通过,说明地址线和数据线基本没问题,控制时序也正确,后面才谈得上配置。

我习惯在ID校验后面再做一次软件复位。把0x03寄存器(网络控制寄存器NCR)的第0位写1,延时20毫秒以上再清零,让芯片内部完成一次复位。复位之后,建议把寄存器0xFE(PHY状态寄存器)和0x3F(PHY电源管理寄存器)检查一遍,确认PHY上电正常,否则后面连不上网线也没法发现。

uint16_t id = dm9000_read_id(); if (id != 0x900A) { // 打印错误信息,终止初始化 } dm9000_write_reg(DM9000_NCR, 0x03); // 软复位 delay_ms(20); dm9000_write_reg(DM9000_NCR, 0x00);

3.3 初始化配置:中断、MAC地址、收发控制

ID校验通过后,就要按项目需求对DM9000做正式配置。这一步配置的东西比较多,我列个最常见的初始化参数对照表:

寄存器含义
DM9000_NCR (0x00)0x00清除复位,内部PHY使能
DM9000_NSR (0x01)读状态确认PHY状态正常
DM9000_ISR (0x02)0xFF清除所有中断标志
DM9000_IMR (0x05)0x01使能PRX接收中断
DM9000_RCR (0x05 注意实际)0x39使能RX缓存、丢弃错误包
DM9000_MAR1~MAR8组播地址按需配置,不组播填0
DM9000_PAR1~PAR6MAC地址写入自己的MAC

这里要注意,IMR和RCR的地址在不同版本的datasheet里写法可能有差异,一定要以手头芯片丝印对应的最新版手册为准。我最早参考老版本代码,把RCR配置写到0x05,结果中断完全进不来,排查了很久才发现是寄存器地址错位。

MAC地址一般从EEPROM读,或者程序里写死。我们项目里用的方式是在配置宏里定义MAC,然后打包写入PAR寄存器组。如果局域网有多台设备,MAC一定不能重复,否则交换机会丢包丢到怀疑人生。

3.4 PHY寄存器配置

DM9000内置PHY,PHY的寄存器需要通过芯片的PHY访问接口来操作,不能直接像普通寄存器那样读。具体做法是先写PHY地址到寄存器0x0A(PHY控制寄存器),然后通过0x0C(PHY状态寄存器)和0x0D(PHY数据寄存器)来读写数据。

要做的关键配置有两件:一是检查PHY的链路状态,判断有没有插网线,网线断开的时候芯片会置位相应状态位;二是配置自动协商,让芯片和交换机自动协商出10M/100M模式和全双工/半双工模式。这些配置做好之后,插上网线,PHY状态寄存器的Link位应该很快变为1,代表物理链路已经建立。

4. 数据收发:驱动主干是怎么跑的

4.1 接收路径与中断处理

DM9000接收数据包的基本流程是:芯片收到完整以太网帧后,会根据RCR里的设置判断是否接收,接收成功后把数据写入内部接收缓冲区SRAM,然后置位中断标志PRX,向MCU发出中断请求。MCU收到中断后,要通过读操作把数据从芯片SRAM里搬出来。

整个接收过程有几个关键步骤:

  1. 读取中断状态寄存器ISR,判断是PRX类型的中断。
  2. 读取接收缓冲区状态字,这个状态字包含当前数据包长度等信息。
  3. 根据状态字指示的长度,连续读取数据,把整帧数据搬到MCU的缓冲区。
  4. 处理完后写ISR清除中断标志,使芯片能继续接收下一个包。

如果用的是裸机轮询方式,就在主循环里循环检查ISR的PRX位;如果使用中断方式,就把这块逻辑放在外部中断服务函数里。我实际项目中用的是外部中断加裸机主循环消费,收到中断后置一个标志位,主循环检测到标志再去取数据,这样不会在中断里做重活,避免了长中断导致的其他外设响应延迟。

4.2 发送路径:写SRAM、触发发送

发送一个数据包的流程相对简单,但要注意芯片的SRAM写入特性。大致步骤如下:

void dm9000_send_packet(uint8_t *data, uint16_t len) { // 清零发送状态 dm9000_write_reg(DM9000_NSR, 0x2C); // 写入发送数据长度 dm9000_write_reg(DM9000_TXPLL, len & 0xFF); dm9000_write_reg(DM9000_TXPLH, (len >> 8) & 0xFF); // 软件发送请求 dm9000_write_reg(DM9000_TCR, 0x01); // 写入数据到发送缓冲区 dm9000_fastwrite(DM9000_MWCMD, data, len); // 启动发送 dm9000_write_reg(DM9000_TCR, 0x00); // 等待发送完成标志 while (!(dm9000_read_reg(DM9000_NSR) & 0x01)); }

有个顺序问题我一开始没注意:写TCR寄存器请求发送和写数据到MWCMD的顺序,一定先触发发送请求,再写数据,还是先写数据再触发请求?踩过坑之后我总结的经验是,必须先写长度、再触发TCR、最后写数据,这个顺序是芯片手册建议的,一些参考代码里顺序不一样也能跑,但按规范来最稳。

发送完成标志位有好几个,TCR为0x01时表示正在发送,NSR的bit0表示发送完成。实际调试中发现,轮询等待发送完成时最好加一个超时机制,否则网线断开等异常情况下,芯片可能一直不置位完成标志,程序就死循环了。

4.3 缓冲区管理与内存规划

DM9000内部的SRAM空间有限,MCU侧也要给收发缓冲区预留足够的内存。我一般在MCU侧开两个环形缓冲,一个接收DMA环形队列,一个应用层可读的队列。中断服务函数只负责从芯片SRAM把数据搬到接收环形队列,主循环再从队列里取数据做协议解析。

这个设计能有效应对突发流量。比如项目里我们曾经连续往板子发1000个包,每个包64字节,裸机不加环形缓冲的做法直接丢了十几个包,加了环形缓冲后一包不丢。内存上,我开了一个256字节的发送缓冲和一个1024字节的接收缓冲,对大多数嵌入式应用已经够用。

5. 调试实录:从完全黑屏到ping通全链路

5.1 常见问题速查表

从我这次调试经历看,大部分问题都集中在下面几个点上,我整理成一个速查表,方便大家对照排查:

现象可能原因排查方法
芯片ID读不对总线时序不满足;数据线接错;芯片没复位成功先用GPIO模拟时序,逐步加大延时;检查复位引脚电平;用万用表量电源和地
中断频繁触发但没数据ISR未正确清除;RX数据读取时序不对在中断里先读ISR再读状态字,读完后写ISR清标志;示波器量RD信号
ping不通但状态正常MAC地址配置错误;发送完成标志轮询超时;网线或交换机问题抓包确认发出的包有没有ARP请求;检查PHY协商模式;换网线测试
发送死循环卡死发送完成标志未被置位;网线未连接加超时退出;检查NSR寄存器;确认TCR触发是否正确
数据丢包严重接收缓冲区溢出;中断响应不够快;RX缓冲未及时释放加大接收缓冲区;优化中断服务函数;检查RX状态字处理逻辑

5.2 我踩过的三个最深的坑

第一个坑是GPIO模拟时序。我最初照搬网上现成的代码,完全没注意人家用的是FSMC,我这边GPIO模拟速度跟不上,导致读ID一直在0xFFFF和0x0000之间跳。后来我把每次信号切换后的延时调到1微秒以上,才稳定读到0x900A。这个教训告诉我,别人的代码只能作为逻辑参考,时序参数必须根据自己硬件和主频重新调。

第二个坑是外部中断和接收数据之间的竞争。我的中断服务函数一开始直接在里面读SRAM数据,结果主程序正在操作总线的时候被中断打断,两边同时访问DM9000的数据口,导致数据错乱。后来改成中断里只置标志位,主循环统一处理数据读取,这个bug就消失了。如果用了RTOS,最好给DM9000的访问加一个互斥锁,防止多线程同时操作。

第三个坑是接收状态字的处理。DM9000的接收状态字长度有两个字节,但实际有效位只有高字节的几位,低字节是长度低位。我没按手册处理,直接把两个字节当作一个16位长度用,导致大包长>255字节时接收数据被截断,上层协议栈一直报CRC错误。改对了之后才真正理解什么叫“按手册办事”。

5.3 用网络抓包验证驱动正确性

驱动写完以后,最有效的验证方法是用wireshark抓包。我把板子和电脑连在同一个交换机上,板子上电后发送一个UDP广播包,电脑上如果能看到这个包,说明发送链路已经通了;然后电脑往板子MAC发一个ARP请求,板子如果能自动回复,说明接收链路和协议栈处理也正常。

抓包的时候要注意,交换机可能会隔离广播域,如果像我一样直接用网线把板子插到电脑网口,需要手动把电脑网口配置成和板子同一网段的静态IP,否则ARP请求到不了板子。我调试的时候就是在这里卡了很久,一直以为是驱动问题,最后发现是电脑防火墙把ICMP包拦截了,关掉防火墙立刻通。

5.4 移植lwIP时要注意的接口细节

驱动调试到能发能收之后,我把它接到了lwIP协议栈上。这里有两个接口必须实现好:一个是底层发送函数,把协议栈传下来的pbuf链按链路层帧格式发送出去;另一个是接收回调,把从DM9000收到的数据封装成pbuf,交给协议栈处理。

移植的时候有个容易忽略的点:lwIP在无操作系统模式下依赖一个周期性的tcpip_timer,用来处理TCP的超时重传。如果只在主循环里轮询,记得把tcpip_thread的延时时间适当调大,或者用定时器中断调用,否则TCP连接会出现莫名其妙的超时。我项目里用的是FreeRTOS,单独开了一个tcpip_task,优先级设得比驱动接收任务低一点,这样既不会占用过多CPU,又能保证收包及时。

6. 可靠性与性能优化:驱动不是通了就完了

6.1 链路异常处理

驱动能ping通只是第一步,真正考验可靠性的是各种异常场景:网线热插拔、交换机重启、电磁干扰导致丢包。DM9000的PHY会自动检测链路状态变化,但驱动层也要主动处理,否则会出现“网线拔了再插回,网络不通”的尴尬局面。

我的做法是每500毫秒读取一次PHY状态寄存器,检查Link位。如果发现链路断开,就清空接收缓冲区、重置收发状态;如果发现链路恢复,就重新初始化PHY并等待自动协商完成。这个轮询逻辑放在一个定时器回调里,不影响主数据通路,实测下来网线插拔后恢复时间在1到2秒之间,完全可以接受。

6.2 收发性能实测数据

驱动稳定后,我做了几组简单的吞吐量测试。用电脑向板子连续发1000个UDP包,每包1472字节(UDP最大载荷),在无丢包情况下板子每秒能处理约800包,换算下来大概9.4Mbps;从板子往电脑发,速度稍高一些,接近11Mbps。这个成绩对GPIO模拟总线的方案来说已经不错了,毕竟瓶颈在MCU的数据搬运能力,不在芯片本身。

如果换成FSMC方式,吞吐量还能提升不少。之前在一个LPC1768项目里同样用DM9000,FSMC总线直接访问,实测UDP吞吐能达到30Mbps以上。所以如果对性能有要求,优先把硬件设计改成并行总线控制器,而不是死磕GPIO模拟。

6.3 降低CPU占用率的技巧

GPIO模拟总线的方式非常消耗CPU,因为每一位数据都需要MCU参与搬运。想降低CPU占用,有两个思路:一是把整个DM9000的寄存器访问封装好,用DMA方式读SRAM数据,前提是MCU的DMA支持外部存储器映射;二是在驱动里减少无效轮询,比如发送完成检测不要每次发完都死等,而是发完立刻返回,等中断或者定时器再确认。

我最终在项目里用的是“发送后等待标志位加入超时+接收中断置标志”的组合,整体CPU占用率在UDP持续收发时约为25%(72MHz主频下),这个数值在可接受范围内。如果希望进一步优化,就只能换FSMC或者换自带MAC的MCU了,但那就是另一个话题。

7. 一些掏心窝的调试心得

DM9000这颗芯片我前前后后调过三次,每次都有新收获。心得谈不上行业标准,但都是实打实踩坑换来的。

第一,拿到芯片先别急着写代码,把硬件电源、时钟、复位、片选每个引脚用电表量一遍。我试过因为一个引脚虚焊,导致芯片始终处于复位状态,花了一个晚上才排查出来。硬件不稳,软件写得再好也是白搭。

第二,驱动分层一定要做好。底层总线访问和应用层收发分开,不要全部揉在一个文件里。我最初的代码把所有功能堆在dm9000.c里,改一个参数要翻几百行,后面重构以后清爽很多,也方便以后移植到其他MCU。

第三,善用示波器和逻辑分析仪。怀疑时序的时候,直接在WE或者RD引脚上看波形,确认有没有毛刺、占空比对不对。虽然有些老师傅靠“猜”也能调通,但工具的帮助是不可替代的。

第四,也是我认为最实用的一条:初期调驱动的时候,把发送和接收分开验证。不要一上来就想着跑TCP/IP协议栈,先从芯片ID开始,再到PHY Link,再到UDP广播,一层层往上加。每层卡住都先假设是这层的问题,而不是去怀疑上层协议,这样定位问题会精确得多。

调网络芯片不像调LED闪烁那样立竿见影,第一次ping通对端设备的那一刻,成就感还是很足的。希望这篇记录能帮正在调DM9000的兄弟少走点弯路,如果读完有什么更好的调试思路,也欢迎在评论区交流。

本文还有配套的精品资源,点击获取

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

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

立即咨询