AutoLeaders控制组——51单片机学习笔记(二)

模块化编程

是把各个模块的代码放在不同的.c文件里,在 h 文件里提供外部可调用函数的声明,其它.c文件想使用其中的代码时,只需要 #lnclude " xxx.h "文件即可。使用模块化编程可极大的提高代码的可阅读性、可维护性、可移植性等。

.c文件:函数变量的定义

以下为delay.c代码

void delay(unsigned int a)	
{
	unsigned char data i, j;
	do
	{
		_nop_();
		i = 2;
		j = 199;
		do
		{
			while (--j);
		} while (--i);
	}while (--a);
}

.h文件:可被外部调用的函数

以下为Delay.h代码

#ifndef __DELAY_H__ //如果没有定义,防止重复包含
#define __DELAY_H__ //开始定义

void Delay(unsigned int xms);

#endif//与#ifndef,#if匹配,组成括号

知识点补充

#define PI 3.14可以定义PI,将PI替换为3.14
#ifdef,#if,#else,#elif,#undef都是存在的
在.c文件中任何自定义的的变量、函数在调用前必须有定义或声明,如函数中有出现REGX52.h中的P2变量,就需要在代码最前面加上#include

LCD1602调试工具

可以作为调试窗口,提供类似printf函数的功能
函数模块由视频提供,以下是一些常用函数和作用

LCD_Init();//初始化
LCD_ShowChar(1,1,'A');//显示一个字符
LCD_ShowString(1,3,"Hello");//显示字符串
LCD_ShowNum(1,9,123,3);//显示十进制数字
LCD_ShowSignedNum(1,13,-66,2);//显示有符号十进制数
LCD_ShowHexNum(2,1,0xA8,2);//显示十六进制数字 
LCD_ShowBinNum(2,4,0xAA,8);//显示二进制数 

前两个参数表示显示位置,前一个为行位置,后一个为列位置。数字需要给出显示长度

矩阵键盘

在键盘中按键数量较多时,为了减少I/O口的占用,通常将按键排列成矩阵形式
采用逐行或逐列的“扫描”,就可以读出任何位置按键的状态
可以使用2n个I/O口来检测n*n个按键的状态,如下图所示
AutoLeaders控制组——51单片机学习笔记(二)_第1张图片

矩阵键盘扫描

原理:轮流将P10到P13置为0,检测P14到P17是否为低电平,假设P13置为0读到P17为低电平,根据图片可确定S1被按下。然后快速循环这个过程,最终实现所有按键同时检测的效果。

代码实现

unsigned char MatrixKey()
{
	unsigned char Key=0;
	P1=0xFF;
	P1_3=0;
	if(P1_7==0){Delay(10);while(P1_7==0);Delay(20);Key=1;}
	if(P1_6==0){Delay(10);while(P1_6==0);Delay(20);Key=5;}
	if(P1_5==0){Delay(10);while(P1_5==0);Delay(20);Key=9;}
	if(P1_4==0){Delay(10);while(P1_4==0);Delay(20);Key=13;}
	P1=0xFF;
	P1_2=0;
	if(P1_7==0){Delay(10);while(P1_7==0);Delay(20);Key=2;}
	if(P1_6==0){Delay(10);while(P1_6==0);Delay(20);Key=6;}
	if(P1_5==0){Delay(10);while(P1_5==0);Delay(20);Key=10;}
	if(P1_4==0){Delay(10);while(P1_4==0);Delay(20);Key=14;}
	P1=0xFF;
	P1_1=0;
	if(P1_7==0){Delay(10);while(P1_7==0);Delay(20);Key=3;}
	if(P1_6==0){Delay(10);while(P1_6==0);Delay(20);Key=7;}
	if(P1_5==0){Delay(10);while(P1_5==0);Delay(20);Key=11;}
	if(P1_4==0){Delay(10);while(P1_4==0);Delay(20);Key=15;}
	P1=0xFF;
	P1_0=0;	
	if(P1_7==0){Delay(10);while(P1_7==0);Delay(20);Key=4;}
	if(P1_6==0){Delay(10);while(P1_6==0);Delay(20);Key=8;}
	if(P1_5==0){Delay(10);while(P1_5==0);Delay(20);Key=12;}
	if(P1_4==0){Delay(10);while(P1_4==0);Delay(20);Key=16;}
	return Key;
}

定时器

是单片机的内部资源。
作用:用于计时系统,可实现软件计时;使程序每隔一段时间完成一次操作(如输出输入扫描);替代长时间的Delay来提高CPU的运行效率和处理速度

