W5500移植HAL库(上)

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

163.12.5W5500移植HAL库(上)

/*
 * 本代码块只收录第 163 课实际复制、生成、修改或定位到的源码片段,并按课程当时的进度保留语句。
 * 片段分别来自 spi.h、spi.c、main.c 和 W5500.c,前后并不是一个可单独编译的完整 C 文件。
 * 本课结束时,SPI1_Send_Byte() 仍调用标准库接口;它要到第 164 课才改成 HAL 接口。
 * 因此这里特意保留未移植完的写法,只加注释,不把下一课的代码提前补进来。
 */

/* spi.h:从 STM32CubeIDE 生成工程移入目标工程,并把初始化函数名改成课程使用的名称。 */
#ifndef __SPI_H__                                      /* 头文件保护开始,防止 spi.h 被重复展开。 */
#define __SPI_H__                                      /* 定义保护宏,后续再次包含时会跳过本文件主体。 */

#include "main.h"                                     /* 取得 HAL 类型、GPIO/SPI 宏以及 Error_Handler() 声明。 */

extern SPI_HandleTypeDef hspi1;                        /* 声明在 spi.c 中定义的 SPI1 HAL 句柄。 */

void Init_SPI1(void);                                  /* 声明课程重命名后的 SPI1 初始化函数。 */

#endif /* __SPI_H__ */                                 /* 头文件保护结束。 */

/* spi.c:由 STM32CubeIDE 按本课参数生成,再移入 Keil 工程。 */
#include "spi.h"                                      /* 引入本文件所需的 SPI1 句柄类型和函数声明。 */

SPI_HandleTypeDef hspi1;                               /* 定义 SPI1 句柄,保存实例和所有初始化参数。 */

void Init_SPI1(void)                                   /* 初始化 SPI1;原生成名 MX_SPI1_Init 在课程中被统一改名。 */
{                                                      /* SPI1 参数配置开始。 */
    hspi1.Instance = SPI1;                             /* 选择芯片内部的 SPI1 外设。 */
    hspi1.Init.Mode = SPI_MODE_MASTER;                 /* STM32 作为主机,主动产生串行时钟。 */
    hspi1.Init.Direction = SPI_DIRECTION_2LINES;       /* 使用独立 MOSI、MISO 的双线全双工模式。 */
    hspi1.Init.DataSize = SPI_DATASIZE_8BIT;           /* 每次 SPI 数据单元为 8 位。 */
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;         /* 空闲时 SCK 为低电平,即 CPOL=0。 */
    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;             /* 在第一个时钟边沿采样,即 CPHA=0。 */
    hspi1.Init.NSS = SPI_NSS_SOFT;                     /* 不让 SPI 硬件管理 NSS,W5500 片选改由普通 GPIO 控制。 */
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; /* SPI1 内核时钟四分频;本工程中得到 18 MHz SCK。 */
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;            /* 每个字节从最高有效位开始发送,这是位序而不是“大小端”。 */
    hspi1.Init.TIMode = SPI_TIMODE_DISABLE;            /* 禁用 TI 串行帧格式,使用 Motorola SPI 帧格式。 */
    hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; /* 关闭 STM32 SPI 外设的硬件 CRC 计算。 */
    hspi1.Init.CRCPolynomial = 10;                     /* 生成器保留的 CRC 多项式值;CRC 已禁用,所以本课中不参与传输。 */
    if (HAL_SPI_Init(&hspi1) != HAL_OK)                /* 写入 SPI 配置,并间接调用下面的底层 GPIO/时钟初始化。 */
    {                                                  /* HAL 初始化失败分支开始。 */
        Error_Handler();                               /* 进入工程统一错误处理函数。 */
    }                                                  /* 初始化失败分支结束。 */
}                                                      /* SPI1 参数配置结束。 */

void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle)     /* HAL_SPI_Init() 回调的底层资源初始化函数。 */
{                                                      /* SPI 底层初始化开始。 */
    GPIO_InitTypeDef GPIO_InitStruct = {0};            /* 建立 GPIO 配置结构体并清零,避免未赋值字段带入随机值。 */
    if(spiHandle->Instance==SPI1)                      /* 只处理 SPI1 句柄,避免误配置其他 SPI 外设。 */
    {                                                  /* SPI1 专属初始化分支开始。 */
        __HAL_RCC_SPI1_CLK_ENABLE();                   /* 打开 SPI1 外设时钟;未开时钟就不能访问 SPI1。 */
        __HAL_RCC_GPIOA_CLK_ENABLE();                  /* 打开 GPIOA 时钟,因为 PA5~PA7 承载 SPI1 信号。 */
        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;  /* 选择较高 GPIO 输出速度,以支持较快的 SPI 边沿。 */
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);        /* 将上述配置写入 PA5、PA7。 */
        GPIO_InitStruct.Pin = GPIO_PIN_6;              /* 改为选择 PA6(MISO)。 */
        GPIO_InitStruct.Mode = GPIO_MODE_INPUT;        /* MISO 是从 W5500 到 STM32 的输入信号。 */
        GPIO_InitStruct.Pull = GPIO_NOPULL;            /* 不启用 STM32 内部上拉或下拉。 */
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);        /* 将输入配置写入 PA6。 */
    }                                                  /* SPI1 专属初始化分支结束。 */
}                                                      /* 本课生成的底层初始化到此为止;PA4 片选初始化留到下一课。 */

