
蓝桥杯嵌入式第十五届省赛笔记
一、赛题功能概述 本题要求实现一个 双通道频率检测与分析系统 ,核心功能包括: | 功能模块 | 描述 | | | | | 频率采集 | 通过输入捕获测量两路信号(通道A、通道B)的频率 | | LCD 显示 | 三个页面切换显示:数据页 DATA、参数页 PARA、记录页 RECD | | 按键控制 | 4 个按键实现页面切换、参数调节、模式切换、长按清零 | | LED 指示 | 根据系统状态点亮/熄灭对应 LED | | 频率波动…
FIELD_GUIDE
FIELD GUIDE
Use the guide rail to jump between sections.
一、赛题功能概述
本题要求实现一个 双通道频率检测与分析系统,核心功能包括:
| 功能模块 | 描述 |
|---|---|
| 频率采集 | 通过输入捕获测量两路信号(通道A、通道B)的频率 |
| LCD 显示 | 三个页面切换显示:数据页 DATA、参数页 PARA、记录页 RECD |
| 按键控制 | 4 个按键实现页面切换、参数调节、模式切换、长按清零 |
| LED 指示 | 根据系统状态点亮/熄灭对应 LED |
| 频率波动检测 | 统计频率波动超标次数 |
| 频率越限检测 | 统计频率超过阈值的次数 |
二、硬件资源分配
2.1 定时器资源
| 定时器 | 功能 | 引脚 | 配置 |
|---|---|---|---|
| TIM2 | 输入捕获(通道A) | PA15 (TIM2_CH1) | PSC=79, ARR=65535, 上升沿捕获 |
| TIM16 | 输入捕获(通道B) | PB4 (TIM16_CH1) | PSC=79, ARR=65535, 上升沿捕获 |
| TIM7 | 基本定时器(1ms 周期中断) | 无 | PSC=79, ARR=999 |
为什么 PSC 都配置为 79?
系统时钟为 80MHz,分频系数 = PSC + 1 = 80,所以定时器计数频率为:
即每个计数值代表 1μs,这样计算频率和周期时非常方便。
2.2 GPIO 资源
| 功能 | 引脚 |
|---|---|
| 按键 B1 | PB0(输入,无上拉) |
| 按键 B2 | PB1(输入,无上拉) |
| 按键 B3 | PB2(输入,无上拉) |
| 按键 B4 | PA0(输入,无上拉) |
| LED 数据线 | PC8 ~ PC15(输出) |
| LED 锁存使能 | PD2(输出) |
三、软件架构设计
3.1 整体架构 —— 非阻塞轮询
// main.c 主循环
while (1)
{
LCD_proc(); // LCD 显示处理
KEY_proc(); // 按键扫描处理
LED_proc(); // LED 状态更新
}
// 典型时间片写法
u32 lcd_tick = 0;
void LCD_proc()
{
if(uwTick - lcd_tick < 100) return; // 100ms 执行一次
lcd_tick = uwTick;
// ... 业务逻辑
}
3.2 文件组织
| 文件 | 职责 |
|---|---|
main.c | CubeMX 生成的主函数,初始化 + 主循环调用 |
user.c / user.h | 所有用户逻辑(LCD、KEY、LED、回调函数) |
tim.c | CubeMX 生成的定时器初始化 |
gpio.c | CubeMX 生成的 GPIO 初始化 |
四、核心功能详解
4.1 输入捕获测频率
这是本题最核心的技术点。采用 两次上升沿捕获法 测量信号周期,再求倒数得到频率。
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim == &htim2)
{
if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
{
if(uhCaptureIndex == 0)
{
// 第一次捕获:记录第一个上升沿的计数值
uwIC2Value1 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
uhCaptureIndex = 1;
}
else if(uhCaptureIndex == 1)
{
// 第二次捕获:记录第二个上升沿的计数值
uwIC2Value2 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
// 计算两次捕获的差值(即信号周期,单位 μs)
if (uwIC2Value2 > uwIC2Value1)
uwDiffCapture = uwIC2Value2 - uwIC2Value1;
else // 处理计数器溢出
uwDiffCapture = (0xFFFF - uwIC2Value1) + uwIC2Value2 + 1;
// 频率 = 1,000,000 / 周期(μs)
uwFrequency = 1e6 / uwDiffCapture;
uhCaptureIndex = 0;
}
}
}
// TIM16 通道B 逻辑完全相同
}
原理图解:
信号: ___/‾‾‾\___/‾‾‾\___/‾‾‾\___
↑ ↑
第1次捕获 第2次捕获
uwIC2Value1 uwIC2Value2
周期 T = uwIC2Value2 - uwIC2Value1 (单位: μs)
频率 f = 1,000,000 / T (单位: Hz)
为什么需要处理溢出?
定时器是 16 位(ARR=65535),如果信号周期较长,第二次捕获时计数器可能已经溢出归零。此时差值需要加上 0xFFFF - Value1 + Value2 + 1。
4.2 基本定时器周期回调(TIM7)
TIM7 每 1ms 产生一次中断,在 HAL_TIM_PeriodElapsedCallback 中完成以下逻辑:
TIM7 中断 (每 1ms)├── time7_tick++└── 每 100ms (time7_tick % 100 == 0)├── 存入缓冲区 a_frq_buff[frq_cnt]├── frq_cnt++└── 当 frq_cnt == 30 (即 3 秒)├── 计算 30 个采样的最大值和最小值└── frq_cnt 清零,重新采集├── 频率越限检测│ ├── a_frq > PH → LED2 亮,若从低到高跳变则 NHA++│ └── b_frq > PH → LED3 亮,若从低到高跳变则 NHB++
为什么用 100ms 采样、30 个点(3秒)做统计?
- 100ms 采样一次频率值,既不会过于频繁占用资源,也能保证足够的时间分辨率
- 30 个采样点 = 3 秒窗口,通过窗口内的最大值-最小值来判断频率波动幅度
- 这是一种简单高效的 滑动窗口统计 方法
4.3 频率越限检测(NHA / NHB)
// 以通道A为例
if(a_frq <= ph)
{
pha_lock = 0; // 频率低于阈值,解锁
led_num &= ~0x02; // LED2 灭
}
else
{
led_num |= 0x02; // LED2 亮
if(pha_lock == 0) // 上次是低于阈值的状态
{
nha++; // 计一次越限
pha_lock = 1; // 加锁,防止重复计数
}
}
为什么需要 pha_lock 锁?
频率可能长时间高于阈值 PH,如果每次检测到高于阈值就计数,会导致 重复计数。使用锁标志实现:
- 只在频率从低于 PH 跳变到高于 PH 的那一刻计数一次
- 必须等频率重新降到 PH 以下(
pha_lock = 0),下次再升上去才再计一次
这是一种经典的 边沿检测 思路。
4.4 按键处理
按键扫描与消抖
void KEY_read()
{
if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == 0) key_value = 1;
else if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == 0) key_value = 2;
else if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) == 0) key_value = 3;
else if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == 0) key_value = 4;
else key_value = 0;
key_down = key_value & (key_value ^ key_old); // 按下瞬间
key_up = ~key_value & (key_value ^ key_old); // 松开瞬间
key_old = key_value;
}
为什么用异或运算做边沿检测?
| 表达式 | 含义 |
|---|---|
key_value ^ key_old | 找出状态发生变化的键 |
& key_value | 变化的且当前为按下 → 按下沿 |
& ~key_value | 变化的且当前为松开 → 松开沿 |
这是蓝桥杯嵌入式中最常用的按键处理模板,配合 20ms 的扫描周期实现软件消抖。
长按检测
// B3 按下时记录时间
else if(key_down == 3 && ui == recd)
{
long_tick = uwTick;
}
// B3 松开时,如果持续时间 > 1000ms,执行清零
else if(key_up == 3 && ui == recd && (uwTick - long_tick) > 1000)
{
nda = 0; ndb = 0; nha = 0; nhb = 0;
}
为什么在松开时判断?
- 按下记录
long_tick,松开时用uwTick - long_tick计算按压时长 - 只有松开后才执行操作,避免长按过程中重复触发
- 阈值 1000ms(1秒)区分短按和长按
4.5 LCD 显示
三个页面通过 ui 变量切换:
| ui 值 | 页面 | 显示内容 |
|---|---|---|
| 0 (data) | 数据页 | 频率/周期模式切换:A、B 通道数据 |
| 1 (para) | 参数页 | PD(波动阈值)、PH(高频阈值)、PX(频率偏移) |
| 2 (recd) | 记录页 | NDA、NDB(波动次数)、NHA、NHB(越限次数) |
数据页的频率/周期切换:
if(data_mode == 0) // 频率模式
{
if(a_frq <= 1000)
sprintf(lcd_buf, " A=%dHz ", a_frq);
else
sprintf(lcd_buf, " A=%.2fKHz ", a_frq / 1000.0f);
if(a_frq < 0)
sprintf(lcd_buf, " A=NULL ");
}
else // 周期模式
{
a_t = 1e6 / a_frq; // 频率转周期 (μs)
if(a_t <= 1000)
sprintf(lcd_buf, " A=%duS ", a_t);
else
sprintf(lcd_buf, " A=%.2fmS ", a_t / 1000.0f);
}
为什么要做单位自适应? 当数值大于 1000 时自动切换更大的单位(Hz→KHz,μs→ms),提升显示可读性。
4.6 LED 控制
void LED_disp(u8 led)
{
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET); // 锁存器使能
HAL_GPIO_WritePin(GPIOC, 0xFF00, GPIO_PIN_SET); // 先全灭
HAL_GPIO_WritePin(GPIOC, led << 8, GPIO_PIN_RESET); // 点亮目标 LED
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET); // 锁存器关闭
}
为什么要用锁存器?
蓝桥杯嵌入式开发板的 LED 通过 74HC573 锁存器 连接到 PC8~PC15,PD2 控制锁存使能。操作流程:
- 拉高 PD2 打开锁存器
- 先将所有 LED 置高(灭)
- 将目标 LED 对应位置低(亮,低电平点亮)
- 拉低 PD2 锁定状态
LED 位分配:
| 位 | LED | 条件 |
|---|---|---|
| bit0 (0x01) | LED1 | 当前处于 DATA 页面 |
| bit1 (0x02) | LED2 | 通道A频率 > PH |
| bit2 (0x04) | LED3 | 通道B频率 > PH |
| bit7 (0x80) | LED8 | NDA ≥ 3 或 NDB ≥ 3(报警) |
五、参数说明
| 参数 | 默认值 | 范围 | 步长 | 含义 |
|---|---|---|---|---|
| PD | 1000 Hz | 100 ~ 1000 | 100 | 频率波动判定阈值 |
| PH | 5000 Hz | 1000 ~ 10000 | 100 | 频率越限判定阈值 |
| PX | 0 Hz | -1000 ~ 1000 | 100 | 频率修正偏移量 |
六、做题技巧总结
6.1 CubeMX 配置要点
- 系统时钟:HSE → PLL → 80MHz(蓝桥杯 G4 板标准配置)
- 输入捕获定时器:PSC 设为 79 使计数频率为 1MHz,ARR 设为 65535 获取最大量程
- 基本定时器:PSC=79, ARR=999 → 1ms 中断周期,用于周期性任务调度
- GPIO:按键配置为 Input No Pull,LED 引脚 PC8~PC15 + PD2 配置为 Output
6.2 代码编写模板
① 时间片轮询框架(必背)
u32 xxx_tick = 0;
void XXX_proc()
{
if(uwTick - xxx_tick < 周期ms) return;
xxx_tick = uwTick;
// 业务逻辑
}
② 按键扫描三件套(必背)
key_down = key_value & (key_value ^ key_old);
key_up = ~key_value & (key_value ^ key_old);
key_old = key_value;
③ 输入捕获测频率(必背)
// 两次上升沿捕获,差值即周期
// 频率 = 计数频率 / 差值
uwFrequency = 1e6 / uwDiffCapture;
④ LED 锁存器操作(必背)
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOC, 0xFF00, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOC, led << 8, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_2, GPIO_PIN_RESET);
6.3 常见陷阱与注意事项
| 问题 | 解决方案 |
|---|---|
| LCD 刷新闪烁 | 控制刷新周期 100ms,避免频繁全屏清除 |
| 按键误触发 | 20ms 扫描 + 异或边沿检测 |
| 频率为 0 时除法出错 | 加 if(a_frq < 0) 判断,显示 NULL |
| 计数器溢出 | 捕获差值计算时处理翻转情况 |
| LED 与 LCD 共用 GPIO | 每次操作 LED 后及时关闭锁存器(拉低 PD2) |
| 长按与短按冲突 | 按下记时间,松开时判断时长 |
| 页面切换时残留内容 | 切换页面时调用 LCD_Clear(Black) |
6.4 做题顺序建议
1. CubeMX 配置(时钟 + GPIO + 定时器) ────── 约 15 分钟
2. LCD 显示框架(三个页面的静态文字) ────── 约 15 分钟
3. 按键扫描 + 页面切换 ────── 约 10 分钟
4. LED 基本控制 ────── 约 5 分钟
5. 输入捕获测频率 ────── 约 15 分钟
6. 参数调节(PD/PH/PX) ────── 约 10 分钟
7. 频率波动检测 + 越限检测 ────── 约 20 分钟
8. 数据模式切换(频率/周期) ────── 约 5 分钟
9. 长按清零 + LED 报警逻辑 ────── 约 10 分钟
10. 测试调试 ────── 约 15 分钟
原则:先搭框架,再填功能,最后处理细节。 优先拿稳定的基础分(LCD 显示 + 按键 + LED),再攻高分项(频率检测 + 统计逻辑)。
七、完整变量一览
| 变量 | 类型 | 含义 |
|---|---|---|
ui | u8 | 当前页面(0=DATA, 1=PARA, 2=RECD) |
data_mode | _Bool | 数据显示模式(0=频率, 1=周期) |
a_frq / b_frq | s32 | 通道 A/B 的当前频率 |
uwFrequency / uwFrequency2 | u32 | 输入捕获原始频率值 |
pd | u32 | 波动检测阈值(默认 1000) |
ph | u32 | 越限检测阈值(默认 5000) |
px | s32 | 频率偏移校准值(默认 0) |
nda / ndb | u8 | 通道 A/B 波动超标次数 |
nha / nhb | u8 | 通道 A/B 越限次数 |
pha_lock / phb_lock | _Bool | 越限计数锁(防重复计数) |
a_frq_buff / b_frq_buff | s32[30] | 频率采样缓冲区 |
led_num | u8 | LED 状态位图 |
para_line | u8 | 参数页当前选中行 |
八、总结
本题考察的知识点覆盖了蓝桥杯嵌入式竞赛的核心内容:
- 定时器输入捕获:测量外部信号频率
- 基本定时器中断:周期性任务调度
- LCD 多页面显示:UI 状态机
- 按键处理:消抖、短按、长按
- LED 锁存控制:74HC573 操作
- 数据统计与分析:滑动窗口、边沿检测