news 2026/10/4 7:58:18

ZYNQ上KSZ9031 PHY调试:MDIO与MMD读取0xFFFF的排查实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ZYNQ上KSZ9031 PHY调试:MDIO与MMD读取0xFFFF的排查实践

最近在一块 ZYNQ-7000 板卡上调千兆以太网,PS 端 GEM0 外接的是 Microchip 的 KSZ9031RNX,软件层面用裸机 LwIP 跑 TCP/IP 协议栈。原本觉得这套组合很常见,结果调试日志直接停在PHY init failed,用XEmacPs_PhyRead读 PHY ID 寄存器,返回的全是0xFFFF,想进一步通过 MMD 方式读 KSZ9031 的扩展寄存器,也是一样读不到。前前后后折腾了两天多,最后把 MDIO 通路、PHY 复位时序、MMD 访问序列拆开排查,才确认问题根源。

这篇文章就把这趟排障过程完整记录下来。内容包括 KSZ9031RNX 的 MMD 读取原理、ZYNQ GEM 在裸机和 Linux 下的 MDIO 配置、实用的驱动代码,以及几个常见但容易被忽视的坑。适合正在用 ZYNQ 跑以太网、或者遇到 PHY 寄存器访问异常的人,不一定能直接抄作业,但至少能帮你把排查范围缩小一大半。

1. 问题现场:LwIP 起不来,MMD 读什么都是 0xFFFF

1.1 最典型的失败现象

先说现象。裸机工程用的是 Xilinx SDK/Vitis 自带的 LwIP 模板,启动日志走到以太网驱动初始化时,串口打印ERROR: PHY init failed或者PHY reset timeout。这时候如果直接在代码里加一个读 PHY 寄存器的动作,比如读0x02和0x03来获取 PHY ID,读回来的值通常是0xFFFF或者0x0000。如果是0xFFFF,基本可以判定 PHY 没有应答 MDIO 请求;如果是0x0000,要么是 PHY 地址不对,要么是驱动读到了错误的总线状态。

我当时还专门写了一个 MMD 读取函数,严格按照 KSZ9031 数据手册里说的,通过0x0D和0x0E这两个 Clause 22 寄存器来访问 MMD 扩展寄存器。逻辑上自认为没问题,结果读出来的数据依然是0xFFFF。一开始我还怀疑是不是 KSZ9031RNX 这个后缀的芯片 MMD 映射和普通版本不一样,后来又查资料又抓波形,最后才意识到,MMD 读不到往往是上层表现,真正的问题在 MDIO 底层通路没打通。

1.2 为什么一定要读 MMD

很多人问,Clause 22 的基础寄存器不是够用了吗,为什么非要折腾 MMD?

这里要解释一下。传统的快速以太网 PHY,比如常见的 10/100M PHY,寄存器空间只需要 32 个 16 位寄存器就够了,IEEE 802.3 的 Clause 22 管理帧可以直接覆盖。但是到了千兆 PHY,尤其是支持 1000BASE-T 的 PHY,协商逻辑、主从时钟配置、线缆诊断、信号质量、温度检测这些功能都放在扩展寄存器空间里。这个扩展空间使用 Clause 45 定义的 MMD 机制来管理,MMD 全称是 MDIO Manageable Device,每个 MMD 设备有独立的设备地址和最多 65536 个寄存器。

KSZ9031RNX 是 AutoMDIX 千兆 PHY,它的很多关键信息,比如 1000BASE-T 协商状态、主从模式、链路质量、RGMII 输入输出时钟偏移调整等,都不在 Clause 22 的基础 0x00-0x1F 寄存器里。软件要精确控制 PHY 行为,必须通过 MMD 读取或者写入。这也是我坚持要打通 MMD 读取的原因:不把这条通路搞定,后面做 RGMII 时序补偿和链路调试都无从谈起。

2. 先把基础打牢:MDIO 通路、PHY 地址和复位时序

2.1 上电第一步:确认 PHY 地址和复位

很多人读不到 PHY 寄存器,第一反应是去改软件里的 PHY 地址,但其实PHY 地址是硬件 strapping 决定的。KSZ9031 的 PHYAD 引脚在芯片复位时会锁定电平,常见地址是0x07、0x00、0x04等。我用过几个不同开发板,有的用0x07,有的用0x04,没有统一标准,所以第一件事永远是看原理图,确认 PHYAD[2:0] 接的上拉还是下拉。

