1. DM9000网络控制器硬件架构与接口特性

DM9000是一款高度集成的以太网控制器芯片,其核心价值在于将MAC(媒体访问控制)层与PHY(物理层)功能封装于单一芯片内,显著降低了嵌入式系统网络接口的设计复杂度与BOM成本。该芯片支持10/100Mbps自适应速率,并内置4KB双端口SRAM作为数据收发缓冲区。在正点原子战舰V3开发板上,DM9000被用作STM32F103ZET6微控制器的以太网外设,通过FSMC(灵活静态存储控制器)总线实现高速数据交换。

1.1 硬件连接拓扑与关键引脚定义

战舰V3开发板采用16位数据总线模式连接DM9000,这一配置由硬件设计决定,而非软件可动态切换。关键引脚的物理连接关系如下:

  • 数据总线 :DM9000的D0-D15直接连接至STM32的FSMC_D0-D15。
  • 地址/命令线 :DM9000的CMD引脚连接至STM32的FSMC_A7。此连接是区分“命令”与“数据”的核心机制。当CMD为低电平时,FSMC_A7为0,表示向DM9000寄存器写入命令;当CMD为高电平时,FSMC_A7为1,表示向DM9000写入或读取数据。这种设计模仿了标准SRAM的访问时序,使驱动开发逻辑清晰。
  • 片选信号 :DM9000的CS引脚连接至STM32的FSMC_NE2,对应FSMC Bank2。Bank2的基地址为0x60000000,而DM9000的寄存器基地址计算为 0x60000000 + (0x6400 << 1) ,即0x6000C800。此处的 0x6400 来源于FSMC Bank2的地址映射偏移量,而左移1位是因为16位总线模式下,地址线存在一位右对齐(A0被忽略),这是STM32参考手册中明确规定的FSMC寻址规则。
  • 中断信号 :DM9000的INT引脚(第34脚)连接至STM32的PG6。根据开发板原理图,R66电阻被焊接,将DM9000的EDCK引脚(第20脚)拉高,从而强制INT引脚为低电平有效。这是一个极易被忽略但至关重要的细节,若在软件中错误地配置为上升沿触发,将导致中断永远无法响应。
  • 复位信号 :DM9000的RST引脚连接至STM32的PA7,由软件可控。

1.2 寄存器空间与内存模型

DM9000的内部寄存器空间和数据缓冲区均通过同一组地址/数据总线进行访问,其逻辑结构完全依赖于CMD信号的状态。整个芯片的16KB内部SRAM被划分为两个主要区域:
- 发送缓冲区(TX SRAM) :起始地址为0x0000,大小为3KB(0x0000-0x0BFF)。所有待发送的数据包均需写入此区域。
- 接收缓冲区(RX SRAM) :起始地址为0x0C00,大小为13KB(0x0C00-0x3FFF)。所有接收到的数据包均被DMA引擎自动写入此区域。

这种划分并非物理隔离,而是逻辑上的地址映射。当向发送缓冲区写入数据时,若地址超出0x0BFF,地址指针会自动回绕至0x0000;同理,从接收缓冲区读取数据时,若地址超出0x3FFF,指针也会回绕至0x0C00。这一特性要求驱动程序在进行数据搬运时必须严格检查地址边界,否则将导致数据覆盖。

2. DM9000驱动程序核心逻辑解析

DM9000驱动程序( dm9000.c/h )是LWIP协议栈在无操作系统环境下运行的基石。其核心目标是为上层网络协议提供一个稳定、可靠、符合标准的数据链路层接口。驱动程序的编写必须严格遵循DM9000数据手册中定义的寄存器操作序列,任何时序或顺序的偏差都可能导致芯片工作异常。

2.1 初始化流程:从硬件复位到功能就绪