/* main.c:本课从标准库例程原样复制的 W5500 业务函数。 */
void W5500_Initialization(void)                        /* 组织 W5500 寄存器、网关检测和 Socket 0 初始化。 */
{                                                      /* W5500 业务初始化开始。 */
    W5500_Init();                                      /* 初始化 W5500 的通用寄存器和网络参数。 */
    Detect_Gateway();                                  /* 通过 ARP 等过程检测配置的网关是否可达。 */
    Socket_Init(0);                                    /* 初始化编号为 0 的硬件 Socket。 */
}                                                      /* W5500 业务初始化结束。 */

void Load_Net_Parameters(void)                         /* 把本示例使用的网络参数写入全局数组。 */
{                                                      /* 网络参数装载开始。 */
    Gateway_IP[0] = 192;                               /* 默认网关第 1 个字节。 */
    Gateway_IP[1] = 168;                               /* 默认网关第 2 个字节。 */
    Gateway_IP[2] = 1;                                 /* 默认网关第 3 个字节。 */
    Gateway_IP[3] = 1;                                 /* 默认网关第 4 个字节,完整地址为 192.168.1.1。 */

    Sub_Mask[0]=255;                                   /* 子网掩码第 1 个字节。 */
    Sub_Mask[1]=255;                                   /* 子网掩码第 2 个字节。 */
    Sub_Mask[2]=255;                                   /* 子网掩码第 3 个字节。 */
    Sub_Mask[3]=0;                                     /* 子网掩码第 4 个字节,完整掩码为 255.255.255.0。 */

    Phy_Addr[0]=0x0c;                                  /* MAC 地址第 1 字节。 */
    Phy_Addr[1]=0x29;                                  /* MAC 地址第 2 字节。 */
    Phy_Addr[2]=0xab;                                  /* MAC 地址第 3 字节。 */
    Phy_Addr[3]=0x7c;                                  /* MAC 地址第 4 字节。 */
    Phy_Addr[4]=0x00;                                  /* MAC 地址第 5 字节。 */
    Phy_Addr[5]=0x01;                                  /* MAC 地址第 6 字节。 */

    IP_Addr[0]=192;                                    /* 本机 IPv4 地址第 1 个字节。 */
    IP_Addr[1]=168;                                    /* 本机 IPv4 地址第 2 个字节。 */
    IP_Addr[2]=1;                                      /* 本机 IPv4 地址第 3 个字节。 */
    IP_Addr[3]=199;                                    /* 本机 IPv4 地址第 4 个字节,完整地址为 192.168.1.199。 */

    S0_Port[0] = 0x13;                                 /* Socket 0 本地端口高字节;0x1388 等于十进制 5000。 */
    S0_Port[1] = 0x88;                                 /* Socket 0 本地端口低字节。 */

    S0_DIP[0]=192;                                     /* Socket 0 目标 IPv4 地址第 1 个字节。 */
    S0_DIP[1]=168;                                     /* Socket 0 目标 IPv4 地址第 2 个字节。 */
    S0_DIP[2]=1;                                       /* Socket 0 目标 IPv4 地址第 3 个字节。 */
    S0_DIP[3]=190;                                     /* Socket 0 目标 IPv4 地址第 4 个字节,完整地址为 192.168.1.190。 */

    S0_DPort[0] = 0x17;                                /* 目标端口高字节;0x1770 等于十进制 6000。 */
    S0_DPort[1] = 0x70;                                /* 目标端口低字节。 */

    S0_Mode=TCP_CLIENT;                                /* 把 Socket 0 的工作模式设为 TCP 客户端。 */
}                                                      /* 网络参数装载结束。 */

void W5500_Socket_Set(void)                            /* 根据 Socket 0 的模式建立监听、连接或 UDP 通道。 */
{                                                      /* Socket 状态管理开始。 */
    if(S0_State==0)                                    /* 只有状态为 0 时才尝试重新初始化 Socket 0。 */
    {                                                  /* 未初始化分支开始。 */
        if(S0_Mode==TCP_SERVER)                        /* 如果配置为 TCP 服务器模式。 */
        {                                              /* TCP 服务器分支开始。 */
            if(Socket_Listen(0)==TRUE)                 /* 初始化并让 Socket 0 进入监听,判断是否成功。 */
                S0_State=S_INIT;                       /* 成功后记录已初始化状态。 */
            else                                      /* 监听建立失败。 */
                S0_State=0;                            /* 保持 0,后续循环还可再次尝试。 */
        }                                              /* TCP 服务器分支结束。 */
        else if(S0_Mode==TCP_CLIENT)                   /* 如果配置为 TCP 客户端模式。 */
        {                                              /* TCP 客户端分支开始。 */
            if(Socket_Connect(0)==TRUE)                /* 让 Socket 0 主动连接目标 IP 和端口。 */
                S0_State=S_INIT;                       /* 调用成功后先记录初始化状态。 */
            else                                      /* 连接流程启动失败。 */
                S0_State=0;                            /* 保持未初始化状态,等待下次重试。 */
        }                                              /* TCP 客户端分支结束。 */
        else                                           /* 其余情况按 UDP 模式处理。 */
        {                                              /* UDP 分支开始。 */
            if(Socket_UDP(0)==TRUE)                    /* 初始化 Socket 0 的 UDP 模式。 */
                S0_State=S_INIT|S_CONN;                /* UDP 无需 TCP 握手,直接标记初始化且可通信。 */
            else                                      /* UDP 初始化失败。 */
                S0_State=0;                            /* 保持未初始化状态。 */
        }                                              /* UDP 分支结束。 */
    }                                                  /* 未初始化分支结束。 */
}                                                      /* Socket 状态管理结束。 */

