51单片机

硬件:硬件调试工具(示波器、万用表、逻辑分析仪)
编程工具:Keil4
下载工具:STC-ISP

安装Keil:安装路径必须是英文路径,不能出现中文。Keil4和keil5不要放在同一个路径下。

1.先创建工程

2.再向工程中添加.c文件(必须和工程在同一目录)

STC-ISP 是 给 STC 系列 51 单片机下载/烧录程序(.hex) 的上位机软件,也常被用来做一些串口相关的调试配置。

把开发板的USB插上电脑之后,在设备管理器查端口号。

在这里插入图片描述

51 单片机的来源与生态:
1980年,Intel公司推出MCS-51系列:8051型号单片机
8051 是一种经典的 8 位 MCU(微控制器)架构/指令集体系。
许多公司推出了兼容 8051 内核的芯片,因此统称“51 单片机/8051 系列”:
• Atmel(AT89 系列等)
• STC(STC89、STC12/15 等)
• Philips/NXP(早期 8051 系列)
CPU:Central Processing Unit,中央处理器,完成数据运算、指令处理
Hz:每秒振荡/时钟次数
1 MHz = 10^6 Hz
1 GHz = 10^9 Hz

时钟周期:T = 1 / f
例:2.1 GHz
• T = 1 / 2.1×10^9 s ≈ 0.476×10^-9 s
• ≈ 0.476 ns(纳秒)

s(秒)
ms(毫秒)= 10^-3 s
us(微秒)= 10^-6 s
ns(纳秒)= 10^-9 s
1、处理器架构(ISA)是啥?
ISA(Instruction Set Architecture,指令集架构)
架构/指令集(ISA):CPU 能理解和执行的指令体系与寄存器模型。
不同 ISA 的程序一般不能直接互跑(要重新编译,甚至完全不能兼容)。

常见架构举例
x86:常见为 32 位时代的 PC 指令集
x86_64:x86 的 64 位扩展(现代 PC 常见)
ARM(常见 32 位/64 位):手机、嵌入式常见
MIPS:教学/路由器等领域出现过
8051(8 位):单片机入门经典架构

51单片机的架构

51 单片机的最小系统:
最小系统 = 电源 + 晶振 + 复位 
最小系统:让 MCU(单片机) “能稳定运行程序”所必须的最少硬件连接。
供电(VCC / GND)、时钟(OSC:晶振/内部振荡)、复位(Reset / RST)

在这里插入图片描述

在最小系统能跑起来后,再一步步接外设:LED、按键、蜂鸣器、数码管、串口模块等。

核心芯片内部(MCU 内部模块)
8051 CPU(或者说Kernel 内核)

RAM (Random Access Memory,随机访问内存)(数据存储器):存放变量、临时数据、栈(掉电丢)。一般用256byte

ROM (Read - Only Memory,只读存储器)存放单片机程序、代码、指令,掉电数据不丢失  在程序运行期间,ROM是只读的。

TIMER(定时器/计数器):用于:精准延时(比死循环更可靠)、周期任务(比如动态数码管扫描)、计数外部脉冲等

GPIO(通用输入输出):连接到外部:LED、按键、蜂鸣器、数码管、继电器等
GPIO 本质:一组寄存器 + 驱动电路
写寄存器→引脚输出高/低;读寄存器→读取引脚电平

UART(串口)
用于与电脑/模块通信:下载、打印调试信息、蓝牙/WiFi 模块通信等

因为变量的地址是随机的,变量存放在RAM中,CPU要访问RAM中的变量来进行指令操作,所以说RAM对于CPU来说是随机访问内存。

在这里插入图片描述

芯片外部(开发板外设)
LED:通常连接到某个端口(如 P2.x),常见“低电平点亮”(取决于电路)
KEY(按键):输入,常见上拉/下拉电阻,按下电平翻转
BEEP(蜂鸣器):输出,可能需要三极管驱动(IO 直接带不动时)
芯片 + 最小系统 + 外围器件 才是你手里那块“51 开发板系统”。

把PC 的 RAM/外存类比到 MCU

在这里插入图片描述

重要概念

RAM :Random Access Memory, 随机访问内存,存放程序中的变量、临时数据掉电数据丢失,读写速度快(256byte)

ROM :Read - Only Memory,只读存储器,存放单片机程序、代码、指令,掉电数据不丢失

