基于 STM32L476 + SAI1 Block A + DMA 循环乒乓缓冲 实现 4 路加速度计 TDM 采集

约定参数(可根据实际芯片修改)

参数项 取值 说明
TDM 模式 共享总线式 所有加速度计并联数据线、时钟、帧同步
时隙数量 4 个 对应 4 颗加速度计,时隙 0~3 分别对应第 1~4 颗
单时隙位宽 32 位 单颗芯片单次输出 32 位数据(含三轴 + 状态位)
帧总长度 128 位 4 时隙 × 32 位
帧同步 高有效、1 位宽、帧起始对齐 通用 TDM 标准时序
采样率 8kHz BCLK = 8kHz × 128 位 = 1.024MHz
传感器配置 SPI 接口 + 独立 CS 引脚 预先给每颗芯片配置时隙号

完整可运行代码示例

1. 全局定义与变量(可添加到 main.c 顶部)

cpp 复制代码
#include "main.h"
#include "sai.h"
#include "dma.h"
#include "spi.h"
#include "gpio.h"

/************************ 硬件引脚定义(根据实际电路板修改)************************/
#define ACC1_CS_Pin      GPIO_PIN_0
#define ACC1_CS_GPIO_Port GPIOA
#define ACC2_CS_Pin      GPIO_PIN_1
#define ACC2_CS_GPIO_Port GPIOA
#define ACC3_CS_Pin      GPIO_PIN_2
#define ACC3_CS_GPIO_Port GPIOA
#define ACC4_CS_Pin      GPIO_PIN_3
#define ACC4_CS_GPIO_Port GPIOA

/************************ 数据结构与全局变量 ************************/
// TDM乒乓接收缓冲区:2帧缓冲(避免数据覆盖),每帧4个时隙(对应4颗芯片)
uint32_t tdm_rx_buf[2][4];
volatile uint8_t  tdm_data_ready = 0;  // 数据就绪标志
volatile uint8_t  tdm_buf_idx = 0;     // 当前就绪的缓冲区索引

// 加速度计数据结构体
typedef struct {
    int16_t x;    // X轴原始值
    int16_t y;    // Y轴原始值
    int16_t z;    // Z轴原始值
    // 可扩展:温度、状态位等
} AccData_t;
AccData_t acc_result[4];  // 4颗芯片的解析结果

2. SAI 初始化函数(CubeMX 生成,对应 TDM 配置)

以下代码与 CubeMX 配置完全对应,无需手动修改,仅作参数对照:

cpp 复制代码
SAI_HandleTypeDef hsai_BlockA1;
DMA_HandleTypeDef hdma_sai1_a;

