☰
Arduino_Core_STM32 SPI 库扩展详解:硬件 CS 引脚管理、Tx/Rx 双缓冲传输与 HAL 混合编程
2026/10/15 3:51:09 网站建设 项目流程
  • 嵌入式
  • 固件
  • 驱动开发
  • 硬件开发

【免费下载链接】Arduino_Core_STM32

STM32 core support for Arduino

项目地址:https://gitcode.com/gh_mirrors/ar/Arduino_Core_STM32
点击查看免费下载

导读

STM32 的 SPI 外设与 AVR 平台不同,其 NSS(片选)引脚可以由 SPI 外设本身直接驱动,从而实现完全由硬件管理片选信号的通信流程。Arduino_Core_STM32仓库中的 SPI 库(libraries/SPI)在完整保留标准 Arduino SPI API 的基础上,为此专门新增了构造函数重载、setSSEL()引脚重映射、transfer(const void *tx_buf, void *rx_buf, size_t count)双缓冲传输以及skipReceive只发不收参数等扩展能力。读完本文,你将掌握:如何创建绑定硬件 CS 引脚的 SPI 实例、如何在begin()之前重定义实例的默认引脚、如何用双缓冲一次性完成收发、以及如何通过getHandle()将 Arduino API 与 STM32Cube HAL 的 DMA 等能力混用。

一、库定位与片选(CS)管理的两种模式

SPI 库在library.properties中声明为This library is based on the official Arduino SPI library and adapted to STM32 boards,即它在标准 Arduino SPI API 之上针对 STM32 平台做了适配与扩展,核心类为SPIClass(定义于 SPI.h),全局默认实例为SPI(SPI.cpp)。

关于 CS 引脚的管理,用户可以有两种选择:

  1. 软件管理 CS:与标准 Arduino SPI 库一致,由用户代码在传输前后手动拉低/拉高 CS 引脚(digitalWrite(csPin, LOW)/digitalWrite(csPin, HIGH));
  2. 硬件 CS 管理:用户把一个由 SPI 外设关联的硬件 NSS 引脚交给 SPI 实例,片选信号的产生与释放由 SPI 外设自动完成。

两种方式的底层差异可以在 spi_com.c 的初始化代码中直接看到:

// Configure the SPI pins if (obj->pin_ssel != NC) { handle->Init.NSS = SPI_NSS_HARD_OUTPUT; // 硬件 NSS,输出由外设驱动 } else { handle->Init.NSS = SPI_NSS_SOFT; // 软件 NSS,片选交给用户代码 }

也就是说:只要实例的pin_ssel被配置为有效引脚(非NC),HAL 的NSS就设置为SPI_NSS_HARD_OUTPUT,片选由外设接管;否则回退为SPI_NSS_SOFT,由用户自己控制。

二、替代构造函数:SPIClass(mosi, miso, sclk, ssel)

标准用法是直接用默认引脚构造全局实例SPIClass SPI;(引脚取自variant*.h中定义的默认MOSI/MISO/SCK宏)。本库新增了一个可传入完整引脚组的构造函数:

SPIClass::SPIClass(uint8_t mosi, uint8_t miso, uint8_t sclk, uint8_t ssel)

参数说明(定义见 SPI.cpp):

参数说明
mosiSPI MOSI 引脚,可接受引脚数字、Arduino 格式(如Dx)或 ST 格式(如Pxy)
misoSPI MISO 引脚,格式同上
sclkSPI 时钟引脚,格式同上
ssel可选参数;必须是硬件 CS 引脚(该引脚必须能由 SPI 外设管理)。配置该引脚后,片选将由 SPI 外设接管

