题目快速浏览
这是一道频率测量与多功能显示的综合题,要实现:
- 外部脉冲频率测量(P3.4引脚输入)
- 3个显示界面切换(频率/周期/电压)
- 双通道电压采集(通道1-光敏电阻,通道3-电位器)
- 数据保存与比较功能
- LED指示灯状态显示
我们按照题目的功能要求,逐个分析怎么实现。
数码管模块
功能概述
数码管模块负责将各种数据(频率、周期、电压等)显示在8位数码管上。核心函数是Seg_Proc(),它根据当前界面模式准备显示缓冲区seg_Buf[],然后由Timer1中断进行动态扫描显示。
显示界面说明
题目要求实现3个显示界面,用S4按键循环切换。
界面0 - 频率界面
显示格式:F + 频率值(F代表Frequency)
比如频率20000Hz时,8位数码管显示:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ F │ │ │ 2 │ 0 │ 0 │ 0 │ 0 │
└───┴───┴───┴───┴───┴───┴───┴───┘
0 1 2 3 4 5 6 7
频率50Hz时:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ F │ │ │ │ │ │ 5 │ 0 │
└───┴───┴───┴───┴───┴───┴───┴───┘
0 1 2 3 4 5 6 7
注意前导0不显示(高位熄灭),不是00050。
界面1 - 周期界面
显示格式:n + 周期值(n代表周期,单位:微秒μs)
比如频率20000Hz时,周期为50μs:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ n │ │ │ │ │ │ 5 │ 0 │
└───┴───┴───┴───┴───┴───┴───┴───┘
0 1 2 3 4 5 6 7
界面2 - 电压界面
显示格式:U-X + 电压值(X代表通道编号,单位:V)
通道1电压3.14V时:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ U │ - │ 1 │ │ 3 │ . │ 1 │ 4 │
└───┴───┴───┴───┴───┴───┴───┴───┘
0 1 2 3 4 5 6 7
通道3电压4.56V时:
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ U │ - │ 3 │ │ 4 │ . │ 5 │ 6 │
└───┴───┴───┴───┴───┴───┴───┴───┘
0 1 2 3 4 5 6 7
Seg_Proc() - 准备显示数据
void Seg_Proc()
{
/* 信息读取区域(分时读取,避免阻塞) */
switch(seg_Scan_Slow)
{
case 30: // 第30ms读取通道1电压
voltage_ch1 = AD_Read(1);
break;
case 60: // 第60ms计算周期
period_value = 1000000 / freq_value; // 周期 = 1000000us / 频率
break;
case 90: // 第90ms读取通道3电压
voltage_ch3 = AD_Read(3);
break;
}
/* 数据显示区域 */
if(seg_Scan_Slow) return;
seg_Scan_Slow = 1;
// 根据界面模式显示不同内容
switch(display_mode)
{
case 0: // 频率显示界面(格式: F XXXXX)
seg_Buf[0] = 11; // F
seg_Buf[1] = seg_Buf[2] = 10; // 熄灭第1-2位
// 显示频率值(5位数字)
seg_Buf[3] = freq_value / 10000 % 10; // 万位
seg_Buf[4] = freq_value / 1000 % 10; // 千位
seg_Buf[5] = freq_value / 100 % 10; // 百位
seg_Buf[6] = freq_value / 10 % 10; // 十位
seg_Buf[7] = freq_value % 10; // 个位
// 消除前导0
for(i = 3; seg_Buf[i] == 0 && i < 7; i++)
seg_Buf[i] = 10;
break;
case 1: // 周期显示界面(格式: n XXXXX)
seg_Buf[0] = 12; // n
seg_Buf[1] = seg_Buf[2] = 10; // 熄灭第1-2位
// 显示周期值(5位数字,单位us)
seg_Buf[3] = period_value / 10000 % 10; // 万位
seg_Buf[4] = period_value / 1000 % 10; // 千位
seg_Buf[5] = period_value / 100 % 10; // 百位
seg_Buf[6] = period_value / 10 % 10; // 十位
seg_Buf[7] = period_value % 10; // 个位
// 消除前导0
for(i = 3; seg_Buf[i] == 0 && i < 7; i++)
seg_Buf[i] = 10;
break;
case 2: // 电压显示界面(格式: U-X X.XX)
seg_Buf[0] = 13; // U
seg_Buf[1] = 14; // -
seg_Buf[2] = channel_select ? 3 : 1; // 通道编号(1或3)
seg_Buf[3] = seg_Buf[4] = 10; // 熄灭第3-4位
// 根据选择的通道显示对应电压值
if(channel_select == 0) // 通道1(光敏电阻)
{
seg_Buf[5] = (unsigned char)voltage_ch1 % 10 + '.'; // 个位+小数点
seg_Buf[6] = (unsigned int)(voltage_ch1 * 10) % 10; // 小数第1位
seg_Buf[7] = (unsigned int)(voltage_ch1 * 100) % 10; // 小数第2位
}
else // 通道3(电位器)
{
seg_Buf[5] = (unsigned char)voltage_ch3 % 10 + '.'; // 个位+小数点
seg_Buf[6] = (unsigned int)(voltage_ch3 * 10) % 10; // 小数第1位
seg_Buf[7] = (unsigned int)(voltage_ch3 * 100) % 10; // 小数第2位
}
break;
}
}
分时读取策略
为了避免AD读取阻塞主程序,采用分时读取策略:
switch(seg_Scan_Slow)
{
case 30: // 第30ms读取通道1电压
voltage_ch1 = AD_Read(1);
break;
case 60: // 第60ms计算周期
period_value = 1000000 / freq_value; // 周期 = 1000000us / 频率
break;
case 90: // 第90ms读取通道3电压
voltage_ch3 = AD_Read(3);
break;
}
这种设计的优势:
- 避免阻塞:AD_Read()函数需要一定时间,分时执行避免阻塞其他功能
- 数据新鲜:100ms内完成所有数据读取,保证显示数据的实时性
- 负载均衡:将计算任务分散在时间轴上
节流控制机制
if(seg_Scan_Slow) return;
seg_Scan_Slow = 1;
每100ms才刷新一次显示数据,有效节省CPU资源,因为:
- 人眼视觉暂存效应,100ms的刷新率足够流畅
- 避免无意义的重复计算
- 为其他任务留出更多CPU时间
按键模块
功能概述
按键模块负责处理用户输入,包括界面切换、通道选择、数据保存和LED开关控制。核心函数是Key_Proc(),它实现了按键消抖、边沿检测和功能映射。
按键相关变量
data unsigned char key_Val; // 当前按键值
data unsigned char key_Down; // 按键按下触发信号
data unsigned char key_Up; // 按键松开触发信号
data unsigned char key_Old; // 上一次按键值
data unsigned char key_Scan_Slow; // 按键扫描减速计数(每10ms扫描)
变量说明:
key_Val:当前按键状态,0表示无按键,4/5/6/7表示S4/S5/S6/S7key_Down:按键按下沿标志,1表示刚按下,用于触发单次动作key_Up:按键释放沿标志,1表示刚释放,用于处理长按逻辑key_Old:上一次按键状态,用于边沿检测key_Scan_Slow:节流控制,每10ms扫描一次按键
按键检测原理
// 按键扫描减速控制
if(key_Scan_Slow) return;
key_Scan_Slow = 1;
// 读取按键并检测按下/松开事件
key_Val = Key_Read();
key_Down = key_Val & (key_Val ^ key_Old); // 按下触发
key_Up = ~key_Val & (key_Val ^ key_Old); // 松开触发
key_Old = key_Val;
边沿检测算法:
按下沿:
key_Down = key_Val & (key_Val ^ key_Old)key_Val ^ key_Old:检测按键状态变化key_Val &变化结果:只保留按下状态(从0变为非0)
释放沿:
key_Up = ~key_Val & (key_Val ^ key_Old)key_Val ^ key_Old:检测按键状态变化~key_Val &变化结果:只保留释放状态(从非0变为0)
Key_Proc() - 按键处理函数
void Key_Proc()
{
// 按键扫描减速控制
if(key_Scan_Slow) return;
key_Scan_Slow = 1;
// 读取按键并检测按下/松开事件
key_Val = Key_Read();
key_Down = key_Val & (key_Val ^ key_Old); // 按下触发
key_Up = ~key_Val & (key_Val ^ key_Old); // 松开触发
key_Old = key_Val;
// 处理按键功能
switch(key_Down)
{
case 4: // S4: 界面切换(频率→周期→电压→频率)
if(++display_mode == 3) display_mode = 0;
// 切换到电压界面时默认显示通道1
if(display_mode == 2)
channel_select = 0;
break;
case 5: // S5: 电压界面通道切换(通道1/通道3)
channel_select ^= 1;
break;
case 6: // S6: 保存当前通道3的电压值
voltage_saved = voltage_ch3;
break;
}
// S7按键松开处理(短按保存频率/长按切换LED显示)
if(key_Up == 7)
{
if(count_1s_key == 1000) // 长按1秒后松开
{
count_1s_key = 0;
led_enable ^= 1; // 切换LED显示开关
}
else // 短按保存频率
{
freq_saved = freq_value;
}
}
}
S4按键 - 界面切换
功能描述:
S4按键实现3个界面的循环切换:频率界面→周期界面→电压界面→频率界面
代码实现:
case 4: // S4: 界面切换(频率→周期→电压→频率)
if(++display_mode == 3) display_mode = 0;
// 切换到电压界面时默认显示通道1
if(display_mode == 2)
channel_select = 0;
break;
切换逻辑:
display_mode取值范围:0(频率)、1(周期)、2(电压)if(++display_mode == 3) display_mode = 0:实现循环切换- 切换到电压界面时,默认选择通道1(光敏电阻)
S5按键 - 通道切换
功能描述:
在电压界面时,S5按键用于切换AD通道:通道1(光敏电阻)↔ 通道3(电位器)
代码实现:
case 5: // S5: 电压界面通道切换(通道1/通道3)
channel_select ^= 1;
break;
切换逻辑:
channel_select:0表示通道1,1表示通道3^= 1:异或操作实现0和1的切换
S6按键 - 电压保存
功能描述:
保存当前通道3的电压值到voltage_saved变量,用于后续比较。
代码实现:
case 6: // S6: 保存当前通道3的电压值
voltage_saved = voltage_ch3;
break;
S7按键 - 双功能操作
功能描述:
- 短按:保存当前频率值到
freq_saved变量 - 长按1秒:切换LED显示开关
代码实现:
// S7按键松开处理(短按保存频率/长按切换LED显示)
if(key_Up == 7)
{
if(count_1s_key == 1000) // 长按1秒后松开
{
count_1s_key = 0;
led_enable ^= 1; // 切换LED显示开关
}
else // 短按保存频率
{
freq_saved = freq_value;
}
}
长按检测机制:
在Timer1中断中维护count_1s_key计数器:
// S7长按计时(达到1秒后保持)
if(key_Old == 7 && ++count_1s_key >= 1000)
count_1s_key = 1000;
- 当S7按下时,
count_1s_key从0开始递增 - 达到1000(1秒)后保持不变
- 松开时判断
count_1s_key == 1000来确定是否长按
LED与电压模块
功能概述
LED模块负责显示系统状态信息,包括电压比较、频率比较和当前界面指示。电压模块负责双通道AD采集,支持光敏电阻和电位器的电压测量。
相关变量
/* 状态标志 */
bit channel_select = 0; // AD通道选择: 0-通道1(光敏), 1-通道3(电位器)
bit led_enable = 1; // LED显示开关: 0-关闭, 1-开启
/* 功能变量 */
float voltage_ch1; // 通道1电压(光敏电阻,单位:V)
float voltage_ch3; // 通道3电压(电位器,单位:V)
float voltage_saved; // 保存的通道3电压值
unsigned int freq_saved; // 保存的频率值
Led_Proc()函数
void Led_Proc()
{
// LED1指示通道3电压是否超过保存值
led_Buf[0] = (voltage_ch3 > voltage_saved);
// LED2指示当前频率是否超过保存值
led_Buf[1] = (freq_value > freq_saved);
// LED3/LED4/LED5指示当前界面
for(i = 0; i < 3; i++)
led_Buf[i + 2] = (i == display_mode);
}
LED功能分配
LED1 - 电压比较指示
led_Buf[0] = (voltage_ch3 > voltage_saved);
- 点亮条件:当前通道3电压 > 保存的电压值
- 熄灭条件:当前通道3电压 ≤ 保存的电压值
- 用途:直观显示电位器电压变化
LED2 - 频率比较指示
led_Buf[1] = (freq_value > freq_saved);
- 点亮条件:当前频率 > 保存的频率值
- 熄灭条件:当前频率 ≤ 保存的频率值
- 用途:显示频率变化趋势
LED3/LED4/LED5 - 界面指示
for(i = 0; i < 3; i++)
led_Buf[i + 2] = (i == display_mode);
界面指示逻辑:
- LED3点亮:
display_mode == 0(频率界面) - LED4点亮:
display_mode == 1(周期界面) - LED5点亮:
display_mode == 2(电压界面)
AD电压采集
分时采集策略
switch(seg_Scan_Slow)
{
case 30: // 第30ms读取通道1电压
voltage_ch1 = AD_Read(1);
break;
case 90: // 第90ms读取通道3电压
voltage_ch3 = AD_Read(3);
break;
}
设计优势:
- 避免阻塞:AD转换需要时间,分时读取不影响其他功能
- 负载均衡:将采集任务分散在时间轴上
- 数据实时性:100ms内完成双通道采集
电压显示格式
// 根据选择的通道显示对应电压值
if(channel_select == 0) // 通道1(光敏电阻)
{
seg_Buf[5] = (unsigned char)voltage_ch1 % 10 + '.'; // 个位+小数点
seg_Buf[6] = (unsigned int)(voltage_ch1 * 10) % 10; // 小数第1位
seg_Buf[7] = (unsigned int)(voltage_ch1 * 100) % 10; // 小数第2位
}
else // 通道3(电位器)
{
seg_Buf[5] = (unsigned char)voltage_ch3 % 10 + '.'; // 个位+小数点
seg_Buf[6] = (unsigned int)(voltage_ch3 * 10) % 10; // 小数第1位
seg_Buf[7] = (unsigned int)(voltage_ch3 * 100) % 10; // 小数第2位
}
显示逻辑:
voltage % 10 + '.':个位数+小数点(通过ASCII码实现)(voltage * 10) % 10:第一位小数(voltage * 100) % 10:第二位小数
示例:电压3.14V
voltage % 10 = 3,3 + '.':显示"3."(3.14 * 10) % 10 = 1:显示"1"(3.14 * 100) % 10 = 4:显示"4"- 最终显示:“3.14”
定时器与中断模块
功能概述
定时器与中断模块是整个系统的核心时基,负责频率测量、系统定时、数码管扫描和LED刷新等功能。本系统使用:
- Timer0:计数器模式,用于外部频率测量
- Timer1:定时器模式,1ms中断,提供系统时基
变量声明
/* 定时变量 */
unsigned int count_1s_freq; // 频率计数1秒定时(每秒读取计数器值)
unsigned int count_1s_key; // 按键长按1秒定时
data unsigned char seg_Pos; // 当前扫描的数码管位置(0-7)
Timer0 - 频率测量计数器
void Timer0_Init(void)
{
AUXR &= 0x7F; // 定时器时钟12T模式
TMOD &= 0xF0; // 设置定时器模式
TMOD |= 0x05; // 设置为计数器模式,不自动重装载
TH0 = TL0 = 0x00; // 计数器初值清零
TF0 = 0; // 清除TF0标志
TR0 = 1; // 计数器0开始计数
}
配置说明:
AUXR &= 0x7F:选择12T时钟模式(传统8051时序)TMOD |= 0x05:设置为16位计数器模式- 模式5:16位计数器,不自动重装载
- T0引脚(P3.4)上的下降沿触发计数
TH0 = TL0 = 0x00:计数器初值清零TR0 = 1:启动计数器
工作原理:
- 外部脉冲信号从P3.4引脚输入
- 每个下降沿使计数器加1
- 16位计数器范围:0-65535
- 每1秒读取计数值,得到频率(Hz)
Timer1 - 系统时基定时器
void Timer1_Init(void) // 1毫秒@12.000MHz
{
AUXR &= 0xBF; // 定时器时钟12T模式
TMOD &= 0x0F; // 设置定时器模式
TL1 = 0x18; // 设置定时初值
TH1 = 0xFC; // 设置定时初值
TF1 = 0; // 清除TF1标志
TR1 = 1; // 定时器1开始计时
EA = 1; // 使能总中断
ET1 = 1; // 使能定时器1中断
}
参数计算:
- 系统时钟:12MHz
- 12T模式:定时器时钟 = 12MHz/12 = 1MHz
- 定时周期:1μs
- 1ms定时需要1000个计数
- 初值 = 65536 - 1000 = 64536 = 0xFC18
TH1 = 0xFC,TL1 = 0x18
Timer1中断服务函数
void Timer1_Server() interrupt 3
{
// 按键扫描减速(每10ms扫描一次)
if(++key_Scan_Slow == 10) key_Scan_Slow = 0;
// 数码管刷新减速(每100ms刷新一次)
if(++seg_Scan_Slow == 100) seg_Scan_Slow = 0;
// 频率测量(每1秒读取一次计数器值)
if(++count_1s_freq == 1000)
{
count_1s_freq = 0;
// 读取计数器值(高8位和低8位组合)
freq_value = (TH0 << 8) | TL0;
// 清零计数器,重新开始计数
TH0 = TL0 = 0x00;
}
// S7长按计时(达到1秒后保持)
if(key_Old == 7 && ++count_1s_key >= 1000)
count_1s_key = 1000;
// 数码管动态扫描(每1ms扫描一位)
if(++seg_Pos == 8) seg_Pos = 0;
// 判断是否显示小数点
if(seg_Buf[seg_Pos] > 20)
Seg_Display(seg_Pos, seg_Buf[seg_Pos] - '.', 1); // 带小数点
else
Seg_Display(seg_Pos, seg_Buf[seg_Pos], 0); // 不带小数点
// 刷新LED显示(根据led_enable开关控制)
Led_Display(led_Buf, led_enable);
}
中断功能详解
1. 节流控制
// 按键扫描减速(每10ms扫描一次)
if(++key_Scan_Slow == 10) key_Scan_Slow = 0;
// 数码管刷新减速(每100ms刷新一次)
if(++seg_Scan_Slow == 100) seg_Scan_Slow = 0;
节流控制原理:
- 1ms中断频率,但不需要每次都执行所有功能
- 按键扫描:每10ms执行一次(100Hz)
- 数码管刷新:每100ms执行一次(10Hz)
- 有效降低CPU负载,提高系统稳定性
2. 频率测量核心逻辑
// 频率测量(每1秒读取一次计数器值)
if(++count_1s_freq == 1000)
{
count_1s_freq = 0;
// 读取计数器值(高8位和低8位组合)
freq_value = (TH0 << 8) | TL0;
// 清零计数器,重新开始计数
TH0 = TL0 = 0x00;
}
测量原理:
- 每1秒读取Timer0的计数值
(TH0 << 8) | TL0:16位数值组合- 计数值就是1秒内的脉冲数,即频率(Hz)
- 读取后清零计数器,开始下1秒的测量
频率范围:
- 16位计数器:0-65535
- 测量范围:0-65535Hz
- 适合中低频信号测量
3. 长按检测
// S7长按计时(达到1秒后保持)
if(key_Old == 7 && ++count_1s_key >= 1000)
count_1s_key = 1000;
检测机制:
- 只有当S7按下(
key_Old == 7)时才计时 count_1s_key从0开始递增- 达到1000(1秒)后保持不变
- 按键松开时在
Key_Proc()中判断长按/短按
4. 数码管动态扫描
// 数码管动态扫描(每1ms扫描一位)
if(++seg_Pos == 8) seg_Pos = 0;
// 判断是否显示小数点
if(seg_Buf[seg_Pos] > 20)
Seg_Display(seg_Pos, seg_Buf[seg_Pos] - '.', 1); // 带小数点
else
Seg_Display(seg_Pos, seg_Buf[seg_Pos], 0); // 不带小数点
扫描原理:
seg_Pos:0-7循环,对应8个数码管位置- 每1ms扫描一位,8ms完成一轮扫描
- 扫描频率:1000ms/8ms = 125Hz(无闪烁)
小数点处理:
seg_Buf[seg_Pos] > 20:表示需要显示小数点seg_Buf[seg_Pos] - '.':减去ASCII小数点值,得到纯数字- 最后参数为1表示点亮小数点
5. LED显示控制
// 刷新LED显示(根据led_enable开关控制)
Led_Display(led_Buf, led_enable);
控制逻辑:
led_Buf[]:LED状态缓冲区led_enable:LED总开关- 实现LED的全局开关控制
中断执行流程总览
Timer1中断每1ms执行一次,按以下顺序处理:
Timer1中断(1ms)
├─ 1. 节流控制
│ ├─ key_Scan_Slow(每10ms触发按键扫描)
│ └─ seg_Scan_Slow(每100ms触发数据刷新)
│
├─ 2. 频率测量(每1秒执行一次)
│ ├─ 读取Timer0计数器值
│ └─ 清零计数器重新开始
│
├─ 3. 长按检测(每1ms检测S7按下状态)
│ └─ count_1s_key递增(最大1000)
│
├─ 4. 数码管扫描(每1ms扫描一位)
│ ├─ seg_Pos循环(0-7)
│ ├─ 小数点判断
│ └─ Seg_Display()输出
│
└─ 5. LED显示(每1ms刷新)
└─ Led_Display()根据开关控制输出
完整代码
以下是经过详细注释的完整 main.c 代码,所有变量、函数和关键逻辑均已标注中文说明:
/* ===================================头文件声明==================================== */
#include <STC15F2K60S2.H> // STC15F2K60S2单片机寄存器定义
#include "Led.h" // LED控制函数(Led_Disp)
#include "Init.h" // 系统初始化函数(System_Init)
#include "Key.h" // 按键读取函数(Key_Read)
#include "Seg.h" // 数码管显示函数(Seg_Disp)
#include "I2C.h" // I2C总线协议,用于AD芯片(AD_Read)
/* ====================================变量定义==================================== */
/* ------------------------- 核心变量(不可更改) ------------------------- */
data unsigned char key_Val; // 当前按键值(Key_Read()返回值,0表示无按键,4/5/6/7表示S4/S5/S6/S7)
data unsigned char key_Down; // 按键按下触发信号(1表示刚按下,用于触发单次动作)
data unsigned char key_Up; // 按键松开触发信号(1表示刚释放,用于处理长按逻辑)
data unsigned char key_Old; // 上一次按键值(用于边沿检测对比)
data unsigned char key_Scan_Slow; // 按键扫描减速计数(每10ms扫描一次,防止按键抖动)
data unsigned char seg_Scan_Slow; // 数码管刷新减速计数(每100ms刷新一次显示数据)
data unsigned char seg_Pos; // 当前扫描的数码管位置(0~7,对应8个数码管从左到右的位置)
data unsigned char seg_Buf[] = {10, 10, 10, 10, 10, 10, 10, 10}; // 数码管显示缓冲区
data unsigned char led_Buf[] = {0, 0, 0, 0, 0, 0, 0, 0}; // LED显示缓冲区
/* ------------------------- 状态标志 ------------------------- */
unsigned char display_mode = 0; // 显示模式: 0-频率界面, 1-周期界面, 2-电压界面
bit channel_select = 0; // AD通道选择: 0-通道1(光敏), 1-通道3(电位器)
bit led_enable = 1; // LED显示开关: 0-关闭, 1-开启
/* ------------------------- 功能变量 ------------------------- */
unsigned char i; // 通用循环变量
unsigned int freq_value; // 测量到的频率(单位:Hz,范围0-65535)
unsigned int period_value; // 测量到的周期(单位:us,通过1e6/频率计算得到)
float voltage_ch1; // 通道1电压(光敏电阻,单位:V,通过AD读取转换得到)
float voltage_ch3; // 通道3电压(电位器,单位:V,通过AD读取转换得到)
float voltage_saved; // 保存的通道3电压值(用于LED1比较显示)
unsigned int freq_saved; // 保存的频率值(用于LED2比较显示)
/* ------------------------- 定时变量 ------------------------- */
unsigned int count_1s_freq; // 频率计数1秒定时(每1000个中断读取一次Timer0计数器)
unsigned int count_1s_key; // 按键长按1秒定时(用于S7长按检测,每1000次中断表示1秒)
/* ====================================任务函数==================================== */
/* ========== 按键处理函数 ========== */
void Key_Proc()
{
// 按键扫描减速控制(每10ms扫描一次按键)
if(key_Scan_Slow) return;
key_Scan_Slow = 1;
// 读取按键并检测按下/松开事件(边沿检测算法)
key_Val = Key_Read(); // 读取当前按键状态
key_Down = key_Val & (key_Val ^ key_Old); // 按下触发:检测从无按键到有按键的变化
key_Up = ~key_Val & (key_Val ^ key_Old); // 松开触发:检测从有按键到无按键的变化
key_Old = key_Val; // 保存当前状态,用于下次边沿检测
// 处理按键按下事件(单次触发)
switch(key_Down)
{
case 4: // S4: 界面切换(频率→周期→电压→频率,循环切换)
if(++display_mode == 3) display_mode = 0; // display_mode在0,1,2之间循环
// 切换到电压界面时默认显示通道1(光敏电阻)
if(display_mode == 2)
channel_select = 0;
break;
case 5: // S5: 电压界面通道切换(通道1/通道3)
channel_select ^= 1; // 异或操作:0变1,1变0,实现通道切换
break;
case 6: // S6: 保存当前通道3的电压值
voltage_saved = voltage_ch3; // 保存电位器当前电压,用于LED1比较
break;
}
// S7按键松开处理(短按保存频率/长按切换LED显示)
if(key_Up == 7) // 检测到S7松开
{
if(count_1s_key == 1000) // 长按1秒后松开
{
count_1s_key = 0; // 清零长按计时器
led_enable ^= 1; // 切换LED显示开关(实现LED全亮/全灭)
}
else // 短按(计时器未达到1秒)
{
freq_saved = freq_value; // 保存当前频率值,用于LED2比较
}
}
}
/* ========== 数码管信息处理函数 ========== */
void Seg_Proc()
{
/* 信息读取区域(分时读取,避免阻塞,提高系统响应性) */
switch(seg_Scan_Slow)
{
case 30: // 第30ms读取通道1电压(光敏电阻)
voltage_ch1 = AD_Read(1); // 读取AD通道1,返回浮点电压值
break;
case 60: // 第60ms计算周期(将频率转换为周期)
period_value = 1000000 / freq_value; // 周期(μs) = 1000000 / 频率(Hz)
break;
case 90: // 第90ms读取通道3电压(电位器)
voltage_ch3 = AD_Read(3); // 读取AD通道3,返回浮点电压值
break;
}
/* 数据显示区域(每100ms刷新一次显示内容) */
if(seg_Scan_Slow) return; // 节流控制:100ms才刷新一次
seg_Scan_Slow = 1;
// 根据当前界面模式显示不同内容
switch(display_mode)
{
case 0: // 频率显示界面(格式: F XXXXX)
seg_Buf[0] = 11; // 显示'F'(代表Frequency)
seg_Buf[1] = seg_Buf[2] = 10; // 熄灭第1-2位(不显示)
// 显示频率值(5位数字,从万位到个位)
seg_Buf[3] = freq_value / 10000 % 10; // 万位
seg_Buf[4] = freq_value / 1000 % 10; // 千位
seg_Buf[5] = freq_value / 100 % 10; // 百位
seg_Buf[6] = freq_value / 10 % 10; // 十位
seg_Buf[7] = freq_value % 10; // 个位
// 消除前导0(从左往右,把前导0改成"不显示")
for(i = 3; seg_Buf[i] == 0 && i < 7; i++)
seg_Buf[i] = 10; // 10表示不显示(熄灭)
break;
case 1: // 周期显示界面(格式: n XXXXX)
seg_Buf[0] = 12; // 显示'n'(代表周期μs)
seg_Buf[1] = seg_Buf[2] = 10; // 熄灭第1-2位(不显示)
// 显示周期值(5位数字,单位μs,从万位到个位)
seg_Buf[3] = period_value / 10000 % 10; // 万位
seg_Buf[4] = period_value / 1000 % 10; // 千位
seg_Buf[5] = period_value / 100 % 10; // 百位
seg_Buf[6] = period_value / 10 % 10; // 十位
seg_Buf[7] = period_value % 10; // 个位
// 消除前导0(从左往右,把前导0改成"不显示")
for(i = 3; seg_Buf[i] == 0 && i < 7; i++)
seg_Buf[i] = 10; // 10表示不显示(熄灭)
break;
case 2: // 电压显示界面(格式: U-X X.XX)
seg_Buf[0] = 13; // 显示'U'(代表Voltage)
seg_Buf[1] = 14; // 显示'-'(分隔符)
seg_Buf[2] = channel_select ? 3 : 1; // 显示通道编号(1或3)
seg_Buf[3] = seg_Buf[4] = 10; // 熄灭第3-4位(不显示)
// 根据选择的通道显示对应电压值(格式:X.XX)
if(channel_select == 0) // 通道1(光敏电阻)
{
seg_Buf[5] = (unsigned char)voltage_ch1 % 10 + '.'; // 个位+小数点
seg_Buf[6] = (unsigned int)(voltage_ch1 * 10) % 10; // 小数第1位
seg_Buf[7] = (unsigned int)(voltage_ch1 * 100) % 10; // 小数第2位
}
else // 通道3(电位器)
{
seg_Buf[5] = (unsigned char)voltage_ch3 % 10 + '.'; // 个位+小数点
seg_Buf[6] = (unsigned int)(voltage_ch3 * 10) % 10; // 小数第1位
seg_Buf[7] = (unsigned int)(voltage_ch3 * 100) % 10; // 小数第2位
}
break;
}
}
/* ========== LED控制函数 ========== */
void Led_Proc()
{
// LED1指示通道3电压是否超过保存值(电位器当前电压 > 保存的电压)
led_Buf[0] = (voltage_ch3 > voltage_saved);
// LED2指示当前频率是否超过保存值(当前频率 > 保存的频率)
led_Buf[1] = (freq_value > freq_saved);
// LED3/LED4/LED5指示当前界面(一个时间只有一个界面,对应LED点亮)
for(i = 0; i < 3; i++)
led_Buf[i + 2] = (i == display_mode); // i=0时LED3对应界面0,以此类推
}
/* ========== 计数器0初始化函数 ========== */
void Timer0_Init(void)
{
AUXR &= 0x7F; // 定时器时钟12T模式(传统8051时序)
TMOD &= 0xF0; // 清除Timer0原有设置
TMOD |= 0x05; // 设置Timer0为16位计数器模式(模式5),不自动重装载
TH0 = TL0 = 0x00; // 计数器初值清零(从0开始计数)
TF0 = 0; // 清除TF0中断标志(本代码未使用中断)
TR0 = 1; // 启动Timer0开始计数(外部脉冲从P3.4输入)
}
/* ========== 定时器1初始化函数 ========== */
void Timer1_Init(void) // 1毫秒@12.000MHz系统时钟
{
AUXR &= 0xBF; // 定时器时钟12T模式(定时器时钟 = 12MHz/12 = 1MHz)
TMOD &= 0x0F; // 清除Timer1原有设置
TMOD |= 0x10; // 设置Timer1为16位定时器模式(模式1)
TL1 = 0x18; // 设置定时初值低8位(65536-1000=64536=0xFC18)
TH1 = 0xFC; // 设置定时初值高8位
TF1 = 0; // 清除TF1中断标志
TR1 = 1; // 启动Timer1开始计时
EA = 1; // 使能总中断(全局中断开关)
ET1 = 1; // 使能Timer1中断(允许Timer1产生中断)
}
/* ========== 定时器1中断服务函数 ========== */
void Timer1_Server() interrupt 3
{
// 按键扫描减速控制(每10ms执行一次按键处理)
if(++key_Scan_Slow == 10) key_Scan_Slow = 0;
// 数码管数据刷新减速控制(每100ms执行一次数据更新)
if(++seg_Scan_Slow == 100) seg_Scan_Slow = 0;
// 频率测量核心逻辑(每1秒读取一次Timer0计数器值)
if(++count_1s_freq == 1000) // 1000次中断 = 1秒
{
count_1s_freq = 0; // 清零1秒定时器,开始下1秒计时
// 读取计数器值(16位数值:高8位左移8位 + 低8位)
freq_value = (TH0 << 8) | TL0;
// 清零计数器,重新开始下1秒的脉冲计数
TH0 = TL0 = 0x00;
}
// S7长按计时检测(达到1秒后保持最大值,用于判断长按)
if(key_Old == 7 && ++count_1s_key >= 1000) // S7按下时计时,达到1000(1秒)后保持
count_1s_key = 1000;
// 数码管动态扫描(每1ms扫描一位,8位数码管轮流点亮)
if(++seg_Pos == 8) seg_Pos = 0; // seg_Pos在0-7之间循环,对应8个数码管位置
// 判断当前位是否需要显示小数点(值>20表示需要小数点)
if(seg_Buf[seg_Pos] > 20)
Seg_Display(seg_Pos, seg_Buf[seg_Pos] - '.', 1); // 显示带小数点的数字
else
Seg_Display(seg_Pos, seg_Buf[seg_Pos], 0); // 显示不带小数点的数字
// LED硬件显示刷新(根据led_enable开关控制LED全亮/全灭)
Led_Display(led_Buf, led_enable);
}
/* ====================================主函数==================================== */
void main(void)
{
// 系统初始化(包括IO口配置、时钟设置等硬件初始化)
System_Init();
AD_Read(1); // 预读取AD通道1(激活AD模块,第一次读取可能无效)
Timer0_Init(); // 初始化Timer0(16位计数器,用于频率测量)
Timer1_Init(); // 初始化Timer1(1ms定时器,提供系统时基)
// 主循环(轮询式处理各模块任务)
while (1)
{
Key_Proc(); // 按键处理(检测按键动作,更新系统状态)
Seg_Proc(); // 数码管处理(准备显示数据,刷新显示内容)
Led_Proc(); // LED处理(更新LED状态,指示系统信息)
}
}
系统特点总结
1. 硬件资源利用
- Timer0:16位计数器,精确测量外部脉冲频率
- Timer1:1ms系统时基,控制所有定时任务
- P3.4引脚:外部脉冲信号输入
- 双通道AD:同时支持光敏电阻和电位器电压采集
2. 软件设计亮点
- 分时处理:AD采集、周期计算分时执行,避免阻塞
- 节流控制:不同功能模块采用不同的执行频率
- 边沿检测:精确的按键按下/释放检测
- 长按识别:通过计时器实现1秒长按检测
3. 用户体验优化
- 多界面显示:频率、周期、电压三种显示模式
- 实时数据:100ms刷新率,保证数据显示的实时性
- 状态指示:LED直观显示系统状态和比较结果
- 操作便捷:S7按键实现短按/长按双功能
4. 工程实践价值
- 模块化设计:每个功能模块职责清晰,便于维护
- 代码规范:详细注释,变量命名规范
- 可扩展性:模块化架构便于添加新功能
- 稳定性:节流控制和分时处理保证系统稳定运行
本方案完整实现了题目要求的所有功能,代码结构清晰,注释详细,具有很强的实用性和学习价值。
以上的代码均由米醋电子工作室提供的模板和相关的考点资料学习而完成的。
如果对单片机感兴趣的,想跟更多大佬一起学习嵌入式的话:
- B站搜索:Alice_西风
- 抖音搜索:米醋电子工作室
浙公网安备 33010602011771号