硬件方面

STC89C52中有3个定时器T0,T1,T2
T0和T1的操作方式是所有51单片机共有的

定时器工作简述

时钟提供时钟脉冲–>每个脉冲计数单元内的值加一,一直到溢出–>发送中断,执行任务

工作模式1:16位定时器/计数器

AutoLeaders控制组——51单片机学习笔记(二)_第2张图片
AutoLeaders控制组——51单片机学习笔记(二)_第3张图片
(以下代号都以T0计时器举例)
SYSClk:即System Clock系统时钟,由晶振决定
12T mode与6T mode:12T是系统走12时钟后产生一次脉冲
C/T非:当置1 ,多路开关连接到外部脉冲输入P3.4/T0 ,即 T0 工作在计数方式。当置0 ,多路开关连接到时钟脉冲,即 T0 工作在定时方式中。
TR0,GATE,INT0非:通过图中逻辑门共同控制是否让脉冲计数单元接收脉冲。
TL0和TH0:组合起来是脉冲计数单元,溢出时产生中断。


AutoLeaders控制组——51单片机学习笔记(二)_第4张图片
TCON:控制了定时器T0中的TF0,TR0,IE0,IT0;
TL0和TH0:组合起来是脉冲计数单元,溢出时产生中断。
TF0:标志是否产生了中断,产生后置为1,当cpu响应了中断后自动置为0。
TR0:名义上是T0的运行控制位,但实际上还得看GATE和INT0的脸色。
IE0:外部中断0请求源,别的中断程序在请求中断时,置为1
IT0:脉冲计数单元接收脉冲(待补充)
M1和M0:组合来确定模式,01组合为模式1
(TCON和TMOD不能一位一位赋值即不可位寻址,需要利用与或式赋值法)

中断系统(只针对定时器0简单讲讲)

当cpu收到中断请求,会暂停主程序去执行中断处理程序,处理完成后再返回主程序
如果有多个不同优先级的中断请求,则会先执行高优先级的中断程序
!AutoLeaders控制组——51单片机学习笔记(二)_第5张图片AutoLeaders控制组——51单片机学习笔记(二)_第6张图片
EA:中断产生总开关(Enable All)

定时器1中断

ET0:允许定时器0中断产生
PT0H,PT0:共同确定中断优先级,规律额是PT0+2*PT0H大,优先级高

程序方面

相关代码

初始化定时器0的代码

void Timer0_Init(void)	//1ms
{                    //以下TMOD利用与或赋值法赋值
	TMOD &= 0xF0;	//保留高4位,清除了低4位
	TMOD |= 0x01;	//将 0001B写入低4位
	TL0 = 0x66;		//设置定时初始值(低8位)
	TH0 = 0xFC;     //设置定时初始值(高8位)
	TF0 = 0;//清除TF0标志
	TR0 = 1;//定时器0开始计时
/*-----以上可以由stc-isp生成-----*/
	ET0=1;//定时器0中断开关
	EA=1;//中断总开关
	PT0H=1;PT0=1;//设置优先级
}

中断程序样例代码

void keep_show() interrupt 1
{
	static unsigned int T0Count;
    TL0 = 0x66;			//重新赋初值
	TH0 = 0xFC;			
	T0Count++;
	if(T0Countt>=20)//每20ms显示一次
	{
		Nixie(p,num);
		T0Count=0;	
	}
}

编写注意事项

用STCISP生成代码时,选择定时器,定时器模式,定时器时钟,按需求选择。
中断程序后面要加中断号。
定时器能给的时间往往比较小,需要在内部嵌套计数直到一定次数再执行需要的程序。
定时器内部的函数占用时间不能太长。

各类中断的中断号

AutoLeaders控制组——51单片机学习笔记(二)_第7张图片

串口通信

51单片机内部自带URAT,异步收发器,实现串口通信(不支持流控制)

硬件电路和补充知识

简单双向串口通信

简单双向串口通信有两根通信线
TX:transmit
RX:receive
要交叉连接
AutoLeaders控制组——51单片机学习笔记(二)_第8张图片
当只需要单向的数据传输时,可以直接一根通信线
当电平标准不一致时,需要加电平转换芯片

