蓝桥杯单片机备赛指南 - 第六讲:模拟电压采集记录器
蓝桥杯单片机复习笔记 - 第六讲:模拟电压采集记录器
一、 功能设计要求 (原题全解)
本试题要求设计一个具备多界面切换、数据格式转换、边缘检测计数及超时报警功能的电压采集系统。以下是基于任务书的详尽功能拆解:
- 系统初始化与量程要求
-
上电状态: 默认处于 电压采集界面 。
-
采集范围: 0 - 10V 。
-
默认参数:
-
电压设置参数 (): 3.0V 。
-
计数值: 0 。
- 数码管显示界面要求
系统共有四个显示界面,需注意不同界面的显示格式与交互细节:
-
界面1:电压采集界面 (默认)
-
显示内容: 当前输入的电压数据 (4位)。
-
格式:
-
提示符:熄灭 。
-
数据单位:mV (毫伏) 。
-
交互特效: 当前输入的数码管单元需以 0.5秒/次 的频率闪烁提示 。
-
数据转换: 输入为mV (如2588mV),实际对应电压需保留两位有效数字 (2.58V) 。
-
界面2:数据显示界面
-
显示内容: 提示符
U+ 当前实际电压数据。 -
格式:
-
第一位:显示
U。 -
后续位:显示实际电压 (V),保留两位小数 (如
2.58) 。 -
界面3:参数设置界面
-
显示内容: 提示符
P+ 当前设置电压参数。 -
格式:
-
第一位:显示
P。 -
后续位:显示电压参数 (如
3.00) 。 -
界面4:计数统计界面
-
显示内容: 提示符
N+ 当前计数值。 -
格式:
-
第一位:显示
N。 -
后续位:显示计数值 (如
12) 。 -
消隐要求: 数据的高位未启用时,数码管需 熄灭 (即不显示前导0) 。
- 逻辑控制功能
- 计数触发条件 (下降沿检测):
- 当满足以下顺序条件时,计数值加1:
-
前一次实际电压 () 大于 电压设置参数 () 。
-
重新输入后,实际电压 () 小于 电压设置参数 () 。
-
按键功能逻辑
-
S1-S10 (数字键): 对应 0-9 输入。仅在 电压采集界面 有效 。
-
S11 (采集/确认):
-
功能A (非采集界面): 按下后进入 电压采集界面,可重新采集一次电压值 。
-
功能B (采集界面): 保存当前输入电压值,并返回 电压显示界面 。
-
异常处理: 若输入数据不符合规范性,则 清空数据 重新输入 。
-
S12 (界面切换):
-
有效性: 仅在 非电压采集界面 有效 。
-
功能: 在 数据显示 参数设置 计数统计 三个界面中循环切换 。
-
S14 (清零/清除):
-
在电压采集界面: 清除当前输入数据 。
-
在计数统计界面: 清除当前计数值 。
-
S15 (参数+):
-
有效性: 仅在 电压参数设置界面 有效 。
-
功能: 电压参数 +0.5V。若达到上限 6V 后,回到下限 1V 。
-
S16 (参数-):
-
有效性: 仅在 电压参数设置界面 有效 。
-
功能: 电压参数 -0.5V。若达到下限 1V 后,回到上限 6V 。
- LED 指示灯功能
-
L1 (超时/长按报警):
-
当
实际电压 < 电压设置参数的状态 持续超过五秒 后,L1点亮,否则熄灭 。 -
L2 (奇偶指示):
-
当当前计数值为 奇数 时,L2点亮,否则熄灭 。
-
L3 (错误报警):
-
当 连续三次以上 无效按键操作时,L3点亮 。
-
当触发 有效按键操作 时熄灭 。
二、 系统架构与变量设计 (main.c)
1. 变量定义的工程逻辑
变量不仅仅是存储数据的容器,更是状态机的控制信号。本系统采用了输入缓冲与数值计算分离的策略。
#include <REGX52.H>
#include "Key.h"
#include "Seg.h"
#include "LED.h"
// === 类型定义 ===
typedef unsigned char u8;
typedef unsigned int u16;
typedef bit b1;
// === 时间片轮询与减速变量 ===
// 用于在定时器中断与主循环之间协调执行频率
u8 Key_Slow_Down=0;
u8 Seg_Slow_Down=0;
// === 按键底层状态 ===
u8 Key_Val=0, Key_Down=0, Key_Old=0; // 经典的边沿检测变量组
// === 显示驱动变量 ===
u8 Seg_Pos=0; // 扫描位选索引
u8 Seg_Buf[6]={10,10,11,11,11,11}; // 显存数组 (10:熄灭, 11:无内容)
u8 Seg_Dot[6]={0,0,0,0,0,0}; // 小数点掩码
u8 Seg_Mode=0; // 状态机核心: 0-采集 1-显示 2-参数 3-计数
b1 Flash_Flag=0; // 闪烁时钟信号 (250ms翻转)
// === 业务逻辑核心变量 ===
// 输入缓冲区: 暂存用户输入的离散数字 (0-9),避免直接操作整数导致的计算复杂化
// 初始化为10/11是为了界面初始化时的视觉效果
u8 Seg_Collect_Vol[6]={10,10,11,11,11,11};
u8 Seg_Collect_Vol_Index=2; // 输入游标,指向当前操作的缓冲区索引
u16 Set_Vol=3000; // 阈值电压 (单位统一为mV,避免浮点运算)
u16 count=0; // 事件计数器
b1 count_flag=1; // 计数互锁标志,防止单次事件重复计数
// 边沿检测专用变量: 维护“前一时刻”与“当前时刻”的电压值
u16 Vol_Collect_Old=0, Vol_Collect_New=0;
// 计时器变量 (由定时器驱动)
u8 Time_250ms=0; // 闪烁周期计数
u16 Time_5000ms=0; // 长按/持续时间计数
u8 error_count=0; // 错误操作计数
三、 逻辑控制层实现 (Key_Proc)
Key_Proc 承载了系统的主要业务逻辑,特别是 S11 的复用逻辑和下降沿的算法实现。
1. 状态分流与输入处理
首先进行按键的消抖与边沿提取,随后根据 Seg_Mode 将按键事件分流为“数据输入”或“指令控制”。
void Key_Proc(){
u8 i=0;
// 1. 时间片控制: 只有当定时器使得 Key_Slow_Down 计数达标时才执行
if(Key_Slow_Down) return;
Key_Slow_Down=1; // 自锁,等待定时器解锁
// 2. 状态获取: 位运算提取下降沿 (按下瞬间为1)
Key_Val=Key_Read();
Key_Down=Key_Val&(Key_Val^Key_Old);
Key_Old=Key_Val;
// 3. 业务分流
// 仅在采集模式(Mode 0)下,将S1-S10映射为数字输入
if(Key_Down<=10 && Key_Down>=1 && Seg_Mode==0){
// 将按键索引转换为数字值存入缓冲区
Seg_Collect_Vol[Seg_Collect_Vol_Index] = Key_Down-1;
// 游标自增,指向下一位
// 注意:此处未做边界检查,边界检查逻辑下放至 S11 确认键中统一处理
Seg_Collect_Vol_Index++;
}
else{
// 功能键处理
switch(Key_Down){
// (代码接下文...)
2. S11: 核心事务处理 (状态切换/数据合成/边沿触发)
S11 的逻辑最为复杂,需要处理两种上下文(进入采集 vs 确认保存)。
逻辑分析:
- 上下文切换: 依靠
Seg_Mode判断当前意图。若非Mode 0,则重置环境进入Mode 0。 - 数据合成: 将
Seg_Collect_Vol数组中的离散数字,按权值(1000, 100, 10, 1)累加合成为u16整数。 - 边沿触发算法:
该算法确保了只有在电压穿越阈值且方向向下时,才触发计数。
// S11: 多功能复用键
case 11:
if(Seg_Mode){
// === 场景 A: 非采集模式 -> 进入采集模式 ===
Seg_Mode=0;
count_flag=1; // 激活计数使能,允许新周期的检测
// 重置输入缓冲区 (置为11/无显示状态)
for(i=2;i<6;i++) Seg_Collect_Vol[i]=11;
Seg_Collect_Vol_Index=2; // 游标复位
}
else{
// === 场景 B: 采集模式 -> 确认保存 ===
// 校验: 游标必须>=6,意味着下标2,3,4,5均已填充数据
if(Seg_Collect_Vol_Index>=6){
// 1. 更新历史状态
Vol_Collect_Old=Vol_Collect_New;
// 2. 数据合成 (Array -> Integer)
Vol_Collect_New = Seg_Collect_Vol[2]*1000 +
Seg_Collect_Vol[3]*100 +
Seg_Collect_Vol[4]*10 +
Seg_Collect_Vol[5];
// 3. 状态机跳转
Seg_Mode=1;
Seg_Collect_Vol_Index=2;
// 4. 下降沿判定逻辑
// 必须同时满足: 计数锁开启 && 旧值>阈值 && 新值<阈值
if(count_flag==1 && Vol_Collect_Old>Set_Vol && Vol_Collect_New<Set_Vol){
count++;
count_flag=0; // 互锁,防止同一次低电平状态下重复计数
}
error_count=0; // 操作合法,清零错误计数
}
else{
// 异常处理: 位数不足
// 清空缓冲区要求重输
for(i=2;i<6;i++) Seg_Collect_Vol[i]=11;
Seg_Collect_Vol_Index=2;
error_count++; // 累积错误次数 (触发L3)
}
}
break;
// S12: 环形状态切换
case 12:
if(Seg_Mode){ // 互斥保护: 采集模式下S12无效
if(++Seg_Mode==4) Seg_Mode=1; // 1->2->3->1
}
break;
// S14: 清除指令
case 14:
// 清空当前输入
for(i=2;i<6;i++) Seg_Collect_Vol[i]=11;
Seg_Collect_Vol_Index=2;
// 注: 若题目要求 Mode 3 下清除计数值,需在此补充 if(Seg_Mode==3) count=0;
break;
// S15/S16: 参数调节 (带边界回环)
case 15:
if(Seg_Mode==2){
Set_Vol+=500;
if(Set_Vol>6000) Set_Vol=1000; // 上限回环
}
break;
case 16:
if(Seg_Mode==2){
Set_Vol-=500;
if(Set_Vol<1000) Set_Vol=6000; // 下限回环
}
break;
}
}
}
四、 视图层实现 (Seg_Proc)
视图层负责将内存数据渲染到数码管,并处理光标闪烁和高位消隐等视觉特效。
1. 显存映射与特效处理
- 闪烁算法: 利用逻辑与操作
Index==i && Flash_Flag,当条件满足时强制写入10(熄灭码),否则写入真实数据。 - 定点数显示: 电压以 mV 存储,显示时通过
Seg_Dot强制点亮第3位小数点,视觉上转换为 V。
void Seg_Proc(){
u8 i=0;
// 显示刷新减速
if(Seg_Slow_Down) return;
Seg_Slow_Down=1;
switch(Seg_Mode){
case 0: // === 采集模式 (含动态光标) ===
Seg_Dot[3]=0;
Seg_Buf[0]=10; Seg_Buf[1]=10; // 提示符区熄灭
for(i=2;i<6;i++){
// 动态光标逻辑:
// 若当前位是待输入位(Index),且时钟信号(Flash_Flag)为高 -> 熄灭
Seg_Buf[i]=(Seg_Collect_Vol_Index==i && Flash_Flag)?10:Seg_Collect_Vol[i];
}
break;
case 1: // === 显示模式 (定点数) ===
Seg_Dot[3]=1; // 物理点亮小数点
Seg_Buf[0]=12; // 'U'
// 数据映射: 直接取数组值
Seg_Buf[3]=Seg_Collect_Vol[2];
Seg_Buf[4]=Seg_Collect_Vol[3];
Seg_Buf[5]=Seg_Collect_Vol[4];
// 注: 根据题目要求保留两位小数,此处截断了最后一位
break;
case 3: // === 计数模式 (高位消隐) ===
Seg_Dot[3]=0;
Seg_Buf[0]=14; // 'N'
// 数值分解
Seg_Buf[1]=count/10000%10;
Seg_Buf[2]=count/1000%10;
Seg_Buf[3]=count/100%10;
Seg_Buf[4]=count/10%10;
Seg_Buf[5]=count/1%10;
// 消隐算法:
// 从最高位(Index 1)开始扫描,若为0则熄灭,直到遇到非0数或抵达个位
i=1;
while(Seg_Buf[i]==0){
Seg_Buf[i]=10;
if(++i==5) break; // 保护位: 确保个位为0时也能显示,而不是全黑
}
break;
// Case 2 参数设置逻辑同 Case 1,略
}
}
五、 定时器与后台任务 (Timer0_Routine)
定时器中断负责系统的“心跳”,处理所有非阻塞计时任务。
逻辑分析
- 扫描驱动: 保证数码管动态扫描的刷新率(通常 >50Hz)。
- 异步计时:
Flash_Flag: 提供 2Hz 的方波信号。Time_5000ms: 实现条件积分。当条件满足时积分,条件破坏时复位(积分清零)。这是实现“连续监测”的标准方法。
void Timer0_Routine() interrupt 1 {
// 1. 重装载 (1ms)
TL0 = 0xCD;
TH0 = 0xD4;
// 2. 更新减速计数器 (为上层逻辑提供时间基准)
if(++Key_Slow_Down==10) Key_Slow_Down=0;
if(++Seg_Slow_Down==200) Seg_Slow_Down=0;
// 3. 数码管位选扫描
if(++Seg_Pos==6) Seg_Pos=0;
Seg_Disp(Seg_Pos,Seg_Buf[Seg_Pos],Seg_Dot[Seg_Pos]);
// 4. 生成闪烁时钟 (250ms 半周期 -> 500ms 周期)
if(++Time_250ms==250){
Time_250ms=0;
Flash_Flag^=1; // 信号翻转
}
// 5. 持续时间监测 (核心逻辑)
// 只有在当前电压持续低于阈值时,计数器才累加
if(Vol_Collect_New < Set_Vol){
Time_5000ms++;
}else{
Time_5000ms=0; // 状态中断,立即复位,重新计时
}
}
六、 关键技术点总结
- 输入缓冲与数值分离:
在人机交互设计中,输入过程通常处理的是字符流(Array of Digits),而业务逻辑处理的是数值(Integer)。代码中通过Seg_Collect_Vol数组和Vol_Collect_New变量清晰地隔离了这两个过程。 - 边沿检测 (Edge Detection):
在离散系统中,检测信号跳变的唯一方法是对比 和 时刻的状态。本题的下降沿逻辑Old > Set && New < Set是数字信号处理中最基础的算法体现。 - 非阻塞计时 (Non-blocking Timing):
在单片机裸机编程中,严禁在主循环使用长延时。利用定时器中断累加计数器,并在主循环查询计数器值,是实现多任务并发的前提。
希望大家多多订阅我的专栏,点点关注!
更多推荐
所有评论(0)