除了地址,还有一个特别容易被忽略的问题:PHY 复位释放时间。KSZ9031 的硬件复位引脚需要保持低电平一段时间,释放后还需要等待内部初始化完成。有的板子用 ZYNQ 的 MIO 或者 GPIO 控制 PHY 复位,如果软件在初始化以太网时没有先释放复位,或者释放后没有延时足够长,PHY 根本还没准备好,MDIO 读回去自然全是0xFFFF。

我当时甚至遇到过更隐蔽的情况:PHY 的复位引脚和 GEM 的复位共用了同一个信号,GEM 复位释放后立即可用,但 PHY 还在起振,导致驱动在初始化前几毫秒去读 PHY ID,读不到就报错。后来在初始化流程最开始加了至少 10ms 的延时,问题立刻缓解。如果把延时拉到 50ms 到 100ms,基本可以覆盖绝大多数 PHY 的上电时序。

2.2 MDC/MDIO 时钟分频:读不到的先查这里

ZYNQ GEM 的 MDIO 接口由两个信号组成:MDC(管理时钟)和 MDIO(管理数据)。MDC 的频率不能太离谱,IEEE 802.3 标准建议最大不超过 2.5MHz 左右。ZYNQ 的 GEM 控制器内部有一个时钟分频配置项,通过XEmacPs_SetMdioDivider设置,常见取值有XEMACPS_MDIO_DIV_16、XEMACPS_MDIO_DIV_32、XEMACPS_MDIO_DIV_64等。

这个分频值选错,表现非常奇怪。分频太小,MDC 频率过高,PHY 识别不过来,寄存器读出来乱跳;分频太大,MDIO 事务太慢,驱动容易超时。我当时的板卡 GEM 参考时钟是 125MHz,试过用 16 分频,MDC 大概 7.8MHz,PHY 基本不响应;换到 64 分频,MDC 降到 1.95MHz,PHY 才正常。这个分频值不能只看 CPU 主频,一定要结合 GEM 的实际输入时钟来计算,或者直接用示波器测 MDC 引脚,确认波形频率符合 PHY 手册要求。

另外要注意,如果 MDIO 是通过 EMIO 引出到 PL 再接 PHY 的,MDC 和 MDIO 在约束文件里必须有正确的管脚分配和 I/O 标准配置。很多人在 MIO 上跑没问题,切到 EMIO 就死活读不到,原因就是 PL 侧的引脚约束没写对,信号根本没到 PHY。

2.3 RGMII/SGMII 的接口模式别选错

ZYNQ GEM 支持多种外部接口模式:MII、GMII、RGMII、SGMII 等。KSZ9031RNX 是 RGMII 接口的 PHY,所以 GEM 需要配置成 RGMII 模式,并且确认连接的是 GEM0 还是 GEM1,以及对应的 MIO/EMIO bank 电压。

这里想多说一句和热词相关的话题:有人用 ZYNQ PL 侧的 SGMII IP 核去接外部 PHY 芯片,结果以太网起不来。这种情况十有八九是SGMII IP 核和 PHY 芯片的角色配置对不上。SGMII IP 核如果作为 MAC 侧使用,需要配置成 MAC 模式;如果作为 PHY 侧使用,需要配置成 PHY 模式。很多人拿到 IP 核默认配置直接烧,PHY 芯片也在按 PHY 模式工作,两边都以为对方是 MAC,协商永远不可能成功。KSZ9031 本身没有 SGMII 接口,但同样的道理适用于所有带 SGMII 的 PHY,比如 Marvell 88E1512 这类。所以遇到链路起不来,先冷静确认接口模式,再动软件。

回到 RGMII,还有一个常见的时序问题:RGMII 要求时钟边沿对齐,但不同 PHY 对内部延迟的默认值不一样。KSZ9031 支持通过 MMD 寄存器调整 RXD 和 RX_CLK 的相位关系,这就是调试千兆链路时经常用到的 RX delay 和 TX delay 配置。如果这部分没配好,可能表现为能协商上千兆但 Ping 不通,或者数据包 CRC 错误频繁。而这个配置就必须访问 MMD 寄存器,所以 MMD 读取能力是绕不过去的基础设施。