void Process_Socket_Data(SOCKET s)                     /* 把指定 Socket 收到的数据原样回发。 */
{                                                      /* 回环处理开始;本课虽复制此函数,但主循环选择了主动发送路径。 */
    unsigned short size;                               /* 保存本次从 W5500 接收缓冲区读出的字节数。 */
    size=Read_SOCK_Data_Buffer(s, Rx_Buffer);           /* 读取 Socket s 的数据到全局接收缓冲区。 */
    memcpy(Tx_Buffer, Rx_Buffer, size);                /* 把收到的 size 字节复制到发送缓冲区。 */
    Write_SOCK_Data_Buffer(s, Tx_Buffer, size);        /* 将相同数据通过原 Socket 发回。 */
}                                                      /* 回环处理结束。 */

/* main.c 中本课实际加入的调用片段;这里只抄展示到的部分,不补成完整 main()。 */
Init_SPI1();                                           /* 在进入 W5500 业务前先初始化 SPI1。 */
Load_Net_Parameters();                                 /* 装载网关、掩码、MAC、本机及目标地址。 */
W5500_Hardware_Reset();                                /* 通过 RSTn 引脚对 W5500 执行硬件复位。 */
W5500_Initialization();                                /* 初始化寄存器、检测网关并初始化 Socket 0。 */

while(1)                                               /* 前台循环持续维护连接并周期发送测试字符串。 */
{                                                      /* 主循环片段开始。 */
    W5500_Socket_Set();                                /* 根据状态尝试初始化或建立 Socket 0 连接。 */
    W5500_Interrupt_Process();                         /* 轮询执行驱动中的 W5500 状态/中断处理函数,并非 MCU 中断入口。 */
    memcpy(Tx_Buffer, "\r\nSPI W5500 NET TEST!\r\n", 23); /* 把固定的 23 字节测试消息装入发送缓冲区。 */
    Write_SOCK_Data_Buffer(0, Tx_Buffer, 23);           /* 通过 Socket 0 发送这 23 个字节。 */
    HAL_Delay(1000);                                   /* 每轮阻塞约 1000 ms,避免无间隔连续发送。 */
    printf("High level duration:  \n");               /* 保留原工程的串口调试打印语句。 */
}                                                      /* 主循环片段结束。 */

/* W5500.c:把 PC5 复位脚的标准库初始化改写为 HAL GPIO 初始化。 */
void W5500_GPIO_Configuration(void)                    /* 初始化 W5500 的硬件复位输出脚。 */
{                                                      /* W5500 GPIO 配置开始。 */
    GPIO_InitTypeDef GPIO_InitStruct = {0};            /* 建立并清零 HAL GPIO 配置结构体。 */
    __HAL_RCC_GPIOC_CLK_ENABLE();                      /* 打开 GPIOC 时钟,因为 W5500_RST_PORT 是 GPIOC。 */
    GPIO_InitStruct.Pin = W5500_RST;                   /* 选择课程头文件映射到 PC5 的 W5500 复位脚。 */
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;        /* 配成普通推挽输出,能主动输出高、低电平。 */
    GPIO_InitStruct.Pull = GPIO_NOPULL;                /* 不使用内部上拉或下拉。 */
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;       /* 选择低速输出;复位信号不需要高速翻转。 */
    HAL_GPIO_Init(W5500_RST_PORT, &GPIO_InitStruct);   /* 将上述配置应用到复位脚。 */
    HAL_GPIO_WritePin(W5500_RST_PORT, W5500_RST, GPIO_PIN_RESET); /* 先把低有效 RSTn 拉低,进入复位状态。 */
}                                                      /* W5500 GPIO 配置结束。 */

/* W5500.c:课程定位到的标准库发送函数。本课停在这里,以下两行尚未 HAL 化。 */
void SPI1_Send_Byte(unsigned char dat)                 /* 通过 SPI1 发送 1 字节;这是驱动移植的关键适配点。 */
{                                                      /* 单字节发送函数开始。 */
    SPI_I2S_SendData(SPI1,dat);                        /* 标准库接口:把 dat 写入 SPI1 数据寄存器;HAL 工程暂不能直接使用。 */
    while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); /* 标准库接口:等待发送缓冲区空;移植留到第 164 课。 */
}                                                      /* 单字节发送函数结束。 */

void SPI1_Send_Short(unsigned short dat)               /* 按高字节在前的顺序发送一个 16 位数。 */
{                                                      /* 16 位发送函数开始。 */
    SPI1_Send_Byte(dat/256);                           /* 整除 256 取高 8 位,效果等价于无符号数右移 8 位。 */
    SPI1_Send_Byte(dat);                               /* 传入 8 位形参时只保留低 8 位。 */
}                                                      /* 16 位发送函数结束;它依赖单字节函数,所以本课不单独重写。 */

