在定时器中断中处理多通道数据采集

乐活   2025-01-02 07:15   内蒙古  
在做这个多通道的数据采集的时候,数据的处理是个难点,如果有蓝牙或者BLE做媒介的时候就更难搞了。
我平时喜欢定时器中断做处理。   一般定时器中断适合的场景是:
需要定时触发:任务必须在固定时间间隔内执行。
实时性要求高:任务需要精确控制时间,不能有太多延迟。
轻量级处理:中断中执行的任务应尽量短小,复杂任务可以通过信号量或标志位移到主循环中完成。
中断虽好,但是不要贪杯哦~
如果 ISR 执行时间过长,可能会导致错过下一个中断(特别是在高频率触发的情况下)。在这种情况下,需要检查中断的处理效率。
给一个demo:
#include "stm32f4xx_hal.h"
// 定义一个标志变量volatile uint8_t timer_flag = 0;
// 定时器中断回调函数void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { // 判断是否是定时器2 timer_flag = 1; // 设置标志,表示定时器触发 }}
int main(void) { HAL_Init(); // 初始化 HAL 库 SystemClock_Config(); // 配置系统时钟 MX_TIM2_Init(); // 初始化定时器2
HAL_TIM_Base_Start_IT(&htim2); // 启动定时器并使能中断
while (1) { if (timer_flag) { // 检查标志是否被设置 timer_flag = 0; // 清除标志 // 定时触发的代码 HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 切换 LED 状态 } }}

定时器来了,一般是来了一个小小的好算的数

比如1S来一次,中断内部有个数值来记录进入的次数。一次又一次,我们就可以判断次数来执行代码。

HAL库启动以前需要手动开启IT一次,后面就在执行触发代码前要清楚我们的标志,这个标志是一个全局变量,作用就是提醒这次的代码。
检查中断频率:确保定时器频率(中断周期)合适,不要让 ISR 执行时间过长。
在中断中设置标志或将任务放入队列,在主循环中处理,从而避免中断中运行复杂代码。
合理分配中断优先级,避免多个中断之间互相影响。这些是我给的编写中断的建议。
下面这个代码就是一个中断函数,但是比较典型。典型在这个中断函数太长了,所以在最下面有一些整改建议。
  1. 定时触发 ADC 数据读取:读取 4 个通道的 ADC 数据并进行累加和平均处理。
  2. 滤波处理:对采集的 ADC 数据进行滤波,包括 50Hz陷波滤波和 IIR 滤波。
  3. 数据打包与发送:将处理后的数据以 BLE(蓝牙低功耗)数据包格式进行封装,并通过 DMA 发送。
  4. CRC 校验:为每个通道的封装数据生成校验码,确保数据完整性。
一般就是要使用一个定时器

判断是否是定时器 2 触发了中断。如果是,则执行后续逻辑。因为是按时进去,那每一次进来都会记一下,然后就可以实现比如5ms,10ms,15ms执行任务。

增加 tim_counter,用于控制每隔 4 次中断保存一次数据。

定时保存数据:
  • 每 4 次中断(tim_counter == 3)将处理后的 ADC 数据保存到 BLE_Packet_to_Send。
  • ADC_Sample_Counter 递增,记录当前采样次数。
满数据发送:
  • 当采样次数达到 DMA_MAX_SEND 时,生成 CRC 校验值。
  • 调用 HAL_UART_Transmit_DMA 通过 UART 的 DMA 模块发送封装的数据。

线保存在BLE的封包里面,当封包里面的DMA满了,就直接使用UART穿出去,这个代码框架可以当做一个模板使用。

我也一直在学习,编程的时候我们在关注什么?我回答是其实是数据。外设都是固定的,无非也是抽象的读写。

但是数据却是我们一直关注,一个数据来了,它是什么样的?现在完整了吗?接下来要干嘛?给下一级?下一级要什么样的?应该怎么修改?其实都是对数据做操作而已。

赶紧进来获得来自ADC的数据

调用 AD7682_Read_4_ADC_Value 采集 ADC 的 4 通道数据,并累加两次。
sum_1[] 是 4 个通道的累加值。

将累加的值右移 1 位(除以 2),得到平均值。
sum_1[] 清零,为下一次累加做准备。

然后滤波

IIR_50HZ_Norch_Filter 是一个 50Hz 陷波滤波器,去除电源噪声。
applyIIRFilter 是一个通用的 IIR 滤波器,对数据进一步平滑处理。

将 16 位 ADC 数据分解成两个 8 位字节,便于通过 BLE 通信协议传输(BLE 通信通常以字节为单位传输数据)。

这是完整的一组

ADC_Value_Receive_1[ADC_Sample_Counter * 2] = (uint8_t)((ADC_Value_u16[2] & 0XFF00) >> 8);ADC_Value_u16[2]
是一个 16 位(uint16_t)的 ADC 数据。
  1. & 0xFF00:保留高 8 位(清除低 8 位)。
  2. >> 8:将高 8 位右移到低 8 位的位置。
  3. (uint8_t):将 16 位数据截断为 8 位(高字节)。
  4. ADC_Sample_Counter * 2:表示数据在 ADC_Value_Receive_1 数组中的位置。
  5. 存储结果:将高 8 位存入 ADC_Value_Receive_1 的当前位置。
ADC_Value_Receive_1[ADC_Sample_Counter * 2 + 1] = (uint8_t)(ADC_Value_u16[2] & 0X00FF);& 0x00FF
  1. 保留低 8 位(清除高 8 位)。
  2. (uint8_t):将低 8 位数据截断为 8 位(低字节)。
  3. ADC_Sample_Counter * 2 + 1:表示数据在 ADC_Value_Receive_1 数组中的位置。
  4. 存储结果:将低 8 位存入 ADC_Value_Receive_1 的下一个位置。

详细分析

BLE_Packet_to_Send[Channel1_Data_Start_Counter + ADC_Sample_Counter * 2] = ADC_Value_Receive_1[ADC_Sample_Counter * 2];
  1. 从 ADC_Value_Receive_1 中读取高字节数据。
  2. 将高字节数据写入 BLE_Packet_to_Send 数组的对应位置。
  3. Channel1_Data_Start_Counter:是 Channel 1 数据在 BLE_Packet_to_Send 中的起始位置。
  4. ADC_Sample_Counter * 2:计算当前样本的偏移。
BLE_Packet_to_Send[Channel1_Data_Start_Counter + ADC_Sample_Counter * 2 + 1] = ADC_Value_Receive_1[ADC_Sample_Counter * 2 + 1];
  1. 从 ADC_Value_Receive_1 中读取低字节数据。
  2. 将低字节数据写入 BLE_Packet_to_Send 数组的对应位置。
  3. 数据偏移计算同上,但存储到紧接的位置。

从MPU6050看传感器原始数据的处理方式-位运算  看不懂?就先复习一下我的文章。

清醒一点,我们的要求就是对原始数据重新塑造然后传到下个封包里面。

分解 16 位数据:
ADC_Value_u16[2] 分解为两个 8 位字节,高位和低位分别存储到 ADC_Value_Receive_1 数组中。
组织 BLE 数据包,将高字节和低字节从:
ADC_Value_Receive_1 
复制到 BLE_Packet_to_Send 数组中,准备通过 BLE 发送。

可以封装一个函数:

void Pack_ADC_Data(uint16_t adc_value, uint8_t *receive_buffer, uint8_t *ble_buffer, int sample_counter, int start_counter) {    receive_buffer[sample_counter * 2] = (uint8_t)((adc_value & 0xFF00) >> 8);    receive_buffer[sample_counter * 2 + 1] = (uint8_t)(adc_value & 0x00FF);    ble_buffer[start_counter + sample_counter * 2] = receive_buffer[sample_counter * 2];    ble_buffer[start_counter + sample_counter * 2 + 1] = receive_buffer[sample_counter * 2 + 1];}

调用:

Pack_ADC_Data(ADC_Value_u16[2]ADC_Value_Receive_1BLE_Packet_to_SendADC_Sample_CounterChannel1_Data_Start_Counter);

CRC和发送

什么时候不适合使用中断?

任务执行时间过长:如果中断内代码耗时过长,会影响其他任务的执行,甚至导致系统崩溃。
如不需要精确时间间隔,主循环或事件触发可能是更好的选择。
中断优先级竞争:当系统中有多个高优先级中断时,可能导致定时器中断被延迟。

然后就批判一下上面这个函数,

第一杀,多次调用函数:
AD7682_Read_4_ADC_Value():多次调用,可能涉及 SPI/I²C 等通信操作,通常较耗时。
这个ADC我还写过:精密小体积ADC-AD7682 16位4通道
IIR_50HZ_Norch_Filter() 和 applyIIRFilter():滤波运算较为复杂,尤其是涉及浮点或大量运算时。
crc16_ibm():用于生成 CRC 校验,处理大量数据时会占用 CPU 资源。可以使用外设CRC好一些。
第二杀,复杂数据处理:采集的 ADC 数据进行求和、平均计算,以及数据封包。BLE 数据填充与打包。  需要大量的执行计算过程来分发新包。
怎么做?
其实很简单,就是剥离耗时操作。

在中断中只执行简单的采样操作,将数据存入缓冲区。主循环读取缓冲区的数据并进行滤波、平均计算和 BLE 打包。
还有什么呢?就是这个频率问题,因为不同的函数,不同的外设,大家的步调不一样
ADC 采样频率过高:如果定时器触发频率较高(如 1 ms),而 BLE 数据发送的需求较低(如每 10 ms),会导致不必要的数据处理。除非你有一个缓存区存这个东西,还要发的快。
BLE 打包延迟:BLE 通信的频率通常低于 ADC 采样频率。如果在每次中断中都执行 BLE 打包,会导致发送延迟和资源浪费。

然后,中断中只采样 ADC 数据并存入一个环形缓冲区。在中断中设置标志位,主循环中根据标志位执行滤波和通信操作。其实就是在较长的时间后开始处理数据。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {    if (htim == &htim2) {        // 简单采样操作        AD7682_Read_4_ADC_Value(ADC_Value_Buffer);        // 将数据存入环形缓冲区        ring_buffer_write(&adc_ring_buffer, ADC_Value_Buffer);        // 设置标志位        data_ready_flag = 1;    }}

然后写个循环代码:

void main_loop() {    while (1) {        if (data_ready_flag) {            data_ready_flag = 0;
// 从环形缓冲区读取数据 ring_buffer_read(&adc_ring_buffer, Processed_Data);
// 执行滤波操作 IIR_50HZ_Norch_Filter(Processed_Data, Filtered_Data); applyIIRFilter(Filtered_Data, Final_Data);
// 数据打包与 BLE 发送 BLE_Packet_to_Send(...); } }}

记住判断和清空,下面就是大家干活,读数据,滤波和发送

最后就简单了, ADC 采样10 ms 触发一次。BLE 数据发送频率较低,可以通过一个较慢的定时器(100 ms)单独触发 BLE 打包和发送。

以前觉得这些东西很难,但是到现在我觉得这些东西分外的清晰,我想就是流程清晰,也能知道各种方法。

大概总结一下,先梳理流程,分割各种任务快,把和外部交互的放在中断,保持实时性,处理放在循环里面。如果一个快一个慢,就搞个缓存区。

云深之无迹
纵是相见,亦如不见,潇湘泪雨,执念何苦。
 最新文章