STM32外扩双网口:KSZ8863三端口交换芯片驱动开发与联调全记录
简介:面向STM32嵌入式开发者,这份驱动包用于解决KSZ8863以太网交换机芯片的初始化与配置问题。代码通过GPIO模拟I2C总线协议与KSZ8863通信,无需额外硬件I2C外设,移植灵活,可直接集成到现有工程。驱动中封装了ksz8863_init、ksz8863_get_id与ksz8863_test三个函数,分别负责寄存器初始化、芯片ID读取和自检测试,调用即可快速确认芯片工作状态及链路连通性。整个压缩包共12个文件,包含7个头文件与5个C源文件,大小仅88KB;头文件清晰定义寄存器地址和数据结构,C文件则实现底层I2C时序及上层交换配置,模块划分合理,便于二次开发。已有2341人学习下载,特别适合正在调试KSZ8863相关硬件或需要参考GPIO模拟I2C实现的嵌入式工程师。 说实话,我一开始以为 KSZ8863 就是一颗普通 PHY,寄存器读一读、链路等一等、收发包能通就算完事。真正调起来才发现,它是三端口交换芯片,不是 PHY,驱动思路要从“MCU 一颗 MAC 连一颗 PHY”的模型切换到“CPU 口进、交换机转发、标签识别出”的模型。这篇文章把我基于 STM32 从零写 KSZ8863 驱动的过程完整记录一遍,包括硬件连接、SMI 总线访问、初始化流程、VLAN 处理和联调踩坑,给正在做多网口网关、工业数据采集或者开发板扩展网口的同学一个可参考的实操路径。
适合读这篇文章的人:手上有一块带以太网 MAC 的 STM32(F407/F429/F767/H743 之类),想通过 KSZ8863 扩展出两个百兆网口;或者已经在用 LAN8720 做了单网口,想往多网口方向升级。默认你熟悉 STM32CubeMX 生成工程、HAL 库以太网驱动这些基础操作,下面我直接讲干货。
1. 为什么 STM32 要外挂 KSZ8863:从“单 MAC 多网口”需求说起
很多工业设备的实际需求是:一个 MCU 要引出两个甚至更多以太网口。比如数据采集网关,一个口接 PLC 或仪表,一个口接上位机;再比如带网口管理的开发板,一个口做调试,一个口做业务。STM32 内置的 MAC 只有一个,直接方案根本不够用,所以要在外面挂交换芯片扩展端口。
有的人可能想“那我不如直接换一颗带双 MAC 的 MCU”,这个方案不是不行,问题是成本和供货都不可控,而且很多情况只是需要把两个网口在物理层和链路层打通,并不需要两个独立 MAC 各自跑协议栈。KSZ8863 的价值就在这:它是三端口二层交换芯片,Port1 和 Port2 是两个内置 PHY 的对外网口,Port3 是主机端口,通过 MII/RMII 接到 STM32 的 MAC 上。数据包在芯片内部按二层转发表转发,CPU 不用参与每个包的搬运,只在需要的时候收包、处理、再发出,这对 MCU 的负载非常友好。
方案对比,我做项目时列过一张表:
| 方案 | 成本 | 硬件复杂度 | MCU负载 | 适用场景 |
|---|---|---|---|---|
| 换双MAC MCU | 高 | 中 | 低 | 两个独立IP栈场景 |
| 外挂多颗LAN8720 | 低 | 高(MAC只有一个,不现实) | 高 | 基本不可行 |
| USB转以太网 | 中 | 低 | 高,依赖USB协议栈 | 对实时性要求低的场合 |
| STM32 + KSZ8863 | 中低 | 中 | 低,硬件转发 | 多网口网关、工业控制 |
KSZ8863 适合的场景很典型:两个物理网口由交换芯片负责二层转发,CPU 通过 Port3 接入,既能转发普通数据帧,也能通过开启端口标签识别收包来自哪个物理端口。这颗芯片内部还带地址查找表、VLAN、QoS、端口镜像等功能,后续做业务扩展空间也比较大。
有一点要提前想清楚:KSZ8863 和 KSZ9897 这类高端交换芯片不同,它只有百兆,端口数也只有 3 个,别拿它当路由器主交换用。它解决的是“STM32 需要两个百兆网口”这种非常具体的需求,选型别贪大。
2. 硬件连接和启动引脚:先把 RMII 和 MDIO 这层布对
2.1 RMII 接口怎么接最稳
STM32 的 MAC 和 KSZ8863 的 Port3 之间,我建议用 RMII 不用 MII,原因很朴素:引脚少。RMII 只需要 TXD[1:0]、TX_EN、RXD[1:0]、CRS_DV、REF_CLK,加 MDC/MDIO 一共 9 根线左右,STM32 上能直接映射的引脚选择也多。MII 虽然有更传统的接口,但引脚翻倍,布线更麻烦,百兆场景 RMII 完全够用。
接线方式上,KSZ8863 的 Port3 侧信号和 STM32 ETH 外设引脚是一一对应的,TXD、RXD、TX_EN、CRS_DV 都是同名直连,唯一需要在硬件上仔细考虑的是 REF_CLK。RMII 接口的收发时钟是共用的,STM32 的 REF_CLK 引脚是输入,外部必须提供 50MHz 参考时钟。
时钟有 3 种常见接法:
- 外部 50MHz 有源晶振,一路分给 STM32 REF_CLK,一路给 KSZ8863。这种方式最稳,抖动小,我最终项目里用的就是这种。
- STM32 MCO 引脚输出 50MHz 给 KSZ8863。少一颗晶振,但 MCO 是从芯片内部 PLL 分出来的,抖动比专用晶振大,大流量下可能出现偶发 CRC 错帧,调试期不建议选这种方式。
- KSZ8863 自身提供时钟输出,需要确认它的时钟方向配置引脚有没有设置对,否则上电没有时钟输出,MCU 直接歇菜。
建议是:第一版板子老老实实用外部有源晶振,等驱动调稳定了,再考虑省晶振的方案。时钟问题在上电初期不容易发现,因为 MDIO 读写不一定依赖 REF_CLK,但网络收发一定依赖,属于那种“查了半天才发现”的隐蔽问题。
2.2 启动配置引脚,决定上电后的默认行为
KSZ8863 有一组 strapping 引脚,上电复位时芯片会锁存这些引脚的电平状态,用来配置 PHY 地址、Port3 工作模式、tail tag 是否使能等。这组引脚不是普通 GPIO,电平不能悬空,必须按 datasheet 里的“Strapping Options”表格接上拉或下拉电阻。
我在第一版画板时就吃过亏:P2_MODE 相关引脚没按手册接,导致 Port3 上电后被识别成 MII 模式,而 STM32 那边明明配置的是 RMII,两边怎么都对不上。后来飞线改电平才恢复。画板之前一定把每个 strapping 引脚的功能查清楚,别依赖模块默认值,模块用的芯片后缀不同,默认值可能会有差异。
有个经验:原理图评审阶段把这些引脚的电平状态做成一个表格,上拉、下拉、悬空,逐项和 datasheet 核对一遍。这比板子回来再飞线省太多事。
2.3 MDC/MDIO 的连接细节
MDC 是时钟线,由 STM32 主控输出;MDIO 是双向数据线,必须接上拉电阻,一般 1kΩ 到 10kΩ 都可以,我在项目里用的 4.7kΩ 到 3.3V 上拉,跑得很稳定。MDC 频率建议先从 2.5MHz 往下调,有些 PCB 走线比较长或者上拉电阻偏大时,高速 MDC 下 MDIO 时序容易不稳定,表现出来就是寄存器读到一半变成 0xFFFF。
如果板子上同时挂了其他 PHY 或交换芯片,要注意 MDIO 地址冲突。KSZ8863 作为 SMI 从设备,它的访问机制和普通 PHY 不一样,后面专门讲。
2.4 复位时序
硬件复位引脚 RESET_N 低电平有效,上电后要保证一个完整的复位脉冲。我习惯上电后先等电源稳定,再延时几十毫秒释放复位,然后软件初始化时先读一次 Chip ID 确认通信正常,再做一次软复位。这能避免上电时序竞争导致的偶发初始化失败。
3. SMI 驱动写法:访问 KSZ8863 寄存器的底层通道
3.1 KSZ8863 不是普通 PHY,寄存器要靠间接访问
用 LAN8720 这类普通 PHY 时,MDIO 地址就是 PHY 地址,寄存器地址就是标准的 MII 寄存器,一堆宏定义直接读。KSZ8863 不一样,它的控制寄存器很多,物理上是通过 MDIO 接口间接访问的:MDIO 总线上只能直接操作两个寄存器,一个是命令寄存器 SMI_CMD(偏移 0x74),一个是数据寄存器 SMI_DATA(偏移 0x75),想读任意内部寄存器,必须先把命令写到 SMI_CMD,然后从 SMI_DATA 拿结果;想写寄存器,则先把数据放 SMI_DATA,再向 SMI_CMD 发写命令。
这个机制有点像 I2C 里通过寄存器地址间接访问设备内部存储,理解了这一点,驱动代码写起来就不会觉得别扭。
3.2 核心的 SMI 读写函数
底层 MDIO 收发,我直接用 STM32 HAL 库的 HAL_ETH_ReadPHYRegister 和 HAL_ETH_WritePHYRegister 实现,它们在内部已经处理了 MDIO 时序。注意这里访问的 PHY 地址是 KSZ8863 的 SMI 设备地址,不是普通 PHY 的地址,需要根据硬件 strapping 设置的值来填。
#define KSZ8863_SMI_CMD_REG 0x74
#define KSZ8863_SMI_DATA_REG 0x75
#define SMI_CMD_READ_OP (0x03u << 14)
#define SMI_CMD_WRITE_OP (0x01u << 14)
#define SMI_CMD_VALID (0x01u << 13)
static int32_t ksz8863_smi_wait_busy(void)
{
uint16_t cmd = 0;
uint32_t timeout = 1000;
while (timeout--) {
/* 读取SMI_CMD寄存器,根据芯片手册判断忙位 */
HAL_ETH_ReadPHYRegister(&heth,
KSZ8863_SMI_PHY_ADDR,
KSZ8863_SMI_CMD_REG,
&cmd);
if ((cmd & SMI_CMD_BUSY_MASK) == 0) {
return 0;
}
}
return -1;
}
uint16_t ksz8863_reg_read(uint8_t reg_high, uint8_t reg_low)
{
uint16_t cmd = 0;
uint16_t val = 0;
if (ksz8863_smi_wait_busy() != 0) {
return 0xFFFF;
}
/* 构造SMI读命令,具体位域按KSZ8863手册SMI_CMD寄存器定义填写 */
cmd = SMI_CMD_READ_OP | SMI_CMD_VALID |
((uint16_t)reg_high << 8) | reg_low;
HAL_ETH_WritePHYRegister(&heth,
KSZ8863_SMI_PHY_ADDR,
KSZ8863_SMI_CMD_REG,
cmd);
HAL_Delay(1);
HAL_ETH_ReadPHYRegister(&heth,
KSZ8863_SMI_PHY_ADDR,
KSZ8863_SMI_DATA_REG,
&val);
return val;
}
void ksz8863_reg_write(uint8_t reg_high, uint8_t reg_low, uint16_t val)
{
uint16_t cmd = 0;
if (ksz8863_smi_wait_busy() != 0) {
return;
}
/* 先写数据寄存器,再发写命令 */
HAL_ETH_WritePHYRegister(&heth,
KSZ8863_SMI_PHY_ADDR,
KSZ8863_SMI_DATA_REG,
val);
cmd = SMI_CMD_WRITE_OP | SMI_CMD_VALID |
((uint16_t)reg_high << 8) | reg_low;
HAL_ETH_WritePHYRegister(&heth,
KSZ8863_SMI_PHY_ADDR,
KSZ8863_SMI_CMD_REG,
cmd);
HAL_Delay(1);
}
这里我故意不写死寄存器地址和页号宏,因为 KSZ8863 的不同后缀(MLL、RLL、FLL 等)在页映射上确实有差异,直接把手册抄过来最靠谱。关键是 SMI 这套“命令 + 数据”的访问模型,理解以后换任何一款 Microchip 交换芯片都能快速上手。
3.3 对内部 PHY 的控制也要走 SMI
KSZ8863 的 Port1 和 Port2 内置了 PHY,这两个 PHY 的寄存器也不是直接挂在 MDIO 地址 0 和 1 上的。想读 Port1 的链路状态、自动协商结果,同样要构造 SMI 命令,把内部 PHY 的 MII 寄存器映射过来读。
我写驱动时把读取链路状态封装成了一个独立接口:
int ksz8863_get_port_link(uint8_t port, uint8_t *link)
{
uint16_t status = 0;
if (port > 2) {
return -1;
}
/* 读取端口状态寄存器,例如PHY1/PHY2的BMSR或端口状态寄存器,
* 具体地址以手册为准 */
status = ksz8863_reg_read(port == 1 ? PAGE_PHY1 : PAGE_PHY2,
REG_MII_BMSR);
*link = (status & 0x0004) ? 1 : 0;
return 0;
}
在实际项目里,STM32 的 lwIP 协议栈通常需要知道物理链路是否就绪,这个接口就很有用。有的驱动直接用读 0xFFFF 来判断 PHY 不存在,但这套方法在 KSZ8863 上不适用,容易误判,所以链路状态一定要走内部寄存器读,别走通用 PHY 驱动那套逻辑。
3.4 把 SMI 底层和上层配置分开,方便移植
驱动写起来以后,我建议把代码分成两层:底层是 SMI 读写函数和 MDIO 通道封装,上层是端口配置、VLAN、tail tag 等业务逻辑。这样以后如果板子改成用 SPI 或 I2C 方式控制 KSZ8863,上层代码几乎不用动,只替换底层的读写接口即可。我的文件结构是这样的:
-
ksz8863_smi.c:MDIO 底层,封装寄存器和 PHY 寄存器读写。 -
ksz8863.c:芯片初始化、端口配置、VLAN、链路查询。 -
ksz8863.h:对外接口声明和常用寄存器宏。
这个小分层习惯帮我省了很多事,因为第一版板子用的是 SMI,第二版因为引脚冲突改成 SPI,上层初始化流程一行没改。
4. 交换核心初始化:端口映射、转发规则与 CPU 口配置
4.1 上电第一件事:读 Chip ID
SMI 驱动写完先别急着配端口,第一步永远是读 Chip ID,确认通信链路本身是通的。KSZ8863 的 Chip ID 读取出来一般是 0x8863 附近的数值,不同封装会有细微差别。
uint16_t id1 = ksz8863_reg_read(0x00, 0x00);
uint16_t id2 = ksz8863_reg_read(0x00, 0x01);
if (id1 != 0x88 || id2 != 0x63) {
/* 打印错误信息,检查MDIO上拉、PHY地址、复位电平 */
return -1;
}
如果这一步读出来的不是预期值,不要继续往下配置,先回头查硬件。我习惯用一个简单的串口打印函数把关键错误打出来,联调效率会高很多。
4.2 软复位和基本配置
读通 Chip ID 之后,给芯片做一次软复位。软复位寄存器一般在 Global Control 区,找到对应的控制位置 1,然后等待芯片完成复位。这里要给足延时,我一般等 50ms 以上。
复位完成之后,按项目需求配置系统控制寄存器。我通常在初始化里做这几件事:
- 关闭不必要的节能模式,避免端口自动进入低功耗状态导致链路起不来。
- 设置背压和流量控制相关位,这个看具体网络环境,默认值有时候不够用。
- 确认 MDIO 的读写访问不会触发芯片内部仲裁冲突。
4.3 CPU 口(Port3)的 RMII 模式和 tail tag
Port3 是 STM32 接入交换芯片的桥头堡,它的配置直接决定 CPU 能不能收发包。要配置的关键项有两个:
一个是把 Port3 设为 RMII 模式,和硬件连接保持一致。如果这里配成 MII,CPU 口的收发时序完全对不上,现象是 ETH 初始化正常,但收不到任何帧。
另一个是 tail tag 功能。这个功能我强烈建议开启,它解决了一个非常实际的问题:当 Port1 和 Port2 的数据都通过 Port3 送到 CPU 后,CPU 怎么知道这一帧是从哪个物理口进来的?KSZ8863 的做法是在帧的尾部、FCS 之前追加 4 字节标签,里面携带端口号信息。
STM32 侧的以太网驱动默认是按普通以太网帧长度处理收包的,开了 tail tag 之后,收到的帧会多出 4 字节尾巴。这就需要在 lwIP 的 netif 收包函数里把这 4 字节剥掉,同时解析出端口号。我的做法是:
void eth_rx_process(void)
{
uint32_t len = 0;
struct pbuf *p = NULL;
if (HAL_ETH_GetReceivedFrame_IT(&heth) != HAL_OK) {
return;
}
len = heth.RxFrameInfos.length;
/* 如果开了tail tag,帧尾会多4字节,先剥掉再交给协议栈 */
if (ksz8863_tail_tag_enabled) {
uint16_t tag;
tag = *(uint16_t *)((uint8_t *)rx_buffer + len - 6);
rx_source_port = (tag >> 12) & 0x0F; /* 端口号在tag高位,具体位按手册 */
len -= 4;
}
p = pbuf_alloc(PBUF_RAW, len, PBUF_POOL);
if (p != NULL) {
pbuf_take(p, (uint8_t *)rx_buffer, len);
/* 交给以太网netif层 */
}
}
这段代码展示了处理思路:先判断使能位,再在帧尾取标签解析端口号,最后把尾巴剥掉再上交协议栈。这样 lwIP 里看到的仍然是一个干净的以太网帧,而业务层又可以通过 rx_source_port 知道数据来自哪个网口,实现类似“双网口独立业务”的需求。
4.4 Port1 和 Port2 的基本配置
对外端口主要配置自动协商、速度和双工模式。KSZ8863 默认支持 10/100M 自适应,一般接普通交换机或电脑网卡都能跑起来。我的初始化流程里对这两个端口主要做三件事:
- 开启自动协商,允许 10M/100M、半双工/全双工自适应。
- 使能端口收发,确保端口没有被禁用。
- 查询端口状态寄存器,确认链路 link up。
有一个容易忽略的点:调试过程中如果反复修改端口模式,最好每次改完都先读回验证一下,因为 KSZ8863 有些寄存器的写使能位有保护,看起来写成功了,实际没写进去。
4.5 VLAN 配置:先关闭,再逐步开启
KSZ8863 内置 VLAN 功能,出厂默认的 VLAN 表可能不是你想要的行为。我第一次调试时遇到的情况是:芯片能通信,但 CPU 口发出去的广播包,Port1 和 Port2 收不到,反过来也一样,三个口像是被隔离在三个不同 VLAN 里。
解决办法是初始化时先把 VLAN 功能关掉或者把所有端口显式加入同一个 VLAN,让芯片先当一台“透明二层交换机”用。VLAN 在这个项目里不是核心需求,所以我直接用默认配置把所有端口放到同一个 VLAN 的成员列表里,保证三端口互通,优先级调到默认。等二层转发稳定了,再去研究 802.1Q 和端口隔离也来得及。
初始化流程总结下来就是一张表:
| 步骤 | 操作 | 需要确认的状态 |
|---|---|---|
| 1 | 读 Chip ID | 0x8863 系列 ID |
| 2 | 软件复位 | 延时 50ms |
| 3 | 配置 Port3 RMII 模式 | 与硬件 strapping 一致 |
| 4 | 使能 tail tag | 收包驱动能剥尾部标签 |
| 5 | 配置 Port1/Port2 自动协商 | 查询链路 up |
| 6 | VLAN 全放通 | 广播包能到所有端口 |
| 7 | 读回校验关键寄存器 | 配置确实生效 |
5. 联调那一周踩过的坑:从 PHY 读不到数据到 Ping 不通
5.1 坑一:寄存器能读能写,但网络就是不通
这个现象最让人抓狂,因为 SMI 通信正常说明 MDIO 没问题,但网络不通是哪一层的问题不好定位。我那次查到最后是 RMII 参考时钟的问题:KSZ8863 的 REF_CLK 引脚需要外部输入 50MHz,但板子上焊的是 25MHz 晶振,芯片虽然能工作,但 MII 和 RMII 的时钟倍数关系完全不对,MAC 和交换芯片对不上。
排查方法很简单:示波器量 REF_CLK 引脚频率,确认是 50MHz。如果相位噪声大,网络可能出现丢包、CRC 错误,但纯频率不对通常是完全不通。
5.2 坑二:MDIO 读回 0xFFFF
MDIO 读回全 F 是典型的通信失败信号,原因我遇到过的有这三种:
- MDIO 上拉电阻缺失或阻值过大,导致数据线无法驱动到有效电平。
- MDC 频率太高,KSZ8863 跟不上升沿采样时序。
- SMI 设备地址配置和实际 strapping 不一致,命令发给了错误的从设备。
排查方法建议先换低 MDC 频率,再把上拉电阻改小,最后确认 PHY 地址。几个步骤都不复杂,但要在硬件回来之后逐个排除。
5.3 坑三:默认 VLAN 配置隔离了 CPU 口
这个前面提过,现象就是端口看起来都 link up,但 P3 发出的包到不了 P1/P2,P1/P2 的包也到不了 P3。很多人在这一步误判成 STM32 的 DMA 或 lwIP 配置问题,折腾很久才发现是交换芯片的 VLAN 表在搞鬼。
我的经验是:初始化里显式操作 VLAN 相关寄存器,别用出厂默认值,即使你的目标就是全互通。因为不同批次或温度下默认值可能不一样,显式配置才能保证行为一致。
5.4 坑四:STM32 HAL 的 PHY 驱动和 KSZ8863 水土不服
HAL 库的以太网驱动里一般会有一个默认的 PHY 驱动,读 BMSR、读协商结果,这里面写死了 PHY 地址和寄存器偏移。KSZ8863 的寄存器模型和普通 PHY 不同,直接把默认驱动套上去,链路状态读了等于白读,甚至可能因为读不到 BMSR 而把网口 down 掉。
解决办法是重写 netif 里的 PHY 相关回调函数,把链路状态检测改成读 KSZ8863 的内部端口状态寄存器,不要让 HAL 的默认 PHY 驱动去碰内部 PHY 的 MII 寄存器。这一步很关键,否则 lwIP 会因为“检测不到 PHY”而拒绝工作,或者一直处于 link down 状态。
5.5 坑五:能 Ping 通但吞吐率低或大包不通
Ping 小包通了说明链路基本没问题,但如果大包(比如 MySQL 备份走 TCP)吞吐率上不去,一般先查两点:一个是 STM32 的 DMA 描述符个数和缓冲区大小是否够用,另一个是交换芯片的缓冲区分配和流控配置。
我在调这个项目时发现,KSZ8863 的内部缓冲配置寄存器里有一个控制帧转发优先级的选项,如果配置不当,在双向流量大的时候会出现高丢包率。我的建议是先用最简单粗暴的配置:所有端口优先发,不限制速率,确认吞吐率没问题之后,再根据实际应用一点点加限制。
5.6 几个帮我快速定位问题的调试手段
如果从头开始做这类驱动,我建议准备几个调试工具和手段,会省很多时间:
- 串口打印寄存器操作日志,尤其是每次初始化关键节点的返回值。
- 在 STM32 的 lwIP 上临时开启一个简单的回显任务,收包后原地址回发,验证二层转发是否正常。
- 用电脑直连 KSZ8863 的一个端口,打流工具 iperf 测吞吐率,区分是否是 PHY 或交换转发的问题。
- 观察 KSZ8863 的 LED 状态引脚,很多网口问题可以通过 LED 直接看出来。链路 up 但 LED 不亮,往往是 PHY 侧配置问题;LED 亮但包不通,往往是转发规则问题。
这些手段组合起来,能在半小时内把问题范围从“整个系统”缩小到“某一段链路”,比盲改代码高效得多。
最后再分享一个小技巧:调试 KSZ8863 这类交换芯片时,不要一开始就开 VLAN、不要一上来就配 tail tag,先把芯片调成透明转发模式,让三个口纯二层互通,确认 lwIP 的收发路径没问题,再一步步加功能和解析标签。这样即使后面出问题,你也能很明确地判断是交换配置引入的还是本来就存在的,排查起来轻松很多。
更多推荐
所有评论(0)