/* W5500.c:片选操作的实际替换示例;其余读写函数按同样规则逐处替换。 */
void Write_W5500_1Byte(unsigned short reg, unsigned char dat) /* 向 W5500 通用寄存器写入 1 字节。 */
{                                                      /* 单字节寄存器写事务开始。 */
    HAL_GPIO_WritePin(W5500_SCS_PORT, W5500_SCS, GPIO_PIN_RESET); /* SCSn 拉低,选中 W5500 并开始同一帧。 */
    SPI1_Send_Short(reg);                              /* 先发送 16 位寄存器地址。 */
    SPI1_Send_Byte(FDM1|RWB_WRITE|COMMON_R);           /* 发送控制字:固定 1 字节、写操作、通用寄存器块。 */
    SPI1_Send_Byte(dat);                               /* 发送要写入的 1 字节数据。 */
    HAL_GPIO_WritePin(W5500_SCS_PORT, W5500_SCS, GPIO_PIN_SET); /* SCSn 拉高,释放 W5500 并结束本帧。 */
}                                                      /* 单字节寄存器写事务结束。 */

void Write_W5500_nByte(unsigned short reg, unsigned char *dat_ptr, unsigned short size) /* 连续写入 n 字节。 */
{                                                      /* 可变长度写事务开始。 */
    unsigned short i;                                  /* 循环计数器,记录已经写出的字节数。 */
    HAL_GPIO_WritePin(W5500_SCS_PORT, W5500_SCS, GPIO_PIN_RESET); /* 一次拉低片选,整帧期间保持低电平。 */
    SPI1_Send_Short(reg);                              /* 发送 16 位起始寄存器地址。 */
    SPI1_Send_Byte(VDM|RWB_WRITE|COMMON_R);            /* 发送控制字:可变长度、写、通用寄存器块。 */
    for(i=0;i<size;i++)                                /* 循环 size 次,连续发送缓冲区中的数据。 */
    {                                                  /* 循环体开始。 */
        SPI1_Send_Byte(*dat_ptr++);                    /* 发送当前字节,再把指针移到下一个字节。 */
    }                                                  /* 循环体结束。 */
    HAL_GPIO_WritePin(W5500_SCS_PORT, W5500_SCS, GPIO_PIN_SET); /* 所有字节发送完后再拉高片选。 */
}                                                      /* 可变长度写事务结束。 */

void Write_W5500_SOCK_4Byte(SOCKET s, unsigned short reg, unsigned char *dat_ptr) /* 向 Socket 寄存器块写 4 字节。 */
{                                                      /* Socket 四字节写事务开始。 */
    HAL_GPIO_WritePin(W5500_SCS_PORT, W5500_SCS, GPIO_PIN_RESET); /* 选中 W5500。 */
    SPI1_Send_Short(reg);                              /* 发送 16 位 Socket 寄存器地址。 */
    SPI1_Send_Byte(FDM4|RWB_WRITE|(s*0x20+0x08));      /* 控制字选择 Socket s 寄存器块、写、固定 4 字节。 */
    SPI1_Send_Byte(*dat_ptr++);                        /* 发送第 1 个字节并推进指针。 */
    SPI1_Send_Byte(*dat_ptr++);                        /* 发送第 2 个字节并推进指针。 */
    SPI1_Send_Byte(*dat_ptr++);                        /* 发送第 3 个字节并推进指针。 */
    SPI1_Send_Byte(*dat_ptr++);                        /* 发送第 4 个字节并推进指针。 */
    HAL_GPIO_WritePin(W5500_SCS_PORT, W5500_SCS, GPIO_PIN_SET); /* 四个数据字节结束后释放片选。 */
}                                                      /* Socket 四字节写事务结束。 */

一、移植W5500的HAL库

本节不是从零编写 W5500 驱动,而是把标准外设库工程中的 W5500 业务代码迁入现有 HAL 工程。移植的关键不是机械地替换全部函数名,而是先分清三层:上层网络业务函数大多可直接复制;W5500 寄存器驱动大多可保留;凡是直接依赖 STM32 标准库的 SPI、GPIO、时钟接口,都必须在硬件适配层中处理。

本课只完成到 SPI 初始化、业务代码复制、复位脚 HAL 化和片选电平操作替换,并定位到 SPI1_Send_Byte()。单字节收发、读函数、PA4 片选引脚初始化及编译调试在第 164 课继续,不能把下一课成果倒填成本课结果。

1.1 新建工程

  • 视频在 STM32CubeIDE 中新建临时工程 18SPI,目标器件选择 STM32F103ZE。这个工程的作用是让配置工具生成可靠的 HAL SPI 初始化代码,不是最终承载 W5500 业务的 Keil 工程。
  • 时钟源选择外部高速晶振 HSE,系统时钟配置为 72 MHz。工程还勾选了“每个外设生成独立的 .c/.h 文件”,因此 SPI 代码落在独立的 spi.c 与 spi.h 中,便于迁移。
  • 选择 SPI1 不是凭感觉:旧 W5500 标准库例程中的 SPI_Configuration()、SPI1_Send_Byte() 和 PA5/PA6/PA7 引脚配置共同表明它使用 SPI1。
  • 这种“用 CubeIDE 生成外设底层,再复制到既有 Keil HAL 工程”的方式可以减少寄存器配置错误,但复制之后仍要检查句柄、头文件、模块宏、源文件分组和 GPIO 引脚是否完整。

