333
你贴出的这段代码实现了一个简单的轮询式定时任务调度器(Scheduler),非常适合用于裸机编程(如STM32)中替代复杂的RTOS(操作系统)功能。下面我会详细讲解这段代码的功能、运行机制以及你可以怎么用它。
🔧 这段代码的作用是什么?
在主循环中定时运行你设定的一些任务函数,比如串口处理、按键检测、ADC采样、LCD刷新等。
📦 核心结构体
typedef struct {
void (*task_func)(void); // 要执行的函数指针(任务)
uint32_t rate_ms; // 执行频率(毫秒)
uint32_t last_run; // 上一次运行时间
} scheduler_task_t;
你声明了一个任务列表数组:
static scheduler_task_t scheduler_task[] = {
{uart_proc, 1, 0}, // 每1ms执行一次 uart_proc 函数
{key_proc, 5, 0}, // 每5ms执行一次 key_proc 函数
};
其他任务比如 led_proc
, lcd_proc
等暂时被注释掉了。
🚀 scheduler_init()
void scheduler_init(void)
{
task_num = sizeof(scheduler_task) / sizeof(scheduler_task_t);
}
计算有多少个任务。task_num
会用于后续循环中。
🔁 scheduler_run()
void scheduler_run(void)
{
for (uint8_t i = 0; i < task_num; i++)
{
uint32_t now_time = HAL_GetTick(); // 当前系统时间(ms)
if (now_time >= scheduler_task[i].rate_ms + scheduler_task[i].last_run)
{
scheduler_task[i].last_run = now_time; // 更新时间戳
scheduler_task[i].task_func(); // 调用任务函数
}
}
}
每次你在主循环(while(1)
)中调用 scheduler_run()
,它就会检查每个任务是否该执行了,然后执行。
🧠 示例用法(主循环中):
int main(void)
{
HAL_Init();
scheduler_init(); // 初始化任务列表
while (1)
{
scheduler_run(); // 每次循环调度任务
}
}
📈 工作机制图解
Time(ms) --> 0 1 2 3 4 5 6 7 ...
↑ ↑ ↑
uart_proc uart_proc key_proc
uart_proc
每 1ms 被调用一次key_proc
每 5ms 被调用一次- 每次调用都会检查时间戳是否到期
你这段代码是一个基于 UART + DMA 双缓冲机制的串口数据接收与处理程序,用于实时接收并解包 FPGA 发送的 ADC 数据帧,并进行 FFT 结果处理和输出。它结合了 HAL 库中 UART DMA 接收中断的功能,典型应用场景是高速数据采集与处理系统,比如脑电、地震、通信信号处理等。
✅ 一句话总结:
这段代码的主要功能是:接收 FPGA 发来的 ADC 数据(使用 UART DMA 半中断+全中断机制),每帧数据解包到 FFT 输出数组中,并定时打印结果或触发下一次采集。
🔍 一步步剖析功能
① 变量说明
#define data_length (DMA_BUFFER_SIZE / 2) // 半缓冲区大小
#define max_frames (data_length / 12) // 每帧12字节,最大帧数
bool AdcConq; // UART DMA接收完成(Full Transfer)中断标志
bool AdcConqHalf; // UART DMA接收半满(Half Transfer)中断标志
bool DataUnpkDone; // 数据解包完成标志
uint16_t frame_index; // 当前帧索引
FrameData FFT_output[2048]; // 解包后的FFT结果数组(结构体)
uint8_t num; // 调试用计数器
② 中断回调函数(DMA接收事件触发)
✅ HAL_UART_RxCpltCallback()
DMA接收完整时调用(数据写入buffer的后半段)
HAL_UART_Receive_DMA(&huart1, &uart_rx_dma_buffer[0], DMA_BUFFER_SIZE); // 重新开启DMA
AdcConq = true; // 标志位置1,说明接收后半段完成
✅ HAL_UART_RxHalfCpltCallback()
DMA接收到一半时调用(数据写入buffer的前半段)
AdcConqHalf = true; // 标志位置1,说明前半段写满
③ 核心数据处理函数 uart_proc()
这是你在任务调度器中以 1ms 间隔运行的任务。
第一步:处理 DMA半中断数据(前半段)
if(AdcConqHalf)
{
AdcConqHalf = false;
uart_unpack(&uart_rx_dma_buffer[0], data_length, FFT_output, &frame_index);
}
第二步:处理 DMA全中断数据(后半段)
if(AdcConq)
{
AdcConq = false;
if (uart_unpack(&uart_rx_dma_buffer[data_length], data_length, FFT_output, &frame_index))
{
DataUnpkDone = 1;
}
printf("nowframe: %u \r\n", num++);
}
第三步:处理 解包后的数据
if(DataUnpkDone)
{
DataUnpkDone = 0;
// 打印两个通道的FFT幅值数据
for(uint16_t i = 0; i < 2048; i++)
printf("%.2f ", FFT_output[i].ch0_mag);
for(uint16_t i = 0; i < 2048; i++)
printf("%.2f ", FFT_output[i].ch1_mag);
// 通知FPGA进行下一次采样
fpga_adc_trigger();
}
🔧 解包函数 uart_unpack(...)
作用推测:
你没有贴出 uart_unpack()
的代码,但根据上下文可以推测它做的事情是:
- 从UART DMA缓冲区中逐帧读取原始数据(每帧可能是12字节)
- 解码每一帧的通道0/1的 FFT 幅值(或原始 ADC 值)
- 存入
FFT_output
结构体数组中
🔁 数据流完整流程图
[FPGA] ----UART---> [STM32 UART DMA Buffer]
↓
Half Transfer Interrupt (缓冲前半满)
↓
-> uart_unpack(&buffer[0])
↓
Full Transfer Interrupt (缓冲后半满)
↓
-> uart_unpack(&buffer[1/2])
↓
→ DataUnpkDone → 打印FFT数据 + 触发FPGA采样
🧠 总结一下整个系统在干嘛?
模块 | 功能 |
---|---|
UART DMA | 高速接收 FPGA 发来的 ADC 数据流 |
回调函数 | 标志位通知缓冲区前半/后半接收完成 |
uart_proc() |
定时调度器中运行,解包并处理数据 |
uart_unpack() |
将原始数据帧解析成 FFT 幅值 |
fpga_adc_trigger() |
通知 FPGA 重新开始下一次 ADC 采样 |
你这段代码实现的是一个按键扫描与处理模块,通过检测多个 GPIO 引脚的状态,识别按键按下/松开的事件,并根据具体按键触发不同的操作(如控制 LED、触发 FPGA 采样等)。
✅ 总体一句话总结:
这段代码用来轮询识别4个按键的按下/释放事件,并根据不同按键执行不同的动作(例如控制 LED 或启动采样),典型用于嵌入式系统中的人机交互输入。
📦 变量含义
uint8_t key_val = 0; // 当前按键值
uint8_t key_old = 0; // 上一次的按键值
uint8_t key_down = 0; // 按键按下事件(当前为1,之前为0)
uint8_t key_up = 0; // 按键释放事件(之前为1,现在为0)
🔍 key_read()
函数 —— 读取当前哪个按键被按下
uint8_t key_read(void)
{
uint8_t temp = 0;
if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_4) == GPIO_PIN_RESET) temp = 1; // 按键1按下
if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_3) == GPIO_PIN_RESET) temp = 2; // 按键2按下
if(HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_2) == GPIO_PIN_RESET) temp = 3; // 按键3按下
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET) temp = 4; // 按键4按下
return temp;
}
注意:不同按键可能是低电平有效(用
RESET
检查),也可能是高电平有效(用SET
检查),具体看电路原理图。
🔁 key_proc()
函数 —— 每次轮询时进行按键事件检测与处理
这个函数是你放到 scheduler_task[]
中周期性执行的。
key_val = key_read(); // 当前键值
key_up = key_val & (key_old ^ key_val); // 按键释放事件检测
key_down = ~key_val & (key_old ^ key_val); // 按键按下事件检测
key_old = key_val; // 更新历史状态
✨ 如何检测“按下”:
使用异或 (key_old ^ key_val)
检测状态变化,然后与当前/反当前值组合:
key_down
:当前为 0,原来是 1 → 按键刚刚按下key_up
:当前为 1,原来是 0 → 按键刚刚释放
🔘 按键功能实现(switch(key_down)
)
根据按下的是哪个按键,执行对应功能:
switch(key_down)
{
case 1:
// 按键1按下:未定义动作(留空)
break;
case 2:
// 按键2按下:LED0灭,LED1亮
ucLed[0] = 0;
ucLed[1] = 1;
break;
case 3:
// 按键3按下:LED0亮,LED1灭
ucLed[0] = 1;
ucLed[1] = 0;
break;
case 4:
// 按键4按下:触发FPGA采样(或者启动UART DMA接收)
// HAL_UART_Receive_DMA(...) 被注释掉了
fpga_adc_trigger();
break;
}
🧠 总结功能表:
按键编号 | 引脚 | 电平逻辑 | 功能描述 |
---|---|---|---|
1 | PE4 | 低电平有效 | 留空 |
2 | PE3 | 低电平有效 | LED0灭、LED1亮 |
3 | PE2 | 低电平有效 | LED0亮、LED1灭 |
4 | PA0 | 高电平有效 | 触发一次 FPGA 采样命令 |