void MX_SAI1_Init(void)
{
  hsai_BlockA1.Instance = SAI1_Block_A;
  hsai_BlockA1.Init.Protocol = SAI_FREE_PROTOCOL;      // 自由协议(实现TDM)
  hsai_BlockA1.Init.AudioMode = SAI_MODEMASTER_RX;     // 主机接收模式
  hsai_BlockA1.Init.DataSize = SAI_DATASIZE_32;        // 单数据单元32位
  hsai_BlockA1.Init.FirstBit = SAI_FIRSTBIT_MSB;       // 高位先行
  hsai_BlockA1.Init.ClockStrobing = SAI_CLOCKSTROBING_RISINGEDGE; // 上升沿采样
  hsai_BlockA1.Init.Synchro = SAI_ASYNCHRONOUS;        // 异步模式
  hsai_BlockA1.Init.OutputDrive = SAI_OUTPUTDRIVE_DISABLE;
  hsai_BlockA1.Init.NoDivider = SAI_MASTERDIVIDER_ENABLE;
  hsai_BlockA1.Init.FIFOThreshold = SAI_FIFOTHRESHOLD_EMPTY;
  hsai_BlockA1.Init.AudioFrequency = SAI_AUDIO_FREQUENCY_MCKDIV;
  hsai_BlockA1.Init.Mckdiv = 24;                       // 时钟分频,按BCLK计算
  hsai_BlockA1.Init.MonoStereoMode = SAI_STEREOMODE;
  hsai_BlockA1.Init.CompandingMode = SAI_NOCOMPANDING;
  hsai_BlockA1.Init.TriState = SAI_OUTPUT_NOTRELEASED;

  // 帧结构配置
  hsai_BlockA1.FrameInit.FrameLength = 128;            // 每帧总位数:4*32=128
  hsai_BlockA1.FrameInit.ActiveFrameLength = 1;        // 帧同步脉冲宽度:1位
  hsai_BlockA1.FrameInit.FSDefinition = SAI_FS_STARTFRAME; // FS表示帧开始
  hsai_BlockA1.FrameInit.FSPolarity = SAI_FS_ACTIVE_HIGH;  // FS高有效
  hsai_BlockA1.FrameInit.FSOffset = SAI_FS_FIRSTBIT;   // FS与第一个数据位对齐

  // 时隙配置(区分不同芯片的核心)
  hsai_BlockA1.SlotInit.FirstBitOffset = 0;
  hsai_BlockA1.SlotInit.SlotSize = SAI_SLOTSIZE_32B;   // 单个时隙32位
  hsai_BlockA1.SlotInit.SlotNumber = 4;                // 总时隙数:4
  hsai_BlockA1.SlotInit.SlotActive = 0x000F;           // 激活时隙0~3

  if (HAL_SAI_Init(&hsai_BlockA1) != HAL_OK)
  {
    Error_Handler();
  }
}

// DMA初始化(CubeMX生成)
void MX_DMA_Init(void)
{
  __HAL_RCC_DMA1_CLK_ENABLE();

  hdma_sai1_a.Instance = DMA1_Channel2;
  hdma_sai1_a.Init.Request = DMA_REQUEST_SAI1_A;
  hdma_sai1_a.Init.Direction = DMA_PERIPH_TO_MEMORY;
  hdma_sai1_a.Init.PeriphInc = DMA_PINC_DISABLE;
  hdma_sai1_a.Init.MemInc = DMA_MINC_ENABLE;
  hdma_sai1_a.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
  hdma_sai1_a.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
  hdma_sai1_a.Init.Mode = DMA_CIRCULAR;                // 循环模式
  hdma_sai1_a.Init.Priority = DMA_PRIORITY_HIGH;

  if (HAL_DMA_Init(&hdma_sai1_a) != HAL_OK)
  {
    Error_Handler();
  }

  __HAL_LINKDMA(&hsai_BlockA1, hdmarx, hdma_sai1_a);
}

3. 加速度计 TDM 配置(SPI 写寄存器)

绝大多数带 TDM 接口的加速度计,都需要先通过 SPI/I2C 配置时隙号和工作模式,再进入 TDM 传输状态。以下为通用示例:

cpp 复制代码
/**
 * @brief  SPI写单个寄存器(通用函数)
 * @param  cs_port: CS引脚端口
 * @param  cs_pin:  CS引脚编号
 * @param  reg:     寄存器地址
 * @param  data:    要写入的值
 */
void ACC_WriteReg(GPIO_TypeDef* cs_port, uint16_t cs_pin, uint8_t reg, uint8_t data)
{
    HAL_GPIO_WritePin(cs_port, cs_pin, GPIO_PIN_RESET);
    HAL_SPI_Transmit(&hspi1, &reg, 1, 100);
    HAL_SPI_Transmit(&hspi1, &data, 1, 100);
    HAL_GPIO_WritePin(cs_port, cs_pin, GPIO_PIN_SET);
}

/**
 * @brief  配置单颗加速度计的TDM时隙
 * @note   寄存器定义为示例,请严格按照加速度计数据手册修改
 *         假设:0x20寄存器为TDM控制寄存器
 *               bit[3:0] = 时隙编号
 *               bit[7]   = TDM模式使能
 */