关键约束(源码注释明确给出):

  • 所有引脚必须挂接在同一个 SPI 外设上(见 SPI.cpp 的注释);spi_init()内部会通过pinmap_merge_peripheral()检查 MOSI/MISO/SCLK/SSEL 四路引脚是否都解析到同一个 SPI 实例,若不一致会打印ERROR: SPI pins mismatch并返回(spi_com.c);
  • 哪些引脚可以作为硬件 SSEL,需要查芯片数据手册,或查看对应板卡PeripheralPins.c中的PinMap_SPI_SSEL[]表。以 STM32G4 系列为例,G431C(6-8)U_G441CBU/PeripheralPins.cU_G441CBU/PeripheralPins.c#L280-L285) 中定义形如{PA_4, SPI1, ...}、{PA_15, SPI1, ...}的条目,即这些引脚属于对应 SPI 外设可管理的硬件 NSS 引脚;
  • 一旦配置了硬件 CS 引脚,不能再传另一个 CS 引脚给本类的任何函数,也不应该再通过digitalWrite()手动控制片选;
  • 注意硬件 NSS 输出时默认带LL_GPIO_PULL_UP(见上表PinMap_SPI_SSEL条目),SCK 引脚则根据 CPOL 极性自动配置上拉/下拉(spi_com.c)。

完整示例:硬件 CS 管理

以下是 README 给出的典型用法(本文补全了语句分号并增加SPISettings定义),适用于例如 STM32F1 系列上把 PC9 作为硬件 SSEL 的场景:

#include <SPI.h> // MOSI MISO SCLK SSEL SPIClass SPI_3(PC12, PC11, PC10, PC9); SPISettings settings(4000000, MSBFIRST, SPI_MODE0); void setup() { SPI_3.begin(); // 以默认 SPISettings 使能 SPI_3 SPI_3.beginTransaction(settings); // 以自定义 settings 重新配置 SPI_3 SPI_3.transfer(0x52); // 向第一个设备传输数据,CS 由外设自动管理 SPI_3.endTransaction(); SPI_3.end(); // 禁用 SPI_3 实例 } void loop() { }

三、begin()与beginTransaction()的初始化行为

在深入 transfer 之前,先明确实例初始化的行为,这关系到硬件 CS 是否真正生效:

  • begin()内部会把实例重置为默认SPISettings(4000000, MSBFIRST, SPI_MODE0, busMode),即默认时钟 4 MHz、MSB 先发、SPI 模式 0(SPI.cpp),默认常量SPI_SPEED_CLOCK_DEFAULT同样定义为 4 MHz(spi_com.h);
  • beginTransaction(settings)只有在新 settings 与当前配置不同时才真正调用spi_init()重新初始化(SPI.cpp);
  • spi_init()会根据设定的目标频率,从 2、4、8…直到 256 的预分频链中选取不低于目标频率的最高可用档位(spi_com.c),目标频率基于spi_getClkFreqInst()得到的该 SPI 实例外设时钟(PCLK1/PCLK2 等)计算;
  • end()调用spi_deinit(),执行 HAL 反初始化并关闭 SPI 外设时钟(SPI.cpp)。

对 STM32WB0x / STM32WL3x 等特殊系列,外设时钟按SystemCoreClock / 4计算(spi_com.c),这一点会影响你能得到的实际波特率上限,建议根据芯片数据手册确认。

四、修改默认SPI实例的引脚:setMISO/setMOSI/setSCLK/setSSEL

当不想新建实例、只想改变全局SPI实例的引脚时,可以使用以下 API(每个函数都有接收pin_size_t引脚数字与PinName两种重载,定义见 SPI.h):

void setMISO(uint32_t miso); // 或 setMISO(PinName miso) void setMOSI(uint32_t mosi); // 或 setMOSI(PinName mosi) void setSCLK(uint32_t sclk); // 或 setSCLK(PinName sclk) void setSSEL(uint32_t ssel); // 或 setSSEL(PinName ssel)

[!WARNING]这些调用必须在begin()之前进行。原因从实现可见:setXXX()只是改写_spi结构体中的PinName字段(SPI.h),真正的引脚 GPIO 配置与外设绑定是在后续begin()→spi_init()中完成的,begin()之后再调用不会重新执行初始化流程。

