模块化编程
是把各个模块的代码放在不同的.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 <REGX52.h>
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个按键的状态,如下图所示
矩阵键盘扫描
原理:轮流将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位定时器/计数器
(以下代号都以T0计时器举例)
SYSClk:即System Clock系统时钟,由晶振决定
12T mode与6T mode:12T是系统走12时钟后产生一次脉冲
C/T非:当置1 ,多路开关连接到外部脉冲输入P3.4/T0 ,即 T0 工作在计数方式。当置0 ,多路开关连接到时钟脉冲,即 T0 工作在定时方式中。
TR0,GATE,INT0非:通过图中逻辑门共同控制是否让脉冲计数单元接收脉冲。
TL0和TH0:组合起来是脉冲计数单元,溢出时产生中断。
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收到中断请求,会暂停主程序去执行中断处理程序,处理完成后再返回主程序
如果有多个不同优先级的中断请求,则会先执行高优先级的中断程序
!
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生成代码时,选择定时器,定时器模式,定时器时钟,按需求选择。
中断程序后面要加中断号。
定时器能给的时间往往比较小,需要在内部嵌套计数直到一定次数再执行需要的程序。
定时器内部的函数占用时间不能太长。
各类中断的中断号
串口通信
51单片机内部自带URAT,异步收发器,实现串口通信(不支持流控制)
硬件电路和补充知识
简单双向串口通信
简单双向串口通信有两根通信线
TX:transmit
RX:receive
要交叉连接
当只需要单向的数据传输时,可以直接一根通信线
当电平标准不一致时,需要加电平转换芯片
串口常用电平标准
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,波特率可变
参数
波特率:通信速率
校验位:校验位用于数据验证(类似身份证最后一位)
停止位:用于数据与数据的间隔
串口模式图
SBUF:是收发数据的缓存,实际上是2个独立的寄存器但共用一个地址,读写时用的寄存器不同,所以可以双向通信
波特率的控制:配置T1(且只能用T1且8位自动重装)来控制收发速率,根据T1的溢出率
发送控制器和接受控制器:根据定时器确定的波特率来收发数据,有数据需要传输时利用TI和RI向中断系统请求中断(ES需要置1)
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) //4800bps@11.0592MHz
{
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
LED的驱动是8个IO口和移位寄存器共同控制的(74HC595,用了3个IO口)控制
P0控制了水平方向的引脚
移位寄存器控制了竖着方向的引脚
74H595移位寄存器
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,如果接了备用电池可以掉电继续运行。
不同的时钟芯片可能内置电源内置晶振等,具体看手册。
引脚定义和应用电路
VCC1:接备用电池正极(本开发板内无备用电池)
VCC2:接电源
X1,X2接32.768KHz的晶振,通过内部电路可以产生精确的1Hz震荡
SCLK:串行时钟
IO:数据输入输出口(要基于一定通信协议)
CE:芯片使能(chip enable)其实是控制能否被外部控制
内部寄存器
WP:写入保护(write protect),置为1表示不许写入
BCD码
用4位二进制数来表示1位十进制数
是时钟芯片内部寄存器采用的方式
数据读写
芯片内部有输入移位寄存器,用IO口和SCLK来控制。
输入数据的原理类似74H595寄存器,
但读取数据,内部的数据会反馈到IO口上,于SPI通信相似
时序定义
写:
会先发送一个命令字,再发送要写入的数据
读:
会先发送一个命令字,再去接收要读取的数据
SCLK的每个上升沿IO口的电平会写入,除了读取命令的后八个字节。
在读取操作的后八个字节,每个下降沿,芯片会把数据放在IO口上。
发送顺序:先发低位置,再发高位。
命令字
第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
且只定义数据类型。