单片机基础知识(理论部分)
Proteus

如何创建 Keil 工程

创建步骤

1. 创建文件夹结构

  • 先创建一个文件夹 code 保存你写的所有文件
  • 创建一个按照工程目的命名的文件夹

2. 打开 Keil

  • 打开 Keil 软件

3. 创建新工程

  • 依据图中选择相应选项

  • ​​new project

  • 选择第二步创建的文件夹

  • 将文件命名为 project(我一般这样命名)

4. 创建不同类型的内容

如创建 .c 文件:

第一步
语言类型

第二步
c语言

头文件

明白头文件包含了什么

头文件包含了该系列单片机的特殊功能寄存器(SFR)定义、位定义以及部分常用常量的声明。

标题具体内容

右键点击头文件名(如 reg52.h),在弹出的菜单中选择 Open document “reg52.h”
或直接 按住 Ctrl 键,用鼠标左键点击头文件名,即可直接打开该头文件。

sfr

(Special Function Register)
语法:
语法:sfr 寄存器名 = 地址;

sbit

(Special Bit)
语法:
① sbit 位名 = 寄存器名^位号;

② sbit 位名 = 位地址;

类似于一个标签,如果用 sbit key = P1^2; 表示的就是在后续使用中,key 可以代表 P1 口的第 3 位,也可以用 sbit key = P1.2; 表示。

bit

一个特殊的数据类型,用于定义位变量。

取值范围:只能是 0(假)或 1(真)

注意

bit 是 Keil C51 编译器的扩展类型,并非标准 C 语言的一部分

主要用于 8051 单片机编程

示例:

bit flag;       // 定义一个位变量flag
bit led_state;  // 定义一个表示LED状态的位变量

延时函数(模块化)

delay.c

#include "reg52.h"

// 假设使用11.0592MHz晶振,STC89C52RC单片机
// 此函数由STC-ISP延时函数生成器生成

/**
 * @brief 微秒级延时函数
 * @param us 延时微秒数,范围:0~65535
 */
void delay_us(unsigned int us)
{
    unsigned int i;
    while(us--)
    {
        i = 2;
        while(i--);
    }
}

/**
 * @brief 毫秒级延时函数
 * @param ms 延时毫秒数,范围:0~65535
 */
void delay_ms(unsigned int ms)
{
    unsigned int i, j;
    for(i = 0; i < ms; i++)
        for(j = 0; j < 112; j++);  // 11.0592MHz下约1ms的循环
}

delay.h

#ifndef __DELAY_H__
#define __DELAY_H__

// 函数声明
void delay_ms(unsigned int ms);  // 毫秒级延时
void delay_us(unsigned int us);  // 微秒级延时

#endif

外部中断

初始化配置→编写中断服务函数 → 中断触发与执行

