蓝桥杯单片机复习笔记 - 第六讲:模拟电压采集记录器

一、 功能设计要求 (原题全解)

本试题要求设计一个具备多界面切换、数据格式转换、边缘检测计数及超时报警功能的电压采集系统。以下是基于任务书的详尽功能拆解:

  1. 系统初始化与量程要求
  • 上电状态: 默认处于 电压采集界面 。

  • 采集范围: 0 - 10V 。

  • 默认参数:

  • 电压设置参数 (): 3.0V 。

  • 计数值: 0 。

  1. 数码管显示界面要求

系统共有四个显示界面,需注意不同界面的显示格式与交互细节:

  • 界面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. 逻辑控制功能
  • 计数触发条件 (下降沿检测):
  • 当满足以下顺序条件时,计数值加1:
  1. 前一次实际电压 () 大于 电压设置参数 () 。

  2. 重新输入后,实际电压 () 小于 电压设置参数 () 。

  3. 按键功能逻辑

  • S1-S10 (数字键): 对应 0-9 输入。仅在 电压采集界面 有效 。

  • S11 (采集/确认):

  • 功能A (非采集界面): 按下后进入 电压采集界面,可重新采集一次电压值 。

  • 功能B (采集界面): 保存当前输入电压值,并返回 电压显示界面 。

  • 异常处理: 若输入数据不符合规范性,则 清空数据 重新输入 。

  • S12 (界面切换):

  • 有效性: 仅在 非电压采集界面 有效 。

  • 功能: 在 数据显示 参数设置 计数统计 三个界面中循环切换 。

  • S14 (清零/清除):

  • 在电压采集界面: 清除当前输入数据 。

  • 在计数统计界面: 清除当前计数值 。

  • S15 (参数+):

  • 有效性: 仅在 电压参数设置界面 有效 。

  • 功能: 电压参数 +0.5V。若达到上限 6V 后,回到下限 1V 。

  • S16 (参数-):

  • 有效性: 仅在 电压参数设置界面 有效 。

  • 功能: 电压参数 -0.5V。若达到下限 1V 后,回到上限 6V 。

  1. 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)

定时器中断负责系统的“心跳”,处理所有非阻塞计时任务。

逻辑分析

  1. 扫描驱动: 保证数码管动态扫描的刷新率(通常 >50Hz)。
  2. 异步计时:
  • 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; // 状态中断,立即复位,重新计时
    }
}


六、 关键技术点总结

  1. 输入缓冲与数值分离:
    在人机交互设计中,输入过程通常处理的是字符流(Array of Digits),而业务逻辑处理的是数值(Integer)。代码中通过 Seg_Collect_Vol 数组和 Vol_Collect_New 变量清晰地隔离了这两个过程。
  2. 边沿检测 (Edge Detection):
    在离散系统中,检测信号跳变的唯一方法是对比 和 时刻的状态。本题的下降沿逻辑 Old > Set && New < Set 是数字信号处理中最基础的算法体现。
  3. 非阻塞计时 (Non-blocking Timing):
    在单片机裸机编程中,严禁在主循环使用长延时。利用定时器中断累加计数器,并在主循环查询计数器值,是实现多任务并发的前提。

希望大家多多订阅我的专栏,点点关注!

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