#include <reg52.h> #include <intrins.h> sfr AUXR = 0x8E; sfr T1MCR = 0xC9; sfr AUXR1 = 0xFF; sbit clk = P1^3; sbit dio = P1^2; sbit P2_4 = P2^4; void Delay_us(unsigned int i) { while(i--) _nop_(); } void I2CStart(void) { clk = 1; dio = 1; Delay_us(2); dio = 0; Delay_us(2); clk = 0; } void I2CAck(void) { clk = 0; Delay_us(2); while(dio); clk = 1; Delay_us(2); clk = 0; } void I2CStop(void) { dio = 0; clk = 1; Delay_us(2); dio = 1; Delay_us(2); } void I2CWrByte(unsigned char oneByte) { unsigned char i; for(i = 0; i < 8; i++) { clk = 0; dio = (oneByte & 0x01) ? 1 : 0; Delay_us(3); oneByte >>= 1; clk = 1; Delay_us(3); } } // ??????(??????) unsigned char code smg_code[] = { 0x3F, // 0 0x06, // 1 0x5B, // 2 0x4F, // 3 0x66, // 4 0x6D, // 5 0x7D, // 6 0x07, // 7 0x7F, // 8 0x6F // 9 }; unsigned long pinlv = 0; unsigned long times = 0; // ????????? unsigned char mode = 0; void key() { static unsigned char key_flag = 0; if(P2_4 == 0) { Delay_us(10000); if(P2_4 == 0 && !key_flag) { mode = (mode + 1) % 3; key_flag = 1; } } else { key_flag = 0; } } void SmgDisplay() { unsigned char digit[4]; unsigned char display_codes[4]; unsigned char has_dot = 0; // ??????? unsigned char i; // ????(???) // ??????????? if (pinlv < 10000) { // ??10kHz:??Hz(??) digit[0] = (pinlv / 1000) % 10; digit[1] = (pinlv / 100) % 10; digit[2] = (pinlv / 10) % 10; digit[3] = pinlv % 10; has_dot = 0; // ???? } else { // ????10kHz:??kHz(?????) unsigned long khz_val = pinlv; // ????????? unsigned int khz = khz_val / 1000; // kHz???? unsigned int fraction = (khz_val % 1000) / 100; // ????(0-9) unsigned int second_frac = (khz_val % 100) / 10; // ?????(??????) digit[0] = khz / 10; // ?? digit[1] = khz % 10; // ?? digit[2] = fraction; // ??? // ?????????? if (second_frac >= 5) { digit[2]++; if (digit[2] > 9) { digit[2] = 0; digit[1]++; if (digit[1] > 9) { digit[1] = 0; digit[0]++; if (digit[0] > 9) { digit[0] = 9; } } } } // ?????????? if (digit[0] > 9) { digit[0] = 9; digit[1] = 9; digit[2] = 9; } digit[3] = 0; // ?????? has_dot = 1; // ????????? } // ??????(?????) display_codes[0] = smg_code[digit[0]]; display_codes[1] = smg_code[digit[1]]; display_codes[2] = smg_code[digit[2]]; display_codes[3] = smg_code[digit[3]]; if (has_dot) { display_codes[1] |= 0x80; // ?????(?????) } // ?????? I2CStart(); I2CWrByte(0x40); // ????? I2CAck(); I2CStop(); I2CStart(); I2CWrByte(0xC0); // ?????? I2CAck(); // ??4????? - ?????? for (i = 0; i < 4; i++) { // ??????i?? I2CWrByte(display_codes[i]); I2CAck(); } I2CStop(); I2CStart(); I2CWrByte(0x8F); // ????+???? I2CAck(); I2CStop(); } void Timer0Init() { AUXR |= 0x80; // T0 1T?? TMOD &= 0xF0; // ??T0?? TMOD |= 0x01; // ??1(16????) TL0 = 0x20; // [email protected] TH0 = 0xD1; TR0 = 1; // ??T0 ET0 = 1; // ??T0?? } void Timer1Init() { TMOD &= 0x0F; // ??T1?? TMOD |= 0x50; // T1??????(??1) T1MCR = 0x03; // ??T1??????? AUXR1 |= 0x40; // T1?????P3.5 TH1 = 0x00; // ????? TL1 = 0x00; TR1 = 1; // ??T1 ET1 = 1; // ??T1?? } void timer0_ISR(void) interrupt 1 { static unsigned int cnt = 0; TL0 = 0x20; // ???? TH0 = 0xD1; if (++cnt >= 1000) { // 1??? cnt = 0; TR1 = 0; // ????? // ?????(????) EA = 0; // ?????????? pinlv = (times * 65536UL) + (TH1 << 8) + TL1; times = 0; // ?????? TH1 = 0; // ????? TL1 = 0; EA = 1; // ??? TR1 = 1; // ????? } } void timer1_ISR(void) interrupt 3 { times++; // ???????+1 } void main(void) { // ????? pinlv = 0; times = 0; mode = 0; // ????? Timer1Init(); Timer0Init(); EA = 1; // ?????? while(1) { key(); // ???? SmgDisplay(); // ???? Delay_us(1000); // ???? } }逐行解释上述代码
时间: 2025-07-04 10:19:34 浏览: 11
<think>我们逐行解释这段代码。这段代码是用C语言为8051单片机(如STC89C52)编写的,主要功能是频率计,通过定时器计数外部脉冲,并在数码管上显示频率值。同时,通过按键切换显示模式(代码中mode变量,但实际按键处理只切换mode,显示部分根据pinlv的值自动选择显示方式)。
注意:代码中使用了两个定时器:T0和T1。T0用于定时(1ms中断,累计1000次即1秒),T1用于计数外部脉冲(通过P3.5引脚输入)。每1秒读取一次T1的计数值(加上中断次数times),得到频率值(单位Hz),然后根据频率值大小决定显示方式(Hz或kHz)。
下面逐行解释:
1. 包含头文件
```c
#include <reg52.h>
#include <intrins.h>
```
reg52.h:8052系列单片机的寄存器定义头文件。
intrins.h:包含一些内部函数,如_nop_()(空操作指令)。
2. 特殊功能寄存器定义
```c
sfr AUXR = 0x8E; // 辅助寄存器
sfr T1MCR = 0xC9; // 定时器1模式控制寄存器(STC特有)
sfr AUXR1 = 0xFF; // 辅助寄存器1
```
这些是STC单片机扩展的寄存器,用于配置定时器的工作模式(1T模式)和引脚映射。
3. 引脚定义
```c
sbit clk = P1^3; // I2C时钟线,用于数码管显示
sbit dio = P1^2; // I2C数据线,用于数码管显示
sbit P2_4 = P2^4; // 按键引脚
```
4. 微秒级延时函数
```c
void Delay_us(unsigned int i) {
while(i--) _nop_();
}
```
通过执行空操作指令进行延时,参数i大致为延时的微秒数(但实际时间与晶振频率有关)。
5. I2C起始信号
```c
void I2CStart(void) {
clk = 1;
dio = 1;
Delay_us(2);
dio = 0;
Delay_us(2);
clk = 0;
}
```
产生I2C起始条件:在时钟线高电平时,数据线产生一个下降沿。
6. I2C应答检查
```c
void I2CAck(void) {
clk = 0;
Delay_us(2);
while(dio); // 等待从机应答(拉低dio)
clk = 1;
Delay_us(2);
clk = 0;
}
```
在发送完一个字节后,检查从机应答(这里等待dio被拉低)。注意:这个函数名是I2CAck,但实际是等待应答,而不是发送应答。
7. I2C停止信号
```c
void I2CStop(void) {
dio = 0;
clk = 1;
Delay_us(2);
dio = 1;
Delay_us(2);
}
```
产生I2C停止条件:在时钟线高电平时,数据线产生一个上升沿。
8. I2C写一个字节
```c
void I2CWrByte(unsigned char oneByte) {
unsigned char i;
for(i = 0; i < 8; i++) {
clk = 0;
dio = (oneByte & 0x01) ? 1 : 0; // 先发送最低位
Delay_us(3);
oneByte >>= 1;
clk = 1;
Delay_us(3);
}
}
```
按照I2C协议,逐位发送一个字节(从最低位开始)。
9. 数码管编码表(共阴数码管)
```c
unsigned char code smg_code[] = {
0x3F, // 0
0x06, // 1
0x5B, // 2
0x4F, // 3
0x66, // 4
0x6D, // 5
0x7D, // 6
0x07, // 7
0x7F, // 8
0x6F // 9
};
```
存储0-9的段码(无小数点)。
10. 全局变量
```c
unsigned long pinlv = 0; // 存储频率值
unsigned long times = 0; // 存储定时器1溢出的次数(用于扩展计数范围)
unsigned char mode = 0; // 显示模式(0,1,2),但代码中只根据频率大小显示,未使用mode
```
11. 按键扫描函数
```c
void key() {
static unsigned char key_flag = 0;
if(P2_4 == 0) { // 按键按下(低电平有效)
Delay_us(10000); // 延时约10ms消抖
if(P2_4 == 0 && !key_flag) {
mode = (mode + 1) % 3; // 切换模式(0->1->2->0)
key_flag = 1; // 防止重复触发
}
} else {
key_flag = 0; // 按键释放,标志清零
}
}
```
检测P2.4引脚的按键,每按一次,mode循环增加(0,1,2)。使用静态变量key_flag进行按键去抖和单次触发。
12. 数码管显示函数
```c
void SmgDisplay() {
unsigned char digit[4]; // 存储4位数字
unsigned char display_codes[4]; // 存储4位数码管的段码
unsigned char has_dot = 0; // 小数点标志
unsigned char i; // 循环变量
// 根据频率值大小决定显示方式
if (pinlv < 10000) { // 小于10kHz,显示单位为Hz(无小数点)
digit[0] = (pinlv / 1000) % 10; // 千位
digit[1] = (pinlv / 100) % 10; // 百位
digit[2] = (pinlv / 10) % 10; // 十位
digit[3] = pinlv % 10; // 个位
has_dot = 0; // 无小数点
}
else { // 大于等于10kHz,显示单位为kHz(带一位小数)
unsigned long khz_val = pinlv; // 保存原始频率值(单位Hz)
unsigned int khz = khz_val / 1000; // 转换为kHz的整数部分
unsigned int fraction = (khz_val % 1000) / 100; // 小数部分的第一位(0-9)
unsigned int second_frac = (khz_val % 100) / 10; // 小数部分的第二位(用于四舍五入)
digit[0] = khz / 10; // 十位(kHz)
digit[1] = khz % 10; // 个位(kHz)
digit[2] = fraction; // 小数部分第一位
// 四舍五入处理:如果第二位小数>=5,则第一位小数进位
if (second_frac >= 5) {
digit[2]++;
if (digit[2] > 9) { // 进位导致小数部分满10
digit[2] = 0;
digit[1]++;
if (digit[1] > 9) {
digit[1] = 0;
digit[0]++;
if (digit[0] > 9) {
digit[0] = 9; // 限制最大值
}
}
}
}
// 处理进位后整数部分超出范围(比如99.9进位后变成100.0,但只显示3位数,所以显示999)
if (digit[0] > 9) {
digit[0] = 9;
digit[1] = 9;
digit[2] = 9;
}
digit[3] = 0; // 最后一位不显示(实际显示3位,但第4位赋0)
has_dot = 1; // 需要显示小数点(在十位和个位之间,即第1位和第2位之间)
}
// 将数字转换为段码
display_codes[0] = smg_code[digit[0]];
display_codes[1] = smg_code[digit[1]];
display_codes[2] = smg_code[digit[2]];
display_codes[3] = smg_code[digit[3]];
// 如果需要显示小数点,则在第二位数码管上加上小数点(段码最高位为1)
if (has_dot) {
display_codes[1] |= 0x80; // 加上小数点(段码最高位置1)
}
// 以下是I2C驱动数码管显示
I2CStart();
I2CWrByte(0x40); // 写入命令:自动地址增加
I2CAck(); // 等待应答
I2CStop();
I2CStart();
I2CWrByte(0xC0); // 写入命令:起始地址为0
I2CAck();
// 写入4个字节的段码数据(从第一个数码管开始)
for (i = 0; i < 4; i++) {
I2CWrByte(display_codes[i]);
I2CAck(); // 每发送一个字节等待应答
}
I2CStop();
// 控制数码管的亮灭和亮度
I2CStart();
I2CWrByte(0x8F); // 命令:开启显示,设置亮度(最高亮度)
I2CAck();
I2CStop();
}
```
13. 定时器0初始化(用于定时1ms)
```c
void Timer0Init() {
AUXR |= 0x80; // T0 1T模式(STC单片机,1个机器周期,比传统12T快)
TMOD &= 0xF0; // 清除T0的设置
TMOD |= 0x01; // 设置T0为模式1(16位定时器)
TL0 = 0x20; // 定时1ms的初值(根据22.1184MHz计算)
TH0 = 0xD1;
TR0 = 1; // 启动T0
ET0 = 1; // 允许T0中断
}
```
14. 定时器1初始化(用于计数外部脉冲)
```c
void Timer1Init() {
TMOD &= 0x0F; // 清除T1的设置
TMOD |= 0x50; // 设置T1为模式1(16位计数器),计数外部引脚(P3.5)的脉冲
T1MCR = 0x03; // 设置T1为16位自动重装模式(STC特有,但这里设置可能无效,因为模式1是16位非自动重装)
AUXR1 |= 0x40; // T1的引脚映射到P3.5(STC单片机)
TH1 = 0x00; // 计数器初值0
TL1 = 0x00;
TR1 = 1; // 启动T1
ET1 = 1; // 允许T1中断
}
```
注意:T1MCR寄存器在STC中用于定时器1的模式控制,但模式1(16位定时器/计数器)没有自动重装功能。这里设置为0x03可能无效,因为模式1不支持自动重装。实际上,T1作为计数器,当计数到0xFFFF后会溢出,然后需要软件重新装载(或清零)并记录溢出次数。
15. 定时器0中断服务函数(1ms中断一次)
```c
void timer0_ISR(void) interrupt 1 {
static unsigned int cnt = 0;
TL0 = 0x20; // 重新装载初值
TH0 = 0xD1;
if (++cnt >= 1000) { // 计数1000次,即1秒
cnt = 0;
TR1 = 0; // 停止T1计数,准备读取计数值
// 读取计数值(注意:在读取过程中禁止中断,防止times变化)
EA = 0; // 关总中断
pinlv = (times * 65536UL) + (TH1 << 8) + TL1; // 计算总脉冲数(频率)
times = 0; // 溢出次数清零
TH1 = 0; // 计数器清零
TL1 = 0;
EA = 1; // 开总中断
TR1 = 1; // 重新启动T1计数
}
}
```
16. 定时器1中断服务函数(计数器溢出)
```c
void timer1_ISR(void) interrupt 3 {
times++; // 溢出次数加1
}
```
17. 主函数
```c
void main(void) {
// 初始化变量
pinlv = 0;
times = 0;
mode = 0;
// 初始化定时器
Timer1Init();
Timer0Init();
EA = 1; // 开总中断
while(1) {
key(); // 按键扫描
SmgDisplay(); // 数码管显示
Delay_us(1000); // 延时约1ms(实际可能不准确,用于控制显示刷新)
}
}
```
总结:
该程序使用定时器T1计数外部脉冲(通过P3.5输入),使用定时器T0定时1秒。每1秒读取T1的计数值(加上溢出次数)得到频率值(单位Hz)。然后根据频率值大小选择显示方式:当频率小于10kHz时,显示4位整数(单位Hz);当频率大于等于10kHz时,转换为kHz并显示3位有效数字(带一位小数,四舍五入)。通过按键可以切换显示模式(虽然代码中定义了mode,但显示部分并未使用mode,而是根据频率值自动选择)。
注意:在显示kHz时,进行了四舍五入处理(取到小数点后第二位,然后四舍五入到第一位)。同时,如果整数部分超过99(即大于99.9kHz),则显示999(即最大显示99.9kHz,超过则显示99.9kHz,但代码中处理为整数部分进位后若超过99,则显示999,实际可以显示到999.9kHz?但代码中只显示3位数,所以整数部分最多两位,小数一位,因此最大显示99.9kHz,超过则显示999,但这样会溢出,所以代码中限制了整数部分最大为9,即最大只能显示9.99kHz?显然这里逻辑有误,需要修正)。
实际上,在kHz显示部分,digit[0]和digit[1]分别代表kHz的十位和个位,所以最大可以显示99.9kHz(即99900Hz)。当频率大于等于100kHz时,整数部分将超过99,代码中进行了处理(digit[0]大于9时,将三个显示位都设为9),所以会显示999(但这样显示不准确)。因此,该程序最大只能测量到99900Hz(99.9kHz),超过则显示999(表示超量程)。
另外,数码管驱动使用了I2C通信,控制的是4位数码管模块(如TM1650)。
相关问题:
§§相关问题§§:
1. 如何修改代码以支持更高频率的测量(例如超过100kHz)?
2. 在数码管显示函数中,四舍五入处理部分是否有更好的方法?
3. 定时器1作为计数器,最大只能计数65535,然后溢出。为什么需要记录溢出次数?如何计算总脉冲数?
4. 如果使用外部中断来计数,与使用定时器计数器模式相比,有什么优缺点?</think>### 逐行解释代码
以下是针对8051单片机(STC89C52)编写的频率计代码的详细解释:
#### 1. 头文件和特殊寄存器定义
```c
#include <reg52.h> // 8051标准寄存器定义
#include <intrins.h> // 提供_nop_()延时函数
sfr AUXR = 0x8E; // 辅助寄存器(用于1T模式设置)
sfr T1MCR = 0xC9; // 定时器1模式控制寄存器
sfr AUXR1 = 0xFF; // 辅助寄存器1(用于引脚重映射)
```
- `reg52.h` 包含8051标准寄存器定义
- `intrins.h` 提供空操作指令 `_nop_()`
- `sfr` 定义特殊功能寄存器地址(STC单片机特有)
#### 2. 引脚定义
```c
sbit clk = P1^3; // I2C时钟线(数码管控制)
sbit dio = P1^2; // I2C数据线(数码管控制)
sbit P2_4 = P2^4; // 按键输入引脚
```
- 定义数码管通信引脚和按键引脚
#### 3. 微秒延时函数
```c
void Delay_us(unsigned int i) {
while(i--) _nop_(); // 每个_nop_()约1us@12MHz
}
```
- 通过空操作实现微秒级延时
- 实际延时时间取决于晶振频率
#### 4. I2C通信协议函数
```c
void I2CStart(void) { /* 产生起始条件 */ }
void I2CAck(void) { /* 等待从机应答 */ }
void I2CStop(void) { /* 产生停止条件 */ }
void I2CWrByte(unsigned char oneByte) { /* 发送单字节 */ }
```
- 实现I2C协议的底层通信函数
- 用于控制数码管显示驱动芯片
#### 5. 数码管段码表
```c
unsigned char code smg_code[] = {
0x3F, // 0 - 共阴数码管段码
0x06, // 1
... // 2-9的段码
};
```
- 存储0-9的7段数码管显示编码
- `code`关键字将数据存储在ROM中
#### 6. 全局变量
```c
unsigned long pinlv = 0; // 测量的频率值
unsigned long times = 0; // 定时器1溢出次数
unsigned char mode = 0; // 显示模式标志
```
- `pinlv`:存储计算的频率值
- `times`:记录定时器1溢出次数
- `mode`:按键切换的显示模式
#### 7. 按键扫描函数
```c
void key() {
static unsigned char key_flag = 0; // 按键状态标志
if(P2_4 == 0) { // 检测按键按下
Delay_us(10000); // 10ms消抖
if(P2_4 == 0 && !key_flag) {
mode = (mode + 1) % 3; // 循环切换0/1/2模式
key_flag = 1; // 防止重复触发
}
} else {
key_flag = 0; // 按键释放
}
}
```
- 实现按键消抖和模式切换
- 每按一次按键,mode在0→1→2→0循环
#### 8. 数码管显示函数
```c
void SmgDisplay() {
// 数据处理部分
if (pinlv < 10000) { /* 显示Hz模式 */ }
else { /* 显示kHz模式(带四舍五入) */ }
// I2C数据传输部分
I2CStart();
I2CWrByte(0x40); // 设置自动地址增加
// ...发送显示数据...
I2CStop();
}
```
- 核心功能:
1. 根据频率值选择显示单位(Hz或kHz)
2. kHz模式下进行四舍五入处理
3. 通过I2C协议驱动数码管
#### 9. 定时器初始化
```c
void Timer0Init() { /* 1ms定时中断设置 */ }
void Timer1Init() { /* 计数器模式设置 */ }
```
- Timer0:1ms定时中断,用于1秒时间基准
- Timer1:计数器模式,用于外部脉冲计数
#### 10. 中断服务函数
```c
void timer0_ISR(void) interrupt 1 { // Timer0中断
static unsigned int cnt = 0;
if (++cnt >= 1000) { // 1秒到达
cnt = 0;
// 读取并计算频率值
pinlv = (times * 65536UL) + (TH1 << 8) + TL1;
// 重置计数器
}
}
void timer1_ISR(void) interrupt 3 { // Timer1中断
times++; // 溢出次数计数
}
```
- Timer0中断:每1秒读取Timer1计数值
- Timer1中断:记录计数器溢出次数
#### 11. 主函数
```c
void main(void) {
// 初始化变量和定时器
Timer1Init(); // 计数器初始化
Timer0Init(); // 定时器初始化
EA = 1; // 开启总中断
while(1) {
key(); // 按键扫描
SmgDisplay(); // 更新显示
Delay_us(1000); // 延时控制刷新率
}
}
```
- 初始化硬件和变量
- 主循环中处理按键和显示刷新
- 通过延时控制显示刷新频率
### 关键功能说明
1. **频率测量原理**:
- Timer1工作在计数器模式(P3.5输入脉冲)
- Timer0每1秒中断一次,读取Timer1计数值
- 频率 = 溢出次数 × 65536 + 当前计数值
2. **显示处理特点**:
- <10kHz:显示Hz(格式:XXXX)
- ≥10kHz:显示kHz带小数点(格式:XX.X)
- 实现四舍五入(精确到0.1kHz)
3. **按键功能**:
- 通过P2.4切换三种显示模式(代码中mode变量)
- 硬件消抖(10ms延时检测)
4. **数码管驱动**:
- I2C协议控制4位数码管
- 动态更新显示内容
### 代码优化建议
1. **显示模式实现**:
```c
// 当前代码中mode未实际用于显示
// 可修改SmgDisplay()开头:
switch(mode) {
case 0: /* 模式0显示 */ break;
case 1: /* 模式1显示 */ break;
case 2: /* 模式2显示 */ break;
}
```
2. **四舍五入简化**:
```c
// 原代码中的四舍五入可简化为:
fraction = (khz_val % 1000 + 50) / 100;
```
3. **量程限制优化**:
```c
// 当前最大显示99.9kHz,可扩展为:
if (khz >= 1000) { /* 显示溢出提示 */ }
```
阅读全文