




一、先确定一组便于验证的成绩
我建议故意设置一组平均值不是整数的数据,例如:
text
80, 81, 82, 83, 84, 85, 86, 87,
88, 89, 90, 91, 92, 93, 94, 95,
80, 81, 82, 83, 84, 85, 86, 87,
88, 89, 90, 91, 92, 93, 94, 95
32 人总成绩为:
text
2800
所以:
text
2800 / 32 = 87.5
这样特别适合实验,因为最后应该得到:
text
avr = 87.5
片内 RAM:
50H = 08H
51H = 07H
52H = 05H
注意题目说的是未压缩 BCD 码。
87 的十位是 8、个位是 7,所以应该:
text
50H = 08H
51H = 07H
而不是把 87 写成一个字节 87H。87H 才是压缩 BCD。
二、推荐的 Keil C51 程序
如果目的是完成这个实验,我建议使用下面这种写法:
c
#include <REG52.H>
#include <absacc.h>
#define SCORE_ADDR 0x1000
unsigned char xdata *score;
unsigned int sum;
float avr;
unsigned char data bcd_tens _at_ 0x50;
unsigned char data bcd_ones _at_ 0x51;
unsigned char data bcd_decimal _at_ 0x52;
unsigned char code score_init[32] = {
80, 81, 82, 83, 84, 85, 86, 87,
88, 89, 90, 91, 92, 93, 94, 95,
80, 81, 82, 83, 84, 85, 86, 87,
88, 89, 90, 91, 92, 93, 94, 95
};
void main(void)
{
unsigned char i;
unsigned int avr10;
unsigned char integer_part;
score = (unsigned char xdata *)SCORE_ADDR;
/* 将32名学生成绩写入片外RAM */
for (i = 0; i < 32; i++)
{
score[i] = score_init[i];
}
/* 计算32人成绩总和 */
sum = 0;
for (i = 0; i < 32; i++)
{
sum += score[i];
}
/*
* 平均成绩扩大10倍进行整数运算。
* 加16相当于除以32之前进行四舍五入。
*/
avr10 = (sum * 10UL + 16) / 32;
/* 保存平均成绩,保留一位小数 */
avr = avr10 / 10.0;
/* 分离整数部分 */
integer_part = avr10 / 10;
/* 转换为未压缩BCD码 */
bcd_tens = integer_part / 10;
bcd_ones = integer_part % 10;
bcd_decimal = avr10 % 10;
while (1);
}
这份代码有意没有为了"代码短"而把所有东西塞到一起,因为实验需要调试 。sum、avr10、integer_part 都保留下来,你在 Keil Watch 窗口里可以清楚地看到计算的每一个阶段。
三、这段程序真正考的东西
首先是:
c
unsigned char xdata *score;
xdata 指明 score 指向 外部数据存储器 XDATA。
然后:
c
score = (unsigned char xdata *)0x1000;
于是:
c
score[0]
score[1]
...
score[31]
实际上对应:
text
XDATA:1000H
XDATA:1001H
...
XDATA:101FH
8051 最终访问这些数据时会使用 MOVX 一类指令。
而:
c
unsigned char data bcd_tens _at_ 0x50;
则完全不同,它告诉 Keil:
在片内 DATA 空间的 50H 地址放置这个变量。
因此三个变量:
text
bcd_tens → DATA:50H
bcd_ones → DATA:51H
bcd_decimal → DATA:52H
这就把题目中的"片外 RAM"和"片内 RAM"真正体现出来了。
四、为什么我没有直接写 avr = sum / 32.0
当然可以写:
c
avr = sum / 32.0;
然后:
c
integer_part = (unsigned char)avr;
但是这样处理"一位小数"的时候会遇到浮点误差和舍入问题。
实验中更漂亮的做法是先计算:
c
avr10 = (sum * 10UL + 16) / 32;
这里 avr10 表示:
平均成绩 × 10
例如:
text
sum = 2800
avr10
= (2800 × 10 + 16) / 32
= 28016 / 32
= 875
于是:
text
avr10 = 875
↓
87.5
因此:
c
avr = avr10 / 10.0;
得到:
text
avr = 87.5
同时 BCD 转换就非常简单:
c
integer_part = avr10 / 10; // 87
bcd_tens = integer_part / 10; // 8
bcd_ones = integer_part % 10; // 7
bcd_decimal = avr10 % 10; // 5
最终:
text
DATA:50H = 08H
DATA:51H = 07H
DATA:52H = 05H
这套算法非常适合画流程图。
五、流程图应该怎么画
你的模板特别强调"按照流程图规范画流程图,与程序一致"。
所以不要画成一个很抽象的:
建议你手画成:
text
┌──────────────┐
│ 开始 │
└──────┬───────┘
↓
┌────────────────────┐
│ score指向XDATA 1000H│
│ i = 0 │
└─────────┬──────────┘
↓
┌─────────┐
┌───→│ i < 32? │
│ └────┬────┘
│ 是│ │否
│ ↓ │
│ ┌───────────────┐
│ │写score[i]到XDATA│
│ │ i++ │
│ └───────┬───────┘
└──────────┘
│
↓
┌──────────┐
│sum=0,i=0 │
└────┬─────┘
↓
┌─────────┐
┌───→│ i < 32? │
│ └────┬────┘
│ 是│ │否
│ ↓ │
│ ┌────────────────┐
│ │sum += score[i] │
│ │ i++ │
│ └───────┬────────┘
└─────────┘
│
↓
┌─────────────────────┐
│avr10=(sum*10+16)/32 │
└──────────┬──────────┘
↓
┌─────────────────────┐
│ avr = avr10 / 10.0 │
│整数部分=avr10 / 10 │
└──────────┬──────────┘
↓
┌─────────────────────┐
│50H = 整数部分 / 10 │
│51H = 整数部分 % 10 │
│52H = avr10 % 10 │
└──────────┬──────────┘
↓
┌────────────┐
│ 无限循环 │
└────────────┘
六、Keil 5 怎么调试这个实验
这部分反而是这次实验很重要的地方。你的报告模板第 7 页明确要求写"调试的过程,中间和最终的结果查看,并分析其正确性"。
因此不要只是点 Run 然后说"运行成功"。
第一个断点:检查外部 RAM
在:
c
sum = 0;
这一行打断点。
也就是说,先让:
c
for (i = 0; i < 32; i++)
{
score[i] = score_init[i];
}
完整执行。
进入:
Debug → Start/Stop Debug Session
运行到断点。
然后:
View → Memory Windows → Memory 1
输入:
text
X:0x1000
Keil 的 Memory Window 中,X: 表示 XDATA。
你应该能看到:
text
50 51 52 53 54 55 56 57
58 59 5A 5B 5C 5D 5E 5F
50 51 52 53 54 55 56 57
58 59 5A 5B 5C 5D 5E 5F
注意这是十六进制显示:
text
50H = 80
51H = 81
...
5FH = 95

这一步证明:
32 个学生成绩确实被存入了片外 RAM,而不是普通变量里。
第二个断点:检查求和
在:
c
avr10 = (sum * 10UL + 16) / 32;
这里打断点。
运行到这里。
打开:
View → Watch Windows → Watch 1
添加:
text
sum
i
此时应该:
text
sum = 2800
i = 32
这证明遍历 XDATA 求和正确。
第三个断点:检查平均值
在:
c
while (1);
这里打断点。
Watch 中加入:
text
sum
avr10
avr
integer_part
bcd_tens
bcd_ones
bcd_decimal
理论结果:
text
sum = 2800
avr10 = 875
avr = 87.5
integer_part = 87
bcd_tens = 8
bcd_ones = 7
bcd_decimal = 5

这样你的程序计算过程已经全部验证完了。
七、最后检查片内 RAM 50H~52H
再开一个 Memory Window,输入:
text
D:0x50
这里:
text
D:
表示 8051 DATA 空间。
应该看到:
text
地址 内容
50H 08H
51H 07H
52H 05H

