1. MCS51 单片机硬件基础
  2. Keil 集成开发环境
  3. C51 知识
  4. 硬件知识

1. MCS51 单片机硬件基础

1.1 硬件结构与引脚

引脚功能

引脚 全称 作用
VCC voltage circuit 电源端
GND ground 接地端
VSS voltage series 公共链接端
XTAL1 External Crystal Oscillator 1 连接外部石英晶体和微调电容或者震荡信号
XTAL2 External Crystal Oscillator 2 同上
RST/VPD Reset/voltage device 复位信号/备用电源
ALE/-PROG Address Latch Enable/Program 地址锁存信号/编程脉冲引脚
-PSEN Programmer Saving Enable 外部程序存储器读选通
-EA/VPP enable/voltage program 访问程序存储器控制信号/编程模式信号
Px port x IO 引脚

1.2 时钟信号

1
2
3
4
5
参考自:https://zhuanlan.zhihu.com/p/72583737
晶振有一个重要的参数,那就是负载电容值,选择与负载电容值相等的并联电容,就可以得到晶振标称的谐振频率。
一般的晶振振荡电路都是在一个反相放大器(注意是放大器不是反相器)的两端接入晶振,再有两个电容分别接到晶振的两端,每个电容的另一端再接到地,这两个电容串联的容量值就应该等于负载电容,请注意一般IC的引脚都有等效输入电容,这个不能忽略。

一般的晶振的负载电容为15p或12.5p ,如果再考虑元件引脚的等效输入电容,则两个22p的电容构成晶振的振荡电路就是比较好的选择。负载电容+等效输入电容=22pF。

震荡周期:震荡源的震荡周期,是时序中的最小单位
时钟周期:震荡源信号二分频后的时钟脉冲信号周期
机器周期:一个机器周期等于 6 个时钟周期,通常用内存中读取一个指令字的最短时间来规定 CPU 周期
指令周期:CPU 执行一个指令的周期,通常包含 1~4 个机器周期,通常取指令一个机器周期,执行指令包含若干机器周期

1.3 存储器结构

单片机内程序与数据分开存放,分别有内部/外部两个存储器

1.3.1 程序存储器

51 单片机内部总线为 16 位,可寻址空间为 64kb,其中内部存储器有 4kb
-EA 引脚接高点平时从内部存储器启动(0-4kb 使用内部存储,4kb-64kb 使用外部存储)
-EA 引脚接低电平时从外部存储器启动(0-64kb 都使用外部存储)

有两个特殊存储位置:

位置 作用
0000H~0002H 起始指令,通常是跳转语句
0003H~000AH 外部中断 0 处理函数地址
000BH~0012H 计数器/定时器 0 处理函数地址
0013H~001AH 外部中断 1 处理函数地址
001BH~0012H 计数器/定时器 1 处理函数地址
0023H~002AH 串行通信中断处理函数地址

1.3.2 数据存储器

51 单片机的数据存储器在逻辑和物理上都分为两个地址空间,分别使用 MOV 指令和 MOVX 指令访问

对于 51 单片机,256 字节分为高 128 字节的特殊寄存器区(SFR 区)和低 128 字节的 RAM 区

特殊寄存器区对用户来说是只读的(标记为*的可位寻址)

寄存器 作用 寄存器 作用
*ACC 累加器 *B 通用寄存器
*PSW 程序状态字寄存器 SP 栈指针寄存器
DPL 数据存储器指针低位 DPH 数据存储器指针高位
*P0 通道寄存器 0 *P1 通道寄存器 1
*P2 通道寄存器 2 *P3 通道寄存器 3
*IP 中断优先级寄存器 *IE 中断允许寄存器
TMOD 定时器模式寄存器 *TCON 定时器控制寄存器
TH0 定时器 0 高位 TL0 定时器 0 低位
TH1 定时器 1 高位 TL0 定时器 1 低位
*SCON 串口控制器 SBUF 串行数据缓冲器
PCON 电源控制及波特率选择

