4. 传感器数据采集基础:轮询模式与中断模式、DMA传输、定时器触发采集、多传感器同步采集

好,咱们今天聊点实在的。传感器数据采集,说白了就是让单片机“感知”外部世界。你想想看,一个温度传感器、一个加速度计,它们把物理量变成了电信号,但单片机怎么知道这个信号变了?怎么把数据拿回来?这里面的门道,就是我这些年一直在跟各种传感器打交道积累下来的经验。

我个人习惯把数据采集分成几个层次:怎么知道数据来了、怎么把数据搬回来、什么时候去采集、多个传感器怎么协调。今天咱们就把这四件事儿彻底讲透。

核心知识点速览

  • 轮询模式:CPU主动问,简单但浪费
  • 中断模式:传感器主动喊,高效但要注意优先级
  • DMA传输:数据自己走,CPU不用管
  • 定时器触发:按固定节奏采集,精准控制
  • 多传感器同步:时间戳对齐,数据才有意义
传感器数据采集 轮询模式 CPU 主动查询状态 中断模式 传感器主动通知 CPU DMA 传输 数据直接搬运到内存 定时器触发 按固定时间间隔采集 多传感器同步 时间戳对齐与协调 选择哪种模式?看实时性要求和功耗预算

4.1 轮询模式:最直接,但别滥用

轮询模式,就是CPU不停地去问传感器:“嘿,你有新数据了吗?” 传感器说“没有”,CPU继续问。直到传感器说“有了”,CPU才去读数据。

我在项目中遇到过一个小白同事,用轮询读一个气压传感器,主循环里每1ms问一次。结果呢?传感器本身更新速率才10Hz,CPU 99%的时间都在做无用功。说白了,轮询模式适合那些数据变化快、且CPU闲着也是闲着的场景。

我的建议:轮询模式适合以下情况:

  • 传感器数据更新频率很高(比如 > 1kHz)
  • CPU没有其他紧急任务要处理
  • 代码简单,调试方便
// 轮询模式示例:读取温度传感器
uint32_t last_read = 0;
while(1) {
    if(HAL_GetTick() - last_read > 100) {  // 每100ms读一次
        if(TSensor_DataReady()) {           // 查询数据是否就绪
            temperature = TSensor_Read();    // 读取数据
            last_read = HAL_GetTick();
        }
    }
    // 其他任务...
}

注意:轮询模式下,如果传感器响应慢,CPU会被卡住。我曾经在一个电机控制项目里用轮询读编码器,结果因为传感器偶尔延迟,导致控制周期抖动,电机嗡嗡响。后来果断换了中断模式。

4.2 中断模式:让传感器主动找你

中断模式就聪明多了。传感器数据准备好了,它会拉一根引脚(INT引脚)告诉CPU:“我有数据了,快来拿!” CPU收到这个信号,暂停手头的工作,跑去读数据,读完了再回来继续干活。

你想想看,这就像你正在写代码,同事有问题会直接过来问你,而不是你每隔一分钟就去问一圈“有问题吗?”——效率高多了。

// 中断模式示例:配置传感器中断
void Sensor_Init_With_Interrupt(void) {
    // 配置传感器INT引脚为输入,上升沿触发中断
    HAL_GPIO_EXTI_Init(&gpio_int_config);
    
    // 使能传感器中断输出
    Sensor_WriteReg(0x0D, 0x01);  // 使能数据就绪中断
}

// 中断服务函数
void EXTI0_IRQHandler(void) {
    if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0)) {
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
        
        // 读取传感器数据(中断里要快进快出)
        sensor_data = Sensor_ReadData();
        
        // 设置标志位,让主循环处理数据
        data_ready_flag = 1;
    }
}

中断模式的关键点:

  • 中断服务函数要短小精悍,别在里面做复杂运算
  • 注意中断优先级,高频率中断别抢了系统心跳
  • 多个传感器共用中断线时,要在中断里判断是谁触发的

我记得有一次调试一个IMU模块,中断信号一直触发,CPU根本没法干别的。查了半天,原来是传感器配置错了,中断模式设成了“数据溢出中断”,而不是“新数据就绪中断”。嗯,配置寄存器一定要仔细看数据手册。

4.3 DMA传输:数据自己走,CPU解放了

DMA(Direct Memory Access)是个好东西。它让数据直接从外设(比如SPI、I2C接口)搬到内存里,完全不用CPU插手。CPU只需要告诉DMA:“你去把传感器那1000个数据搬过来”,然后就可以去干别的事了。搬完了,DMA会发个中断通知CPU。

我做过一个音频采集项目,麦克风以48kHz采样,每秒钟48000个16位数据。如果用中断模式,CPU每20微秒就要进一次中断,基本啥也干不了。用了DMA之后,CPU占用率从95%降到了5%。

// DMA传输示例:通过SPI读取传感器数据
#define BUFFER_SIZE  256
uint8_t spi_rx_buffer[BUFFER_SIZE];

