W5500移植HAL库(下)

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-10-08 01:26:37 Views: 1 Likes: 0 Comments: 0

164.12.6W5500移植HAL库(下)

/*
 * 本代码块只收录本节视频实际查看、修改和调试的源码片段,并按课程最终工程原样保留语句。
 * 这些片段分别来自 W5500.h、W5500.c、spi.c 和 main.c,不是可以单独编译的完整程序。
 * 课程工程在接收缓冲区路径中仍有两处遗留问题,代码不在笔记中擅自修正,已在对应行和正文中标明。
 */

/* W5500.h:课程使用的片选与复位引脚定义。 */
#define W5500_SCS       GPIO_PIN_4       /* W5500 的 SCSn/CS 接到 PA4;HAL 库引脚宏为 GPIO_PIN_4。 */
#define W5500_SCS_PORT  GPIOA            /* SCSn 所在的 GPIO 端口是 GPIOA。 */
#define W5500_RST       GPIO_PIN_5       /* W5500 的低有效复位脚 RSTn 接到 PC5。 */
#define W5500_RST_PORT  GPIOC            /* RSTn 所在的 GPIO 端口是 GPIOC。 */

/* W5500.c:把标准库的单字节 SPI 发送替换为 HAL 全双工收发。 */
void SPI1_Send_Byte(unsigned char dat)                         /* 发送一个字节;SPI 同时收到的字节在写操作中不使用。 */
{                                                             /* 单字节交换函数开始。 */
    uint8_t recv;                                               /* 为 HAL 的接收指针准备一个真实的 8 位变量。 */
    HAL_SPI_TransmitReceive(&hspi1, &dat, &recv, 1, 100);       /* SPI1 交换 1 个 8 位数据单元,最长阻塞 100 ms。 */
}                                                             /* 函数结束;课程源码没有检查 HAL 返回状态。 */

void SPI1_Send_Short(unsigned short dat)                       /* 按高字节在前的顺序发送一个 16 位数。 */
{                                                             /* 16 位发送函数开始。 */
    SPI1_Send_Byte(dat / 256);                                  /* 整除 256 取得高 8 位并先发送。 */
    SPI1_Send_Byte(dat);                                        /* 传给 8 位形参时保留低 8 位并发送。 */
}                                                             /* 16 位发送函数结束。 */

unsigned char Read_W5500_1Byte(unsigned short reg)             /* 从 W5500 通用寄存器读取 1 字节。 */
{                                                             /* 通用寄存器单字节读取开始。 */
    unsigned char i, y = 0;                                     /* i 保存返回数据,y 是为读操作产生时钟的哑字节 0x00。 */
    HAL_GPIO_WritePin(W5500_SCS_PORT, W5500_SCS, GPIO_PIN_RESET); /* SCSn 拉低,开始一帧 W5500 SPI 事务。 */
    SPI1_Send_Short(reg);                                       /* 先发送 16 位寄存器偏移地址。 */
    SPI1_Send_Byte(FDM1 | RWB_READ | COMMON_R);                 /* 再发送控制字:通用寄存器、读、固定 1 字节。 */
    HAL_SPI_TransmitReceive(&hspi1, &y, &i, 1, 100);            /* 发送哑字节产生 8 个时钟,同时从 MISO 收到寄存器值。 */
    HAL_GPIO_WritePin(W5500_SCS_PORT, W5500_SCS, GPIO_PIN_SET); /* SCSn 拉高,结束本帧事务。 */
    return i;                                                   /* 返回读取到的 8 位寄存器值。 */
}                                                             /* 通用寄存器单字节读取结束。 */