RAM 区域是用户可用的内存,还可以分为三个部分

RAM 区域 大小 作用
00H~1FH 32 bytes 寄存器组,分为 4 个寄存器组,每组有寄存器 R0~R7 8 个寄存器
20H~2FH 16 bytes 可寻址区,对应位寻址空间的 00~7FH
30H~7FH 80 bytes 一般 RAM 区

1.4 定时器

定时器有两种工作方式:
定时器模式:每个机器周期计数加一,当定时器数值与 THL0/1 寄存器中数值相同时执行特定动作
计数器模式:接受 P3.4 和 P3.5 引脚上的脉冲,出现高-低跳变时计数加一

TMOD 寄存器可以操控定时器的工作方式
TCON 定时器可以操控定时器的启动/复位/停止等动作

1.5 I/O 端口

1.5.1 并行接口

51 单片机共 4 个并行接口,但是其各自的内部结构都不相同

  1. P0 端口

    P0 口为漏极开路输出,所以在执行输出的时候必须外接上拉电阻(10K 即可)
    执行输入的时候,首先要输出高电平

  2. P1 端口

    P1 端口自带一个上拉电阻,可以直接作为输出引脚
    同时在读取外部引脚输入的时候,必须先向锁存器写入高电平,使场效应管 T 截止
    否则读到的一定是低电平

  3. P2 端口

    P2 端口也有内部上拉电阻
    其余功能与 P0,P1 类似

  4. P3 端口

    相比于其他端口,P3 端口可以支持第二功能
    不同位的端口第二功能参考手册或引脚图

1.5.2 串行端口

串行端口复用了 P3 端口的第二功能
使用 PCON 寄存器可以控制波特率,SCON 寄存器可以控制串行通信的过程
SBUF 寄存器用来存储发送/接受的数据,由两个独立的缓冲器组成

1.6 中断系统

8051 一共可以处理 5 个中断源:2 个时钟中断,2 个外部中断,1 个串行通信中断
IE 寄存器用来控制中断处理的开启与关闭
IP 寄存器用来控制 5 个中断的优先级

1.7 总线

  1. 数据总线

    51 单片机数据总线为 8 位,由 P0 端口提供

  2. 地址总线

    51 单片机地址总线为 16 位,由 P0(低位)和 P2(高位)共同提供
    在访问时,P0 口首先提供地址低八位,然后由 ALE 锁存信号数据锁存到外部地址锁存器中
    然后 P0 转为数据总线,准备接受数据

  3. 控制总线

    在读取外部程序存储器时,-PSEN(外部程序存储器选通信号)生效(低电平)
    在读取外部数据存储器时,P3 口自动产生-RD/-WR 信号(P3.6 和 P3.7)

2. Keil 集成开发环境

Keil 开发流程

其中 RTX51 是一种运行在 51 单片机上的实时操作系统

2.1 开发流程

  1. 编写完源文件并将其添加到工程中

    编写源码文件,并将其添加到项目中

  2. 设置项目

    1. Target 选项卡

      • Xtal(MHz):单片机 CPU 的工作频率,默认为最高频率
      • Memory Model:数据存储空间类型-Small(变量存到内部 RAM 中),Compact(变量存到外部 RAM 中,使用 8 位间接寻址),Large(变量存到外部 RAM 中,使用 16 位间接寻址)
      • Code Rom Size:代码存储空间类型-Small(程序不超过 2kb),Compact(函数不超过 2kb,整个程序可使用 64kb),Large(程序可以完全使用 64kb)
      • Operating:使用的操作类型类型,Keil 提供了 RTX51 操作系统,默认为 None
      • Off-Chip Code Memory:用于确定外接 ROM 的范围
      • Off-Chip XData Memory:用于确定外接 RAM 的范围
    2. Output 选项卡

      • Select Folder for Objects:生成的目标文件文件夹
      • Name of Executable:生成的目标文件名称
      • Debug information:生成用于调试的信息
      • Browse information:产生浏览信息
      • Create HEX File:生成 HEX 文件
      • Create Library:生成库文件
    3. Listing 选项卡
      调整生成的.list 文件选项,如可以设置生成汇编代码等
    4. C51 选项卡
      调整编译优化等级相关内容
  3. 编译和链接(略)

  4. 仿真和调试

    Keil 提供了 dScope(Debug Scope)工具来进行调试
    在项目设置中可以选择关于调试的一些配置:

    左侧为使用仿真进行调试,右侧为连接硬件调试

