4. SPI驱动移植:配置SPI四线/三线模式,实现ToF模组高速数据流读取
好,咱们今天聊点硬核的——SPI驱动移植。说实话,很多做ToF的工程师,最后都卡在这一步。不是不会配SPI,而是不知道怎么把数据“又快又稳”地读出来。
我个人习惯把SPI配置分成两个层面:物理层和数据流层。物理层管的是几根线、什么模式;数据流层管的是怎么把数据从模组搬到内存里。咱们一个一个说。
4.1 四线模式 vs 三线模式:到底选哪个?
先看个表格,一目了然:
| 特性 | 四线模式(标准SPI) | 三线模式(半双工) |
|---|---|---|
| 信号线 | SCLK, MOSI, MISO, CS | SCLK, SDIO(双向), CS |
| 数据传输 | 全双工,同时收发 | 半双工,分时收发 |
| 典型场景 | ToF帧数据读取(高速) | 寄存器配置、直方图读取 |
| 引脚占用 | 4根 | 3根 |
| 速率上限 | 通常更高(可达50MHz+) | 受限于方向切换开销 |
你可能会问:“那我直接用四线不就行了?”嗯,这里要注意——有些ToF模组(比如VL53L1系列)在读取直方图时,硬件上只支持三线模式。我刚开始做的时候没仔细看手册,硬用四线去读,结果数据全是乱的。后来才发现,模组的MISO引脚在直方图模式下根本不出数据。
4.2 配置实战:以STM32为例
咱们直接上代码。这是我在实际项目中用过的配置,针对的是ToF模组的帧数据读取(四线模式,DMA传输):
// SPI配置结构体
SPI_HandleTypeDef hspi1;
void MX_SPI1_Init(void)
{
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER; // 主机模式
hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 四线全双工
hspi1.Init.DataSize = SPI_DATASIZE_8BIT; // 8位数据
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // 时钟空闲低电平
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // 第一个边沿采样
hspi1.Init.NSS = SPI_NSS_SOFT; // 软件控制CS
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2; // 42MHz(主频84MHz)
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; // 高位先行
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 10;
if (HAL_SPI_Init(&hspi1) != HAL_OK)
{
Error_Handler();
}
}
这里有个细节:时钟极性和相位。不同的ToF模组要求不一样。我见过最坑的是某款模组,手册上写的是Mode 0,但实际测试必须用Mode 3才能稳定通信。所以我的建议是——别完全信手册,用逻辑分析仪抓一下实际波形,确认数据在正确的时钟边沿被采样。
4.3 DMA双缓冲:让数据飞起来
为什么要用DMA双缓冲?说白了,就是不让CPU在搬数据这件事上浪费时间。ToF模组一帧数据可能有几万个字节,如果用CPU轮询去读,那其他任务基本就别想跑了。
双缓冲的核心思想是:一个缓冲区在给DMA填数据,另一个缓冲区在给应用程序处理数据。两个角色轮流切换,没有空档期。
// DMA双缓冲配置示例
#define TOF_FRAME_SIZE 1024 // 每帧数据大小(字节)
#define TOF_BUFFER_NUM 2 // 双缓冲
uint8_t tof_rx_buffer[TOF_BUFFER_NUM][TOF_FRAME_SIZE];
volatile uint8_t active_buffer = 0;
void DMA_Init_DoubleBuffer(void)
{
// 配置DMA通道,使用循环模式
hdma_spi1_rx.Instance = DMA2_Stream0;
hdma_spi1_rx.Init.Channel = DMA_CHANNEL_3;
hdma_spi1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_spi1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_spi1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_spi1_rx.Init.Mode = DMA_CIRCULAR; // 循环模式,关键!
hdma_spi1_rx.Init.Priority = DMA_PRIORITY_HIGH;
hdma_spi1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
// 设置双缓冲区地址
HAL_DMAEx_ChangeMemory(&hdma_spi1_rx,
(uint32_t)tof_rx_buffer[0], // 缓冲区0
(uint32_t)tof_rx_buffer[1], // 缓冲区1
DMA_MEMORY_0);
// 关联到SPI
__HAL_LINKDMA(&hspi1, hdmarx, hdma_spi1_rx);
// 启动DMA传输
HAL_SPI_Receive_DMA(&hspi1, tof_rx_buffer[0], TOF_FRAME_SIZE);
}
你可能会问:“那怎么知道当前哪个缓冲区是满的?”答案是:利用DMA的中断回调。当DMA传输完成一半(半传输中断)和全部完成(传输完成中断)时,分别切换缓冲区索引。
// DMA中断回调
void HAL_SPI_RxHalfCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi->Instance == SPI1)
{
// 缓冲区0已满,可以处理
active_buffer = 0;
process_tof_frame(tof_rx_buffer[0]);
}
}
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi->Instance == SPI1)
{
// 缓冲区1已满,可以处理
active_buffer = 1;
process_tof_frame(tof_rx_buffer[1]);
}
}
4.4 中断优先级:别让SPI被“饿死”
中断优先级这件事,说大不大,说小不小。但一旦出问题,就是那种“偶尔丢一帧数据”的玄学bug,特别难查。
我的原则是:SPI的DMA中断优先级,要高于普通外设中断,但低于系统滴答定时器。为什么?因为SPI数据流是实时的,如果被其他中断长时间阻塞,DMA的FIFO可能会溢出,导致数据丢失。
以NVIC配置为例:
// 中断优先级分组:抢占优先级2位,子优先级2位
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
// SPI1 DMA中断:抢占优先级1,子优先级0
HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
// 其他外设中断:抢占优先级2,子优先级0
HAL_NVIC_SetPriority(USART1_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
这里有个容易忽略的点:同一个DMA Stream的不同中断标志位(半传输、传输完成、传输错误)共享同一个中断优先级。所以你在中断服务函数里要判断到底是哪个事件触发了中断,别搞混了。
4.5 知识体系总览
说了这么多,咱们用一张图把整个SPI驱动移植的知识体系串起来:
从这张图可以看出来,SPI驱动移植不是孤立的——它从物理层配置开始,到数据流层设计,最后落到系统集成与优化。每一层都依赖上一层做对,才能保证最终的数据流稳定可靠。
好了,这一章的内容就到这里。记住:SPI配置只是开始,真正考验功力的是怎么把数据“无损、实时、高效”地搬进你的系统里。下一章咱们聊聊直方图数据的解析算法,那个更有意思。
公众号:蓝海资料掘金营,微信deep3321