在 RT-Thread 中使用 SPI 作为从机(Slave)模式并配合 DMA 接收数据时,由于框架原生支持有限,需要结合 HAL 库和 RT-Thread 的中断管理机制实现。以下是完整的解决方案:
1. 驱动框架扩展思路
RT-Thread 的 SPI 框架主要面向主机模式,从机模式需自行扩展:
- 使用 HAL 库底层配置 SPI 从机
- 手动管理 DMA 传输
- 通过 RT-Thread 中断管理和同步机制处理数据
2. 关键代码实现
2.1 初始化 SPI 从机 + DMA
#include
#include
#include
// 定义接收缓冲区
#define BUFFER_SIZE 128
static uint8_t rx_buffer[BUFFER_SIZE];
static struct rt_semaphore dma_sem;
static SPI_HandleTypeDef hspi_slave;
void SPI_IRQHandler(void)
{
rt_interrupt_enter();
HAL_SPI_IRQHandler(&hspi_slave);
rt_interrupt_leave();
}
void DMA_IRQHandler(void)
{
rt_interrupt_enter();
HAL_DMA_IRQHandler(hspi_slave.hdmarx);
rt_interrupt_leave();
}
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
{
if(hspi == &hspi_slave) {
rt_sem_release(&dma_sem); // 通知数据接收完成
}
}
static void spi_slave_init(void)
{
__HAL_RCC_SPI2_CLK_ENABLE();
__HAL_RCC_DMA1_CLK_ENABLE();
// 配置SPI为从机模式
hspi_slave.Instance = SPI2;
hspi_slave.Init.Mode = SPI_MODE_SLAVE;
hspi_slave.Init.Direction = SPI_DIRECTION_2LINES;
hspi_slave.Init.DataSize = SPI_DATASIZE_8BIT;
hspi_slave.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi_slave.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi_slave.Init.NSS = SPI_NSS_SOFT;
hspi_slave.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;
hspi_slave.Init.FirstBit = SPI_FIRSTBIT_MSB;
HAL_SPI_Init(&hspi_slave);
// 配置DMA接收
hspi_slave.hdmarx = &hdma_spi2_rx;
hdma_spi2_rx.Instance = DMA1_Channel3;
hdma_spi2_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_spi2_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi2_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi2_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_spi2_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_spi2_rx.Init.Mode = DMA_CIRCULAR; // 循环模式避免频繁重启
HAL_DMA_Init(&hdma_spi2_rx);
// 绑定DMA中断
rt_hw_interrupt_install(DMA1_Channel3_IRQn, DMA_IRQHandler, RT_NULL, "spi_dma");
HAL_NVIC_SetPriority(DMA1_Channel3_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel3_IRQn);
// 启动DMA接收
HAL_SPI_Receive_DMA(&hspi_slave, rx_buffer, BUFFER_SIZE);
// 创建信号量用于同步
rt_sem_init(&dma_sem, "spi_sem", 0, RT_IPC_FLAG_FIFO);
}
2.2 数据接收线程
static void spi_slave_thread_entry(void *param)
{
while (1) {
// 等待DMA传输完成
if (rt_sem_take(&dma_sem, RT_WAITING_FOREVER) == RT_EOK) {
// 处理接收到的数据
rt_kprintf("Received %d bytesn", BUFFER_SIZE);
// 此处添加数据处理代码...
// 可选:重新启动下一次接收(非循环模式时需要)
// HAL_SPI_Receive_DMA(&hspi_slave, rx_buffer, BUFFER_SIZE);
}
}
}
void spi_slave_thread_init()
{
rt_thread_t thread = rt_thread_create("spi_slv", spi_slave_thread_entry, RT_NULL, 1024, 20, 10);
rt_thread_startup(thread);
}
3. 关键注意事项
DMA 配置模式:
- 循环模式(
DMA_CIRCULAR):推荐使用,避免每次传输后重新启动。
- 非循环模式:需在回调中重启传输,注意总线竞争问题。
中断优先级管理:
// 确保SPI/DMA中断优先级高于RT-Thread调度锁
HAL_NVIC_SetPriority(SPI2_IRQn, 1, 0); // 高于最低中断优先级
缓冲区管理:
- 使用双缓冲技术避免数据覆盖:
uint8_t rx_buf1[BUFF_SIZE], rx_buf2[BUFF_SIZE];
HAL_SPI_Receive_DMA(&hspi, rx_buf1, BUFF_SIZE); // 启动第一次
在回调中切换缓冲区:
void HAL_SPI_RxCpltCallback(...)
{
static uint8_t toggle = 0;
rt_sem_release(&dma_sem);
toggle ^= 1;
HAL_SPI_Receive_DMA(hspi, toggle ? rx_buf2 : rx_buf1, BUFF_SIZE);
}
时钟配置冲突:
- 检查
CubeMX 生成的时钟配置与 board.h 是否一致。
- 确认 SPI 时钟未超过从机最大速率(尤其 STM32 作从机时通常 ≤ fPCLK/2)。
4. 调试建议
- DMA 传输错误检查:
void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi)
{
rt_kprintf("SPI Error: %dn", hspi->ErrorCode);
}
- 逻辑分析仪验证:
- 使用示波器/逻辑分析仪检查:
- SCK/CS 信号是否存在
- MOSI 数据是否与主机发送一致
- DMA 传输启动时机(应在主机发起传输前)
5. 替代方案建议
若仍存在问题,可尝试以下方案:
// 使用中断模式替代DMA(适合小数据量)
HAL_SPI_Receive_IT(&hspi_slave, rx_buffer, BUFFER_SIZE);
void HAL_SPI_RxCpltCallback(...)
{
rt_sem_release(&dma_sem);
HAL_SPI_Receive_IT(&hspi_slave, rx_buffer, BUFFER_SIZE); // 重启
}
完整集成步骤
- 将上述代码放入
drv_spi.c 的自定义驱动部分
- 在
rt_hw_spi_init() 中初始化从机配置
- 创建线程处理数据
- 在
main.c 中调用初始化函数
提示:STM32 HAL 库的 SPI 从机 DMA 接收需要严格满足时序:
- 必须在主机发起通信前启动 DMA
- 循环模式可避免时序问题
- 检查
NSS 引脚管理(软件模式需手动控制)
通过以上步骤,可实现在 RT-Thread 中稳定运行的 SPI 从机 DMA 接收。最终效果应与裸机程序一致,同时利用 RTOS 的任务管理优势。