dm9000_init() 函数是驱动程序的入口点,其执行过程是一个严谨的、多阶段的硬件初始化流水线:

  1. GPIO与中断配置 :首先,配置PA7(复位)、PG6(中断)为推挽输出和浮空输入模式。中断线PG6被映射至EXTI_Line6,并配置为下降沿触发(因硬件为低电平有效)。中断优先级被设置为 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02 ,确保其能及时响应网络事件而不被其他高优先级任务阻塞。

  2. FSMC Bank2配置 :这是最关键的一步。FSMC需要被配置为与DM9000的时序完美匹配。配置参数包括:

    • FSMC_DataAddressMux_DISABLE :禁用地址/数据复用,因DM9000使用独立的CMD线。
    • FSMC_MemoryType_SRAM :内存类型为SRAM。
    • FSMC_MemoryDataWidth_16b :数据总线宽度为16位。
    • FSMC_BurstAccessMode_DISABLE :禁用突发访问。
    • FSMC_WaitSignalPolarity_LOW :等待信号极性为低。
    • FSMC_AsynchronousWait_DISABLE :禁用异步等待。
    • FSMC_WrapMode_DISABLE :禁用回绕模式。
    • FSMC_WaitSignal_DISABLE :禁用等待信号。
    • FSMC_WriteOperation_ENABLE :使能写操作。
    • FSMC_WaitTiming FSMC_HoldTiming :这些时序参数需根据DM9000数据手册中的 tAS , tWP , tWH , tDS , tDH 等指标精确计算。例如, FSMC_SetupTime 通常设置为2个HCLK周期, FSMC_WaitSetupTime 设置为3个HCLK周期,以确保满足DM9000最严格的建立和保持时间要求。
  3. 芯片软复位与ID校验 :通过向 DM9000_NCR (网络控制寄存器)的Bit0写入1来触发软复位。复位后,必须执行至少100μs的延时(实际代码中常使用 delay_us(100) ),以确保内部状态机完全稳定。随后,读取 DM9000_VIDL (厂商ID低字节)和 DM9000_VIDH (厂商ID高字节)寄存器,校验其值是否为 0x90 0x09 。若校验失败,则表明硬件连接或供电存在问题,初始化应立即终止并返回错误码。

  4. PHY配置与自动协商启动 :DM9000内部集成了PHY,其工作模式由 DM9000_NSR (网络状态寄存器)和 DM9000_NCR 寄存器控制。驱动程序默认启用 PHY_AUTO_NEGOTIATION (自动协商)模式,这通过向PHY寄存器0x00(控制寄存器)的Bit12写入1来实现。自动协商是建立最优链路(如100Mbps全双工)的前提,它允许DM9000与远端交换机或PC网卡动态协商出双方都支持的最佳速率和双工模式。

2.2 寄存器读写:底层通信的原子操作

所有与DM9000的交互都归结为对特定地址的读写操作。驱动程序提供了 dm9000_read_reg() dm9000_write_reg() 两个基础函数,它们是整个驱动的“呼吸器官”。

  • dm9000_read_reg(u8 reg) :该函数首先向 DM9000_REG (寄存器索引寄存器,地址0x00)写入要读取的目标寄存器地址 reg ,然后从 DM9000_DATA (数据寄存器,地址0x02)读取其值。这是一个典型的“先写地址,再读数据”的两步操作。

  • dm9000_write_reg(u8 reg, u16 value) :该函数同样先向 DM9000_REG 写入目标寄存器地址 reg ,再向 DM9000_DATA 写入 value

对于PHY寄存器的访问则更为复杂,因为它需要通过DM9000的“中介”寄存器来完成:
- 写PHY寄存器 :首先向 DM9000_EPAR (外部PHY地址寄存器)写入目标PHY寄存器的地址(如0x00),再向 DM9000_EPCR (外部PHY控制寄存器)的Bit0写入1以启动写操作,最后向 DM9000_EPDRH / DM9000_EPDL (外部PHY数据寄存器高/低字节)写入数据。写操作完成后,必须清除 DM9000_EPCR 的Bit0。
- 读PHY寄存器 :同样先写 DM9000_EPAR ,再向 DM9000_EPCR 的Bit1写入1以启动读操作,然后从 DM9000_EPDRH / DM9000_EPDL 读取数据。读操作完成后,也必须清除 DM9000_EPCR 的Bit1。