2.2 仿真与调试

Keil 与 Proteus 可以联合调试
一共有两种操作方式:

  • Proteus 绘制原理图并编写代码,利用 Keil 的编译器编译后调试
  • Proteus 绘制原理图,Keil 编写代码,二者基于同一个端口进行通信并调试

2.2.1 Proteus+Keil 编译器

如上,建立完项目后,原理图中会自动生成一个单片机,同时多了一个源码窗口
编写完代码,构建项目(就是编译),然后调试即可
适合一些小项目,调试不太方便

2.2.2 Proteus+Keil

首先需要一些准备工作(仅在首次使用时设置)

  1. 把将 ${PROTEUS_HOME}\MODELS\VDM51.dll 复制到 keil 的目录 ${KEIL4}\C51\BIN 中,如果没有可以自行搜索并下载
  2. 打开 ${KEIL4}\TOOLS.INI[c51] 后面添加TDRV5=BIN\VDM51.DLL ("Proteus VSM Monitor-51 Driver")
    或者使用 这个软件 一步到位
  1. 正常建立 Proteus 项目

  2. 正常建立 Keil 项目(选择 80C51 作为 CPU)

  3. 编写 Keil 文件并编译

  4. 设置 Keil 调试模式为与 Proteus 联合调试(默认使用 8000 端口)

  5. 设置 Proteus 允许远程调试

  6. 在 Kiel 中启动调试后会自动在 Proteus 中进行仿真

3. C51 知识

这一章内容参考自 Keil 中自带的教程中的 Complete User's Guide Selection 中的 Cx51 Compiler User's Guide

主要是 51 单片机的 C 语言对标准 C 语言的扩展

除了下述内容,51 单片机还提供了一个 RTX 实时操作系统,详细可以参考Complete User's Guide Selection教程

3.1 内存扩展

首先是单片机内部的内存分布

  1. 代码:代码存储器
  2. 全局数据:通用内存中地址最低的部分
  3. 局部数据:存放在运行时堆栈中
  4. 堆栈空间:通用内存中全局数据上方,向上生长

与大多数通用计算机把程序,数据放在同一个物理内存空间不同,51 单片机提供了拥有不同特性的几种存储空间

  1. Program Memory(程序内存)
    只读内存,用来存储程序,静态数据也可以存进去以节省其他内存的空间
    可以在声明变量的时候使用code关键字显式声明变量存储位置为程序内存
    code int i=0;

  2. Internal Data Memory(片内数据存储空间)
    片内数据存储空间,分为两个部分:高位 128 字节,低位 128 字节

    • 高位 128 字节只能间接寻址,如果直接寻址的话会被映射到特殊寄存器(注意是映射,这里并不是真的特殊寄存器存储位置)
    • 低位 128 字节同时支持直接寻址和间接寻址,同时在20h的位置有 16 字节的按位寻址区,这里的数据还能通过位寻址(映射到位寻址空间的 00~7FH)

    data显式声明数据存储位置为低位 128 字节
    idata显式声明存储位置为整个片内数据存储空间
    bdata显式声明数据存放在位寻址区

  3. External Data Memory(片外数据存储空间)
    片外数据存储空间,及通过片外扩展获得的存储空间,最高 64kb,只能通过数据指针寄存器间接访问
    xdata声明变量存放在整个外部数据存储空间
    pdata声明变量存放在外部数据存储空间的第一页,共 256 字节

  4. SFR Memory(特殊寄存器内存)
    每个特殊寄存器都对应一个硬件功能
    可以通过直接访问内部数据内存来操作这些寄存器