unsigned char Read_W5500_SOCK_1Byte(SOCKET s, unsigned short reg) /* 从指定 Socket 的寄存器读取 1 字节。 */
{                                                             /* Socket 寄存器单字节读取开始。 */
    unsigned char i, y = 0;                                     /* i 接收有效数据,y 提供读阶段要发送的哑字节。 */
    HAL_GPIO_WritePin(W5500_SCS_PORT, W5500_SCS, GPIO_PIN_RESET); /* 选中 W5500。 */
    SPI1_Send_Short(reg);                                       /* 发送 16 位寄存器偏移地址。 */
    SPI1_Send_Byte(FDM1 | RWB_READ | (s * 0x20 + 0x08));        /* 控制字选择 Socket s 寄存器块,并读取 1 字节。 */
    HAL_SPI_TransmitReceive(&hspi1, &y, &i, 1, 100);            /* 发送一个 0x00,同时接收一个有效字节。 */
    HAL_GPIO_WritePin(W5500_SCS_PORT, W5500_SCS, GPIO_PIN_SET); /* 释放片选,结束事务。 */
    return i;                                                   /* 返回读取结果。 */
}                                                             /* Socket 寄存器单字节读取结束。 */

unsigned short Read_W5500_SOCK_2Byte(SOCKET s, unsigned short reg) /* 从指定 Socket 寄存器读取连续 2 字节。 */
{                                                             /* Socket 寄存器双字节读取开始。 */
    unsigned char i, j, y = 0;                                  /* i 接收高字节,j 接收低字节,y 是哑字节。 */
    HAL_GPIO_WritePin(W5500_SCS_PORT, W5500_SCS, GPIO_PIN_RESET); /* 选中 W5500。 */
    SPI1_Send_Short(reg);                                       /* 发送 16 位寄存器偏移地址。 */
    SPI1_Send_Byte(FDM2 | RWB_READ | (s * 0x20 + 0x08));        /* 控制字选择 Socket s 寄存器块,并读取 2 字节。 */
    HAL_SPI_TransmitReceive(&hspi1, &y, &i, 1, 100);            /* 第一次交换取得高 8 位。 */
    HAL_SPI_TransmitReceive(&hspi1, &y, &j, 1, 100);            /* 第二次交换取得低 8 位。 */
    HAL_GPIO_WritePin(W5500_SCS_PORT, W5500_SCS, GPIO_PIN_SET); /* 两个数据字节完成后再结束同一帧事务。 */
    return (unsigned short)(i) * 256 + (unsigned short)(j);     /* 高字节乘 256 后与低字节相加,拼成 16 位结果。 */
}                                                             /* Socket 寄存器双字节读取结束。 */