MCU:Micro Controller Unit,微控制器,集成度高,将所有功能集成到一块芯片里(CPU、RAM、ROM、GPIO、TIMER、UART),成本低,简单控制

MPU:Micro  Processing Unit,微处理器,集成度低,只有一块单独的CPU,需要外接一些功能模块(RAM、ROM、控制器芯片),成本高,应用处理复杂任务,可以跑Linux操作系统

GPU:Graphics Processing Unit, 图形处理单元,处理图形数据,图像渲染

NPU:Neural Processing Unit,神经网络处理单元,AI推理、硬件加速(Mate 60 达芬奇NPU AI拍照优化)

FPU:Float Point Unit,浮点数运算单元,完成浮点数运算

SOC:System On Chip,片上系统,将多个芯片集成到一起
GPU 和 显卡是“零件 vs 整块板/整套系统”的关系:
1)GPU 是什么
GPU(Graphics Processing Unit)= 图形处理器芯片
主要负责并行计算/图形渲染:把图形数据处理成可显示的结果(也可做通用计算,如 CUDA/OpenCL 等)
它本质上是一颗核心芯片,自己单独放桌上不能直接用来输出画面(还需要配套电路、显存、供电、接口等)

2)显卡是什么
显卡(Graphics Card/Video Card)= 一整块用于显示输出的硬件模块
显卡里通常包含:
    GPU 芯片
    显存(VRAM):存纹理、帧缓冲等
    供电/电源管理(VRM):把电源转换成 GPU/显存需要的电压电流
    散热系统:散热片/风扇/热管
    显示输出接口:HDMI/DP 等
    以及 PCB、电容电感等外围

在这里插入图片描述

C语言 位运算

&:按位与: 将对应位的bit位进行比较,若两个bit都为1,结果为1,若有一个bit为0,结果为0。“全1为1,有0为0”
      int  a = 3;
      int  b = 5;
      a & b;      0000 0011     &   0000 0101     0000 0001
| :按位或:将对应位的bit进行比较,若有一个bit为1,结果为1, 若两个bit都为0, 结果为0。“全0为0”
      int  a = 3;
      int  b = 5;
      a | b;      0000 0011     |   0000 0101      0000 0111
^: 按位异或:将对应位的bit进行比较,若两个bit相同,结果为0;不相同为1。(相同为0,相异为1)
      int  a = 3;
      int  b = 5;
      a ^ b;      0000 0011     ^   0000 0101       0000 0110
     char t = 0;           // 0000 0000   // 0000 0010
  	 0000 0000 | 0000 0010  -> 0000 0010
     t = t |  (1 << 1);                // 0000 0010
     
     t = t | (1 << 7);                //1000 0000
     
     t |= (1 << 2) | (1 << 6);  
     1 << 2:把 1 左移 2 位
    0000 0001 -> 0000 0100(十六进制 0x04)
    1 << 6:把 1 左移 6 位
    0000 0001 -> 0100 0000(0x40)
    (1 << 2) | (1 << 6):把这两个掩码合并
    0000 0100 | 0100 0000 = 0100 0100(0x44)
    
其余位不变,指定位置1:使用按位或 t  |=  (1 << n);
 char t = 0xFF;                  // 1111 1111   -> 1111 1110

其余位不变,指定位清0: 使用按位与  t &= ~(1 << n)
 t &  1111 1110
 t = t & ~(1 << 0)             // 1111 1110

38译码器工作原理

在这里插入图片描述

38译码器(3→8Decoder)的工作原理:用3根输入线(A、B、C)的二进制组合,选通8个输出中的某一个。

74HC138有3个使能端:
G1:高有效(要为1)
\G2A、\G2B:低有效(要为0) (有头上那一横杠,就是表示这个位低电平有效)
只有当G1=1 且 \G2A=0 且 \G2B=0 时,芯片才“打开”,开始译码。
否则芯片关闭:所有输出都为高电平(因为输出是低有效。
在这里插入图片描述

在这里插入图片描述

数码管动态显示的原理

在这里插入图片描述

段选+位选配合
段选:决定这一位显示“几”(a~g、dp哪些段亮)
位选:决定“哪一位被点亮”
动态显示就是:位选轮流开,段码同步切换。
数码管动态显示是通过段选输出段码、位选依次选通各位,使每一位在短时间内显示其对应
数字,并以较高频率循环扫描,利用人眼视觉暂留使人感觉多位同时稳定显示。

动态显示:在某位数码管上显示一位数值,快速刷新,再下一位数码管再去显示另一位数值,
利用人眼视觉暂留效应使人感觉多位同时稳定显示。
(视觉暂留效应、余晖效应)

#include <reg51.h>
#include "delay.h"

//0-9  段码表
unsigned char seg_table[] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};