3. MMD 访问原理与 ZYNQ 实操实现

3.1 0x0D/0x0E 窗口:用两个影子寄存器访问整个 MMD 空间

KSZ9031 支持通过 MDIO 管理接口进行 MMD 访问。如果你仔细看芯片手册,会发现它提供了一套间接访问机制,利用 Clause 22 的两个保留寄存器0x0D和0x0E做窗口。

0x0D被称为 MMD 访问控制寄存器,低 5 位用来选择 MMD 设备地址,最高位(bit15)用来切换工作模式:bit15 为 0 时,0x0E是地址寄存器;bit15 为 1 时,0x0E是数据寄存器。整个流程可以总结成四步:

  1. 写 PHY 寄存器0x0D,写入 MMD 设备地址,此时 bit15 保持 0,表示接下来要写地址。
  2. 写 PHY 寄存器0x0E,写入目标寄存器地址(16 位偏移)。
  3. 再写 PHY 寄存器0x0D,写入0x4000 | MMD 设备地址,也就是把 bit15 置 1,切换到数据模式。
  4. 对 PHY 寄存器0x0E进行读或写,拿到的就是对应 MMD 寄存器里的数据。

这四步的本质,就是把一个原本需要 Clause 45 管理帧才能访问的寄存器空间,映射到了 Clause 22 的两个通用寄存器上。ZYNQ GEM 的 MDIO 控制器虽然没有直接暴露 Clause 45 的无缝操作接口,但通过这种窗口方式,一样能访问到扩展寄存器。而且这种窗口方式在业界非常通用,KSZ9031、RTL8211F、Marvell 88E1518 等都大同小异,只是个别芯片把0x0D叫 MMD Control,把0x0E叫 MMD Address/Data,操作逻辑完全相同。

写个小表格方便对照:

步骤操作寄存器写入值含义
1写0x0Dmmd_dev_addr选择 MMD 设备,地址模式
2写0x0Ereg_addr写入 MMD 寄存器地址
3写0x0D0x4000 | mmd_dev_addr切换为数据模式
4读/写0x0Edata读取或写入数据

注意,第 4 步如果是写操作,写入的是0x0E寄存器;如果是读操作,返回的就是目标 MMD 寄存器的值。整个事务完成后,最好把0x0D写回 0,把窗口关掉,否则后续如果驱动里有人直接读0x0E,拿到的内容会和预期不符。

3.2 ZYNQ 裸机下的 MMD 读取代码

在 Xilinx SDK 或者 Vitis 的裸机环境里,MDIO 底层读写一般通过XEmacPs_PhyWrite和XEmacPs_PhyRead完成。这两个函数封装了 GEM 的 PHY 维护寄存器操作。基于这个封装,可以很轻松实现 KSZ9031 的 MMD 读写函数。

下面是一个可以直接拿去用的示例:

#include "xemacps.h" int ksz9031_mmd_read(XEmacPs *mac, u32 phy_addr, u32 mmd_dev, u32 reg_addr, u16 *value) { int status; /* Step 1: 选择 MMD 设备,进入地址模式 */ status = XEmacPs_PhyWrite(mac, phy_addr, 0x0D, mmd_dev & 0x1F); if (status != XST_SUCCESS) return status; /* Step 2: 写入要访问的寄存器地址 */ status = XEmacPs_PhyWrite(mac, phy_addr, 0x0E, reg_addr & 0xFFFF); if (status != XST_SUCCESS) return status; /* Step 3: 切换为数据模式,bit15置1 */ status = XEmacPs_PhyWrite(mac, phy_addr, 0x0D, 0x4000 | (mmd_dev & 0x1F)); if (status != XST_SUCCESS) return status; /* Step 4: 读取数据 */ status = XEmacPs_PhyRead(mac, phy_addr, 0x0E, value); if (status != XST_SUCCESS) return status; /* 关闭窗口,避免影响后续访问 */ XEmacPs_PhyWrite(mac, phy_addr, 0x0D, 0x0000); return XST_SUCCESS; }