于是可以形成一个非常漂亮的实验验证链:
text
XDATA 1000H~101FH
↓
32人成绩正确
↓
sum = 2800
↓
avr10 = 875
↓
avr = 87.5
↓
DATA 50H = 08H
DATA 51H = 07H
DATA 52H = 05H
八、思考题 1:C51 对绝对地址的访问
C51 中常见的绝对地址访问方法主要可以从两类来理解。
第一种是使用 _at_:
c
unsigned char data a _at_ 0x50;
unsigned char xdata b _at_ 0x1000;
通过不同存储类型决定访问的地址空间,例如:
text
data → 片内直接寻址RAM
idata → 片内间接寻址RAM
xdata → 外部数据存储器
code → 程序存储器
第二种是 Keil 提供的绝对地址访问宏,在:
c
#include <absacc.h>
中提供,例如:
c
DBYTE[0x50]
XBYTE[0x1000]
其中:
text
DBYTE → DATA空间
XBYTE → XDATA空间
CBYTE → CODE空间
例如本实验完全可以把:
c
score = (unsigned char xdata *)0x1000;
score[0] = 80;
改成:
c
XBYTE[0x1000] = 80;
而:
c
bcd_tens = 8;
也可以直接写:
c
DBYTE[0x50] = 8;
这也解释了为什么这个实验要你同时操作"片外 RAM"和"片内 RAM":它实际上就是让你理解 8051 并不是一个简单的统一地址空间。
九、思考题 2:如果成绩本身就是压缩 BCD
这个问题也非常值得注意。
假设成绩:
text
87分
现在外部 RAM 存放的不再是普通二进制:
text
0101 0111 = 57H = 十进制87
而是压缩 BCD:
text
1000 0111 = 87H
^^^^ ^^^^
十位 个位
8 7
所以不能直接把 XDATA 中的值拿来相加。
首先需要 BCD → 二进制:
c
unsigned char bcd_to_bin(unsigned char bcd)
{
return ((bcd >> 4) * 10) + (bcd & 0x0F);
}
求和部分修改为:
c
sum = 0;
for (i = 0; i < 32; i++)
{
sum += bcd_to_bin(score[i]);
}
其他平均值计算和结果转换部分:
c
avr10 = (sum * 10UL + 16) / 32;
avr = avr10 / 10.0;
integer_part = avr10 / 10;
bcd_tens = integer_part / 10;
bcd_ones = integer_part % 10;
bcd_decimal = avr10 % 10;
都可以继续使用。
整个思考题的核心其实就是:
text
XDATA读取压缩BCD成绩
↓
取高4位 → 十位
取低4位 → 个位
↓
二进制成绩 = 十位×10 + 个位
↓
32人成绩累加
↓
计算平均成绩×10
↓
整数、小数分离
↓
转换成题目要求的未压缩BCD
↓
写入50H、51H、52H
keil5中怎么一步步操作
可以。你这个实验最好从新建 Keil 5 工程 → 写代码 → 编译 → 进入软件仿真 → 下断点 → 查看片外 RAM → 单步观察求和 → 查看 avr → 查看片内 50H~52H完整走一遍。
1. 新建 Keil 5 工程
打开 Keil μVision5。
依次选择:
text
Project
→ New μVision Project...
例如建立文件夹:
text
D:\MCU\Average\
工程名:
text
Average
保存后 Keil 会让你选择芯片。
搜索:
text
AT89C52
通常选择:
text
Atmel
→ AT89C52
然后确定。
如果出现:
text
Copy STARTUP.A51 to Project Folder and Add File to Project?
对于这个实验选择:
text
Yes
即可。
2. 创建 C51 源程序
点击:
text
File
→ New
把下面程序粘进去:
c
#include <REG52.H>
#define SCORE_ADDR 0x1000
unsigned char xdata *score;
unsigned int sum;
float avr;
unsigned char data bcd_tens _at_ 0x50;
unsigned char data bcd_ones _at_ 0x51;
unsigned char data bcd_decimal _at_ 0x52;
unsigned char code score_init[32] = {
80, 81, 82, 83, 84, 85, 86, 87,
88, 89, 90, 91, 92, 93, 94, 95,
80, 81, 82, 83, 84, 85, 86, 87,
88, 89, 90, 91, 92, 93, 94, 95
};
void main(void)
{
unsigned char i;
unsigned int avr10;
unsigned char integer_part;
score = (unsigned char xdata *)SCORE_ADDR;
/* 将32名学生成绩写入片外RAM */
for (i = 0; i < 32; i++)
{
score[i] = score_init[i];
}
/* 计算总成绩 */
sum = 0;
for (i = 0; i < 32; i++)
{
sum += score[i];
}
/* 计算平均成绩,保留一位小数 */
avr10 = (sum * 10UL + 16) / 32;
avr = avr10 / 10.0;
/* 分离平均成绩整数部分 */
integer_part = avr10 / 10;
/* 转换成未压缩BCD码 */
bcd_tens = integer_part / 10;
bcd_ones = integer_part % 10;
bcd_decimal = avr10 % 10;
while (1);
}
然后:
text
File
→ Save As...
保存为:
text
main.c
一定要有 .c 后缀。
3. 把 main.c 加入工程
左边应该能看到:
text
Project
└─ Target 1
└─ Source Group 1
右键:
text
Source Group 1
选择:
text
Add Existing Files to Group 'Source Group 1'...
找到:
text
main.c
点击:
text
Add
然后 Close。
最后左边应该类似:
text
Target 1
├─ Source Group 1
│ └─ main.c
└─ STARTUP.A51
4. 先编译
点击工具栏的:
text
Build
或者直接按:
text
F7
下面的 Build Output 应该出现类似:
text
compiling main.c...
linking...
Program Size: ...
"Average" - 0 Error(s), 0 Warning(s).
关键是:
text
0 Error(s)
有 warning 不一定意味着程序不能运行,但这个实验最好也做到 0 Warning(s)。
5. 设置 Keil 软件仿真
这个实验写的是"计算机(仿真软件)",所以我们不需要真的接一片 AT89C52 和外部 SRAM。
点击:
text
Project
→ Options for Target 'Target 1'...
或者点击工具栏那个魔术棒图标。
找到:
text
Debug
你会看到类似:
text
○ Use Simulator
○ Use: ...
选择:
text
● Use Simulator
然后:
text
OK
这一点很重要。
我们后面查看:
text
XDATA
DATA
变量
全部使用 Keil 自带的 8051 Simulator。
6. 进入 Debug
点击:
text
Debug
→ Start/Stop Debug Session
快捷键通常是:
text
Ctrl + F5
第一次可能弹警告,正常进入即可。
进入之后你会发现 Keil 界面发生变化。
左边可能出现:
text
Registers
R0
R1
...
SP
DPTR
ACC
...
程序当前应该停在启动代码或者 main() 附近。
7. 第一个实验:观察成绩有没有写进片外 RAM
我们先不要直接 Run。
找到这一段:
c
for (i = 0; i < 32; i++)
{
score[i] = score_init[i];
}
sum = 0;
在:
c
sum = 0;
这一行左边的灰色区域双击。
应该出现一个红色圆点:
🔴
这就是断点。
相当于告诉 CPU:
执行到
sum = 0的时候给我停下来。
然后点击:
text
Run
或者:
text
F5
程序会快速执行:
c
score = (unsigned char xdata *)SCORE_ADDR;
for (...)
{
score[i] = score_init[i];
}
然后停在:
c
sum = 0;
8. 查看 XDATA 1000H
现在我们验证片外 RAM。
点击:
text
View
→ Memory Windows
→ Memory 1
下面会出现一个 Memory 窗口。
在地址框输入:
text
X:0x1000
回车。
这里非常关键:
text
X:
表示:
XDATA,也就是外部数据存储空间。
现在应该看到类似:
text
X:1000 50 51 52 53 54 55 56 57
X:1008 58 59 5A 5B 5C 5D 5E 5F
X:1010 50 51 52 53 54 55 56 57
X:1018 58 59 5A 5B 5C 5D 5E 5F
别看到:
text
50 51 52...
就以为程序错了。
Memory Window 默认显示的是十六进制。
例如:
text
十进制成绩 RAM中的十六进制
80 → 50H
81 → 51H
82 → 52H
...
90 → 5AH
...
95 → 5FH
因此:
text
50 51 52 ... 5F
恰恰证明程序正确。
这张图建议截图。
它可以直接作为实验报告中的第一个调试结果:
通过 Keil Memory Window 查看 XDATA 1000H~101FH,32 名学生成绩已正确存入片外 RAM。
9. 第二个断点:观察求和
现在找到:
c
avr10 = (sum * 10UL + 16) / 32;
在这一行下断点。
然后再次:
text
F5
程序就会执行:
c
sum = 0;
for (i = 0; i < 32; i++)
{
sum += score[i];
}
最后停在 avr10 = ...。
现在我们要看 sum。
10. 打开 Watch 窗口
点击:
text
View
→ Watch Windows
→ Watch 1
下面会出现:
text
Watch 1
双击空白位置。
输入:
text
sum
再输入:
text
i
应该看到:
text
Expression Value
sum 2800
i 32
因为:
text
80 + 81 + ... + 95
+
80 + 81 + ... + 95
= 2800
所以:
text
sum = 2800
正确。
第二张截图可以在这里截。
11. 用单步运行观察平均值计算
现在不要按 F5。
我们开始真正体验一下 Debug。
当前停在:
c
avr10 = (sum * 10UL + 16) / 32;
Watch 加入:
text
avr10
avr
integer_part
然后按:
text
F10
也就是:
text
Step
执行这一行。
理论上:
text
avr10 = 875
因为:
text
2800 × 10 = 28000
(28000 + 16) / 32
= 28016 / 32
= 875
Watch 应该看到:
text
avr10 = 875
再按一次:
text
F10
执行:
c
avr = avr10 / 10.0;
这时候:
text
avr = 87.5
这就是题目要求的:
平均成绩保留十分之一位,存至变量
avr中。
12. 继续单步观察 BCD 转换
继续:
text
F10
执行:
c
integer_part = avr10 / 10;
得到:
text
integer_part = 87
继续执行:
c
bcd_tens = integer_part / 10;
得到:
text
bcd_tens = 8
然后:
c
bcd_ones = integer_part % 10;
得到:
text
bcd_ones = 7
最后:
c
bcd_decimal = avr10 % 10;
得到:
text
bcd_decimal = 5
Watch 中最后应该是:
text
sum 2800
avr10 875
avr 87.5
integer_part 87
bcd_tens 8
bcd_ones 7
bcd_decimal 5
这时候你就已经通过单步调试亲眼看到了:
text
2800
↓
875
↓
87.5
↓
87 + 0.5
↓
8 7 5
13. 最重要的一步:查看片内 RAM 50H~52H
题目不仅要求变量正确,还明确要求:
整数部分以未压缩 BCD 码存放在片内 RAM 50H、51H,小数部分存放在 52H。
所以还必须验证物理地址。
重新:
text
View
→ Memory Windows
→ Memory 2
输入:
text
D:0x50
注意这次不是:
text
X:
而是:
text
D:
因为现在看的是 DATA,即片内 RAM。
执行完 BCD 转换以后,你应该看到开头:
text
D:0050 08 07 05 ...
也就是:
text
地址 内容
50H → 08H
51H → 07H
52H → 05H
这三个字节就是:
text
8 7 . 5
↑ ↑ ↑
50H 51H 52H
所以最终表达的是:
text
87.5
而且:
text
08H
07H
05H
每个字节只保存一个十进制数字,因此符合未压缩 BCD的要求。
这张图强烈建议截图,这是实验最核心的结果之一。
14. 你最后应该得到的完整调试结果
完成以后,你可以对照下面这个表检查:
| 检查对象 | Keil 中查看方法 | 正确结果 |
|---|---|---|
| 32 个学生成绩 | Memory → X:0x1000 |
50 51 ... 5F 重复两次 |
| 总成绩 | Watch sum |
2800 |
| 扩大 10 倍平均值 | Watch avr10 |
875 |
| 平均成绩 | Watch avr |
87.5 |
| 整数部分 | Watch integer_part |
87 |
| 十位 BCD | Watch bcd_tens |
8 |
| 个位 BCD | Watch bcd_ones |
7 |
| 小数位 BCD | Watch bcd_decimal |
5 |
| 片内 RAM | Memory → D:0x50 |
08 07 05 |
这样整个实验就验证闭环了。
15. 顺便试一下真正的"单步看循环"
老师如果要求你展示会使用 Keil 调试,可以再做一个非常直观的操作。
重新启动 Debug 后,在:
c
sum += score[i];
这里下断点。
Watch 添加:
text
i
score[i]
sum
第一次停下来可能看到:
text
i = 0
score[i] = 80
sum = 0
按 F10:
text
sum = 80
循环到下一次:
text
i = 1
score[i] = 81
sum = 80
再执行:
text
sum = 161
于是你可以亲眼观察:
text
i=0 sum=80
i=1 sum=161
i=2 sum=243
...
i=31 ...
最终 sum=2800
这比直接看到最终 2800 更能说明你确实会调试程序。
你的实验报告模板实际上也正需要这样的内容:实验分析部分要求写调试过程、中间和最终结果以及正确性分析。
你真正做实验的时候可以就按这个顺序来:
text
新建 AT89C52 工程
↓
创建 main.c
↓
F7 编译
↓
Debug 设置 Use Simulator
↓
Ctrl+F5 进入调试
↓
断点①
查看 X:0x1000
↓
断点②
Watch → sum = 2800
↓
F10 单步
avr10 = 875
avr = 87.5
↓
继续 F10
BCD = 8、7、5
↓
查看 D:0x50
08 07 05
↓
实验完成
其中建议至少保留 XDATA 成绩、Watch 最终计算结果、DATA 50H~52H 这三张截图,后面写实验报告第 7 页"实验分析"时正好能用。
对,就是这里。你现在需要在 Keil 5 里把调试器设置成软件仿真器(Simulator),因为这个实验没有连接实际 AT89C52 开发板。
具体操作:
① 点击工具栏的"魔术棒"图标,它实际上是:
Options for Target...
也可以从菜单进入:
Project → Options for Target 'Target 1'...
② 在弹出的窗口顶部选择 Debug 选项卡。
你应该能看到两组选择,大致是:
text
Debug
○ Use Simulator
○ Use: [某种硬件调试器]
选择:
text
● Use Simulator
然后点击:
text
OK
③ 接下来按 Ctrl + F5,或者:
Debug → Start/Stop Debug Session
进入调试模式。
如果成功,你会发现 Keil 界面发生变化,左侧通常会出现 **Registers(寄存器)**窗口,工具栏上的 Run、Step 等调试按钮也会启用。
然后我们先不要急着跑完整个程序。下一步就是在 sum = 0; 这一行下断点,然后运行到这里,再打开 Memory Window 看 X:0x1000。

