约定参数(可根据实际芯片修改)
| 参数项 | 取值 | 说明 |
|---|---|---|
| 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, ®, 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 个参数必须和加速度计手册完全一致,错一个就全乱:
- 帧同步极性(高有效 / 低有效)
- 位时钟采样边沿(上升沿 / 下降沿)
- 帧同步偏移(FS 与第一个数据位的对齐关系)
- 单时隙位宽、时隙总数量
3. 防止总线冲突
- 同一总线上时隙号绝对不能重复,否则两颗芯片同时驱动数据线,数据全部损坏;
- 确认传感器在非自身时隙时输出高阻态,建议数据线增加 10kΩ 上拉电阻;
- 必须先配置完所有传感器,再启动 MCU 的 SAI 接收,避免初始帧错位。
4. 菊花链模式的差异
如果你的加速度计是菊花链式 TDM:
- 软件配置完全相同,无需单独配置时隙号;
- 数据顺序与芯片物理级联顺序严格对应(第一颗→时隙 0,第二颗→时隙 1...);
- 无需独立 CS 引脚,硬件接线更简单,同步精度更高。