unsigned short Read_SOCK_Data_Buffer(SOCKET s, unsigned char *dat_ptr) /* 从 Socket RX 环形缓冲区读出一批数据。 */
{                                                             /* 接收缓冲区读取函数开始。 */
    unsigned short rx_size;                                     /* 保存 W5500 报告的本次可读字节数。 */
    unsigned short offset, offset1;                             /* offset 用于物理地址,offset1 保存并更新逻辑读指针。 */
    unsigned short i;                                           /* 循环计数器。 */
    unsigned char j, y = 0;                                     /* j 接收一个字节,y 是课程后来补入的哑字节变量。 */
    rx_size = Read_W5500_SOCK_2Byte(s, Sn_RX_RSR);              /* 读取 Socket 已接收数据长度。 */
    if (rx_size == 0) return 0;                                 /* 没有数据就立即返回 0。 */
    if (rx_size > 1460) rx_size = 1460;                         /* 单次最多读取 1460 字节。 */
    offset = Read_W5500_SOCK_2Byte(s, Sn_RX_RD);                /* 读取当前 RX 读指针。 */
    offset1 = offset;                                           /* 保存未取模的逻辑指针,稍后写回寄存器。 */
    offset &= (S_RX_SIZE - 1);                                  /* 在 2 KiB 环形缓冲区内求实际物理偏移。 */
    HAL_GPIO_WritePin(W5500_SCS_PORT, W5500_SCS, GPIO_PIN_RESET); /* 选中 W5500,开始读取 RX 缓冲区。 */
    SPI1_Send_Short(offset);                                    /* 发送 RX 缓冲区内的起始偏移地址。 */
    SPI1_Send_Byte(VDM | RWB_READ | (s * 0x20 + 0x18));         /* 选择 Socket s 的 RX 缓冲区并进入可变长度读取。 */
    HAL_SPI_TransmitReceive(&hspi1, &y, &j, 1, 100);            /* 课程源码先交换一次但未保存该字节;这是遗留的首字节丢弃风险。 */
    if ((offset + rx_size) < S_RX_SIZE)                         /* 判断本次数据是否会跨越环形缓冲区末尾。 */
    {                                                         /* 不跨界分支开始。 */
        for (i = 0; i < rx_size; i++)                           /* 按字节读取全部数据。 */
        {                                                     /* 单字节读取循环开始。 */
            SPI1_Send_Byte(0x00);                               /* 课程原语句保留:该函数本身已经交换一次并丢弃接收值。 */
            HAL_SPI_TransmitReceive(&hspi1, &y, &j, 1, 100);    /* 随后又交换一次,因此本分支存在每轮多发送一个哑字节的问题。 */
            *dat_ptr = j;                                       /* 保存第二次交换收到的字节。 */
            dat_ptr++;                                          /* 输出缓冲区指针移到下一个位置。 */
        }                                                     /* 单字节读取循环结束。 */
    }                                                         /* 不跨界分支结束。 */
    else                                                      /* 数据跨越 RX 环形缓冲区末尾。 */
    {                                                         /* 跨界分支开始。 */
        offset = S_RX_SIZE - offset;                            /* 计算从当前地址到缓冲区末尾的字节数。 */
        for (i = 0; i < offset; i++)                            /* 先读取末尾前的第一段数据。 */
        {                                                     /* 第一段循环开始。 */
            SPI1_Send_Byte(0x00);                               /* 课程原语句保留:再次产生一次 SPI 交换。 */
            HAL_SPI_TransmitReceive(&hspi1, 0x00, &j, 1, 100);  /* 课程最终源码仍传入 0x00 空指针;HAL 会返回错误,未真正发送哑字节。 */
            *dat_ptr = j;                                       /* 把 j 写入接收缓冲区;上一行失败时 j 不代表新收到的数据。 */
            dat_ptr++;                                          /* 移到输出缓冲区下一位置。 */
        }                                                     /* 第一段循环结束。 */
        HAL_GPIO_WritePin(W5500_SCS_PORT, W5500_SCS, GPIO_PIN_SET); /* 到达环形末尾,先结束当前 SPI 帧。 */
        HAL_GPIO_WritePin(W5500_SCS_PORT, W5500_SCS, GPIO_PIN_RESET); /* 重新拉低 SCSn,开始从地址 0 读取第二段。 */
        SPI1_Send_Short(0x00);                                  /* 第二段从 RX 缓冲区偏移 0 开始。 */
        SPI1_Send_Byte(VDM | RWB_READ | (s * 0x20 + 0x18));     /* 再次选择 Socket s 的 RX 缓冲区。 */
        HAL_SPI_TransmitReceive(&hspi1, &y, &j, 1, 100);        /* 课程源码再次先交换并丢弃一个接收字节。 */
        for (; i < rx_size; i++)                                /* 继续读取绕回缓冲区开头后的剩余数据。 */
        {                                                     /* 第二段循环开始。 */
            SPI1_Send_Byte(0x00);                               /* 课程原语句保留:本身已完成一次交换。 */
            HAL_SPI_TransmitReceive(&hspi1, &y, &j, 1, 100);    /* 又交换一次并保存结果,仍有跳字节风险。 */
            *dat_ptr = j;                                       /* 保存接收到的数据。 */
            dat_ptr++;                                          /* 输出指针后移。 */
        }                                                     /* 第二段循环结束。 */
    }                                                         /* 跨界分支结束。 */
    HAL_GPIO_WritePin(W5500_SCS_PORT, W5500_SCS, GPIO_PIN_SET); /* 结束最后一帧 RX 缓冲区读取。 */
    offset1 += rx_size;                                        /* 按已消费长度推进逻辑读指针。 */
    Write_W5500_SOCK_2Byte(s, Sn_RX_RD, offset1);              /* 把新的读指针写回 W5500。 */
    Write_W5500_SOCK_1Byte(s, Sn_CR, RECV);                    /* 发出 RECV 命令,通知 W5500 释放已读数据。 */
    return rx_size;                                            /* 返回本次声称读取的字节数。 */
}                                                             /* 接收缓冲区读取函数结束。 */

