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或者一些随机值。这直接导致初始化函数失败。

原因分析与解决方案:

  1. 基地址错误 :如上文所述, DM9000_INDEX DM9000_DATA 的地址必须严格对应硬件原理图中CMD引脚连接的地址线。我最初参考的某个例程使用了 0x20000000 0x20000004 ,结果就是无法正确读写。使用 0x20000300 0x20000304 后问题解决。 务必用万用表或查看原理图确认

  2. 内存控制器时序配置不当 :即使地址对了,如果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包中的设置,这是一个比较可靠的值。

  3. MMU缓存问题(再次强调) :如果BANK4的地址空间在MMU中被错误地配置为缓存模式( RW_CB ),那么第一次读取ID可能正确(因为缓存是空的),但后续操作可能会因为缓存一致性问题导致错乱。确保其映射属性为 RW_NCNB

3.3 问题三:能进中断但接收的数据全是乱码

这是最折磨人的一个问题。现象是:网络连接指示灯正常,发送数据包似乎也成功(用Wireshark能在电脑端抓到ARP请求包),接收中断也能触发。但是,从DM9000的接收缓冲区读上来的数据,经过校验和检查总是失败,或者解析出来的MAC地址、协议类型全是错的。

根本原因:误读了DM9000_MRCMD寄存器 DM9000的数据接收流程一般是:

  1. 进入接收中断后,读取中断状态寄存器 ISR ,判断是否为接收中断。
  2. 如果是,则读取 MRCMDX 寄存器(地址 0xF0 )来获取接收到的数据帧的第一个字(Word)。这个字包含了接收状态信息。
  3. 随后, 继续读取 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。驱动硬件就像解谜,每一次问题的解决都是对系统理解的一次深化。希望我的这些踩坑记录,能帮你节省一些时间。如果在实现过程中遇到新的问题,欢迎在评论区交流讨论。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