目录
写在最前面的话
本篇笔记整理自江科大自化协的51单片机教程~
很感谢AL的师兄师姐和小伙伴们,以及zzh师兄和zzl师兄在这个阶段的学习给予过我的帮助~
模块化编程
模块化编程概述
- 模块化编程是什么?有什么好处?
传统方式编程:所有的函数放在main.c里,当使用模块较多时,文件内的代码十分杂乱,不利于代码的组织和管理,也很影响编程者的思路。
模块化编程:把各个模块的代码放在不同的.c文件里,在.h文件里提供外部可调用的函数声明,其他.c文件想使用其中代码时,只需要include“xxx.h”文件即可。使用模块化编程可以极大提高代码的可阅读性、可维护性、可移植性。
- 为什么我们可以选择使用模块化代码?
单片机的编程过程中,关于如何显示、如何控制的代码才是我们真正需要重点关注、不断优化和修改的;而除此之外,还有很多模块是为我们程序目标做准备而存在的(驱动代码),在写好之后,几乎不需要更改了(比如上一篇笔记中设计的Delay函数)。所以我们可以采取模块化结构来设计代码的编写,使其更加井井有条,思路清晰。
模块化编程注意事项
- .c文件:函数、变量的定义。
- .h文件:可被外部调用的函数、声明和变量的声明。
- 任何自定义的变量、函数在调用前必须有定义或声明(在同一个.c内)。
- 使用到的自定义函数的.c文件必须添加到工程参与编译(所以在局部调试时,不需要调用的函数应当注释掉)。
- 使用到的.h文件必须放在编译器可寻找到的地方(建议放在与main.c同一个目录里)。
C语言的预编译
- 特征:是以“#include”开头。
- 实现方式为:预编译,实际上就是帮助我们将代码“复制粘贴”到main里面。先预处理,再编译。
- 作用:可以对程序中某些部分是否编译做出选择,从而达到程序代码的精简化。
例一:将Delay函数模块化,实现第一个LED以1s为周期闪烁。
main.c文件中的内容如下:
#include <REGX52.H> //<> :在安装目录中寻找文件
#include "Delay.h" //"" : 在自己程序目录中寻找文件
void main()
{
while(1)
{
P2_0=1;
Delay(500);
P2_0=1;
Delay(500);
}
}
Delay.c文件中的内容如下:
void Delay(unsigned int xms)
{
unsigned char data i, j;
while(xms)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}
}
Delay.h文件中的内容如下:
#ifndef _DELAY_H_ //每次新建.h文件都要做这一步:#ifndef _(填名字,不需要括号)_H_
#define _DELAY_H_
void Delay(unsigned int xms);//对函数进行声明。复制函数的第一行,并加上分号(一定不要忘记打分号)
#endif //与#ifndef配套使用,与其一起相当于一个括号的作用
例二:将Delay函数和nixie函数模块化,实现对数码管的控制(静止显示123)
main.c文件中的内容如下:
(相比于上一篇笔记实现类似功能的代码,运用模块化编程后整个mian.c文件真的精简了很多,而且都只呈现体现逻辑的核心代码)
#include <REGX52.H>
#include "Delay.h"
#include "nixie.h"
void main()
{
while(1)
{
nixie(1,1);
nixie(2,2);
nixie(3,3);
}
}
Delay.c文件中的内容如下:
void Delay(unsigned int xms)
{
unsigned char data i, j;
while(xms)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}
}
Delay.h文件中的内容如下:
#ifndef _DELAY_H_
#define _DELAY_H_
void Delay(unsigned int xms);//(一定不要忘记打分号)
#endif
nixie.c文件中的内容如下:
#include <REGX52.H>
#include "Delay.h"//在数码管函数中要用到Delay函数,所以也要给个头文件!
unsigned char nixietable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};
void nixie(unsigned char location,unsigned char number)
{
switch(location)
{
case 1:P2_4=1;P2_3=1;P2_2=1;break;
case 2:P2_4=1;P2_3=1;P2_2=0;break;
case 3:P2_4=1;P2_3=0;P2_2=1;break;
case 4:P2_4=1;P2_3=0;P2_2=0;break;
case 5:P2_4=0;P2_3=1;P2_2=1;break;
case 6:P2_4=0;P2_3=1;P2_2=0;break;
case 7:P2_4=0;P2_3=0;P2_2=1;break;
case 8:P2_4=0;P2_3=0;P2_2=0;break;
}
P0=nixietable[number];
Delay(1);
P0=0x00;
}
nixie.h文件中的内容如下:
#ifndef _NIXIE_H_
#define _NIXIE_H_
void nixie(unsigned char location,unsigned char number);//分号!!
#endif
再次强调一下,函数声明必须加分号。如果忘了加,有可能会报错,显示下一个文件的开头有错误。
LCD1602调试工具
简介
- 基于模块化编程的一种工具,既是模块化的一个实例,也为我们代码的调试提供了极大的方便。
- LCD1602液晶屏作为调试窗口时,能够提供类似printf函数的功能,能够实时单片机内部数据的变换情况,便于调试和演示。
- 视频提供的LCD1602代码属于模块化代码。具体函数及其说明如下图所示:
使用前必备小知识~
①组装:将LCD1602插到液晶屏右侧的排孔中(对准,装紧)。
②使用LCD1602后,LCD1602的引脚与数码管(P0口)以及左边三个LED灯发生了冲突,也就是说数码管和边三个LED灯就不能正常使用了,数码管就算有显示也是乱码。
虽然会发生冲突,但我们仍然使用LCD1602进行调试:因为LCD1602只与这几个的引脚产生冲突,相对于整块开发板而言,调试的效率还是很高的。
③可以用螺丝刀调节LCD1602屏幕的对比度。
尝试&注记
下面做一些尝试~
①使用LCD_ShowChar()显示“6-111 FOREVER”:
#include <REGX52.H>
#include "LCD1602.h"
void main()
{
LCD_Init();//初始化,每次使用LCD1602之前都必须调用这个初始化函数。一般来说初始化一次就好了。
LCD_ShowChar(1,1,'');//三个参数,分别是行、列、要显示的字符。字符要用单引号括起来
LCD_ShowChar(1,2,'6');
LCD_ShowChar(1,3,'-');
LCD_ShowChar(1,4,'1');
LCD_ShowChar(1,5,'1');
LCD_ShowChar(1,6,'1');
LCD_ShowChar(1,7,' ');
LCD_ShowChar(1,8,'F');
LCD_ShowChar(1,9,'O');
LCD_ShowChar(1,10,'R');
LCD_ShowChar(1,11,'E');
LCD_ShowChar(1,12,'V');
LCD_ShowChar(1,13,'E');
LCD_ShowChar(1,14,'R');
while(1)
{
}
}
②当需要显示较多字符时,用LCD_ShowChar()函数会方便很多~
注意字符个数不能超出边界(不然直接被吃掉or显示乱码)
#include <REGX52.H>
#include "LCD1602.h"
void main()
{
LCD_Init();
LCD_ShowString(1,3,"jyjj is cute!!");//三个参数分别对应:起始行,起始列,字符串
while(1)
{
}
}
③和Delay函数结合,实现在屏幕上显示读秒:
#include <REGX52.H>
#include "LCD1602.h"
#include "Delay.h"
int Result=0;
void main()
{
LCD_Init();
while(1)
{
Result++;
Delay(1000);
LCD_ShowNum(1,1,Result,3) ;
}
}
其他注记:
①LCD_ShowNum()
三个参数分别是起始行,起始列,给定数字,显示位数
如果显示的位数多于给定数的位数,则高位补零
②LCD_ShowSignedNum()
三个参数分别是起始行,起始列,给定数字,显示位数(正负号不是第一位!)
Signed,可正可负
③LCD_ShowHexNum()
三个参数分别是起始行,起始列,给定十六进制数,显示位数
可以用来验证数据的正确性
④LCD_ShowBinNum()
三个参数分别是起始行,起始列,给定二进制数(不能直接输入二进制数!!通常用十六进制表示),显示位数
可以用来验证数据的正确性
矩阵键盘
(小tips:视频中矩阵键盘的调试都基于LCD1602)
矩阵键盘简介
- 优势:减少l/O口的占用(由16个减少为8个)。矩阵应用得越多,在减少l/O口的占用上越明显。几乎所有的显示器都会采用矩阵方式进行扫描。
- 特点:采用逐行或逐列“扫描”,就可以读出任意位置按键的状态。
- 与数码管的比较:
①扫描方式:
数码管扫描是一种“输出扫描”(显示)
原理:显示第一位 ->显示第二位 ->显示第三位->......,并且快速循环这整个过程,实现所有数码管“同时显示”的效果。
矩阵键盘扫描是一种“输入扫描”(检测)
原理:读取第一行(列)->读取第二行(列)->读取第三行(列)->......,并且快速循环这整个过程,实现所有按键“同时检测”的效果。
值得一提的是,由于内部电路的设计,在本块开发板中如果采用逐行扫描,由于外设的互相干扰,蜂鸣器会自己响。所以我们采用逐列扫描。
这两种扫描的共性:减少l/O口的占用(由16个减少为8个)
②连接方式:
相同之处:其中一端都连在GND上,另一端都连在四个不同的接口上
实例演示
(注意事项及原因说明请看注释)
示例一:读取矩阵键盘的数码值,并显示在LCD1602显示屏上
main.c文件内代码如下所示:
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "Matrixkey.h"
unsigned char KeyNumber;
void main()
{
LCD_Init();
LCD_ShowString(1,1,"Matrixkey");
while(1)
{
KeyNumber=Matrixkey();
if(KeyNumber)//如果if语句的条件是一个变量,那么非零即真
{
LCD_ShowNum(2,1,KeyNumber,2);
}
}
}
Matrixkey.c文件内代码如下所示:
#include <REGX52.H>
#include "Delay.h"
/**
* @brief 矩阵键盘读取按键键码
* @param 无
* @retval KeyNumber 按下按键的数码值
如果按住不放,程序会停留在子函数
松手的一瞬间,返回按键键码
没有按键按下时,返回0
*/
unsigned char Matrixkey()/*这个函数用于处理对按键的操作
根据按键判断按键是否按下
这样能够实现按键的操作和按键的扫描独立开来
从而降低代码的耦合性*/
{
unsigned char KeyNumber=0;
P1=0xFF;//先将P1全部置高
P1_3=0;//要扫描第一列,所以单独把P1_3置零
if (P1_7==0) {Delay(20);while(P1_7==0);Delay(20);KeyNumber=1;}/*如果按下按键
消抖松开后,返回键数*/
if (P1_6==0) {Delay(20);while(P1_6==0);Delay(20);KeyNumber=5;}
if (P1_5==0) {Delay(20);while(P1_7==0);Delay(20);KeyNumber=9;}
if (P1_4==0) {Delay(20);while(P1_7==0);Delay(20);KeyNumber=13;}
P1=0xFF;
P1_2=0;
if (P1_7==0) {Delay(20);while(P1_7==0);Delay(20);KeyNumber=2;}
if (P1_6==0) {Delay(20);while(P1_6==0);Delay(20);KeyNumber=6;}
if (P1_5==0) {Delay(20);while(P1_7==0);Delay(20);KeyNumber=10;}
if (P1_4==0) {Delay(20);while(P1_7==0);Delay(20);KeyNumber=14;}
P1=0xFF;
P1_1=0;
if (P1_7==0) {Delay(20);while(P1_7==0);Delay(20);KeyNumber=3;}
if (P1_6==0) {Delay(20);while(P1_6==0);Delay(20);KeyNumber=7;}
if (P1_5==0) {Delay(20);while(P1_7==0);Delay(20);KeyNumber=11;}
if (P1_4==0) {Delay(20);while(P1_7==0);Delay(20);KeyNumber=15;}
P1=0xFF;
P1_0=0;
if (P1_7==0) {Delay(20);while(P1_7==0);Delay(20);KeyNumber=4;}
if (P1_6==0) {Delay(20);while(P1_6==0);Delay(20);KeyNumber=8;}
if (P1_5==0) {Delay(20);while(P1_7==0);Delay(20);KeyNumber=12;}
if (P1_4==0) {Delay(20);while(P1_7==0);Delay(20);KeyNumber=16;}
return KeyNumber;
}
Matrixkey.c文件内代码如下所示:
#ifndef _MATRIXKEY_H_
#define _MATRIXKEY_H_
unsigned char Matrixkey();
#endif
最终实现的效果是:按下哪个按键,LCD_1602显示屏会在第二行一、二列显示键码
示例二:①矩阵键盘的应用——电子密码锁
main.c文件内代码如下所示:
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "Matrixkey.h"
unsigned char KeyNumber;
unsigned int Password,Count;/*LCD1602能显示数字的范围是0~65535
所以我们确定四位密码就能保证不溢出
所以用unsigned int就可以了*/
void main()
{
LCD_Init();
LCD_ShowString(1,1,"Password:");
while(1)
{
KeyNumber=Matrixkey();
if(KeyNumber)//如果if语句的条件填写一个变量,那么非零即真
{
if(KeyNumber<=10)//如果s1~s10按键按下,输入密码
{
if(Count<4)//引入计数器,防止输入超出范围,出现乱码
{
Password*=10;/密码左移一位
Password+=KeyNumber%10;/*获取一位密码
能够直接解决“用s10表示数字0”的表达
而无需专门写一个if条件判断
很巧妙的设计!*/
Count++;
}
LCD_ShowNum(2,1,Password,4);/*更新显示
最后一个参数4
在这里表示“显示四位密码”*/
}
if(KeyNumber==11) //如果s11按下,确认
{
if(Password==2345)
{
LCD_ShowString(1,14,"OK ");
Delay(100);//如果按太用力容易直接跳转显示“ERR”,所以加了个延迟~
Password=0;//密码清零
Count=0;//计数器清零
LCD_ShowNum(2,1,Password,4);//更新显示
}
else
{
LCD_ShowString(1,14,"ERR");
Password=0;//密码清零
Count=0;//计数器清零
LCD_ShowNum(2,1,Password,4);//¸更新显示
}
}
if(KeyNumber==12) //如果按键s12被按下,取消
{
Password=0; //密码清零
Count=0; //计数器清零
LCD_ShowNum(2,1,Password,4); //更新显示
}
}
}
}
定时器
定时器介绍
①51单片机的定时器属于单片机的内部资源,其电路的连接和运转均在单片机内部完成(按键/数码管/LCD1602是I/O口控制的外设)。
②作用:
(1)用于计时系统,可实现软件计时,或者使程序每隔一定固定时间就完成一项操作(比如采样)
(2)替代长时间的Dealy,提高CPU的运行效率和处理速度
(3)切换任务的执行
(4)......
STC89C52定时器资源
①定时器个数:3个(T0、T1、T2),T0和T1与传统的单片机兼容,T2是此型号单片机增加的资源
②需要注意的是,定时器资源和单片机型号是关联在一起的,不同型号会有不同的定时器和操作方式,但一般来说,T0和T1的操作方式是所有51单片机共有的
定时器框图
定时器在单片机内部根据时钟的输出信号,每隔一定的时间就输出相应的信号,计数单元也增加1。当计数单元增加到设定的“终点”时,计数单元会向中断系统发出中断申请,产生“响铃提醒”,使程序跳转到中断服务函数中执行。具体如下图所示:
定时器工作模式
STC89C52的T0和T1均有四种工作模式:
- 模式0:13位定时器/计数器
- 模式1:16位定时器/计数器(最常用)
- 模式2:8位自动重装模式
- 模式4:两个8位计数器
计时器时钟
- 计时器实质上可以视为一个计数器,数值加到溢出即中断,“闹钟响”
- 计数系统能存2个字节(高字节TH,低字节TL),能够数0到65535(有点像沙漏,可以类比unsigned int)
- 溢出后,计数器会从65535回到0的位置,会置一个标志位(TF0),申请中断
- TR0:控制定时器启动和暂停
- 时钟有两个来源:①系统时钟(SYSclk),由晶振提供脉冲,晶振周期为(12MHz)。当把SYSclk接到12分频器时(如上图所示),周期就是1μs,即每隔1μs记一次数。 ②外部引脚(用T0口)提供的时钟。时钟提供“脉冲”,每检测到一个脉冲,计数器就+1。
- C/T0:定时器 C/T1:计数器(选择C/T1还是C/T0的过程其实是配置寄存器的过程)
中断系统
相关概念~
中断系统是为CPU具有对外界紧急时间的实时处理能力而设置的。很多外设都需要中断系统,比如定时器,I/O口,串口等等,都需要。
中断:当中央处理器CPU正在处理某件事的时候外界发生了紧急事件请求,要求CPU暂停当时的工作,转而去处理这个紧急时间,处理完以后,再回到原来被中断的地方,继续原来的工作,这样的过程称为中断。
中断源:请示CPU中断的请求源成为中断源(比如定时器溢出就是一个中断源)。微型机中断系统一般允许多个中断源,当几个中断源同时向CPU请求中断时,CPU总是先相应优先级别高的中断请求。
中断程序流程:
注记:当定时器溢出,即发出中断请求时,那么执行就会跳转到中断处理程序。执行完后,再返回断点,继续执行主函数。这就相当于同时完成两项任务,能够大大提高效率。
STC89C52的中断资源
- 中断源个数:外部中断0,定时器0中断,外部中断1,定时器1中断,串口中断,外部中断2,外部中断3(共8个)。
- 中断优先级个数:4个
- 中断号:如果使用C语言编程,查询次序号就是中断号(程序的定义与普通的子函数的区别:名字后面多了interrup+数字,数字表示中断入口)。
- 中断的资源和单片机的型号是关联在一起的,不同型号可能会有不同的中断系资源,例如中断源个数不同、中断优先级个数不同等等。
定时器相关寄存器
- 简介
寄存器是连接软硬件的媒介,相当于一个复杂机器的“操作按钮”
在单片机中,寄存器就是一段特殊的RAM存储器。
一方面,寄存器可以存储和读取数据;另一方面,每一个寄存器背后都连接了一根导线,控制着电路的连接方式(单片机通过配置寄存器来控制内部线路的连接,从而使不同电路完成不同的功能)。
可位寻址&不可位寻址:
①可位寻址寄存器可以整体赋值,也可以单独对某一位赋值;②不可位寻址寄存器只能整体赋值
- 定时器/计数器控制寄存器TCON(timer control)
SFR name Address bit B7 B6 B5 B4 B3 B2 B1 B0
TCON 88H name TF1 TR1 TF0 TR0 IE1 ITI IE0 IT0
TF1:
定时器/计数器T1溢出标志。T1被允许计数以后,从初值开始加1计数。当最高位产生溢
出时由硬件置“1”TF1,向CPU请求中断,一直保持到CPU响应中断时,才由硬件清“0”TF1 (TFI也可由程序查询清“0”)。
TR1:
定时器T1的运行控制位。该位由软件置位和清零。当GATE(TMOD7)=0,TR1=1时就
允许T1开始计数,TR1=0时禁止T1计数。当GATE(TMOD.7)=1,TR1=1且INTT输入高电平时,才允许T1计数。
TF0:
定时器/计数器T0溢出中断标志。T0被允许计数以后,从初值开始加1计数,当最高位产
生溢出时,由硬件置“1”TF0,向CPU请求中断,一直保持CPU响应该中断时,才由硬件清“0”TF0(TF0也可由程序查询清“0”)。这一位一般只读不写。
TR0:
定时器T0的运行控制位。该位由软件置位和清零。当GATE(TMOD.3)=0,TR0=1时就
允许T0开始计数,TR0=0时禁止T0计数。当GATE(TMOD.3)=1,TR0=0且INT0输入高电平时,才允许T0计数。
IE1:
外部中断1请求源(INTI/P3.3)标志。IE1=1,外部中断向CPU请求中断,当CPU响应该
中断时由硬件清“0”IE1。
IT1:
外部中断1触发方式控制位。IT1=0时,外部中断1为低电平触发方式,当INTI(P3.3)
输入低电平时,置位IE1,采用低电平触发方式时,外部中断源(输入到INTI)必须保持低电平有效,直到该中断被CPU响应,同时在该中断服务程序执行完之前,外部中断源必须被清除(P3.3要变高),否则将产生另一次中断。当IT1=1时,则外部中断1(INTI)端口由“1”→“0”下降沿跳变,激活中断请求标志位IE1,向主机请求中断处理。
- 定时器/计数器工作模式寄存器TMOD(timer mode)
M0,M1:
控制定时器/计数器的工作模式
TMOD.1/TMOD.0 M1、M0 定时器/计数器0模式选择
0 0 13位定时器/计数器,兼容8048定时模式,TL1只用低5位参与
分频,TH1整个8位全用。
0 1 16位定时器/计数器,TL1、TH1全用
1 0 8位自动重装载定时器,当溢出时将TH1存放的值自动重装入TL1
1 1 定时器0此时作为双8位定时器/计数器。
- C/T~置0:定时器;C/T~置1:计数器
- GATE(门控端):
启动or暂停可以由TR0单独控制,也可以由TR0和外部中断联合控制,使用哪种控制方式由GATE的置位控制。
当GATE置0,TR0为1时,定时器启动
当GATE置1,只有外部引脚(NT0:外部中断引脚)为高且TR0为1时,定时器启动
- 中断允许寄存器IE和XICON
EA(interrupt all):
CPU的总中断允许控制位,EA=1,CPU开放中断,EA=0,CPU屏蔽所有的中断申请。
EA的作用是使中断允许形成两级控制。即各中断源首先受EA控制;其次还受各中断源自己的中断允许控制位控制。
ET2:
定时/计数器T2的溢出中断允许位。ET2=1,允许T2中断:ET2=0,禁止T2中断。
ES:
串行口1中断允许位。ES=1,允许串行口1中断;ES=0,禁止串行口1中断。
ET1:
定时/计数器T1的溢出中断允许位。ET1=1,允许T1中断;ET1=0,禁止T1中断。
EX1:
外部中断1中断允许位。EX1=1,允许外部中断1中断;EX1=0,禁止外部中断1中断。
ET0:
T0的溢出中断允许位。ET0=1,允许T0中断:ET0=0禁止T0中断
EX0:
外部中断0中断允许位。EX0=1,允许中断;EX0=0禁止中断。
- 中断优先级控制寄存器高(不可位寻址)
① PX3H,PX3:外部中断3优先级控制位
当PX3H=0且PX3=0时,外部中断3为最低优先级中断(优先级0)
当PX3H=0且PX3=1时,外部中断3为较低优先级中断(优先级1)
当PX3H=1且PX3=0时,外部中断3为较高优先级中断(优先级2)
当PX3H=1且PX3=1时,外部中断3为最高优先级中断(优先级3)
②PX2H,PX2:外部中断2优先级控制位
当PX2H=0且PX2=0时,外部中断2为最低优先级中断(优先级0)
当PX2H=0且PX2=1时,外部中断2为较低优先级中断(优先级1)
当PX2H=1且PX2=0时,外部中断2为较高优先级中断(优先级2)
当PX2H=1且PX2=1时,外部中断2为最高优先级中断(优先级3)
- 中断优先级控制寄存器低(可位寻址)
①PT1:定时器1中断优先级控制位
PT1=1,定时器1中断为最高优先级
PT1=0,定时器1中断为最低优先级
PX1:外部中断1优先级控制位
PX1=1,外部中断1为最高优先级
PX1=0,外部中断1为最低优先级
②PT0:定时器1中断优先级控制位
PT0=1,定时器0中断为最高优先级
PT0=0,定时器0中断为最低优先级
③PX0:外部中断0优先级控制位
PX0=1,外部中断0为最高优先级
PX0=0,外部中断0为最低优先级
实例演示
按键控制LED流水灯模式
效果:在LED模块呈现以500ms(由计时器控制)为周期的流水灯;每次按下按键K1,流水灯反向
//Timer0.c文件:
#include <REGX52.H>
#include "Timer0.h"
/**
* @brief 定时器初始化,周期为1ms 12MHz
* @param 无
* @retval 无
*/
void Timer0_Init()//1ms
{
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0=1;//允许T0中断
EA=1;//CPU开放中断
PT0=0;//定时器0为最低优先级
}
//Timer0.c
#ifndef _TIMER0_H_
#define _TIMER0_H_
void Timer0_Init();
#endif
//Key.c
#include <REGX52.H>
#include "Delay.h"
/**
* @brief 获取独立按键键码
* @param 无
* @retval 键码,范围是0~4 */
unsigned char Key()
{
unsigned char KeyNumber=0;
if(P3_1==0){Delay(20);while(P3_1==0);KeyNumber=1;}
if(P3_0==0){Delay(20);while(P3_0==0);KeyNumber=2;}
if(P3_2==0){Delay(20);while(P3_2==0);KeyNumber=3;}
if(P3_3==0){Delay(20);while(P3_3==0);KeyNumber=4;}
return KeyNumber;
}
//Key.h
#ifndef _KEY_H_
#define _KEY_H_
unsigned char Key();
#endif
//main.c
#include < REGX52.H>
#include "Time.h"
#include "Key.h"
#include <INTRINS.H>
unsigned char KeyNum,LEDMode;
void main()
{
P2=0xFE;//点亮第一个LED
Timer0_Init();
while(1)
{
KeyNum=Key();
if(KeyNum)
{
if(KeyNum==1)
{
LEDMode++;
if(LEDMode>=2)LEDMode=0;
}
}
}
}
void Time0_Routine() interrupt 1
{
static unsigned int T0Count;//设置静态变量,在子函数结束后仍然占位
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if(T0Count>=500)//数500ms
{
T0Count=0;
if(LEDMode==0)
P2=_crol_(P2,1);
if(LEDMode==1)
P2=_cror_(P2,1);
}
}
注记:小tips~
①实现每隔一秒产生中断的思路::
赋初值 0~65535,每隔1us计数加一,总共能记录的时间为65535us
记满之后才溢出——赋初值为64535,离计算器起初还差10个数,所以计时时间为1ms
②采用“与或赋值法”就能达到单独操作某些位的目的!
TMOD=TMOD&0xF0(TMOD&=0xF0):把TMOD低四位清零,高四位保持不变
原因:1&任何数->任何数,0&任何数->0
TMOD=TMOD|0x01(TMOD|=0x01):把TMOD最低位置1,高四位保持不变
原因:0|任何数->任何数,1|任何数->1
③可以直接用STC—ISP里面“定时器计算器”的功能生成计时器代码!不过要记得配置相关参数(以下以定时1毫秒为例)
④实现流水灯新方法:
调用INTRINS函数库,使用“crol”(循环左移)和“cror”(循环右移)这两个函数
这两个函数分别都需要两个参数:数值,移动的位数
好处:溢出后能够重新从零开始不需要做越界判断
定时器时钟
//main.c(需要调用之前写过的模块)
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "Timer0.h"
unsigned int Sec=50;
unsigned int Min=59;
unsigned int Hour=23; //实际应用中可以设置一个准确的时间~这里这样写只是基于验证进位的需要
void main()
{
LCD_Init();
Timer0_Init();
while(1)
{
LCD_ShowString(1,1,"Clock:");
LCD_ShowNum(2,1,Hour,2);
LCD_ShowString(2,3,":");
LCD_ShowNum(2,4,Min,2);
LCD_ShowString(2,6,":");
LCD_ShowNum(2,7,Sec,2);
}
}
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x18;
TH0 = 0xFC;
T0Count++;
if(T0Count>=1000) {
Sec++;
T0Count=0;
if(Sec>=60)
{
Min++;
Sec=0;
if(Min>=60)
{
Hour++;
Min=0;
if(Hour>=24)
{
Hour=0;
}
}
}
}
}
串口通讯
串口介绍
- 串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个或多个设备的互相通信。
- 单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大的扩展了单片机的应用范围,增强了。单片机系统的硬件实力。
- 51单片机内部自带UART (Universal Asynchronous Receiver Transmitter,通用异步收发器),可实现单片机的串口通信。
硬件电路
- 简单双向串口通信有两根通信线(发送端TXD和接收端RXD)
- 发送端TXD和接收端RXD交叉连接(一个发送,另一个接收。其实很好理解,同一时间一个设备不能既发送又接收)
- 当只需单向的数据传输时,可以只接一根通信线
- 搭建串口通信前要了解电压、电平协议。如果协议不同可以则需要电平转化芯片(不然会烧坏)。
- 单片机和电脑进行串口通讯时,需要用到串口助手。蓝牙通讯往往会有一个app充当“串口助手”
- 普遍来说,通信至少需要三根线(多了一根供电线)。如果设备一、设备二都有独立供电,那么不需要VCC;如果其中一个是模块,需要另一个设备供电,则需要VCC,使得具有独立供电能力的设备向模块供电
电平标准
- 传输线缆中认为规定的电压与数据的对应关系,串口常用的电平标准有以下几种
①TTL电平:+5V表示1,0V表示0
②RS232电平:-3~-15V表示1,+3~+15V表示0
③RS485电平:两线压差+2~+6V表示1,-2~-6V表示0(差分信号)
- 差异
RS232是反逻辑电平,数据传输最多十多米;
RS485最多能达到千米级别,这有赖于差分传输的高稳定性(USB就是)
接口及引脚定义
DSR,RTS属于数据流控制(流控)
可以用于调整发送和接受的速率,但是C51不支持
常见通信接口比较
- UART TXD RXD:属于点对点通信 ,严格上的用法只能一对一
- I方C:单片机与其他设备进行串口通信时,数据的写入读入都依赖I方C
- CAN传输稳定性好,运用了差分传输 ,在汽车上有应用
- 全双工:通信双方可以同一时刻互相传输数据
- 半双工:通信双方可以互相传输数据,但必须分时复用一根数据线
- 单工:只能一方发给另一方,单线传播,比如遥控器
- 同步:对时间的要求很严格,通信双方靠一根时钟线来约定通信速率
- 异步:通信双方各自约定通信速率,对时间要求那么严格,没有时钟线
- 总线:连接各个设备数据的传输线路,可以说USB/SPI/I方C/......总线,但没有“串口总线”这一提法
51单片机的UART
- STC89C52有1个UART
- STC89C52的UART有四种模式,其中最常用的是模式1(八位UART,波特率可变)
- P3口 两份线 I/O口与串口复用 通过寄存器来配置
- 两个单片机一般可以直接进行串口通信
- 串口是内部资源,不需要再额外写入
串口参数及时序图
- 波特率:可以被理解为一个设备在单位时间内发送(或接收)了多少码元的数据。它是对符号传输速率的一种度量,表示单位时间内传输符号的个数。当串口通讯异步时,就需要约定一个 波特率
- 比特率:与上类似,表示数据采样等的速率,保证数据同步
- 检验位:用于数据验证。9位,多一位可以用来校验。奇偶校验最常用,但是排错率不高。如果双方都翻转了,就检测不到了。使用校验之前双方要约定好、配置好
- 停止位:用于数据帧间隔
- 串口 串型 一位一位收发 每次一个字节
串口模式图
- 都是单片机内部的东西。由TXD RXD连成的一整片都是总线
- 靠定时器溢出率来控制数据采样的收发速率
- 写入 读入:先把8位数据写入缓存 再控制发送 接受时也是一位一位移到寄存器中,需要数据时再读出 发的时候只要写入 就能自动发送。发完后会产生发送中断;读的时候 当外部有数据时自动写入寄存器,并且生接收中断。发送中断可以不用管 接受中断能够提醒我们有数据进来
串口和中断系统
- 串口通讯可以概括为:收发过程+中断系统
- 申请中断 去中断 发送完成or接受完成 后在同一个线路产生中断(通过R1还是I1区分两者)
- 使用单片机进行串口通讯时,中断优先级不是必须配置的,当中断优先级较多时才需要
串口相关寄存器
- 属于特殊功能寄存器
- 每一位下面都有一根连线
- 电源控制寄存器的前两位和串口有关
- STC89C52有四个优先级,所以需要两个寄存器来控制
实例演示
实现效果:电脑通过发送数据控制LED,并且单片机会把数据返回给电脑
部分代码如下所示:
//main.c:
#include <REGX52.H>
#include "Delay.h"
#include "UART.h"
void main()
{
UART_Init();//初始化
while(1)
{
}
}
void UART_Routine() interrupt 4
{
if(R1==1)
{
P2=SBUF;
UART_SendByte(SBUF);
RI=0;//硬件只能置1,要用软件复位
}
//UART.c
void UART_Init()//2400bps@12.000MHz
{
SCON=0x50;//ÔÊÐí½ÓÊÕ
PCON |=0x80;
TMOD &= 0x0F;
TMOD |= 0x20;
TL1 = 0xF3;
TH1 = 0xF3;
ET1 = 0;
TR1 = 1;
EA=1;
ES=1;
}
void UART_SendByte(unsigned char Byte)
{
SBUF=Byte;
while(TI==0);//检测
TI=0;//复位
}
相关注记:
- 配置16位计时器两个语句会占用时间,影响精度,所以在串口中使用双八位(0~255),自动存装
- 波特率加倍的原因:后面会分频
- 输入不需要配置中断。只要计时器有溢出,就能形成波特率
- 如果误差较大,可以设置发送速度慢一些。波特率越低,发送越稳定。如果在发送时有问题,可以写个延迟
- 同一个函数不能既在主函数里调用又在中断函数里调用,函数的重录可能导致错误
- 串口助手:HEX模式/十六进制模式——以原始数据形式显示;文本模式——以编码模式显示
LED点阵屏
简介
- LED点阵屏由若干个独立的LED组成,LED以矩阵的形式排列,以灯珠亮灭来显示文字、图片、视频等。
- LED点阵屏广泛应用于各种公共场合,如汽车报站器、广告屏以及公告牌等
- LED点阵屏分类:
按颜色:单色、双色、全彩
按像素:8*8、16*16等(大规模的LED点阵通常由很多个小点阵拼接而成) 一般是八的倍数,可以充分利用内存
显示原理
- 结构类似于数码管,只不过是数码管把每一列的像素以"8"字型排列而已,所以操作方式可以相互借鉴~
- LED点阵屏与数码管一样,有共阴和共阳两种接法(使用之前要了解清楚,特别是多色点阵屏)。不同的接法对应的电路结构不同,引脚是乱序排列的,使用之前要验证一下
- LED点阵屏需要进行逐行或逐列扫描(由于I/O的有限),才能使所有LED同时显示
74HC595
- 74HC595是串行输入并行输出的移位寄存器,可用3根线输入串行数据,由八根线输出并行数据,多片级联后,可输出16位、24位、32位等,常用于IO口扩展。
- 上升沿移位&上升沿锁
上升沿移位用于“排队”默认是低电平,写入数据后记得清零
上升沿锁存用于“搬运”,默认高电平
- 输入会有延时
C51的sfr、sbit
- sfr (special function register)
特殊功能寄存器声明
例: sfr P0 =0x80;
声明PO口寄存器,物理地址为0x80
- sbit(special bit)
特殊位声明
例: sbit P0_1=0x81;或sbit p0_1=P0^1;
声明PO寄存器的第1位
可位寻址/不可位寻址:在单片机系统中,操作任意寄存器或者某一位的数据时,必须给出其物理地址
又因为一个寄存器里有8位,所以位的数量是寄存器数量的8倍,单片机无法对所有位进行编码,故每8个寄存器中,只有一个是可以位寻址的。
对不可位寻址的寄存器,若要只操作其中一位而不影响其它位时,可用“&=”、“|=”、"^="(对某一位进行取反)的方法进行位操作
实例演示
效果:在LED点阵屏上显示弹幕“ZL!!!”
核心代码如下所示:
#include <REGX52.H>
#include "Delay.h"
#include "MATRIX.h"
unsigned char code Animation[]={0x81,0x83,0x85,0x89,0x91,0xA1,0xC1,0x81,
0x00,0x00,0x00,0xFF,0x01,0x01,0x01,0x01,
0x00,0x00,0x00,0xFD,0x00,0x00,0x00,0xFD,
0x00,0x00,0x00,0xFD,0x00,0x00,0x00,0x00,};//由字模提取插件生成 存在flash中
void main()
{
unsigned char offset=0;//偏移量
unsigned char i;
unsigned char count=0;//用于控制滚动的速度
MatrixLED_Init();//初始化
while(1)
{
for(i=0;i<8;i++)
{
MatrixLED_ShowColumn(i,Animation[i+offset]);
}
count++;
if(count>10) {
count=0;
offset++;
if(offset>24)//防止数组溢出
{
offset=0;
}
}
}
}
注记:
- 和数码管扫描相类似,也要对LED点阵屏进行消影处理 。段选 位选 段选->段选 位选 延时 位清零
- 将数组存储在flash里面(不能更改),不会占据RAM的空间
DS1302实时时钟
内部结构框图
引脚定义和应用电路
- 一般都是用这个频率的晶振(石英),稳定性最高,精度最高
- 任务:读取和写入,不需要配置寄存器
寄存器定义
时序定义
- 在时钟的上升沿写入数据
- 在时钟的下降沿读出数据
实例演示
时钟~显示年月日时分秒
//main.c
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
void main()
{
LCD_Init();
DS1302_Init();
LCD_ShowString(1,1," - - ");
LCD_ShowString(2,1," : : ");
DS1302_SetTime();//设置时间
while(1)
{
DS1302_ReadTime();
LCD_ShowNum(1,1,DS1302_Time[0],2);//年
LCD_ShowNum(1,4,DS1302_Time[1],2);//月
LCD_ShowNum(1,7,DS1302_Time[2],2);//日
LCD_ShowNum(2,1,DS1302_Time[3],2);//时
LCD_ShowNum(2,4,DS1302_Time[4],2);//分
LCD_ShowNum(2,7,DS1302_Time[5],2);//秒
}
}
//DS1302.C
#include <REGX52.H>
sbit DS1302_SCLK=P3^6;
sbit DS1302_IO=P3^4;
sbit DS1302_CE=P3^5;
#define DS1302_SECOND 0x80
#define DS1302_MINUTE 0x82
#define DS1302_HOUR 0x84
#define DS1302_DATE 0x86
#define DS1302_MONTH 0x88
#define DS1302_DAY 0x8A
#define DS1302_YEAR 0x8C
#define DS1302_WP 0x8E
unsigned char DS1302_Time[]={23,6,6,23,59,50,2};//年、月、日、时、分、秒、星期
/**
* @brief DS1302初始化
* @param 无
* @retval 无
*/
void DS1302_Init(void)
{
DS1302_CE=0;
DS1302_SCLK=0;
}
/**
* @brief DS1302写一个字节
* @param Command 命令字 地址
* @param Data 要写入的数据
* @retval 无
*/
void DS1302_WriteByte(unsigned char Command,Data)
{
unsigned char i;
DS1302_CE=1;
for(i=0;i<8;i++)
{
DS1302_IO=Command&(0x01<<i);
DS1302_SCLK=1;
DS1302_SCLK=0;
}
for(i=0;i<8;i++)
{
DS1302_IO=Data&(0x01<<i);
DS1302_SCLK=1;
DS1302_SCLK=0;
}
DS1302_CE=0;
}
/**
* @brief DS1302读一个字节
* @param Command 命令字 地址
* @retval 读出的数据
*/
unsigned char DS1302_ReadByte(unsigned char Command)
{
unsigned char i,Data=0x00;
Command|=0x01; DS1302_CE=1;
for(i=0;i<8;i++)
{
DS1302_IO=Command&(0x01<<i);
DS1302_SCLK=0;
DS1302_SCLK=1;
}
for(i=0;i<8;i++)
{
DS1302_SCLK=1;
DS1302_SCLK=0;
if(DS1302_IO){Data|=(0x01<<i);}
}
DS1302_CE=0;
DS1302_IO=0;
return Data;
}
/**
* @brief DS1302设置时间
* @param 无
* @retval 无
*/
void DS1302_SetTime(void)
{
DS1302_WriteByte(DS1302_WP,0x00);
DS1302_WriteByte(DS1302_YEAR,DS1302_Time[0]/10*16+DS1302_Time[0]%10);
DS1302_WriteByte(DS1302_MONTH,DS1302_Time[1]/10*16+DS1302_Time[1]%10);
DS1302_WriteByte(DS1302_DATE,DS1302_Time[2]/10*16+DS1302_Time[2]%10);
DS1302_WriteByte(DS1302_HOUR,DS1302_Time[3]/10*16+DS1302_Time[3]%10);
DS1302_WriteByte(DS1302_MINUTE,DS1302_Time[4]/10*16+DS1302_Time[4]%10);
DS1302_WriteByte(DS1302_SECOND,DS1302_Time[5]/10*16+DS1302_Time[5]%10);
DS1302_WriteByte(DS1302_DAY,DS1302_Time[6]/10*16+DS1302_Time[6]%10);
DS1302_WriteByte(DS1302_WP,0x80);
}
/**
* @brief DS1302读取时间
* @param 无
* @retval 无
*/
void DS1302_ReadTime(void)
{
unsigned char Temp;
Temp=DS1302_ReadByte(DS1302_YEAR);
DS1302_Time[0]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_MONTH);
DS1302_Time[1]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_DATE);
DS1302_Time[2]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_HOUR);
DS1302_Time[3]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_MINUTE);
DS1302_Time[4]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_SECOND);
DS1302_Time[5]=Temp/16*10+Temp%16;
Temp=DS1302_ReadByte(DS1302_DAY);
DS1302_Time[6]=Temp/16*10+Temp%16;
}
可调时钟
//main.c
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
#include "Matrixkey.h"
#include "Timer0.h"
unsigned char KeyNum,MODE,TimeSetSelect,TimeSetFlashFlag;
void TimeShow(void)
{
DS1302_ReadTime();
LCD_ShowNum(1,1,DS1302_Time[0],2);
LCD_ShowNum(1,4,DS1302_Time[1],2);
LCD_ShowNum(1,7,DS1302_Time[2],2);
LCD_ShowNum(2,1,DS1302_Time[3],2);
LCD_ShowNum(2,4,DS1302_Time[4],2);
LCD_ShowNum(2,7,DS1302_Time[5],2);
}
void TimeSet(void)
{
if(KeyNum==2)
{
TimeSetSelect++;
TimeSetSelect%=6;
}
if(KeyNum==3)
{
DS1302_Time[TimeSetSelect]++;
if(DS1302_Time[0]>99){DS1302_Time[0]=0;}
if(DS1302_Time[1]>12){DS1302_Time[1]=1;}
if( DS1302_Time[1]==1
|| DS1302_Time[1]==3
|| DS1302_Time[1]==5
|| DS1302_Time[1]==7
|| DS1302_Time[1]==8
|| DS1302_Time[1]==10
|| DS1302_Time[1]==12)
{
if(DS1302_Time[2]>31){DS1302_Time[2]=1;}
}
else if(DS1302_Time[1]==4
|| DS1302_Time[1]==6
|| DS1302_Time[1]==9
|| DS1302_Time[1]==11)
{
if(DS1302_Time[2]>30){DS1302_Time[2]=1;}
}
else if(DS1302_Time[1]==2)
{
if(DS1302_Time[0]%4==0)
{
if(DS1302_Time[2]>29){DS1302_Time[2]=1;}
}
else
{
if(DS1302_Time[2]>28){DS1302_Time[2]=1;}
}
}
if(DS1302_Time[3]>23){DS1302_Time[3]=0;}
if(DS1302_Time[4]>59){DS1302_Time[4]=0;}
if(DS1302_Time[5]>59){DS1302_Time[5]=0;}
}
if(KeyNum==4)
{
DS1302_Time[TimeSetSelect]--;
if(DS1302_Time[0]<0){DS1302_Time[0]=99;}
if(DS1302_Time[1]<1){DS1302_Time[1]=12;}
if( DS1302_Time[1]==1
|| DS1302_Time[1]==3
|| DS1302_Time[1]==5
|| DS1302_Time[1]==7
|| DS1302_Time[1]==8
|| DS1302_Time[1]==10
|| DS1302_Time[1]==12)
{
if(DS1302_Time[2]<1){DS1302_Time[2]=31;}
if(DS1302_Time[2]>31){DS1302_Time[2]=1;}
}
else if(DS1302_Time[1]==4
|| DS1302_Time[1]==6
|| DS1302_Time[1]==9
|| DS1302_Time[1]==11)
{
if(DS1302_Time[2]<1){DS1302_Time[2]=30;}
if(DS1302_Time[2]>30){DS1302_Time[2]=1;}
}
else if(DS1302_Time[1]==2)
{
if(DS1302_Time[0]%4==0)
{
if(DS1302_Time[2]<1){DS1302_Time[2]=29;}
if(DS1302_Time[2]>29){DS1302_Time[2]=1;}
}
else
{
if(DS1302_Time[2]<1){DS1302_Time[2]=28;}
if(DS1302_Time[2]>28){DS1302_Time[2]=1;}
}
}
if(DS1302_Time[3]<0){DS1302_Time[3]=23;}
if(DS1302_Time[4]<0){DS1302_Time[4]=59;}
if(DS1302_Time[5]<0){DS1302_Time[5]=59;}
}
if(TimeSetSelect==0 && TimeSetFlashFlag==1){LCD_ShowString(1,1," ");}
else {LCD_ShowNum(1,1,DS1302_Time[0],2);}
if(TimeSetSelect==1 && TimeSetFlashFlag==1){LCD_ShowString(1,4," ");}
else {LCD_ShowNum(1,4,DS1302_Time[1],2);}
if(TimeSetSelect==2 && TimeSetFlashFlag==1){LCD_ShowString(1,7," ");}
else {LCD_ShowNum(1,7,DS1302_Time[2],2);}
if(TimeSetSelect==3 && TimeSetFlashFlag==1){LCD_ShowString(2,1," ");}
else {LCD_ShowNum(2,1,DS1302_Time[3],2);}
if(TimeSetSelect==4 && TimeSetFlashFlag==1){LCD_ShowString(2,4," ");}
else {LCD_ShowNum(2,4,DS1302_Time[4],2);}
if(TimeSetSelect==5 && TimeSetFlashFlag==1){LCD_ShowString(2,7," ");}
else {LCD_ShowNum(2,7,DS1302_Time[5],2);}
}
void main()
{
LCD_Init();
DS1302_Init();
Timer0_Init();
LCD_ShowString(1,1," - - ");
LCD_ShowString(2,1," : : ");
DS1302_SetTime();
while(1)
{
KeyNum=Matrixkey();
if(KeyNum==1)
{
if(MODE==0){MODE=1;TimeSetSelect=0;}
else if(MODE==1){MODE=0;DS1302_SetTime();}
}
switch(MODE)
{
case 0:TimeShow();break;
case 1:TimeSet();break;
}
}
}
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x18;
TH0 = 0xFC;
T0Count++;
if(T0Count>=500)
{
T0Count=0;
TimeSetFlashFlag=!TimeSetFlashFlag;
}
}