//存放数码管显示的数值
unsigned char display_buf[8] = {0,0,0,0,0,0,0,0};


//位选
void Bit_Select(unsigned char n)
{
	//将P2 2  P2 3  P2 4清零
	P2 &= ~(7 << 2);

	switch(n)
	{
		//A:P2 2   B:P2 3  C:P2 4
		case 1:	             //Y0  A:0 B:0 C:0
			P2 &= ~(1 << 2);
			P2 &= ~(1 << 3);
			P2 &= ~(1 << 4);
			break;

		case 2:	             // Y1 A:1 B:0 C:0
			P2 |= (1 << 2);
			P2 &= ~(1 << 3);
			P2 &= ~(1 << 4);
			break;

		case 3:	             // Y2 A:0 B:1 C:0
			P2 &= ~(1 << 2);
			P2 |= (1 << 3);
			P2 &= ~(1 << 4);
			break;

		case 4:	             // Y3 A:1 B:1 C:0
			P2 |= (1 << 2);
			P2 |= (1 << 3);
			P2 &= ~(1 << 4);
			break;

		case 5:	             // Y4 A:0 B:0 C:1
			P2 &= ~(1 << 2);
			P2 &= ~(1 << 3);
			P2 |= (1 << 4);
			break;

		case 6:	              // Y5 A:1 B:0 C:1
			P2 |= (1 << 2);
			P2 &= ~(1 << 3);
			P2 |= (1 << 4);
			break;

		case 7:	              // Y6  A: 0 B: 1 C: 1
			P2 &= ~(1 << 2);
			P2 |= (1 << 3);
			P2 |= (1 << 4);
			break;

		case 8:	              // Y7 A:1 B:1 C:1
			P2 |= (1 << 2);
			P2 |= (1 << 3);
			P2 |= (1 << 4);
			break;

		default:
			break;
	}
}

//段选 (数码管显示的数字)
void Seg_Select(unsigned int n)
{
	P0 = seg_table[n];
}

//将显示的数据存入到数组
void num_to_buff(unsigned long n)
{
	int i = 0;

	//数组清零
	for(i=0; i < 8; i++)
	{
		display_buf[i] = 0;
	}

	//将数码管显示的数据存放到数组
	i = 0;
	while(n != 0 && i < 8)
	{
		display_buf[i++] = n%10;
		n /= 10;
	}
}

//数码管动态显示
void digiter_show(void)
{
	static int pos = 1;

	P0 = 0;
	//位选
	Bit_Select(pos);

	//段选
	if(display_buf[pos-1] != 0)
	{
		Seg_Select(display_buf[pos-1]);
	}
	delay(100);

	pos++;
	if(pos > 8)
	{
		pos = 1;
	}
}

GPIO

GPIO = General Purpose Input/Output,通用目的输入输出

GPIO 是可编程的通用数字引脚,既能当输入也能当输出

GPIO输入:检测引脚电平变化
             悬空输入
             上拉输入
             下拉输入
              模拟输入
GPIO输出:给定引脚高电平/低电平,控制引脚输出高电平/低电平
            推挽输出
            开漏输出

中断

中断概念:当CPU在执行一个任务时,此时外界来了一个更为紧急的任务,要求CPU 能够暂停当前的任务,转而去执行更为紧急的任务,执行完之后再回到刚才被打断的 地方继续向下执行。

中断源:能够打断CPU执行当前任务源头/事件叫做中断源

51单片机:5个中断源(我使用的这款单片机只有5个中断源) 外部中断0 外部中断1 定时器0 定时器1 串口

外部中断:引脚电平的变化所引发的中断

中断优先级:CPU处理中断任务时,需要比较中断优先级,优先去处理优先级更高的 中断请求

中断嵌套:CPU再处理一个中断任务时,嵌套处理其他优先级更高的中断任务(51单片机最多 允许嵌套两层)

中断处理流程:

1. 中断源发起中断请求
2. 检查CPU是否允许中断及中断源是否被屏蔽
3. 比较中断优先级
4. 保护现场
5. 执行中断服务函数
6. 恢复现场

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

外部中断0

现在,我要使用外部中断0。

首先,查原理图,外部中断0连接P32。

在这里插入图片描述
在这里插入图片描述

可以用按键K3检测外部中断0。

然后查手册,看要使用外部中断0需要哪些条件。

在这里插入图片描述

设置触发方式为下降沿触发,按一次K3按键,来一个下降沿,外部中断0触发

在这里插入图片描述

至此,软件方面需要我们初始化的条件都查到了,下面是使用外部中断0的流程。

在这里插入图片描述

在这里插入图片描述

#include <reg51.h>
#include "key.h"

unsigned int g_n = 0;

//外部中断0 中断服务函数
void int0_handler(void)  interrupt 0
{
	g_n ++;
}

// 外部中断0  初始化函数
void int0_init(void)
{
	// 将外部中断0引脚拉高
	P3 |= (1 <<2);

	//打开CPU中断总开关
	IE |= (1 << 7);

	//开启外部中断0子开关
	IE |= (1 << 0);

	//设置外部中断0触发方式:下降沿方式
	TCON |= (1 << 0);

}

中断向量表:

中断向量表:本质上是一个数组,数组中存放的是中断服务函数的入口地址

中断向量:中断向量表中各中断服务函数的标号

在这里插入图片描述

定时器0:16位自增型定时器

定时器:能够产生一个精准的定时,对时序要求严格(高电平和低电平的时间必须是 精准的)

下面这幅图,横轴是时间,纵轴是电压。

在这里插入图片描述

定时器是以时间先后次序,描述引脚上电压变化
我使用的这款开发板有两个定时器,Timer0、Timer1
这是16位自增型定时器/计数器  :0 - 65535
从初值开始++,+到65535溢出后向CPU发起定时器中断请求
	初值大小
	自增速率
Clock:时钟,CPU运行的速率

定时 1ms 的初值计算方法:
晶振频率:11.0592MHZ
分频器:可以将晶振提供频率 / 分频系数,降低频率
51单片机工作频率:11.0592MHZ / 12 = 0.9216MHZ
++所需要的时间: 1 / 0.9216MHZ = 1.085us
1ms定时:1ms = 1000us
计数次数:1000us / 1.085us = 922
初值:65535 - 922 = 64,613
unsigned  short count = 64613;
void timer0_init(void) 定时器0初始化函数
配置定时器工作模式:16位(TMOD寄存器中的bit0置1)
将定时器初值装入到TL0和TH0
允许定时器0开始计数(TCON寄存器中的TR0置1)
允许CPU响应所有中断(IE寄存器中的EA位置1)
允许定时器0产生中断(IE寄存器中的ET0位置1)

在这里插入图片描述

#include <reg51.h>
#include "timer.h"
#include "led.h"

unsigned int g_i = 0;

//外部中断0服务函数
void timer0_handler(void)  interrupt 1
{
	TH0 = g_i >> 8;
	TL0 = g_i;
	P2 =  P2 ^ (1 << 5);
}

//定时器0初始化函数
void timer0_init(void)
{
	//1.将TMOD寄存器低4位清零
	TMOD &= ~(0x0F << 0);

	//2.设置定时器工作模式:16位
	TMOD |= (1 << 0);

	//3.设置定时器处置  64613
	TH0 = g_i >> 8;
	TL0 = g_i;

	//4.开启定时器计数
	TCON |= (1 << 4);

	//5.CPU响应所有中断(总开关)
	IE |=(1 << 7);

	//6.允许定时器0产生中断(子开关)
	IE |= (1 << 1);
}
8位自动重装定时器:实际参与计数的只有TL0,当溢出后会将TH0的值再重新装回到TL0
TL0     230          0 - 255
TH0    230

PWM波

PWM:脉冲宽度调制,能够产生一个方波,能够让引脚电平周期性发生变化
PWM周期:一个方波所经历的时间(从上升沿/下降沿到下一个上升沿/下降沿 )
PWM占空比:高电平在一个周期内所占的比例

蜂鸣器

蜂鸣器:
声  -> 波 -> 音调 -> 频率   高音 高频  低音 低频  音量 -> 波的振幅 能量大小
有源蜂鸣器:存在震荡源,通电后会持续发出固定频率声音
无源蜂鸣器:不存在震荡源,通电后不会发出声音,需要给定一个震荡