写操作的函数类似,区别只是在第 4 步改成XEmacPs_PhyWrite(mac, phy_addr, 0x0E, data):

int ksz9031_mmd_write(XEmacPs *mac, u32 phy_addr, u32 mmd_dev, u32 reg_addr, u16 data) { int status; status = XEmacPs_PhyWrite(mac, phy_addr, 0x0D, mmd_dev & 0x1F); if (status != XST_SUCCESS) return status; status = XEmacPs_PhyWrite(mac, phy_addr, 0x0E, reg_addr & 0xFFFF); if (status != XST_SUCCESS) return status; status = XEmacPs_PhyWrite(mac, phy_addr, 0x0D, 0x4000 | (mmd_dev & 0x1F)); if (status != XST_SUCCESS) return status; status = XEmacPs_PhyWrite(mac, phy_addr, 0x0E, data); if (status != XST_SUCCESS) return status; XEmacPs_PhyWrite(mac, phy_addr, 0x0D, 0x0000); return XST_SUCCESS; }

使用的时候,可以先用基础寄存器确认 PHY ID。KSZ9031RNX 的典型 PHY ID 是0x0022和0x1631,也就是寄存器0x02读到0x0022,寄存器0x03读到0x1631。如果这个都读不对,就别急着调 MMD,回到第二章节查 MDIO 通路。

确认基础寄存器没问题后,可以随便读一个 MMD 寄存器验证函数是否正确。比如读 MMD 设备 3 下面的 1000BASE-T 状态寄存器,正常能返回一个非0xFFFF的值。如果依然读到0xFFFF,就继续往下查问题。

3.3 Linux/PetaLinux 下怎么验证 MMD

如果你用的是 PetaLinux 或者主线 Linux,情况会比裸机好一些,因为 Linux PHY 驱动框架已经封装了phy_read_mmd和phy_write_mmd。但前提是 PHY 驱动里正确绑定了芯片型号,而且 MDIO 控制器驱动支持对应操作。

在调试阶段,我最常用的办法是直接改内核设备树,让 mdio 总线先正常工作,然后通过用户态工具验证。新版 Linux 里有一个叫 mdio-tools 的工具集,提供了mdio命令,可以直接对 MDIO 总线发起读操作,甚至发起 Clause 45 访问。比如:

mdio mdio-bus read 0x07 0x0d

这里0x07是 PHY 地址,0x0d是寄存器地址。通过组合读多个寄存器,可以验证 MMD 窗口是否工作。需要注意的是,老版本内核里某些驱动对 Clause 45 原生操作支持不完整,但通过0x0D/0x0E窗口读取通常都有效,因为窗口底层用的还是标准 Clause 22 帧。

如果你不想额外装工具,也可以先用 devmem2 直接访问 GEM 的 PHY 维护寄存器,但那样要手动组帧,太麻烦。我在调试时一般优先用 mdio-tools,它不仅支持 Clause 22,还支持 Clause 45,省掉了自己写驱动的麻烦。这点对于快速排除是 PHY 问题还是驱动问题非常关键。

4. 常见问题与排查技巧实录

4.1 读基础寄存器正常,MMD 读回来全是 0xFFFF

这是最让人抓狂的情况。Clause 22 基础寄存器一切正常,PHY ID 能读出来,Vendor 信息也正常,但按照标准 MMD 窗口流程读取,返回永远是0xFFFF。

我排查这种问题时,按优先级列了三个怀疑对象:

  • 第一,MMD 设备地址是不是写错了。不同寄存器组对应不同 MMD 设备地址,比如 IEEE 标准里常见的0x1是 PMA/PMD 寄存器组,0x2和0x3是 1000BASE-T 相关控制状态寄存器组,但厂商特有功能通常在更高地址的厂商定义区域。如果你拿厂商功能寄存器的设备地址去套标准流程,读回来自然不对。这种问题的特点是读某些 MMD 寄存器正常,读另一些全是 FFFF。
  • 第二,第 4 步是否真的进入了数据模式。有些芯片对0x0D的 bit15 判断不是立刻生效,需要在写入0x4000 | mmd_dev之后稍等几个 MDC 周期。软件里可以在第 3 步和第 4 步之间加一个非常短的 busy wait,或者至少保证驱动实现里没有把两次写操作合并优化掉。
  • 第三,PHY 当前状态不允许读取。如果 PHY 还在启动或者复位阶段,部分 MMD 寄存器会返回0xFFFF。等链路状态稳定之后再读,数据立刻恢复正常。

