从零开始学习嵌入式P36----单片机(1)

上一篇我们用 SQLite 给 Linux 程序加上了数据管理能力,从 C 语言、数据结构、文件 IO、进程线程、网络编程一路走到了数据库。从这一篇开始,学习场景要从“跑在操作系统上的应用程序”切换到“直接控制硬件的单片机程序”:没有 Linux,没有进程和文件系统,我们面对的是寄存器、引脚、时钟、中断和定时器。本篇先把单片机世界最重要的地基打好:弄清楚单片机最小系统是什么、单片机内部由哪些部件组成、时钟周期和机器周期怎样换算;再复习寄存器操作离不开的位运算;最后学会使用外部中断和定时器中断,让单片机从“死等”走向“按事件响应、按时间工作”。

本篇目标

学完这一篇,我们应该能够:

  • 说清单片机最小系统包含哪些硬件,以及它为什么不是操作系统;
  • 说清单片机内部的主要组成部分,理解 P0P1P2 等 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程序程序没有下载成功、启动引脚配置错误
5IO 外设引脚接错、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 单片机常见有 P0P1P2P3 等并行 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
~按位取反~a0 变 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);

拆开看:

  1. 0x1 << i:生成只有第 i 位为 1 的数;
  2. ~(...):取反,让第 i 位变成 0,其他位变成 1;
  3. & 0xFF:只保留低 8 位;
  4. 把结果写入 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. 中断执行的基本过程

一次中断大致经历以下步骤:

  1. 中断源产生请求,例如外部引脚出现下降沿;
  2. 单片机检查对应中断开关和总开关是否打开;
  3. 如果允许中断,CPU 保存当前执行位置;
  4. CPU 跳转到对应的中断向量地址;
  5. 执行中断服务函数;
  6. 中断返回,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 的配置可以记成五步:

  1. 选模式:配置 TMOD,选择 16 位方式;
  2. 装初值:给 TH0TL0 写入计算好的初值;
  3. 开中断ET0 = 1,允许定时器 0 中断;
  4. 开总开关EA = 1
  5. 启动定时器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 位清 0REG &= ~(1 << n);
第 n 位取反REG ^= (1 << n);
判断第 n 位if (REG & (1 << n))

4. 外部中断 0

步骤代码
打开总中断EA = 1;
打开外部中断 0EX0 = 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 RAMFlash/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 翻转。掌握这些基础后,单片机程序就不再只是“点亮一个灯”,而是开始具备按事件响应、按时间调度的基本骨架。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