[!NOTE] 使用setSSEL()即可启用与 SPI 外设关联的硬件 CS 引脚管理(pin_ssel非NC时NSS自动切换为SPI_NSS_HARD_OUTPUT)。

示例:数字格式与 PinName 格式混用

void setup() { SPI.setMISO(PC_4); // 使用 PinName 格式 PY_n SPI.setMOSI(PC2); // 使用引脚数字格式 PYn // SPI.setSSEL(PA_4); // 可选:启用硬件 CS 管理 SPI.begin(2); // 以 2 MHz 波特率初始化默认 SPI 实例(busMode 可选) }

SPI.begin(2)传入了自定义时钟频率,此时实例将以 2 MHz 为目标速率初始化。此外begin()还支持传入SPIBusMode(SPI_CONTROLLER或SPI_PERIPHERAL,见 SPI.h),其中SPI_PERIPHERAL对应 HAL 的从机模式(SPI_MODE_SLAVE,见 spi_com.c),可用于把 STM32 配置为 SPI 从设备。

五、扩展 transfer 系列:双缓冲收发与 skipReceive

1. 新增双缓冲接口transfer(tx_buf, rx_buf, count)

标准 Arduino SPI 只有单缓冲transfer(buf, count)(收发共用同一块内存),本库新增了收发分离的双缓冲版本:

void transfer(const void *tx_buf, void *rx_buf, size_t count);

参数语义(见 SPI.cpp 的完整注释):

  • tx_buf:用户预先填好的待发送字节数组。若传NULL,外设将以 0xFF 哑字节(dummy bytes)作为发送内容;
  • rx_buf:接收缓冲区,从设备发回的数据会写入这里。若传NULL,收到的数据将被直接丢弃;
  • count:本次要发送/接收的字节数。

底层实现为spi_transfer()(spi_com.c),其逐字节流程是:等待发送缓冲区可写(TXP/TXE标志)→ 写入下一字节(tx_buf为NULL时写入0XFF)→ 等待接收标志(RXP/RXNE)→ 读取一字节写入rx_buf(rx_buf为NULL时仅读走丢弃)。因此这个接口特别适合"发送命令、读取响应"的典型 SPI 事务,例如:

uint8_t command[2] = {0x1F, 0x00}; uint8_t response[2]; SPI.transfer(command, response, 2); // 发送 2 字节并同时接收 2 字节

2. 所有标准 transfer 增加skipReceive参数

本库给全部标准transfer()接口追加了一个可选的bool skipReceive参数,用于跳过接收阶段、只做发送。两个常量定义于 SPI.h:

#define SPI_TRANSMITRECEIVE false // 默认值:发送并接收 #define SPI_TRANSMITONLY true // 只发送,不读取

涉及的重载(实现见 SPI.cpp):

uint8_t transfer(uint8_t data, bool skipReceive); // 单字节 uint16_t transfer16(uint16_t data, bool skipReceive); // 双字节(16 位) void transfer(void *buf, size_t count, bool skipReceive); // 多字节单缓冲

不传该参数时默认取SPI_TRANSMITRECEIVE(保持标准行为)。transfer16()在非 MSB 先发(LSB first)模式下还会自动做字节交换以保持数据语义正确(SPI.cpp)。

3. 传输超时保护

所有传输带 1000 ms 的默认超时保护(SPI_TRANSFER_TIMEOUT,定义于 spi_com.h),超时后spi_transfer()返回SPI_TIMEOUT。SPI_TRANSFER_TIMEOUT可被外部宏覆盖,但不能设为 0 或负数(源码中有#error防护)。若需要,可通过编译选项自定义该值。

六、与 STM32Cube HAL 混合编程:getHandle()

当标准 Arduino API 无法满足需求(例如要用 DMA 或直接操作 HAL 句柄)时,可以取回底层句柄:

SPI_HandleTypeDef *getHandle(void); // 传统 HAL(USE_HALV2_DRIVER 未定义时) hal_spi_handle_t *getHandle(void); // HAL v2 驱动(USE_HALV2_DRIVER 定义时)

实现见 SPI.h,它返回_spi.handle的地址,即与spi_t结构体(spi_com.h)中的 HAL 句柄共享同一份内存。README 的提示同样适用于此:Use at your own risk——拿到句柄后可以调用HAL_SPI_Transmit_DMA()、HAL_SPI_Receive_DMA()等 HAL 层 API,但要自行保证时序与资源管理的正确性,并注意不要在begin()之前取用句柄(此时尚未完成初始化)。

七、综合实战:从仓库示例看两种 CS 用法

仓库自带两个官方示例,恰好分别对应两种 CS 管理方式:

  • 软件 CS 示例:DigitalPotControl 驱动 AD5206 数字电位器,在digitalPotWrite()中手动digitalWrite(slaveSelectPin, LOW)/HIGH包住两次SPI.transfer(),片选完全由用户代码控制;
  • 软件 CS 示例(寄存器读写):BarometricPressureSensor 读取 SCP1000 气压传感器,同样通过digitalWrite(chipSelectPin, LOW/HIGH)手工管理片选,并使用SPI.transfer(0x00)获取从设备回读数据。

若改用本文介绍的硬件 CS 模式,则需把上述示例中的 CS 引脚替换为对应外设的硬件 NSS 引脚,并在构造或setSSEL()中配置,之后删除所有手动的digitalWrite(csPin, ...)调用,片选信号由 SPI 外设在每次传输时自动产生。两种模式的取舍可总结为:

维度软件 CS硬件 CS(本库扩展)
片选时机用户代码控制,可自由拉长/自定义时序由 SPI 外设自动产生/释放,时序紧凑
多从机灵活,任意 GPIO 均可作 CS受限,只能用外设关联的 NSS 引脚,一次只绑定一个设备
使用成本每个事务前后各一次digitalWrite构造/setSSEL时配置一次即可

八、易错点与使用建议

  1. 引脚合法性:ssel必须是PinMap_SPI_SSEL[]中存在的硬件 NSS 引脚,且 MOSI/MISO/SCLK/SSEL 四脚必须属于同一个 SPI 外设,否则spi_init()会报SPI pins mismatch/at least one SPI pin has no peripheral并放弃初始化(spi_com.c);
  2. 调用顺序:setMISO/setMOSI/setSCLK/setSSEL必须在begin()之前调用,beginTransaction()必须在begin()之后调用;
  3. 配置了硬件 CS 后不要混用手动 CS:README 明确要求配置硬件 CS 后不得再向任何函数传入其他 CS 引脚;
  4. 只发送场景:对纯发送(如写寄存器)可使用SPI_TRANSMITONLY跳过接收,减少不必要的等待;
  5. 哑字节:需要读取从设备数据但无需发送有效内容时,tx_buf传NULL即可,外设会自动发送 0xFF。

九、相关源码导航

  • 库主体与元数据:libraries/SPI(SPI.h、SPI.cpp、library.properties、keywords.txt)
  • 底层驱动封装:spi_com.c、spi_com.h
  • 官方示例:DigitalPotControl、BarometricPressureSensor
  • 硬件 SSEL 引脚映射表(各系列板卡):以 G431C(6-8)U_G441CBU/PeripheralPins.cU_G441CBU/PeripheralPins.c#L280-L285) 为参考,各 variant 目录下均有同名文件
  • SPI 能力查询宏:digitalPinHasSPI()(pins_arduino.h)可用于判断某引脚是否具备 SPI 复用功能
  • 嵌入式
  • 固件
  • 驱动开发
  • 硬件开发

【免费下载链接】Arduino_Core_STM32

STM32 core support for Arduino

项目地址:https://gitcode.com/gh_mirrors/ar/Arduino_Core_STM32
点击查看免费下载

相关推荐

上一篇:bup-config:Bup 备份系统的 Git 风格配置指南(仓库标识、分块粒度与服务器去重)
下一篇:PI 自定义工具完全指南:用 registerTool 给 LLM 赋予新能力

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询