51单片机学习笔记
1 模块化编程
1.1 什么是模块化编程
模块化编程是指将大型、笨拙的编程任务分解为单独的、更小更易于管理的子任务或模块的过程。然后可以像构建块一样拼凑单个模块以创建更大的应用程序。
1.2 模块化编程的好处
1.模块化代码更易于理解、调试和维护。
2.模块化代码可以提高代码的可重用性和可维护性。
3.模块化代码可以提高代码的可扩展性和可升级性。
4.模块化代码可以提高代码的可靠性和稳定性。
1.3 如何实现模块化编程
首先构建一个c函数文件,一个头文件
在c函数文件中写出函数原型,在头文件中声明
#ifndef __min_H__//这里写函数名(加如是min函数)
#define __min_H__
int min();//这里声明函数
#endif
#inclue "min.h"//如果在main函数文件夹中用双引号,其他最好用<>,放在软件的根目录等地外要添加地址
2 矩阵按键
2.1 什么是矩阵按键
以上便是矩阵键盘的原理图
P1_0到3是1到4列的IO口,P1_4到7是1到4行的IO口,如果是列扫描P1_0给0,s4按键按下,P1_7检测到0,然后就能执行下一步。通过这种方式检测节约了管脚,使得只需8个管脚便能控制16个按键。
2.2 按键读取方式
方式一
逐行扫描:我们可以通过高四位轮流输出低电平来对矩阵键盘进行逐行扫描,当低四位接收到的数据不全为1的时候,说明有按键按下,然后通过接收到的数据是哪一位为0来判断是哪一个按键被按下。
方法二:
行列扫描:我们可以通过高四位全部输出低电平,低四位输出高电平。当接收到的数据,低四位不全为高电平时,说明有按键按下,然后通过接收的数据值,判断是哪一列有按键按下,然后再反过来,高四位输出高电平,低四位输出低电平,然后根据接收到的高四位的值判断是那一行有按键按下,这样就能够确定是哪一个按键按下了。
2.3 驱动设计
#include <REGX52.H>
#include "Delay.h"
/**
* @brief 矩阵键盘读取按键键码
* @param 无
* @retval KeyNumber 按下按键的键码值
如果按键按下不放,程序会停留在此函数,松手的一瞬间,返回按键键码,没有按键按下时,返回0
*/
unsigned char MatrixKey()
{
unsigned char KeyNumber=0;
P1=0xFF;
P1_3=0;
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_5==0);Delay(20);KeyNumber=9;}
if(P1_4==0){Delay(20);while(P1_4==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_5==0);Delay(20);KeyNumber=10;}
if(P1_4==0){Delay(20);while(P1_4==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_5==0);Delay(20);KeyNumber=11;}
if(P1_4==0){Delay(20);while(P1_4==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_5==0);Delay(20);KeyNumber=12;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=16;}
return KeyNumber;
}
3 定时器
3.1 CPU时序的有关知识
振荡周期:为单片机提供定时信号的振荡源的周期(晶振周期或外加振荡周期)
状态周期:2个振荡周期为1个状态周期,用S表示。振荡周期又称S周期或时钟周期。
机器周期:1个机器周期含6个状态周期,12个振荡周期。
指令周期:完成1条指令所占用的全部时间,它以机器周期为单位。
下面给个例子
例如:外接晶振为12MHz时,51单片机相关周期的具体值为:
振荡周期=1/12us;
状态周期=1/6us;
机器周期=1us;
指令周期=1~4us;
3.2 定时器工作原理
定时/计数器实质上是一个加1计数器。它随着计数器的输入脉冲进行自加1,也就是每来一个脉冲,计数器就自动加1,,当加到计数器为全1时,再输入一个脉冲就使计数器回零,且计数器的溢出使相应的中断标志位置1,向CPU发出中断请求(定时/计数器中断允许时)。如果定时/计数器工作于定时模式,则表示定时时间已到;如果工作于计数模式,则表示计数值已满。
内部结构图
下面两个灰矩形是TCON是控制寄存器,控制T0、T1的启动和停止及设置溢出标志。
下面是其原理图
首先可以看到其字节地址为89H,低四位用于T0,高四位用于T1
- GATE是门控位, GATE=0时,用于控制定时器的启动是否受外部中断源信号的影响。GATA=1时,要用软件使TR0或TR1为1,同时外部中断引脚INT0/1也为高电平时,才能启动定时/计数器工作。
- C/T表示置1为计数器,置0为计时器
- M1M0:工作方式设置位。定时/计数器有四种工作方式。看下图详解,一般我们用的是01模式
模式01
这里我也想介绍一下与或非门
与门:有0置0,只有输入1,1会输出1
或门:有1置1,只有输入0,0会输出0
非门:非门的逻辑功能是对输入信号进行取反,即当输入为1时,输出为0,输入为0时,输出为1。
于是我们可以这样写代码保证某个定时器正常运转
TMOD &= 0xF0; //设置定时器模式(低4位清0,高四位不变)
TMOD |= 0x01; //设置定时器模式(最低位置1,高四位不变)
下面是中断结构,中断信号从定时器发出后到T0口,开始配置中断优先级
所以ET0要置1,EA置1,PT0置0或1自行选择,置1为高优先级,置0为低优先级(上面的图连错了,最后应该连到下面)
3.3 总的定时器初始化代码
void Timer0Init(void)
{
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0=1; //其实上面的可以用STC来生成,我们只需配置下面代码即可
EA=1;
PT0=0;
}
使用STC生成定时器初始化代码
4 串口
4.1 什么是串口
串行接口简称串口,也称串行通信接口或串行通讯接口(通常指COM接口),是采用串行通信方式的扩展接口。其特点是通信线路简单,只要一对传输线就可以实现双向通信(可以直接利用电话线作为传输线),从而大大降低了成本,特别适用于远距离通信,但传送速度较慢。
通信方式
单工是指数据传输仅能沿一个方向,不能实现反向传输。
半双工是指数据传输可以沿两个方向,但需要分时进行。
全双工是指数据可以同时进行双向传输。
4.2 工作原理
VCC意思是正极电源电压,GND意思是地线或0线
- 简单双向串口通信有两根通信线(发送端TXD和接收端RXD)
- TXD与RXD交叉连接
配置寄存器
串行口工作之前,应对其进行初始化,主要是设置产
生波特率的定时器1、串行口控制和中断控制。具体
步骤如下:
确定T1的工作方式(编程TMOD寄存器);
TI:发送中断标志位。在方式 0 时,当串行发送第 8 位数据结束时,或在其它方式,串行发送停止位的开始时,由内部硬件使 TI 置 1,向 CPU 发中断申请。 在中断服务程序中,必须用软件将其清 0,取消此中断申请.
计算T1的初值,装载TH1、TL1;
启动T1(编程TCON中的TR1位);
确定串行口控制(编程SCON寄存器);
串行口在中断方式工作时,要进行中断设置(编程IE、
IP寄存器)。
4.3 接收,输出函数
初始化函数
void UART_Init()
{
SCON=0x50;
PCON |= 0x80;
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0xF3; //设定定时初值
TH1 = 0xF3; //设定定时器重装值
ET1 = 0; //禁止定时器1中断
TR1 = 1; //启动定时器1
EA=1;
ES=1;
}
发送函数
void UART_SendByte(unsigned char Byte)
{
SBUF=Byte;
while(TI==0);
TI=0;
}
串口中断函数模板
void UART_Routine() interrupt 4
{
if(RI==1)
{
RI=0;
}
}
尝试
#include <REGX52.H>
#include "Delay.h"
#include "UART.h"
void main()
{
UART_Init(); //串口初始化
while(1)
{
}
}
void UART_Routine() interrupt 4
{
if(RI==1) //如果接收标志位为1,接收到了数据
{
P2=~SBUF; //读取数据,取反后输出到LED
UART_SendByte(SBUF); //将受到的数据发回串口
RI=0; //接收标志位清0
}
}
5 LED点阵屏
5.1 什么是LED点阵屏
某一行置 1 电平,某一列置 0 电平,则相应的二极管就亮,通过此种方式使得某一个二极管点亮
5.2 工作原理
首先LED点阵屏那么多I/O口,不可能全都接到CPU上,那么我们是怎么解决的呢?
上面的74HC595 P3_4到6连接到了CPU
同时分别连接到了RCLK,SRCLK,SER口
左侧矩形为移位寄存器,SER输出1时,当SERCLK达到上升沿时(即1),向下推移一个数字,当八个数字储存完毕,RCLK上升沿锁位打开,八位数字同时进入右侧矩形。
然后控制LED点阵屏
5.3 代码实例
LED显示图形
#include <REGX52.H>
#include "Delay.h"
sbit RCK=P3^5; //RCLK
sbit SCK=P3^6; //SRCLK
sbit SER=P3^4; //SER
#define MATRIX_LED_PORT P0
/**
* @brief 74HC595写入一个字节
* @param Byte 要写入的字节
* @retval 无
*/
void _74HC595_WriteByte(unsigned char Byte)
{
unsigned char i;
for(i=0;i<8;i++)
{
SER=Byte&(0x80>>i);
SCK=1;
SCK=0;
}
RCK=1;
RCK=0;
}
/**
* @brief LED点阵屏显示一列数据
* @param Column 要选择的列,范围:0~7,0在最左边
* @param Data 选择列显示的数据,高位在上,1为亮,0为灭
* @retval 无
*/
void MatrixLED_ShowColumn(unsigned char Column,Data)
{
_74HC595_WriteByte(Data);
MATRIX_LED_PORT=~(0x80>>Column);
Delay(1);
MATRIX_LED_PORT=0xFF;
}
void main()
{
SCK=0;
RCK=0;
while(1)
{
MatrixLED_ShowColumn(0,0x3C);
MatrixLED_ShowColumn(1,0x42);
MatrixLED_ShowColumn(2,0xA9);
MatrixLED_ShowColumn(3,0x85);
MatrixLED_ShowColumn(4,0x85);
MatrixLED_ShowColumn(5,0xA9);
MatrixLED_ShowColumn(6,0x42);
MatrixLED_ShowColumn(7,0x3C);
}
}
LED点阵屏显示动画
#include <REGX52.H>
#include "Delay.h"
#include "MatrixLED.h"
//动画数据
unsigned char code Animation[]={
0x3C,0x42,0xA9,0x85,0x85,0xA9,0x42,0x3C,
0x3C,0x42,0xA1,0x85,0x85,0xA1,0x42,0x3C,
0x3C,0x42,0xA5,0x89,0x89,0xA5,0x42,0x3C,
};
void main()
{
unsigned char i,Offset=0,Count=0;
MatrixLED_Init();
while(1)
{
for(i=0;i<8;i++) //循环8次,显示8列数据
{
MatrixLED_ShowColumn(i,Animation[i+Offset]);
}
Count++; //计次延时
if(Count>15)
{
Count=0;
Offset+=8; //偏移+8,切换下一帧画面
if(Offset>16)
{
Offset=0;
}
}
}
}
6 DS1302时钟
6.1 什么是DS1302时钟
DS1302是一款由美国DALLAS公司推出的具有涓细电流充电能力的低功耗实时时钟芯片。它能够对年、月、日、周、时、分、秒进行计时,并且具有闰年补偿等多种功能。DS1302采用串行数据传输,并采用32.768kHz晶振
6.2 工作原理
下面是其控制的寄存器第0位为读写,0为写,1为读
第1到6位为时间寄存器地址,第七位不用管
下图为其时分秒…寄存器地址
BCD码
DS1302中的时间是以16进制储存的,所以要记得把它转化为10进制,
转换方式为:(16进制数)a/16*10+a%16
知道寄存器后那么读写原理是什么呢?
看下图
首先要初始化,使能CE置0,SCLK置0
接着看IO口上,前八位是控制寄存器的16进制数,而后8位为数据
- 单字节写入:在CE为高电平后,SCLK会输出周期脉冲,每一个上升沿,IO线的数据就会进入控制寄存器,当控制寄存器配置完成(为写入数据,并且地址已给),紧接着的脉冲IO线的数据就会在上升沿进入对应地址的寄存器;(上升沿读入)
写入函数
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;
}
- 单字节读出:在CE为高电平后,SCLK会输出周期脉冲,每一个上升沿,IO线的数据就会进入控制寄存器,当控制寄存器配置完成(为读出数据,并且地址已给),紧接着对应地址的寄存器的数据就会在下降沿进入IO线;(下降沿读出)(注意读出的只有15个震荡,所以第8此读出时应保持SCLK为高电平,然后读出时才高低电平利用下降沿读出,否则会出现读出数据错误)
读出函数
/**
* @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; //读取后将IO设置为0,否则读出的数据会出错
return Data;
}