C51单片机原理及应用-实验一 C51 程序设计(求平均值)

一、先确定一组便于验证的成绩

我建议故意设置一组平均值不是整数的数据,例如:

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 写成一个字节 87H87H 才是压缩 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);
}

这份代码有意没有为了"代码短"而把所有东西塞到一起,因为实验需要调试sumavr10integer_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=0x20sum=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

为什么 sumavr10 没有 'W'

text 复制代码
sum           2800        uint
avr10         875         uint

i             87 'W'      uchar
integer_part  87 'W'      uchar
bcd_tens      8           uchar

sumavr10 是:

c 复制代码
unsigned int

iinteger_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",主要是为了两个目的:

  1. 明确控制保留一位小数和四舍五入
  2. 方便后面直接提取十分位并转换成 BCD

比如平均值如果是 87.56,题目要求一位小数,我们希望得到:

text 复制代码
87.6

就可以先得到:

text 复制代码
876

再把它解释成 87.6

所以可以把这个技巧记成:

需要精确处理固定的小数位时,可以先把小数放大成整数处理,最后再缩小回来。

这其实就是一种很常见的定点数思想。在单片机里尤其常见,因为很多嵌入式场景并不一定需要或者适合全程使用浮点运算。

至于我们代码里的 +16

c 复制代码
avr10 = (sum * 10UL + 16) / 32;
                         ↑

这个又是另一个知识点------它是为了实现四舍五入 。如果想弄懂,可以接着用 87.5387.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 + 整数除法实现四舍五入"的算法逻辑就被破坏了。

所以你可以记住:

如果只求平均数:

c 复制代码
avr = sum / 32.0;

最简单。

如果还要保留一位小数、四舍五入并拆 BCD:

c 复制代码
avr10 = (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. 实验目的

  1. 掌握 MCS-51 单片机的存储器结构,理解片内数据存储器 DATA、片外数据存储器 XDATA 和程序存储器 CODE 的区别。
  2. 熟悉 Keil μVision5 集成开发环境,掌握 C51 工程的建立、程序编辑、编译以及软件仿真的基本方法。
  3. 掌握 C51 程序设计的基本方法,能够使用循环、数组、指针以及 C51 存储类型完成数据处理。
  4. 掌握 Keil 的基本调试方法,能够利用断点、Watch 窗口和 Memory 窗口观察程序运行过程。
  5. 掌握十进制数据与未压缩 BCD 码之间的转换方法。

2. 实验要求

在片外 RAM 中存放 32 名学生的高等数学成绩,学生成绩均小于 100。

程序读取 32 名学生的成绩并计算平均值,平均成绩保留一位小数,结果存放在变量 avr 中。

平均成绩的整数部分采用未压缩 BCD 码存储,其中十位存放在片内 RAM 50H,个位存放在 51H,小数第一位存放在 52H

在 Keil μVision5 中完成程序编译和软件仿真,通过设置断点观察程序执行过程,并查看相关存储器和变量内容,以验证程序运行结果的正确性。


3. 实验设备

  1. 计算机一台;
  2. Keil μVision5 集成开发环境;
  3. 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 的 50H51H52H 地址,因此理论结果为:

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 通过 dataxdatacode 等存储类型区分这些不同的地址空间。


思考题 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

为什么是 5051 而不是 8081

因为 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;

特点:

  1. 整个变量固定分配到指定存储空间的绝对地址
  2. 变量名直接读写,语法简单;
  3. ⚠不能对局部变量(函数内)使用_at_,只能用于全局/静态变量;
  4. 数组也可以用_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;

特点:

  1. 灵活,可以在函数内部使用;
  2. 支持运行时动态改变指针地址;
  3. 强制转换必须带上正确的存储修饰符(xdata/data/idata/code,否则编译器生成错误汇编指令;
  4. p[i]等价于*(p+i)访问连续地址。

重点:(unsigned char xdata *)0x1000xdata告诉编译器:这个地址属于片外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];

还有对应字访问:DWORDXWORDCWORD等。

特点:

  1. 直接下标访问,简洁;
  2. code空间CBYTE仅可读,不能写(ROM);
  3. 适合零散的绝对地址操作。

方法4:直接使用寄存器定义(针对SFR特殊功能寄存器)

对于0x80‑0xFF的特殊功能寄存器SFR(P0、P1、TMOD等),

  • 方式A:引入REG52.H,头文件内部用sfr / sbit定义绝对地址寄存器。

    sfr P1 = 0x90; //特殊功能寄存器P1,地址0x90
    sbit P1_0 = P1^0;

sfrsbit专门用于访问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)