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 采样命令
posted @ 2025-07-20 20:31  无敌烤肉大王  阅读(6)  评论(0)    收藏  举报