nRF52832 SPIM主机驱动:EasyDMA与软件片选时序实战
简介面向嵌入式开发者的 nRF52832 SPI 主机驱动代码适用于需要控制传感器、显示屏或闪存等从设备的低功耗蓝牙项目开发者可据此快速理解并实现 nRF52832 作为 SPI 主机的完整通信逻辑并减少底层寄存器配置的重复工作。资源为 zip 压缩包共 2 个文件含 1 个 .c 源文件和 1 个 .h 头文件包体仅 8KB结构精简便于直接查阅、修改与移植。驱动覆盖时钟配置、引脚复用、四种 SPI 模式选择、片选信号管理、初始化、数据收发、中断处理及错误处理等关键环节并提供了 SPI 配置结构体与常量定义使用者可在此基础上针对具体从设备调整时钟速率、极性和相位或扩展多片选管理与多字节传输逻辑。目前已有 162 人学习下载适合熟悉 C 语言和基础 SPI 协议、希望基于 Nordic SDK 快速上手外设驱动的开发者参考。1. nRF52832 SPI主机驱动的真正难点不在初始化而在DMA和片选时序把STM32那套“读写DR寄存器、等标志位”的SPI主机驱动代码搬到nRF52832上至少会让CPU多烧掉一大截运气差一点的还会在读数据时莫名其妙收到0xFF。原因在于nRF52832的SPI主机外设不叫SPI叫SPIMSPI Master with EasyDMA它把发送和接收都交给了EasyDMA通道驱动代码更像是在“安排任务、等事件通知”而不是“逐字节搬运”。本标题要解决的就是这一套从零搭建的驱动路径外设选型、寄存器与事件机制、可编译的最小主机驱动、软件片选时的SPI时序处理以及不依赖示波器的验证方法。适合正在调试OLED、Flash、SD卡或射频从机、被EasyDMA的buffer约束和片选拉高时机卡住的人。2. SPIM与nrfx_spim选型先把“谁的SPI是主机”这件事说清楚2.1 为什么优先用nrfx_spim而不是从寄存器手写nRF52832内部有SPI0、SPI1、SPI2三个SPI外设每个外设既可以工作在SPIM主机模式也可以工作在SPIS从机模式同一时刻只能选一种。做主机驱动时SDK里对应的就是nrfx_spim驱动层文件为nrfx_spim.h。老项目里还能看到nrf_drv_spi.h那是SDK 15之前对nrfx_spim的再封装现在的新代码建议直接用nrfx_spim接口更薄回调参数也更直观。从寄存器手写也完全可行但不划算。SPIM的寄存器结构是“任务/事件”风格写TASKS_START启动传输接收EVENTS_END通知结束期间数据由EasyDMA从RAM直接搬进TXD和RXD缓冲区。自己管理这套逻辑至少要处理SHORTS、INTENSET和MAXCNT三个寄存器组而nrfx_spim已经把“缓存描述符、发起传输、事件回调、错误码”给包好了。更关键的是nrfx_spim能正确处理PSEL引脚断开时的配置避免把SPI引脚误配成NFC天线引脚。2.2 SPIM的“任务/事件”模型与两个DMA问题传统MCU的SPI主机驱动是写数据寄存器、查询忙标志nRF52832的SPIM不是这样。它把一次双向传输描述成提供发送缓冲区指针、接收缓冲区指针、各自长度然后写入TASKS_START。此后SCK由外设自己产生MOSI数据由EasyDMA按字节搬出MISO数据由另一个EasyDMA通道搬入RAM全部完成后产生EVENTS_END事件。驱动代码要做的就是三件事描述这次传输、启动它、在事件回调里收尾。这也直接回答了热词里“SPI需要两个DMA吗”这个普遍疑问。在nRF52832上SPIM内部为发送和接收各配备了一个EasyDMA通道用户不需要像某些MCU那样手动分配两个DMA通道并配置传输方向。但代价是收发缓冲区必须是RAM地址不能是Flash里的常量数组也不能是栈上未对齐的小变量。这是移植STM32代码时最常踩的坑把一个const uint8_t数组直接丢给发送接口结果是复位或者总线错误。2.3 SPIM关键寄存器与事件速查即使最终用nrfx_spim封装理解下面这张表也能帮助排查驱动问题。这些寄存器都在外设基地址上芯片手册里的章节名就是“SPIM — SPI master with EasyDMA”。寄存器/事件作用驱动排错时的意义PSEL.SCK/MOSI/MISO/CSN配置引脚复用读回值不是预期引脚时说明GPIO被占用或未写入FREQUENCY设置SCK频率频率枚举值与实际波形要一致TASKS_START启动一次传输写1后应看到SCK有输出EVENTS_END整帧收发完成中断里判断这个事件不能查“忙标志”EVENTS_STARTED传输已经开始做时序测量时使用TXD.MAXCNT/RXD.MAXCNT收发最大字节数超出255字节时要确认是否分段RXD.AMOUNT实际接收到的字节数短帧时核对从机是否真的回了数据3. 从零搭建nrfx_spim最小主机驱动配置、初始化与一次真实读写3.1 在 sdk_config.h 里启用 nrfx_spimnRF5 SDK默认很多模块是关闭的直接调用nrfx_spim_init会链接报错。需要先在sdk_config.h中找到NRFX_SPIM_ENABLED把它置为1。如果项目里还用了老式nrf_drv_spi要同时确认NRFX_SPI_ENABLED不会和SPIM抢占外设号。建议所有SPI主机代码统一走NRFX_SPIM_ENABLED因为nrfx_spim支持0到2三个实例分别对应SPI0、SPI1、SPI2。#define NRFX_SPIM_ENABLED 1 #define NRFX_SPIM0_ENABLED 1 #define NRFX_SPIM1_ENABLED 0 #define NRFX_SPIM2_ENABLED 0提示如果还要用PPI或定时器做自动周期采样SPIM0的实例名是NRFX_SPIM_INSTANCE(0)不要和SPI0外设宏搞混。3.2 初始化参数频率、模式、位序一次设对nrfx_spim_config_t是初始化结构体最少要关注9个字段。引脚分配按实际硬件连接填SCK和MOSI是输出MISO是输入CSN在纯硬件片选时可交给外设但大多数从机要求CS低电平保持到整包结束所以我这里用普通GPIO做软件片选ss_pin填NRF_GPIO_PIN_MAP(0, 10)这类具体引脚也可以后面再详细对比如下。参数取值说明sck_pin如NRF_GPIO_PIN_MAP(0, 3)SPI时钟必须支持高频输出mosi_pin主机输出引脚从机MISO接这里miso_pin主机输入引脚从机MOSI接这里ss_pin软件片选引脚可填NRF_GPIO_PIN_MAP或特殊断开值frequencyNRF_SPIM_FREQ_1M/2M/4M/8M实际最高还要受GPIO驱动能力影响modeNRF_SPIM_MODE_0/1/2/3CPOL/CPHA四个组合bit_orderNRF_SPIM_BIT_ORDER_MSB_FIRST绝大多数传感器和Flash是MSB先出miso_loopbackfalse自测时可以置trueuse_hw_ssfalse软件片选时必须为false3.3 最小主机驱动代码读一次JEDEC ID下面这段代码可以直接编译进一个空的nRF52832工程作用是向一颗常见的SPI NOR Flash发送0x9F命令读取3字节JEDEC ID。这里选Flash是因为它只依赖最标准的SPI时序不涉及LCD初始化那种复杂的“先配置后写显存”流程。#include nrfx_spim.h #include nrf_gpio.h #include nrf_delay.h #define PIN_SCK NRF_GPIO_PIN_MAP(0, 3) #define PIN_MOSI NRF_GPIO_PIN_MAP(0, 4) #define PIN_MISO NRF_GPIO_PIN_MAP(0, 5) #define PIN_CS NRF_GPIO_PIN_MAP(0, 6) static nrfx_spim_t spim NRFX_SPIM_INSTANCE(0); static void spi_event_handler(nrfx_spim_evt_t const *p_event, void *p_context) { if (p_event-type NRFX_SPIM_EVENT_DONE) { nrf_gpio_pin_set(PIN_CS); // 拉高片选结束整帧 } } static void spi_init(void) { nrfx_spim_config_t cfg NRFX_SPIM_DEFAULT_CONFIG(PIN_SCK, PIN_MOSI, PIN_MISO, PIN_CS); cfg.frequency NRF_SPIM_FREQ_4M; cfg.mode NRF_SPIM_MODE_0; cfg.bit_order NRF_SPIM_BIT_ORDER_MSB_FIRST; cfg.use_hw_ss false; nrf_gpio_cfg_output(PIN_CS); nrf_gpio_pin_set(PIN_CS); nrfx_spim_init(spim, cfg, spi_event_handler, NULL); } static void read_jedec_id(uint8_t *id) { uint8_t cmd[4] { 0x9F, 0x00, 0x00, 0x00 }; nrf_gpio_pin_clear(PIN_CS); // 拉低片选从机开始响应 nrfx_spim_xfer_desc_t xfer { .p_tx_buffer cmd, .tx_length 4, .p_rx_buffer id, .rx_length 3, }; nrfx_spim_xfer(spim, xfer, 0); }这段代码初始化了GPIO片选然后发起一次4字节发送、3字节接收的全双工传输。nrfx_spim_xfer是异步的函数返回时数据可能还没收完真正的完成信号在spi_event_handler里。片选拉低放在调用nrfx_spim_xfer之前保证从机在SCK第一个边沿之前就看到CS有效。因为读JEDEC ID时主机还需要继续发时钟所以发送了4字节而接收缓冲区只收3字节SPIM会同时产生12个SCK时钟正好满足0x9F加3字节读出的时序。3.4 buffer约束EasyDMA只认RAMp_tx_buffer和p_rx_buffer都必须指向RAM不能指向Flash里的字符串常量或const数组。如果一定要发送一个存放在Flash的协议头先拷贝到一个局部uint8_t数组再传给SPIM。另一个约束是缓冲区地址需要按sizeof(uint32_t)对齐栈上申明uint8_t buf[4]在大多数情况下没问题但结构体里只包含uint8_t成员时可能出现未对齐指针。遇到HardFault时优先怀疑是不是某个buffer的地址落到了非RAM区域。对超过255字节的传输MAXCNT寄存器是8位的理论上限是255。实际项目中读写Flash页或LCD显存时经常超过这个长度正确做法是拆成多个不超过255字节的nrfx_spim_xfer或者改用nrfx_spim的LIST模式配合数组描述符。LIST模式能让DMA自动跳转多个不连续缓冲区但代码复杂度明显上升大部分场景先拆分就够了。4. 中断驱动与高吞吐把SPI主机从“忙等”里解放出来4.1 阻塞版和中断版的分界线有的教程会把nrfx_spim_xfer后面直接跟一个while(!done)自旋等待这是典型的伪异步写法。它在单次小包传输时没问题但SPI时钟达到4MHz以上时128字节的传输需要约256微秒这期间CPU完全被占住。nRF52832还要跑BLE协议栈、ADC采样或按键扫描时这种忙等会对系统实时性造成明显影响。分界线很明确一次传输在10微秒以内、且系统里没有更高优先级的任务时可以用忙碌等待简化代码其他情况都应使用事件回调。SPIM本身的设计目标就是“发起后CPU去睡觉事件来了再醒”回调里拉高片选、置标志或直接处理数据都比阻塞等待更符合这颗芯片的定位。4.2 非阻塞收发封装带busy标志的最小状态机实际工程里不建议在业务代码里直接调用nrfx_spim_xfer而是包一层带busy标志的驱动接口。这样上层可以随时查询是否忙避免在上一次传输还没完成时又发起下一次传输这属于SPI主机驱动最常见的使用错误。static volatile bool spi_busy false; void spi_transfer(uint8_t *tx_buf, size_t tx_len, uint8_t *rx_buf, size_t rx_len) { if (spi_busy) { return; } spi_busy true; nrf_gpio_pin_clear(PIN_CS); nrfx_spim_xfer_desc_t xfer { .p_tx_buffer tx_buf, .tx_length tx_len, .p_rx_buffer rx_buf, .rx_length rx_len, }; nrfx_spim_xfer(spim, xfer, 0); } static void spi_event_handler(nrfx_spim_evt_t const *p_event, void *p_context) { if (p_event-type NRFX_SPIM_EVENT_DONE) { nrf_gpio_pin_set(PIN_CS); spi_busy false; } }这里把spi_busy声明为volatile因为它在中断回调里被写、在普通函数里被读。nrf_gpio_pin_set(PIN_CS)放在回调里确保片选高电平时所有数据都已从移位寄存器送出。注意tx_buf和rx_buf的生命周期必须覆盖整个异步传输过程不能传一个函数内部的局部数组后就退出函数否则DMA会在数组失效后继续读写内存。4.3 选择END还是DONE事件nrfx_spim回调里有两种完成事件NRFX_SPIM_EVENT_END表示DMA已经把接收数据写完到RAMNRFX_SPIM_EVENT_DONE表示整个传输被标记完成。大多数场景用NRFX_SPIM_EVENT_DONE更合适因为它比END多包含了外设内部收尾动作。从机如果对CS拉高时刻很敏感比如在CS上升沿锁存数据用DONE事件拉高片选更稳妥。之前做OLED驱动时试过在END事件后立刻拉高CS屏幕偶发花屏改成DONE事件后现象消失。原因是END触发时最后一位数据可能还在线上立即拉高CS会截断最后一个SCK周期后的数据保持时间。SPIM硬件上还有NRFX_SPIM_EXTRA_OPTIONS_END_POST_DELAY选项可以在事件触发后自动插入一段延时再收尾但软件片选时直接用DONE事件是更简单的做法。5. 软件片选与SPI时序从OLED到射频收发器的参数调整5.1 硬件片选时序对常见从机不够友好SPIM外设自带CSN引脚可以配置为硬件自动控制称为硬件片选。理论上去掉GPIO操作能让代码更精简但实际时序并不总是符合从机要求。硬件片选在每次传输开始前自动拉低在传输结束后自动拉高问题是这个“结束后”的时机由外设决定不能针对不同从机单独调整保持时间。比如某些射频收发器要求CS低电平至少保持几百纳秒后再开始SCK还有的传感器要求在CS高电平之前留出数据采样窗口硬件片选都做不了。另一个问题是多从机系统中每个从机需要独立的CS引脚而SPIM的CSN只有一个。虽然可以通过sep_ss_pin配置不同CS引脚但硬件的切换逻辑不如软件GPIO控制直观。基于这些原因我一般在驱动代码里只用use_hw_ss falseCS完全交给普通GPIO。5.2 软件片选的标准动作数据在前CS在后软件片选不是简单地在nrfx_spim_xfer前后各写一次GPIO顺序错误会导致从机完全无响应。一套稳妥的动作用下面几步描述确保CS引脚初始为高电平。在调用nrfx_spim_xfer之前把CS拉低。在SPI事件回调里确认收到NRFX_SPIM_EVENT_DONE后再把CS拉高。连续两帧之间至少留出一个SCK周期的间隔部分从机需要CS高电平保持时间。拉低CS后不能立即启动SCK。GPIO写入到引脚实际输出有延迟极端情况下从机在CS还没稳定时就看到SCK边沿会丢掉第一个bit。建议在低频场景下调用nrf_delay_us(1)但更好的方案是把CS操作也挂在PPI和GPIOTE上由硬件保证时序。普通应用里先拉CS再调用nrfx_spim_xfer的间隙加上函数调用本身的耗时通常已经够用。5.3 不同从机的参数速记表从机类型典型mode常用频率片选策略SSD1306 OLEDSPI模式mode 0或mode 31M8M软件片选每帧数据一次CS拉低W25Q系列Flashmode 0或mode 34M8M命令和地址可共用一段CS低电平nRF24L01mode 01M4M每字节或每包独立CSCS高后等待4usST7789/ILI9341 LCDmode 08M16M用LCD的DC引脚区分命令/数据CS整帧拉低SD卡SPI模式mode 0400kHz初始化后可达8M初始化阶段降频CS不能随意拉高DA需要注意nRF52832的SPIM最高可配8MHz但很多模块在4MHz以上会不稳定。SD卡初始化时必须先用400kHz左右通信否则CMD0都回不来。OLED则相反只要接线短8MHz也能跑但实际帧率瓶颈往往在显示数据量而不是SPI速度。5.4 频率上不去的物理原因同样是SPI从4M升到8M后波形变形是常见的。nRF52832的GPIO输出驱动能力和压摆率有限SCK走线过长或者MOSI上挂了较大负载电容时8MHz方波会变成梯形波。此时从机可能还能工作但噪声容限明显降低。排查方法是把频率降回4M如果从机恢复正常基本就是信号完整性问题不是驱动代码的问题。软件层面还可以检查FREQUENCY枚举是否真的写入了预期值部分SDK版本里NRF_SPIM_FREQ_8M依赖HFCLK和PCLK16M配置。BLE协议栈运行时会占用高频时钟SPIM的PCLK来源发生变化会导致实际SCK频率漂移必要时用逻辑分析仪实测。6. 验证与排查回环、逻辑分析仪和中断到CS的延迟测量6.1 回环自测不接从机先证明主机没写错在没有示波器和逻辑分析仪的条件下最快的验证方法是把MOSI和MISO用杜邦线短接然后把miso_loopback配置打开或直接物理连接。发一串已知数据读回来的必须一模一样。这能同时验证引脚配置、DMA方向、buffer管理三件事。uint8_t tx_data[8] {0xA5, 0x5A, 0x00, 0xFF, 0x0F, 0xF0, 0x55, 0xAA}; uint8_t rx_data[8] {0}; spi_transfer(tx_data, 8, rx_data, 8); for (int i 0; i 8; i) { if (rx_data[i] ! tx_data[i]) { // 打印错误位置和值 } }回环成功后再接入真实从机。此时如果仍然读不到数据问题就从主机转移到从机配置、接线或者从机的电源和复位电路。6.2 逻辑分析仪重点看这4个边沿时刻连接SCK、MOSI、MISO和CS四个通道抓一段读JEDEC ID的波形。首先看CS拉低与第一个SCK下降沿之间的时间至少要大于从机要求的片选建立时间。其次看时钟极性mode 0下SCK空闲是低电平数据在上升沿采样。然后看MISO数据是否与SCK的采样边沿对齐偏差过大时考虑降低频率或调整mode。最后看CS拉高时最后一个SCK时钟是否已经完整结束。这四个时间点可以定位大部分SPI通信问题。6.3 用GPIO脉冲实测ISR到CS拉高的延迟如果怀疑回调里拉高CS太慢可以在事件回调开始处翻转一个空闲GPIO用逻辑分析仪同时测量该GPIO和CS引脚时间差就是中断响应到片选释放的延迟。nRF52832的中断延迟通常在几个微秒以内但BLE协议栈抢占优先级时可能抖动。对时序苛刻的从机与其优化中断延迟不如改用定时器加PPI由硬件完成片选控制。这套SPIM主机驱动从外设选型到最小代码、异步封装和排查手法覆盖了实际项目中最常用的路径。最后送一个实用参数调法遇到从机行为怪异时先把频率降到1M、把mode改成mode 0、把CS拉高后的延时加长到10微秒三分之二的诡异问题会先消失剩下的再用逻辑分析仪逐个边沿排查。本文还有配套的精品资源点击获取