1.2 寻找SPI初始化代码

1.2.1 SPI初始化函数与标准库
  • 标准库例程通过 SPI_InitTypeDef 填参数,再调用 SPI_Init(SPI1, &SPI_InitStructure);HAL 工程则把同类参数放在 hspi1.Init 中,再调用 HAL_SPI_Init(&hspi1)。
  • 两套库最终都在配置同一个 SPI1 硬件,但 API、类型名、初始化流程和错误处理方式不同,所以不能把“原理相同”理解为“函数可以混用”。
  • HAL_SPI_Init() 还会进入 HAL_SPI_MspInit(),由后者打开 SPI/GPIO 时钟并配置 PA5、PA6、PA7。只复制 Init_SPI1() 而漏掉 MSP 初始化,参数看似齐全,实际引脚仍不会工作。
  • 配套标准库工程中的 SPI_Configuration() 同时配置 SPI 引脚、PA4 片选和 SPI1 本体;CubeIDE 生成的代码在本课只生成 SPI1 的 SCK/MISO/MOSI,片选 GPIO 尚未补上。这正是下一课继续排查的原因之一。
1.2.2 SPI配置与参数设置
配置项本课取值初学者应理解的含义
实例SPI1使用 PA5=SCK、PA6=MISO、PA7=MOSI
主从SPI_MODE_MASTERSTM32 主动产生 SCK,W5500 为从设备
方向SPI_DIRECTION_2LINESMOSI、MISO 分线,全双工传输
数据宽度SPI_DATASIZE_8BIT每次 HAL 数据单元为 8 位
CPOL/CPHALow / 1EdgeCPOL=0、CPHA=0,即 SPI Mode 0
NSSSPI_NSS_SOFT不使用硬件 NSS,PA4 作为普通 GPIO 控制 SCSn
位序SPI_FIRSTBIT_MSB每个字节先发送 bit7,再到 bit0
TI 模式Disable使用常规 Motorola SPI 格式
CRCDisable不启用 STM32 SPI 硬件 CRC
  • 原笔记把 FirstBit 解释成“大小端”不准确。这里控制的是一字节在线路上的发送位序;多字节整数的高低字节顺序由 SPI1_Send_Short() 等软件逻辑决定。
  • W5500 数据手册支持 SPI Mode 0 和 Mode 3,并规定每个 SPI 帧包含 16 位地址、8 位控制字以及数据阶段。本课使用 Mode 0 与芯片要求相符。
1.2.3 SPI波特率预分频值的选择
  • 本工程 APB2 时钟为 72 MHz,SPI1 挂在 APB2。选择四分频后:72 MHz ÷ 4 = 18 MHz,因此 SCK 为 18 MHz。
  • 视频中尝试二分频时配置界面出现校验标记,于是改为四分频;旧例程也已经被课程作者改成四分频并能工作。本节最终源码必须以 SPI_BAUDRATEPRESCALER_4 为准。
  • “W5500 能承受多高时钟”和“STM32 当前电压、器件等级及配置允许输出多高 SPI 时钟”是两个约束,最终时钟必须同时满足两边,不能只看到 W5500 的上限就直接选择最快分频。
  • 分频越小,SCK 越快,但布线、信号完整性、从设备时序和 MCU 外设上限都更难满足。移植阶段先采用已经验证的 18 MHz,是可复现而非盲目追求峰值的选择。
1.2.4 SPI数据传输与CRC配置
  • CLKPolarity=LOW 与 CLKPhase=1EDGE 合起来表示 Mode 0:SCK 空闲低,接收端在第一个有效边沿采样。
  • SPI_NSS_SOFT 只说明 SPI 外设内部不使用硬件 NSS 管理从设备;它不会自动把 PA4 配成输出,也不会自动拉低 W5500 的 SCSn。
  • CRCCalculation=DISABLE 才是“CRC 关闭”的有效配置。CRCPolynomial=10 只是生成器仍写入的字段,在 CRC 禁用时不参与本课传输;原笔记所说“CRC 设置为 0”不符合生成的 HAL 源码。
  • 保存 CubeIDE 配置后生成代码,再从生成工程提取 spi.c 和 spi.h。生成代码只是起点,仍需按目标工程的命名和组织方式接入。

1.3 将SPI代码移植到新工程

1.3.1 移植前的准备与工程选择
  • 最终目标是 Keil 的 10W5500 工程,它由先前的 8IIC_LOED_IC 工程复制而来;视频多次提醒不要把文件加到仍然打开的其他工程中。
  • 复制旧工程能保留已经验证的 HAL 启动、72 MHz 系统时钟、GPIO、UART、I²C 和 OLED 基础,但也会带来上一实验的输入捕获、定时器和调试代码,需要有选择地清理。
  • SPI 初始化代码的来源是 CubeIDE 生成工程,不是从标准库 W5500 例程中直接复制。这样目标工程不会继续依赖 stm32f10x_spi.* 标准库接口。