void W5500_Hardware_Reset(void)                               /* 对 W5500 执行硬件复位并等待网线链路。 */
{                                                             /* 硬件复位函数开始。 */
    HAL_GPIO_WritePin(W5500_RST_PORT, W5500_RST, GPIO_PIN_RESET); /* RSTn 拉低,进入复位。 */
    HAL_Delay(50);                                              /* 保持低电平 50 ms,超过数据手册要求的最短复位脉宽。 */
    HAL_GPIO_WritePin(W5500_RST_PORT, W5500_RST, GPIO_PIN_SET); /* RSTn 拉高,释放复位。 */
    HAL_Delay(200);                                             /* 等待芯片内部和 PHY 稳定。 */
    while ((Read_W5500_1Byte(PHYCFGR) & LINK) == 0);           /* 一直等待 LINK=1;未插网线时会永久阻塞。 */
}                                                             /* 硬件复位与链路等待结束。 */

/* spi.c:在 HAL_SPI_MspInit() 中补上 PA4/SCSn 初始化。 */
void HAL_SPI_MspInit(SPI_HandleTypeDef *spiHandle)             /* HAL_SPI_Init() 会调用的底层时钟和 GPIO 初始化入口。 */
{                                                             /* SPI MSP 初始化开始。 */
    GPIO_InitTypeDef GPIO_InitStruct = {0};                     /* 建立并清零 GPIO 配置结构体。 */
    if (spiHandle->Instance == SPI1)                            /* 只处理 SPI1 的底层资源。 */
    {                                                         /* SPI1 分支开始。 */
        __HAL_RCC_SPI1_CLK_ENABLE();                            /* 打开 SPI1 外设时钟。 */
        __HAL_RCC_GPIOA_CLK_ENABLE();                           /* 打开 PA4~PA7 所在 GPIOA 的时钟。 */
        GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_7;          /* 选择 PA5/SCK 和 PA7/MOSI。 */
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;                 /* SCK、MOSI 由 SPI1 复用功能推挽驱动。 */
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;           /* 配置输出驱动速度等级,不等于直接设置 SPI 波特率。 */
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);                 /* 应用 SCK 与 MOSI 的配置。 */
        GPIO_InitStruct.Pin = GPIO_PIN_6;                       /* 选择 PA6/MISO。 */
        GPIO_InitStruct.Mode = GPIO_MODE_INPUT;                 /* 主机从 MISO 接收数据,因此配置为输入。 */
        GPIO_InitStruct.Pull = GPIO_NOPULL;                     /* 不启用 STM32 内部上拉或下拉。 */
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);                 /* 应用 MISO 输入配置。 */
        GPIO_InitStruct.Pin = W5500_SCS;                        /* 选择课程定义的 PA4/SCSn。 */
        GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;             /* SCSn 是普通 GPIO,由软件以推挽方式控制。 */
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;           /* 设置 PA4 的输出驱动速度等级。 */
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);                 /* 应用 SCSn GPIO 配置;宏定义当前确实指向 GPIOA。 */
        HAL_GPIO_WritePin(W5500_SCS_PORT, W5500_SCS, GPIO_PIN_SET); /* 初始置高,保持 W5500 未选中。 */
    }                                                         /* SPI1 分支结束。 */
}                                                             /* SPI MSP 初始化结束。 */