4.2 PHY init failed 和 LwIP 起不来

如果你用的是 SDK/Vitis 的 LwIP 模板,初始化出错一般就卡在xemacpsif_phyinit这个函数里。它做的事情很简单:从某个起始 PHY 地址开始,循环读0x02和0x03寄存器,判断有没有读到合法 PHY ID。如果循环到最后都找不到,就报ERROR: PHY init failed。

这种问题除了前面提到的硬件通路问题,还有一个很容易被忽视的点:模板里默认的 PHY 地址可能跟你板子上的硬件地址不一致。Vitis 模板往往默认PHY_ADDR是某个固定值,比如 7,而你的板子上 PHY strap 出来是 4。这个时候改一个宏定义就能过,但要意识到这个问题必须和硬件原理图对应起来,不能凭空猜。

另外,如果你修改过 FSBL 里 MIO 的配置,比如把 MDIO 引脚从 MIO 改到了 EMIO,那么 FSBL 里必须同步更新 GEM 的引脚配置。当时我遇到一个烧写 flash 时报valid FSBL file is required的提示,一度以为跟 PHY 有关,后来发现只是烧写工具要求指定 FSBL 文件而工程里没有输出有效 FSBL。这种问题跟 PHY 和 LwIP 没有关系,不要混在一起排查。

4.3 读完 MMD 后,普通寄存器也跟着变奇怪

这是一个非常常见的“二次伤害”。很多人在调试时直接在调试器里手动写0x0D、0x0E,读到了想要的数据,但之后就忘了恢复窗口状态。等到驱动再读0x0E这个保留或者半保留寄存器时,拿到的数据完全不能看。

所以我在实现 MMD 读写时,强制在函数尾部执行XEmacPs_PhyWrite(mac, phy_addr, 0x0D, 0x0000),把窗口状态复位。这个动作对后续驱动行为影响很大,尤其是在跑 LwIP 时,驱动会周期性读取 PHY 的链接状态寄存器,如果你把 MMD 窗口停留在数据模式,驱动读0x01其实读的是0x0E窗口寄存器,链路状态会变得不可预期。

4.4 问题速查表

现象可能原因检查与处理
所有 PHY 寄存器读回 0xFFFFMDIO 通路不通、PHY 未上电、PHY 地址错误用示波器量 MDC/MDIO,查原理图 PHYAD strap
能读基础寄存器,MMD 读回 0xFFFFMMD 设备地址错误、窗口时序不满足、PHY 状态未就绪选标准设备地址重试,加延时后重读,查手册
LwIP 初始化报 PHY init failedPHY 地址宏定义与硬件不一致、复位未释放、MDC 分频不对确认宏定义,释放复位,调整分频值
MMD 读完后普通寄存器乱掉0x0D 窗口没有复位在 MMD 读写函数尾部写 0x0D 为 0x0000
千兆协商正常但 PING 不通或 CRC 错误RGMII 时钟偏移未调整通过 MMD 寄存器调整 RX/TX delay
SGMII 接 PHY 起不来IP 核 MAC/PHY 角色配置错误核对 SGMII IP 配置,改成 MAC 模式
烧写 flash 提示 valid FSBL requiredFSBL 文件缺失或无效先构建有效 FSBL,再执行 flash 操作

5. 调试工具与实战技巧:别靠猜,靠波形和日志

5.1 示波器是排查 MDIO 问题的第一工具

说实话,这类 PHY 调试问题里,超过一半都是硬件或者引脚配置问题,再强的软件理论分析也抵不过示波器看一眼。MDIO 总线空闲时是高电平,MDC 时钟稳定输出。用示波器探头量 PHY 侧的 MDC 引脚,如果看不到时钟,软件写得再对也没用。

再看 MDIO 数据线,读操作时 MAC 发起地址和寄存器号,然后释放总线,PHY 在特定的时钟窗口驱动数据回传。如果能看到 MDIO 上有数据变化但 PHY 一直没有驱动总线,大概率是 PHY 地址不对或者 PHY 芯片根本没进入工作状态。这种时候再去翻数据手册也好,改软件也好,都有明确方向了。