最后是三个内存模型,可以通过在 Keil 的项目设置中更改

  1. Small:只使用片内数据存储空间的低位 128 字节,等同于所有数据都使用data声明
  2. Compact:片外数据存储空间的第一页,共 256 字节,等同于搜有数据都使用pdata声明
  3. Large:使用片外数据存储空间,最高 64kb,等同于所有数据都使用xdata声明

3.2 数据扩展

数据类型如下,复制自 Keil 中自带的教程中的 Complete User's Guide Selection 中的 Cx51 Compiler User's Guide

数据类型 bit 数量 byte 数量 范围
bit 1 0 to 1
signed char 8 1 -128 — +127
unsigned char 8 1 0 — 255
enum 8 / 16 1 or 2 -128 — +127 or -32768 — +32767
signed short int 16 2 -32768 — +32767
unsigned short int 16 2 0 — 65535
signed int 16 2 -32768 — +32767
unsigned int 16 2 0 — 65535
signed long int 32 4 -2147483648 — +2147483647
unsigned long int 32 4 0 — 4294967295
float 32 4 ±1.175494E-38 — ±3.402823E+38
double 32 4 ±1.175494E-38 — ±3.402823E+38
sbit 1 0 or 1
sfr 8 1 0 — 255
sfr16 16 2 0 — 65535

其中新增的类型为

  1. bit
    单个 bit,存放一个布尔值,不可声明其指针或数组

    1
    bit name <[>= value<]>;
  2. sbit
    声明一个特殊寄存器一位的引用

    1
    2
    3
    sbit name = sfr-name ^ bit-position;
    sbit name = sfr-address ^ bit-position;
    sbit name = sbit-address;
  3. sfr
    声明一个 8 位特殊寄存器的引用

    1
    sfr name = address;
  4. sfr16
    声明一个 16 位特殊寄存器的引用

    1
    sfr16 name = address;

    然后是一个变量修饰符—volatile:声明一个变量为”易变的”,用于禁止编译器的过度优化

在这份代码中,每次 while 循环判断中,编译器不会重复读取 reg1 寄存器,而是使用上一次的值
这会导致 reg1 被其他行为改变后无法及时更新到代码中

1
2
3
4
5
6
7
8
9
10
11
unsigned char reg1;   // Hardware Register #1
unsigned char reg2; // Hardware Register #2

void func (void)
{
while (reg1 & 0x01) // Repeat while bit 0 is set
{
reg2 = 0x00; // Toggle bit 7
reg2 = 0x80;
}
}

改用 volatile 修饰后,编译器会在每次 while 循环判断时都会重新读取 reg1 寄存器

1
2
3
4
5
6
7
8
9
10
11
volatile unsigned char reg1;   // Hardware Register #1
volatile unsigned char reg2; // Hardware Register #2

void func (void)
{
while (reg1 & 0x01) // Repeat while bit 0 is set
{
reg2 = 0x00; // Toggle bit 7
reg2 = 0x80;
}
}

3.3 函数扩展

3.3.1 堆栈传参

  1. 首先堆栈位于所有的全局变量地址上方,栈向上生长
  2. 然后是,一般参数较少的函数调用会使用寄存器传参(三个参数,原文如下)
    By default, the Cx51 Compiler passes up to three function arguments in registers. This enhances speed performance.
  3. 最后是一个奇妙的机制,编译器会为每个函数参数指定一个固定的地址,然后栈中只存放函数返回地址,函数调用者把参数放到指定地点,被调用者从这些地方去取,节省了栈空间
    The total stack space of the classic 8051 is limited to 256 bytes. Rather than consume stack space with function parameters or arguments, the Cx51 Compiler assigns a fixed memory location for each function parameter. When a function is called, the caller must copy the arguments into the assigned memory locations before transferring control to the desired function. The function then extracts its parameters, as needed, from these fixed memory locations. Only the return address is stored on the stack during this process. Interrupt functions require more stack space because they must switch register banks and save the values of a few registers on the stack.