/* main.c:课程用 OLED 汉字作为“程序执行到哪里”的分段标记,并每秒发送测试字符串。 */
int main(void)                                                /* HAL 工程主入口。 */
{                                                             /* 主函数开始。 */
    HAL_Init();                                                 /* 初始化 HAL,并建立 HAL_Delay() 使用的系统节拍。 */
    SystemClock_Config();                                       /* 配置系统时钟。 */
    Init_GPIO();                                                /* 初始化工程通用 GPIO。 */
    Init_UART1();                                               /* 初始化串口调试接口。 */
    //Init_TIM2();                                              /* 课程最终代码保留注释,不启用原标准库例程的定时器节拍。 */
    Init_SPI1();                                                /* 初始化 SPI1,并间接调用上面的 HAL_SPI_MspInit()。 */
    Init_I2C1();                                                /* 初始化 OLED 使用的 I2C1。 */
    OLED_Init();                                                /* 初始化 OLED 控制器。 */
    OLED_Clear();                                               /* 清空 OLED 旧内容。 */
    OLED_ShowCHinese(0, 0, 0);                                  /* 显示第 1 个调试汉字,证明程序运行到此处。 */
    OLED_ShowCHinese(18, 0, 1);                                 /* 显示第 2 个调试汉字。 */
    Load_Net_Parameters();                                      /* 把课程设置的 IP、端口和 MAC 等装入变量。 */
    OLED_ShowCHinese(36, 0, 2);                                 /* 显示第 3 个调试汉字。 */
    OLED_ShowCHinese(54, 0, 3);                                 /* 显示第 4 个调试汉字;前四字组成“好好学习”。 */
    W5500_Hardware_Reset();                                     /* 复位 W5500 并等待 PHY LINK。 */
    OLED_ShowCHinese(72, 0, 4);                                 /* 只有复位函数返回后才显示第 5 个字。 */
    OLED_ShowCHinese(90, 0, 5);                                 /* 显示第 6 个字。 */
    W5500_Initialization();                                     /* 初始化 W5500 寄存器和 Socket 0。 */
    OLED_ShowCHinese(108, 0, 6);                                /* 显示第 7 个字。 */
    OLED_ShowCHinese(64, 2, 7);                                 /* 显示第 8 个字,最终组成“好好学习,天天向上”。 */
    while (1)                                                   /* 进入不会主动退出的主循环。 */
    {                                                         /* 主循环开始。 */
        W5500_Socket_Set();                                     /* 尝试初始化或建立 Socket 0 连接。 */
        W5500_Interrupt_Process();                              /* 轮询 W5500 的 Socket 中断状态并更新软件标志。 */
        memcpy(Tx_Buffer, "\r\nSPI W5500 NET TEST!\r\n", 23); /* 准备 23 字节网络测试报文。 */
        Write_SOCK_Data_Buffer(0, Tx_Buffer, 23);               /* 经 Socket 0 发送测试报文。 */
        HAL_Delay(1000);                                        /* 每轮间隔约 1 秒,避免连续高速发送。 */
        printf("High level duration:  \n");                    /* 课程最终源码保留的串口调试输出。 */
    }                                                         /* 主循环结束位置;实际运行不会跳出 while。 */
}                                                             /* 主函数结束。 */

上面的代码保持课程最终工程语句不变,只增加解释性注释。特别是 Read_SOCK_Data_Buffer() 中残留的 0x00 空指针和重复哑字节交换没有被偷偷修正;它们是本课发送演示没有覆盖到的接收路径问题。

一、查找和替换

  • HAL 驱动按外设拆分。SPI 的公开接口声明在 stm32f1xx_hal_spi.h,实现位于 stm32f1xx_hal_spi.c。阅读 HAL 源码时,应先看函数原型、参数和返回值,再决定怎样替换标准库调用,不能只靠函数名称猜测。
  • 本节选择阻塞式 HAL_SPI_TransmitReceive(),因为 SPI 本来就是全双工:主机每送出一位,MISO 也同时移入一位。写 W5500 时可以忽略收到的字节;读 W5500 时仍必须在 MOSI 上发送哑字节,主机才能产生 SCK,让从机把数据移出。
  • HAL_SPI_Transmit()、HAL_SPI_Receive() 和 HAL_SPI_TransmitReceive() 都是 HAL 提供的合法接口。“前两个一定不好用、只有同时收发才兼容”不是通用结论。对当前 SPI1 主机双线全双工配置,使用收发接口能最直观地表达“发送哑字节并同时取回数据”。
  • 全局替换只适合名称明确且语义完全一致的情况。本节把旧 Delay(...) 替换为 HAL_Delay(...) 前,应限制在当前文件并逐个复核,避免误改函数声明、注释、其他同名符号或不同时间单位的延时。

HAL_SPI_TransmitReceive() 在本工程中的参数含义如下:

