基于STM32 HAL库SPI+DMA读取LIS2DH12加速度计数据的问题
STM32F103CBT6 SPI+DMA读取LIS2DH12双缓冲实现问题
问题背景
需求是在STM32F103CBT6上通过SPI+DMA读取LIS2DH12加速度计数据,采用双缓冲机制(一个缓冲处理数据,另一个缓冲填充数据)释放CPU时间。需采集三轴共6字节数据(寄存器0x28为X轴低字节,依次到Z轴高字节),总计2048组,已声明如下变量:
#define SAMPLE_COUNT 2048 uint8_t sample_buf1[SAMPLE_COUNT * 6] = { 0 }; uint8_t sample_buf2[SAMPLE_COUNT * 6] = { 0 }; uint16_t current_sample = 0; uint8_t current_buf = 0; uint8_t *sample_buf[2] = {sample_buf1, sample_buf2};
核心疑问
- 对HAL库函数
HAL_SPI_TransmitReceive_DMA(&hspi1, buff_source, buff_destination, 6);的收发逻辑理解模糊:比如是否需要读取7字节(首字节异常)、应选择Normal模式还是Circular模式 - 使用
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)中断时,CPU 90%时间处于中断中,不知如何优化 - 若推荐LL/SPL/CMSIS等其他库,需提供最小可复现示例
当前测试异常
尝试读取设备ID的代码如下:
uint8_t cmd_accelerometer_read_ID[2] = { LIS2DH12_WHO_AM_I_ADDR | 0x80, 0 }; uint8_t dma_received_data[2] = {0, 0}; HAL_SPI_TransmitReceive_DMA(&hspi1, cmd_accelerometer_read_ID, dma_received_data, 2);
结果dma_received_data[0]为0(符合预期),但dma_received_data[1]始终为0xFF,预期值应为0x33。
问题解决与优化方案
1. 设备ID读取失败的原因与修复
ID读取失败大概率是CS引脚未正确控制:LIS2DH12的SPI传输需要在通信前拉低CS引脚,传输完成后拉高。HAL库的HAL_SPI_TransmitReceive_DMA不会自动处理CS引脚,需手动控制:
// 传输前拉低CS(假设CS引脚为GPIOA_PIN_4) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_SPI_TransmitReceive_DMA(&hspi1, cmd_accelerometer_read_ID, dma_received_data, 2); // 在TxRxCpltCallback中拉高CS void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if(hspi->Instance == SPI1) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 此处可处理读取到的ID数据 } }
另外需确认SPI配置与LIS2DH12匹配:器件默认支持SPI模式0(CPOL=0,CPHA=0)或模式3(CPOL=1,CPHA=1),需确保STM32的SPI参数与之对应。
2. HAL_SPI_TransmitReceive_DMA逻辑与模式选择
- 收发逻辑:该函数是全双工同步收发,每发送1字节的同时接收1字节。读取LIS2DH12连续寄存器时,需在发送缓冲的首字节写入
0x28 | 0x80 | 0x40(0x80表示读操作,0x40表示自动增量,后续寄存器地址自动递增),后续5字节可填任意值(dummy数据)。此时接收缓冲的6字节中,全部是有效数据:第一个接收字节对应0x28寄存器(X轴低字节),依次到第六个字节对应0x2D寄存器(Z轴高字节)——无需额外读取7字节,首字节并非异常,是器件返回的目标寄存器数据。 - 模式选择:
- 若用软件双缓冲,选择Normal模式:单次传输6字节完成后触发中断,在回调中切换到另一个缓冲继续传输,同时CPU处理已完成的缓冲数据。
- 若需连续无间隔采集,可选择Circular模式配合软件双缓冲,但STM32F1的DMA无硬件双缓冲,需在回调中手动切换目标缓冲地址。
3. 中断占用过高的优化
中断占比高的核心原因是单次传输字节数太少(每次6字节触发一次中断),优化方式:
- 增加单次DMA传输的数据量:比如一次传输32组数据(32*6=192字节),中断次数从2048次降至64次,大幅降低CPU占用。
- 在中断回调中仅做缓冲切换的轻量操作,将数据处理逻辑放到主循环中执行(比如设置标志位,主循环检测到标志位后处理缓冲数据)。
示例优化后的HAL库回调逻辑:
volatile uint8_t buf_process_flag = 0; void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if(hspi->Instance == SPI1) { HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 切换缓冲 current_buf = !current_buf; buf_process_flag = 1; // 设置数据处理标志 // 准备下一次传输的发送缓冲(首字节为自动增量读地址,其余为dummy) static uint8_t tx_buf[6] = {0x28 | 0x80 | 0x40, 0, 0, 0, 0, 0}; HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); HAL_SPI_TransmitReceive_DMA(&hspi1, tx_buf, sample_buf[current_buf], 6); current_sample++; if(current_sample >= SAMPLE_COUNT) { // 采集完成,停止DMA HAL_SPI_DMAStop(&hspi1); } } } // 主循环中处理数据 while(1) { if(buf_process_flag) { buf_process_flag = 0; uint8_t *processed_buf = sample_buf[!current_buf]; // 处理processed_buf中的6字节数据,比如转成加速度值 for(int i=0; i<6; i++) { // 数据处理逻辑 } } // 其他主循环任务 }
4. LL库最小可复现示例
LL库更轻量化,以下是SPI+DMA读取LIS2DH12的双缓冲示例(仅核心代码):
#include "stm32f1xx_ll_spi.h" #include "stm32f1xx_ll_dma.h" #include "stm32f1xx_ll_gpio.h" #define SAMPLE_COUNT 2048 uint8_t sample_buf1[SAMPLE_COUNT * 6] = {0}; uint8_t sample_buf2[SAMPLE_COUNT * 6] = {0}; uint8_t current_buf = 0; uint8_t tx_buf[6] = {0x28 | 0x80 | 0x40, 0, 0, 0, 0, 0}; volatile uint8_t buf_ready = 0; // DMA中断回调(需在stm32f1xx_it.c中修改) void DMA1_Channel2_IRQHandler(void) { if(LL_DMA_IsActiveFlag_TC2(DMA1)) { LL_DMA_ClearFlag_TC2(DMA1); LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_4); // 拉高CS buf_ready = 1; current_buf = !current_buf; // 重新配置DMA目标地址,启动下一次传输 LL_DMA_SetMemoryAddress(DMA1, LL_DMA_CHANNEL_2, (uint32_t)sample_buf[current_buf]); LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_4); // 拉低CS LL_SPI_EnableDMAReq_RX(SPI1); LL_SPI_EnableDMAReq_TX(SPI1); } } // SPI与DMA初始化(需根据硬件调整引脚) void SPI_DMA_Init(void) { // 初始化CS引脚(PA4) LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_4, LL_GPIO_MODE_OUTPUT); LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_4); // 初始化SPI1 LL_SPI_InitTypeDef spi_init = {0}; spi_init.TransferDirection = LL_SPI_FULL_DUPLEX; spi_init.Mode = LL_SPI_MODE_MASTER; spi_init.DataWidth = LL_SPI_DATAWIDTH_8BIT; spi_init.ClockPolarity = LL_SPI_POLARITY_LOW; // 模式0 spi_init.ClockPhase = LL_SPI_PHASE_1EDGE; spi_init.NSS = LL_SPI_NSS_SOFT; spi_init.BaudRate = LL_SPI_BAUDRATEPRESCALER_16; // 调整波特率 spi_init.BitOrder = LL_SPI_MSB_FIRST; LL_SPI_Init(SPI1, &spi_init); LL_SPI_Enable(SPI1); // 初始化DMA1通道2(SPI_RX)和通道3(SPI_TX) LL_DMA_InitTypeDef dma_rx_init = {0}; dma_rx_init.PeriphOrM2MSrcAddress = (uint32_t)&SPI1->DR; dma_rx_init.MemoryOrM2MDstAddress = (uint32_t)sample_buf1; dma_rx_init.Direction = LL_DMA_DIRECTION_PERIPH_TO_MEMORY; dma_rx_init.NbData = 6; dma_rx_init.PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT; dma_rx_init.MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT; dma_rx_init.PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_BYTE; dma_rx_init.MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_BYTE; dma_rx_init.Mode = LL_DMA_MODE_NORMAL; dma_rx_init.Priority = LL_DMA_PRIORITY_MEDIUM; LL_DMA_Init(DMA1, LL_DMA_CHANNEL_2, &dma_rx_init); LL_DMA_InitTypeDef dma_tx_init = {0}; dma_tx_init.PeriphOrM2MSrcAddress = (uint32_t)&SPI1->DR; dma_tx_init.MemoryOrM2MDstAddress = (uint32_t)tx_buf; dma_tx_init.Direction = LL_DMA_DIRECTION_MEMORY_TO_PERIPH; dma_tx_init.NbData = 6; dma_tx_init.PeriphOrM2MSrcIncMode = LL_DMA_PERIPH_NOINCREMENT; dma_tx_init.MemoryOrM2MDstIncMode = LL_DMA_MEMORY_INCREMENT; dma_tx_init.PeriphOrM2MSrcDataSize = LL_DMA_PDATAALIGN_BYTE; dma_tx_init.MemoryOrM2MDstDataSize = LL_DMA_MDATAALIGN_BYTE; dma_tx_init.Mode = LL_DMA_MODE_NORMAL; dma_tx_init.Priority = LL_DMA_PRIORITY_MEDIUM; LL_DMA_Init(DMA1, LL_DMA_CHANNEL_3, &dma_tx_init); // 启用DMA中断 LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_2); NVIC_EnableIRQ(DMA1_Channel2_IRQn); NVIC_SetPriority(DMA1_Channel2_IRQn, 1); } // 启动采集 void Start_Collection(void) { LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_4); LL_SPI_EnableDMAReq_RX(SPI1); LL_SPI_EnableDMAReq_TX(SPI1); LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_2); LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_3); } // 主循环 int main(void) { // 系统初始化(省略) SystemClock_Config(); SPI_DMA_Init(); Start_Collection(); while(1) { if(buf_ready) { buf_ready = 0; uint8_t *processed_buf = (current_buf == 0) ? sample_buf2 : sample_buf1; // 处理processed_buf中的6字节数据 } } }
内容的提问来源于stack exchange,提问作者Keineprogrammer
相关产品推荐
相关产品推荐