3.3.2 寄存器传参

不同位置和类型的参数使用不同的寄存器,复制自Complete User's Guide Selection 中的 Cx51 Compiler User's Guide

ArgumentNumber char/1-byte ptr int/2-byte ptr/long float /generic ptr
1 R7 R6 & R7 R4-R7 R1-R3
2 R5 R4 & R5 R4-R7 R1-R3
3 R3 R2 & R3 R1-R3

如果参数类型与上述表中不一样,则其连带其后变量使用堆栈传参(如 bit 型参数)
If the first parameter of a function is a bit type, then other parameters are not passed in registers. This is because parameters that are passed in registers are out of sequence with the numbering scheme shown above. For this reason, bit parameters should be declared at the end of the argument list.

3.3.3 返回值类型

返回值永远使用寄存器传值

Return Type Registers Storage Format
bit Carry Flag
char, unsigned char, 1-byte ptr R7
int, unsigned int, 2-byte ptr R6 & R7 MSB in R6, LSB in R7
long, unsigned long R4-R7 MSB in R4, LSB in R7
float R4-R7 32-Bit IEEE format
generic ptr R1-R3 Memory type in R3, MSB R2, LSB R1

3.3.4 内存模型

首先是,编译器会自动优化变量存储,如下…
(本来想试试局部变量存储位置,结果找了半天找不到,一看汇编代码简直给我气笑了)

正经来说,首先可以指定一个全局存储模型,然后也可以给不同函数指定特殊的存储模型
存储模型名字和作用和前面讲的一致
(试了半天都会被编译器最终优化成寄存器变量…算了先不管这个)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#pragma small         /* Default to small model */

extern int calc (char i, int b) large reentrant;
extern int func (int i, float f) large;
extern void *tcp (char xdata *xp, int ndx) compact;


int mtest (int i, int y) /* Small model */
{
return (i * y + y * i + func(-1, 4.75));
}


int large_func (int i, int k) large /* Large model */
{
return (mtest (i, k) + 2);
}

3.3.5 寄存器组机制

51 单片机提供了 4 组寄存器组(4*8 个),可以实现上下文切换(类似 x86 中的进程控制块 PCB)
默认使用 0 号寄存器组,可以通过 using 关键字指定函数使用哪一个

1
2
3
4
5
6
void rb_function (void) using 3
{
.
.
.
}

3.3.6 中断函数

C51 编译器最高支持 32 个中断(芯片本身可能支持不到,如 80C51 只支持 5 个)
使用interrupt num声明一个函数为一个中断处理函数,其中num为中断号

中断处理时的上下文切换规则如下:

  1. ACC, B, DPH, DPL, PSW 寄存器存入堆栈并在中断处理结束后恢复
  2. 如果使用了 using 关键字指定寄存器组则不进行额外操作
  3. 如果没有使用 using 关键字指明寄存器组,则原寄存器组存入堆栈,中断处理结束后恢复,中断函数使用原寄存器组
1
2
3
4
5
6
7
8
unsigned int  interruptcnt;
unsigned char second;

