恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
STM32驱动KSZ8863交换芯片:SPI接口与链路状态调试指南
首页
资讯中心
/
STM32驱动KSZ8863交换芯片:SPI接口与链路状态调试指南
STM32驱动KSZ8863交换芯片:SPI接口与链路状态调试指南
发布时间:2026/10/14 18:24:20
简介这份资源面向使用STM32驱动KSZ8863交换芯片的嵌入式开发者提供了一套通过GPIO模拟I2C总线完成芯片配置的驱动实现。KSZ8863作为三端口以太网交换芯片常需在无硬件I2C外设或引脚受限的场景下完成寄存器读写本驱动正是针对这一痛点用软件模拟时序替代硬件I2C降低了对MCU外设资源的依赖。压缩包共12个文件包含7个.h头文件与5个.c源文件整体约88KB头文件主要定义寄存器地址、芯片接口与GPIO模拟I2C的声明源文件则实现I2C时序、芯片初始化、ID读取及自测逻辑。使用时可依次调用ksz8863_init()、ksz8863_get_id()与ksz8863_test()三个函数完成基本验证便于快速确认硬件连接与配置是否正确。目前已有2349人学习下载适合需要移植或调试KSZ8863驱动的嵌入式工程师参考能帮助理解GPIO模拟I2C的时序细节与交换芯片寄存器配置流程缩短底层驱动开发与排错周期。1. 从一块吃灰的板子说起ksz8863 在 stm32 上到底该怎么点灯手头有一块 stm32 的板子网口旁边焊着一颗 ksz8863翻遍官方例程只有 lan8720 和 dp83848 的驱动ksz8863 的寄存器手册翻来覆去看了三遍还是不知道怎么把链路 up 起来。这不是个例ksz8863 这颗三端口交换芯片在 stm32 生态里属于典型的“资料散、例程少、坑还深”的器件。它不像 phy 那样只有标准 32 个寄存器ksz8863 内部有交换机功能、vlan 表、静态 mac 表、端口镜像还有一套 spi 或者 rmii 的访问方式选错了接口模式后面全是玄学问题。这份驱动资源就是围绕 stm32 平台把 ksz8863 从复位、寄存器读写、端口初始化到链路状态轮询整条链路串起来适合正在做工业网关、多网口设备或者单纯想把板子上那颗交换芯片跑通的嵌入式工程师。如果你手里正好有 ksz8863 的硬件又不想从零啃 200 多页的寄存器手册这份东西能省掉至少一周的试错时间。2. ksz8863 的寄存器模型与 stm32 侧接口选型spi 还是 rmii2.1 先搞清楚 ksz8863 和普通 phy 的本质区别很多从 lan8720 转过来的人第一反应是“不就是个 phy 吗读写 bcr/bsr 就完事了”结果一上电发现链路状态读出来永远是 0。ksz8863 严格来说是一颗三端口 10/100 以太网交换机芯片内部集成了两个 phy 和一个 mac还有一个交换引擎。它对外提供两种主机接口spi 从模式和 rmii 模式。spi 模式下 stm32 通过 spi 总线访问 ksz8863 的全部寄存器包括 phy 寄存器、交换机寄存器、vlan 表rmii 模式下 stm32 的 mac 直接和 ksz8863 的端口 3 对接数据走 rmii但寄存器配置仍然需要 spi 或者 i2c 来读写。常见做法是 spi 负责控制面rmii 负责数据面两者配合使用。如果你只做链路状态监测和简单收发spi 模式就够了如果要跑 lwip 或者 tcp 通信rmii 加 spi 的组合是绕不开的。这里有个容易翻车的地方ksz8863 的 spi 协议不是标准 spi它有自己的命令格式。读操作是 1 字节命令加 1 字节地址加 1 字节空周期再读数据写操作是 1 字节命令加 1 字节地址加 1 字节数据。命令字节的最高位区分读写bit6 区分寄存器类型0 是普通寄存器1 是交换机寄存器bit5 到 bit0 是地址高位。很多人在这一步直接用 stm32 的 hal_spi_transmit 发两个字节就完事结果读回来全是 0xff就是因为没按 ksz8863 的时序来。2.2 stm32 侧 spi 初始化的关键参数以 stm32f4 系列为例spi 时钟不能超过 ksz8863 手册规定的 25mhz实际跑 10mhz 左右比较稳。cpol 和 cpha 要设成 mode 0也就是 cpol0、cpha0数据在第一个时钟沿采样。片选建议用软件控制因为 ksz8863 的 spi 帧之间需要片选拉高来分隔。下面是一段初始化代码基于 hal 库// ksz8863 spi 初始化stm32f4 hal 库 SPI_HandleTypeDef hspi1; void ksz8863_spi_init(void) { hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; // cpol 0 hspi1.Init.CLKPhase SPI_PHASE_1EDGE; // cpha 0 hspi1.Init.NSS SPI_NSS_SOFT; // 软件片选 hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_8; // 84mhz/8 ≈ 10.5mhz hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; hspi1.Init.TIMode SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation SPI_CRCCALCULATION_DISABLE; HAL_SPI_Init(hspi1); }参数说明baudrateprescaler 根据你的 apb2 时钟来算保证 spi 时钟在 10mhz 到 15mhz 之间。nss 设成 soft 是因为 ksz8863 的片选需要手动拉低拉高硬件 nss 在某些 stm32 型号上会有时序问题。firstbit 必须是 msbksz8863 的 spi 帧是高位先出。2.3 寄存器读写函数的实现与边界处理ksz8863 的 spi 读写函数是整个驱动的地基写错了后面全崩。下面这个实现把读和写分开读的时候多发一个空字节// ksz8863 寄存器读写spi 模式 #define KSZ_CMD_READ 0x00 #define KSZ_CMD_WRITE 0x80 #define KSZ_REG_NORMAL 0x00 #define KSZ_REG_SWITCH 0x40 static void ksz_cs_low(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); } static void ksz_cs_high(void) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); } uint8_t ksz8863_read_reg(uint8_t reg_type, uint8_t addr) { uint8_t tx[3], rx[3]; tx[0] KSZ_CMD_READ | reg_type | ((addr 3) 0x1F); tx[1] (addr 5) 0xE0; tx[2] 0x00; // 空周期 ksz_cs_low(); HAL_SPI_TransmitReceive(hspi1, tx, rx, 3, 100); ksz_cs_high(); return rx[2]; // 第三个字节才是数据 } void ksz8863_write_reg(uint8_t reg_type, uint8_t addr, uint8_t data) { uint8_t tx[3]; tx[0] KSZ_CMD_WRITE | reg_type | ((addr 3) 0x1F); tx[1] (addr 5) 0xE0; tx[2] data; ksz_cs_low(); HAL_SPI_Transmit(hspi1, tx, 3, 100); ksz_cs_high(); }逻辑说明命令字节的 bit7 是读写标志bit6 是寄存器类型bit5 到 bit0 是地址的高 5 位。地址的低 3 位放在第二个字节的 bit7 到 bit5。读操作需要第三个字节作为空周期来移位数据写操作第三个字节直接是数据。注意片选在每次传输前后必须拉高否则 ksz8863 会把连续帧当成一个长帧解析。提示如果你读回来的值一直是 0xff 或者 0x00先用示波器看 spi 的 clk 和 mosi 波形确认片选时序和时钟极性。九成以上的 spi 读失败都是时序问题不是芯片坏了。3. 端口初始化与链路状态轮询把 link up 跑通3.1 上电后的复位与 id 校验ksz8863 上电后需要先软复位然后读 chip id 确认 spi 通路是通的。chip id 寄存器在普通寄存器空间的 0x00 和 0x01预期值是 0x88 和 0x63 左右具体以手册为准不同批次可能有小差异。如果读出来不对先别急着写初始化回头查 spi 时序。// ksz8863 复位与 id 校验 #define KSZ_REG_CHIP_ID0 0x00 #define KSZ_REG_CHIP_ID1 0x01 #define KSZ_REG_GLOBAL_CTRL 0x02 void ksz8863_reset(void) { // 软复位bit4 写 1 ksz8863_write_reg(KSZ_REG_NORMAL, KSZ_REG_GLOBAL_CTRL, 0x10); HAL_Delay(10); // 等复位完成 } uint8_t ksz8863_check_id(void) { uint8_t id0 ksz8863_read_reg(KSZ_REG_NORMAL, KSZ_REG_CHIP_ID0); uint8_t id1 ksz8863_read_reg(KSZ_REG_NORMAL, KSZ_REG_CHIP_ID1); // 根据手册id0 应为 0x88id1 应为 0x63 左右 if (id0 0x88 (id1 0xF0) 0x60) { return 1; // id 匹配 } return 0; }参数说明global_ctrl 寄存器的 bit4 是软复位位写 1 后芯片会在几个毫秒内完成复位。复位后所有寄存器回到默认值包括端口使能状态。id 校验是判断 spi 通路是否正常的唯一标准id 读不对后面所有操作都没有意义。3.2 端口使能与 phy 寄存器配置ksz8863 有三个端口端口 1 和端口 2 是内部 phy端口 3 是 rmii 接口。每个端口都有一套 phy 寄存器地址偏移是 0x20 的倍数。端口 1 的 phy 寄存器基地址是 0x20端口 2 是 0x40。要使能端口并配置成自协商模式需要写 phy 的 bmcr 寄存器偏移 0x00和 bmsr 寄存器偏移 0x01。// 端口 phy 寄存器配置 #define KSZ_PORT1_BASE 0x20 #define KSZ_PORT2_BASE 0x40 #define PHY_BMCR 0x00 #define PHY_BMSR 0x01 #define PHY_ANAR 0x04 void ksz8863_port_init(uint8_t port_base) { // 使能自协商重启自协商 ksz8863_write_reg(KSZ_REG_NORMAL, port_base PHY_BMCR, 0x33); // 通告 10/100 全双工和半双工 ksz8863_write_reg(KSZ_REG_NORMAL, port_base PHY_ANAR, 0x01E1); HAL_Delay(100); // 等自协商完成 } uint8_t ksz8863_get_link_status(uint8_t port_base) { uint8_t bmsr ksz8863_read_reg(KSZ_REG_NORMAL, port_base PHY_BMSR); // bit2 是 link statusbit5 是自协商完成 if ((bmsr 0x04) (bmsr 0x20)) { return 1; // 链路 up 且自协商完成 } return 0; }逻辑说明bmcr 写 0x33 是使能自协商加重启自协商anar 写 0x01e1 是通告所有 10/100 模式。bmsr 的 bit2 是实时链路状态bit5 是自协商完成标志两个都置位才算真正 up。注意 bmsr 是只读寄存器读的时候 bit2 反映的是当前链路状态不是锁存值。3.3 交换机寄存器的基本配置如果你要用到 vlan 或者端口隔离就需要操作交换机寄存器。交换机寄存器的访问方式和普通寄存器一样只是命令字节的 bit6 要置 1。常见的配置包括端口使能、vlan 模式、mac 表老化时间。下面是一个端口使能的例子// 交换机寄存器端口使能 #define KSZ_SW_PORT_CTRL1 0x00 #define KSZ_SW_PORT_CTRL2 0x01 void ksz8863_switch_port_enable(void) { // 端口 1 和端口 2 使能端口 3 使能 ksz8863_write_reg(KSZ_REG_SWITCH, KSZ_SW_PORT_CTRL1, 0x07); // 其他交换机配置按需添加 }参数说明交换机寄存器的地址空间和普通寄存器是独立的通过命令字节的 bit6 来区分。端口使能寄存器的低 3 位分别对应三个端口写 1 使能。如果你不需要 vlan 功能这一步可以跳过但端口使能建议加上否则某些批次芯片默认端口是关闭的。注意ksz8863 的 phy 寄存器写操作后需要等待至少 10ms 才能读回生效值自协商过程需要 100ms 到 500ms。不要在写完之后立刻读 bmsr 判断链路会误判成 down。4. 避坑与排查ksz8863 驱动里最容易翻车的五个地方4.1 spi 读回来全是 0xff现象调用 ksz8863_read_reg 读 chip id返回值一直是 0xff换寄存器地址也一样。原因spi 时序不对。最常见的是 cpol/cpha 设错或者片选没有在帧之间拉高。ksz8863 的 spi 帧要求片选在每 3 个字节后拉高至少一个时钟周期如果片选一直拉低芯片会把后续帧当成同一个命令的延续。解决用示波器抓 clk、mosi、cs 三根线确认 cs 在每 3 字节后拉高。检查 spi 初始化里的 cpol 和 cpha 是否都是 0。如果用的是硬件 nss改成软件 nss 再试。4.2 链路状态读出来永远是 down现象端口初始化写完了bmsr 的 bit2 始终是 0网线插着也不亮灯。原因bmcr 的自协商重启位没有正确置位或者 anar 没有配置。还有一种可能是端口 phy 的基地址算错了ksz8863 的端口 1 基地址是 0x20端口 2 是 0x40不是 0x00 和 0x20。解决确认端口基地址。写 bmcr 的时候 bit9 是重启自协商bit12 是使能自协商两个都要置 1。写完之后延时 500ms 再读 bmsr。如果还不 up读一下 phy 的 id 寄存器确认端口 phy 是否正常响应。4.3 rmii 模式下数据收发不通现象spi 配置都正常链路也 up 了但 rmii 数据发不出去ping 不通。原因rmii 的时钟源没有配置。ksz8863 在 rmii 模式下需要提供 50mhz 的参考时钟给 stm32 的 mac这个时钟通常由 ksz8863 的 clko 引脚输出或者由外部晶振提供。如果 stm32 的 rmii_ref_clk 没有时钟mac 根本不工作。解决检查硬件原理图确认 rmii_ref_clk 的来源。如果是 ksz8863 输出需要配置 ksz8863 的时钟控制寄存器使能 clko。如果是外部晶振确认晶振是否起振。用示波器量 rmii_ref_clk 引脚应该有 50mhz 方波。4.4 交换机寄存器读写失败现象普通寄存器读写正常但交换机寄存器读出来全是 0 或者写不进去。原因命令字节的 bit6 没有置 1。普通寄存器和交换机寄存器共用同一套 spi 命令格式区别就在 bit6。很多人写了一个读写函数只传地址不传类型导致交换机寄存器被当成普通寄存器访问。解决把读写函数的第二个参数改成寄存器类型普通寄存器传 0x00交换机寄存器传 0x40。或者在函数内部根据地址范围自动判断。4.5 复位后芯片不响应现象上电后第一次 spi 通信就失败读什么都是 0xff。原因ksz8863 的上电复位时间不够。某些电源方案下ksz8863 的复位引脚需要保持低电平至少 10ms如果复位电路是 rc 延时电容选小了会导致复位不充分。解决在初始化代码最前面加 100ms 延时等电源和复位都稳定后再操作 spi。如果复位引脚是 gpio 控制的手动拉低 10ms 再拉高。检查电源电压是否在手册规定范围内ksz8863 对 3.3v 电源的纹波比较敏感。5. 进阶技巧用轮询加状态机替代阻塞延时以及一个验证链路的小工具5.1 为什么不要在初始化里塞一堆 hal_delay上面那些初始化代码里用了 hal_delay这在裸机 demo 里没问题但放到实际项目里就是灾难。自协商要等 500ms复位要等 10ms如果这些延时都阻塞在主循环里整个系统的实时性就没了。我一般会把 ksz8863 的初始化拆成状态机每个状态只做一件事然后靠 systick 或者定时器来驱动状态迁移。// ksz8863 初始化状态机 typedef enum { KSZ_STATE_RESET, KSZ_STATE_CHECK_ID, KSZ_STATE_PORT_INIT, KSZ_STATE_WAIT_AN, KSZ_STATE_DONE, KSZ_STATE_ERROR } ksz_state_t; ksz_state_t ksz_state KSZ_STATE_RESET; uint32_t ksz_tick 0; void ksz8863_poll(void) { switch (ksz_state) { case KSZ_STATE_RESET: ksz8863_reset(); ksz_tick HAL_GetTick(); ksz_state KSZ_STATE_CHECK_ID; break; case KSZ_STATE_CHECK_ID: if (HAL_GetTick() - ksz_tick 50) return; // 等复位完成 if (ksz8863_check_id()) { ksz_state KSZ_STATE_PORT_INIT; } else { ksz_state KSZ_STATE_ERROR; } break; case KSZ_STATE_PORT_INIT: ksz8863_port_init(KSZ_PORT1_BASE); ksz8863_port_init(KSZ_PORT2_BASE); ksz_tick HAL_GetTick(); ksz_state KSZ_STATE_WAIT_AN; break; case KSZ_STATE_WAIT_AN: if (HAL_GetTick() - ksz_tick 500) return; // 等自协商 if (ksz8863_get_link_status(KSZ_PORT1_BASE)) { ksz_state KSZ_STATE_DONE; } else { ksz_state KSZ_STATE_ERROR; } break; case KSZ_STATE_DONE: case KSZ_STATE_ERROR: default: break; } }逻辑说明每个状态只执行一次动作然后靠 hal_gettick 判断是否满足迁移条件。这样主循环里调用 ksz8863_poll 不会阻塞其他任务照常跑。参数 ksz_tick 记录状态进入的时间戳用来算延时。实际项目中可以把 hal_gettick 换成自己的定时器计数。5.2 一个验证链路状态的小工具调试的时候我习惯写一个简单的 shell 命令通过串口打印 ksz8863 的关键寄存器值。这样不用每次改代码重新烧录直接在终端里敲命令就能看状态。// 串口调试命令打印 ksz8863 状态 void ksz8863_dump(void) { printf(chip id: 0x%02x 0x%02x\r\n, ksz8863_read_reg(KSZ_REG_NORMAL, 0x00), ksz8863_read_reg(KSZ_REG_NORMAL, 0x01)); printf(port1 bmsr: 0x%02x\r\n, ksz8863_read_reg(KSZ_REG_NORMAL, KSZ_PORT1_BASE 0x01)); printf(port2 bmsr: 0x%02x\r\n, ksz8863_read_reg(KSZ_REG_NORMAL, KSZ_PORT2_BASE 0x01)); printf(port1 bmcr: 0x%02x\r\n, ksz8863_read_reg(KSZ_REG_NORMAL, KSZ_PORT1_BASE 0x00)); }参数说明bmsr 的 bit2 是链路状态bit5 是自协商完成。bmcr 的 bit12 是自协商使能bit9 是重启自协商。打印出来一眼就能看出是链路没 up 还是自协商没完成。5.3 几个寄存器速查寄存器地址关键位说明chip id00x00bit7-0预期 0x88chip id10x01bit7-4预期 0x6xglobal ctrl0x02bit4软复位port1 bmcr0x20bit12/bit9自协商使能/重启port1 bmsr0x21bit2/bit5链路状态/自协商完成port2 bmcr0x40bit12/bit9同 port1port2 bmsr0x41bit2/bit5同 port1这张表我一般贴在显示器边上调 ksz8863 的时候随时瞄一眼比翻手册快得多。从那以后我每次拿到新的 ksz8863 板子都强制先跑一遍 id 校验和 bmsr 打印确认 spi 通路和链路状态都正常了再往下写应用逻辑。这个习惯帮我省掉了至少三次“以为是软件问题结果是硬件没焊好”的返工。希望帮到你。本文还有配套的精品资源点击获取