参数本节传入值含义
hspi&hspi1SPI1 句柄的地址,里面保存实例、模式、数据位宽和运行状态等信息
pTxData&dat 或 &y发送缓冲区地址,不能把要发送的数值直接当作地址
pRxData&recv、&i 或 &j接收缓冲区地址,HAL 把同步收到的数据写入这里
Size1交换 1 个数据单元;当前配置为 8 位,所以是 1 字节
Timeout100阻塞调用最多等待 100 ms,超时返回 HAL_TIMEOUT

课程源码没有保存 HAL_SPI_TransmitReceive() 的返回值。调试真实项目时,应区分 HAL_OK、HAL_BUSY、HAL_TIMEOUT 和 HAL_ERROR,否则“SPI 失败”会被误当成“W5500 返回了某个数据”。

二、W5500的SPI接口移植HAL库

2.1 SPI发送接收函数

SPI1 配置为主机、双线全双工、8 位、MSB 先行、CPOL=0、CPHA=第一个边沿,即 SPI Mode 0。W5500 官方数据手册说明它支持 Mode 0 和 Mode 3,并要求数据按 MSB 到 LSB 传输,因此本节配置与 W5500 匹配。

SPI1_Send_Byte() 中:

  • dat 是真正要发出的一个字节,所以传 &dat。
  • recv 只是承接同时移入的字节。写命令时该值没有业务意义,但必须给 HAL 一个有效接收地址。
  • Size=1 是一个 8 位数据单元,不是“1 bit”。
  • Timeout=100 的单位是毫秒,不是微秒。
  • 课程没有检查返回值,这不影响理解迁移思路,但不利于定位总线超时或状态错误。

W5500 的一帧 SPI 访问不是“随便发几个字节”,而是严格分为:

SCSn 拉低
   │
   ├─ 16 位 Offset Address
   ├─ 8 位 Control Byte:BSB[4:0] + RWB + OM[1:0]
   ├─ 1/2/4/N 字节 Data Phase
   │
SCSn 拉高

固定长度模式 FDM 用控制字中的 OM 指定 1、2 或 4 字节;可变长度模式 VDM 由 SCSn 何时拉高决定数据长度。无论哪种模式,同一帧的地址、控制字和数据阶段之间都不能随意释放片选。

2.1.1 例题1:W5500函数移植与调试

本节依次把 W5500.c 中与 MCU 平台相关的接口替换为 HAL:

  1. SPI_I2S_SendData() 与标志轮询替换为 HAL_SPI_TransmitReceive()。
  2. SPI_I2S_ReceiveData() 改为通过接收指针取得数据。
  3. GPIO_ResetBits()、GPIO_SetBits() 改为 HAL_GPIO_WritePin()。
  4. Delay() 改为 HAL_Delay()。
  5. 标准库工程中的 u8 改为 HAL/CMSIS 已定义的 uint8_t。

标准库读取函数直接返回寄存器数据,HAL 收发函数则通过 pRxData 指向的内存写回结果。两者的接口形式不同,所以不能机械地只替换函数名。

读取 16 位寄存器时,W5500 先返回高字节 i,再返回低字节 j:

i = 0x12,j = 0x34

高字节移到高 8 位:0x12 × 256 = 0x1200
加上低字节:      0x1200 + 0x34 = 0x1234

视频中一度说成“左移 256 位”,这是口误;乘以 256 等价于左移 8 位。还要补充一个 C 语言细节:在 STM32F1 的常见编译环境中,unsigned char 参加算术运算前会发生整型提升,因此 i * 256 本身通常不会先在 8 位宽度内变成 0。课程加入 (unsigned short) 强制转换能明确表达目标位宽,但“没有强转就一定丢失”并不是普遍结论。若把移位结果先强制存回 uint8_t,高 8 位才会被截掉。

不同函数中的同名局部变量 i、j 处于不同作用域,彼此不共享内存,也不会互相覆盖。