这种分步操作的设计,是为了保证在复杂的总线环境下,对PHY的访问能够被DM9000芯片正确识别和转发。

3. 数据包收发机制:DMA与状态机的协同

DM9000的数据包收发并非简单的内存拷贝,而是一套由硬件状态机驱动、软件精确控制的精密流程。理解其内在机制是编写健壮驱动的关键。

3.1 发送过程:从应用层到物理线缆

发送一个数据包涉及三个核心步骤,每一步都依赖于前一步的成功完成:

  1. 数据装载 :调用 dm9000_send_packet(u8 *buf, u16 len) 。该函数首先关闭网卡中断( dm9000_write_reg(DM9000_IMR, 0x8000) ),以防止在数据搬运过程中被接收中断打断。接着,它将 buf 指向的应用层数据,通过FSMC总线,逐字(word)写入DM9000的TX SRAM(起始地址0x0000)。写入完成后,将数据长度 len 分别写入 DM9000_TXPLL (低字节)和 DM9000_TXPLH (高字节)寄存器。

  2. 启动发送 :向 DM9000_TCR (发送控制寄存器)的Bit0写入1,触发硬件发送引擎。此时,DM9000会自动从TX SRAM的0x0000地址开始,将数据打包成符合IEEE 802.3标准的以太网帧(包含7字节前导码、1字节帧起始定界符、6字节目的MAC、6字节源MAC、2字节类型/长度、有效载荷、4字节FCS),并通过PHY发送到物理线缆上。

  3. 状态轮询与清理 :发送启动后,软件进入一个轮询循环,持续读取 DM9000_ISR (中断状态寄存器)的Bit1(TX_PKT_DONE)。当该位被硬件置1时,表示数据包已成功发送完毕。此时,软件必须清除该中断标志位(向 DM9000_ISR 写入0x02),然后重新使能网卡中断( dm9000_write_reg(DM9000_IMR, 0x8002) ),恢复正常的中断服务。

值得注意的是,DM9000支持双缓冲发送(Packet 1和Packet 2),但在战舰V3的无OS移植中,驱动仅实现了单包发送。这意味着在发送完一个包后,必须等待其完全结束才能发送下一个包,这在高吞吐量场景下会成为瓶颈,但对于教学和一般应用已足够。

3.2 接收过程:从物理线缆到应用层

接收过程比发送更为复杂,因为数据是异步到达的,且每个数据包的长度是未知的。DM9000通过一个精巧的四字节包头(Packet Header)来解决这个问题:

  1. 中断触发与首字节读取 :当DM9000接收到一个完整的以太网帧并将其存入RX SRAM后,会通过PG6引脚产生一个低电平中断。在中断服务函数 EXTI9_5_IRQHandler() 中,首先清除EXTI_Line6的挂起位,然后调用 dm9000_receive_packet()

  2. 包头解析 dm9000_receive_packet() 首先向 DM9000_MRCMDX (内存读命令X)寄存器写入0x01,这是一个特殊的命令,用于从RX SRAM的当前读指针位置读取一个字节。读取的第一个字节即为包头的第一个字节。根据DM9000规范,该字节的Bit0必须为1,表示这是一个“已接收”的有效数据包。如果为0,则表示数据包未就绪或已损坏,需丢弃并重试。

  3. 状态与长度获取 :在确认第一个字节为0x01后,驱动程序向 DM9000_MRCMD (内存读命令)寄存器写入0x00。这是一个自增读命令,意味着后续每次读取都会使读指针自动递增。紧接着,连续两次读取 DM9000_DATA 寄存器:

    • 第一次读取得到的是 状态字节(Status Byte) ,其高8位包含了接收状态信息(如CRC错误、长帧、短帧等),低8位为0。
    • 第二次读取得到的是 数据包长度的低字节(Length Low)
    • 再次读取得到的是 数据包长度的高字节(Length High)
      这样,一个16位的完整数据包长度( len = (high_byte << 8) | low_byte )就被获取到了。
  4. 数据提取与交付 :在获取长度后,驱动程序会再次使用 DM9000_MRCMD 命令,连续读取 len 个字节,将真正的以太网帧有效载荷(不包括前导码、FCS等)提取出来。最后,这些原始字节被封装进LWIP的 pbuf 结构体中,并通过 netif->input(pbuf, netif) 回调函数提交给LWIP协议栈进行进一步处理。