void timer0 (void) interrupt 1 using 2 {
if (++interruptcnt == 4000) { /* count to 4000 */
second++; /* second counter */
interruptcnt = 0; /* clear int counter */
}

3.3.7 可重入函数

一般的函数不支持递归操作,且不能被多个进程同时调用,因为其局部变量可能存在寄存器中(与 x86 很不一样)
可以使用reentrant关键字声明一个可重入函数,可重入函数的局部变量存放在栈中,且其使用额外的栈指针来管理

1
2
3
4
5
int calc (char i, int b) reentrant  {
int x;
x = table [i];
return (x * b);
}

可重入函数有如下限制:

  1. 不能使用 bit 型变量
  2. 不能从 alien 函数中调用
  3. 不能同时有 alien 属性,也不使用 using 和 interrupt 关键字

关于可重入函数的其他内容可以自行参考 Complete User's Guide Selection

4. 硬件知识

4.1 基础概念

参考自:
[深入理解 setup time 和 hold time | 知乎]

  • 建立时间(setup time): 数据信号必须先于时钟信号到达,以保证触发器可靠翻转
  • 保持时间(hold time): 时钟信号到达后,数据信号必须保持一段时间,以保证触发器翻转可靠翻转
  • 传输延迟时间(propagation delay time): 时钟动作信号到达后,一段延迟后触发器才建立新的稳定状态
  • 最高时钟频率(maximum clock frequency): 时钟最高频率

4.2 总线

4.2.1 I2C 总线

参考自:
[简单优雅的总线协议——I2C | 知乎]
[I2C 通信协议介绍 | 知乎]

I2C 总线全称为 Inter-Integrated Circuit(内部集成总线),为一个(多)主从总线结构
线上有主设备和从设备两种设备,二者都可以有多个
主设备可以与从设备通信(通过从设备地址指定从设备)
I2C 仅仅需要两根数据线,一根为时钟信号(Series Clock,SCL),一根为数据信号(Series Data,SDA)

I2C 中的数据线为漏极开路结构,必须添加一个额外的上拉电阻

I2C 协议定义了两个特殊标志

  • 开始信号:SCL 高电平,SDA 下降沿
  • 结束信号:SCL 高电平,SDA 上升沿

除了特殊标志以外的信息必须遵守以下规定:

  • SCL 高电平时,SDA 必须稳定(否则就是特殊信号了)
  • SDA 对于 SCL 上升沿有 setup time 要求(SDA 要提前稳定)
  • SDA 对于 SCL 下降沿有 hold time 要求(SDA 要保持一段时间稳定)
  • SDA 只能在 SCL 低电平时变化(好像是废话…)
  1. 主设备写入从设备

    1. 主设备发送起始信号
    2. 主设备立即发送从设备地址及写入命令
    3. 主设备把 SDL 拉到高电平
      如果从设备准备接受,则发送一个低电平 ACK 信号
      但是如果从设备错误(或者根本没有对应的从设备),则 SDL 为高电平 NACK 信号
    4. 主设备发送写入地址,从设备发送 ACK/NACK
    5. 主设备发送停止信号,通信结束
  2. 主设备读取从设备

    1. 主设备发送起始信号
    2. 主设备立即发送从设备地址及读取命令
    3. 主设备把 SDL 拉到高电平
      如果从设备准备接受,则发送一个低电平 ACK 信号
      但是如果从设备错误(或者根本没有对应的从设备),则 SDL 为高电平 NACK 信号
    4. 从设备发送写入地址,主设备发送 ACK/NACK
    5. 主设备发送停止信号,通信结束,通信结束前需要发送一个 NACK 信号
  3. 复合操作

    1. 主设备发送起始信号
    2. 主设备立即发送从设备地址及读取命令
    3. 主设备把 SDL 拉到高电平
      如果从设备准备接受,则发送一个低电平 ACK 信号
      但是如果从设备错误(或者根本没有对应的从设备),则 SDL 为高电平 NACK 信号
    4. 从设备发送写入地址,主设备发送 ACK/NACK
    5. 主设备重新发送起始信号,并开始一个新的操作周期

4.2.2 SPI 总线

SPI 全称 Serial Peripheral Interface(串行外设接口)

相关资料(懒得写了):[简单快速的总线协议——SPI | 知乎]