5.2 mdio-tools 这类命令行工具能省一半时间

现在的 Linux 环境里,我强烈建议每个做 PHY 调试的人都备一套 mdio-tools。它的用法很简单,不需要写内核模块,直接用户态操作/dev/mdio设备节点,就能够读写 Clause 22 和 Clause 45 寄存器。最新版本在 gitcode 上就有镜像仓库,编译也比较简单。

在 PetaLinux 或者嵌入式 Linux 里,把这个工具放到 rootfs 中,调试时直接在串口终端敲命令:

mdio mdio-bus read 0x04 0x02 mdio mdio-bus read 0x04 0x03

先确认基础 ID,再通过组合读写完成 MMD 窗口的验证。如果命令能读到0x0022和0x1631,基本可以确定 PHY 管理通路没问题。这个工具比裸机下打印日志高效得多,尤其是当你需要连续读大量寄存器分析链路协商状态时,简直像开了外挂。

5.3 把 MMD 操作封装成统一接口,后面会非常省心

无论是裸机还是 Linux,我都建议把 MMD 读写封装成独立接口,而不是在业务代码里散落各种XEmacPs_PhyWrite调用。封装之后,调试阶段可以非常方便地加打印、加延时、加错误计数。比如:

int phy_debug_read_reg(u32 phy_addr, u32 reg) { u16 val = 0; int status = ksz9031_mmd_read(&g_emac, phy_addr, 0x3, reg, &val); if (status == XST_SUCCESS) { xil_printf("PHY %02x MMD%d reg %04x = %04x\n\r", phy_addr, 0x3, reg, val); } return status; }

有了这种接口,你就可以在初始化流程里加一个“PHY 自检模式”,逐段打印基础寄存器、MMD 寄存器、协商状态。等整个系统跑起来之后,这套调试代码可以留着,以后遇到链路问题随时能用。实测下来,这种调试代码的价值远远超过了它占用的一点 Flash 空间。

另外提醒一句,不同平台上的 MMD 实现有一点差异。STM32 的 ETH 驱动、NXP 的 MDIO 驱动,在寄存器窗口实现细节上跟 ZYNQ GEM 不完全一样,但0x0D/0x0E窗口思路是通用的。如果换一颗国产百兆 PHY 芯片,也一定先查它的寄存器手册确认 MMD 设备地址和访问时序,别想当然认为和 KSZ9031 完全一样。把这一步做好,后面调 LwIP 也好,调裸机协议栈也好,都会顺畅很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 7:56:42

ZYNQ无DDR运行方案:OCM片上存储器的启动流程与链接脚本实战

1. 为什么要折腾“不带DDR的ZYNQ”:OCM在无外置内存方案中的真实位置1.1 先搞清楚ZYNQ里OCM到底是什么ZYNQ这类芯片和普通单片机最大的不同,就是它内部集成了ARM Cortex-A9双核处理器,但这并不意味着离开了外部DDR它就不会工作。很多人拿到Vi…

作者头像 李华
网站建设 2026/10/4 7:55:33

Calibre License失效真相:开源软件的License认知误区与HOSTID排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 7:55:15

医药知识图谱自动问答:BERT+词典+图谱三合一实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 7:54:38

STM32 SYSTICK原理与高可靠延时系统设计

1. 为什么SYSTICK是STM32开发绕不开的“呼吸节拍器”在STM32项目里,你写过多少次delay_ms(10)?又在调试时被HAL_Delay()卡死过几次?我刚带新人做基于STM32F103的智能鱼缸控制器时,就遇到过一个典型场景:主循环里调用HA…

作者头像 李华
网站建设 2026/10/4 7:54:20

STM32 LCD显示汉字:点阵取模到Keil工程接入全流程

玩嵌入式的朋友应该都遇到过这个尴尬:LCD上英文数字显示得好好的,一到中文就集体“摆烂”,屏幕上该显示“温度”的地方变成一堆方块和乱码。网上搜了一圈,有人让你用取模软件,有人让你移植字库,但对于只想在…

作者头像 李华