当前位置:   article > 正文

江科大51单片机入门学习笔记合集_51单片机江科大教程下载

51单片机江科大教程下载

博主文档下载

最近有很多童鞋来问需要文档的事情。博主也很开心自己的记录能被大家认可。所以最近就提取出来了 pdf 的永久链接,链接如下:
【超级会员V3】通过百度网盘分享的文件:51单片机入门_…
链接:https://pan.baidu.com/s/14bsuiwUYGbWqq3ZWOuuu1A?pwd=rsy8
提取码:rsy8
复制这段内容打开「百度网盘APP 即可获取」
另外,博主现在也是在学习嵌入式的阶段,有感兴趣想一起学习的同学可以加博主的公众号一起交流沟通~(公众号名:灰海宽松,见文章底部)
本文档仅为博主个人学习江协科技51单片机课程过程中记录的学习笔记,未做任何商业用途,侵删!

软件下载

开发:Keil。如果想要没乱码的中文注释,那么设置编码方式为 UTF8 或 GB2312。

程序文件下载到单片机:STC/普中(STC需要冷启动,先点击下载再开启单片机电源)

介绍

Micro Controller Unit, MCU 单片机,其中包含了CPU RAM ROM 输入输出设备 等一系列电脑硬件常用功能。

功能:通过传感器采集数据,通过CPU处理数据,控制硬件。

可以说是一个性能低的小电脑,是了解计算机原理的很好的学习方法。

右上角的跳线帽使用数码管时跳到VCC,使用点阵时跳到GND。

STC89C52RC 命名规则

STC:芯片为 STC 公司生产的产品。

8:该芯片为 8051 内核芯片。

9:表示内部含有 Flash EEPROM 存储器,还有如 80C51 中 0 表内部含有 MaskROM(掩模 ROM)存储器;如 87C51 中 7 表示内部含有 EPROM(紫外线可擦除 ROM)存储器。

C–表示该器件为 CMOS 产品。还有如 89LV52 和 89LE58 中的 LV 和 LE 都表示 该芯片为低电压产品(通常为 3.3V 电压供电);而 89S52 中 S 表示该芯片含有 可串行下载功能的 Flash 存储器,即具有 ISP 可在线编程功能。

5–固定不变。

2:表示该芯片内部程序存储(FLASH)空间大小,1 为 4KB,2 为 8KB,3 为 12KB,即该数乘以 4KB 就是芯片内部的程序存储空间大小。程序空间大小决定了 一个芯片所能装入执行代码的多少。一般来说,程序存储空间越大,芯片价格也 越高,所以我们再选择芯片的时候要根据自己需求选择合适芯片。 RC–STC 单片机内部 RAM(随机读写存储器)为 512B。还有如 RD+表示内部 RAM 为 1280B。还有芯片会省略此部分

image-20220828144814559

芯片介绍

芯片在 PDIP 里。黑色的部分 PDIP 是一种封装方式,可能还有 LQFP 等(一个正方形的形状)封装方式。

8051 内核基本上都是中间绿色块的样子,只是外设、封装等方式不同。

image-20220828145334929

管脚图:

Vcc 是电源,XTAL 管时钟,RST 是复位,等等。

image-20220828145345268

image-20230125214429628

整个是一个总线结构,所有外设都挂在上面。如最下面一行左边是晶振,右边是外部引脚。

只有这一个单片机是不能运行的,看我们的开发板上面还外接了好多好多外设呢。能让单片机运行的最小应用系统如下:

image-20230125214745143

三角是正极,三线符号是负极。

首先需要 Vcc 接正,GND 接负。

然后需要接晶振。没有晶振单片机程序无法一条条往下执行,有了晶振按照固定的周期才能一条条往下执行。晶振就是板子上银色的椭圆形的一个东西,频率写在上面,一般是有12MHz和11.多MHz的两种(有的芯片自带晶振。不过很明显我们的芯片并不自带)。

然后还有复位电路,让程序回到第一条的位置。

开发板介绍

image-20220903113237968

中间黑色的是刚刚介绍的单片机。拉起拉杆,可以取下单片机,但放回时一定不能放反。单片机有缺口的一端左侧从01开始,逆时针逐渐增大到40。

右侧中间有8个 LED 灯,我是点灯大师!

下面是一个矩阵按键,用户可以通过按按键输入。

最下面一行右侧有个红外接收传感器,接收红外线的。

左边无线模块,8个插孔的,做无线模块(如2.4G)用的。

再左边四个独立按键。

最左下是 USB 自动下载模块,插上 USB 线后按开关就会自动下载程序,不用了解。

DS1302 时钟芯片,可以做一个小时钟,读取时间。

红色按钮是 RST 按钮。

AD/DA 模数转换器,使单片机在数字与模拟领域之间转化。

74H595 可以扩展出更多的 IO 口。

步进电机可以精确控制脚步(转一圈、转半圈)。比如空调会用。

超声波模块可以测距。

蜂鸣器模块可以放歌。但我()()()()。

138译码器控制数码管,也可以扩展 IO 口。

24c02 也是一种 ROM,还是 EEP ROM(掉电不丢失)。其实单片机自带的 Flash ROM 更先进,但是只能用来存储程序。

温度传感器可以用来检测温度。

74HC245 可以驱动数码管(我的单片机是 HC138)。

左上角的电位器和排座用于接显示屏。电位器可以调整显示屏的亮度。

最大的黑色方阵是一个 LED 点阵。可以点亮8*8的方阵,甚至用来做动画。

之后的课程中还会详细介绍每一个模块,以及对应的电路图。

逻辑运算

&与,|或,!非,⊙同或(相同结果才=1),异或⊕(不同结果才为1)

C语言语法

int 16位,char 8位。

image-20220904115124141

image-20220904115224027

基本语法其他的都好说,再复习一下位运算。

image-20220904145323095

左右移补0.

位运算符也可以参与成为复合赋值运算符,如^=, <<=

逗号运算符=最后一个表达式的值

image-20220904145638619

函数在C语言基础上做的拓展

重入函数

在函数形参括号后加修饰符 reentrant,代表这个函数是重入函数,可以被递归调用,但这样就不能有bit变量,也不能进行位运算。

中断函数

在函数形参括号后加修饰符 interrupt m,系统编译时把对应函数转化为中断函数,自动加上程序头段和尾段,并按 51 系 统中断的处理方式自动把它安排在程序存储器中的相应位置。

在该修饰符中,m 的取值为 0~31,对应的中断情况如下:

0——外部中断 0

1——定时/计数器 T0

2——外部中断 1

3——定时/计数器 T1

4——串行口中断

5——定时/计数器 T2

其它值预留。

外部函数

如果要调用的函数不在本文件内,在其他文件内,定义函数时函数开头要加 extern 修饰符。

sfr sbit

用于定义特殊功能寄存器或特殊位。

sfr P0=0x80;//把地址 0x80 处的寄存器定义为 P0
sbit P0_1=P0^1;//取第一位定义为 P0_1
  • 1
  • 2

其实头文件 regx52.h 中都有。

能不能给位单独赋值要看是不是可位寻址。因为物理地址有限,每8个寄存器只能有一个可位寻址。

51单片机最小系统组成

  • 晶振电路,提供时钟,相当于心脏
  • 复位电路,系统运行不正常时可以重启
  • 电源电路,注意单片机的供电电压要求
  • 下载电路,烧入程序

另外注意,单片机的P0口是漏级开路,输出高电平会导致高阻态,因此输出高电平时要接上拉电阻,通常选择 4.7K~10K 阻值。

程序编写前言

新建项目 new μversion project

选择 CPU 型号:Keil 中没有完全对应的 STC89C52 版本,用Atmel 中的 AT89C52 即可,不用把8051启动文件添加到工程中。

AT 和 STC 是两种型号的单片机。有的 STC 单片机上面还有 AT 接口,AT 使用那个接口烧录程序。STC 就用 USB 下载。