void Sensor_Start_DMA_Read(void) {
    // 启动SPI DMA接收,数据直接存入缓冲区
    HAL_SPI_Receive_DMA(&hspi1, spi_rx_buffer, BUFFER_SIZE);
}

// DMA传输完成回调
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
    if(hspi->Instance == SPI1) {
        // 数据已经在 spi_rx_buffer 里了,直接处理
        Process_Sensor_Data(spi_rx_buffer, BUFFER_SIZE);
        
        // 启动下一次DMA传输(循环模式)
        HAL_SPI_Receive_DMA(&hspi1, spi_rx_buffer, BUFFER_SIZE);
    }
}

使用DMA的几个坑:

  • DMA缓冲区要小心Cache一致性问题(特别是带Cache的MCU)
  • DMA传输完成中断里不要做耗时操作,否则会影响下一次传输
  • 环形缓冲区(Circular Buffer)配合DMA循环模式,可以实现无间断采集

4.4 定时器触发采集:精准的时间节拍

很多传感器支持“定时采集”模式。你设置一个定时器,让它每隔固定时间触发一次ADC转换或者传感器读取。这样做的好处是:采集间隔精确可控,不受代码执行时间影响。

我做过一个振动监测项目,要求以1kHz的固定频率采集加速度数据。如果用主循环里延时,代码里加个if判断都会导致时间抖动。后来我用定时器直接触发ADC,精度到了微秒级。

// 定时器触发ADC采集配置
void Timer_Trigger_ADC_Init(void) {
    // 配置定时器,1ms触发一次(1kHz)
    htim2.Init.Period = 999;  // 假设定时器时钟1MHz
    htim2.Init.Prescaler = 0;
    HAL_TIM_Base_Init(&htim2);
    
    // 配置ADC为定时器触发模式
    hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_TRGO;
    HAL_ADC_Init(&hadc1);
    
    // 启动定时器
    HAL_TIM_Base_Start(&htim2);
    // 启动ADC,每次定时器触发自动采集
    HAL_ADC_Start_DMA(&hadc1, adc_buffer, BUFFER_SIZE);
}

定时器触发的优势:

  • 采集间隔精确,适合FFT、控制环路等需要固定采样率的场景
  • 可以同时触发多个外设(比如ADC和DAC同步)
  • 配合DMA,实现“零CPU开销”的周期性采集

4.5 多传感器同步采集:时间对齐是灵魂

这是最难的一块。当你有多个传感器(比如加速度计、陀螺仪、磁力计),它们各自有各自的时钟,各自有各自的采集时刻。如果你只是分别读它们的数据,然后拼在一起,你会发现:这些数据根本不在同一个时间点上。

举个例子,你用一个加速度计和一个陀螺仪做姿态解算。加速度计在t=0时刻采集,陀螺仪在t=5ms时刻采集。如果你把它们当成同一时刻的数据去算姿态,结果就是——姿态会漂移,而且漂得离谱。

我踩过这个坑。在一个无人机飞控项目里,IMU数据不同步,导致姿态解算出来的角度一直在抖,飞机根本稳不住。后来我用了硬件同步机制才解决。

多传感器同步的几种方案:

方案 精度 复杂度 适用场景
软件时间戳 低(ms级) 低 对同步要求不高的场景
硬件触发线 高(us级) 中 IMU、多轴传感器
共享时钟/同步信号 极高(ns级) 高 高速数据采集系统
// 多传感器硬件同步示例:使用一个GPIO同时触发多个传感器
void Multi_Sensor_Sync_Init(void) {
    // 配置一个定时器,产生同步脉冲
    HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
    
    // 配置传感器1:接收同步信号
    Accel_WriteReg(0x1A, 0x40);  // 使能硬件触发模式
    
    // 配置传感器2:接收同一个同步信号
    Gyro_WriteReg(0x1B, 0x40);   // 使能硬件触发模式
    
    // 现在,定时器每产生一个脉冲,两个传感器同时采集
    // 数据天然就是同步的!
}

我的经验:如果传感器不支持硬件同步,退而求其次可以用“中断+时间戳”方案。每个传感器数据就绪时,在中断里记录当前系统时间(比如定时器的计数值)。这样虽然采集时刻不同,但你知道每个数据是什么时候采集的,后期可以通过插值对齐。

小结一下

传感器数据采集,没有银弹。轮询简单但浪费CPU,中断高效但要注意优先级,DMA解放CPU但要注意缓冲区管理,定时器触发精准但配置复杂,多传感器同步是系统级难题。

我个人习惯是:能用DMA就用DMA,能用硬件同步就用硬件同步。实在不行,中断+时间戳兜底。轮询嘛...除非你确定CPU真的闲得发慌,否则别用。

好了,这一章的内容就到这儿。记住,采集数据只是第一步,怎么处理这些数据才是真正的挑战。下一章咱们聊聊数据滤波和特征提取——那又是另一片天地了。

可可精品资料库