1.3.2 新建与添加SPI相关文件
  • 在目标工程的 Core/Inc 放置 spi.h,在 Core/Src 放置 spi.c,并在 Keil 工程树的源文件组中使用 Add Existing Files 加入 spi.c。
  • 文件存在于磁盘不等于已经参与 Keil 构建;只有加入工程分组后,编译器才会编译它,链接器才可能找到其中定义的 hspi1 和 Init_SPI1()。
  • spi.h 通过 extern SPI_HandleTypeDef hspi1; 只做声明,真正的全局对象在 spi.c 中定义一次。若在头文件里再次定义,会造成重复定义问题。
1.3.3 修改代码与解决红线问题
  • 课程把 CubeIDE 生成的 MX_SPI1_Init() 改名为 Init_SPI1();函数定义、spi.h 原型和 main.c 调用必须同步修改。
  • 在 stm32f1xx_hal_conf.h 中启用 HAL_SPI_MODULE_ENABLED,HAL 的 SPI 类型和接口才会被纳入当前工程配置。
  • 同时检查三件事:main.c 包含 spi.h、spi.c 已加入 Keil 工程、HAL SPI 模块宏已启用。任何一项缺失都可能出现未声明或未定义错误。
  • 编辑器红线只是语法索引器的即时判断,最终应以实际 Build 输出为准;反过来,红线消失也不代表链接和硬件运行已经成功。
1.3.4 主函数修改与调试技巧
  • 清理从旧工程继承但本实验不再使用的 LED、输入捕获和定时器业务,保留 HAL_Init()、系统时钟、GPIO、UART、SPI、I²C、OLED 等本实验仍需要的初始化。
  • 把 Init_SPI1() 放在进入 W5500 业务之前,确保驱动第一次访问 SPI1 时外设已经配置。
  • 视频用串口 printf 和 OLED 显示位置作为“路标”:如果某条标记能显示而下一条不能,就能把停住的位置缩小到两条标记之间。它们是定位手段,不是 W5500 协议的一部分。
  • 此阶段构建达到 0 个错误,但仍有若干警告;所以原笔记的“确保工程干净无错”只能理解为先消除阻断编译的错误,不能把视频结果写成 0 warning。
1.3.5 添加W5500文件并编译
  • 从课程配套的标准库客户端例程复制 W5500.h 到头文件目录、复制 W5500.c 到源文件目录,并把 W5500.c 加入 Keil 工程。
  • 这两个文件来自课程使用的“泥人 W5500 模块客户端模式例程”,不能笼统称为 WIZnet 官方驱动。寄存器宏与网络逻辑可以复用,但 MCU 相关接口仍是 STM32 标准库写法。
  • 一旦把 W5500.c 加入构建,之前没有被编译到的标准库依赖才会暴露。因此“文件复制完”只是移植开始,不是移植完成。

1.4 正式的代码移植

1.4.1 例题2: 代码移植与初始化配置
  • main.c 新增 #include "W5500.h" 以获得驱动声明和全局变量声明,新增 #include <string.h> 以声明 memcpy()。
  • 不复制旧例程的 System_Initialization()、标准库 RCC/NVIC、软件 Delay() 和定时器初始化,因为目标 HAL 工程已经有自己的 HAL_Init()、SystemClock_Config() 和 HAL_Delay()。
  • 直接复制的是与网络业务有关的函数:W5500_Initialization()、Load_Net_Parameters()、W5500_Socket_Set() 和 Process_Socket_Data()。这些函数通过 W5500 驱动接口工作,本身不直接操作 STM32 寄存器。
1.4.2 例题3: 网络参数与函数移植
参数课程源码中的值说明
网关192.168.1.1本地网段的默认网关
子网掩码255.255.255.0本机与目标都处于 192.168.1.0/24
MAC0C:29:AB:7C:00:01示例写入 W5500 的物理地址
本机 IP192.168.1.199W5500 使用的 IPv4 地址
本地端口0x1388 = 5000Socket 0 的源端口
目标 IP192.168.1.190TCP 客户端准备连接的主机
目标端口0x1770 = 6000目标主机监听端口
模式TCP_CLIENTSocket 0 作为 TCP 客户端
  • 两字节端口号按高字节、低字节拆开保存:0x13 × 256 + 0x88 = 5000,0x17 × 256 + 0x70 = 6000。
  • 这些地址只是课程局域网示例,换到自己的网络时必须结合本机网段、网关以及服务端地址修改,不能把视频中的地址当作普适配置。
  • 示例 MAC 地址也不应在多块设备上重复使用;同一二层网络中 MAC 冲突会造成帧被错误投递。
1.4.3 例题4: 主函数与中断处理框架移植
  • 初始化顺序是:装载网络参数 → 硬件复位 W5500 → 初始化寄存器与 Socket。业务循环再调用 W5500_Socket_Set() 和 W5500_Interrupt_Process()。
  • 函数名虽然叫 W5500_Interrupt_Process(),本课是在 while(1) 中主动调用它,所以这里属于前台轮询处理,不是 STM32 的中断服务函数。
  • 旧标准库例程原本用定时器计数,在满足连接状态和 500 ms 条件时发送;本课暂时改成每轮直接写入 23 字节测试字符串,再用 HAL_Delay(1000) 控制频率。
  • Process_Socket_Data() 被复制进来,但本课选择“只发送、不做回环”的路径,主循环没有调用它。保留函数不等于当前执行了接收回发。
