从零开始学习嵌入式P36----单片机(1)
从零开始学习嵌入式P36----单片机(1)
上一篇我们用 SQLite 给 Linux 程序加上了数据管理能力,从 C 语言、数据结构、文件 IO、进程线程、网络编程一路走到了数据库。从这一篇开始,学习场景要从“跑在操作系统上的应用程序”切换到“直接控制硬件的单片机程序”:没有 Linux,没有进程和文件系统,我们面对的是寄存器、引脚、时钟、中断和定时器。本篇先把单片机世界最重要的地基打好:弄清楚单片机最小系统是什么、单片机内部由哪些部件组成、时钟周期和机器周期怎样换算;再复习寄存器操作离不开的位运算;最后学会使用外部中断和定时器中断,让单片机从“死等”走向“按事件响应、按时间工作”。
本篇目标
学完这一篇,我们应该能够:
- 说清单片机最小系统包含哪些硬件,以及它为什么不是操作系统;
- 说清单片机内部的主要组成部分,理解
P0、P1、P2等 IO 口和寄存器的关系; - 分清时钟周期、机器周期、指令周期,并会根据晶振频率进行换算;
- 熟练使用置位、清位、取反、读位这四个位运算套路;
- 说清轮询和中断的区别,会配置 51 单片机的外部中断 0;
- 理解定时器 0 的 16 位工作方式,会计算初值并实现约 1 秒的 LED 翻转。
一、什么是单片机最小系统
单片机入门时经常听到“最小系统”这个词,也有人把它口头叫成“最小运行系统”,甚至误写成“最小操作系统”。需要先纠正一个概念:单片机最小系统通常不是软件层面的操作系统,而是指让单片机能够稳定运行程序所需的最少硬件条件。
一块单片机芯片单独摆在桌面上不会自己工作,就像一台没有电源、没有启动盘的电脑无法运行一样。对常见 51 单片机来说,最小系统一般包括下面几个部分。
1. 电源电路
单片机首先要获得稳定、符合要求的电源。常见 51 单片机使用 5V 供电,具体电压范围要以芯片数据手册为准。
电源电路不只是“接上正负极”这么简单,通常还要注意:
VCC接电源正极,GND接电源负极;- 电源电压不能超过芯片允许范围;
- 芯片电源引脚附近放去耦电容,滤除电源波动;
- 单片机、下载器、外设模块必须共地,否则电平没有共同参考点。
没有稳定电源,单片机可能完全不启动,也可能出现程序偶尔跑飞、复位、外设误动作等奇怪问题。
2. 时钟电路
CPU 执行指令、定时器计数、串口通信,都需要一个稳定的节拍。这个节拍由时钟电路提供。
常见时钟方案有两种:
- 外部晶振:晶振配合两个负载电容,为单片机提供稳定时钟;
- 内部振荡器:部分新型单片机内部自带时钟,精度要求不高时可以不接外部晶振。
经典 51 开发板经常使用 11.0592MHz 晶振。这个频率不仅能产生合适的定时基准,还方便串口通信生成标准波特率。
3. 复位电路
复位的作用是让单片机回到确定的初始状态:程序计数器回到程序入口,相关寄存器恢复默认值,程序从头开始执行。
常见复位电路由电阻、电容和按键组成。按下复位键时,复位引脚获得有效电平,单片机复位;松开后,单片机重新开始运行。
复位电路看似简单,却是排查故障时的重要对象:如果复位引脚一直处于有效状态,单片机会一直复位,程序永远跑不起来。
4. 程序下载和启动电路
单片机要运行,芯片内部必须有程序。程序通过下载器写入单片机的 Flash 或 ROM 中。
对使用内部程序存储器的经典 51 单片机,还要注意启动选择引脚。例如部分芯片的 EA 引脚用于选择执行内部程序还是外部程序,使用内部 Flash 时需要按数据手册把它接到正确电平。
因此,最小系统可以用一句话概括:
电源让芯片活过来,时钟给芯片节拍,复位让程序从确定状态开始,程序下载电路把代码送进芯片。
这四者齐了,单片机才具备运行程序的基本条件。发光二极管、数码管、按键、蜂鸣器等都属于具体应用外设,不属于严格意义上的最小系统。
5. 最小系统不工作的排查顺序
单片机没有任何反应时,不要急着怀疑程序,可以按下面的顺序排查:
| 排查顺序 | 检查内容 | 常见问题 |
|---|---|---|
| 1 | 电源 | 电压不对、正负接反、没有共地、电源电流不足 |
| 2 | 复位 | 复位脚一直有效、复位按键短路、复位电平方向弄反 |
| 3 | 时钟 | 晶振没焊好、负载电容不合适、时钟来源配置错误 |
| 4 | 程序 | 程序没有下载成功、启动引脚配置错误 |
| 5 | IO 外设 | 引脚接错、LED 正负极接反、共阴共阳类型弄错 |
二、单片机由什么组成
单片机是把 CPU、存储器和多种外设接口集成到一颗芯片里的微型计算机。 “单片”不是说它只能做一件事,而是说传统计算机分散在多颗芯片上的部件,被集成到了同一颗芯片中。
一颗典型 51 单片机内部主要包括:
| 组成部分 | 作用 |
|---|---|
| CPU | 取指令、译码、执行运算和逻辑判断 |
| 程序存储器 Flash/ROM | 保存程序,断电后程序不丢失 |
| 数据存储器 RAM | 保存运行过程中的变量和临时数据 |
| 特殊功能寄存器 SFR | 控制 IO、定时器、串口、中断等硬件 |
| 并行 IO 口 | 连接 LED、按键、数码管、继电器等外部设备 |
| 定时器/计数器 | 定时、计数、产生时间基准 |
| 中断系统 | 让单片机及时响应外部事件或定时事件 |
| 串行通信口 UART | 与电脑、蓝牙模块、WiFi 模块等设备通信 |
| 时钟和复位控制 | 提供运行节拍,让芯片回到初始状态 |
1. CPU:单片机的大脑
CPU 负责从程序存储器中取出指令并执行。它内部包含运算器、控制器、程序计数器等部件。
我们写的 C 语言代码最终会被编译成机器指令,例如:
- 把一个数送入寄存器;
- 对两个数做加法;
- 判断某个条件是否成立;
- 跳转到另一段代码;
- 向某个特殊功能寄存器写入控制值。
单片机程序看起来仍然是 C 语言,但每一句最终都会落实为对寄存器和硬件的具体操作。
2. Flash/ROM:程序住在哪里
编译下载后的程序保存在程序存储器中。Flash/ROM 的特点是断电后内容不丢失,所以下次上电还能继续运行同一段程序。
这也是单片机和普通 Linux 程序的重要区别:Linux 程序通常从文件系统中加载到内存执行,而单片机程序固化在芯片内部,上电后直接从程序入口运行。
3. RAM:变量住在哪里
程序运行中的变量、数组、栈等临时数据保存在 RAM 中。RAM 断电后数据丢失。
因为单片机 RAM 通常比较小,写程序时要养成两个习惯:
- 能用
unsigned char就不要随便用int; - 不要在单片机里随意定义超大数组。
例如只表示 0 到 255 的计数器,使用 unsigned char 往往比 int 更合适。
4. SFR:软件和硬件之间的门
特殊功能寄存器 SFR 是理解单片机编程的关键。普通变量只是存放在 RAM 中,而特殊功能寄存器背后连接着真实硬件。
向一个控制寄存器写 1,可能意味着:
- 打开某个中断;
- 启动一个定时器;
- 选择一种工作模式;
- 让某个引脚输出高电平。
读取一个状态寄存器,可能意味着:
- 查询按键是否按下;
- 判断定时器是否溢出;
- 判断串口是否收到数据。
所以,单片机编程的核心不是单纯写算法,而是通过寄存器配置硬件、控制硬件。
5. IO 口:单片机与外界交换信息

