S3C2440裸机DM9000驱动开发:解决中断与数据接收三大难题
1. 项目概述:从零到一,让DM9000在S3C2440裸机上“活”过来
搞嵌入式开发的朋友,尤其是玩过ARM9 S3C2440这类老平台的,估计都对DM9000这颗经典的10/100M自适应以太网控制芯片不陌生。它价格便宜,接口简单(通常是8/16位总线),一度是各种开发板、工控板上的网络标配。但“经典”往往也意味着资料老旧、调试过程充满“惊喜”。这不,我最近就在为一块Micro2440开发板移植DM9000的裸机驱动,目标很简单:让板子能通过网线正确收发数据。听起来是个基础活,但实际折腾了好几天,卡在数据接收这一步,怎么都收不到正确的以太网帧,那种对着逻辑分析仪和代码反复琢磨却不得其解的郁闷,懂的都懂。好在,经过一番痛苦的排查,终于拨云见日,驱动稳定跑起来了。这篇文章,我就把自己踩过的坑、解决问题的思路以及完整的工程代码框架分享出来,希望能给后来者铺平一点道路。
这篇总结适合谁看呢?如果你正在或即将在S3C2440(或其他类似使用内存总线接口的ARM9芯片)上调试DM9000的裸机驱动,特别是遇到了初始化能过但数据收发不正常的问题,那么我遇到的这几个“坑”很可能就是你正在面对的。我会从硬件连接、内存控制器配置、中断处理,到最棘手的接收数据错位问题,一步步拆解,不仅告诉你“怎么做”,更重点解释“为什么这么做”,以及“做错了会怎样”。最终,你会得到一个经过实测、可以直接编译使用的裸机工程。
2. 核心思路与硬件框架解析
2.1 为什么选择DM9000与S3C2440的裸机驱动?
在物联网和嵌入式设备中,网络功能几乎是标配。对于像S3C2440这样没有集成以太网MAC控制器的老款ARM9芯片,外扩一个像DM9000这样的PHY+MAC二合一芯片是性价比很高的方案。所谓“裸机驱动”,就是指在不依赖任何操作系统(如Linux、uC/OS)的情况下,直接通过读写芯片的寄存器来控制它完成网络包的处理。这么做的好处是代码量小、执行路径确定、对硬件控制力强,非常适合用于学习网络协议栈底层原理、构建极简的网络设备或作为后续移植到RTOS的坚实基础。当然,挑战也正在于此:所有事情,从芯片初始化、中断响应到数据包的搬运和解析,都需要你亲手用代码构建。
2.2 DM9000与S3C2440的硬件连接要点
DM9000通常通过数据总线和地址线与CPU连接。在Micro2440开发板上,DM9000被映射到了S3C2440的BANK4地址空间。这是理解后续所有软件配置的基石。S3C2440的内存控制器将外部设备划分到不同的BANK,每个BANK有独立的片选信号(nGCS4对应BANK4)和可配置的访问时序。
关键点在于,DM9000只有两个地址线来区分命令端口和数据端口(通常对应CMD引脚)。在常见的接法下:
- 当CMD引脚为低电平时,访问的是DM9000的地址端口(索引寄存器)。
- 当CMD引脚为高电平时,访问的是DM9000的数据端口(数据寄存器)。
在硬件设计上,CMD引脚通常会连接到CPU的某根地址线(比如ADDR2)。因此,我们在软件中就需要定义两个不同的内存地址来访问这两个端口。在我的工程中,定义如下:
#define DM9000_INDEX (*((volatile unsigned short *) 0x20000300)) // CMD=0
#define DM9000_DATA (*((volatile unsigned short *) 0x20000304)) // CMD=1
这里 0x20000000 是BANK4的起始地址, 0x300 和 0x304 的偏移量就是由CMD引脚连接的地址线(ADDR2)决定的。 0x300 对应ADDR2=0, 0x304 对应ADDR2=1。 务必根据你的实际原理图核对这个地址偏移! 这是第一个容易出错的地方。
3. 驱动实现中的三大“拦路虎”与解决方案
3.1 问题一:MMU未开启导致中断完全失灵
我的驱动最初版本,初始化流程看起来一切正常:能正确读取到DM9000的VID(0x9000)和PID(0x9000),说明芯片通信基本没问题。但是,一旦我使能接收中断并等待,程序就像石沉大海,永远进不去中断服务程序。我用示波器去测量DM9000的中断输出引脚,明明已经看到了跳变,但CPU就是没反应。
排查过程与核心原因: 首先怀疑是中断控制器(S3C2440的VIC)配置错误。我反复检查了中断号(DM9000通常连接EINT7)、触发模式(边沿触发)、中断使能位,都没有问题。然后我注意到一个关键细节:我的程序是通过J-Link直接加载到SDRAM(地址0x30000000)中运行的,而S3C2440的异常向量表(包括中断向量)默认在0x0地址。当发生中断时,CPU会跳转到0x18地址(IRQ异常入口)执行指令。如果0x0地址开始的内存没有有效的指令(比如是NOR Flash或未初始化),或者MMU没有正确地将物理地址映射到对应的虚拟地址,CPU就会跑飞。
解决方案:开启并正确配置MMU。 对于裸机程序,尤其是运行在SDRAM中的程序, 即使你不需要虚拟内存管理功能,也强烈建议开启MMU 。其主要目的不是为了内存保护或虚拟地址,而是为了控制内存区域的访问属性(Cache和Buffer),以及进行简单的地址映射,确保异常向量表可访问。
我添加了如下MMU初始化代码,重点在于设置BANK4的映射:
void MMU_Init(void)
{
// ... 其他初始化代码,如设置域访问控制等
// 关键:映射BANK4 (0x20000000 - 0x27FFFFFF) 为无缓存无缓冲模式
MMU_SetMTT(0x20000000, 0x27f00000, 0x20000000, RW_NCNB); // RW_NCNB: Read/Write, Non-cached, Non-buffered.
// 映射SDRAM区域 (0x30000000 - 0x33FFFFFF) 为缓存模式以提高性能
MMU_SetMTT(0x30000000, 0x33f00000, 0x30000000, RW_CB);
// 映射0x0开始的异常向量表区域(可能是Nor Flash或Steppingstone)
MMU_SetMTT(0x00000000, 0x00f00000, 0x00000000, RW_NCNB);
// ... 使能MMU
}
这里的 RW_NCNB 属性至关重要。对于像DM9000这类外部设备寄存器, 绝对不能使用缓存 。因为缓存会导致CPU读写的是缓存中的数据副本,而不是真实的设备寄存器,使得驱动完全无法工作。设置成 Non-cached, Non-buffered 后,每一次读写操作都会直接作用在总线上,确保了与DM9000通信的实时性和正确性。完成MMU设置并开启后,中断立刻就能正常触发了。
实操心得 :在ARM裸机开发中,当外设(特别是使用中断的外设)表现异常时,除了检查外设本身的配置,一定要从CPU核心的视角审视:异常向量表是否可到达?MMU/MPU的配置是否阻止了对外设地址空间的访问或引入了缓存问题?把外设所在的内存区域设置为
Non-cached是一个非常重要的原则。
3.2 问题二:读取DM9000芯片ID失败或错误
在解决了中断问题后,我回到了最初的芯片检测阶段。有时会发现读回来的VID/PID不是预期的0x9000/0x9000,而是0xffff或者一些随机值。这直接导致初始化函数失败。
原因分析与解决方案:
-
基地址错误 :如上文所述,
DM9000_INDEX和DM9000_DATA的地址必须严格对应硬件原理图中CMD引脚连接的地址线。我最初参考的某个例程使用了0x20000000和0x20000004,结果就是无法正确读写。使用0x20000300和0x20000304后问题解决。 务必用万用表或查看原理图确认 。 -
内存控制器时序配置不当 :即使地址对了,如果CPU访问BANK4的时序与DM9000的要求不匹配,也会导致读写失败。S3C2440的BANK4时序由
BWSCON和BANKCON4寄存器控制。DM9000是16位总线设备,我们需要正确设置位宽、等待周期等。// 设置BANK4为16位总线宽度,使能WAIT,设置访问周期 BWSCON &= ~(0xf<<16); // 清除旧设置 BWSCON |= (1<<16); // 设置位宽为16位 (DW4=01) BANKCON4 = (0x1<<13) | (0x1<<11) | (0x3<<8) | (0x1<<6); // Tacs=1clk, Tcos=1clk, Tacc=6clk, Toc=1clk...这里的
Tacc(访问周期)设置尤为重要,它需要满足DM9000数据手册上的读/写周期要求。如果设置过短,可能导致数据采样不稳定。我参考了开发板厂商的Linux内核BSP包中的设置,这是一个比较可靠的值。 -
MMU缓存问题(再次强调) :如果BANK4的地址空间在MMU中被错误地配置为缓存模式(
RW_CB),那么第一次读取ID可能正确(因为缓存是空的),但后续操作可能会因为缓存一致性问题导致错乱。确保其映射属性为RW_NCNB。
3.3 问题三:能进中断但接收的数据全是乱码
这是最折磨人的一个问题。现象是:网络连接指示灯正常,发送数据包似乎也成功(用Wireshark能在电脑端抓到ARP请求包),接收中断也能触发。但是,从DM9000的接收缓冲区读上来的数据,经过校验和检查总是失败,或者解析出来的MAC地址、协议类型全是错的。
根本原因:误读了DM9000_MRCMD寄存器 DM9000的数据接收流程一般是:
- 进入接收中断后,读取中断状态寄存器
ISR,判断是否为接收中断。 - 如果是,则读取
MRCMDX寄存器(地址0xF0)来获取接收到的数据帧的第一个字(Word)。这个字包含了接收状态信息。 - 随后, 继续读取
MRCMDX寄存器 ,会自动依次读出后续的数据(长度、实际数据包内容)。注意,这里的关键是 整个接收数据的读取过程,都是通过连续读取MRCMDX这一个寄存器地址来完成的 。芯片内部有一个指针,每次读操作后会自动递增,指向下一个字。
我的错误代码片段如下:
// 错误的读法
rx_status = DM9000_ReadReg(DM9000_MRCMDX); // 读状态
rx_len = DM9000_ReadReg(DM9000_MRCMDX); // 读长度
for(i=0; i< (rx_len+1)/2; i++) {
*rx_data++ = DM9000_ReadReg(DM9000_MRCMD); // 这里错了!用了另一个寄存器
}
我错误地认为 DM9000_MRCMDX (0xF0)是用于读取状态和长度的,而 DM9000_MRCMD (0xF2)是用于读取实际数据的。实际上,在启动接收数据读取流程后, 必须始终读取同一个寄存器地址( MRCMDX ) ,直到整个数据包读完。如果我中途切换到了 MRCMD 寄存器,内部的数据指针就乱了,后续读上来的自然全是无效数据。
正确的接收数据函数核心逻辑如下:
unsigned short DM9000_Read_Packet(unsigned char *buf)
{
unsigned short status, len;
unsigned short *rbuf = (unsigned short *)buf;
unsigned short i;
// 1. 选择MRCMDX寄存器
DM9000_INDEX = DM9000_MRCMDX;
// 2. 读取第一个字(状态)
status = DM9000_DATA;
// 3. 读取第二个字(长度)
len = DM9000_DATA;
// 4. 连续读取剩余的数据(长度单位是字节,寄存器读操作单位是字)
for(i = 0; i < (len + 1) >> 1; i++) {
rbuf[i] = DM9000_DATA; // 注意:这里仍然是读取DM9000_DATA,但芯片内部指针在自动递增
}
// 5. 读取完成后,丢弃可能存在的填充字(如果长度是奇数)
if (len & 0x01) {
(void)DM9000_DATA; // 读一次丢弃
len++; // 长度补正
}
return len;
}
避坑指南 :仔细阅读数据手册!对于DM9000这类通过“读指针自动递增”来连续读取数据的设备,一定要确认整个读取流中不能随意改变目标寄存器。最好的做法是,在读取数据包的函数里,一开始就锁死要操作的寄存器索引(写入
DM9000_INDEX),然后后续所有DM9000_DATA的读操作就都会作用在该寄存器对应的数据缓冲区上。
4. 完整的驱动实现与代码结构
4.1 工程文件结构
一个清晰的代码结构有助于管理和调试。我的工程主要包含以下文件:
dm9000_driver/
├── inc/
│ ├── dm9000.h // DM9000寄存器定义、驱动函数声明
│ ├── s3c2440.h // S3C2440芯片寄存器定义
│ └── net_config.h // 网络配置(MAC地址、IP地址等)
├── src/
│ ├── dm9000.c // DM9000驱动核心实现(初始化、收发、中断)
│ ├── startup.s // 启动文件、中断向量表
│ ├── mmu.c // MMU初始化代码
│ ├── interrupt.c // 中断控制器初始化与管理
│ └── main.c // 主程序,测试网络收发
└── project.uvproj // Keil MDK工程文件
4.2 核心驱动函数详解
1. 初始化流程 DM9000_Init() 初始化的顺序和关键步骤不能错:
void DM9000_Init(void)
{
// 1. 硬件复位(通过GPIO控制DM9000的RST引脚)
DM9000_HW_Reset();
// 2. 软件复位(写入NCR寄存器)
DM9000_WriteReg(DM9000_NCR, NCR_RST);
delay_ms(10); // 等待复位完成
// 3. 验证芯片ID
if((DM9000_ReadReg(DM9000_VIDL) | (DM9000_ReadReg(DM9000_VIDH) << 8)) != DM9000_VID) {
// 打印错误,ID验证失败
return;
}
// 同样检查PID
// 4. 配置GPCR寄存器,使能内部PHY
DM9000_WriteReg(DM9000_GPCR, GPCR_GEP_CNTL);
// 5. 配置GPR寄存器,选择PHY
DM9000_WriteReg(DM9000_GPR, 0);
// 6. 配置物理层(PHY)
DM9000_Phy_Write(0, 0x2100); // 重启自动协商
delay_ms(1000); // 等待协商完成
// ... 读取PHY状态寄存器,确认连接速度和双工模式
// 7. 配置MAC层
// 设置MAC地址
DM9000_WriteReg(DM9000_PAR, mac_addr[0]); // PAR0
DM9000_WriteReg(DM9000_PAR+1, mac_addr[1]);
// ... 设置PAR1-PAR5
// 设置接收控制寄存器(RCR):使能广播、多播、接收错误包等
DM9000_WriteReg(DM9000_RCR, RCR_DIS_LONG | RCR_DIS_CRC | RCR_RXEN);
// 设置发送控制寄存器(TCR)
DM9000_WriteReg(DM9000_TCR, 0);
// 8. 清除所有中断状态
DM9000_WriteReg(DM9000_ISR, 0xFF);
// 9. 使能接收中断(IMR)
DM9000_WriteReg(DM9000_IMR, IMR_PAR | IMR_PRM | IMR_PTM);
// 10. 激活设备(配置NCR寄存器)
DM9000_WriteReg(DM9000_NCR, NCR_WAKEEN | NCR_FDX);
}
这个流程涵盖了从硬件复位到网络功能就绪的全过程。其中,PHY的配置和自动协商等待时间非常重要,协商不成功会导致链路不通。
2. 数据发送函数 DM9000_Send_Packet() 发送相对接收简单,但要注意数据对齐和长度计算:
void DM9000_Send_Packet(unsigned char *buf, unsigned short len)
{
// 1. 检查发送缓冲区是否就绪(检查NSR寄存器)
while(!(DM9000_ReadReg(DM9000_NSR) & (NSR_TX1READY | NSR_TX2READY)));
// 2. 写入发送长度(两个字节)
DM9000_WriteReg(DM9000_TXPLL, len & 0xff);
DM9000_WriteReg(DM9000_TXPLH, (len >> 8) & 0xff);
// 3. 选择MWCMD寄存器,准备写入数据
DM9000_INDEX = DM9000_MWCMD;
// 4. 将数据循环写入数据端口
unsigned short *pbuf = (unsigned short *)buf;
unsigned short word_len = (len + 1) >> 1; // 计算需要写入的字数
for(int i=0; i<word_len; i++) {
DM9000_DATA = pbuf[i];
}
// 5. 触发发送(写入TCR寄存器,选择发送缓冲区0或1)
DM9000_WriteReg(DM9000_TCR, TCR_TXREQ0); // 使用发送缓冲区0
}
注意事项 :DM9000有两个发送缓冲区(TX0和TX1)。上述代码使用了TX0。在连续发送时,可以交替使用两个缓冲区以提高效率。发送后,需要通过中断或轮询
ISR寄存器的PTM位来判断发送是否完成。
3. 中断服务程序(ISR)框架 中断处理是驱动稳定性的关键。我的ISR框架如下:
void __irq DM9000_IRQ_Handler(void)
{
unsigned char isr_status;
// 1. 读取DM9000的中断状态寄存器
isr_status = DM9000_ReadReg(DM9000_ISR);
// 2. 处理接收中断
if(isr_status & ISR_PRS) {
// 调用接收数据包函数
DM9000_Receive_Packet();
// 清除接收中断标志
DM9000_WriteReg(DM9000_ISR, ISR_PRS);
}
// 3. 处理发送中断
if(isr_status & ISR_PTS) {
// 可以在这里处理发送完成后的工作,如释放缓冲区
// 清除发送中断标志
DM9000_WriteReg(DM9000_ISR, ISR_PTS);
}
// 4. 处理其他中断(如链路状态变化)
if(isr_status & ISR_LNKCHG) {
// 重新读取PHY状态,更新连接信息
// 清除链路变化中断标志
DM9000_WriteReg(DM9000_ISR, ISR_LNKCHG);
}
// 重要:清除S3C2440 VIC中的中断标志位
VIC0ADDRESS = 0; // 写任意值清除VIC的向量地址寄存器
EXTINTPND = (1<<7); // 清除外部中断挂起位(EINT7)
EINTPEND = (1<<7);
}
在中断处理中, 一定要先读取DM9000的 ISR 寄存器来判断中断源 ,然后处理相应事件,并 在退出前清除DM9000和CPU中断控制器两级的中断标志 ,否则会引发中断持续触发,导致系统死锁。
5. 调试技巧与常见问题排查实录
调试硬件驱动,尤其是网络驱动,需要软硬件结合。以下是我总结的排查清单:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全读不到芯片ID (0xFFFF) | 1. 电源或复位不正常。 2. 总线连接错误(数据线、地址线)。 3. 片选信号(nGCS4)未使能或时序不对。 4. 寄存器地址定义错误。 |
1. 用万用表/示波器检查VCC、晶振、复位引脚电平。 2. 用逻辑分析仪抓取读ID时的总线波形,看地址、数据、片选、读使能信号是否正常。 3. 检查S3C2440内存控制器 BWSCON 和 BANKCON4 寄存器配置。 4. 核对 DM9000_INDEX 地址与原理图CMD引脚连接是否一致。 |
| 能读到ID但中断不触发 | 1. 中断线(EINT7)未连接或配置错误。 2. MMU未开启或映射错误,导致CPU无法跳转到中断向量。 3. DM9000中断输出未使能(IMR寄存器)。 4. 中断标志未清除,导致后续中断被屏蔽。 |
1. 用示波器测量DM9000的INT引脚和CPU的EINT7引脚,在发送数据包时是否有跳变。 2. 检查MMU配置,确保中断向量表所在区域(0x0)和DM9000所在区域(0x20000000)映射正确且属性为 Non-cached 。 3. 检查 IMR 寄存器是否已使能接收中断( IMR_PAR 等)。 4. 在ISR中确认清除了DM9000的 ISR 和S3C2440的 EINTPEND 等寄存器。 |
| 中断能触发,但接收数据错误(CRC错、长度错) | 1. 最可能:误读了 MRCMD 寄存器 ,导致数据指针错乱。 2. 接收缓冲区溢出(FIFO溢出)。 3. 内存访问时序( Tacc )设置过快,数据采样不稳定。 4. 数据对齐问题(字节序)。 |
1. 重点检查接收数据函数 ,确保从读取状态到读取完整个数据包, 只对 MRCMDX 寄存器进行一次索引写入 ,后续全部读取数据端口。 2. 检查 RCR 寄存器,是否使能了流控或设置了合适的接收阈值。 3. 适当增加 BANKCON4 中的 Tacc 等待周期,比如从6个时钟增加到8个。 4. 确认网络数据包的字节序(DM9000是16位接口,数据是Little-Endian)。 |
| 能发送,对方收不到;或对方能发,自己收不到 | 1. 物理链路未连通(网线、交换机)。 2. PHY自动协商失败。 3. MAC地址设置错误。 4. 发送的数据包格式错误(如以太网帧校验和CRC错误)。 |
1. 观察DM9000的Link LED指示灯是否常亮。 2. 读取PHY状态寄存器(1Bh),检查Link Status、Speed、Duplex位。 3. 用Wireshark抓包,看发送的数据包MAC源地址是否正确,以及是否有任何数据包从板卡发出。 4. 发送一个简单的ARP请求包,并用Wireshark验证其格式。 |
| 运行一段时间后死机或不稳定 | 1. 中断嵌套或中断标志未清除干净,导致持续中断。 2. 内存越界,破坏了堆栈或关键数据。 3. 接收缓冲区未及时处理,导致溢出。 |
1. 在ISR入口处禁用全局中断,处理完再开启。确保所有中断标志位(DM9000和CPU)都已清除。 2. 检查数组边界,特别是接收数据缓冲区的长度。 3. 优化代码,确保接收中断服务程序执行时间尽可能短,或者使用“中断+轮询”的方式处理接收队列。 |
一个关键的调试工具:Wireshark 在电脑端用Wireshark抓包是调试网络驱动的“眼睛”。你可以:
- 验证发送 :看你的板卡发出的ARP、ICMP包格式是否正确,源MAC地址对不对。
- 验证接收 :从电脑Ping板卡的IP,在Wireshark里能看到电脑发出的ARP请求和ICMP Echo请求。结合板卡的调试串口输出,可以判断是否收到了包,以及收到的包内容是否正确。
- 分析错误 :如果收到包但校验和错误,Wireshark会标记为“Malformed Packet”。
最后,我将整理好的源代码工程上传到了GitHub(此处应替换为你的实际仓库链接)。工程基于Keil MDK开发,在Micro2440开发板上测试通过,实现了基础的ARP应答和ICMP Ping回复功能。你可以以此为起点,去实现更完整的协议栈,如UDP、TCP。驱动硬件就像解谜,每一次问题的解决都是对系统理解的一次深化。希望我的这些踩坑记录,能帮你节省一些时间。如果在实现过程中遇到新的问题,欢迎在评论区交流讨论。
更多推荐

所有评论(0)