一、初始化配置

  • EA:总中断允许位(=1 允许总中断)
  • EX0:INT0 中断允许位(P3.2
  • EX1:INT1 中断允许位(P3.3

void External0_Init(void) {
    IT0 = 1;  // 设置INT0为下降沿触发方式
    EX0 = 1;  // 使能INT0中断
    EA = 1;   // 开总中断
}

触发方式选择(IT0 = 0 低电平触发,= 1 下降沿触发)如果不进行选择的话就是 低电平触发

二、编写中断服务函数

void External0_ISR(void) interrupt 0 {
    // 假设连接在P2.0引脚的LED,每次中断翻转其状态
    P2 ^= 0x01; 
}

外部中断函数后缀是INT0 interrupt 0

注意INT1 中断的中断号是 2(interrupt 2

关于定时器

初始化定时器 → 编写中断服务函数 → 启动定时器

一、初始化定时器

使用 TMOD 寄存器设置工作模式
常用模式:
模式 1:16 位定时器 / 计数器
模式 2:8 位自动重装载
模式比较
比较

根据晶振频率和所需定时时间计算初值
装入 THx 和 TLx 寄存器

开总中断:EA = 1
开定时器中断:ETx = 1
设置优先级 (可选):PTx = 0/1

二、编写中断服务函数

模式一

  • T:想要的定时时间(单位:秒)
  • fosc:晶振频率(单位:Hz)
  • 12:因为 51 单片机默认每个机器周期 = 12 个时钟周期
    定时时间公式:T = (2^N - 初值) × 12 /fosc

模式 1 (16 位) 初值计算:

初值 = 65536 - (T × fosc) / 12

示例:晶振 12MHz,定时 50ms

初值 = 65536 - (50000 × 12000000) / 12 = 15536
TH0 = 15536 / 256;
TL0 = 15536 % 256;

void Timer0_ISR(void) interrupt 1 {
    // 1. 重装载初值(模式1需要)
    TH0 = (65536 - 初值) / 256;
    TL0 = (65536 - 初值) % 256;
    
    // 2. 执行定时任务
    // ...
    
    // 3. 设置标志位(可选)
    timer_flag = 1;
}

模式二

自动重装载(8位)

8位模式下,初值高八位和低八位相同,即TH0=TL0=初值
自动重装载

// 定时器0初始化函数
void Timer0_Init(void) {
    TMOD &= 0xF0;   // 清除定时器0的模式(低4位)
    TMOD |= 0x02;   // 配置定时器0为模式2(8位自动重装载)
    TH0 = 156;      // 设置自动重装载的初值(高8位)
    TL0 = 156;      // 设置初始计数的初值(低8位)
    ET0 = 1;        // 使能定时器0中断
    EA = 1;         // 使能总中断
    TR0 = 1;        // 启动定时器0
}

三、启动定时器

TRx = 1;  // 启动定时器

关于中断与段号

中断号

中断注意

“中断内部不能执行时间超过 100ms,否则循环不能完成”

核心逻辑:中断的高优先级会抢占 CPU,若 ISR 耗时过长,会导致主循环 / 关键任务 “得不到 CPU 资源”,要么被频繁打断无法推进,要么因中断延迟 / 丢失导致数据异常,最终表现为 “循环不能完成”。

中断服务函数中声明的局部变量必须放在函数体的最开头,即紧接在函数左大括号后面

串口通信

什么是串口

串口对应的就是并口

要传递信息,两器件之间需要共地

核心通信参数(“波特率” 是关键)

串口通信前,收发双方必须约定好以下 4 个参数(参数不匹配会导致通信失败):

波特率(Baud Rate):数据传输的 “速度”,单位是 bps(bit per second,每秒传输的 bit 数)。常见值:9600bps、115200bps(最常用,约 11.5KB/s)、460800bps。类比:波特率就像 “说话速度”,双方必须语速一致,否则一方说太快,另一方听不清。
数据位(Data Bits):每次传输的 “有效数据位数”,通常是 8 位(1 个字节)。比如要传字符 “a”(ASCII 码 0x61,二进制 01100001),就用 8 位数据位传输。
停止位(Stop Bits):每传输完一个 “数据帧” 后,加 1~2 个 bit 的 “停止位”,用于标识 “一帧数据结束”。常见值:1 位停止位(默认)、2 位停止位(用于传输可靠性要求高的场景,如工业控制)。
校验位(Parity Bit):可选参数,用于 “检查数据是否传错”(纠错机制)。常见类型:无校验(None,最常用)、奇校验(Odd)、偶校验(Even)。比如 8 位数据位 + 奇校验:8 个数据 bit 的 “1” 的个数是偶数,就加 1 个 “1” 使总个数为奇数;若传输后 “1” 的个数不对,说明数据出错。

数据低位先发(对于串口)

举例

如果数据是0x55(0101 0101)

那么在波形图中显示应该是 1010 1010(从小往大看)

扩展

代码流程
硬件初始化 → 发送数据 → 接收数据

初始化流程(以方式 1 为例
方式 1 是最常用的:8 位异步通信波特率可变(由定时器 1 溢出率决定)。

1、初始化步骤
设置 SCON:

  1. SM0 = 0, SM1 = 1 → 方式 1
  2. REN = 1 → 允许接收
  3. 其余位(SM2、TB8、RB8)可置 0
SCON = 0x50;  // 0101 0000

2、设置波特率(用定时器 1,方式 2 自动重载):

  1. 晶振频率 fosc 已知(如 11.0592MHz)

  2. 波特率公式:(波特率 = \frac{2^{SMOD}}{32} \times
    \frac{fosc}{12 \times (256 - TH1)})

  3. 例:波特率
    9600,fosc=11.0592MHz,SMOD=0:(TH1 = 256 - \frac{fosc}{32 \times 12
    \times 波特率})(TH1 = 256 - \frac{11059200}{32 \times 12 \times 9600}
    = 0xFD)

TMOD |= 0x20; // 定时器1 方式2
TH1 = 0xFD;   // 波特率9600
TL1 = 0xFD;
TR1 = 1;      // 启动定时器1

开启中断(可以选择):

ES = 1; // 串口中断允许
EA = 1; // 总中断允许
  1. 发送数据流程
    (1)查询方式
void UartSendByte(unsigned char dat)
{
    SBUF = dat;       // 写入要发送的数据
    while(TI == 0);   // 等待发送完成
    TI = 0;           // 软件清零发送标志
}

void UartSendString(unsigned char *str)
{
    while(*str)
    {
        UartSendByte(*str++);
    }
}

(2)中断方式

unsigned char sendBuf[100];
unsigned int sendLen = 0;
unsigned int sendIndex = 0;

void UartSendString_IT(unsigned char *str)
{
    sendLen = 0;
    while(str[sendLen]) sendLen++;
    sendIndex = 0;
    SBUF = str[sendIndex++]; // 先发第一个字节
    TI = 0;
    ES = 1; // 允许串口中断
}

void UartIsr(void) interrupt 4
{
    if(TI)  // 发送中断
    {
        TI = 0;
        if(sendIndex < sendLen)
        {
            SBUF = sendBuf[sendIndex++];
        }
        else
        {
            ES = 0; // 发送完成,关闭中断
        }
    }
    if(RI)  // 接收中断
    {
        RI = 0;
        // 处理接收数据
    }
}
  1. 接收数据流程
    (1)查询方式
unsigned char UartRecvByte(void)
{
    while(RI == 0); // 等待接收完成
    RI = 0;         // 清标志
    return SBUF;    // 返回接收到的数据
}

(2)中断方式

unsigned char recvByte;

void UartIsr(void) interrupt 4
{
    if(RI)
    {
        RI = 0;
        recvByte = SBUF; // 读取数据
        // 在这里处理接收到的数据
    }
}

数码管

宏定义——>管段与位选——>显示缓冲区初始化——>数据更新函数——>动态扫描——>

宏定义

#define seg_sel P0  // 段选端口
#define dig_sel P2  // 位选端口

管段与位选

对于共阴极共阳极数码管一般有不同的管段
比如以下就是一个共阴极数码管管段

unsigned char code seg_code[] = {
    0x3F, 0x06, 0x5B, 0x4F, 0x66,  // 0-4
    0x6D, 0x7D, 0x07, 0x7F, 0x6F,  // 5-9
    0x40,  // - 10
    0x00   // 全灭 索引11
};


以下是一个八位共阴极数码管的位选,用于进行动态扫描

unsigned char code dig_code[] = {
    0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, 0x7F
};

显示缓冲区初始化

//初始化
unsigned char disp_buf[8] = {0, 0, 0, 0, 0, 0, 0, 0}; //初始设置,根据你的段码控制
//比如{0,0,10,0,0,10,0,0}表示00-00-00

数据更新函数

依据具体情况书写,就是一个格式的规定
比如说这是一个时钟的显示更新00-00-00

unsigned char hour = 0, minute = 0, second = 0;
void update_display()
{
    // 将时间变量转换为数码管显示索引
    disp_buf[0] = hour / 10;      // 小时十位
    disp_buf[1] = hour % 10;      // 小时个位
    disp_buf[3] = minute / 10;    // 分钟十位  
    disp_buf[4] = minute % 10;    // 分钟个位
    disp_buf[6] = second / 10;    // 秒钟十位
    disp_buf[7] = second % 10;    // 秒钟个位
    // disp_buf[2]和[5]固定为10,显示横杠"-"
}

它的主要作用

动态扫描

一般情况下用延时进行显示,但是主要还是使用定时器中断来控制

延时函数

void display_with_delay(void)
{
    unsigned char i;
    
    for(i = 0; i < 8; i++)
    {
        dig_sel = 0xff;           // 关闭所有数码管
        seg_sel = seg_code[disp_buf[i]];  // 送段选数据
        dig_sel = dig_code[i];    // 送位选数据
        DelayMS(2);               // 延时2ms显示
    }
}

void main()
{
    while(1)
    {
        display_with_delay();     // 数码管显示
        check_keys();             // 按键检测
        // 其他任务...
    }
}

定时器中断

void timer0_int(void) interrupt 1
{
    static unsigned char i = 0;//0-7位选
   //在这里添加你需要的变量或者flag
   
    // 重装定时器初值
    TL0 = (65536 - 2000) % 256;   // 2ms定时
    TH0 = (65536 - 2000) / 256;
    
    // 数码管扫描
   
        dig_sel = 0xff; 0   // 关闭所有位选,消除数码管拖影
        seg_sel = seg_code[disp_buf[i]]; // 送段选数据
        dig_sel = dig_code[i]; // 送位选信号    
 			 if(++i >= 8) i = 0;
	//一般会需要实现一些功能比如按键
	update_display();  //所以会有数码管段显示的更新
}

void main()
{
    // 定时器初始化
    TMOD = 0x01;
    TL0 = (65536 - 2000) % 256;
    TH0 = (65536 - 2000) / 256;
    TR0 = 1;
    ET0 = 1;
    EA = 1;
    
    while(1)
    {
        check_keys();             // 按键检测
        // 其他任务...
    }
}

lcd1602

“|”按位或运算
| 数据部分:0010 0000
| 控制部分:0000 0100
|最终结果:0010 0100

关于指令码

二进制位:第7位 第6位 第5位 第4位 第3位 第2位 第1位 第0位
LCD1602指令码的位功能核心是“标识位定类型,控制位定操作”,按指令类型简洁总结如下:
//了解就行

  1. 功能设置(0x20~0x2F)
    标识位:位7-5=001
    控制位:位4(DL:8/4位总线)、位3(N:2/1行显示)、位2(F:5×10/5×7点阵)

  2. 显示控制(0x08~0x0F)
    标识位:位7-4=0000
    控制位:位2(D:显示开关)、位1(C:光标开关)、位0(B:光标闪烁)

  3. 输入模式设置(0x04~0x07)
    标识位:位7-3=00000
    控制位:位1(I/D:光标左右移)、位0(S:屏幕是否移位)

  4. 光标/显示移位(0x10~0x1F)
    标识位:位7-4=0001
    控制位:位3(S/C:显示/光标移位)、位2(R/L:左右方向)

  5. 置DDRAM地址(0x800x9F/0xC00xDF)
    标识位:位7=1
    控制位:位6-0(A5-A0:显示位置地址,第一行0x000x1F,第二行0x400x5F)

  6. 置CGRAM地址(0x40~0x5F)
    标识位:位7-6=01
    控制位:位5-0(A5-A0:自定义字符存储地址)

  7. 读忙标志
    触发:RS=0、RW=1
    核心位:位7(BF:1=忙,0=空闲)+ 位6-0(当前地址)

  8. 写/读数据
    写:RS=1、RW=0(数据写入DDRAM/CGRAM)
    读:RS=1、RW=1(从DDRAM/CGRAM读取数据)

RS

用于控制 RS(寄存器模式) 的位是最低位(第 0 位)

RS=0 → 命令模式(向 LCD 发送控制指令,如清屏、设置显示模式等);
0x04(00000100):第 0 位 = 0 → RS=0(命令模式)

RS=1 → 数据模式(向 LCD 发送要显示的字符数据,如 ‘A’、‘B’ 或字符串)。
0x05(00000101):第 0 位 = 1 → RS=1(数据模式)

E

用于控制E(使能信号)的位是第2位

E=1(使能高电平):用 |0x04 实现(0x04 二进制 00000100)
E=0(使能低电平):用 &0xFB 实现(0xFB 二进制 11111011)

接口扩展


比如说74LS373是一种常见的“地址锁存器”:解决总线分时复用问题
38译码器则是常见的“地址译码器”:将地址信号转换为设备选择信号

三总线

1.1 数据总线(DB - Data Bus)

宽度:8位双向
端口:P0口
特点:分时复用(地址/数据)
负载能力:可驱动8个TTL负载

1.2 地址总线(AB - Address Bus)

宽度:16位(最大64KB寻址)
组成:
P0口:低8位地址(A0-A7)
P2口:高8位地址(A8-A15)
寻址范围:0000H-FFFFH

1.3 控制总线(CB - Control Bus)

主要信号:

信号 全称 功能描述
ALE Address Latch Enable 地址锁存使能,锁存低8位地址
PSEN Program Store Enable 程序存储使能,读取外部程序存储器
EA External Access 外部访问使能,选择内部/外部程序存储器
RD Read 外部数据存储器读使能
WR Write 外部数据存储器写使能
EA

功能:程序存储器选择
EA = 1:先访问内部ROM,超过4K后访问外部ROM
EA = 0:只访问外部程序存储器

PSEN和RD
PSEN(自动读指令)
  • 程序存储器访问
    用途:读取指令代码
    时序:每个机器周期有效两次
    对应指令:所有指令取指周期
RD(受movx控制)
  • 数据存储器访问
    用途:读取数据
    时序:执行MOVX指令时有效
    对应指令:MOVX A, @DPTR 等

重要区别:
PSEN访问程序空间(64KB)
RD访问数据空间(64KB)
两者独立,地址空间重叠但通过不同信号区分

WR (受movx控制)
  • 数据存储器写入
    用途:向外部数据存储器或外设写入数据
    时序:执行MOVX写指令时有效
    对应指令:MOVX @DPTR, A 或 MOVX @Ri, A

MOVX指令与控制信号的对应关系

MOVX指令:用于访问外部数据存储器

读外部数据存储器
MOVX A, @DPTR ; 产生RD信号
;写外部数据存储器
MOVX @DPTR, A ; 产生WR信号

读外部程序存储器(自动)
执行任何指令时,PSEN自动有效
在这里插入图片描述

接口芯片

74LS273
7432并
38译码器()
74ls373

代码部分

扩展端口编程 = 地址定义 + 数据准备 + 时序控制

扩展端口地址定义

unsigned char xdata ADDR1 _at_ 0xFFFE;
unsigned char xdata ADDR2 _at_ 0xFFFD;
unsigned char xdata ADDR3 _at_ 0xFFFB; 
unsigned char xdata ADDR4 _at_ 0xFFF7;

xdata 表示这是外部数据存储器地址
at 指定具体的硬件地址
这些地址对应硬件译码器输出的片选信号
功能重定义(提高可读性)
相当于就是一个备注

#define seg_sel1 ADDR1  // 数码管1段选
#define dig_sel1 ADDR2  // 数码管1位选  
#define seg_sel2 ADDR3  // 数码管2段选
#define dig_sel2 ADDR4  // 数码管2位选

数据准备部分

1、显示数据缓冲区(相关显示的初始化
// 原始数据存储
unsigned char disp_buf1[8] = {2,0,2,5,1,1,1,8};
unsigned char disp_buf2[8] = {1,2,10,5,8,10,3,0};

// 数据处理示例
void process_data(void)
{
    // 时间数据转显示数据
    disp_buf1[0] = hour / 10;      // 时的十位
    disp_buf1[1] = hour % 10;      // 时的个位
    disp_buf1[2] = 10;             // 分隔符"-"
    disp_buf1[3] = minute / 10;    // 分的十位
    // ...
}
2、编码转换表
// 七段数码管编码(共阴极)
unsigned char code seg_code[] = {
    0x3F, // 0 - 00111111
    0x06, // 1 - 00000110  
    0x5B, // 2 - 01011011
    // ...
    0x40  // - - 01000000
};

// BCD码转七段码
unsigned char bcd_to_7seg(unsigned char bcd)
{
    return seg_code[bcd & 0x0F];  // 只取低4位
}
3、状态数据的维护
// 系统状态变量
unsigned char current_mode = 0;    // 当前模式
unsigned char blink_flag = 0;      // 闪烁标志
unsigned char edit_position = 0;   // 编辑位置

// 数据有效性检查
if(hour > 23) hour = 0;
if(minute > 59) minute = 0;

时序控制部分

延时
定时器延时
动态扫描时序
状态机控制
按键消抖时序
定时器中断时序

进行模块化

单独创建一个文件夹储存模块化文件

.c和.h,当需要是复制到相关文件夹下,在将其添加到列表
添加

注意!!!!

一定要看是不是在一个路径下面,否则很有可能找不到相关文件

路径

错误与警告

​​警告

有函数未被调用
WARNING LI6: UNCALLED SEGMENT, IGNORED FOR OVERLAY PROCESS

面对类似警告,是因为有的函数未被调用,可以不用理会,忽略就好了


Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