1.4.4 例题5: 代码移植后的调试与问题排查
  • 把业务函数粘进 main.c 后,表面上已经形成完整调用链;真正编译 W5500.c 时仍会遇到 GPIO_Init()、GPIO_ResetBits()、SPI_I2S_SendData() 等标准库接口。
  • 编译器通常在遇到前面的错误后继续能力有限,显示“一个错误”不代表文件中只剩一个问题。正确策略是从上到下修复最先出现的底层依赖,再重新构建观察新的错误。
  • 可移植部分与平台相关部分应分开看:网络参数、Socket 状态逻辑大多可直接保留;GPIO、SPI、时钟和延时必须按目标 HAL 工程逐项适配。
  • 本课末尾仍存在未移植的 SPI 标准库调用,所以不能宣称驱动已经能够运行。精教版源码也据此保留未完成状态。

1.5 W5500头的代码分析

1.5.1 W5500.h文件内容概览
  • W5500.h 主要包含寄存器地址、Socket 状态与命令、控制字字段、GPIO 宏、网络参数 extern 声明和驱动函数原型。
  • 大量 W5500 芯片寄存器宏不依赖 STM32 库,可以直接保留;但 GPIO 引脚宏仍写成标准库风格的 GPIO_Pin_4、GPIO_Pin_5,并非完全无需修改。这个宏不兼容问题在第 164 课才处理。
  • 因此,本课视频中“头文件基本不用动”应理解为先把注意力放到 W5500.c,不能理解成头文件已被验证为完全兼容 HAL。
  • 课程文件来自模块例程,不宜称为“官方预设的 WIZnet 驱动”;判断寄存器含义时应以 W5500 数据手册为准。
1.5.2 W5500.c文件修改与初始化
  • 先去掉旧工程专用的标准库头文件依赖,改用目标工程的 main.h、W5500.h 等头文件;后续 HAL SPI 句柄的使用还需要与 spi.h 衔接。
  • 从 HAL 工程已有 GPIO 初始化风格中复制结构体写法,再把目标改成 W5500_RST_PORT 与 W5500_RST,由此得到代码块中的 W5500_GPIO_Configuration()。
  • PC5 配为推挽输出、无上下拉、低速,初始化后立即输出低电平。这一动作使低有效的 W5500 RSTn 进入复位,后续 W5500_Hardware_Reset() 再按驱动时序释放它。
  • 只改函数名不够:标准库 GPIO_InitTypeDef 与 HAL GPIO_InitTypeDef 的字段布局不同,必须按 HAL 字段重新赋值,不能混搭 GPIO_Pin、GPIO_Speed 等旧字段。
1.5.3 W5500端口初始化配置与中断处理
  • 原笔记把 GPIO 速度写成“10 Hz、20 Hz”等,单位和含义均不正确。STM32F1 标准库常见的是 2 MHz、10 MHz、50 MHz 输出模式;HAL 用 LOW、MEDIUM、HIGH 等枚举表达对应档位。
  • GPIO “Speed”约束的是输出驱动边沿/带宽能力,并不表示这个引脚会自动以该频率翻转。RSTn 只偶尔改变电平,用低速档即可;SPI SCK/MOSI 则使用高速复用输出。
  • 本课没有为 W5500 INTn 建立 STM32 外部中断配置。主循环调用的处理函数仍是轮询,因此标题里的“中断处理”不能误解成 NVIC/EXTI 已经移植完成。
1.5.4 W5500 GPIO的配置
  • W5500 与 STM32 间至少涉及两类控制线:PC5 上的 RSTn 用于硬件复位,PA4 上的 SCSn 用于划定每一帧 SPI 事务。SCK/MOSI/MISO 则由 SPI1 外设驱动或采样。
  • 本课先完成 RSTn 的 HAL 初始化,再把各寄存器读写函数中的 SCSn 高低电平操作改为 HAL_GPIO_WritePin()。
  • 这里不是“移植 Pot”。转录中的该词是语音识别错误,视频实际是在继续移植 GPIO/Port 相关代码。
1.5.5 GPIO初始化与低电平设置
  • HAL_GPIO_Init() 只决定引脚工作模式;HAL_GPIO_WritePin(..., GPIO_PIN_RESET) 才把输出锁存值写成低电平。两步分别回答“这个脚怎样工作”和“它当前输出什么”。
  • GPIO_PIN_RESET 表示低电平,GPIO_PIN_SET 表示高电平。它们是 HAL 的电平状态枚举,不要与 W5500 的复位引脚名称 W5500_RST 混淆。
  • W5500 的 RSTn 低有效,因此初始化后拉低代表“进入复位”,不是“复位完成”。驱动稍后必须再拉高并满足芯片复位时序,芯片才能开始正常工作。
1.5.6 时钟与结构体初始化
  • 使用 GPIOC 前先调用 __HAL_RCC_GPIOC_CLK_ENABLE();外设时钟关闭时,即使后面的 GPIO 语句语法正确,寄存器配置也不会按预期生效。
  • GPIO_InitTypeDef GPIO_InitStruct = {0}; 同时完成变量声明和全字段清零。随后只对本次需要的 Pin、Mode、Pull、Speed 赋值,能避免结构体残留未定义内容。
  • 原笔记中的“把 a 改为 c 以触发屏”是识别错误。视频操作的真实含义是:代码模板原本开 GPIOA 时钟,而复位脚位于 PC5,所以要改为开启 GPIOC 时钟。