串口常用电平标准

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等利用总线通信。
全双工:通信双方可以在同一时刻互相传输数据
半双工:通信双方可以互相传输数据,但必须分时复用一根数据线
单工:通信只能有一方发送到另一方,不能反向传输
异步:通信双方各自约定通信速率
同步:通信双方靠一根时钟线来约定通信速率
总线:连接各个设备的数据传输线路(类似于一条马路,使住户可以相互交流)

51单片机的UART

工作模式

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

参数

波特率:通信速率
校验位:校验位用于数据验证(类似身份证最后一位)
停止位:用于数据与数据的间隔

串口模式图

AutoLeaders控制组——51单片机学习笔记(二)_第9张图片
SBUF:是收发数据的缓存,实际上是2个独立的寄存器但共用一个地址,读写时用的寄存器不同,所以可以双向通信
波特率的控制:配置T1(且只能用T1且8位自动重装)来控制收发速率,根据T1的溢出率
发送控制器和接受控制器:根据定时器确定的波特率来收发数据,有数据需要传输时利用TI和RI向中断系统请求中断(ES需要置1)
AutoLeaders控制组——51单片机学习笔记(二)_第10张图片
SM0和SM1:确定工作模式
REN:receive enable,允许接收
TI,RI:发送/接收完毕后置为1,必须软件进行置为1
SMOD:波特率选择位,加倍还是不加倍
SMOD0:是否进行帧错误检测

波特率的计算

已知需要的波特率为4800(HZ)
那么需要乘以16,再看SMOD是1还是0,是0就乘2
得到的是T1的溢出率(较小),系统晶振除以溢出率再除以12
得到的就是TL1加多少溢出的这一个数值

以下是个人对晶振为11.05942的验算

4800乘以16为 76800
11.05942乘以10的6次方再去除以15360除以12为 12
0xFF-0x0C+1=0xF4

程序相关

URAT初始化(由STCISP生成)

void UART_Init(void)	//[email protected]
{
	PCON |= 0x80;		//使能波特率倍速位SMOD
	SCON = 0x50;		//8位数据,可变波特率
	/*---上为串口设置,下为定时器设置-----*/
//	AUXR &= 0xBF;		删去定时器时钟12T模式
//	AUXR &= 0xFE;		这是默认的(串口1选择定时器1为波特率发生器)
	TMOD &= 0x0F;		//设置定时器模式
	TMOD |= 0x20;		//设置定时器模式
	TL1 = 0xF4;			//设置定时初始值
	TH1 = 0xF4;			//设置定时重载值
	ET1 = 0;			//禁止定时器中断
	TR1 = 1;			//定时器1开始计时
}

发送函数

void UART_SendBy(unsigned char Byte)
{
	SBUF=Byte;
	while(TI==0);//发送没结束
	TI=0;//发送结束后TI被置1,需要被置为0
	Delay(1);//防止发送特定数据时出错
}

接收函数

 //写在main.c中。
void UART_Routine() interrupt 4
{
    if(RI==1)//要确定是在接收数据
    {
	    a=SBUF;//假设a是一个全局变量
        RI=0;
    }
}

在中断函数中不应该调用主函数中出现过的函数

数据显示模式

HEX模式是以原始数据的形式显示
文本/字符模式是以原始数据编码后的形式显示,具体参照记忆中的高中信息技术课本最后一页ASCII码表

LED点阵屏

硬件相关

点阵屏的结构

开发板上LED的点阵为8x8
AutoLeaders控制组——51单片机学习笔记(二)_第11张图片
LED的驱动是8个IO口和移位寄存器共同控制的(74HC595,用了3个IO口)控制
P0控制了水平方向的引脚
移位寄存器控制了竖着方向的引脚

74H595移位寄存器

AutoLeaders控制组——51单片机学习笔记(二)_第12张图片
OE:置0为输出使能,相当于开关
RCLK:上升沿锁存,每个上升沿(从0到1)使得寄存器中数据输出到对应引脚
SER:数据输入处
SERCLK:上升沿移位,每个上升沿(从0到1),使得数据移位
SRCLR:数据清零用,因为接在了VCC,不会清空
QH’:用于级联多个芯片来组成高于8位的移位寄存器,接到另一个芯片的SER

补充知识

单片机输出的高电平不能用于驱动二极管发光
但可以经过三极管来提高电流大小,从而驱动功耗较大的的器件
sfr(special function register):特殊功能寄存器声明
sfr P0 = 0x80;
声明P0口寄存器,物理地址为0x80
sbit(special bit):特殊位声明
例:sbit P0_1 = 0x81;sbit P0_1 = P0^1;
声明P0寄存器的第1位
如果对不可位寻址的地址中的某一位进行修改,需要用&,|,^(与,或,异或)来操作
如果对于地址a,需要修改其中从右往左第3位为1,模拟代码如下