新建好后有一个文件夹:source group,代码文件都在其中。

选中该文件夹,右键新建new item,新建c语言文件。可以选c/cpp/asm

在魔术棒 Output 选项中添加 “ create HEX file".

程序框架

#include "reg52.h"
void main()
{
    while(1)
    {
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

编译:translate按钮

建立:build按钮,也有编译的作用,只编译发生变动的文件。

重新建立:rebuild,编译所有文件(速度慢不建议)。

报错如果显示:缺少root segment根段,即没有找到主函数。

头文件作用

#include<reg52.h>和`#include “reg52.h"都可以。区别在于<>直接去软件安装处搜索头文件,而”"先在该项目下查找头文件,找不到再去软件安装处,再找不到就报错。

查看头文件可以在左侧的结构树对应的c文件目录下打开,或者右键“reg52.h" open 打开。

该头文件中定义了52单片机内部所有功能寄存器,把地址值如0x80赋值给P0等端口。

程序烧录

程序编译建立没有错误,也开启了魔术棒创建 HEX 文件选项,那么 build 后就会在对应路径中找到生成的 HEX 文件。

在 STC-ISP 中选定单片机型号、串口、晶振频率(可以直接看开发板上的晶振上面有写),选择对应的 HEX 文件,先断电开发板,再点击下载,再开机,就可以查看呈现在开发板上的效果。

HELLO WORLD——LED部分

LED 发光二极管。

image-20230125222255962

image-20230125222332854

下面两个黑色的方块就是8个电阻。电阻是限流作用,防止电流过大烧毁 LED。

电阻上面写着小小的“102”,代表10*10^2,即1kΩ。

每个 LED 正极是一定通电流的,如果负极接地,那么这个 LED 被点亮。否则两头都是高电平点不亮(这里的电平是 TTL 电平,高5低0)。

单片机如何驱动高低电平?在 MCU 内,CPU 接到指令(如P2^0口赋1,即高电平)CPU 把数据写入寄存器,寄存器数据通过驱动器放大后变为5V/0V 电平输出。

点亮 LED

GPIO(general purpose input output) 即通用输入输出端口,可以通过软件控制其输入和输出.

image-20220905111036720

  • 电源引脚: Vcc, GND
  • 晶振引脚:XTAL1 2
  • 复位引脚:RST VPD,不做其他功能。
  • 下载引脚:TXD RXD
  • GPIO引脚:Px.x的都是 GPIO 引脚,大致分为P0 P1 P2 P3,每组8个IO,P3还有附加功能,比如串口、外部中 断、计数器等。每个引脚每次只能使用一个功能。
#include "reg52.h"
sbit LED1=P2^0; //将 P2.0 管脚定义为 LED1
//我们也可以直接给P2整个赋值。比如P2=0xFE,即1111 1110,就只会点亮最后一个 LED 灯,和 P2^0=0 效果是一样的。
//另,我们的这种做法只是寻找特殊寄存器P2的第几位。而头文件 REGX52.H 中是真正包含所有引脚信息的,如P2_0 就是2.0引脚,也能起到一样的效果。
void main()
{
    LED1=0; //LED1 端口设置为低电平,就会被点亮
    while(1)//单片机默认不断执行主程序。如果没有这个死循环,单片机就会不断点亮点亮点亮点亮……不如点亮一次之后无限延时。
    {
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

编译结果里面的几个数据的意义:

code:表示程序所占用 FLASH 的大小。

data:数据储存器内部 RAM 占用大小。

xdata:数据储存器外部 RAM 占用大小。

LED 闪烁

只需要点亮——延时——熄灭——延时循环即可。

单片机频率单位是 MHz 兆赫兹,所以只是单纯的亮灭亮灭肉眼看不出亮灭的效果。所以需要延时。

延时可以写一个这样的函数:

typedef unsigned int u16
void delay(u16 ten_us){
    while(ten_us--);
}
  • 1
  • 2
  • 3
  • 4

u16 代表16位的无符号整型数据。这是一个比较常用的定义,unsigned char 定义为 u8, unsigned int 定义为 u16。当 ten_us 超出 u16 的范围后,跳出 while 循环。

然后就LED1=0;delay(50000);LED1=1;delay(50000);循环即可.

但是,STC-ISP 可以根据晶振频率和要延时的时间生成延时函数,真的牛!不过注意软件上标明的适用系列版本。

image-20230125224132185

其中 _nop_() 函数包括在 INTRINS.H 头文件中,是一个空语句,就只会产生延时的效果。

不过 STC-ISP 只能生成固定时长的延时函数。如果想要像自己写的那个 delay() 函数一样传入参数,延时对应长度的毫秒/微秒呢?

很简单,我们先生成延时1毫秒/微秒的函数,然后把函数中的内容重复执行传入参数遍。

void Delay1ms(unsigned int xms)		//@11.0592MHz
{
	unsigned char i, j;
 	while(xms--){//这里是修改过的
        _nop_();
        _nop_();
        _nop_();
        i = 11;
        j = 190;
        do
        {
            while (--j);
        } while (--i);
     }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

Keil 软件仿真

使用仿真功能查看 LED 闪烁案例中的实际延时时间。

  1. 点击魔术棒,选择 Target 选项卡,设置 Xtal 为12M或11.0592M,根据开发板晶振修改对应值。
  2. 点击黑色放大镜中有红色d的仿真按钮,进入仿真页面

image-20220905153847511

我们要关注的参数是sec。

  1. 点击RST按钮重新复位系统参数,sec 变为0。然后在要调试的行前双击,就会出现红色块的断点,点击8运行时就会直接运行到断点处。再次点击就会运行到下一处断点处。
  2. 点击红色标记8运行,运行到36行时显示用时:0.00039s,再次点击运行到37行,用时:0.45s
  3. 可见delay花费时间约为0.45s

LED 流水灯

学会了点亮和延时,流水灯的原理就很好懂了。就是给P2的所有端口赋值为:1111 1110,每次只有一个为0即点亮,这个点亮的0从最高位逐渐降到最低位。

取反后即为:

1000 0000

0100 0000

0010 0000

0001 0000

0000 1000

0000 0100

0000 0010

0000 0001

也就是一个移位运算,0x01<<i的循环。

#include "reg52.h"
# define LED P2
void delay(unsigned int i){
	while(i--){}
}
void main()
{
    while(1)
    {
			int i=0;
			for(i;i<8;i++){
				LED=~(0x01<<i);
				delay(50000);
			}
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

移位函数

位运算的移位操作只能补0,但是 Keil C51 软件内有对应的移位库函数,左移_crol_(),右移_cror_(),包含在 intrins.h 库中

移位函数会把移出去的位补到空位,一个循环。

#include "reg52.h"
#include "intrins.h"
# define LED P2
void delay(unsigned int i){
	while(i--){}
}
void main()
{
    LED=~(0x01);
	delay(50000);
    while(1)
    {
			LED=_crol_(LED,1);
			delay(50000);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

蜂鸣器实验

蜂鸣器简单地说,就是电磁线圈和磁铁对振动膜的作用。

单片机的是无源蜂鸣器,不能一直充电,需要外部控制器发送震荡信号,可以改变频率产生不同的音色、音调。

大多数有源蜂鸣器则没有这个效果,有源蜂鸣器外观与之相同,内部自带震荡源,接上电就能响,但不能改变频率。

image-20230207154444964

我们知道三极管的作用是不用单片机自己直接驱动单片机。

另一种方法是步进电机。

image-20230207155956609

ULN 2003,高电压 高电流驱动器,给信号就被驱动。IN 取反输出 OUT。

简谱

image-20230207163112788

首先整个谱大概分为几个区。大字组、小字组、小字1组、小字2组。每个组之间差8度,每相邻的两个键(如黑白)差半音,相邻的两个同色键差一个全音。

几个白键的表示方法就是下面的简谱,差半音的黑键用左上角的#表示升半音,b表示降半音。

演奏两大要素:音高和时值。

img

谱上一个数字是1/4 音符,二分是其两倍,数字加个横线 - 。全音符就是(2 - - -)。这个线叫增时线。

八分是其1/2,数字下加一条线(2).再/2就再加一条,叫减时线。

试着识一个完整的谱:

image-20230207233115087

4/4:以四分音符为一拍,每小节有四拍。

第二节 1 ˙ \dot{1} 1˙ · 上面的点我们知道代表高音,后面的点代表:前一位音符延长1/2长度,即四分音符+1/2的四分音符。也就是3/8哈哈哈。

看第一节,一般连着两个八分音符就把 underline 连起来。但是这种哪怕是一个音,中间也要先断开再重响。比如右上角的3 3

升音和降音在本小节中有效。如第三行的 7 #4 4 7 ,两个4都是升音。

不过如果顶端画了延音线,就是连起来的不用断开。如中间的 77 ^ \widehat{7 7} 77 ,拆开写是为了好读谱。

接下来就是如何把谱转化为单片机代码。左上角 1=c 说明是c调的。d大调会出现黑键,c调只有白键。

image-20230208000727170

音具体是怎么定义的?首先以中音a为基准,高音a是其2倍,低音a是其1/2。

中间每次升音都是等比数列递增的,即*2的1/12次方

使用蜂鸣器

响起来很简单:不断反转 P1^5 口(是不是这个口得看自己的板子型号)。

void main()
{
	u16 i=2000;//决定时值
	while(1){
		while(i--)
		{
			BEEP=!BEEP;
			delay10Us(100);//决定音高
		}
        i=2000;
        BEEP=0;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

时值还好确认,音高怎么说?

首先我们有上图的音符与频率对照表。我们把频率转化为周期,即1/频率。这里周期单位是us。

image-20230208093509168

然后周期时长转化为机器周期,即记一个数需要的时间。我们看看需要多少机器周期。

1机器周期=12时钟周期,时钟周期=1/单片机晶振。比如对于我的11.0592MHZ 晶振,机器周期=12/11.0592MHZ (单位:us)。

据此把“需要切换的周期时长”转化为“需要切换的周期需要执行几次指令”。即周期/机器周期。如果是12MHZ 晶振这一步相当于没有。

image-20230208094537336

然后电平从低到高,从高到低才是一个周期。所以实际电平反转一次的周期是周期的一半。

image-20230208095158230

我们知道定时器原理是 TH TL 加至65536触发中断。因此重装载值(定时器初值)=65536-取整值。

音符频率周期需要的机器周期数需要的机器周期数/2取整重装载值
12623816.7943517.5572521758.778626175963777
1#2773610.1083327.0758121663.537906166463872
22943401.3613134.6938781567.346939156763969
2#3113215.4342963.3440511481.672026148264054
33303030.3032792.7272731396.363636139664140
43492865.332640.6876791320.34384132064216
4#3702702.7032490.8108111245.405405124564291
53922551.022351.0204081175.510204117664360
5#4152409.6392220.7228921110.361446111064426
64402272.7272094.5454551047.272727104764489
6#4662145.9231977.682403988.841201798964547
74942024.2911865.587045932.793522393364603
15231912.0461762.141491881.070745788164655
1#5541805.0541663.537906831.768953183264704
25871703.5781570.017036785.008517978564751
2#6221607.7171481.672026740.836012974164795
36591517.4511398.482549699.241274769964837
46981432.6651320.34384660.171919866064876
4#7401351.3511245.405405622.702702762364913
57841275.511175.510204587.75510258864948
5#8311203.3691109.025271554.512635455564981
68801136.3641047.272727523.636363652465012
6#9321072.961988.8412017494.420600949465042
79881012.146932.7935223466.396761146665070
11046956.0229881.0707457440.535372844165095
1#1109901.7133831.018936415.50946841665120
21175851.0638784.3404255392.170212839265144
2#1245803.2129740.2409639370.120481937065166
31318758.7253699.2412747349.620637335065186
41397715.8196659.6993558329.849677933065206
4#1480675.6757622.7027027311.351351431165225
51568637.7551587.755102293.87755129465242
5#1661602.047554.846478277.42323927765259
61760568.1818523.6363636261.818181826265274
6#1865536.193494.155496247.07774824765289
71976506.0729466.3967611233.198380623365303

使用方法:TH=重装载值/256,TL=重装载值%256.

音高从低到高逐位响起代码:

#include "reg52.h"
#include "Delay.h"
#include "Timer0.h"
sbit beep=P1^5;

unsigned int beep_table[]={//可以加个0代表不响的0
	63777,63872,63969,64054,64140,64216,64291,64360,64426,64489,64547,64603,
	64655,64704,64751,64795,64837,64876,64913,64948,64981,65012,65042,65070,
	65095,65120,65144,65166,65186,65206,65225,65242,65259,65274,65289,65303	
};

unsigned char beep_select=0;

void main(){																	 
	unsigned char i;
	timer0Init();
	while(1){
		beep_select++;
		delayMs(50);//时值		
	}
}

void timer0Interrupt() interrupt 1
{
    TH0 = beep_table[beep_select]/256; // 因为触发中断时,TH TL 归零,所以记得赋初值!
    TL0 = beep_table[beep_select]%256;
	beep=!beep;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

编曲:

image-20230208125043337

根据乐谱写一个数组。

unsigned int little_star[]={12, 12, 19, 19,
                            21, 21, 19,    //增时线
                            17, 17, 16, 16,
                            14, 14, 12,
                            19, 19, 17, 17,
                            16, 16, 14,
                            19, 19, 17, 17,
                            16, 16, 14,
                            12, 12, 19, 19,
                            21, 21, 19, 
                            17, 17, 16, 16,
                            14, 14, 12
                           };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

遍历数组,得到的音高再去 beep_table 中获取重装载值。

TH0 = beep_table[little_star[beep_select]]/256; // 因为触发中断时,TH TL 归零,所以记得赋初值!
TL0 = beep_table[little_star[beep_select]]%256;
  • 1
  • 2

但是播放起来都是连着的,听起来效果并不好。可以每次播完一个音先关闭中断并延时一段时间,再继续播放。

while(1){
		beep_select++;
		delayMs(50);
		TR0=0;
		delayUs(1);
		TR0=1;		
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

増时线如何处理?中间是不断开一直想的,因此需要几个特定的音符delay时间更长一些。怎么区分哪些音符加长哪些不加呢?

最好还是存储乐谱时搞一个二维数组(逻辑上物理上都可以),既能存储音高,也能存储时值。

unsigned int little_star[]={12, 4,
                            12, 4,
                            19, 4,
                            19, 4,
                            21, 4,
                            21, 4,
                            19, 8,  //增时线
                            17, 4,
                            17, 4,
                            16, 4,
                            16, 4,
                            14, 4,
                            14, 4,
                            12, 8,
                            19, 4,
                            19, 4,
                            17, 4,
                            17, 4,
                            16, 4,
                            16, 4,
                            14, 8,
                            19, 4,
                            19, 4,
                            17, 4,
                            17, 4,
                            16, 4,
                            16, 4,
                            14, 8,
                            12, 4,
                            12, 4,
                            19, 4,
                            19, 4,
                            21, 4,
                            21, 4,
                            19, 8,
                            17, 4,
                            17, 4,
                            16, 4,
                            16, 4,
                            14, 4,
                            14, 4,
                            12, 8,
                            0xFF,4//终止标志防越界
                           };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

如果数组大小超限,在魔术棒-Target-Memory Model 中选择第三个。不过这只是治标不治本,因为 RAM 只有512字节所以存不下太长。可以在定义数组时加上关键词 code 来存在 ROM 8K 的闪存中。不过这样的数组是只读的。

当然这样找索引比较麻烦。最好是索引全部重新宏定义。

//音符与索引对应表,P:休止符,L:低音,M:中音,H:高音,下划线:升半音符号#
#define P	0
#define L1	1
#define L1_	2
#define L2	3
#define L2_	4
#define L3	5
#define L4	6
#define L4_	7
#define L5	8
#define L5_	9
#define L6	10
#define L6_	11
#define L7	12
#define M1	13
#define M1_	14
#define M2	15
#define M2_	16
#define M3	17
#define M4	18
#define M4_	19
#define M5	20
#define M5_	21
#define M6	22
#define M6_	23
#define M7	24
#define H1	25
#define H1_	26
#define H2	27
#define H2_	28
#define H3	29
#define H4	30
#define H4_	31
#define H5	32
#define H5_	33
#define H6	34
#define H6_	35
#define H7	36
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

数码管

动态数码管原理是什么?首先我们知道每个数码管都有 abcdefg 七个段。

8个数码管那我们按理来说是需要8*7个引脚,很浪费。

于是设计了动态数码管。首先所有数码管是共阴极的。然后我们选中哪一个数码管阴极赋0,就会启动哪一个数码管,传入的 abcdefg 就会点亮该数码管的对应段。

然后8个数码管像流水灯一样,以极高频率依次点亮,肉眼看到的就是8个数码管都被点亮且呈现出不同的图案。

image-20230126130808285

开发板上的数码管是共阴极数码管,所有位共接一个阴极,给对应ABCDEF输入高电平点亮。

直接引脚:

直接引脚

image-20230126125258985

74译码器使用3位 bit 输入表示8种状态,调整 LED1~8 哪一个输出低电平,代表要启动8个数码管的哪一个的公共端。

输入的三位从最低位到最高位分别是P2^2, P2^3, P2^4,代表数码管从左到右的第几位是输入取反。

比如P2^4=1, P2^3=1, P2^2=0, 输入就是110,取反后就是001,就是从左到右第1位数码管(从第0位开始)。

VCC 和 GND 是使能,接到译码器上一上电就工作。

image-20230126005938488

P00~P07 代表控制当前数码管的 a~g 显示形式,接到 74HC245 缓冲器上而不是直接接到数码管上,使得单片机不用直接驱动数码管,Ai 连到 Bi 上。

OE 是使能,接地工作不接地不工作的原理。

DIR 是规定方向,高电平从左边读取数据传输到右边。低电平从右边读数据到左边。开发板上有个 J21 跳线帽,可以调整是 GND 与 LE 相连还是 VCC 与 LE 相连,也就是高电平输入 DIR 还是低电平输入 DIR。

数码管上面的 COM 是公共端,选中哪一个公共端(使得其=0,因为是共阴极)就是调整哪一个数码管的点亮方式。

点亮一位数码管代码:

#include "reg52.h"
#define Display P0 
unsigned char display_code[17]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};
/*具体显示什么图案是怎么推算出来的呢?首先 abcdefg 和 dp 小数点段一共有8个要控制的段。
比如我们要呈现数字6,就是 acdefg 亮,b和小数点不亮。
因为数码管是共阴极,所以我们想让哪个段亮哪个段就输入高电平,和 LED 相反。
所以P00~P07 的输入应该是 1011 1110
然后我们直接给 P0 赋值的话,是 P0_7 在最高位,P0_0 在最低位,所以输入应该正好反过来,0111 1101,即0x7d。*/
void main()
{
    Display=display_code[0];
    while(1)
    {}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
#define SMG_A_DP_PORT P0 //使用宏定义数码管段码口
sbit LSA=P2^2;
sbit LSB=P2^3;
sbit LSC=P2^4;

for(i=0;i<8;i++){
    //这个程序是从左到右显示12345678.当然完全也可以把确定哪一位数码管和确定要显示的数字封装成一个函数,点亮会更方便。
		switch(i){
			case 0: LSC=1;LSB=1;LSA=1;break;//从左往右第0个数码管
			case 1: LSC=1;LSB=1;LSA=0;break;
			case 2: LSC=1;LSB=0;LSA=1;break;
			case 3: LSC=1;LSB=0;LSA=0;break;
			case 4: LSC=0;LSB=1;LSA=1;break;
			case 5: LSC=0;LSB=1;LSA=0;break;
			case 6: LSC=0;LSB=0;LSA=1;break;
			case 7: LSC=0;LSB=0;LSA=0;break;
			break;
		}
		SMG_A_DP_PORT=gsmg_code[i];
		delay_10us(100);
		SMG_A_DP_PORT=0x00;//消影
    //为什么要消影? 不延时的话动态数码管会反复重复 位选 段选 位选 段选……位选之后不一定及时段选,可能前一位的位选就会赋给后一位。因此需要消影。
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

我们这种动态数码管扫描方式是单片机直接扫描,硬件会简单很多,但是会占据大量的 CPU 时间。有的动态数码管自带显存和扫描电路,只要告诉他要显示什么他会自动扫描显示。

按键

内部有一个金属片,按下后电路接通。

image-20230125234043319

和学习 LED 时类似,所有 IO 口一开始都是高电平,我们给其接地就变成低电平了。

按下按键后一段时间内电平会高低抖动。

image-20230125235945530

1,先设置 IO 口为高电平(由于开发板 IO 都有上拉电阻,所以默认 IO 为高 电平)。

2,读取 IO 口电平确认是否有按键按下。

3,如有 IO 电平为低电平后,延时几个毫秒。

4,再读取该 IO 电平,如果仍然为低电平,说明按键按下。

5,执行按键控制程序。

#include "reg52.h"
typedef unsigned int u16; //对系统默认数据类型进行重定义
typedef unsigned char u8;
//定义独立按键控制脚
sbit KEY1=P3^1;
sbit KEY2=P3^0;
sbit KEY3=P3^2;
sbit KEY4=P3^3;
//定义 LED1 控制脚
sbit LED1=P2^0;
sbit LED2=P2^1;
sbit LED3=P2^2;
sbit LED4=P2^3;
//使用宏定义独立按键按下的键值
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4
#define KEY_UNPRESS 0

void delay_10us(u16 ten_us)
{
while(ten_us--);
}

u8 key_scan(u8 mode)
{
	static u8 key=1;
	if(mode)key=1;//连续扫描按键
	if(key==1&&(KEY1==0||KEY2==0||KEY3==0||KEY4==0))//任意按键按下
	{
		delay_10us(1000);//10ms 消抖
        /*另一种消抖的方法:
        if(KEY==0){
        	delay_ms(20);
        	while(KEY==0);
        	delay(20ms);
        }
        这个是针对按下按钮又抬起按钮之后的执行。*/
		key=0;
		if(KEY1==0)
		return KEY1_PRESS;
		else if(KEY2==0)
		return KEY2_PRESS;
		else if(KEY3==0)
		return KEY3_PRESS;
		else if(KEY4==0)
		return KEY4_PRESS;
	}
	else if(KEY1==1&&KEY2==1&&KEY3==1&&KEY4==1) //无按键按下
	{
		key=1;
	}
	return KEY_UNPRESS;
}
void main()
{
	u8 key=0;
	while(1)
	{
		key=key_scan(0);
		if(key==KEY1_PRESS)//检测按键 K1 是否按下
		LED1=!LED1;//LED1 状态翻转
		if(key==KEY2_PRESS)//检测按键 K2 是否按下
		LED2=!LED2;//LED1 状态翻转
		if(key==KEY3_PRESS)//检测按键 K3 是否按下
		LED3=!LED3;//LED1 状态翻转
		if(key==KEY4_PRESS)//检测按键 K4 是否按下
		LED4=!LED4;//LED1 状态翻转
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71

矩阵按键

为了减少 IO 口的占用,用4个 IO 口代表行,4个 IO 口代表列。

类似动态数码管快速扫描实现几乎同时点亮的效果,矩阵键盘也是快速扫描。

image-20230126221349232

主要有两种扫描方法

行列式扫描法:每次给某一列赋值为0,然后检测这一列有无按钮按下。

​ 按行扫描:通过设置 P17 16 15 14 中的一个为低电平来选择扫描哪一行。根据 P10 P11 P12 P13 的输入判断是哪一列。但是 P15 口是蜂鸣器,不断反转会响。所以最好还是用按列扫描。

线翻转扫描方法:给所有列赋1,给所有行赋0,先判断在哪一行;然后用同样的方法判断在哪一列。

这里有一点问题就是:本单片机是准双向口输出,每个口既能做输入也能做输出而不用重新配置口线输出状态。其实这样相当于单片机一个引脚输出高电平,直接与另一个为低电平的引脚相连接。不会短路吗?

单片机的处理方法是这样的:内部 VCC 电源还附带一个下拉电阻。低电平的驱动力比高电平强,高电平直接接低电平就会被变为低电平,而不会短路。

#include "reg52.h"
typedef unsigned int u16; //对系统默认数据类型进行重定义
typedef unsigned char u8;
#define KEY_MATRIX_PORT P1 //使用宏定义矩阵按键控制口
#define SMG_A_DP_PORT P0 //使用宏定义数码管段码口
//共阴极数码管显示 0~F 的段码数据
u8 gsmg_code[17]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};

void delay_10us(u16 ten_us)
{
    while(ten_us--);
}

u8 key_matrix_ranks_scan(void)
{
    u8 key_value=0;
    KEY_MATRIX_PORT=0xf7;//1111 0111 给第一列也就是 P1_3 赋值 0,其余全为 1
    if(KEY_MATRIX_PORT!=0xf7)//判断第一列按键是否按下
    {
        delay_10us(1000);//消抖。因为这里用了类似动态数码管的循环扫描的方法。
        switch(KEY_MATRIX_PORT)//保存第一列按键按下后的键值
        {
            case 0x77: key_value=1;break;//0111 0111
            case 0xb7: key_value=5;break;//1011 0111
            case 0xd7: key_value=9;break;//1101 0111
            case 0xe7: key_value=13;break;//1110 0111
        }
    }
    while(KEY_MATRIX_PORT!=0xf7);//等待按键松开
    KEY_MATRIX_PORT=0xfb;//给第二列赋值 0,其余全为 1
    if(KEY_MATRIX_PORT!=0xfb)//判断第二列按键是否按下
    {
        delay_10us(1000);//消抖
        switch(KEY_MATRIX_PORT)//保存第二列按键按下后的键值
        {
            case 0x7b: key_value=2;break;
            case 0xbb: key_value=6;break;
            case 0xdb: key_value=10;break;
            case 0xeb: key_value=14;break;
        }
    }
    while(KEY_MATRIX_PORT!=0xfb);//等待按键松开
    KEY_MATRIX_PORT=0xfd;//给第三列赋值 0,其余全为 1
    if(KEY_MATRIX_PORT!=0xfd)//判断第三列按键是否按下
    {
        delay_10us(1000);//消抖
        switch(KEY_MATRIX_PORT)//保存第三列按键按下后的键值
        {
            case 0x7d: key_value=3;break;
            case 0xbd: key_value=7;break;
            case 0xdd: key_value=11;break;
            case 0xed: key_value=15;break;
        }
    }
    while(KEY_MATRIX_PORT!=0xfd);//等待按键松开
    KEY_MATRIX_PORT=0xfe;//给第四列赋值 0,其余全为 1
    if(KEY_MATRIX_PORT!=0xfe)//判断第四列按键是否按下
    {
        delay_10us(1000);//消抖
        switch(KEY_MATRIX_PORT)//保存第四列按键按下后的键值
        {
            case 0x7e: key_value=4;break;
            case 0xbe: key_value=8;break;
            case 0xde: key_value=12;break;
            case 0xee: key_value=16;break;
        }
    }
    while(KEY_MATRIX_PORT!=0xfe);//等待按键松开
    return key_value;
}

u8 key_matrix_flip_scan(void)//另一种扫描方式,这个函数暂时没有投入使用
{
    static u8 key_value=0;
    KEY_MATRIX_PORT=0x0f;//给所有行赋值 0,列全为 1
    if(KEY_MATRIX_PORT!=0x0f)//判断按键是否按下
    {
        delay_10us(1000);//消抖
        if(KEY_MATRIX_PORT!=0x0f)
        {
            //测试列
            KEY_MATRIX_PORT=0x0f;
            switch(KEY_MATRIX_PORT)//保存行为 0,按键按下后的列值
            {
                case 0x07: key_value=1;break;
                case 0x0b: key_value=2;break;
                case 0x0d: key_value=3;break;
                case 0x0e: key_value=4;break;
            }
            //测试行
            KEY_MATRIX_PORT=0xf0;
            switch(KEY_MATRIX_PORT)//保存列为 0,按键按下后的键值
            {
                case 0x70: key_value=key_value;break;
                case 0xb0: key_value=key_value+4;break;
                case 0xd0: key_value=key_value+8;break;
                case 0xe0: key_value=key_value+12;break;
            }
            while(KEY_MATRIX_PORT!=0xf0);//等待按键松开
        }
    }
    else
    key_value=0;
	return key_value;
}

void main()
{
    u8 key=0;
    while(1)
    {
        key=key_matrix_ranks_scan();
        if(key!=0)
        SMG_A_DP_PORT=gsmg_code[key-1];//得到的按键值减 1 换算成数组下标
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116

IO 扩展(串转并)-74HC595

前面接的一些输入输出设备都是直接连接的单片机 IO 口,单片机仅有的 IO 口非常有限。而使用 IO 扩展可以大量增加可使用的端口。比如后面要使用的 LED 点阵,8*8个格子,使用扩展 IO 输入就更为合适。如果多级联一个,就又有了8位输出,能实现16*16的点阵。

由图可知,OE 低电平有效,因此 LED 点阵旁的跳线帽一定要接到 OE-GND 一端。

74HC595 是一个位移缓存器,有8位串行输入、并行输出,并行输出是三态输出(高电平、低电平、高阻抗)。比如一次输入一个比特,输入八次,并行输出可以输出一个8位的字 1010 1010.

输出是由 VCC 驱动的,原理有那么一点像三极管。因为单片机内部是弱上拉,输出不足以点亮 LED 点阵,所以抛开 IO 口不够的问题,也不能直接接到 LED 点阵上,至少要有三极管。

点亮 LED 点阵

image-20230130004758073

image-20230130004829583

传入数据如列是0100 0000,行是0000 0001,则代表最后一行第二列的点会被点亮。

SRCLK:移位寄存器,数据先传输到移位寄存器中。移位寄存器上升沿时移位,再接收下一次数据。

RCLK:存储寄存器。存储寄存器上升沿时把寄存器中所有数据都通过端口输出。

相当于手枪,每次 SRCLK 上升时我们填入一枚子弹,RCLK 上升时把弹夹塞入。

QH 是级联用的。

列数据直接输入引脚,行数据输入 IO 拓展。

#include "reg52.h"
//编写程序先定义管脚和端口。管脚用sbit,端口宏定义
#define LED_MATRIX P0
sbit SRCLK=P3^6;
//因为 RCLK 是关键字不能被复用了
sbit rCLK=P3^5;
sbit SER=P3^4;
unsigned char hc_led_arr[8]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};
void delay(int i){while(i--);}

void hc_write_data(unsigned char c){
	//要传入8个输入,需要循环
	int i=0;
	for(i;i<8;i++){
		//注意 芯片传数据先传高位 再传低位,所以要反着写。这个问题在write函数内部解决,传入的数据和想要的形式一样就好。
		//通过移位获取
		SER=c>>7;
		//想获取下一位寄存器,需要移位寄存器移位。需要创造上升沿
		SRCLK=0;
		//芯片一般给定一个延时时间,经过这个时间之后才能处理完毕
		delay(1);
		SRCLK=1;
		delay(1);
		//让传入数据的次高位变为下次循环的高位
		c<<=1;
	}
	//最后通过存储寄存器的上升沿,传输全部数据
	rCLK=0;
	delay(1);
	rCLK=1;
}
void main(){
	LED_MATRIX=0x00;
	while(1){
		 int i=0;
		 for(i;i<8;i++){
		 	hc_write_data(0x00);//消隐
		 	hc_write_data(hc_led_arr[i]);
			delay(500000);
		 }
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

比如0000 0001,传入LED阵列的数据是:每轮循环传入最高位的值,并且所有数据向左移动一位。因此前7轮 SER 传入都是0,最后一轮 SER 传入1,最下面一行全亮。

LED 点阵实验

上面的方法只能确定某一具体的行被点亮。可不可以具体确定哪些点点亮的方法?

我们让想被点亮的点列为低电平,行为高电平,就会被点亮。如果我们只想点亮第一行第一列的点,只需行脚只有第一行接高电平,列脚只有第一列接低电平即可。

所以只要先让第一列为低电平,其他列为高天平来只读取第一列,遍历所有行检查第一列哪些点应该被点亮;然后第二列,第三列……一次类推,每轮循环不用消除上次的结果即可。

#include "reg52.h"
//编写程序先定义管脚和端口。管脚用sbit,端口宏定义
#define LED_MATRIX P0
sbit SRCLK=P3^6;
sbit rCLK=P3^5;
sbit SER=P3^4;
unsigned char hc_led_arr[8]={0x38,0x7C,0x7E,0x3F,0x3F,0x7E,0x7C,0x38};
unsigned char col[8]={0x7f,0xbf,0xdf,0xef,0xf7,0xfb,0xfd,0xfe};
void delay(int i){while(i--);}

void hc_write_data(unsigned char c){
	//要传入8个输入,需要循环
	int i=0;
	for(i;i<8;i++){
		//注意 芯片传数据先传高位 再传低位,所以要反着写
		//通过移位获取
		SER=c>>7;
		//想获取下一位寄存器,需要移位寄存器移位。需要创造上升沿
		SRCLK=0;
		//芯片一般给定一个延时时间,经过这个时间之后才能处理完毕
		delay(1);
		SRCLK=1;
		delay(1);
		//让传入数据的次高位变为下次循环的高位
		c<<=1;
	}
	//最后通过存储寄存器的上升沿,传输全部数据
	rCLK=0;
	delay(1);
	rCLK=1;
}
void main(){
	LED_MATRIX=0x00;
	while(1){
		 int i=0;
		 for(i;i<8;i++){
		 	LED_MATRIX=col[i];
		 	hc_write_data(hc_led_arr[i]);
             //不知道为什么,下面两部分不写图形会偏移。不知道会不会有大佬解答一下
			delay(100);
			hc_write_data(0x00);
		 }
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

点阵的具体图案生成方法:字模提取软件。

image-20220908234813283

步进电机

电脉冲信号转化为角位移。

**注意步进电机红色线接到5V的地方。**以下程序意为:启动步进电机后,按按钮1旋转方向改变,按按钮2加速,按按钮3减速。

#include "reg52.h"
typedef unsigned int u16;
typedef unsigned char u8;
sbit IN1_A=P1^0;
sbit IN2_B=P1^1;
sbit IN3_C=P1^2;
sbit IN4_D=P1^3;

//定义独立按键控制脚
sbit KEY1=P3^1;
sbit KEY2=P3^0;
sbit KEY3=P3^2;
sbit KEY4=P3^3;
//使用宏定义独立按键按下的键值
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4
#define KEY_UNPRESS 0
#define STEPMOTOR_MINSPEED 1
#define STEPMOTOR_MAXSPEED 5

void delay(u16 ten_us)
{
	while(ten_us--);
}

void step_motor_28BYJ48_send_pulse(u8 step,u8 dir){
	u8 temp=step;
	if(dir==0)temp=7-step;//逆时针旋转
	switch(temp)//8 个节拍控制:A->AB->B->BC->C->CD->D->DA
	{
		case 0: IN1_A=1;IN2_B=0;IN3_C=0;IN4_D=0;break;
		case 1: IN1_A=1;IN2_B=1;IN3_C=0;IN4_D=0;break;
		case 2: IN1_A=0;IN2_B=1;IN3_C=0;IN4_D=0;break;
		case 3: IN1_A=0;IN2_B=1;IN3_C=1;IN4_D=0;break;
		case 4: IN1_A=0;IN2_B=0;IN3_C=1;IN4_D=0;break;
		case 5: IN1_A=0;IN2_B=0;IN3_C=1;IN4_D=1;break;
		case 6: IN1_A=0;IN2_B=0;IN3_C=0;IN4_D=1;break;
		case 7: IN1_A=1;IN2_B=0;IN3_C=0;IN4_D=1;break;
		default:break;
	}
}


u8 key_scan(u8 mode)
{
	static u8 key=1;
	if(mode)key=1;//连续扫描按键
	if(key==1&&(KEY1==0||KEY2==0||KEY3==0||KEY4==0))//任意按键按下
	{
	delay(1000);//消抖
	key=0;
	if(KEY1==0)
	return KEY1_PRESS;
	else if(KEY2==0)
	return KEY2_PRESS;
	else if(KEY3==0)
	return KEY3_PRESS;
	else if(KEY4==0)
	return KEY4_PRESS;
	}
	else if(KEY1==1&&KEY2==1&&KEY3==1&&KEY4==1) //无按键按下
	{
	key=1;
	}
	return KEY_UNPRESS;
}

void main(){
	u8 key=0;
	u8 dir=0;
	u8 speed=STEPMOTOR_MAXSPEED;
	u8 step=0;
	while(1){
		key=key_scan(0);
		switch(key)
		{
			case KEY1_PRESS:dir=!dir;break;
			case KEY2_PRESS:if(speed>STEPMOTOR_MINSPEED)speed-=1;break;
			case KEY3_PRESS:if(speed<STEPMOTOR_MAXSPEED)speed+=1;break;
			default:break;
		}
		step_motor_28BYJ48_send_pulse(step++,dir);
		step%=8;
		delay(speed*1000);
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88

中断

使单片机能对外部或者内部随机发生的事件实时处理。

分时操作,实时响应,可靠性高。

中断相应条件:首先我们要确保相关配置都准备好了,CPU 允许中断,中断源允许中断,然后发生中断事件时才会正确触发中断。

中断可能还会被优先级更高的中断打断,支持这种操作的系统叫多级中断系统。

STC89C52 有8个中断,4外部,3定时器,1串口。

image-20230127002315074

这是传统51单片机定时器中断结构,原理与 STC89C52 相近。

image-20230127002418325

通过配置寄存器控制线路连接。比如上图中的开关就是由寄存器控制。

EA ENABLE ALL:即使能所有中断。

ET:中断允许位。

PT:中断优先级。只有一个 PT 只能决定是高或低两种优先级。更多的中断优先级寄存器可以决定更多中断优先级。

TCON 部分: time controller,不属于 CPU 部分,等到定时器部分展开叙述。

代码编写:主程序中需要包含:

EA=1;//总中断开关:打开
EX0=1;//外部中断0开关:打开。
IT0=0/1;//外部中断触发方式的选择。如下降沿触发,或低电平触发。
//如果要配置外部中断1,则改为EX1和IT1

//中断服务函数
void int0() interrupt 0 using 1//using 1 可省略
{
    
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

定时器中断实验

本章利用单片机自带的定时器来实现之前做过的操作:LED灯间歇闪烁。一直以来实现的方法都是借助while循环来拖延时间。定时器不仅更加准确,还可以节省下 CPU 的资源。

STC89C52 有3个定时器。

CPU时序的相关知识

振荡周期:为单片机提供信号的振荡源的周期(晶振周期)。12MHZ 的晶振振荡周期就是1/12us, 求倒数。

状态周期:两个振荡周期=1状态周期s(时钟周期)。

机器周期:6状态周期=1机器周期。

指令周期:完成一条指令所用的全部时间,以机器周期为单位。

定时器的相关知识

定时器又可以计数,也叫计数器。不需要CPU参与自己就能完成任务,根据脉冲信号对寄存器中数据+1。来一个脉冲定时器+1,加到全为1后输出一个脉冲并归0.同时,向CPU发出计时器中断信息。

一般有四种工作模式:

13位定时器,16位定时器,8位自动重装模式,双8位计数器。

我们的工作模式用的是16位。

串口通信中为了精度考虑要使用自动重装模式。因为我们要自己设置每次溢出产生中断后 TH TL 的初值,这样吧比较容易出错且有点慢。8位自动重装模式就是舍弃了16位存储数据(到65535),只采用后八位 TL 计数(到255),初始值TH TL 赋一样的初值。TL 每次溢出,TH 把自己的值赋给 TL,这样就不用我们自己手动重新赋初值了,初值一直保存在 TH 中。

而且,在本例中我们使用定时器中断就是为了产生中断时给定时器赋初值并让灯切换状态。使用8位自动重装模式后,不用手动给定时器赋初值了;如果没有其他的需要定时器溢出时必须做的操作,定时器可以不用设置中断,起到一个可以看时间但是不会响的闹钟的功能,即 ET=0。

image-20230126235200709

左上角支路是时钟功能,左下角支路是计数功能,最终实现中断功能。

TH TL 寄存器最大能存储到65535.每来一次脉冲+1,加到最大值时 flag 申请中断。

SYSCLK 是晶振周期。另一个时钟是 T0 引脚,如果启用 T0 引脚定时器就变成计数器了,每来一个脉冲+1。

默认使用12T 的分频,把 12MHZ 分成12份,每一份就是1us。这个单片机上是没有对应调整的寄存器的,如果想使用 6T 的分频需要在 STC-ISP 中选择使能 6T 模式。

CT 是一位寄存器,赋1为C,即计数器;赋0为T,即时钟(T上面的横线就代表0时)。

每个定时器主要有两种寄存器:TCON TMOD。

TCON 包括:TF, TR, IE, IT。

image-20230127114845707

​ TF 可见上图主路,TH TL 被允许计数后周期性+1计数,加到最大值时 TF=1,并发起中断。处理完中断后恢复为0.

​ TR 可见上图支路,是开启中断的条件之一。

​ IE 是外部中断。

​ IT 是设置中断触发模式,比如设置为0是低电平触发,设置为1是下降沿触发。

TMOD 是不可寻址的寄存器,也就是只能整体赋值,不能像 P2 一样分开给每个变量赋值。包含:GATE, CT, M0, M1.

image-20230127114908062

​ GATE 用于开启定时器。当 GATE 打开, TR=1(timer reset)且 I N T 1 / I N T 0 ‾ \overline{INT1/INT0} INT1/INT0 为高(即打开中断引脚)时定时器开始工作。这一部分内容对应上图电路中的左下角。

​ M0M1 用于选定时钟的4个模式。比如16 位就是01. 两者包含一个叫做 TMOD 的不可位寻址的变量里,

​ CT就是打开定时器的计时还是时钟功能。

开启定时器计数功能及总中断:

void time0_init(void)
{
    TMOD&=0xF0;//设置定时器模式
    TMOD|=0X01;//我们知道 TMOD 是不可位寻址,也就是里面虽然既包含了定时器1和0的寄存器,但是我们赋值只能一整个赋值。
    //如果直接给 TMOD=0x01,就会影响定时器1的值。因此我们用这两部先清空后四位,再单独给后四位赋值为0x01。
    
    //TCON 中的 IE IT 对应就是支路图中的 INT0,因为我们开启了 GATE,或电路,因此 IE IT 不用设置也行。
    //我们知道 TH TL 合起来达到65535,也就是过了 65535个机器周期 后会触发中断。
    //比如我们现在想1ms触发一次中断,怎么处理呢?
    //12MHZ 下1us一个周期,1ms 1000个周期,因此我们每次设置初始值为64535,变为65535正好需要1ms。
    //11.0592MHz除12为921600Hz,就是1us 0.9216 个机器周期,因此初值为65535-922=64613.
    //我们可以给 TH TL 赋初始值,64535,这样只要过 1000us 就会触发中断。
    //然后因为 TH 和 TL 拼接变为一整个16位的寄存器,所以 TH 是高8位,TL 是低8位,分别用计算出的初值/256 %256得到最终结果。
    TH0=0XFC; //64613/256=252=0xFC,我的单片机是 11.0592 MHZ
    TL0=0X65;//64613%256=0x65
    
    TF0=0;//归零,防止刚配置好就产生中断。可有可无
    TR0=1;//打开定时器,开启中断条件之一
    ET0=1;//打开定时器 0 中断允许
    EA=1;//打开总中断
    PT0=0;//设置中断优先级为低。默认也是低,不写也没关系。
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

STC-ISP 上也有生成定时器函数。不过 AUXR 设置定时器时钟那一步是针对最新版本可以调整单片机定时器使能而添加的,我们的单片机加上会报错,需要删掉。

另外需要手动添加 ET EA PT。

image-20230127164307914

LED灯间隔1s闪烁代码:

#include "reg52.h"
sbit LED=P2^0;
void time0_init(void);//不再重写了~

void  time0() interrupt 1{
	static int i;
	TH0=0XFC; //因为触发中断时,TH TL 归零,所以记得赋初值!
	TL0=0X65;
	i++;
	if(i==1000)
	{
		i=0;
		LED=!LED;//闪烁
	}
}
void main(){
	time0_init();
	while(1){}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

外部中断实验

运行程序前,请摘下红外接收传感器。因为共用P3^2引脚,会干扰实验结果。

51单片机都有2个外部中断。STC89C5X系列有INT0~INT3四个。

对于三个参数的初始化,一般用一个init函数执行,在main的最开头。

本例中,我们用按键3作为外部中断源。按下按键3就会产生中断。中断执行的指令就是点亮或熄灭LED灯。

#include "reg52.h"
sbit LED=P2^0;
sbit KEY3=P3^2;
void delay(int ten_us)
{
while(ten_us--);
}
void interrupt_init(){
	EA=1;//总中断开关:打开
	EX0=1;//外部中断0开关:打开。
	IT0=1;//外部中断触发方式的选择。如下降沿触发,或低电平触发。	
}

void int0() interrupt 0{
	delay(1000);
	if(KEY3==0){
		LED=!LED;
	}
}

void main(){
	interrupt_init();
	while(1){
		
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

通信

通信基础知识

单片机还可以通过IO口实现多种通信。

串行通信:一条数据线,一次发1bit,发很久。

并行通信:多条数据线,同时发送,发的速度快多了但是费用高、接收困难、抗干扰性差。

异步通信:发送和接收方时钟可以不用完全一致。

同步通信 :发送和接收方时钟要完全一致。

单工、半双工、全双工通信:数据的传输方式,略。

比特率:位/s。

波特率:码元/s。

溢出率:比如13us溢出一次。溢出率就是1/13us。

校验位:如奇偶校验位。

停止位:分隔数据帧。

(有那么一点点感谢通原了~)

串口

串口通信,指外设和计算机之间通过数据线等传输数据的一种通信方式。比如RS-系列,大多数计算机应该都有对应的梯形接口。51单片机内自带UART(Universal Asynchronous Receiver Transmitter,通用异步收发器),可实现与计算机之间的串口通信!

单片机串口通信的管脚:VCC TXD(发送数据,串行输出)RXD(接收数据,串行输入)SGND(信号接地)。

电脑的串口还有很多管脚,如 RTS CTS,单片机的相对简单很多。

TXD和RXD用正负电压表示逻辑1和0,51单片机采用TTL 晶体管-晶体管逻辑集成电路,用高低电平表示逻辑状态(+5V:1;0V:0),所以需要转换型时候才能与计算机串口通信。

还有两种电平状态:RS232,3~15V 表示低(注意),-15~-3V 表示高。RS485,两线压差(差分信号)2~6V 表示高,-6~-2V 表示低。

image-20230128231105461

STC89C52系列有一个通用异步收发器(UART P30 P31端口),有四种工作模式。

  • 模式0:同步移位寄存器;
  • 模式1:8位UART,波特率可变(常用);
  • 模式2:9位UART,波特率固定;
  • 模式3:9位UART,波特率可变.

image-20230129013008311

TXD RXD 直接接到单片机 P30 P31 上. 另一端是单片机上自带的,我们把数据线连到电脑上就接上了。

image-20230129014312317

溢出率到波特率的计算见图。

串口助手和单片机要规定好发送数据的形式。

image-20230129193307966

串口数据缓存寄存器:SBUF。物理上是接收和发送两个寄存器,实际上共用相同的地址,只是使用时有不同的意义。我们只需要把数据放入其中就行,发送原理暂不用弄明白。

SCON:串口控制寄存器。控制电路。包含:

​ SM0,SM1:设置工作方式。比如我们采用8位 UART,就赋值01.

​ SM2:与工作方式1无关。

​ REN:是否允许串行接收状态。1允许接收。

​ TB8 RB8:接收到的第9位数据,与工作方式1无关。

​ TI RI:发送接收中断请求标志位。代表发送完了。硬件赋1,需要用软件复位。

赋值的话只有 SM0 SM1=01,和 REN 需要注意,其他的初始值都=0。

PCON:电源管理。包含:

​ SMOD:可见支路图,用于设置波特率是否加倍。

​ SMOD1:纠错或协助 SM0 设置工作方式。

IE:打开中断。

移位寄存器会触发对应中断。在中断图中的 TI RI,触发的是同一个中断。

实施串口通信

STC-ISP自带一个串口调试助手。

image-20230103020705868

串口选择左侧和串口号一致的选项。

STC89C52串口初始化函数:

void uart_init(void)
{
    TMOD|=0X20; //设置计数器工作方式 2
    SCON=0X50; //设置为工作方式 1。40是 REN 关闭,50是打开,代表单片机是否可以接收数据
    PCON=0X80; //波特率加倍,0就是不加倍
    TH1=0XFA; //计数器初始值设置,根据波特率为9600
    TL1=0XFA;
    ES=1; //打开接收中断
    EA=1; //打开总中断
    TR1=1; //打开计数器1
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

初始化函数也可以在 STC-ISP 中生成。这里会发现 12MHZ 的晶振相较 11.0592 MHZ 的晶振误差较大,要通过波特率加倍才能减少一些。这就是 11.0592 MHZ 晶振的设计原因。

单片机向电脑发送数据:给SBUF赋值即可。

在程序中发送可以直接SBUF=0X11;单片机就会收到11的信息,点击复位按钮后可以在串口助手的接收缓冲区中看到。

赋值后需要一段时间才能发送完成,发送完成后TI不再是0.

void main(){
    uart_init();
    SBUF=0x11;
    while(TI==0);TI=0;//=1说明发送完成,然后手动复位
    while(1){}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这样就在程序中发送了11信息。

电脑给单片机发送数据:通过串口助手发信息可以通过串口中断interrupt 4实现。

void uart() interrupt 4 //串口通信中断函数
{
    
    u8 rec_data;
    P2=0x00;//这一句使得函数成功触发时LED灯全亮,便于调试
    if(RI==1){P2=~SBUF;RI=0;}//因为发送和接收中断共用4中断,这句用于区分具体是发送还是接收中断
    //如果是接收中断,RI=1,那么只简单执行这两句即可。
    SBUF=rec_data; //将接收到的数据放入到发送寄存器
    while(!TI); //等待发送数据完成
    TI=0; //清除发送完成标志位
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

以上程序可以将发送缓冲区中输入的数据发给单片机,单片机再在接收缓冲区中原封不动地呈现出来。

文本模式和 HEX 模式就是文本和 ASCII 码的转换。

封装头文件;绘制LED动画

类似C语言的语法,部分函数等内容可以封装到头文件里,需要的时候引入到source file 中,再在 include 中指明即可正常使用。

编写.h文件:如:

image-20230103162731330

引入.h文件:右键左侧的.c文件→options for file→C51→include path→找到.h文件所在的文件夹并选中,注意一定不能有中文路径。然后就可以使用.h文件中定义的变量和函数了,注意不能重复定义

什么内容封装到函数里呢?静态的方法待调用的封装进去。逻辑判断后决定使用哪个方法这类的就不用放进函数里了,因为逻辑判断很可能经常改。

接下来就自己试着先把delay函数和矩阵LED绘制函数写入头文件,然后制作矩阵动画,这样动起来也会更方便一些。

//h file
#include "reg52.h"
typedef unsigned char u8;
typedef unsigned int u16;

	sbit SRCLK = P3 ^ 6;
	sbit rCLK = P3 ^ 5;
	sbit SER = P3 ^ 4;
	
/**
 * Function name: time0_init
 * Function paremeter: void
 * Function performance: 初始化定时器。只有执行此函数后才能使用单片机的定时器功能
 */
void time0_init(void)
{
	TMOD |= 0X01; // 选择为定时器 0 模式,工作方式 1
	TH0 = 0XFC;	  // 给定时器赋初值,定时 1ms
	TL0 = 0X18;
	ET0 = 1; // 打开定时器 0 中断允许
	EA = 1;	 // 打开总中断
	TR0 = 1; // 打开定时器
}
/**
 * Function name:time0
 * Function paremeter: void
 * Function performance: 启动定时器。定时器计数器time_counter在0~10000之间循环。
 */
/*
void time0() interrupt 1{
	TH0=0XFC; //给定时器赋初值,定时 1ms
	TL0=0X18;
	time_counter++;
	bit pass_1s=0;
	if(time_counter==1000)
	{
		time_counter=0;
		pass_1s=~pass_1s;
	}
}
*/
void delay(u16 delay_10us){
	while(delay_10us--);
}

void hc_write_data(unsigned char c)
{
	// 要传入8个输入,需要循环
	int i = 0;
	for (i; i < 8; i++)
	{
		// 注意 芯片传数据先传高位 再传低位,所以要反着写
		// 通过移位获取
		SER = c >> 7;
		// 想获取下一位寄存器,需要移位寄存器移位。需要创造上升沿
		SRCLK = 0;
		// 芯片一般给定一个延时时间,经过这个时间之后才能处理完毕
		delay(1);
		SRCLK = 1;
		delay(1);
		// 让传入数据的次高位变为下次循环的高位
		c <<= 1;
	}
	// 最后通过存储寄存器的上升沿,传输全部数据
	rCLK = 0;
	delay(1);
	rCLK = 1;
}

void matrix_led_animation(u8 hc_led_arr[])
{
	unsigned char col[8]={0x7f,0xbf,0xdf,0xef,0xf7,0xfb,0xfd,0xfe};
	int i=0;
	P0=0x00;
	
	for(i;i<8;i++){
		P0=col[i];
		hc_write_data(hc_led_arr[i]);
        //不知道为什么,下面两部分不写图形会偏移。不知道会不会有大佬解答一下
		delay(1);
		hc_write_data(0x00);
	}
	
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84

然后就是利用取模软件得到要绘制的图案的字模。这里我选定的图案是之前圣诞节临摹过的像素画中”Merry Christmas“的字体。参照来源:圣诞节 像素画 圣诞树

推荐阅读
相关标签