void ACC_SetTDMSlot(GPIO_TypeDef* cs_port, uint16_t cs_pin, uint8_t slot)
{
    uint8_t reg_val = (slot & 0x0F) | (1 << 7);
    ACC_WriteReg(cs_port, cs_pin, 0x20, reg_val);
}

/**
 * @brief  初始化全部4颗加速度计
 */
void ACC_AllInit(void)
{
    // 所有CS引脚初始拉高
    HAL_GPIO_WritePin(ACC1_CS_GPIO_Port, ACC1_CS_Pin, GPIO_PIN_SET);
    HAL_GPIO_WritePin(ACC2_CS_GPIO_Port, ACC2_CS_Pin, GPIO_PIN_SET);
    HAL_GPIO_WritePin(ACC3_CS_GPIO_Port, ACC3_CS_Pin, GPIO_PIN_SET);
    HAL_GPIO_WritePin(ACC4_CS_GPIO_Port, ACC4_CS_Pin, GPIO_PIN_SET);
    HAL_Delay(20); // 等待传感器上电稳定

    // 给4颗芯片分别分配时隙0~3(必须不重复)
    ACC_SetTDMSlot(ACC1_CS_GPIO_Port, ACC1_CS_Pin, 0);
    ACC_SetTDMSlot(ACC2_CS_GPIO_Port, ACC2_CS_Pin, 1);
    ACC_SetTDMSlot(ACC3_CS_GPIO_Port, ACC3_CS_Pin, 2);
    ACC_SetTDMSlot(ACC4_CS_GPIO_Port, ACC4_CS_Pin, 3);

    // 可补充:配置采样率、量程、带宽等参数
    // ACC_WriteReg(ACC1_CS_GPIO_Port, ACC1_CS_Pin, 0x21, 0x03);

    HAL_Delay(10); // 等待配置生效
}

4. TDM 数据接收回调与解析

使用乒乓缓冲机制:DMA 写前半帧时处理后半帧数据,写后半帧时处理前半帧数据,保证连续采集不丢数。

cpp 复制代码
/**
 * @brief  SAI DMA半传输完成回调
 * @note   前半帧(第1帧4个时隙)数据就绪
 */
void HAL_SAI_RxHalfCpltCallback(SAI_HandleTypeDef *hsai)
{
    if (hsai->Instance == SAI1_Block_A)
    {
        tdm_buf_idx = 0;
        tdm_data_ready = 1;
    }
}

/**
 * @brief  SAI DMA全传输完成回调
 * @note   后半帧(第2帧4个时隙)数据就绪
 */
void HAL_SAI_RxCpltCallback(SAI_HandleTypeDef *hsai)
{
    if (hsai->Instance == SAI1_Block_A)
    {
        tdm_buf_idx = 1;
        tdm_data_ready = 1;
    }
}

/**
 * @brief  解析单时隙32位原始数据为三轴加速度
 * @note   解析规则为示例,请按芯片手册的TDM输出格式修改
 *         假设格式:32位 = X轴16位(高) + Y轴8位 + Z轴8位(低)
 */
void ACC_ParseRaw(uint32_t raw, AccData_t* out)
{
    out->x = (int16_t)((raw >> 16) & 0xFFFF);          // 高16位:X轴
    out->y = (int16_t)((int8_t)((raw >> 8) & 0xFF));   // 中间8位:Y轴(符号扩展)
    out->z = (int16_t)((int8_t)(raw & 0xFF));          // 低8位:Z轴(符号扩展)

    // 如需转为物理量(g),乘以对应灵敏度系数即可
    // 例:±2g量程、16位分辨率 → 灵敏度 = 2 / 32768 ≈ 0.000061 g/LSB
    // float x_g = out->x * 0.000061f;
}

5. 主函数调用流程