4. 关键寄存器详解与实战调试技巧

在驱动开发与调试过程中,对关键寄存器的深入理解是定位问题的“显微镜”。以下是对几个核心寄存器的深度剖析。

4.1 中断管理寄存器(IMR)

DM9000_IMR (中断屏蔽寄存器,地址0x2C)是一个16位寄存器,每一位对应一种中断源。其位定义如下:
- Bit0: IMR_PR —— 接收包中断(Packet Received)
- Bit1: IMR_PT —— 发送包中断(Packet Transmitted)
- Bit2: IMR_RO —— 接收溢出中断(Receive Overflow)
- Bit3: IMR_RU —— 接收更新中断(Receive Update)
- Bit4: IMR_RU —— 发送更新中断(Transmit Update)
- Bit5: IMR_LNKCHG —— 链路状态改变中断(Link Change)

在战舰V3的驱动中,初始化时通常将 IMR 设置为 0x8002 ,即只使能接收包中断(Bit1)和链路状态改变中断(Bit15)。Bit15( IMR_LNKCHG )是一个非常有用的调试工具。当网线插拔时,该位会被置位,你可以在中断服务函数中打印一条调试信息,从而快速验证硬件连接和中断路径是否畅通。

4.2 网络状态寄存器(NSR)

DM9000_NSR (网络状态寄存器,地址0x30)是诊断网络物理层连接状态的“仪表盘”。其关键位定义为:
- Bit0: NSR_LINKST —— 链路状态(Link Status)。为1表示链路已建立(Link Up),为0表示链路断开(Link Down)。
- Bit1: NSR_SPEED —— 速度指示(Speed Indicator)。为1表示10Mbps,为0表示100Mbps。
- Bit2: NSR_DUPLEX —— 双工模式(Duplex Mode)。为1表示全双工(Full Duplex),为0表示半双工(Half Duplex)。

dm9000_get_speed() 函数中,驱动程序会轮询 NSR 寄存器的Bit0,直到其变为1,才认为自动协商完成。这是一个典型的“忙等待”(busy-waiting)操作,必须配合超时机制,否则一旦网线未连接,程序将在此处无限死循环。因此,代码中会设置一个计数器(如 timeout = 0xFFFF ),在每次轮询后递减,当计数器为0时强制退出。

4.3 接收状态寄存器(RSR)

DM9000_RSR (接收状态寄存器,地址0x32)是判断接收到的数据包是否有效的“法官”。其关键位定义为:
- Bit0: RSR_FRAMERR —— 帧错误(Frame Error)
- Bit1: RSR_CRCERR —— CRC校验错误(CRC Error)
- Bit2: RSR_RUNT —— 短帧错误(Runt Frame, < 64 bytes)
- Bit3: RSR_LONG —— 长帧错误(Long Frame, > 1518 bytes)

