You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 18:50:33