- 嵌入式
- 固件
- 驱动开发
- 硬件开发
【免费下载链接】Arduino_Core_STM32
STM32 core support for Arduino
导读
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 引脚的管理,用户可以有两种选择:
- 软件管理 CS:与标准 Arduino SPI 库一致,由用户代码在传输前后手动拉低/拉高 CS 引脚(
digitalWrite(csPin, LOW)/digitalWrite(csPin, HIGH)); - 硬件 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):
| 参数 | 说明 |
|---|---|
mosi | SPI MOSI 引脚,可接受引脚数字、Arduino 格式(如Dx)或 ST 格式(如Pxy) |
miso | SPI MISO 引脚,格式同上 |
sclk | SPI 时钟引脚,格式同上 |
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时配置一次即可 |
八、易错点与使用建议
- 引脚合法性:
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); - 调用顺序:
setMISO/setMOSI/setSCLK/setSSEL必须在begin()之前调用,beginTransaction()必须在begin()之后调用; - 配置了硬件 CS 后不要混用手动 CS:README 明确要求配置硬件 CS 后不得再向任何函数传入其他 CS 引脚;
- 只发送场景:对纯发送(如写寄存器)可使用
SPI_TRANSMITONLY跳过接收,减少不必要的等待; - 哑字节:需要读取从设备数据但无需发送有效内容时,
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
相关推荐
ESP-IDF项目:ESP32-P4 SPI控制器硬件CS信号数量解析
ESP IDF项目:ESP32 P4 SPI控制器硬件CS信号数量解析 在ESP32 P4开发过程中,开发者经常需要了解其外设接口的具体规格,特别是SPI控制器
物联网嵌入式klog 过滤器完全教程:从日期范围到复杂逻辑表达式的进阶玩法
klog 过滤器完全教程:从日期范围到复杂逻辑表达式的进阶玩法 如果你正在用 klog 过滤器 来管理自己的时间记录,那么这篇教程就是为你准备的。klog 是一
OpenSearch Analytics Framework:理解分析引擎的 SPI 契约库与插件扩展体系
OpenSearch Analytics Framework:理解分析引擎的 SPI 契约库与插件扩展体系 本篇技术指南围绕 analytics framewo
搜索引擎全文检索可观测性数据分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考