这里的数字为什么和你的不一样
这个其实是对的,而且数据完全正确 😂。和我之前展示的不一样,只是因为 Keil Memory 窗口每行显示的字节数不同 。
看你截图的第一行:
text
X:0x001000: 50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 50 51 52 53 54
第二行从:
text
X:0x001015: 55 56 57 58 59 5A 5B 5C 5D 5E 5F 00 00 ...
开始。
为什么第二行是 1015H?
因为你的 Keil 窗口一行显示了 21 个字节。
第一行从:
text
1000H
开始,一共有 21 个地址:
text
1000H ~ 1014H
所以下一行自然从:
text
1015H
开始。
而我之前为了方便讲解,人为排成了每行 8 字节:
text
1000: 50 51 52 53 54 55 56 57
1008: 58 59 5A 5B 5C 5D 5E 5F
1010: 50 51 52 53 54 55 56 57
1018: 58 59 5A 5B 5C 5D 5E 5F
内存里的内容实际上完全一样。
结果已经正确
32 个成绩实际上是:
text
地址 HEX 十进制
1000H 50H 80
1001H 51H 81
1002H 52H 82
...
100FH 5FH 95
1010H 50H 80
1011H 51H 81
...
101FH 5FH 95
也就是我们设置的:
text
80 81 82 ... 95
80 81 82 ... 95
在 101FH 后面,你看到:
text
00 00 00 00...
也完全正常,因为我们的数组只有 32 个元素:
text
1000H ~ 101FH → 32 字节成绩
1020H以后 → 没有写入,所以保持00
它证明了程序已经成功把 32 名学生的成绩写到了 AT89C52 的 XDATA 片外 RAM 1000H~101FH。
另外,地址栏里写的是:
text
X:0x1000
这也是正确的。X: 表示 XDATA 空间。
接下来已经完成了第一个调试验证。现在先别退出 Debug。
在代码:
c
avr10 = (sum * 10UL + 16) / 32;
这一行下断点,然后按 F5。
程序停下来后,我们下一步看:
text
sum
是不是:
text
2800

这一步也完全正确 。只是 Keil 的 Watch 窗口现在把数值按十六进制显示了。
现在看到的是:
text
sum = 0x0AF0
i = 0x20
换成十进制:
text
0x0AF0 = 2800
0x20 = 32
所以恰好验证了我们预期的结果:
text
32 名学生全部遍历完成 → i = 32
32 人总成绩 → sum = 2800
怎么让 Keil 直接显示十进制?
在 Watch 窗口中的 sum 上右键,找类似:
text
Hexadecimal Display
把十六进制显示取消掉。不同 Keil 版本菜单文字可能略有差异。
取消以后应该显示:
text
sum 2800
i 32
不过实验报告里保留你现在这个截图也没问题,只要在实验分析中说明:
程序完成32名学生成绩累加后,通过 Watch 窗口观察到
i=0x20、sum=0x0AF0。其中0x20=32,说明32名学生成绩均已完成遍历;0x0AF0=2800,与人工计算的总成绩2800一致,说明累加程序运行正确。
这张截图正好可以作为报告要求的"中间结果查看及正确性分析"。
接下来不要按 F5,开始单步
现在把下面几个变量加到 Watch 1:
text
avr10
avr
integer_part
bcd_tens
bcd_ones
bcd_decimal
当前程序应该停在:
c
avr10 = (sum * 10UL + 16) / 32;
按一次 F10。
然后重点看 avr10。
它应该变成:
text
avr10 = 0x036B
因为:
text
0x036B = 875
再按一次 F10,执行:
c
avr = avr10 / 10.0;
这时候 avr 应该得到 87.5。