51 单片机常见有 P0、P1、P2、P3 等并行 IO 口,每组通常有 8 个引脚。一个引脚可以配置为输出或输入。
一个51单片机核心结构如图所示。
输出例子——控制 LED:
#include <reg52.h>
sbit Led1 = P2^0;
int main(void)
{
Led1 = 0; // 许多开发板的 LED 低电平点亮
while (1)
{
}
return 0;
}
输入例子——读取按键:
#include <reg52.h>
sbit Key1 = P1^4;
sbit Led1 = P2^0;
int main(void)
{
while (1)
{
if (Key1)
{
Led1 = 1;
}
else
{
Led1 = 0;
}
}
return 0;
}
这里的 sbit 是 Keil C51 提供的扩展语法,用来把某个可寻址位起一个直观的名字。P2^0 表示 P2 口的第 0 位。
需要特别注意:开发板上的 LED 经常是低电平点亮。也就是说:
- 引脚输出
0,LED 亮; - 引脚输出
1,LED 灭。
这不是程序写反了,而是由硬件连接方式决定的。
6. 数码管:一个典型的 IO 输出设备
四位数码管通常需要两组信号:
- 段选信号:控制某一位显示什么数字;
- 位选信号:控制当前点亮哪一位数码管。
数字 0 到 9 的段码可以放在数组中:
unsigned char NumCode[10] =
{
0x3F, 0x06, 0x5B, 0x4F, 0x66,
0x6D, 0x7D, 0x07, 0x7F, 0x6F
};
显示一个四位数时,先把它拆成千、百、十、个四个数字:
Qian = TmpNum / 1000;
Bai = TmpNum % 1000 / 100;
Shi = TmpNum % 100 / 10;
Ge = TmpNum % 10;
然后依次选中每一位、送出对应段码、延时很短的时间。由于切换速度很快,人眼看到的是四位同时显示,这种现象叫动态扫描。
从这个例子可以看到:单片机程序经常要同时考虑软件逻辑和硬件特性。程序不仅要算出数字,还要知道段码是什么、位选电平是什么、扫描速度是否合适。
三、时钟周期、机器周期和指令周期
单片机里的所有动作都按照节拍进行。理解这三个周期,是后面计算延时和定时器初值的基础。
1. 三个周期分别是什么
| 名称 | 含义 |
|---|---|
| 时钟周期 | 晶振振荡一次所需的时间,是单片机最小的时间单位 |
| 机器周期 | CPU 完成一个基本操作所需的时间,经典 8051 为 12 个时钟周期 |
| 指令周期 | 执行一条机器指令所需的时间,通常为 1、2 或 4 个机器周期 |
它们的关系是:
时钟周期 = 1 / 晶振频率
机器周期 = 12 × 时钟周期
指令周期 = 1~4 × 机器周期
注意:经典 8051 是 12 时钟内核;一些增强型 51 单片机可以采用 6 时钟甚至 1 时钟内核。具体换算必须看芯片手册,不能把所有“51 单片机”都简单当成 12 时钟。
2. 用 11.0592MHz 晶振计算
晶振频率:
Fosc = 11.0592MHz = 11059200Hz
时钟周期:
Tclock = 1 / 11059200 ≈ 0.0904us
经典 8051 的机器周期:
Tmachine = 12 × 0.0904us ≈ 1.085us
也就是说,在 11.0592MHz 晶振下,经典 8051 大约每 1.085us 完成一个机器周期。
3. 为什么软件延时不适合精确定时
下面这种延时函数通过循环消耗时间:
void Delay1000ms(void)
{
unsigned char data i, j, k;
i = 8;
j = 1;
k = 243;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
它的延时时间和下面这些因素有关:
- 晶振频率;
- 编译器生成的机器指令;
- 编译优化等级;
- 单片机内核是 12 时钟还是 1 时钟;
- 循环变量所在存储区域。
所以软件延时只能用于“大概等一下”,不适合精确定时。晶振从 11.0592MHz 换成 12MHz,或者换一家编译器,延时都会变化。
更重要的是,执行软件延时时 CPU 一直空转,不能及时处理按键、通信等其他任务。后面使用硬件定时器,就能让 CPU 在等待期间继续执行主循环。
四、位运算复习
单片机寄存器通常按位控制硬件。比如某个寄存器的第 0 位控制定时器,第 1 位控制中断,第 7 位是总开关。位运算是单片机 C 语言的基本功。
1. 六种位运算符
| 运算符 | 名称 | 示例 | 结果特点 |
|---|---|---|---|
& | 按位与 | a & b | 两位都为 1,结果才为 1 |
| ` | ` | 按位或 | `a |
^ | 按位异或 | a ^ b | 两位不同为 1,相同为 0 |
~ | 按位取反 | ~a | 0 变 1,1 变 0 |
<< | 左移 | a << n | 向左移动,低位补 0 |
>> | 右移 | a >> n | 向右移动,注意高位补什么 |
2. 最常用的四个套路
假设要操作寄存器 REG 的第 n 位。
置位——把第 n 位变成 1,其他位保持不变:
REG |= (1 << n);
清位——把第 n 位变成 0,其他位保持不变:
REG &= ~(1 << n);
取反——把第 n 位从 0 变 1、从 1 变 0:
REG ^= (1 << n);
读位——判断第 n 位是否为 1:
if (REG & (1 << n))
{
/* 第 n 位为 1 */
}
这四个套路一定要熟,因为配置寄存器时几乎每天都在用。
3. 位运算实现流水灯
下面这句代码一次点亮一个 LED:
P2 = 0xFF & ~(0x1 << i);
拆开看:
0x1 << i:生成只有第i位为 1 的数;~(...):取反,让第i位变成 0,其他位变成 1;& 0xFF:只保留低 8 位;- 把结果写入
P2。
当 LED 低电平点亮时,只有输出 0 的那个 LED 亮,其余输出 1 的 LED 全部熄灭。i 从 0 增加到 7,就形成了流水灯。
4. 只改定时器模式,不碰其他控制位
配置定时器 0 为 16 位定时器时,可以写:
TMOD &= ~0x03;
TMOD |= 0x01;
0x03 的二进制低 8 位是:
0000 0011
第一步 TMOD &= ~0x03 把 TMOD 的最低两位清零,其他位保持不变;第二步 TMOD |= 0x01 把最低两位设置成 01,也就是定时器 0 的方式 1:16 位定时器。
为什么不直接写 TMOD = 0x01?因为这样会把 TMOD 里的其他控制位全部清掉。如果这些位还负责定时器 1 或其他功能,就会被误改。
单片机寄存器配置的原则是:
想改哪几位,就只动哪几位;其他位必须原样保留。
5. 位运算常见细节
- 位运算优先级容易记错,建议主动加括号:
REG |= (1 << n); - 移位数不能超过数据类型的位宽,例如 8 位数不要移 8 位以上;
- 对无符号数做位运算更安全;
~会翻转所有位,不只是想象中的某一位;- 修改寄存器前先想清楚是“整个赋值”还是“只改部分位”。
五、中断:让硬件事件主动叫醒程序
1. 为什么需要中断
按键检测最直接的写法是轮询:
while (1)
{
if (0 == Key1)
{
Led1 = ~Led1;
}
}
主循环一遍又一遍地读取按键状态。这种方式简单,但有两个问题:
- CPU 必须不停地检查,浪费执行时间;
- 如果主循环正在执行耗时任务,可能错过按键。
中断的思路不同:平时 CPU 正常执行主程序;当按键、电平变化、定时器溢出等事件发生时,硬件通知 CPU,CPU 暂停当前工作,转去执行一段专门的处理函数,处理完再回到刚才的位置继续执行。
这段专门处理事件的函数叫中断服务函数,英文是 Interrupt Service Routine,简称 ISR。
2. 中断执行的基本过程
一次中断大致经历以下步骤:
- 中断源产生请求,例如外部引脚出现下降沿;
- 单片机检查对应中断开关和总开关是否打开;
- 如果允许中断,CPU 保存当前执行位置;
- CPU 跳转到对应的中断向量地址;
- 执行中断服务函数;
- 中断返回,CPU 回到主程序刚才被打断的位置继续执行。
可以把中断理解成:老师正在讲课,门口有人举手说“有急事”。老师先记住自己讲到哪里,然后处理急事,处理完回来接着讲。
3. 51 单片机外部中断 0
外部中断 0 通常连接在 P3.2 引脚上。相关控制位主要有:
| 控制位 | 作用 |
|---|---|
EA | 中断总开关,置 1 才允许中断 |
EX0 | 外部中断 0 的分开关 |
IT0 | 外部中断 0 触发方式选择 |
IT0 的两种常用方式:
IT0 = 0:低电平触发;IT0 = 1:下降沿触发。
下降沿就是电平从 1 变成 0 的瞬间。按键按下时,如果引脚电平由高变低,就可以用下降沿触发。
4. 外部中断控制 LED 状态
不同的单片机有不同的状态,想要具体写一个正确的代码应当参考对应的参考手册,以如图开发板为例。

下面的程序实现:每触发一次外部中断 0,就翻转一次 LED 状态。
#include <reg52.h>
sbit Led1 = P2^0;
volatile unsigned char Led1State = 0;
void Eint0Handler(void) interrupt 0
{
Led1State = ~Led1State;
}
int main(void)
{
EA = 1; // 打开中断总开关
EX0 = 1; // 打开外部中断 0
IT0 = 1; // 设置为下降沿触发
while (1)
{
if (Led1State)
{
Led1 = 0;
}
else
{
Led1 = 1;
}
}
return 0;
}
5. interrupt 0 是什么意思
interrupt 0 是 Keil C51 的扩展语法,表示这个函数是 0 号中断的中断服务函数。
对经典 8051 来说:
- 外部中断 0 的中断号是
0; - 对应中断向量地址是
0003H; - 编译器会把中断入口安排到正确位置,并生成合适的中断返回指令。
普通函数不能随便加 interrupt 0。中断号写错,程序就会去响应别的中断源。
6. 为什么共享变量要加 volatile
Led1State 同时被两处使用:
- 中断服务函数修改它;
- 主循环读取它。
编译器优化时可能认为主循环里的变量“不可能自己变化”,于是把变量缓存在寄存器中,不再每次都从内存读取。结果中断已经改了变量,主程序却还在用旧值。
volatile 的作用是告诉编译器:这个变量可能被当前代码以外的因素改变,每次都必须重新读取,不要做危险优化。
凡是同时被主程序和中断服务函数访问的变量,都应该考虑加 volatile。
7. 中断服务函数的原则
中断服务函数不是普通任务函数,写的时候必须遵守几个原则:
- 短:能置标志就不做复杂计算;
- 快:不要写长延时,不要等待另一个事件;
- 少做 IO 之外的复杂操作:不要在 51 中断里做耗时字符串处理;
- 共享变量加
volatile; - 主程序和中断分工明确:中断负责记录事件,主程序负责处理业务。
比较推荐的模式是:
volatile unsigned char KeyEvent = 0;
void Eint0Handler(void) interrupt 0
{
KeyEvent = 1; // 中断里只记录事件
}
int main(void)
{
/* 中断配置 */
while (1)
{
if (KeyEvent)
{
KeyEvent = 0;
/* 主程序里处理按键事件 */
}
}
}
这样中断响应快,主程序结构也清晰。
六、定时器:让单片机按精确时间工作
1. 为什么需要硬件定时器
前面用循环做延时,CPU 会一直空转。硬件定时器则不同:
- 定时器按照机器周期自动计数;
- 到达设定值后产生溢出中断;
- CPU 在等待期间可以继续执行主循环;
- 定时时间由晶振和计数初值决定,更加稳定。
定时器和计数器本质上是同一个硬件:
- 定时器模式:对单片机内部机器周期计数;
- 计数器模式:对外部引脚输入的脉冲计数。
数内部固定节拍就是定时,数外部不确定脉冲就是计数。
2. 定时器 0 的方式 1
经典 8051 的定时器 0 有多种工作方式,其中方式 1 是 16 位定时器。
16 位计数范围是:
0 ~ 65535
当计数值从 65535 再加 1 时发生溢出,定时器可以向 CPU 请求中断。
16 位初值由两个 8 位寄存器组成:
TH0:高 8 位;TL0:低 8 位。
如果初值是 0x4C00:
TH0 = 0x4C;
TL0 = 0x00;
3. 定时时间怎么计算
经典 8051 定时器模式下,定时器每个机器周期加 1:
定时时间 = (65536 - 定时初值) × 机器周期
在 11.0592MHz 晶振下:
机器周期 = 12 / 11059200 ≈ 1.085us
取初值 0x4C00,十进制是 19456:
计数次数 = 65536 - 19456 = 46080
定时时间 = 46080 × 1.085us ≈ 49.9968ms
非常接近 50ms。
如果每 50ms 进一次中断,中断 20 次就是:
50ms × 20 = 1000ms = 1s
所以可以让中断里的计数器从 0 数到 20,然后翻转一次 LED。
4. 定时器 0 实现 1 秒翻转 LED
#include <reg52.h>
sbit Led1 = P2^0;
volatile unsigned char LedState = 0;
volatile unsigned char cnt = 0;
void Timer0Handler(void) interrupt 1
{
// 16 位定时器溢出后不会自动装初值,需要重新赋值
TH0 = 0x4C;
TL0 = 0x00;
cnt++;
if (20 == cnt)
{
LedState = ~LedState;
cnt = 0;
}
}
int main(void)
{
// 配置定时器 0 为方式 1:16 位定时器
TMOD &= ~0x03;
TMOD |= 0x01;
// 装入约 50ms 的定时初值
TH0 = 0x4C;
TL0 = 0x00;
ET0 = 1; // 打开定时器 0 中断
EA = 1; // 打开中断总开关
TR0 = 1; // 启动定时器 0
while (1)
{
if (LedState)
{
Led1 = 0;
}
else
{
Led1 = 1;
}
}
return 0;
}
5. 定时器配置流程
定时器 0 的配置可以记成五步:
- 选模式:配置
TMOD,选择 16 位方式; - 装初值:给
TH0和TL0写入计算好的初值; - 开中断:
ET0 = 1,允许定时器 0 中断; - 开总开关:
EA = 1; - 启动定时器:
TR0 = 1。
缺一不可。最常见的问题就是只写了 ET0 = 1,忘了 EA = 1,结果定时器一直计数,却永远进不了中断。
6. 为什么中断里要重新装初值
定时器从初值开始加 1,溢出后计数值变成 0。如果不重新装初值,下一次就不是从 0x4C00 开始,而是从 0x0000 开始,定时时间会变成完整 16 位计数的时间。
因此在 16 位非自动重装方式中,每次进入中断后都要执行:
TH0 = 0x4C;
TL0 = 0x00;
这样下一次仍然是约 50ms。
7. interrupt 1 对应定时器 0
经典 8051 中,定时器 0 溢出中断的中断号是 1:
void Timer0Handler(void) interrupt 1
{
}
中断号不能随便改。外部中断 0 用 interrupt 0,定时器 0 用 interrupt 1。写错中断号,就会出现“配置没问题,但处理函数永远不执行”的故障。
8. 软件延时和定时器中断的区别
| 对比项 | 软件延时 | 定时器中断 |
|---|---|---|
| 时间来源 | 循环执行耗时 | 晶振和计数初值 |
| 精度 | 受编译器、优化、频率影响大 | 更稳定 |
| CPU 使用 | 延时期间空转 | 等待期间可执行其他代码 |
| 多任务能力 | 差 | 好 |
| 适合场景 | 简单演示、对时间要求不高 | 周期采样、闪烁、通信超时、任务调度 |
学习单片机,要尽早从“用循环等时间”过渡到“用定时器管理时间”。
七、中断和定时器程序的整体结构
一个结构清晰的单片机程序,通常分成四层:
┌─────────────────────────────┐
│ 主循环 while(1) │
│ 处理业务逻辑 │
├─────────────────────────────┤
│ 中断服务函数 │
│ 只记录事件、更新标志 │
├─────────────────────────────┤
│ 寄存器初始化 │
│ 配置 IO、中断、定时器 │
├─────────────────────────────┤
│ 硬件 │
│ 引脚、按键、LED、定时器 │
└─────────────────────────────┘
推荐写法是:
volatile unsigned char Flag = 0;
void Timer0Handler(void) interrupt 1
{
/* 重装初值 */
Flag = 1;
}
int main(void)
{
/* 初始化 IO */
/* 初始化定时器 */
/* 打开中断 */
while (1)
{
if (Flag)
{
Flag = 0;
/* 执行具体任务 */
}
/* 主循环还能继续处理其他事情 */
}
}
这套结构以后做按键消抖、数码管扫描、串口收发、周期采样时都会用到。
八、常见错误
1. 把“最小系统”理解成操作系统
最小系统是硬件运行条件,不是 Linux、RTOS 等软件系统。单片机程序可以直接裸机运行,也可以在硬件资源足够时运行 RTOS。
2. LED 电平判断反了
许多开发板的 LED 阳极接电源,阴极接单片机引脚,所以输出低电平点亮。看到程序写 Led1 = 0 点亮 LED,不要急着改成 1,先看硬件连接。
3. 只开分开关,忘开总开关
外部中断需要同时打开:
EX0 = 1;
EA = 1;
定时器中断需要同时打开:
ET0 = 1;
EA = 1;
EA 是总闸门,总闸门不开,分开关打开也没有用。
4. 中断号写错
外部中断 0 是:
void Eint0Handler(void) interrupt 0
定时器 0 是:
void Timer0Handler(void) interrupt 1
中断号和中断源必须一一对应。
5. 共享变量没有加 volatile
主循环和中断共同使用的变量,要写成:
volatile unsigned char cnt = 0;
否则优化后可能出现中断里明明改了变量,主循环却检测不到的情况。
6. 中断函数里写长延时
中断里放 Delay1000ms(),会长时间占用 CPU,导致其他中断和主循环无法及时运行。中断里应该只做最紧急、最简短的事情。
7. 定时器溢出后忘记重装初值
16 位方式下,第一次约 50ms,第二次如果没有重装初值就可能变成完整 65536 计数。现象通常是:第一次时间正确,后面的周期突然变长。
8. 直接整体覆盖 TMOD
TMOD = 0x01;
这句虽然能配置定时器 0,但会把 TMOD 的其他控制位清掉。更稳妥的写法是:
TMOD &= ~0x03;
TMOD |= 0x01;
9. 换了晶振还沿用旧初值
同一个初值在不同晶振频率下对应不同时间。晶振从 11.0592MHz 换成 12MHz 后,机器周期和定时时间都会改变,必须重新计算。
九、单片机基础速查
1. 最小系统
| 部分 | 作用 |
|---|---|
| 电源 | 提供稳定工作电压和共同参考地 |
| 时钟 | 提供 CPU 和定时器工作的节拍 |
| 复位 | 让程序从确定的初始状态开始 |
| 下载/启动 | 把程序写入芯片,并选择正确启动位置 |
2. 三个周期
| 名称 | 经典 8051 换算 |
|---|---|
| 时钟周期 | 1 / 晶振频率 |
| 机器周期 | 12 × 时钟周期 |
| 指令周期 | 1~4 × 机器周期 |
11.0592MHz 晶振下:
时钟周期 ≈ 0.0904us
机器周期 ≈ 1.085us
3. 位运算
| 操作 | 写法 |
|---|---|
| 第 n 位置 1 | `REG |
| 第 n 位清 0 | REG &= ~(1 << n); |
| 第 n 位取反 | REG ^= (1 << n); |
| 判断第 n 位 | if (REG & (1 << n)) |
4. 外部中断 0
| 步骤 | 代码 |
|---|---|
| 打开总中断 | EA = 1; |
| 打开外部中断 0 | EX0 = 1; |
| 设置下降沿触发 | IT0 = 1; |
| 写中断函数 | void Handler(void) interrupt 0 |
5. 定时器 0
| 步骤 | 代码 |
|---|---|
| 选择 16 位方式 | `TMOD &= ~0x03; TMOD |
| 装入初值 | TH0 = 0x4C; TL0 = 0x00; |
| 打开定时器 0 中断 | ET0 = 1; |
| 打开总中断 | EA = 1; |
| 启动定时器 | TR0 = 1; |
| 写中断函数 | void Handler(void) interrupt 1 |
6. 易混淆点
| 易混淆点 | 结论 |
|---|---|
| 最小系统 vs 操作系统 | 最小系统是硬件运行条件,不是软件 OS |
| Flash/ROM vs RAM | Flash/ROM 存程序,断电保留;RAM 存变量,断电丢失 |
| 轮询 vs 中断 | 轮询是反复主动检查;中断是事件来了硬件通知 CPU |
| 时钟周期 vs 机器周期 | 经典 8051 的 1 个机器周期等于 12 个时钟周期 |
| 软件延时 vs 定时器 | 软件延时让 CPU 空转;定时器由硬件计数,更精确 |
| 主程序变量 vs 中断共享变量 | 被中断修改、主程序读取的变量要加 volatile |
小结
这一篇正式走进了单片机。先说清单片机最小系统不是操作系统,而是让芯片运行所需的最少硬件:稳定电源、时钟电路、复位电路以及程序下载和启动条件;再看单片机内部结构,CPU 负责执行指令,Flash/ROM 保存程序,RAM 保存变量,SFR 连接软件和硬件,IO、定时器、中断、串口共同构成单片机与外界打交道的能力。接着理清时间概念:时钟周期来自晶振,经典 8051 的机器周期是 12 个时钟周期,指令周期又由若干机器周期组成;在 11.0592MHz 晶振下,一个机器周期约为 1.085us。随后复习位运算,掌握置位、清位、取反、读位四个套路,就拿到了操作寄存器的钥匙。最后使用中断和定时器:外部中断让按键事件主动触发处理函数,定时器 0 通过 16 位计数实现约 50ms 中断,累计 20 次得到约 1 秒的 LED 翻转。掌握这些基础后,单片机程序就不再只是“点亮一个灯”,而是开始具备按事件响应、按时间调度的基本骨架。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)