2.1.2 例题2:W5500的GPIO定义与初始化
  • STM32 标准外设库把 PA4 写作 GPIO_Pin_4,HAL 库写作 GPIO_PIN_4,字母大小写不同。C 语言标识符区分大小写,直接搬用旧宏会导致未定义符号。
  • 本工程的 W5500_RST 是 PC5,W5500_SCS 是 PA4。复位脚已在 W5500_GPIO_Configuration() 中初始化,但 Cube 生成的 SPI 代码只配置了 SCK、MISO、MOSI,不会自动知道 PA4 是业务层手动片选,因此必须额外初始化 SCSn。
  • 代码中反复执行 HAL_GPIO_WritePin() 不等于引脚已经配置为输出。没有先使能 GPIO 时钟并调用 HAL_GPIO_Init(),写引脚可能不会得到预期的电气行为。

2.3 查找W5500的SCS引脚初始化位置

2.3.1 例题1: 查找W5500的SCS引脚初始化位置

课程使用 Keil 的全局查找定位 W5500_SCS,再回到旧标准库工程确认 SCSn 原本在 SPI_Configuration() 中初始化。迁移时不能只搜索“哪里拉高、哪里拉低”,还要找到“哪里打开 GPIO 时钟、哪里设置输出模式和初始电平”。

在 HAL 工程里,Init_SPI1() 调用 HAL_SPI_Init(&hspi1),HAL 随后进入 HAL_SPI_MspInit() 配置底层时钟和引脚。因此,本节把 SCSn 的 GPIO 配置加到 HAL_SPI_MspInit() 的 SPI1 分支中。

2.3.2 例题2:W5500的SCS引脚初始化配置

SPI1 与 W5500 的连接关系为:

STM32F103(主机)                         W5500(从机)

PA5 / SPI1_SCK   ──────────────────────► SCLK
PA7 / SPI1_MOSI  ──────────────────────► MOSI
PA6 / SPI1_MISO  ◄────────────────────── MISO
PA4 / 普通 GPIO  ──────────────────────► SCSn(低电平选中)
PC5 / 普通 GPIO  ──────────────────────► RSTn(低电平复位)
GND              ─────────────────────── GND
  • PA5/SCK 和 PA7/MOSI 是 SPI 外设输出,配置为复用推挽 GPIO_MODE_AF_PP。
  • PA6/MISO 是主机输入,配置为 GPIO_MODE_INPUT。
  • PA4/SCSn 是软件片选,配置为普通推挽输出 GPIO_MODE_OUTPUT_PP。
  • 初始化完成后立即把 SCSn 拉高,表示暂不选中 W5500,防止初始化过程中形成半截 SPI 帧。
  • GPIO_SPEED_FREQ_HIGH 设置的是 STM32 GPIO 输出驱动的速度/边沿能力,不是把网络速度或 SPI 波特率直接设为“高速”。SPI 时钟频率由外设时钟和 SPI_BAUDRATEPRESCALER_4 决定。
  • 课程代码调用 HAL_GPIO_Init(GPIOA, ...),而片选宏也确实定义在 GPIOA,因此当前能对应上;若以后改引脚,初始化端口和 W5500_SCS_PORT 必须一起修改。
2.3.3 例题3:W5500的SCS引脚初始化代码错误排查与修改

链接器报告旧 Delay 未定义,说明 W5500.c 仍依赖原标准库工程的延时函数。本节把当前文件中的 9 处 Delay(...) 改为 HAL_Delay(...)。HAL_Init() 必须先完成,系统节拍正常后 HAL_Delay() 才能按毫秒工作。

视频编译输出实际是 0 个错误、6 个警告,不是完全无警告。能够生成并烧写程序只说明没有阻止链接的错误,警告仍应逐条查看,尤其要关注整数与指针转换、未使用变量及函数声明不匹配。

2.4 编译错误处理

2.4.1 编译错误现象与初步排查

本节先遇到的是下载工具/软件占用问题:关闭冲突软件后,程序才能重新烧录。这类问题属于主机端工具链或调试器资源冲突,与 W5500 协议代码不是同一层故障。排查时应把“编译”“下载”“程序运行”“网络通信”四个阶段分开,不要全部笼统称作“代码不好使”。

2.4.2 调试方法与硬件复位问题

课程把八个 OLED 汉字拆开放到关键函数前后,形成简易的执行路径标记:

显示“好好学习”
        │
        ▼
W5500_Hardware_Reset()
        │
        ▼