蜂鸣器“响”的本质是:让蜂鸣器里的振动片(压电片或小喇叭结构)做周期性振动,推动空气形成声波。周期越短(频率越高)听起来音调越高;周期越长音调越低。

为什么需要定时器?——因为要产生“稳定的频率”
定时器是“定时”的。关键是:
要让引脚 每隔固定时间翻转一次(0变1、1变0、1变0…)
这就形成了一个周期性方波
翻转间隔 = 半个周期
举例:想要 400Hz 的方波:
周期 T=1/400=2.5ms
半周期 T/2=1.25ms
所以要每隔 1.25ms 触发一次中断,在中断里把蜂鸣器引脚取反
引脚就会输出 400Hz 的方波(近似 50% 占空比)

计算200HZ 频率 占空比 50%

时间:1 / 200HZ = 0.005s
定时器定时时间:0.005s / 2 = 0.0025s
单片机工作频率:11.0592MHZ  / 12 = 0.9216MHZ
0.9216MHZ * 10^6 = 921,600HZ
完成一条指令所需时间:1 / 921600HZ = 0.000001085s
计数次数:0.0025s / 0.000001085s = 2,304
初值:65535 - 2304 = 63,231

例子——4个按键控制蜂鸣器4种不同频率的声音

在这里插入图片描述
在这里插入图片描述

蜂鸣器连接P25口。

//main.c的代码
#include <reg51.h>
#include "timer.h"
#include "key.h"

#define HZ_200 63231
#define HZ_400 64382
#define HZ_600 64767
#define HZ_800 64959


int main(void)
{
	timer0_init();
	key_init();
	
	while(1)
	{
		int ret = key_press();
		switch(ret)
		{
			case 1:	g_i = HZ_200;break;
			case 2:	g_i = HZ_400;break;
			case 3:	g_i = HZ_600;break;
			case 4:	g_i = HZ_800;break;
			default: break;
		}
	}

	

	return 0;
}
//timer.c的代码
#include <reg51.h>
#include "timer.h"
#include "led.h"

unsigned int g_i = 0;

//unsigned int g_i = 0;

//外部中断0服务函数
void timer0_handler(void)  interrupt 1
{
	TH0 = g_i >> 8;
	TL0 = g_i;
	P2 =  P2 ^ (1 << 5); //定时器溢出时翻转P2.5引脚,从而输出方波
}

//定时器0初始化函数
void timer0_init(void)
{
	//1.将TMOD寄存器低4位清零
	TMOD &= ~(0x0F << 0);

	//2.设置定时器工作模式:16位
	TMOD |= (1 << 0);

	//3.设置定时器处置  64613
	TH0 = g_i >> 8;
	TL0 = g_i;

	//4.开启定时器计数
	TCON |= (1 << 4);

	//5.CPU响应所有中断(总开关)
	IE |=(1 << 7);

	//6.允许定时器0产生中断(子开关)
	IE |= (1 << 1);
}
//key.c的代码
#include <reg51.h>
#include "key.h"

unsigned int g_n = 0;

//外部中断0 中断服务函数
void int0_handler(void)  interrupt 0
{
	g_n ++;
}

// 外部中断0  初始化函数
void int0_init(void)
{
	// 将外部中断0引脚拉高
	P3 |= (1 <<2);

	//打开CPU中断总开关
	IE |= (1 << 7);

	//开启外部中断0子开关
	IE |= (1 << 0);

	//设置外部中断0触发方式:下降沿方式
	TCON |= (1 << 0);

}
//按键初始化
void key_init(void)
{
	//P3 |= (0x0F << 0);
	P3 |= 0x0F; // 0x0F = 0000 1111
}

//按键检测
int key_press(void)
{
	int ret = 0;

	if((P3 & (1 << 1)) == 0)  // P31 —— KEY1  P31为0,表示KEY1按下
	{
		ret = 1;
	}
	else if((P3 & (1 << 0)) == 0)  // P30 —— KEY2
	{
		ret = 2;
	}
	else if((P3 & (1 << 2)) == 0)  // P32 —— KEY3
	{
		ret = 3;
	}
	else if((P3 & (1 << 3)) == 0)  // P33 —— KEY4
	{
		ret = 4;
	}

	return ret;
}
Logo

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

更多推荐