1. 认识nRF52832的SPI硬件架构
第一次接触nRF52832的SPI外设时,我被它简洁而高效的设计惊艳到了。这颗来自Nordic的蓝牙SoC内置了3个独立的SPI接口(SPI0/1/2),每个接口都支持主从模式切换。最让我惊喜的是它的双缓冲机制——TXD和RXD寄存器各有一个深度为2的缓冲区,这意味着我们可以在前一个字节传输完成前就准备下一个字节,实现近乎无缝的数据流传输。
实际项目中我常用的是主模式配置,这时需要特别注意引脚映射的灵活性。通过PSELSCK、PSELMOSI和PSELMISO这三个寄存器,我们可以将SPI信号动态映射到任意GPIO引脚。记得有次调试时,我把SCK从P0.27改到P0.05,只需要修改PSELSCK寄存器的值,硬件就会自动完成信号路由,完全不需要飞线。
时钟配置是另一个关键点。FREQUENCY寄存器支持从125kHz到8MHz共8种速率,我在驱动OLED屏时发现4MHz是最稳定的选择。更高的8MHz虽然理论可用,但实际布线稍有不当就会导致信号完整性问题。这里有个小技巧:CONFIG寄存器里的CPOL和CPHA位一定要与从设备严格匹配,有次我调试温湿度传感器,就因为模式设错导致读数全乱。
2. 寄存器级SPI驱动开发实战
2.1 初始化流程详解
写寄存器级驱动时,我最喜欢nRF52832的模块化设计。以SPI2为例,完整的初始化需要5个步骤:
// 1. 引脚配置 NRF_SPI2->PSEL.SCK = 27; // SCK -> P0.27 NRF_SPI2->PSEL.MOSI = 26; // MOSI -> P0.26 NRF_SPI2->PSEL.MISO = 28; // MISO -> P0.28 // 2. 时钟频率设置 NRF_SPI2->FREQUENCY = SPI_FREQUENCY_FREQUENCY_M4; // 4MHz // 3. 工作模式配置 NRF_SPI2->CONFIG = (SPI_CONFIG_CPHA_Leading << SPI_CONFIG_CPHA_Pos) | (SPI_CONFIG_CPOL_ActiveHigh << SPI_CONFIG_CPOL_Pos); // 4. 使能中断(可选) NRF_SPI2->INTENSET = SPI_INTENSET_READY_Msk; // 5. 启用SPI NRF_SPI2->ENABLE = SPI_ENABLE_ENABLE_Enabled << SPI_ENABLE_ENABLE_Pos;特别提醒:PSEL寄存器的配置必须在SPI禁用状态下进行。我有次在ENABLE之后修改引脚映射,导致整个通信异常,排查了半天才发现这个细节。
2.2 双缓冲机制深度优化
双缓冲是提升SPI效率的关键。在发送数据时,可以这样利用双缓冲特性:
void spi_send_burst(uint8_t *data, uint16_t len) { NRF_SPI2->EVENTS_READY = 0; // 填充第一个字节 NRF_SPI2->TXD = data[0]; for(int i=1; i<len; i++) { // 等待第一个字节发送完成 while(!NRF_SPI2->EVENTS_READY); NRF_SPI2->EVENTS_READY = 0; // 立即填充下一个字节 NRF_SPI2->TXD = data[i]; // 读取接收数据(如果需要) uint8_t rx = NRF_SPI2->RXD; } // 等待最后一个字节完成 while(!NRF_SPI2->EVENTS_READY); NRF_SPI2->EVENTS_READY = 0; uint8_t rx = NRF_SPI2->RXD; }实测这种写法比单字节传输快40%以上。不过要注意,READY事件会在SCK最后一个时钟边沿后延迟约半个时钟周期才触发,这是芯片设计特性,不是bug。
3. Zephyr集成技巧与性能调优
3.1 设备树配置秘籍
在Zephyr中集成nRF52832的SPI时,设备树配置是首要任务。这是我的典型配置模板:
&spi2 { compatible = "nordic,nrf-spi"; status = "okay"; sck-pin = <27>; mosi-pin = <26>; miso-pin = <28>; cs-gpios = <&gpio0 24 GPIO_ACTIVE_LOW>; clock-frequency = <DT_FREQ_M(4)>; };有个坑我踩过多次:Zephyr默认使用SPIM(带EasyDMA的改进型SPI),如果非要使用传统SPI,需要在prj.conf中添加:
CONFIG_SPI_NRFX_SPI=y CONFIG_SPI_NRFX_RAM_BUFFER_SIZE=03.2 混合编程技巧
有时我们需要突破Zephyr抽象层直接操作寄存器。这是我总结的安全混用方法:
#include <zephyr/drivers/spi.h> #include <hal/nrf_spi.h> const struct device *spi_dev = DEVICE_DT_GET(DT_NODELABEL(spi2)); void high_speed_transfer() { // 先通过Zephyr API获取控制权 spi_config cfg = { .frequency = 4000000, .operation = SPI_OP_MODE_MASTER | SPI_TRANSFER_MSB }; spi_transceive(spi_dev, &cfg, NULL, 0, NULL, 0); // 直接操作寄存器 NRF_SPI_Type *reg = NRF_SPI2; reg->FREQUENCY = SPI_FREQUENCY_FREQUENCY_M8; // 临时升到8MHz // 关键数据传输... reg->TXD = 0xAA; // 恢复Zephyr控制 reg->FREQUENCY = SPI_FREQUENCY_FREQUENCY_M4; }这种方法在驱动高速ADC时特别有用,但要注意操作时序,避免与Zephyr的SPI线程产生冲突。
4. 典型问题排查指南
4.1 信号完整性问题
当SPI速率超过2MHz时,PCB布局就变得至关重要。我曾遇到一个典型案例:MISO线上出现数据错误,最终发现是走线过长(>10cm)且没有终端匹配。解决方案有:
- 缩短走线长度,最好控制在5cm内
- 在SCK和MISO上加33Ω串联电阻
- 降低时钟速率到2MHz
用示波器测量时,要特别注意SCK与MOSI/MISO的相位关系。理想情况下,数据应在SCK稳定沿采样,实际测量时发现抖动不应超过时钟周期的15%。
4.2 中断冲突处理
当同时使用SPI和其他外设时,可能会遇到中断冲突。我的经验法则是:
- SPI中断优先级应设为1或2(共0-7级)
- 在中断服务程序中尽快清除EVENTS_READY
- 避免在SPI中断中进行复杂运算
void spi_isr(void) { if(NRF_SPI2->EVENTS_READY) { NRF_SPI2->EVENTS_READY = 0; uint8_t data = NRF_SPI2->RXD; // 简单处理立即返回 k_fifo_put(&rx_fifo, data); } }对于实时性要求高的场景,建议使用DMA而非中断。虽然nRF52832的标准SPI不支持DMA,但可以通过SPIM外设实现。