在做这个多通道的数据采集的时候,数据的处理是个难点,如果有蓝牙或者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 执行时间过长。
在中断中设置标志或将任务放入队列,在主循环中处理,从而避免中断中运行复杂代码。
合理分配中断优先级,避免多个中断之间互相影响。这些是我给的编写中断的建议。
下面这个代码就是一个中断函数,但是比较典型。典型在这个中断函数太长了,所以在最下面有一些整改建议。

一般就是要使用一个定时器

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

增加 tim_counter,用于控制每隔 4 次中断保存一次数据。
定时保存数据:
满数据发送:
线保存在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 数据。
ADC_Value_Receive_1[ADC_Sample_Counter * 2 + 1] = (uint8_t)(ADC_Value_u16[2] & 0X00FF);& 0x00FF
详细分析
BLE_Packet_to_Send[Channel1_Data_Start_Counter + ADC_Sample_Counter * 2] = ADC_Value_Receive_1[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];从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_1, BLE_Packet_to_Send, ADC_Sample_Counter, Channel1_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 打包和发送。
以前觉得这些东西很难,但是到现在我觉得这些东西分外的清晰,我想就是流程清晰,也能知道各种方法。
大概总结一下,先梳理流程,分割各种任务快,把和外部交互的放在中断,保持实时性,处理放在循环里面。如果一个快一个慢,就搞个缓存区。