dm9000_receive_packet() 的末尾,驱动程序会读取 RSR 寄存器,并检查上述各位。如果任意一位被置1,则说明该数据包在传输过程中出现了错误,应立即丢弃,并调用 dm9000_reset() 进行软复位,以清除DM9000内部可能存在的错误状态。这是一个非常重要的容错机制,能有效防止因单个错误包导致整个网络栈崩溃。

5. LWIP无OS移植的工程实践要点

将LWIP协议栈移植到无操作系统的STM32平台,本质上是将一个为RTOS或Linux设计的、高度并发的网络协议栈,“降维”到一个单线程、事件驱动的裸机环境中。这要求开发者对LWIP的内部工作机制有深刻的理解,并做出一系列关键的适配。

5.1 网络接口(netif)的定制化实现

struct netif 是LWIP中网络接口的抽象。在无OS移植中, netif input output 函数指针必须被正确绑定:
- netif->input :被设置为 dm9000_input ,这是一个包装函数,其内部调用 dm9000_receive_packet() 获取 pbuf ,然后调用 ethernet_input() pbuf 提交给LWIP的ARP和IP层。
- netif->output :被设置为 dm9000_output ,这是一个包装函数,其内部调用 ethernet_output() 将LWIP生成的IP包封装成以太网帧,最终调用 dm9000_send_packet() 将帧发送出去。

最关键的适配点在于 netif->linkoutput 。在标准移植中,它直接调用 dm9000_send_packet() 。但在无OS环境下,由于 dm9000_send_packet() 是一个阻塞函数,它会一直等待发送完成。这意味着,在 tcp_output() 等函数中调用 netif->linkoutput() 时,整个TCP/IP栈的执行都会被挂起。这是一种可以接受的权衡,它牺牲了并发性,换取了代码的简洁性和确定性。

5.2 内存管理与pbuf结构

pbuf (packet buffer)是LWIP的核心数据结构,用于在各协议层之间高效地传递数据包。在无OS移植中, pbuf 的分配策略至关重要。战舰V3的代码通常采用 PBUF_POOL 模式,即预先在内存中分配一个固定大小的 pbuf 池(例如32个),每个 pbuf 又关联一个固定大小的数据缓冲区(例如1536字节)。

dm9000_receive_packet() 中,当成功接收到一个数据包后,驱动会调用 pbuf_alloc(PBUF_RAW, len, PBUF_POOL) 从池中申请一个 pbuf 。如果申请失败( pbuf == NULL ),则意味着 pbuf 池已耗尽,此时必须丢弃该数据包,并记录一个错误计数器。这是一个典型的“背压”(back pressure)机制,它通过丢包来防止内存耗尽,而不是让系统崩溃。

5.3 定时器与协议栈心跳

LWIP的许多协议(如ARP、TCP、ICMP)都依赖于定时器来驱动其状态机。在无OS环境下,没有 sys_timeout() 这样的系统API可用。解决方案是实现一个简单的、基于SysTick的软件定时器队列。在 SysTick_Handler() 中断服务函数中,维护一个全局的毫秒计数器 sys_now ,并在主循环中定期(例如每10ms)调用 sys_check_timeouts() 。该函数会遍历所有注册的超时事件,检查其到期时间,并调用相应的回调函数。

这个“心跳”机制是整个LWIP协议栈能够正常运转的生命线。如果 sys_check_timeouts() 未能被及时调用,TCP连接将无法重传,ARP缓存将永不刷新,最终导致网络功能完全失效。因此,在主循环中,必须确保 sys_check_timeouts() 的调用频率足够高,且不能被任何长时间运行的阻塞操作所延迟。

我在实际项目中曾遇到过一个棘手的问题:当系统启用了大量串口打印调试信息时,主循环被严重拖慢, sys_check_timeouts() 的调用间隔从10ms变成了100ms以上,结果导致所有TCP连接在30秒内全部超时断开。最终的解决方案是将所有非关键的调试打印移到一个低优先级的中断中,或者干脆在发布版本中将其全部关闭,以保障网络协议栈的实时性。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