这里为什么是87W
这是 Keil Watch 窗口对 unsigned char 的显示方式,不是你的程序出了问题。
截图里:
text
i 87 'W' uchar
integer_part 87 'W' uchar
其中:
87:这个变量的数值'W':数值 87 对应的 ASCII 字符
因为十进制:
text
87 = 0x57
而 ASCII 编码中:
text
0x57 = 'W'
所以 Keil 对 unsigned char(也就是 uchar)会很贴心地同时告诉你:
text
87 'W'
│ │
│ └─ 按 ASCII 字符解释 → 'W'
└───── 按数值解释 → 87
为什么 sum 和 avr10 没有 'W'?
text
sum 2800 uint
avr10 875 uint
i 87 'W' uchar
integer_part 87 'W' uchar
bcd_tens 8 uchar
sum 和 avr10 是:
c
unsigned int
而 i 和 integer_part 是:
c
unsigned char
Keil 对 char 类型会额外显示它对应的字符,所以才出现 'W'。
不过这里有个更有意思的问题:为什么 i 也是 87?
这个也正常,但原因和 'W' 不一样。
我们之前循环:
c
for (i = 0; i < 32; i++)
{
sum += score[i];
}
结束以后理论上:
text
i = 32
现在这张截图非常适合作为最终计算结果:
text
sum = 2800
avr10 = 875
avr = 87.5
integer_part = 87
bcd_tens = 8
bcd_ones = 7
bcd_decimal = 5
其中 87 'W' 只需要读作 87 ,'W' 是 Keil 顺带显示的 ASCII 解释,与我们的平均成绩计算没有关系。
现在其实只剩最后一个关键验证了:打开 Memory 2,输入:
text
D:0x50
看看 50H、51H、52H 是否为:
text
08 07 05
如果是,那么这个实验的所有核心结果就全部验证完成了。
为什么要扩大十倍再缩小十倍
因为题目要求平均成绩保留一位小数 ,而我们又希望主要使用整数运算 ,所以把"小数点临时向右移动一位"。
例如最终平均分是:
87.5 87.5 87.5
把它扩大 10 倍:
87.5×10=875 87.5\times10=875 87.5×10=875
这样 875 就是一个整数,可以方便地保存和拆分。
在程序里:
c
avr10 = (sum * 10UL + 16) / 32;
已知:
text
sum = 2800
那么不考虑四舍五入项时,本质就是:
2800×1032=875 \frac{2800\times10}{32}=875 322800×10=875
所以:
text
avr10 = 875
这里可以把 avr10 理解成:
以 0.1 分为单位表示的平均成绩。
也就是说:
text
avr10 = 875
↓
87.5 分
最后题目又明确要求平均成绩存入变量 avr,所以再:
c
avr = avr10 / 10.0;
恢复小数点:
text
875 / 10.0 = 87.5
得到:
text
avr = 87.5
更重要的是,avr10 = 875 对后面的 BCD 拆分特别方便:
c
integer_part = avr10 / 10; // 87
bcd_tens = integer_part / 10; // 8
bcd_ones = integer_part % 10; // 7
bcd_decimal = avr10 % 10; // 5
于是直接得到:
text
875
││└── 个位 5 → 小数第一位
│└─── 十位 7 → 成绩个位
└──── 百位 8 → 成绩十位
→ 87.5
那为什么不直接 avr = sum / 32.0?
其实完全可以。
最直观的写法就是:
c
avr = (float)sum / 32;
对于你现在的数据:
text
2800 / 32 = 87.5
没有任何问题。
之所以前面的程序用了"×10 再 ÷10",主要是为了两个目的:
- 明确控制保留一位小数和四舍五入;
- 方便后面直接提取十分位并转换成 BCD。
比如平均值如果是 87.56,题目要求一位小数,我们希望得到:
text
87.6
就可以先得到:
text
876
再把它解释成 87.6。
所以可以把这个技巧记成:
需要精确处理固定的小数位时,可以先把小数放大成整数处理,最后再缩小回来。
这其实就是一种很常见的定点数思想。在单片机里尤其常见,因为很多嵌入式场景并不一定需要或者适合全程使用浮点运算。
至于我们代码里的 +16:
c
avr10 = (sum * 10UL + 16) / 32;
↑
这个又是另一个知识点------它是为了实现四舍五入 。如果想弄懂,可以接着用 87.53、87.56 这样的例子解释为什么除以 32 前加的是 16。
完整解读程序
c
#include <REG52.H>
#define SCORE_ADDR 0x1000
unsigned char xdata *score;
unsigned int sum;
float avr;
unsigned char data bcd_tens _at_ 0x50;
unsigned char data bcd_ones _at_ 0x51;
unsigned char data bcd_decimal _at_ 0x52;
unsigned char code score_init[32] = {
80, 81, 82, 83, 84, 85, 86, 87,
88, 89, 90, 91, 92, 93, 94, 95,
80, 81, 82, 83, 84, 85, 86, 87,
88, 89, 90, 91, 92, 93, 94, 95
};
void main(void)
{
unsigned char i;
unsigned int avr10;
unsigned char integer_part;
score = (unsigned char xdata *)SCORE_ADDR;
/* 将32名学生成绩写入片外RAM */
for (i = 0; i < 32; i++)
{
score[i] = score_init[i];
}
/* 计算总成绩 */
sum = 0;
for (i = 0; i < 32; i++)
{
sum += score[i];
}
/* 计算平均成绩,保留一位小数 */
avr10 = (sum * 10UL + 16) / 32;
avr = avr10 / 10.0;
/* 分离平均成绩整数部分 */
integer_part = avr10 / 10;
/* 转换成未压缩BCD码 */
bcd_tens = integer_part / 10;
bcd_ones = integer_part % 10;
bcd_decimal = avr10 % 10;
while (1);
}
1. 整个程序到底在完成什么
text
片外 RAM 中有32名学生成绩
↓
把32个成绩取出来
↓
全部相加
↓
除以32
↓
得到平均成绩,例如87.5
↓
avr = 87.5
↓
把87.5拆成:
8、7、5
↓
分别写入片内RAM:
50H = 08H
51H = 07H
52H = 05H
所以这个程序实际上有两个非常重要的存储区域:
text
AT89C52
┌───────────────────┐
│ CPU │
│ │
│ 读取 → 计算 → 写入│
└────↑─────────↓────┘
│ │
│ │
片外RAM 片内RAM
XDATA DATA
│ │
1000H~101FH 50H~52H
│ │
32名学生成绩 8 7 5
这也是为什么实验目的第一条就是:
掌握 MCS-51 单片机的存储器结构。
2. #include <REG52.H> 是什么
第一行:
c
#include <REG52.H>
这是 Keil C51 提供的 AT89C52/8052 系列寄存器定义头文件。
里面定义了大量特殊功能寄存器,例如类似:
c
sfr P0 = 0x80;
sfr SP = 0x81;
sfr DPL = 0x82;
sfr DPH = 0x83;
sfr P1 = 0x90;
...
因此如果以后写:
c
P1 = 0x55;
C51 编译器才知道 P1 指的是 8051 的 P1 特殊功能寄存器。
本实验实际上没有直接操作这些 SFR。
所以从纯 C 语言角度,本程序对 REG52.H 的依赖并不强。但单片机实验通常会保留:
c
#include <REG52.H>
后面的实验控制 LED、定时器、串口、中断时就会大量使用它。
3. #define SCORE_ADDR 0x1000
c
#define SCORE_ADDR 0x1000
这是一个宏定义。
告诉预处理器:
程序中凡是出现
SCORE_ADDR,都可以理解成0x1000。
所以:
c
score = (unsigned char xdata *)SCORE_ADDR;
实际上相当于:
c
score = (unsigned char xdata *)0x1000;
为什么不直接把 0x1000 写死?
因为这样:
c
#define SCORE_ADDR 0x1000
非常清楚地告诉阅读代码的人:
1000H 是成绩存储区的起始地址。
假设以后老师要求改成 2000H,也只需要:
c
#define SCORE_ADDR 0x2000
不用满程序找 0x1000。
这就是减少"魔法数字/硬编码"的一种最基本方法。
4. unsigned char xdata *score 是本实验的重点
c
unsigned char xdata *score;
普通 C 语言里你可能写:
c
unsigned char *score;
但是 C51 多了:
text
xdata
因为 8051 不是简单的统一内存地址空间。
C51 中存在不同的存储空间,例如:
text
data
idata
xdata
code
这里:
c
unsigned char xdata *score;
可以拆成:
text
unsigned char xdata *score
↑ ↑ ↑
数据类型 存储空间 指针
意思就是:
score是一个指针,它指向 XDATA 空间中的unsigned char数据。
而 XDATA 就是:
External Data Memory,外部数据存储器。
5. 为什么成绩使用 unsigned char
成绩:
text
< 100
也就是说范围:
text
0 ~ 99
而:
c
unsigned char
能够表示:
text
0 ~ 255
所以一个字节完全够了。
例如:
text
80 十进制 = 50H
95 十进制 = 5FH
每个学生只占:
text
1 Byte
32 个学生就是:
text
32 Byte
因此从:
text
1000H
开始存 32 个字节,最后一个地址就是:
text
101FH
因为:
text
1000H + 31 = 101FH
这就是你刚才在 Keil Memory Window 看到的:
text
XDATA 1000H ~ 101FH
6. unsigned int sum
c
unsigned int sum;
这个变量用于保存32 名学生的总成绩。
为什么不能也用:
c
unsigned char sum;
因为 unsigned char 最大只有:
text
255
而 32 个学生每个人最高 99 分:
32×99=3168 32\times99=3168 32×99=3168
明显:
text
3168 > 255
所以一个字节装不下。
在 Keil C51 中,unsigned int 是 16 位的,可以表示:
text
0 ~ 65535
因此:
text
3168 < 65535
完全足够。
你刚才调试得到:
text
sum = 2800
也说明必须使用超过 8 位的数据类型。
7. float avr
c
float avr;
这个变量就是题目明确要求的:
平均成绩保留十分之一位,存至变量
avr中。
因为平均成绩可能出现:
text
87.5
这种带小数的数据,所以使用:
c
float
最后你在 Keil Watch 中已经看到:
text
avr = 87.5
Type = float
8. 这三句话非常重要
c
unsigned char data bcd_tens _at_ 0x50;
unsigned char data bcd_ones _at_ 0x51;
unsigned char data bcd_decimal _at_ 0x52;
这就是在完成题目的:
平均成绩的整数部分以未压缩的 BCD 码形式存放在片内 RAM 50H 和 51H 中,小数部分存放在 52H 中。
先看:
c
unsigned char data bcd_tens _at_ 0x50;
拆开:
text
unsigned char data bcd_tens _at_ 0x50
↑ ↑ ↑ ↑
数据类型 DATA空间 变量名 绝对地址
意思就是:
创建一个
unsigned char变量bcd_tens,把它放在片内 DATA 空间的 50H 地址。
于是:
c
bcd_tens = 8;
最终就是:
text
片内 RAM 50H ← 08H
同理:
c
bcd_ones = 7;
就是:
text
片内 RAM 51H ← 07H
而:
c
bcd_decimal = 5;
就是:
text
片内 RAM 52H ← 05H
9. _at_ 是 C51 的扩展语法
注意:
c
_at_
不是标准 C 语言。
这是 Keil C51 提供的扩展关键字。
普通 PC 上的 GCC/MSVC 不会这么写。
它的用途就是:
把变量安排到指定的绝对地址。
所以:
c
unsigned char data a _at_ 0x50;
就是:
text
变量a
↓
DATA空间
↓
绝对地址50H
这也正好对应你实验的思考题:
C51 中,对绝对地址的访问有几种方法,访问空间有什么区别。
10. score_init[32] 是什么
接下来:
c
unsigned char code score_init[32] = {
80, 81, 82, 83, 84, 85, 86, 87,
88, 89, 90, 91, 92, 93, 94, 95,
80, 81, 82, 83, 84, 85, 86, 87,
88, 89, 90, 91, 92, 93, 94, 95
};
这里定义了一组我们人为设置的成绩。
特别注意:
c
code
又是一个 C51 存储类型。
它表示:
这些数据存放在 CODE 程序存储空间中。
也就是:
text
score_init
↓
CODE
程序存储器
为什么这么设计?
因为 Keil Simulator 一开始:
text
XDATA 1000H~101FH
里面没有我们想要的成绩。
所以为了模拟:
"片外 RAM 已经有 32 名学生成绩"
我们先准备:
c
score_init[]
然后程序启动以后把它们复制到 XDATA。
11. main() 开始
c
void main(void)
{
就是程序的主函数。
AT89C52 完成启动初始化以后,最终进入这里。
里面首先定义三个局部变量:
c
unsigned char i;
unsigned int avr10;
unsigned char integer_part;
12. i 是循环计数器
c
unsigned char i;
后面:
c
for (i = 0; i < 32; i++)
需要用它记录:
text
第0个
第1个
第2个
...
第31个
为什么是:
text
0 ~ 31
而不是:
text
1 ~ 32
因为 C 数组下标从 0 开始。
32 个元素:
text
score[0]
score[1]
...
score[31]
总共正好 32 个。
13. avr10 是什么意思
c
unsigned int avr10;
这个名字可以理解为:
text
average × 10
也就是:
平均成绩扩大十倍后的整数。
例如:
text
平均成绩 = 87.5
那么:
text
avr10 = 875
我们上一轮已经讨论过,主要目的是方便:
- 保留一位小数;
- 四舍五入;
- 提取小数位;
- 尽量使用整数运算。
14. integer_part
c
unsigned char integer_part;
表示平均成绩的整数部分。
例如:
text
avr = 87.5
那么:
text
integer_part = 87
后面再把 87 拆成:
text
8
7
15. 最关键的指针赋值
c
score = (unsigned char xdata *)SCORE_ADDR;
由于:
c
#define SCORE_ADDR 0x1000
所以实际上是:
c
score = (unsigned char xdata *)0x1000;
意思就是:
让
score指向 XDATA 空间的 1000H 地址。
于是:
text
score
↓
XDATA:1000H
那么:
c
score[0]
就是:
text
XDATA:1000H
score[1]:
text
XDATA:1001H
score[2]:
text
XDATA:1002H
......
score[31]:
text
XDATA:101FH
因此:
text
score[i]
本质上就是:
text
XDATA地址 = 1000H + i
16. 第一个 for:把成绩写入片外 RAM
c
for (i = 0; i < 32; i++)
{
score[i] = score_init[i];
}
第一次:
text
i = 0
score[0] = score_init[0]
= 80
所以:
text
XDATA:1000H ← 80
第二次:
text
i = 1
score[1] = score_init[1]
= 81
所以:
text
XDATA:1001H ← 81
一直到:
text
i = 31
score[31] = score_init[31]
= 95
所以:
text
XDATA:101FH ← 95
这就是你之前在 Keil 输入:
text
X:0x1000
看到:
text
50 51 52 53 ... 5F
50 51 52 53 ... 5F
的原因。
因为 Keil Memory Window 显示的是十六进制:
text
50H = 十进制80
51H = 十进制81
...
5FH = 十进制95
17. sum = 0
接下来:
c
sum = 0;
在累加之前必须把总成绩清零。
因为后面做:
c
sum += score[i];
实际上等价于:
c
sum = sum + score[i];
如果 sum 一开始不是 0,那么最终结果当然就错了。
18. 第二个 for:把 32 个成绩加起来
c
for (i = 0; i < 32; i++)
{
sum += score[i];
}
我们可以手动模拟前几次。
开始:
text
sum = 0
第一次:
text
i = 0
score[0] = 80
sum = 0 + 80
= 80
第二次:
text
i = 1
score[1] = 81
sum = 80 + 81
= 161
第三次:
text
i = 2
score[2] = 82
sum = 161 + 82
= 243
......
32 次以后:
text
sum = 2800
你已经在 Watch 窗口亲自验证了:
text
sum = 2800
i = 32
所以这里证明:
XDATA 中的 32 个成绩已经全部读取并正确累加。
19. 最值得理解的一行
c
avr10 = (sum * 10UL + 16) / 32;
这一行看起来最奇怪,我们完整拆开。
首先:
c
sum * 10UL
为什么不是简单的:
c
sum * 10
UL 表示:
text
Unsigned Long
也就是说:
c
10UL
是一个 unsigned long 常量。
这样可以让乘法提升到更宽的数据类型进行。
虽然当前成绩范围下 sum × 10 并不会很大,但这样写可以明确避免中间乘法使用过窄类型导致溢出的风险。
20. 为什么乘 10
如果直接:
c
sum / 32
就是:
text
2800 / 32 = 87
如果使用纯整数除法,小数部分会被直接丢弃。
但我们要求:
text
87.5
所以先:
text
2800 × 10 = 28000
再:
text
28000 / 32 = 875
于是:
text
avr10 = 875
实际上代表:
text
87.5
只是暂时把小数点向右移动了一位。
21. 那 +16 到底是什么
这一点我们之前没有完整展开。
c
avr10 = (sum * 10UL + 16) / 32;
这里:
text
16 = 32 / 2
它用于实现整数除法中的四舍五入。
假设我们有:
text
175 / 32 = 5.46875
普通整数除法:
text
175 / 32 = 5
因为整数会截断。
如果想四舍五入:
text
(175 + 16) / 32
= 191 / 32
= 5
还是 5。
再例如:
text
177 / 32 = 5.53125
正常应该四舍五入为:
text
6
于是:
text
(177 + 16) / 32
= 193 / 32
= 6
所以对于:
text
整数 / 32
想实现四舍五入,可以在除之前加:
text
32 / 2 = 16
因此:
c
(sum * 10UL + 16) / 32
就是:
计算平均成绩 ×10,并按照最近的一位整数进行四舍五入。
最终对应到原始成绩,就是平均成绩保留一位小数。
22. 当前数据为什么得到 875
现在代入:
text
sum = 2800
所以:
avr10=2800×10+1632 avr10=\frac{2800\times10+16}{32} avr10=322800×10+16
即:
text
2800 × 10 = 28000
28000 + 16 = 28016
28016 / 32 = 875
整数除法余数被丢弃,因此:
text
avr10 = 875
你在 Watch 中也已经看到了:
text
avr10 = 875
23. avr = avr10 / 10.0
接下来:
c
avr = avr10 / 10.0;
注意这里不是:
c
10
而是:
c
10.0
10.0 是浮点数。
因此这里进行浮点除法:
text
875 / 10.0
= 87.5
所以:
text
avr = 87.5
这就正式满足了:
平均成绩保留十分之一位,存至变量 avr 中。
24. 开始拆 BCD
现在:
text
avr10 = 875
avr = 87.5
但题目还要求把结果写到:
text
50H
51H
52H
所以要把:
text
87.5
拆成:
text
8
7
5
25. 先获得整数部分
c
integer_part = avr10 / 10;
由于都是整数:
text
875 / 10 = 87
余数 5 被丢弃。
因此:
text
integer_part = 87
你在 Keil Watch 中看到:
text
integer_part = 87 'W'
这里:
text
87
才是变量的数值。
'W' 只是 Keil 顺便告诉你:
text
ASCII 87 = 'W'
与我们的成绩没有关系。
26. 提取十位
c
bcd_tens = integer_part / 10;
也就是:
text
87 / 10 = 8
整数除法丢掉余数。
所以:
text
bcd_tens = 8
由于:
c
bcd_tens
被固定在:
text
DATA:50H
因此:
text
50H = 08H
27. 提取个位
c
bcd_ones = integer_part % 10;
% 是取余运算。
text
87 ÷ 10
商 = 8
余数 = 7
所以:
text
87 % 10 = 7
得到:
text
bcd_ones = 7
而它固定在:
text
DATA:51H
因此:
text
51H = 07H
28. 提取小数位
c
bcd_decimal = avr10 % 10;
记住:
text
avr10 = 875
它实际上代表:
text
87.5
所以:
text
875 % 10 = 5
恰好得到十分位:
text
5
因此:
text
bcd_decimal = 5
又因为它固定在:
text
DATA:52H
最终:
text
52H = 05H
29. 为什么叫"未压缩 BCD"
现在最终得到:
text
50H = 08H
51H = 07H
52H = 05H
每个字节:
text
08H = 0000 1000
07H = 0000 0111
05H = 0000 0101
可以看到每个字节只存一个十进制数字:
text
0000 1000
└──8
0000 0111
└──7
0000 0101
└──5
这就是:
未压缩 BCD(Unpacked BCD)
如果是压缩 BCD,87 可以把两个数字塞进一个字节:
text
1000 0111
^^^^ ^^^^
8 7
= 87H
而题目明确要求的是未压缩 BCD,所以必须:
text
08H 07H
而不是:
text
87H
30. 最后的 while(1) 是干什么的
c
while (1);
意思是:
text
while (条件永远为真)
什么也不做;
也就是:
无限循环。
在 PC 程序里,我们通常会:
c
return 0;
然后程序退出。
但是单片机不存在 Windows/Linux 帮你"退出程序"。
MCU 上电之后就不断执行代码。
所以 main() 最后通常不会让它返回,而是:
c
while (1)
{
}
让 CPU 停留在这里。
这对我们调试也很方便,因为计算完成后:
text
avr = 87.5
50H = 08H
51H = 07H
52H = 05H
这些数据就保持在那里供我们查看。
31. 现在把整段程序"翻译成人话"
你现在再看程序,可以直接这样读:
c
#define SCORE_ADDR 0x1000
规定成绩区从片外 RAM 的 1000H 开始。
c
unsigned char xdata *score;
建立一个用于访问片外 RAM 成绩的指针。
c
unsigned int sum;
建立总成绩变量。
c
float avr;
建立平均成绩变量。
c
unsigned char data bcd_tens _at_ 0x50;
unsigned char data bcd_ones _at_ 0x51;
unsigned char data bcd_decimal _at_ 0x52;
把三个 BCD 数字固定放在片内 RAM 的 50H、51H、52H。
c
score = (unsigned char xdata *)0x1000;
让 score 指向片外 RAM 1000H。
c
for (...)
score[i] = score_init[i];
模拟实验条件,把32个学生成绩写进片外 RAM。
c
sum = 0;
for (...)
sum += score[i];
从片外 RAM 逐个读取32人成绩并求和,得到2800。
c
avr10 = (sum * 10UL + 16) / 32;
求平均成绩×10,并进行四舍五入,得到875。
c
avr = avr10 / 10.0;
恢复一位小数,得到87.5。
c
integer_part = avr10 / 10;
得到整数部分87。
c
bcd_tens = integer_part / 10;
提取8,写入50H。
c
bcd_ones = integer_part % 10;
提取7,写入51H。
c
bcd_decimal = avr10 % 10;
提取5,写入52H。
c
while (1);
程序计算完成后停留在无限循环中。
最终整个程序的数据变化
把刚才在 Keil 中实际看到的结果全部串起来,就是:
text
CODE
score_init[32]
│
│ 复制
↓
片外 RAM
XDATA 1000H~101FH
│
│ CPU逐个读取
↓
sum = 2800
│
│ ×10、÷32、四舍五入
↓
avr10 = 875
│
├──────────────→ avr = 87.5
│
│ 拆分
↓
8 7 5
│ │ │
↓ ↓ ↓
片内RAM 50H 51H 52H
│ │ │
08H 07H 05H
X:0x1000 是验证"输入数据在哪里";Watch 是验证"CPU 中间计算是否正确";D:0x50 是验证"最终结果有没有按照题目要求写入指定的片内 RAM"。
这正好构成了从输入 → 运算 → 输出的完整调试过程,也符合报告要求记录中间和最终结果并分析正确性的要求。
为什么不可以直接 / 32.0
可以直接 / 32.0。而且如果只是为了求平均值 avr,这样反而更直观:
c
avr = sum / 32.0;
例如:
text
sum = 2800
avr = 2800 / 32.0
= 87.5
之所以我们的程序没有直接这么做,是因为后面还有一个要求:保留一位小数,并把整数十位、个位和小数第一位分别存到 50H、51H、52H。
两种思路的区别
如果直接使用浮点数:
c
avr = sum / 32.0;
得到:
text
avr = 87.5
接下来还得从 float 中提取:
text
8
7
5
例如可能写:
c
integer_part = (unsigned char)avr;
bcd_tens = integer_part / 10;
bcd_ones = integer_part % 10;
bcd_decimal = (unsigned char)(avr * 10) % 10;
看起来也没问题。
但问题出现在四舍五入和浮点误差上。
假设实际平均成绩是:
text
87.56
要求保留一位小数,正确结果应该:
text
87.6
单纯:
c
(unsigned char)(avr * 10)
实际上是截断,可能得到:
text
875 → 87.5
而不是四舍五入后的 87.6。
我们现在的方案:
c
avr10 = (sum * 10UL + 16) / 32;
实际上直接计算:
avr10=round(sum32×10) \text{avr10}=\operatorname{round}\left(\frac{sum}{32}\times10\right) avr10=round(32sum×10)
因此 avr10 已经是四舍五入到一位小数后的结果 ×10。
比如:
text
平均值 avr10
87.53 → 875 → 87.5
87.56 → 876 → 87.6
87.59 → 876 → 87.6
然后:
c
avr = avr10 / 10.0;
只是为了满足题目:
平均成绩保留十分之一位,存至变量
avr。
而 avr10 本身又特别适合 BCD 拆分:
text
avr10 = 875
875 / 10 = 87
87 / 10 = 8 → 50H
87 % 10 = 7 → 51H
875 % 10 = 5 → 52H
所以程序采用的是:
text
sum
↓
整数运算完成四舍五入
↓
avr10 = 875
↓ ↓
÷10.0 拆数字
↓ ↓
avr=87.5 8、7、5
而不是:
text
sum
↓
浮点除法
↓
avr=87.5
↓
再从float里想办法四舍五入、拆数字
那 /32 为什么不能直接换成 /32.0?
c
avr10 = (sum * 10UL + 16) / 32;
这里不能简单改成 /32.0 而保持其他逻辑不变。
因为这里我们故意需要整数除法的截断特性 ,配合前面的 +16 实现四舍五入。
例如:
c
avr10 = (sum * 10UL + 16) / 32;
是:
text
28016 / 32
↓
整数除法
↓
875
+16 就是为了配合这个整数除法。
如果改成:
c
avr10 = (sum * 10UL + 16) / 32.0;
右边变成浮点运算,例如得到:
text
875.5
但 avr10 又是:
c
unsigned int avr10;
赋值时还是会转换成整数 875。
这样虽然我们这组数据碰巧结果仍然对 ,但整个"+16 + 整数除法实现四舍五入"的算法逻辑就被破坏了。
所以你可以记住:
如果只求平均数:
cavr = sum / 32.0;最简单。
如果还要保留一位小数、四舍五入并拆 BCD:
cavr10 = (sum * 10UL + 16) / 32; avr = avr10 / 10.0;更方便。
这也是为什么这里"先 ×10,最后再 ÷10"看起来绕了一圈------真正目的不是求 avr,而是顺便得到一个非常容易处理 BCD 的整数 avr10。
X:0x1000:查看片外存储器
在 Memory Window 输入:
text
X:0x1000
这里的:
text
X:
表示 XDATA 地址空间。
你看到:
text
50 51 52 ... 5F
50 51 52 ... 5F
是在直接查看片外 RAM 1000H~101FH 的内容。
这属于:
查看存储器。
avr = 87.5:查看变量
Watch 里面添加:
text
avr
看到:
text
avr 87.5 float
这里你没有告诉 Keil "我要看哪个 RAM 地址",而是直接告诉调试器:
我要查看 C 程序里的变量
avr。
Keil 根据编译时产生的调试信息,自己找到 avr 当前存放的位置并把值解释成 float 给你看。
所以这是:
查看变量。
这和 Memory Window 有本质区别。
一、预习报告
1. 实验内容
在片外 RAM 中存放 32 名学生的高等数学成绩,每名学生的成绩均小于 100,成绩数据自行选定。
编写 C51 程序读取片外 RAM 中的 32 个成绩并求和,计算平均成绩。平均成绩保留一位小数,并存放在变量 avr 中。
将平均成绩的整数部分转换为未压缩 BCD 码,其中十位存放在片内 RAM 的 50H 单元,个位存放在片内 RAM 的 51H 单元,小数第一位存放在片内 RAM 的 52H 单元。
程序在 Keil μVision5 环境中进行编译和仿真调试,通过设置断点、Watch 窗口和 Memory 窗口观察程序运行过程,验证片外 RAM 中的原始成绩、平均值变量以及片内 RAM 中的最终结果。
本实验选取的 32 名学生成绩为:
text
80,81,82,83,84,85,86,87,
88,89,90,91,92,93,94,95,
80,81,82,83,84,85,86,87,
88,89,90,91,92,93,94,95
32 人总成绩为:
sum=2800 sum=2800 sum=2800
平均成绩为:
avr=280032=87.5 avr=\frac{2800}{32}=87.5 avr=322800=87.5
因此预期结果为:
text
avr = 87.5
50H = 08H
51H = 07H
52H = 05H
2. 程序流程图及源程序
(1)程序流程图
本实验按照以下流程完成数据处理:
text
┌───────────────┐
│ 开始 │
└───────┬───────┘
↓
┌──────────────────────────┐
│ score 指向 XDATA 1000H │
└────────────┬─────────────┘
↓
┌───────────┐
│ i = 0 │
└─────┬─────┘
↓
┌─────────────┐
┌───→│ i < 32 ? │
│ └──────┬──────┘
│ 是 │ 否
│ ↓ └────────────┐
│ ┌─────────────────────┐ │
│ │score[i]=score_init[i]│ │
│ └──────────┬──────────┘ │
│ ↓ │
│ i = i+1 │
└─────────────┘ │
↓
┌────────────────┐
│ sum = 0,i = 0 │
└───────┬────────┘
↓
┌────────────┐
┌───→│ i < 32 ? │
│ └─────┬──────┘
│ 是 │ 否
│ ↓ └──────────┐
│ ┌─────────────────┐ │
│ │sum += score[i] │ │
│ └────────┬────────┘ │
│ ↓ │
│ i = i+1 │
└───────────┘ │
↓
┌──────────────────────────┐
│计算 avr10=(sum×10+16)/32│
└────────────┬─────────────┘
↓
┌────────────────────────┐
│ avr = avr10 / 10.0 │
└───────────┬────────────┘
↓
┌────────────────────────┐
│integer_part=avr10 / 10│
└───────────┬────────────┘
↓
┌────────────────────────────────────┐
│ 50H ← integer_part / 10 │
│ 51H ← integer_part % 10 │
│ 52H ← avr10 % 10 │
└──────────────────┬─────────────────┘
↓
┌─────────────────┐
│ 无限循环 │
└─────────────────┘
正式手绘时建议按照标准流程图符号重新绘制:**开始/结束使用椭圆,处理过程使用矩形,条件判断使用菱形,流程使用带箭头的流程线。**模板明确要求流程图与实际程序一致。
(2)源程序
c
#include <REG52.H>
#define SCORE_ADDR 0x1000
unsigned char xdata *score;
unsigned int sum;
float avr;
/* 平均成绩的未压缩BCD码存放地址 */
unsigned char data bcd_tens _at_ 0x50;
unsigned char data bcd_ones _at_ 0x51;
unsigned char data bcd_decimal _at_ 0x52;
/* 32名学生的测试成绩 */
unsigned char code score_init[32] = {
80, 81, 82, 83, 84, 85, 86, 87,
88, 89, 90, 91, 92, 93, 94, 95,
80, 81, 82, 83, 84, 85, 86, 87,
88, 89, 90, 91, 92, 93, 94, 95
};
void main(void)
{
unsigned char i;
unsigned int avr10;
unsigned char integer_part;
/* 指向片外RAM 1000H */
score = (unsigned char xdata *)SCORE_ADDR;
/* 将32名学生的成绩写入片外RAM */
for (i = 0; i < 32; i++)
{
score[i] = score_init[i];
}
/* 计算32名学生的总成绩 */
sum = 0;
for (i = 0; i < 32; i++)
{
sum += score[i];
}
/* 求平均成绩,四舍五入保留一位小数 */
avr10 = (sum * 10UL + 16) / 32;
avr = avr10 / 10.0;
/* 获得平均成绩的整数部分 */
integer_part = avr10 / 10;
/* 转换为未压缩BCD码 */
bcd_tens = integer_part / 10;
bcd_ones = integer_part % 10;
bcd_decimal = avr10 % 10;
while (1);
}
二、实验报告
1. 实验目的
- 掌握 MCS-51 单片机的存储器结构,理解片内数据存储器 DATA、片外数据存储器 XDATA 和程序存储器 CODE 的区别。
- 熟悉 Keil μVision5 集成开发环境,掌握 C51 工程的建立、程序编辑、编译以及软件仿真的基本方法。
- 掌握 C51 程序设计的基本方法,能够使用循环、数组、指针以及 C51 存储类型完成数据处理。
- 掌握 Keil 的基本调试方法,能够利用断点、Watch 窗口和 Memory 窗口观察程序运行过程。
- 掌握十进制数据与未压缩 BCD 码之间的转换方法。
2. 实验要求
在片外 RAM 中存放 32 名学生的高等数学成绩,学生成绩均小于 100。
程序读取 32 名学生的成绩并计算平均值,平均成绩保留一位小数,结果存放在变量 avr 中。
平均成绩的整数部分采用未压缩 BCD 码存储,其中十位存放在片内 RAM 50H,个位存放在 51H,小数第一位存放在 52H。
在 Keil μVision5 中完成程序编译和软件仿真,通过设置断点观察程序执行过程,并查看相关存储器和变量内容,以验证程序运行结果的正确性。
3. 实验设备
- 计算机一台;
- Keil μVision5 集成开发环境;
- AT89C52 单片机软件仿真环境。
4. 实验原理图、程序流程图及源程序
本实验为 C51 程序设计软件仿真实验,不涉及外部硬件电路连接,因此不需要绘制单片机与其他器件的硬件连接原理图。这也符合实验报告模板中"第 1 个实验不需要"连接图的要求。
程序流程图采用预习报告中所设计的流程图,并根据实际调试后的程序进行修正。
源程序采用上述经 Keil μVision5 编译、仿真和调试后的程序。正式报告要求这里填写的是修正后的流程图和带注释的修正程序代码。
5. 实验分析
模板对这一部分的要求非常明确:需要写出调试过程、中间和最终结果的查看,并分析其正确性。
(1)片外 RAM 成绩数据验证
程序首先令指针 score 指向片外 RAM 的 1000H 地址,然后将 32 名学生的成绩依次写入片外 RAM。
程序执行完成绩初始化循环后,在 Keil 中打开 Memory Window,输入:
text
X:0x1000
观察 XDATA 空间。
得到的数据为:
text
50 51 52 53 54 55 56 57
58 59 5A 5B 5C 5D 5E 5F
50 51 52 53 54 55 56 57
58 59 5A 5B 5C 5D 5E 5F
其中 50H~5FH 分别对应十进制的 80~95。
32 个成绩各占一个字节,因此占用的片外 RAM 地址范围为:
text
1000H ~ 101FH
与程序设定的 32 名学生成绩一致,说明成绩已正确写入片外 RAM。
【此处插入 X:0x1000 Memory Window 调试截图】
(2)平均成绩变量验证
32 名学生的成绩总和为:
sum=2800 sum=2800 sum=2800
程序采用:
c
avr10 = (sum * 10UL + 16) / 32;
计算保留一位小数后的平均成绩的 10 倍。
其中乘以 10 是为了将十分位转换为整数进行处理;由于除数为 32,在除法前加上其一半 16,可以利用整数除法实现正数情况下的四舍五入。
本实验得到:
avr10=875 avr10=875 avr10=875
然后执行:
c
avr = avr10 / 10.0;
得到:
avr=87.5 avr=87.5 avr=87.5
在 Keil Watch Window 中添加变量 avr,程序执行完成后观察到:
text
avr = 87.5
与理论计算:
280032=87.5 \frac{2800}{32}=87.5 322800=87.5
一致,因此平均成绩计算正确。
【此处插入 Watch Window 中 avr = 87.5 的截图】
(3)片内 RAM 最终结果验证
平均成绩为:
text
87.5
其整数部分为:
text
87
程序执行:
c
bcd_tens = integer_part / 10;
bcd_ones = integer_part % 10;
bcd_decimal = avr10 % 10;
因此分别得到:
text
bcd_tens = 8
bcd_ones = 7
bcd_decimal = 5
由于三个变量分别通过 _at_ 指定在片内 RAM 的 50H、51H 和 52H 地址,因此理论结果为:
text
地址 内容
50H 08H
51H 07H
52H 05H
在 Keil Memory Window 中输入:
text
D:0x50
观察 DATA 空间,若显示:
text
08 07 05
则说明平均成绩 87.5 已按照题目要求正确转换为未压缩 BCD 码并存入指定片内 RAM 单元。
其中 08H 表示十位数字 8,07H 表示个位数字 7,05H 表示小数第一位数字 5。
【此处插入 D:0x50 Memory Window 调试截图】
(4)实验结果分析
本实验中,片外 RAM 1000H~101FH 中的 32 个数据与预设成绩完全一致,说明片外 RAM 数据写入和访问正确。
程序计算得到:
text
sum = 2800
avr = 87.5
与人工计算结果一致。
最终片内 RAM:
text
50H = 08H
51H = 07H
52H = 05H
分别表示平均成绩 87.5 的十位、个位和小数第一位,符合未压缩 BCD 码一个字节保存一个十进制数字的特点。
因此,程序能够正确完成片外 RAM 成绩读取、求和、平均值计算、未压缩 BCD 转换以及指定片内 RAM 地址存储,实验结果正确。
6. 思考题和实验总结
实验报告模板要求这一部分抄写实验项目中的思考题并进行回答,同时写出本次实验的收获。
思考题 1:C51 中,对绝对地址的访问有几种方法?访问空间有什么区别?
C51 中常用的绝对地址访问方法包括使用 _at_ 关键字定义绝对地址变量、使用指针访问指定地址,以及使用 absacc.h 中提供的绝对地址访问宏。
本实验采用 _at_ 方法:
c
unsigned char data bcd_tens _at_ 0x50;
表示变量 bcd_tens 位于 DATA 空间的 50H 地址。
也可以使用带有存储空间限定符的指针。例如:
c
unsigned char xdata *p;
p = (unsigned char xdata *)0x1000;
此时 p 指向 XDATA 空间的 1000H 地址。本实验中的 score 即采用这种方法访问片外 RAM。
C51 中不同存储空间的主要区别如下:
| 存储类型 | 存储空间 | 特点 |
|---|---|---|
data |
片内 RAM | 直接寻址,访问速度快 |
idata |
片内 RAM | 间接寻址,可访问 8052 的内部扩展 RAM |
xdata |
外部数据存储器 | 外部 RAM 空间,最大可达 64 KB |
code |
程序存储器 | 主要用于保存程序代码和只读常量 |
因此,即使两个地址的数值相同,例如 50H,如果所属存储空间不同,实际访问的存储区域也可能不同。C51 通过 data、xdata、code 等存储类型区分这些不同的地址空间。
思考题 2:若片外 RAM 中的学生成绩采用压缩 BCD 码存放,程序和流程图应如何修改?
当前程序中的成绩采用普通二进制数存储。例如十进制成绩 87 在片外 RAM 中实际上保存为:
text
十进制 87 = 57H
如果改用压缩 BCD 码存储,则十进制 87 应保存为:
text
87H = 1000 0111B
──── ────
8 7
一个字节的高四位表示十位,低四位表示个位。
因此,从片外 RAM 读取成绩之后不能直接进行求和,而应该首先将压缩 BCD 码转换为普通二进制数。
可增加转换函数:
c
unsigned char bcd_to_bin(unsigned char bcd)
{
return ((bcd >> 4) * 10) + (bcd & 0x0F);
}
例如:
text
BCD数据 = 87H
高四位 = 8
低四位 = 7
8 × 10 + 7 = 87
原程序:
c
sum += score[i];
应修改为:
c
sum += bcd_to_bin(score[i]);
流程图也应在"从片外 RAM 读取一个成绩"和"累加成绩"之间增加:
text
读取压缩BCD成绩
↓
提取高4位和低4位
↓
转换为普通二进制成绩
↓
累加到sum
其余平均成绩计算和最终未压缩 BCD 结果存储过程可以继续按照原程序的方法进行。
实验总结
通过本次实验,进一步理解了 MCS-51 单片机不同存储空间的特点以及 C51 对这些存储空间的访问方法。程序使用 xdata 指针访问片外 RAM,并使用 _at_ 将变量固定在片内 RAM 的指定地址,使 C51 中的变量与单片机实际存储器地址建立了对应关系。
通过编写循环程序,实现了 32 名学生成绩的读取和累加,并利用整数运算完成平均成绩一位小数的保留以及未压缩 BCD 码的转换。通过实验也进一步理解了普通二进制数、压缩 BCD 码和未压缩 BCD 码之间的区别。
在 Keil μVision5 调试过程中,通过设置断点控制程序执行位置,使用 Memory Window 分别观察 XDATA 和 DATA 存储空间,并使用 Watch Window 查看变量 avr 的值。调试结果表明,片外 RAM 中 32 名学生成绩正确,平均成绩为 87.5,片内 RAM 的 50H~52H 最终分别为 08H、07H、05H,与理论计算结果一致。
一、简洁版程序:首先编译程序
打开你的 Keil 工程,确认 main.c 是现在这一版:
c
#include <REG52.H>
unsigned char xdata *score = (unsigned char xdata *)0x1000;
float avr;
unsigned char data bcd_tens _at_ 0x50;
unsigned char data bcd_ones _at_ 0x51;
unsigned char data bcd_decimal _at_ 0x52;
void main(void)
{
unsigned char i;
unsigned int sum = 0;
unsigned int avr10;
unsigned char code scores[32] = {
80,81,82,83,84,85,86,87,
88,89,90,91,92,93,94,95,
80,81,82,83,84,85,86,87,
88,89,90,91,92,93,94,95
};
/* 将成绩存入片外RAM并求和 */
for (i = 0; i < 32; i++)
{
score[i] = scores[i];
sum += score[i];
}
/* 计算平均成绩,保留一位小数 */
avr10 = (sum * 10UL + 16) / 32;
avr = avr10 / 10.0;
/* 转换为未压缩BCD码 */
bcd_tens = avr10 / 100;
bcd_ones = avr10 / 10 % 10;
bcd_decimal = avr10 % 10;
while (1);
}
按:
text
F7
或者:
text
Project
↓
Build Target
下面 Build Output 最好看到:
text
0 Error(s)
0 Warning(s)
至少必须保证:
text
0 Error(s)
二、设置 Keil 使用软件仿真
点击:
text
Project
↓
Options for Target 'Target 1'
或者直接点工具栏的魔法棒图标。
打开以后选择:
text
Debug
找到左侧:
text
Use Simulator
选中它。
也就是我们现在不连接真正的 AT89C52 开发板,而是让:
Keil 在电脑上模拟 AT89C52 的 CPU 和存储器。
点击:
text
OK
三、进入 Debug 模式
现在点击菜单:
text
Debug
↓
Start/Stop Debug Session
快捷键一般是:
text
Ctrl + F5
Keil 会进入 Debug 模式。
这时候你会发现界面发生变化,工具栏上会出现:
text
Run
Stop
Step
Step Over
...
程序此时还没有完整执行。
四、先设置第一个断点
我们第一个要验证的是:
32 个成绩是否真的进入了 XDATA 1000H~101FH。
所以断点应该设置在第一个 for 循环执行完成以后。
找到:
c
for (i = 0; i < 32; i++)
{
score[i] = scores[i];
sum += score[i];
}
/* 计算平均成绩,保留一位小数 */
avr10 = (sum * 10UL + 16) / 32;
在:
c
avr10 = (sum * 10UL + 16) / 32;
这一行左侧灰色区域点击一下。
会出现一个:
🔴 红色圆点
这就是断点。
意思是:
程序运行到计算
avr10之前停下来。
此时前面的循环已经执行完了。
五、运行到第一个断点
点击:
text
Run
或者按:
text
F5
程序会开始运行。
它会执行:
c
for (i = 0; i < 32; i++)
{
score[i] = scores[i];
sum += score[i];
}
循环 32 次以后,到:
c
avr10 = (sum * 10UL + 16) / 32;
之前停下来。
这时候:
text
32个成绩已经写入XDATA
sum也已经计算完成
avr10还没有计算
所以这是非常合适的第一个观察位置。
六、查看片外 RAM
现在点击:
text
View
↓
Memory Windows
↓
Memory 1
底部会出现一个 Memory Window。
在地址输入框输入:
text
X:0x1000
按 Enter。
注意一定有:
text
X:
因为我们要看的是:
XDATA,片外数据存储器。
这时候应该看到类似:
text
X:0x001000 50 51 52 53 54 55 56 57 ...

继续往后就是:
text
50 51 52 53 54 55 56 57
58 59 5A 5B 5C 5D 5E 5F
50 51 52 53 54 55 56 57
58 59 5A 5B 5C 5D 5E 5F
为什么是 50、51 而不是 80、81?
因为 Memory Window 默认按照十六进制显示:
text
十进制 十六进制
80 → 50H
81 → 51H
82 → 52H
...
95 → 5FH
所以看到:
text
50 51 ... 5F
说明数据是正确的。
而且 32 个成绩正好占:
text
1000H ~ 101FH
这里截第一张图
建议截图时把:
text
X:0x1000
和下面的:
text
50 51 52 ...
一起截进去。
报告可以写:
程序运行至第一个断点后,在 Memory Window 中输入
X:0x1000查看片外 RAM,可以观察到1000H~101FH中存放的 32 个成绩与预设数据一致,说明片外 RAM 数据写入正确。
七、顺便查看 sum
这一步不是最终必须截图,但很适合作为调试过程。
点击:
text
View
↓
Watch Windows
↓
Watch 1
Watch 窗口打开以后,在:
text
<Enter expression>
输入:
text
sum
按 Enter。
应该看到:
text
sum 2800
因为:
text
80+81+...+95
+
80+81+...+95
=
2800
这就说明循环的累加也正确。
如果你还加入:
text
i
此时通常应该看到循环完成后的值:
text
i = 32
也就是:
text
0~31 共执行32次

不过 i 是局部变量,后续继续执行以后 C51 编译器可能复用它的存储位置,所以不要把程序最终停下来时 i 的值当作最终实验结果。
八、开始单步执行平均值计算
现在黄色箭头应该停在:
c
avr10 = (sum * 10UL + 16) / 32;
在 Watch 中继续添加:
text
avr10
然后按一次:
text
F10
也就是:
Step Over,单步执行。
执行完这一行:
c
avr10 = (sum * 10UL + 16) / 32;
Watch 中应该看到:
text
sum 2800
avr10 875
因为:
avr10=2800×10+1632=875 avr10=\frac{2800\times10+16}{32}=875 avr10=322800×10+16=875
九、继续执行 avr = avr10 / 10.0
黄色箭头现在来到:
c
avr = avr10 / 10.0;
在 Watch Window 添加:
text
avr
然后再按:
text
F10
执行:
c
avr = avr10 / 10.0;
此时:
text
avr10 = 875
所以:
avr=87510.0=87.5 avr=\frac{875}{10.0}=87.5 avr=10.0875=87.5
Watch 应该出现:
text
avr 87.5
这里截第二张图
这一张非常重要,因为题目明确要求:
平均成绩保留十分之一位,存至变量
avr中。
所以截图最好让 Watch Window 清楚显示:
text
avr 87.5 float
如果同时有:
text
sum 2800
avr10 875
avr 87.5

也完全可以。
报告里写:
在 Watch Window 中观察变量
avr,程序计算得到avr=87.5,与理论计算结果2800/32=87.5一致,因此平均成绩计算正确。
十、继续单步执行 BCD 转换
接下来黄色箭头应该来到:
c
bcd_tens = avr10 / 100;
按:
text
F10
执行以后:
text
bcd_tens = 875 / 100 = 8
然后来到:
c
bcd_ones = avr10 / 10 % 10;
再按:
text
F10
得到:
text
875 / 10 = 87
87 % 10 = 7
bcd_ones = 7
再来到:
c
bcd_decimal = avr10 % 10;
再按:
text
F10
得到:
text
875 % 10 = 5
bcd_decimal = 5
至此:
text
bcd_tens = 8
bcd_ones = 7
bcd_decimal = 5

而根据我们的声明:
c
unsigned char data bcd_tens _at_ 0x50;
unsigned char data bcd_ones _at_ 0x51;
unsigned char data bcd_decimal _at_ 0x52;
实际上已经变成:
text
DATA 50H ← 08H
DATA 51H ← 07H
DATA 52H ← 05H
十一、查看片内 RAM
现在三个 BCD 赋值语句都已经执行完。
再次打开一个 Memory Window:
text
View
↓
Memory Windows
↓
Memory 2
或者继续使用原来的 Memory Window 也可以。
在地址框输入:
text
D:0x50
按 Enter。
注意这一次不是:
text
X:
而是:
text
D:
因为我们要查看:
DATA,片内 RAM。
应该看到从 50H 开始:
text
08 07 05 ...
也就是:
text
DATA:50H = 08H
DATA:51H = 07H
DATA:52H = 05H
分别表示:
text
08H → 8
07H → 7
05H → 5
→ 87.5
这里截第三张图
这是最终结果的存储器验证。
报告中可以写:

在 Memory Window 中输入
D:0x50查看片内 DATA 存储空间,可以观察到50H、51H、52H中的数据分别为08H、07H、05H,分别表示平均成绩 87.5 的十位、个位和小数第一位,符合未压缩 BCD 码的存储要求。
十二、最后让程序运行到无限循环
现在代码剩下:
c
while (1);
可以按:
text
F5
让程序继续运行。
程序最终就会一直执行:
c
while (1);
不会退出。
这是正常现象。
因为单片机程序不像 Windows 程序一样执行完成以后"关闭"。单片机上电后程序会一直运行,因此通常在最后放置无限循环。
十三、如果想重新调试一次
如果哪里操作错了,或者截图没截好,不用重新打开 Keil。
可以先停止当前运行,然后点击:
text
Reset
使模拟的 AT89C52 回到复位状态。
然后再次:
text
F5
程序就会重新从头运行到你设置的断点。
如果彻底退出调试模式:
text
Debug
↓
Start/Stop Debug Session
或者:
text
Ctrl + F5
即可退出。
修改代码以后,则建议:
text
退出Debug
↓
修改代码
↓
F7重新编译
↓
Ctrl+F5重新进入Debug
C51(Keil C51)绝对地址访问的4种方法
51单片机是哈佛架构 ,存储器分为:
code(ROM程序存储器)、data(片内RAM低128B)、idata(片内RAM全部256B)、xdata(片外RAM)、pdata(页寻址片外RAM)。绝对地址:直接指定硬件真实存储编号,不是由编译器自动分配变量地址。
方法1:_at_ 关键字(变量绝对定位,最常用)
语法:
类型 存储类型 变量名 _at_ 地址常数;
示例:
// 片内data RAM,地址0x50
unsigned char data val _at_ 0x50;
// 片外xdata RAM,地址0x1000
unsigned char xdata buf _at_ 0x1000;
// code ROM绝对地址
unsigned char code str _at_ 0x2000;
特点:
- 将整个变量固定分配到指定存储空间的绝对地址;
- 变量名直接读写,语法简单;
- ⚠不能对局部变量(函数内)使用
_at_,只能用于全局/静态变量; - 数组也可以用
_at_定位一块连续内存。
方法2:指针强制类型转换访问(你刚才遇到的方式)
把数字地址强制转为对应存储空间的指针类型。
// 访问xdata片外RAM 0x1000
unsigned char xdata *p = (unsigned char xdata *)0x1000;
*p = 0x11; //写0x1000
unsigned char t = *p; //读0x1000
//访问data片内RAM 0x30
unsigned char data *pd = (unsigned char data *)0x30;
*pd = 0x22;
特点:
- 灵活,可以在函数内部使用;
- 支持运行时动态改变指针地址;
- 强制转换必须带上正确的存储修饰符(
xdata/data/idata/code),否则编译器生成错误汇编指令; p[i]等价于*(p+i)访问连续地址。
重点:
(unsigned char xdata *)0x1000,xdata告诉编译器:这个地址属于片外RAM,生成MOVX指令。
方法3:使用头文件 <absacc.h> 宏定义访问
absacc.h提供5个宏,直接按【空间+地址】读写字节/字,不需要定义变量、指针。
#include <absacc.h>
//DBYTE:data空间字节访问
DBYTE[0x30] = 0x55; //片内data RAM 0x30
//IDBYTE:idata空间字节
IDBYTE[0x80] = 0x66; //片内高128字节RAM
//XBYTE:xdata片外RAM字节访问
XBYTE[0x1000] = 0x77; //片外RAM地址0x1000
//PBYTE:pdata(片外RAM页寻址)
PBYTE[0x02] = 0x88;
//CBYTE:code程序ROM读(只能读!不能写)
unsigned char ch = CBYTE[0x2000];
还有对应字访问:DWORD、XWORD、CWORD等。
特点:
- 直接下标访问,简洁;
code空间CBYTE仅可读,不能写(ROM);- 适合零散的绝对地址操作。
方法4:直接使用寄存器定义(针对SFR特殊功能寄存器)
对于0x80‑0xFF的特殊功能寄存器SFR(P0、P1、TMOD等),
-
方式A:引入
REG52.H,头文件内部用sfr / sbit定义绝对地址寄存器。sfr P1 = 0x90; //特殊功能寄存器P1,地址0x90
sbit P1_0 = P1^0;
sfr、sbit专门用于访问SFR,不能用于普通RAM地址。
✔四种方法简单对比
| 方式 | 语法要点 | 使用位置 | 适用空间 |
|---|---|---|---|
_at_ |
全局变量 _at_ 地址 |
仅全局、静态变量 | data/idata/xdata/code |
| 指针强制转换 | (类型 空间 *)地址 |
全局、函数内部都可以 | data/idata/xdata/code |
| absacc.h宏 | DBYTE\[\] / XBYTE\[\] / CBYTE\[\] | 任意位置 | data/idata/xdata/pdata/code |
| sfr/sbit | sfr 名字=地址 |
全局 | 仅SFR特殊功能寄存器(0x80‑0xFF) |
注意:
sfr只能访问特殊功能寄存器,不能用来访问普通RAM。
C51五大存储空间区别(访问绝对地址必须分清空间)
51是哈佛结构,不同空间地址编号可以重复,但物理硬件完全不一样,汇编指令不同。
| 存储类型 | 物理硬件 | 地址范围 | 汇编指令 | 读写特性 |
|---|---|---|---|---|
data |
片内RAM低128字节 | 0x00‑0x7F | MOV |
可读可写,访问速度最快 |
idata |
片内全部256字节 | 0x00‑0xFF | MOV @R0 |
低128=data,高128仅间接访问 |
pdata |
片外RAM分页(低256B) | 0x00‑0xFF | MOVX @R0 |
片外RAM,仅低8位地址 |
xdata |
片外RAM(外部存储器) | 0x0000‑0xFFFF | MOVX DPTR |
可读可写,16位地址;需要外部RAM芯片 |
code |
程序Flash ROM | 0x0000‑0xFFFF | MOVC |
只能读,不能写,存放程序、常量 |
⚠非常重要易错点:地址数字相同≠同一个存储单元
例如:
0x1000
- 如果是
xdata 0x1000:代表片外RAM的0x1000单元 ,用MOVX读写;- 如果是
code 0x1000:代表程序ROM的0x1000单元 ,用MOVC读;
二者数字一样,但是完全两块物理存储器!
所以C51访问绝对地址,光写数字地址不够,必须指明属于哪一个存储空间(data/xdata/code/idata)。