51单片机学习(中)
这是51单片机学习的中篇,其中主要涉及的内容是串口通信、矩阵键盘、LED点阵屏、DS1302实时时钟、蜂鸣器等。其中最重要的其实就是串口通信了,串口通信是指仅用一根接收线和一根发送线就能将数据以位进行传输的一种通信方式。尽管串行通讯的比按字节传输的并行通信慢,但是串口可以在仅仅使用两根线的情况下就能实现数据的传输,后面要用到串口通信的场景也会比较多。
6、矩阵键盘检测
在键盘中按键数量较多时,为了减少I/O口的占用,通常将按键排列成矩阵形式。采用逐行或逐列的 “扫描”,就可以读出任何位置按键的状态。下图是STC89C51系列单片机开发板上的的矩阵键盘电路原理图:
其实数码管扫描(输出扫描)的原理就是显示第1位 → 显示第2位 → 显示第3位→……,然后快速循环这个过程,最终实现所有数码管同时显示的效果。那么对于矩阵键盘扫描就是一种输入扫描,原理:读取第1行(列)→读取第2行(列) →读取第3行(列) → ……,然后快速循环这个过程,最终实现所有按键同时检测的效果
以上两种扫描方式的共性:节省I/O口。下面看看Demo:
MatrixKey.h 与 MatrixKey.c 用于检测按键按下,按下时会返回键码,键码默认从上到下,从左到右为1-16:
#ifndef __MATRIX_KEY_H__
#define __MATRIX_KEY_H__
unsigned char MatrixKey();
#endif // !__MATRIX_KEY_H__
#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;
}
main.c
#include <REGX52.H>
#include "LCD1602.h"
#include "MatrixKey.h"
unsigned char KeyNum;
void main()
{
LCD_Init();
LCD_ShowString(1, 1, "MatrixKey:");
while(1)
{
KeyNum = MatrixKey();
if(KeyNum != 0)
{
LCD_ShowNum(2, 1, KeyNum, 2);
}
}
}
7、串口通信
串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信。单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大的扩展了单片机的应用范围,增强了单片机系统的硬件实力。
51单片机内部自带UART(Universal Asynchronous Receiver Transmitter,通用异步收发器),可实现单片机的串口通信。典型的串口通信使用3根线完成,分别是地线、发送、接收。由于串口通信是异步的,所以端口能够在一根线上发送数据同时在另一根线上接收数据。串口通信最重要的参数是波特率、数据位、停止位和奇偶的校验。对于两个需要进行串口通信的端口,这些参数必须匹配,这也是能够实现串口通讯的前提。
串口通信的硬件电路
1、简单双向串口通信有两根通信线(发送端TXD和接收端RXD
2、TXD与RXD要交叉连接
3、当只需单向的数据传输时,可以直接一根通信线
4、当电平标准不一致时,需要加电平转换芯片
电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,串口常用的电平标准有如下三种:
TTL电平:+5V
表示1,0V
表示0
RS232电平:-3
~ -15V
表示1,+3
~ +15V
表示0
RS485电平:两线压差 +2
~ +6V
表示1,-2
~ -6V
表示0(差分信号)
常见通信接口比较
名称 | 引脚定义 | 通信方式 | 特点 |
---|---|---|---|
UART | TXD、RXD | 全双工、异步 | 点对点通信 |
I²C | SCL、SDA | 半双工、同步 | 可挂载多个设备 |
SPI | SCLK、MOSI、MISO、CS | 全双工、同步 | 可挂载多个设备 |
1-Wire | DQ | 半双工、异步 | 可挂载多个设备 |
此外还有:CAN、USB等通信协议
全双工与半双工等概念在计算机网络就已经学习过,全双工就是通信双方可以在同一时刻互相传输数据。半双工就是通信双方可以互相传输数据,但必须分时复用一根数据线,即不能同时向对方发送数据。单工就是通信只能有一方发送到另一方,不能反向传输。
异步通信的时候,通信双方各自约定通信速率;同步通信的时候,通信双方靠一根时钟线来约定通信速率。
STC89C52有1个UART,STC89C52的UART有四种工作模式:
模式0 | 模式1 | 模式2 | 模式3 |
---|---|---|---|
同步移位寄存器 | 8位UART,波特率可变(由定时器控制),比较常用的一种模式 | 9位UART,波特率固定 | 9位UART,波特率可变 |
下面是用于串口通信的引脚:
串口参数及时序图
波特率:串口通信的速率(发送和接收各数据位的间隔时间)
检验位:用于数据验证
停止位:用于数据帧间隔
串口工作模式图:
SBUF:串口数据缓存寄存器,物理上是两个独立的寄存器,但占用相同的地址。写操作时,写入的是发送寄存器,读操作时,读出的是接收寄存器。
波特率的计算
在12MHz晶振下,机器周期就是13us,即每隔13us就会溢出一次,1/13 = 0.076923MHz,如果我们选择的是SMOD为1的话,则:0.076923 / 16 = 0.0048076 MHz = 4807.6923 Hz,所以会存在0.16%的误差:
对于11.0592MHz的晶振,机器周期是13.02083333333us,即每隔13.02083333us就会溢出一次,1/13.02083333 = 0.0768MHz,如果我们选择的是SMOD为1的话,则:0.0768 / 16 = 0.0048 MHz = 4800Hz,所以误差为0%:
顺便说一下机器周期的计算方式,目前只是设置定时器/计数器1工作于定时模式的工作方式2(8位自动重装)
当单片机工作在12T模式时时,定时器/计数器1溢出一次所需的时间(即机器周期)为:
当单片机工作在12T模式时时,定时器/计数器1溢出一次所需的时间(即机器周期)为:
串口相关的寄存器
当软件设置SCON的SMO、SM1为“01”时,串行通信则以模式1工作。此模式为8位UART格式,一帧信息为10位:1位起始位,8位数据位(低位在先)和1位停止位。波特率可变,即可根据需要进行设置。TxD(TxD/P3.1)为发送信息,RxD(RxD/P3.0)为接收端接收信息,串行口为全双工接受/发送串行口。
串口通信示例程序
01、MCU向PC发送数据
UART.h 与 UART.c
#ifndef __UART_H__
#define __UART_H__
#include <REGX51.H>
void UartInit(void); // 4800bps@11.0592MHz
void UART_SendByte(unsigned char ch);
#endif // !__UART_H__
// ----------------------------------------------------------------
#include "UART.h"
void UartInit(void) //4800bps@11.0592MHz
{
PCON |= 0x80; //使能波特率倍速位SMOD
SCON = 0x50; //8位数据,可变波特率
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0xF4; //设置定时初始值
TH1 = 0xF4; //设置定时重载值
ET1 = 0; //禁止定时器1中断
TR1 = 1; //定时器1开始计时
}
void UART_SendByte(unsigned char ch){
SBUF = ch;
while(TI == 0);
TI = 0;
}
main.c
#include <REGX51.H>
#include "Delay.h"
#include "UART.h"
void main(){
unsigned char ch = 0;
UartInit();
P2_0 = 0;
while(1){
Delay(500);
UART_SendByte(ch++);
P2_0 = ~P2_0;
}
}
02、MCU与PC数据双向通信
UART.h 与 UART.c
#ifndef __UART_H__
#define __UART_H__
#include <REGX51.H>
void UartInit(void); // 4800bps@11.0592MHz
void UART_SendByte(unsigned char ch);
#endif // !__UART_H__
// ----------------------------------------------------------------
#include "UART.h"
void UartInit(void) //4800bps@11.0592MHz
{
PCON |= 0x80; //使能波特率倍速位SMOD
SCON = 0x50; //8位数据,可变波特率
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0xF4; //设置定时初始值
TH1 = 0xF4; //设置定时重载值
ET1 = 0; //禁止定时器1中断
TR1 = 1; //定时器1开始计时
EA = 1;
ES = 1; // 启用串口中断
}
void UART_SendByte(unsigned char ch){
SBUF = ch;
while(TI == 0);
TI = 0;
}
main.c
#include <REGX51.H>
#include "Delay.h"
#include "UART.h"
void main(){
unsigned char ch = 0;
UartInit();
while(1){
}
}
void UART_Routine() interrupt 4
{
// 接收中断
if(1 == RI){
// 通过收到的一个字节的数据改变LED的亮灭
P2 = SBUF;
// 收到的数据又发送给PC
UART_SendByte(P2);
// 软件置位
RI = 0;
}
}
HEX模式/十六进制模式/二进制模式:以原始数据的形式显示
文本模式/字符模式:以原始数据编码后的形式显示
8、LED点阵屏
LED点阵屏由若干个独立的LED组成,LED以矩阵的形式排列,以灯珠亮灭来显示文字、图片、视频等。LED点阵屏广泛应用于各种公共场合,如汽车报站器、广告屏以及公告牌等。LED点阵屏按颜色分类:单色、双色、全彩;按像素分为 8×8、16×16等(大规模的LED点阵通常由很多个小点阵拼接而成)。
LED点阵屏的结构类似于数码管,只不过是数码管把每一列的像素以“8”字型排列而已;LED点阵屏与数码管一样,有共阴和共阳两种接法,不同的接法对应的电路结构不同;LED点阵屏需要进行逐行或逐列扫描,才能使所有LED同时显示,这一点也与数码管是一样的。
74HC595
74HC595是串行输入并行输出的移位寄存器,可用3根线输入串行数据,8根线输出并行数据,多片级联后,可输出16位、24位、32位等,常用于IO口扩展。
点阵屏示例程序
MatrixLED.h 与 MatrixLED.c
#ifndef __MATRIX_LED_H__
#define __MATRIX_LED_H__
void MatrixLED_Init();
void MatrixLED_ShowColumn(unsigned char Column,Data);
#endif
// -----------------------------------------
#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 点阵屏初始化
* @param 无
* @retval 无
*/
void MatrixLED_Init()
{
SCK=0;
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;
}
main.c
#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;
}
}
}
}
9、DS1302实时时钟
DS1302是由美国DALLAS公司推出的具有涓细电流充电能力的低功耗实时时钟芯片。它可以对年、月、日、周、时、分、秒进行计时,且具有闰年补偿等多种功能。RTC(Real Time Clock):实时时钟,是一种集成电路,通常称为时钟芯片。
引脚定义和应用电路
引脚名 | 作用 | 引脚名 | 作用 |
---|---|---|---|
VCC2 | 主电源 | CE | 芯片使能 |
VCC1 | 备用电池 | IO | 数据输入/输出 |
GND | 电源地 | SCLK | 串行时钟 |
X1、X2 | 32.768KHz晶振 |
操作DS1302的大致过程,就是将各种数据写入DS1302的寄存器,以设置它当前的时间的格式。然后使DS1302开始运作,DS1302时钟会按照设置情况运转,再用单片机将其寄存器内的数据读出。再用液晶显示,就是我们常说的简易电子钟。所以总的来说DS1302的操作分2步(显示部分属于液晶显示的内容,不属于DS1302本身的内容),但是在讲述操作时序之前,我们要先看看寄存器,DS1302有一个控制寄存器、12个日历、时钟寄存器和31个RAM。
与时钟/RAM 通讯只需要三根线:CE、I/O (数据线)、 SCLK (串行时钟)。数据输出输入时钟/RAM 一次1字节或者在脉冲串中多达31字节。DS1302 被设计工作在非常低的电能下,在低于1uW 时还能保持数据和时钟信息。
DS1302命令字
命令字启动每一次数据传输,MSB(位7)必须是逻辑1,如果是0,则禁止对DS1302写入;位6在逻辑0时规定为时钟/日历数据,逻辑1时为RAM数据。位1至位5表示了输入输出的指定寄存器;LSB(位0)在逻辑0时为写操作(输出),逻辑1时为读操作(输入)。命令字以LSB(位0)开始总是输入。
寄存器定义
示例程序
DS1302.h 与 DS1302.c
#ifndef __DS1302_H__
#define __DS1302_H__
//外部可调用时间数组,索引0~6分别为年、月、日、时、分、秒、星期
extern unsigned char DS1302_Time[];
void DS1302_Init(void);
void DS1302_WriteByte(unsigned char Command,Data);
unsigned char DS1302_ReadByte(unsigned char Command);
void DS1302_SetTime(void);
void DS1302_ReadTime(void);
#endif
// --------------------------------------------------------------
#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
//时间数组,索引0~6分别为年、月、日、时、分、秒、星期
unsigned char DS1302_Time[]={21,8,18,23,39,58,3};
/**
* @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; //读取后将IO设置为0,否则读出的数据会出错
return Data;
}
/**
* @brief DS1302设置时间,调用之后,DS1302_Time数组的数字会被设置到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);//十进制转BCD码后写入
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读取时间,调用之后,DS1302中的数据会被读取到DS1302_Time数组中
* @param 无
* @retval 无
*/
void DS1302_ReadTime(void)
{
unsigned char Temp;
Temp=DS1302_ReadByte(DS1302_YEAR);
DS1302_Time[0]=Temp/16*10+Temp%16;//BCD码转十进制后读取
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"
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);//显示秒
}
}
什么是BCD码?
BCD码(Binary Coded Decimal),用4位二进制数来表示1位十进制数
例:0001 0011表示13,1000 0101表示85,0001 1010不合法。
在十六进制中的体现:0x13表示13,0x85表示85,0x1A不合法。
BCD码转十进制:DEC=BCD/16×10 + BCD%16; (2位BCD)
十进制转BCD码:BCD=DEC/10×16 + DEC%10; (2位BCD)
10、蜂鸣器
蜂鸣器是一种将电信号转换为声音信号的器件,常用来产生设备的按键音、报警音等提示信号。
蜂鸣器按驱动方式可分为有源蜂鸣器和无源蜂鸣器。有源蜂鸣器:内部自带振荡源,将正负极接上直流电压即可持续发声,频率固定;无源蜂鸣器:内部不带振荡源,需要控制器提供振荡脉冲才可发声,调整提供振荡脉冲的频率,可发出不同频率的声音。
蜂鸣器按驱动可由三极管驱动,也可以由集成电路驱动,在普中的单片机开发板上是由步进电机的集成电路驱动。
ULN2003
ULN2003是一个单片高电压、高电流的达林顿晶体管阵列集成电路。它是由7对NPN达林顿管组成的,它的高电压输出特性和阴极箝位二极管可以转换感应负载。单个达林顿对的集电极电流是500mA。达林顿管并联可以承受更大的电流。此电路主要应用于继电器驱动器,字锤驱动器,灯驱动器,显示驱动器(LED气体放电),线路驱动器和逻辑缓冲器。
示例代码
Buzzer.c 与 Buzzer.h
#ifndef __BUZZER_H__
#define __BUZZER_H__
void Buzzer_Time(unsigned int ms);
#endif
// ---------------------------------------------------------------
#include <REGX52.H>
#include <INTRINS.H>
//蜂鸣器端口:
sbit Buzzer=P1^5;
/**
* @brief 蜂鸣器私有延时函数,延时500us
* @param 无
* @retval 无
*/
void Buzzer_Delay500us() //@12.000MHz
{
unsigned char i;
_nop_();
i = 247;
while (--i);
}
/**
* @brief 蜂鸣器发声
* @param ms 发声的时长,范围:0~32767
* @retval 无
*/
void Buzzer_Time(unsigned int ms)
{
unsigned int i;
for(i=0;i<ms*2;i++)
{
Buzzer=!Buzzer;
Buzzer_Delay500us();
}
}
main.c
#include <REGX52.H>
#include "Delay.h"
#include "Buzzer.h"
unsigned char KeyNum;
void main()
{
while(1)
{
Buzzer_Time(200);
Delay(150);
}
}