显示“天天” → W5500_Initialization() → 显示“向上”

第一次运行只看到“好好学习”,说明程序已经完成前面的初始化,但没有从 W5500_Hardware_Reset() 返回。函数中的两个延时都会结束,真正可能永久停住的是:

while ((Read_W5500_1Byte(PHYCFGR) & LINK) == 0);

PHYCFGR 的 bit 0 是只读的链路状态位:1 表示 Link up,0 表示 Link down。若 SPI 读取失败、网线未连接、对端未上电或自协商尚未完成,这个无超时循环都会让程序一直停在此处。因此 OLED 分段法只能把故障定位到函数或区间,不能单凭显示内容断言一定是哪一种根因。

更通用的调试顺序是先读 W5500 的 VERSIONR。官方数据手册规定该寄存器应为 0x04;读对版本号后,再检查 PHYCFGR.LNK 和 Socket 状态,可以把“SPI 底层错误”与“网线链路错误”分开。

2.4.3 发送函数错误与修正

课程最关键的问题是下面这个调用:

HAL_SPI_TransmitReceive(&hspi1, 0x00, &i, 1, 100);

第二个形参类型是发送缓冲区指针 const uint8_t *pTxData。这里的 0x00 不是“发送数值 0”,而是空指针。STM32 HAL 会检查发送或接收指针是否为空;为空时返回 HAL_ERROR,并不会替程序自动发送一个零。正确理解是:先定义一个值为 0 的 8 位变量,再把变量地址传入。课程因此增加 y=0,并改传 &y。

要特别注意,课程最终工程并没有把接收路径全部修净:

  • Read_SOCK_Data_Buffer() 的跨界第一段循环仍有一处 HAL_SPI_TransmitReceive(&hspi1, 0x00, &j, 1, 100)。
  • 同一函数先调用已经完成一次全双工交换的 SPI1_Send_Byte(0x00),随后又调用一次 HAL_SPI_TransmitReceive();这会为每个保存的数据产生两次 SPI 字节交换。
  • 控制字后还先执行了一次未保存结果的 HAL_SPI_TransmitReceive(),存在丢弃首个有效数据字节的风险。

这些语句已按课程源码原样保留在顶部代码块中。它们不影响本节演示的周期发送路径,因此“电脑收到测试字符串”不能证明 RX 缓冲区读取路径也正确。后续真正使用网络接收功能时,必须按 W5500 数据手册重新验证每一个时钟对应的接收字节。

2.4.4 修正后验证与成功

修正单字节寄存器读取所用的发送指针后,OLED 能完整显示“好好学习,天天向上”,说明程序已经越过硬件复位和 W5500 初始化位置。电脑端随后收到 SPI W5500 NET TEST!,证明本次演示所需的 SPI 寄存器访问、Socket 建立和发送路径可以运行。

这个验证范围需要准确表述:它验证了课程当时实际走到的初始化与发送路径,没有覆盖断网重连、HAL 超时返回、Socket RX 缓冲区跨界读取或长时间双向收发。

2.4.5 SPI通信原理

SPI 不是“TX、RX 两根线再加一根时钟线”的 UART。以主机为参照:

  • MOSI:Master Out, Slave In,主机发送、从机接收;
  • MISO:Master In, Slave Out,从机发送、主机接收;
  • SCK:由主机产生的同步时钟;
  • SCSn/CS:低电平选中 W5500,并界定一帧访问的起止;
  • 所有设备必须共地,W5500 还需要独立的低有效复位脚 RSTn。

SPI 的“快”来自同步时钟、硬件移位和很少的帧间协议开销,但实际可靠速率仍受 MCU 外设时钟、分频、GPIO 边沿、PCB 走线、线缆、模块电平和从机时序限制。W5500 数据手册提到理论设计速度可达 80 MHz,同时也警告高速下信号可能失真;本工程 APB2 为 72 MHz、SPI1 四分频时,SCK 为 18 MHz。

读 W5500 时,主机必须继续发送哑字节来制造 SCK。哑字节的具体数值通常不承载业务意义;重要的是传入一个真实、有效的内存地址,并保证一个期望接收字节只对应一次 SPI 交换。

参考资料