a&=0xFB; //aaaa aaaa & 1111 1011 = aaaa a0aa
a|=0x00; //aaaa aa0a | 0000 0100 = aaaa a1aa

如果置为0,可略去或运算
如果要对特定位置反,就可以使用异或运算

程序编写

移位寄存器的写入函数

觉得可以对课程的函数进行如下调整,这样就不用再写初始化函数了

sbit RCK=P3^5;		//RCLK
sbit SCK=P3^6;		//SRCLK
sbit SER=P3^4;		//SER

void _74HC595_WriteByte(unsigned char Byte)
{
	unsigned char i;
	RCK=0;//B
	for(i=0;i<8;i++)
	{
		SCK=0;//A
		SER=Byte&(0x80>>i);
		SCK=1;
		//SCK=0;移到上面A
	}
	RCK=1;
	//RCK=0;移到上方B
}

LED矩阵单列显示

void MatrixLED_ShowColumn(unsigned char Column,Data)
{
	_74HC595_WriteByte(Data);
	MATRIX_LED_PORT=~(0x80>>Column);
	Delay(1);//保持短时显示
	MATRIX_LED_PORT=0xFF;//消影
}

动画显示

思路上是写一个数组来表示动画的每一帧

//动画数据
unsigned char code Animation[]={
    0x78,0x84,0x82,0x41,0x41,0x82,0x84,0x78,
    0x00,0x38,0x44,0x22,0x22,0x44,0x38,0x00,
    0x00,0x00,0x78,0x24,0x24,0x78,0x00,0x00,
};//这是收缩的爱心(__的任务罢了)

void main()
{
	//MatrixLED_Init();
    unsigned char i,Offset=0,Count=0;
    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;
            }
        }
    }
}

补充知识

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,
};

代码中的code表示把数据保存在flash芯片中,而不是内存。
如果数据较大且是只读的可以放在flash芯片中。
内存内数据的操作速度更快,但大小有限。

DS1302实时时钟

硬件知识

DS1302是有涓细电流充电能力的低功耗实时时钟芯片,有年月日周时分秒的计算。
有些单片机会写带RTC即Real Time Clock,实时时钟
优点:精度高,不占用单片机CPU,如果接了备用电池可以掉电继续运行。
不同的时钟芯片可能内置电源内置晶振等,具体看手册。

引脚定义和应用电路

AutoLeaders控制组——51单片机学习笔记(二)_第13张图片
VCC1:接备用电池正极(本开发板内无备用电池)
VCC2:接电源
X1,X2接32.768KHz的晶振,通过内部电路可以产生精确的1Hz震荡
SCLK:串行时钟
IO:数据输入输出口(要基于一定通信协议)
CE:芯片使能(chip enable)其实是控制能否被外部控制

内部寄存器

AutoLeaders控制组——51单片机学习笔记(二)_第14张图片
WP:写入保护(write protect),置为1表示不许写入

BCD码

用4位二进制数来表示1位十进制数
是时钟芯片内部寄存器采用的方式

数据读写

芯片内部有输入移位寄存器,用IO口和SCLK来控制。
输入数据的原理类似74H595寄存器,
但读取数据,内部的数据会反馈到IO口上,于SPI通信相似

时序定义

AutoLeaders控制组——51单片机学习笔记(二)_第15张图片

写:
会先发送一个命令字,再发送要写入的数据
读:
会先发送一个命令字,再去接收要读取的数据
SCLK的每个上升沿IO口的电平会写入,除了读取命令的后八个字节。
在读取操作的后八个字节,每个下降沿,芯片会把数据放在IO口上。
发送顺序:先发低位置,再发高位。

命令字

AutoLeaders控制组——51单片机学习笔记(二)_第16张图片
第6位:0操作时钟,1操作RAM
第0位:0为写,1为读

代码编写

单字节写入

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;
}

单字节读取

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;
}

整体输入

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);
}

整体输出

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;
}

(自己写的part,把BCD数值转换写成函数了)

补充知识

extern可以声明是外部函数或者参数。
如果模块需要引入一个全局变量。如char DS1302_Time[]={19,11,16,12,59,55,6};
在.c文件里正常写,在.h里再前面加extern且只定义数据类型。

你可能感兴趣的:(51单片机,学习,笔记)