本文主要介绍关于单片机,c语言,proteus的知识点,对【单片机基础项目(上)】和【如何学习单片机?】有兴趣的朋友可以看下由【小罗のdiary】投稿的技术文章,希望该技术和经验能帮到你解决你所遇的【单片机原理】相关技术问题。
掌握的一定的单片机理论基础之后,那么应该要实战练习一下,接下来,我会将自己所学到的知识记录下来,方便自己和他人的学习,若不会创建项目没装软件,请看一下往期教程
proteus7.6和keil4的安装_mxzzzr的博客-CSDN博客基于AT89系列单片机的第一个项目-点亮LED灯_mxzzzr的博客-CSDN博客注:要资源的请点赞加关注,Thanks?(?ω?)?!
目录
一、文章图解
二、流水灯制作
三、数码管原理
1.静态数码管部分
2.动态数码管部分
四、键盘,中断和定时器/计数器
?1.独立式键盘和中断部分
2.矩阵式键盘和定时器
五、定时器
六.lcd灯
1602lcd灯?(只能显示字符,不能显示汉字)
前面我们几乎都是用延时函数来对电子钟来进行定时,这就导致了我们的延时和真实的秒数有一定的差别,但我们也可以通过更改delay的值来减少这种差别。
delay和定时器的差别????????1.delay和显示是在同一个cpu上运行的,也就是说,我们的delay和显示是连在一起的
????????2.定时器和显示是在不同的cpu上运行的,也就是说我们的定时器和显示是独立运行的
?类似于java的多线程机制。
下面的项目是毫秒级别的计时,精度很高,所以说delay函数来计时的话,误差太大,不适用,那就采用定时器吧!
定时器的使用步骤:????????有2种运行方式
????????1.不进入中断函数,也叫软件清零方式(写入TF=0)跟delay差不多。
#include
void main(){ TM0D = 0x01;//设置T0为方式1 TR0 = 1;//启动T0 while(1){ TH0=0xfc;//设置T0高8位初值 TL0=0x18;//设置T0低8位初值 while(!TF0){}//判断TF0是否为1,否则一直循环,这一段类似于delay; TF0 = 0;//输入清0代码(以软件的方式) } } ????????2.进入中断函数,也叫硬件清零方式,执行完之后TF自动清0。(多线程机制)
#include
void main(){ EA = 1;//开启中断总开关 ET0 = 1;//运行T0中断 TMOD = 0x01;//设置T0工作方式为工作方式1 TH0=0xfc;//设置T0高8位初值 TL0=0x18;//设置T0低8位初值 TR0 = 1;//启动定时器 while(1); } void _T0_ interrupt 1{ //TR0 = 0;如果没让TR0置0,那么中断函数会不断被调用 TH0=0xfc;//设置T0高8位初值 TL0=0x18;//设置T0低8位初值 }
1.仿真图0-99.9s
用到的元器件:
1.7SEG-MPX4-CC
2.74LS138
3.74LS245
4.BUTTON
5.RES
2.流程图
?3.代码
代码讲解:先在main开启定时器,分为2个过程,main中的while死循环一直更新数码管的值,
计数器,对value的值进行加1操作,这2个是不同的过程,多线程。
#include
unsigned char code seg[]={0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f}; unsigned int val=0; unsigned int jl=0;//记录秒数 int value = 0;//当前的值0-999如果用unsigned char的话,会有一定的错误,因为unsigned char的范围为0-255 unsigned char temp; sbit A0=P2^0; sbit A1=P2^1; sbit A2=P2^2; void delay(unsigned int x){ unsigned int i,j; for(i = 0; i
4.结果
计数器完整视频
六.lcd灯1602lcd灯?(只能显示字符,不能显示汉字)lcd灯是常用的显示元件,它可以动态显示和静态显示,他比起数码管的显示更丰富,所以说,我们需要记忆的东西也相对来说很多。
我们必须先熟练记住他们的引脚所对应的功能,还有一些指令的命令字,才能得心应手的进行编程,控制。
我们要对lcd灯进行编程,首先我们需要背下,或者拿起有关的书本来看,才能正确的执行命令。
1602字符的显示位置地址????????? ? ? ????????? 图1是1602的地址,图2是显示字符的ASSCLL码?以上2个图必须要记住或者收藏起来,不然无法在指定位置显示字符
1602引脚 LCD1602命令字?(字节操作)? ? ? ?? ? ? ? ?命令1:清屏,光标回到地址00H位置(屏幕的左上角)
? ? ? ? ?命令2:光标返回到00H位置(屏幕的左上角)
? ? ? ? ?命令3:显示屏幕之后的设置
????????????????I/D=1,写完字符后地址右移,I/D=0,写完字符后地址左移
? ? ? ? ? ? ? ? S=1,写入字符后地址移动,S=0,写入字符后地址不移动
? ? ? ? 命令4:显示屏的开关和光标
? ? ? ? ? ? ? ? D=0,关显示,D=1,开显示
? ? ? ? ? ? ? ? C=0,无光标,C=1,有光标
? ? ? ? ? ? ? ? B=0,光标不闪烁,B=1,光标闪烁
? ? ? ? 命令5:光标和字符移动
? ? ? ? ? ? ? ? S/C=1,移动显示的字符,S/C=0,移动光标
? ? ? ? ? ? ? ? R/L=1,左移,R/L=0,右移
? ? ? ? 命令6:功能设置
? ? ? ? ? ? ? ? DL=1,8位数据接口,DL=0,4位数据接口
? ? ? ? ? ? ? ? N=0,单行显示,N=1,两行显示
? ? ? ? ? ? ? ? F=0,5*7点阵,F=1,5*10点阵
? ? ? ? 命令7:CGROM地址设置
? ? ? ? 命令8:显示RAM地址设置(屏幕的位置)
? ? ? ? 命令9:读忙标志或地址
? ? ? ? ? ? ? ? BF=1,不能读取指令或数据
? ? ? ? ? ? ? ? BF=0,能够读取指令或数据
? ? ? ? 命令10:写数据
? ? ? ? 命令11:读数据?
显示指令的步骤?? ? ? ? ????????正脉冲:E=0,E=1,E=0,//正脉冲图像为凸状
????????读状态->写命令->写数据->自动显示。4个步骤,步骤1时加时不加
? ? ? ? ????????读状态代码:
void check(){ unsigned char dt; do{ dt = 0xff;//无执行命令初始值为0 E = 0; RS = 0; RW = 1; E = 1; dt = P0;//读取P0状态 }while(dt & 0x80);//检测BF的值命令9 E = 0; }
? ? ? ? ????????写命令代码:
????????????????
void command(uchar com){//写命令 check();//检测显示屏是否忙命令9 //对照读写操作规定表 E = 0; RS = 0; RW = 0; //对照命令字表进行设值 P0 = com;//将命令写入 E = 1; _nop_();//延时1个机械周期 E = 0; delay(1);//延时 }
????????????????写数据
void write(unsigned char dat){ check(); E = 0; RS = 1; RW = 0; P0 = dat;//将字符对应的ASSCLL码写入P0总线 E = 1; _nop_();//延时一个机器周期 E = 0; delay(1);//延时 }
一.1602静态显示?
1.仿真图
?2.流程图
用到的元器件:
1.74LS245
2.LM016L
3.AT89C51
3.代码
代码讲解:首先先写入最基本的几个LCD灯的操作函数,状态函数,命令函数,写入数据函数,写完之后,main先调用init函数,然后看我直接调用了string函数,显示屏就直接显示,下面就去看我的string函数代码段,注释很详细,
#include
#include
//包含——nop()空函数指令的头文件 unsigned int i;//string函数里面用于写入数据 sbit RS = P1^0; sbit RW = P1^1; sbit E = P1^2; //屏幕初始化函数 void init(); //命令函数 void command(unsigned char com); //写入数据函数 void write(unsigned char dat); //检查屏幕是否忙函数 void check(); //显示输入字符串函数 void string(unsigned char ad,unsigned char s[]); //延时函数 void delay(unsigned int j); void main(void){ init(); while(1){ string(0x85,"Hello"); string(0xc2,"Stefanie Sun"); } } void init(){ command(0x38);//命令6;2行显示,5*7点阵,8位数据接口 command(0x0c);//命令4;开2行显示,光标关,无闪烁 //command(0x06);//命令3:写入1个字符整屏显示右移 //command(0x01);//命令1:清屏 //write_command(0x08);//滚动指令 delay(1); } void command(unsigned char com){//写命令 check();//检测显示屏是否忙命令9 //对照读写操作规定表 E = 0; RS = 0; RW = 0; //对照命令字表进行设值 P0 = com;//将命令写入 E = 1; _nop_();//延时1个机械周期 E = 0; delay(1);//延时 } void write(unsigned char dat){ check(); E = 0; RS = 1; RW = 0; P0 = dat;//将字符对应的ASSCLL码写入P0总线 E = 1; _nop_();//延时一个机器周期 E = 0; delay(1);//延时 } void check(){ unsigned char dt; do{ dt = 0xff;//无执行命令初始值为0 E = 0; RS = 0; RW = 1; E = 1; dt = P0;//读取P0状态 }while(dt & 0x80);//检测BF的值命令9 E = 0; } void string(unsigned char ad,unsigned char s[]){ command(ad);//命令8,写入显示的位置 for(i = 0; s[i]!='\0';i++){//遍历字符数组利用for循环一个一个写入字符(字符自动转换为ASSCLL码) write(s[i]);//写入数据 } } void delay(unsigned int x){ unsigned int i,j; for(i = 0; i
4.结果
STC89Cx开发板LCD模块
?二.1602动态显示(电子钟)
1.仿真图
用到的元器件:
1.74LS245
2.LM016L
3.AT89C51
2.流程图
?3.代码
代码讲解:写入状态函数,命令函数,数据函数,string函数,动态显示函数ddisplay(下面代码段有解释),mian中陆续调用init,string,并且启动了T0定时器,是为了更新时分秒的值,在while死循环中一直调用ddisplay以达到更新lcd显示屏的目的。注意:main函数和定时器是2个线程
#include
#include
//包含——nop()空函数指令的头文件 unsigned int i;//string函数里面用于写入数据 sbit RS = P2^6; sbit RW = P2^5; sbit E = P2^7; unsigned char hour = 0;//小时 unsigned char min = 0;//分钟 unsigned char secOnd= 0;//秒 unsigned char temp;//替换常量 unsigned char cishu = 20;//50ms*20=1s //屏幕初始化函数 void init(); //命令函数 void command(unsigned char com); //写入数据函数 void write(unsigned char dat); //检查屏幕是否忙函数 void check(); //显示输入字符串函数 void string(unsigned char ad,unsigned char s[]); //延时函数 void delay(unsigned int j); //动态显示函数 void ddisplay(unsigned char ad, unsigned char value); void main(void){ init(); //给显示屏设置初值 string(0x81,"Day 2022-4-22");//13 string(0xc1,"Time 00:00:00");//13 EA = 1;//开启中断总开关 ET0 = 1;//运行T0中断 TMOD = 0x01;//设置T0工作方式为工作方式1 //50ms TH0 = (65536 - 50000) / 256;//设置T0高8位初值 TL0 = (65536 - 50000) % 256;//设置T0低8位初值 TR0 = 1;//启动定时器 while(1){ ddisplay(0xc6, hour);//小时 ddisplay(0xc9, min);//分钟 ddisplay(0xcc, second);//秒 } } void NowTime() interrupt 1{ //5ms TH0 = 15536 / 256; TL0 = 15536 % 256; cishu--; if(cishu == 0){ second++; cishu = 20;}//50ms*20=1000ms=1s if(secOnd== 60){ secOnd= 0; min++; } if(min == 60){ min = 0; hour++; } if(hour == 24){ hour = 0; } } void init(){ command(0x38);//命令6;2行显示,5*7点阵,8位数据接口 command(0x0c);//命令4;开2行显示,光标关,无闪烁 //command(0x06);//命令3:写入1个字符整屏显示右移 //command(0x01);//命令1:清屏 //write_command(0x08);//滚动指令 delay(1); } void command(unsigned char com){//写命令 check();//检测显示屏是否忙命令9 //对照读写操作规定表 E = 0; RS = 0; RW = 0; //对照命令字表进行设值 P0 = com;//将命令写入 E = 1; _nop_();//延时1个机械周期 E = 0; delay(1);//延时 } void write(unsigned char dat){ check(); E = 0; RS = 1; RW = 0; P0 = dat;//将字符对应的ASSCLL码写入P0总线 E = 1; _nop_();//延时一个机器周期 E = 0; delay(1);//延时 } void check(){ unsigned char dt; do{ dt = 0xff;//无执行命令初始值为0 E = 0; RS = 0; RW = 1; E = 1; dt = P0;//读取P0状态 }while(dt & 0x80);//检测BF的值命令9 E = 0; } void string(unsigned char ad,unsigned char s[]){ command(ad); for(i = 0; s[i]!='\0';i++){ write(s[i]); } } void ddisplay(unsigned char ad, unsigned char value){ command(ad); temp = value / 10 + '0';//十位数转换成ASSCLL码 write(temp);//十位数写入显示屏 temp = value % 10 + '0';//个位数转换成ASSCLL码 write(temp);//个位数写入显示屏 } void delay(unsigned int x){ unsigned int i,j; for(i = 0; i
4.结果
三.电子钟(有功能键)
1.仿真图
用到的元器件
1.74LS245
2.AT89C51
3.BUTTON
4.LM016L
5.NPN
6.RES
7.BUZZER
?2.流程图
?3.代码
代码分析:main函数中调用init,string函数,开启外部中断0和启动计时器T0,进入while死循环,用ddispaly实时更新。进入闹钟设置的关键代码写到main中,因为,如果写到中断里面,那么T0会被阻断,时分秒不再更新。而设置当前时间的关键代码写到外部中断0里面去,因为当你在设置时间,时分秒是不会更新的
#include
#include
sbit RS = P2^0; sbit RW = P2^1; sbit E = P2^2; sbit k2 = P1^0;//进入设置现在的时间中断 sbit k3 = P1^1;//进入设置现在的时间中断小时加1操作 sbit k4 = P1^2;//进入设置现在的时间中断分钟加1操作 sbit secOndadd= P1^3;//进入设置闹钟按下按钮秒数加1 sbit minadd = P1^4;//进入设置闹钟按下按钮分钟加1 sbit houradd = P1^5;//进入设置闹钟按下按钮小时加1 sbit re = P1^6;//退出闹钟设置 sbit buzz = P1^7;//到点了闹钟响 sbit clock = P2^7;//设置闹钟(main里面) unsigned char clocksecOnd= 0;//记录闹钟的秒 unsigned char clockmin = 1;//记录闹钟的分钟 unsigned char clockhour = 0;//记录闹钟的小时 unsigned char secOnd= 0;//记录当前的秒 unsigned char min = 0;//记录当前的分钟 unsigned char hour = 0;//记录当前的小时 unsigned char temp;//中间变量 unsigned char count = 5;//喇叭响的次数 unsigned char cishu = 20;//50ms*20=1s unsigned int i;//string函数里面用于写入数据 //命令函数 void command(char com); //写入数据函数 void write(char dat); //初始化函数 void init(); //显示当前设置的闹钟时间函数 void clockdisplay(); //检查显示屏是否忙 void check(); //显示输入字符串函数 void string(unsigned char ad,unsigned char s[]); //延时函数 void delay(unsigned int x); //动态显示函数 void ddisplay(unsigned char ad, unsigned char value); void main(){ init(); //初始化显示屏 string(0x81,"Day 2022-4-22");//13 string(0xc1,"Time 00:00:00");//13 //T0用来计时,外部中断0用来设置当前的时间 //打开中断总开关 EA = 1; //允许外部中断0中断 EX0 = 1; //中断方式为下降沿方式 IT0 = 1; //允许定时器T0中断 ET0 = 1; //T0的工作方式为工作方式1 TMOD = 0x01; //初始化喇叭 buzz = 0; //初始化定时器初值 TH0 = 15536 / 256;//0.05s TL0 = 15536 % 256;//0.05s TR0 = 1;//T0开始定时 while(1){ ddisplay(0xc6, hour);//小时 ddisplay(0xc9, min);//分钟 ddisplay(0xcc, second);//秒; //到点闹钟响 if(clocksecOnd== second && clockmin == min && clockhour == hour){ while(count--){ ddisplay(0xc6, clockhour);//小时 ddisplay(0xc9, clockmin);//分钟 ddisplay(0xcc, clocksecond);//秒; buzz = 1; delay(500); ddisplay(0xc6, clockhour);//小时 ddisplay(0xc9, clockmin);//分钟 ddisplay(0xcc, clocksecond);//秒; buzz = 0; delay(500); } count = 5; } if(clock == 0){//进入闹钟设置不阻断T0进入当前时间设置阻断T0 while(re != 0){ ddisplay(0xc6, clockhour);//小时 ddisplay(0xc9, clockmin);//分钟 ddisplay(0xcc, clocksecond);//秒; if(secOndadd== 0){ delay(300);//去抖 clocksecond++; if(clocksecOnd== 60) clocksecOnd= 0; } if(minadd == 0){ delay(300); clockmin++; if(clockmin == 60) clockmin = 0; } if(houradd == 0){ delay(300); clockhour++; if(clockhour == 60) clockhour = 0; } } } } } void delay(unsigned int x){ unsigned int i, j; for(i = 0; i
4.结果
AT89CX开发板外部中断0是K3按钮
用开发板实现的话,和仿真差别有点大
1.按键不够,必须用到矩阵式键盘(上面的使用方法,可看函数)
2.喇叭无法使用,因为喇叭和矩阵式键盘的P口都是一样都是P1服了
解决方案
自己买材料按照仿真图焊接一个
下面是模块图(我的开发板喇叭是P1.5,外部中断0是P32->K3
#include
#include
//矩阵键盘的列 sbit S1 = P1^3; sbit S2 = P1^2; sbit S3 = P1^1; sbit S4 = P1^0; sbit buzz = P1^5;//蜂鸣器和按键的P0一样写不了响的功能 //一共四行起到扫描行的作用 int line; unsigned char flag; sbit RS = P2^6; sbit RW = P2^5; sbit E = P2^7; sbit clock = P3^0;//clock是独立式键盘按钮K3->P3.0 unsigned char clocksecOnd= 0;//记录闹钟的秒 unsigned char clockmin = 1;//记录闹钟的分钟 unsigned char clockhour = 0;//记录闹钟的小时 unsigned char secOnd= 0;//记录当前的秒 unsigned char min = 0;//记录当前的分钟 unsigned char hour = 0;//记录当前的小时 unsigned char temp;//中间变量 unsigned char count = 5;//喇叭响的次数 unsigned char cishu = 20;//50ms*20=1s unsigned int i;//string函数里面用于写入数据 //命令函数 void command(char com); //写入数据函数 void write(char dat); //初始化函数 void init(); //显示当前设置的闹钟时间函数 void clockdisplay(); //检查显示屏是否忙 void check(); //显示输入字符串函数 void string(unsigned char ad,unsigned char s[]); //延时函数 void delay(unsigned int x); //动态显示函数 void ddisplay(unsigned char ad, unsigned char value); //扫描函数 void saomiao(); void main(){ init(); //初始化显示屏 string(0x81,"Day 2022-4-22");//13 string(0xc1,"Time 00:00:00");//13 //T0用来计时,外部中断0用来设置当前的时间 //打开中断总开关 EA = 1; //允许外部中断0中断 EX0 = 1; //中断方式为下降沿方式 IT0 = 1; //允许定时器T0中断 ET0 = 1; //T0的工作方式为工作方式1 TMOD = 0x01; //初始化喇叭 buzz = 0; //初始化定时器初值 TH0 = 15536 / 256;//0.05s TL0 = 15536 % 256;//0.05s TR0 = 1;//T0开始定时 while(1){ ddisplay(0xc6, hour);//小时 ddisplay(0xc9, min);//分钟 ddisplay(0xcc, second);//秒; //到点闹钟响 if(clocksecOnd== second && clockmin == min && clockhour == hour){ while(count--){ ddisplay(0xc6, clockhour);//小时 ddisplay(0xc9, clockmin);//分钟 ddisplay(0xcc, clocksecond);//秒; buzz = 1; delay(500); ddisplay(0xc6, clockhour);//小时 ddisplay(0xc9, clockmin);//分钟 ddisplay(0xcc, clocksecond);//秒; buzz = 0; delay(500); } count = 5; } if(clock == 0){//进入闹钟设置不阻断T0进入当前时间设置阻断T0 flag = 1;//如果flag=0跳出闹钟 while(flag){ ddisplay(0xc6, clockhour);//小时 ddisplay(0xc9, clockmin);//分钟 ddisplay(0xcc, clocksecond);//秒; P1=0xef;//按行扫描键盘初始化 for(line=0;line<4;line++){ if(S1 == 0 && line == 2) { delay(300); clockhour++; if(clockhour == 60) clockhour = 0;}//按到S5代表闹钟小时设置 if(S2 == 0 && line == 2) { delay(300); clockmin++; if(clockmin == 60) clockmin = 0;}//按到S6代表闹钟分钟设置 if(S3 == 0 && line == 2) { delay(300); clocksecond++; if(clocksecOnd== 60) clocksecOnd= 0;}//按到S7代表闹钟秒设置 if(S4 == 0 && line == 2) { delay(300); flag=0;}//按到S8代表退出闹钟设置 //P1进行左移操作为什么要与运算呢?若P1=10111011,temp=~p->左移后01110111这样行扫描会出现低电平 temp=~(P1|0x0f);//注意:与数码管部分的左右移代码不同,原因是由于我们按钮按下导致P1口前4个口有低电平(代表按下)->显示对面的字符 temp=temp<<1;//数码管项目有解释说明 P1=~temp; } } } } } void delay(unsigned int x){ unsigned int i, j; for(i = 0; i 左移后01110111这样行扫描会出现低电平 temp=~(P1|0x0f);//注意:与数码管部分的左右移代码不同,原因是由于我们按钮按下导致P1口前4个口有低电平(代表按下)->显示对面的字符 temp=temp<<1;//数码管项目有解释说明 P1=~temp; } } } void ddisplay(unsigned char ad, unsigned char value){ command(ad); temp = value / 10 + '0';//十位数转换成ASSCLL码 write(temp);//十位数写入显示屏 temp = value % 10 + '0';//个位数转换成ASSCLL码 write(temp);//个位数写入显示屏 } //下面是扫描函数的逻辑 /* void saomiao(){ P1=0xef;//按行扫描键盘初始化 for(line=0;line<4;line++){ if(S1 == 0 && line == 3) { delay(300);delay(300);hour++; if(hour == 24) hour = 0; }//按到S1代表当前时间小时设置 if(S2 == 0 && line == 3) { delay(300); min++; if(min == 60) min = 0;}//按到S2代表当前时间分钟设置 if(S3 == 0 && line == 3) { delay(300); string(0xce," "); break;}//按到S3代表当前时间结束设置 if(S1 == 0 && line == 2) { delay(300); clockhour++; if(clockhour == 60) clockhour = 0;}//按到S5代表闹钟小时设置 if(S2 == 0 && line == 2) { delay(300); clockmin++; if(clockmin == 60) clockmin = 0;}//按到S6代表闹钟分钟设置 if(S3 == 0 && line == 2) { delay(300); clocksecond++; if(clocksecOnd== 60) clocksecOnd= 0;}//按到S7代表闹钟秒设置 if(S4 == 0 && line == 2) { delay(300); break;}//按到S8代表退出闹钟设置 //P1进行左移操作为什么要与运算呢?若P1=10111011,temp=~p->左移后01110111这样行扫描会出现低电平 temp=~(P1|0x0f);//注意:与数码管部分的左右移代码不同,原因是由于我们按钮按下导致P1口前4个口有低电平(代表按下)->显示对面的字符 temp=temp<<1;//数码管项目有解释说明 P1=~temp; } } */
本文《单片机基础项目(上)》版权归小罗のdiary所有,引用单片机基础项目(上)需遵循CC 4.0 BY-SA版权协议。