cpp 复制代码
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_DMA_Init();
    MX_SAI1_Init();
    MX_SPI1_Init();

    /************************ 关键步骤 ************************/
    // 1. 先配置所有加速度计的TDM参数(必须先配置传感器,再启动SAI)
    ACC_AllInit();

    // 2. 启动SAI DMA循环接收
    // 长度=8个Word(32位),对应2帧×4时隙
    HAL_SAI_Receive_DMA(&hsai_BlockA1, (uint8_t*)tdm_rx_buf, 4 * 2);

    while (1)
    {
        if (tdm_data_ready)
        {
            tdm_data_ready = 0;

            // 按时隙顺序解析4颗芯片的数据
            for (uint8_t i = 0; i < 4; i++)
            {
                uint32_t raw_data = tdm_rx_buf[tdm_buf_idx][i];
                ACC_ParseRaw(raw_data, &acc_result[i]);
            }

            /***** 数据处理区 *****/
            // acc_result[0] → 第1颗加速度计(时隙0)
            // acc_result[1] → 第2颗加速度计(时隙1)
            // acc_result[2] → 第3颗加速度计(时隙2)
            // acc_result[3] → 第4颗加速度计(时隙3)
            // 可在此添加数据上传、存储、算法处理等逻辑
        }
    }
}

关键注意事项与调坑指南

1. 时钟配置是核心

  • BCLK 计算公式位时钟频率 = 采样率 × 时隙数 × 单时隙位宽
  • 必须使用 PLLSAI1 作为 SAI 时钟源,推荐外部 HSE 晶振输入,保证时钟精度;
  • CubeMX 中 Mckdiv 分频值需精确计算:BCLK = SAI内核时钟 / (2 × Mckdiv),若时钟偏差过大,数据会逐步错位。

2. 时序必须与传感器完全匹配

以下 4 个参数必须和加速度计手册完全一致,错一个就全乱:

  1. 帧同步极性(高有效 / 低有效)
  2. 位时钟采样边沿(上升沿 / 下降沿)
  3. 帧同步偏移(FS 与第一个数据位的对齐关系)
  4. 单时隙位宽、时隙总数量

3. 防止总线冲突

  • 同一总线上时隙号绝对不能重复,否则两颗芯片同时驱动数据线,数据全部损坏;
  • 确认传感器在非自身时隙时输出高阻态,建议数据线增加 10kΩ 上拉电阻;
  • 必须先配置完所有传感器,再启动 MCU 的 SAI 接收,避免初始帧错位。

4. 菊花链模式的差异

如果你的加速度计是菊花链式 TDM

  • 软件配置完全相同,无需单独配置时隙号;
  • 数据顺序与芯片物理级联顺序严格对应(第一颗→时隙 0,第二颗→时隙 1...);
  • 无需独立 CS 引脚,硬件接线更简单,同步精度更高。
相关推荐
深圳市晶科鑫实业有限公司1 小时前
AI服务器为何对低抖动差分晶振如此挑剔?
服务器·人工智能·单片机·物联网·车载系统·云计算·信息与通信
破晓单片机1 小时前
063、STM32项目分享:智能儿童防丢书包系统
stm32·单片机·嵌入式硬件
fffzd2 小时前
STM32:定时器--旋钮->旋转编码器!
stm32·单片机·嵌入式硬件·定时器·旋钮·旋转编码器
xxwxx__2 小时前
51单片机串口通信完全指南:从原理到实战(发送、接收、回环与控制)
c语言·单片机·嵌入式硬件·51单片机
一路往蓝-Anbo10 小时前
第三篇:ADC 与模拟前端
stm32·嵌入式硬件·嵌入式·硬件设计
Net_Walke11 小时前
【Linux系统】静态链接库与动态链接库
linux·嵌入式硬件
努力小周13 小时前
STM32智能安防系统
c语言·stm32·单片机·嵌入式硬件·物联网·计算机网络·pcb工艺
华科大胡子15 小时前
在STM32上跑通TinyML
stm32·单片机·嵌入式硬件
iCxhust16 小时前
C#进程管理程序
开发语言·汇编·stm32·单片机·c#·微机原理