1.5.7 SPI配置
  • HAL 工程已经由 Init_SPI1() 和 HAL_SPI_MspInit() 完成 SPI1 参数、时钟及 PA5~PA7 配置,不应再保留标准库版 SPI_Configuration()。
  • 两套初始化同时存在不仅重复,还会混入无法解析的标准库类型和函数。因此视频把旧 SPI_Configuration() 整段删除。
  • 但删除旧函数也会一并失去其中对 PA4/SCSn 的 GPIO 初始化。本课尚未补回这一段,这个缺口会在第 164 课通过调试发现并修复。

1.6 W5500头的代码分析

这一节标题与 1.5 重复,是原笔记既有结构,精校版按要求保留;内容实际转向 W5500 驱动最底层的 SPI 字节发送和片选操作。

1.6.1 SPI_Configuration函数分析
  • 标准库版函数一次完成 GPIOA/SPI1 时钟、PA5~PA7、PA4 片选和 SPI1 参数配置。HAL 版将这些职责拆到 Init_SPI1()、HAL_SPI_MspInit() 和普通 GPIO 初始化中。
  • 视频删除标准库函数的原因是目标工程已经调用 HAL 版 Init_SPI1(),不是因为 SPI 不再需要初始化。
  • 移植时删除一个“大而全”的旧初始化函数,必须列出它原来承担的每项职责。本课只接回了 SPI1 和 PA5~PA7,遗漏的 PA4 片选初始化成为下一课的问题。
1.6.2 发送字节函数send byte分析
  • SPI1_Send_Byte() 是上层所有 W5500 地址、控制字和数据写入最终汇聚的接口,因此只要把这个硬件适配点迁好,大量调用它的上层函数无需改写。
  • 本课只定位并分析了标准库的 SPI_I2S_SendData() 与 SPI_I2S_GetFlagStatus(),尚未写入 HAL 替代实现。精教版代码原样停在这里。
  • SPI1_Send_Short() 不直接依赖某套 STM32 库,它只是调用两次 SPI1_Send_Byte(),所以本课保留。dat/256 取得高字节,等价于对无符号 16 位数右移 8 位,不是“右移一位”。
  • 先发高字节、再发低字节,正好满足 W5500 SPI 帧开头 16 位地址的顺序。
1.6.3 片选信号CS操作分析
  • W5500 数据手册把该引脚记作 SCSn:低电平有效。拉低表示选中芯片并开始一帧,保持低电平发送地址、控制字和全部数据,最后拉高结束这一帧。
  • 标准库的 GPIO_ResetBits(port, pin) 对应 HAL 的 HAL_GPIO_WritePin(port, pin, GPIO_PIN_RESET);GPIO_SetBits(...) 对应 GPIO_PIN_SET。视频采用逐处替换,避免把复位脚等其他同名操作误改。
  • 原笔记曾把低电平替换成 HAL 的 SET,这是错误的。最终规则始终是:传输前 RESET(低,选中),传输后 SET(高,释放)。
  • 本课虽然替换了驱动函数中的“写电平”语句,却还没有把 PA4 配成输出。GPIO 未初始化时,正确的拉低/拉高调用也不足以保证引脚行为,所以下一课还要补 SCSn 初始化。
1.6.4 SPI1发送1个字节数据函数分析
  • 标准库 SPI_I2S_SendData(SPI1, dat) 的核心效果是把数据写入 SPI1 数据寄存器 DR,硬件随后按已配置的时序移出 MOSI,同时也会从 MISO 移入数据。
  • SPI_I2S_FLAG_TXE 表示发送数据寄存器为空,可以再写一个数据;它不等同于整帧已经在引脚上发送完毕,后者还涉及 BSY 状态。
  • SPI 是全双工外设,即使只想“发送”,接收方向也同时得到一个字节。第 164 课因此会寻找 HAL 的全双工收发接口;本课不能提前写出那段实现。
  • 原笔记把数据寄存器写成“DRR”不准确,STM32F1 SPI 使用的数据寄存器名称是 SPI_DR/DR。
1.6.5 SPI1发送2个字节数据函数分析
  • 课程代码没有寻找一个独立的“HAL 发送 2 字节函数”;SPI1_Send_Short() 只是把 16 位参数拆开,并连续调用两次单字节适配函数。
  • 第一次传入高 8 位,第二次由于形参是 unsigned char 而保留低 8 位。例如 0x1234 的发送顺序是 0x12、0x34。
  • 这种设计把平台差异集中在 SPI1_Send_Byte():下一课只要正确替换单字节底层接口,SPI1_Send_Short() 以及大量 W5500 写函数即可继续复用。
  • 截止本课,迁移状态可概括为:HAL SPI 初始化已接入、业务层已复制、RSTn 与 SCSn 的写电平语句已换成 HAL,但单字节 SPI 收发和 SCSn GPIO 初始化仍未完成,因此暂时不能宣称通信成功。

参考资料