51单片机入门学习记录(1)
教程:江科协
目录:
[LED](#1.4 LED)
[独立按键](#1.5 独立按键)
[数码管](#1.10 数码管)
模块化编程
[矩阵键盘](#1.12 矩阵键盘)
[定时器](#1.14 定时器)
[串口](#1.18 串口)
1.1
安装kail和stc-isp 买了块板子
1-2视频还没有连接板子(板子刚下单
1.4 LED
AT89C52!!!
单片机到了 连接串口安装驱动程序CH341ser 一开始一直显示预安装 搜了很多帖子也没找到解决方案
在等官方客服回复中摸鱼了会 一个小时后再次回来连接惊喜的连接成功了
1.12回顾:应该是usb线没有连接好的问题,后续很多时候都是正常的,只有在usb未完全插入电脑的时候会提示检测未知
听了很多的硬件内部介绍 听不懂
要照明第一个LED灯:
点亮第一个LED
0赋给P20口(最低位)——操作LED 写代码的时候要转化为十进制或者十六进制
代码下载入单片机中 如下操作:
但是P2命令会一直被执行 我们需要让他持续 而不是重复执行命令
1 2 3 4 5 6 7
| #include <REGX52.H>
void main(){ P2=0xFE; while(1){ }
|
亮不同的灯:
1 2 3 4 5 6 7 8
| #include <REGX52.H>
void main(){ P2 = 0xFE; 1111 1110 P2=0xA5; while(1){ } }
|
以1s为周期闪烁
1 2 3 4 5 6 7 8 9
| #include <REGX52.H>
void main(){
while(1){ P2 = 0xFE; P2 = 0xFF; } }
|
此时单片机并没有按预期闪烁 而是减小了亮度 原因为单片机的速度是Mhz 速度很快 肉眼无法分辨
所以我们需要在两个代码里执行一定的延时
这里选错了 应该选毫秒…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include <REGX52.H> #include <INTRINS.H>
void Delay500ms() { unsigned char i, j, k;
_nop_(); i = 4; j = 205; k = 187; do { do { while (--k); } while (--j); } while (--i); }
void main(){
while(1){ P2 = 0xFE; Delay500ms(); P2 = 0xFF; Delay500ms(); } }
|
成功咯!
LED流水灯
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #include <REGX52.H> #include <INTRINS.H>
void Delay500ms() { unsigned char i, j, k;
_nop_(); i = 4; j = 205; k = 187; do { do { while (--k); } while (--j); } while (--i); }
void main(){ while(1){ P2 = 0xFE; Delay500ms(); P2 = 0xFD; Delay500ms(); P2 = 0xFB; Delay500ms(); P2 = 0xF7; Delay500ms(); P2 = 0xEF; Delay500ms(); P2 = 0xDF; Delay500ms(); P2 = 0xBF; Delay500ms(); P2 = 0x7F; Delay500ms(); } }
|
以200ms为变化间隔 实现
1.5 独立按键
独立按键
按下点亮 弹起熄灭
电子开关 按下时接通 送开始断开 通过内部的金属弹片受力弹动
按下为低电平(与GND相连)
源代码 P2 = 0xFE;
要同时给八个赋值 只想操控最低位
sbit
可以操纵位寄存器
1 2 3 4 5 6 7 8 9
| #include <REGX52.H>
void main(){
P2_0 = 0; while(1){ } }
|
要实现电子元件控制:
注意 K1和K2 的接脚不是按照顺序 K1是接在P3_1的口上
1 2 3 4 5 6 7 8 9 10 11 12 13
| #include <REGX52.H>
void main(){
while(1){ if(P3_1 == 0){ P2_0 = 0; } else{ P2_0 = 1; } } }
|
按键的抖动
由于机械触点的弹性作用,一个开关在闭合时不会马上稳定地连接,在断开时也不会一下子断开,所以在开关闭合及断开的瞬间会伴随一连串的抖动
实现按键的消抖:硬件的过滤、软件程序处理(检测)
以及检测松手
按下弹起一个周期后在执行命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <REGX52.H>
void Delay(unsigned int xms) { unsigned char i, j; while(xms--){ i = 2; j = 239; do { while (--j); } while (--i); } }
void main(){ while(1){ if(P3_1==0){ Delay(20); while(P3_1==0); Delay(20); P2_0 =~P2_0; } } }
|
但是刚刚不小心打成了P3_0==0
灯是按下后不停闪烁 其实这是一个无效语句 真正实现的内核是:
1 2 3 4 5 6 7 8 9 10
| void main(){ while(1){ if(P3_1==0){ Delay(20); Delay(20); P2_0 =~P2_0; } } }
|
只要按下P2_0会不停地取反 灯就不停地闪烁
独立按键控制LED显示二进制
P2上电默认都是高电平
方案一:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include <REGX52.H>
void Delay(unsigned int xms) { unsigned char i, j; while(xms--){ i = 2; j = 239; do { while (--j); } while (--i); } }
void main(){ while(1){ if(P3_1==0){ Delay(20); while(P3_1==0); Delay(20);
P2--; } } }
|
方案二:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #include <REGX52.H>
void Delay(unsigned int xms) { unsigned char i, j; while(xms--){ i = 2; j = 239; do { while (--j); } while (--i); } }
void main(){ unsigned char LEDnumber=0; while(1){ if(P3_1==0){ Delay(20); while(P3_1==0); Delay(20); LEDnumber++; P2 =~LEDnumber; } } }
|
独立按键控制LED移位
一个开关左右移动
原本想要更简洁一些
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void main(){ P2 = ~0x01; int i; while(1){ if(P3_1==0){ Delay(20); while(P3_1==0); Delay(20); } for(i=0;i<8;i++){ P2=~(0x01<<i); } } }
|
发现运行后 LED全灯都是小亮度亮的 只有最后一个灯在按下按钮的时候是常亮的 发现是在一次while循环里 立马就执行了移位 延时函数失效
修改程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <REGX52.H>
void Delay(unsigned int xms) { unsigned char i, j; while(xms--){ i = 2; j = 239; do { while (--j); } while (--i); } }
void main(){ unsigned char LEDnumber; P2 =~0x01; while(1){ if(P3_1==0){ Delay(20); while(P3_1==0); Delay(20); LEDnumber++; if(LEDnumber>=8){ LEDnumber=0; } P2=~(0x01<<LEDnumber); } } }
|
实现两个开关左右移动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| #include <REGX52.H>
void Delay(unsigned int xms) { unsigned char i, j; while(xms--){ i = 2; j = 239; do { while (--j); } while (--i); } }
unsigned char LEDnumber; void main(){ P2 =~0x01; while(1){ if(P3_1==0){ Delay(20); while(P3_1==0); Delay(20); LEDnumber++; if(LEDnumber>=8){ LEDnumber=0; } P2=~(0x01<<LEDnumber); } if(P3_0==0){ Delay(20); while(P3_0==0); Delay(20); if(LEDnumber==0){ LEDnumber = 7; } else{ LEDnumber--; } P2=~(0x01<<LEDnumber); } } }
|
呜呜呜还是有很多代码的小细节要注意的
注意:unsigned char LEDnumber;//最好定义全局变量
默认是0 在main里定义可能不是0 在初始定义时好像不能赋初始值 会报错 呜呜呜已经把这些细碎知识点忘光了
1.10 数码管
数码管:多个发光二极管封装在一起组成“ 8 ”字型的器件
在数码管上任意位置显示任意数据(静态显示)
引脚就近原则
要点亮一个数字 以“6”为例子
开发板上有两个四位一体的数码管
138编码器
注意Y0
对应LED1
(有错位)
数码管
74HC254
是一个双向数据缓冲,为了提高单片机的负载能力 (能够带动很多LED灯)
当DIR
(连接LE
)的输入为高电平时,从左向右缓冲数据,反之,从右向左读取数据
而左边部分与138编码器相连接,目的是为了使得单片机提供的为控制信号,缓冲到右边,由电源VCC直接提供能量控制LED灯
主要是因为高电平驱动能力弱(低电平驱动能力强)
使某一位显示一个数据:
以LED6显示6为例
1 2 3 4 5 6 7 8 9 10 11 12
| #include <REGX52.H>
void main(){ P2_4 = 1; P2_3 = 0; P2_2 = 1; P0 = 0x7D; while(1){ } }
|
LED模块因为引脚冲突所以也会亮
把数码管的某一位显示某一个数字进行封装,抽取出变成子函数以达到代码的复用和优化程序结构的目的
参数:位置、数字
一般用数组显示段码
段码表:
1 2 3
| 0 1 2 3 4 5 6 7 8 9
0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,
|
优化程序结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <REGX52.H>
unsigned char NixieTable[] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};
void Nixie(unsigned char location,number){ switch(location){ case 1:P2_4=1;P2_3=1;P2_2=1;break; case 2:P2_4=1;P2_3=1;P2_2=0;break; case 3:P2_4=1;P2_3=0;P2_2=1;break; case 4:P2_4=1;P2_3=0;P2_2=0;break; case 5:P2_4=0;P2_3=1;P2_2=1;break; case 6:P2_4=0;P2_3=1;P2_2=0;break; case 7:P2_4=0;P2_3=0;P2_2=1;break; case 8:P2_4=0;P2_3=0;P2_2=0;break; } P0 = NixieTable[number]; }
void main(){ Nixie(1,1); while(1){ } }
|
数码管的动态显示 任意位置 多显示(动态扫描)
不断地扫描
不添加延时的话会出现这样的情况:
故而数码管需要 消影
位选—>段选—>位选—>段选—>位选—>段选—>位选—>段选
前一个段选到下一个位选很短的时刻 上一个数据会窜到下一个数据上!!!(段没变 位变了)
所以要
位选—>段选—>清零—>位选
但不能立马清零 立马清零数码管会变得比较暗
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| #include <REGX52.H>
unsigned char NixieTable[] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};
void Delay(unsigned int xms) { unsigned char i, j; while(xms--){ i = 2; j = 239; do { while (--j); } while (--i); } }
void Nixie(unsigned char location,number){ switch(location){ case 1:P2_4=1;P2_3=1;P2_2=1;break; case 2:P2_4=1;P2_3=1;P2_2=0;break; case 3:P2_4=1;P2_3=0;P2_2=1;break; case 4:P2_4=1;P2_3=0;P2_2=0;break; case 5:P2_4=0;P2_3=1;P2_2=1;break; case 6:P2_4=0;P2_3=1;P2_2=0;break; case 7:P2_4=0;P2_3=0;P2_2=1;break; case 8:P2_4=0;P2_3=0;P2_2=0;break; } P0 = NixieTable[number]; Delay(1); P0=0x00; }
void main(){ while(1){ Nixie(1,1);
Nixie(2,2);
Nixie(3,3);
} }
|
数码管驱动方式:
单片机直接扫描:硬件设备简单,但会耗费大量的单片机CPU时间;
专用驱动芯片:内部自带显存、扫描电路,单片机只需告诉它显示什么即可 GM1640
1.11模块化编程
模块化编程
避免所有函数都放在main函数里
要将不同模块的代码放在不同的c文件里
在.h文件里提供外部可调用函数的声明
预编译:
1 2 3 4 5 6 7
| #ifndef __DELAY_H__ #define __DELAY_H__
void Delay(unsigned int xms);
#endif
|
•此外还有#ifdef
,#if
,#else
,#elif
,#undef
等
代码模块化实例
include优先到标准库中去搜索模块。
include"file"优先到自定义库中去搜索模块。
main.c
1 2 3 4 5 6 7 8 9 10 11 12
| #include <REGX52.H> #include "Delay.h" #include "Nixie.h"
void main(){ while(1) { Nixie(1,1); Nixie(2,2); Nixie(3,3); } }
|
Delay.h
1 2 3 4 5 6
| #ifndef __DELAY_H__ #define __DELAY_H__
void Delay(unsigned int xms);
#endif
|
Delay.c
1 2 3 4 5 6 7 8 9 10 11 12
| void Delay(unsigned int xms) { unsigned char i, j; while(xms--){ i = 2; j = 239; do { while (--j); } while (--i); } }
|
Nixie.h
1 2 3 4 5 6
| #ifndef __NIXIE_H__ #define __NIXIE_H__
void Nixie(unsigned char location,number);
#endif
|
Nixie.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #include <REGX52.H> #include "Delay.h"
unsigned char NixieTable[] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};
void Nixie(unsigned char location,number){ switch(location){ case 1:P2_4=1;P2_3=1;P2_2=1;break; case 2:P2_4=1;P2_3=1;P2_2=0;break; case 3:P2_4=1;P2_3=0;P2_2=1;break; case 4:P2_4=1;P2_3=0;P2_2=0;break; case 5:P2_4=0;P2_3=1;P2_2=1;break; case 6:P2_4=0;P2_3=1;P2_2=0;break; case 7:P2_4=0;P2_3=0;P2_2=1;break; case 8:P2_4=0;P2_3=0;P2_2=0;break; } P0 = NixieTable[number]; Delay(1); P0=0x00; }
|
要注意继承问题 在数码管模块里也用到了Delay
函数 所以要在模块里引用Delay
的头文件
感觉每次调用要复制这些模块到同一个文件夹下面好麻烦
有空的时候要专门做一个自建库 然后修改C51的路径
LCD1602调试
连接好后 数码管会乱码
使用LCD1602液晶屏作为调试窗口,提供类似printf函数的功能,可实时观察单片机内部数据的变换情况,便于调试和演示。
只需要知道所提供函数的作用和使用方法
有报错:
requires ANSI-style prototype
搜了一下:
需要要把调用的子函数放到main前面,或者在main函数前面先把需要调用的子函数定义声明一下。
也不行
把文件里的函数名字复制过来就好了
然后自己再重新写也好了 好迷惑···
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <REGX52.H> #include "LCD1602.h"
void main(){ LCD_Init(); LCD_ShowChar(1,1,'A'); LCD_ShowString(1,3,"Hello"); LCD_ShowNum(1,9,123,3); LCD_ShowSignedNum(1,13,-66,1); LCD_ShowHexNum(2,1,0xA8,2); LCD_ShowBinNum(2,4,0xAA,8); while(1){ } }
|
方便地显示字符串/数字:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <REGX52.H> #include "LCD1602.h" #include "Delay.h"
int result =0;
void main(){ LCD_Init(); while(1){ result++; Delay(500); LCD_ShowNum(1,1,result,3); } }
|
1.12 矩阵键盘
数码管扫描(输出扫描)
原理:显示第1位→显示第2位→显示第3位→……,然后快速循环这个过程,最终实现所有数码管同时显示的效果
矩阵键盘扫描(输入扫描)
原理:读取第1行(列)→读取第2行(列) →读取第3行(列) → ……,然后快速循环这个过程,最终实现所有按键同时检测的效果
以上两种扫描方式的共性:节省I/O口
读取按键显示
扫描法
逐行扫描
如果P17,P16,P15,P14
全部给高电平1 无论按键按不按下 下面的都是高电平
而将这几个口连接低电平(GND),可以查看P13,12,11,10
,如果是0的话说明按键被按下了
所以想要扫描第二行的话只需要将第二行P16
置0,其他三行给1,然后判断P13,12,11,10
相当于把键盘逐行扫描了一遍
⚠但逐行扫描的话,P15有可能会驱动五线四相步进电机(引脚冲突),导致无源蜂鸣器自己响应,如下图
故采用
逐列扫描
即给P13,12,11,10
进行赋值,读取P17,P16,P15,P14
单片机配置是准双向接口输出配置,当口线输出为1时驱动能力很弱,允许外部装置将其拉低,而当引脚输出为低时,它的驱动能力很强,可吸收相当大的电流。
P1,P2,P3
都是“弱上拉”,P0
是一种开漏输出
弱上拉:当输出高电平时,能够输出的电流很小,很容易被别的强下拉拉低
开漏输出:输出端相当于三极管的集电极. 高电平状态需要上拉电阻才行.
上拉:将不确定的信号,固定在高电平,电源到器件引脚上的电阻叫上拉电阻,作用是平时使用该引脚为高电平,上拉是对器件注入电流,即灌电流
下拉:将不确定的信号,固定到地点平,地到器件引脚的电阻叫下拉电阻,作用是平时使该引脚为低电平,下拉是从器件输出电流,即拉电流
参考博客
💡想在他编程之前创建自建库 呜呜好像要在一个project下 类似 下次再看看惹
up主的代码是一条条写下去的 类似这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <REGX52.H> #include "Delay.h"
unsigned char MatrixKey(){ unsigned char key=0; P1 = 0xFF; P1_3 = 0; if(P1_7 ==0){Delay(20);while(P1_7==0);Delay(20);key=1;} if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);key=5;} if(P1_5 ==0){Delay(20);while(P1_5==0);Delay(20);key=9;} if(P1_4 ==0){Delay(20);while(P1_4==0);Delay(20);key=13;}
return key; }
|
感觉有点麻烦 想用循环 参考一个博主的代码进行修改和理解
得到扫描法代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #include <REGX52.H> #include "Delay.h" #include "intrins.h" #define keyboard P1
unsigned char MatrixKey() { unsigned char row_scan_code=0x01; unsigned char col_scan_code=0xEF; unsigned char keycode; unsigned char i,x,j;
for(i=0;i<4;i++) { keycode=i+1; keyboard=col_scan_code; x=keyboard; for(j=0;j<4;j++) { if(!(x&row_scan_code)) { keycode+=4*j; Delay(20); while((keyboard&0x0f)!=0x0f); Delay(20); P1=0x0F; return keycode; } else row_scan_code=_crol_(row_scan_code,1); } col_scan_code=_crol_(col_scan_code,1); row_scan_code=0x01; } keycode=0; return keycode; }
|
_crol_
函数
真实测试后跳转回来,博主的代码好像和我实际的东西是反的,如果从1开始编号,原始状态的列应该是1111 0111
(P1_3~P1_0
是低四位),而博主是 1110 1111
,是先把P1_4
置0了,不过回去看博主是三行四列的键盘,引脚是不一样的,上述代码只改动了行列的问题,未考虑到引脚引出的不同,大意了~重新修改了一下代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| #include <REGX52.H> #include "Delay.h" #include "intrins.h" #define keyboard P1
unsigned char MatrixKey() { unsigned char row_scan_code=0x80; unsigned char col_scan_code=0xF7; unsigned char keycode; unsigned char i,x,j;
for(i=0;i<4;i++) { keycode=i+1; keyboard=col_scan_code; x=keyboard; for(j=0;j<4;j++) { if(!(x&row_scan_code)) { keycode+=4*j; Delay(20); while((keyboard&0xF0)!=0xF0); Delay(20); P1=0xF0; return keycode; } else row_scan_code=_cror_(row_scan_code,1); } col_scan_code=_cror_(col_scan_code,1); row_scan_code=0x80; } keycode=0; return keycode; }
|
耶✌
线反法
原理:
首先使P1口的高四位输出高电平,P1口低四位输出低电平,这时键盘的行线被拉高,列线被拉低。如果有按键按下,则某一条行线将被拉低,此时读取P1口高四位,读取到的将不再全为高电平,说明有按键按下。(在判断是否有按键按下这一点上,线反法与逐行逐列扫描法是一致的)根据读取到0值的I/O口所连接的行线,就可以判断出按下的按键位于哪一行。接下来使P1口的高四位输出低电平,P1口低四位输出高电平(即与上次输出的电平相反,因此称为线反法)。如果有按键按下,此时读取P1口低四位,读取到的将不再全为高电平,根据读取到0值的I/O口所连接的列线,就可以判断出按下的按键位于哪一列。综合按键所在的行线与列线,即可唯一确定按键所在位置,进而获取按键的键值。
第一步:P1.0–P1.3输出为0(4行),P1.4–P1.7做输入(4列)。
第二步:读取P1.4–P1.7的数据并保存。
第三步:将第二步中保存的数据从P1.4–P1.7输出,此时P1.0–P1.3做输入。
第四步:读取P1的数据,若无按键按下,此时读取的数据为0xff,若有按键按下,P1.0–P1.3中必定有一个为0(对应着被按下按键的行),P1.4–P1.7中必定有一个为0(对应着被按下按键的列)。
第五步:依据按键数组查找被按下的按键号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
#define keyboard P1 unsigned char Check_Keydown() { unsigned char KeyValue=0; keyboard=0x0f; if(keyboard!=0x0f) { delay_ms(10); if(keyboard!=0x0f) { keyboard=0X0F; switch(keyboard) { case(0X07): KeyValue=1;break; case(0X0b): KeyValue=2;break; case(0X0d): KeyValue=3;break; case(0X0e): KeyValue=4;break; } keyboard=0XF0; switch(keyboard) { case(0X70): KeyValue=KeyValue;break; case(0Xb0): KeyValue=KeyValue+4;break; case(0Xd0): KeyValue=KeyValue+8;break; case(0Xe0): KeyValue=KeyValue+12;break; } while(keyboard!=0xf0); return KeyValue; } else { return 0; } } return 0; }
|
version2 参考博主:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| #include <REGX51.H>
#define SEGPORT P0 #define KEYPORT P1
typedef unsigned char u8; typedef unsigned int u16;
unsigned char code LEDCODE[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71,0x40};
unsigned char KEYCODE[]= {0XEE,0XDE,0XBE,0X7E, 0XED,0XDD,0XBD,0X7D, 0XEB,0XDB,0XBB,0X7B, 0XE7,0XD7,0XB7,0X77};
void delayxms(u16 s); u8 KEY_Scan(void);
void main (void) { while(1) { SEGPORT=~LEDCODE[KEY_Scan()]; } }
void delayxms(u16 s) { u16 i,j; for(i=0;i<s;i++) { for(j=0;j<120;j++) {} } }
u8 KEY_Scan(void) { u8 temp,num; KEYPORT=0xf0; temp=KEYPORT; temp&=0xf0; if(temp!=0xf0) { delayxms(5); KEYPORT=0xf0; temp=KEYPORT; temp&=0xf0; if(temp!=0xf0) { temp|=0x0f; KEYPORT=temp; temp=KEYPORT; for(num=0;num<16;num++) { if(temp==KEYCODE[num]) break; } return num; } return 16; } else return 16; }
|
version3 参考博主:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| #include <reg52.h> typedef unsigned char uchar; typedef unsigned int uint; uchar code KEY_TABLE[] = { 0xEE, 0xDE, 0xBE, 0x7E, 0xED, 0xDD, 0xBD, 0x7D, 0xEB, 0xDB, 0xBB, 0x7B, 0xE7, 0xD7, 0xB7, 0x77 }; uchar code TABLE[] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71, }; void Delay(uchar m) { --m; } void main() { uchar temp, key, i; while(1) { P3 = 0xF0; if (P3 != 0xF0) { Delay(2000); if (P3 != 0xF0) { temp = P3; P3 = 0x0F; key = temp | P3; for (i = 0; i < 16; ++i) if (key == KEY_TABLE[i]) break; P2 = TABLE[i]; } } } }
|
实现形式有很多 有空的时候对比一下这三个方案 今天单片机花了很久的时间 就先用扫描法继续推进吧~
main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <REGX52.H> #include "Delay.h" #include "LCD1602.h" #inlcude "MatrixKey.h"
unsigned char keynumber;
void main(){ LCD_Init(); while(1){ keynumber = MatrixKey(); if(keynumber){ LCD_ShowNum(1,1,keynumber,2); } } }
|
!!得要有if(keynumber)
的判断
如果没有的话:1.会显示00 2.按下按键后松开由于进入下一次循环过快会立马刷新为00 无法看清01 会感觉一直是00 所以要判断!!
电子密码锁
~矩阵键盘的应用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| #include <REGX52.H> #include "Delay.h" #include "LCD1602.h" #inlcude "MatrixKey.h"
unsigned char keynumber; unsigned int password,count; unsigned int truenumber = 3060;
void main(){ LCD_Init(); LCD_ShowString(1,1,"Password:"); while(1){ keynumber = MatrixKey(); if(keynumber){ if(keynumber<=10){ if(count<4){ password = password*10+keynumber%10; count++; } else{ if(password>=1000){ password = password%(password/1000*1000)*10+keynumber%10; } else{ password = password*10+keynumber%10; } } LCD_ShowNum(2,1,password,4); } if(keynumber==11){ if(password == truenumber){ LCD_ShowString(1,11," OK!"); } else{ LCD_ShowString(1,11,"Wrong!"); password = 0; count = 0; LCD_ShowNum(2,1,password,4); } } if(keynumber==12){ password = 0; count = 0; LCD_ShowNum(2,1,password,4); } } } }
|
由于while的存在 可以接二连三地往后更新密码 不用担心按键下一次要执行是否会清零的问题
但是按到第六位左右会超过int的范围 于是要有预防措施 添加count
变量来计次 在up主的功能上多加了一个计次超过4也可以始终不断读取的功能(那这样好像S12就没有用了哦hhh 作为一键清零吧~)
基础上加一个撤销的功能 只删除一个数字 比较简单就不写下去了~
有一个bug 密码不能为0
今日总结:
好家伙!一个矩阵键盘写的字比之前几天加起来的都多!三千多个字 也可能因为代码贴的太多了hhh
一个功能的代码实现可以多样化 耐心尝试与探索 仔细理解 遇到不会的即使搜索相关帖子解决 可以有很大收获
美中不足今天花了好多时间几乎下午三四点开始今天单片机的学习 除了晚饭和饭后散步 一直搞到现在十点
但是今天学习大家有趣的代码以及一些方便的函数 真的会很开心 主动性吸收知识真的很取悦自己
1.14 定时器
独立按键与流水灯的联动! 直接加在一起的话会有很长时间的delay延迟 用定时器使其灵敏
之前都是单片机控制的L口外部实现的,而定时器属于单片机的内部资源,其电路的连接和运转均在单片机内部完成
(1)用于计时系统,可实现软件计时,或者使程序每隔一固定时间完成一项操作
(2)替代长时间的Delay,提高CPU的运行效率和处理速度(原来的Delay会耗CPU 效率低)
定时器内部工作原理
STC89C52
的T0和T1四种工作模式:
模式0:13位定时器/计数器
模式1:16位定时器/计数器(常用)
模式2:8位自动重装模式
模式3:两个8位计数器
以模式1为例
中断系统
流程
内部结构图
PT0为1 走高优先级 PT0为0 走低优先级
单片机通过配置寄存器来控制内部线路的连接—>完善不同的功能
中断程序设置
应用定时器0
先配置工作模式寄存器TMOD:
结合流程图👇中的门电路可以看出GATE
给0
是为了让INT1
单独控制 C/T
给0
是为了工作在计时器模式 M1M0
给0、1
是为了工作在工作模式1
(16位)
接下来配置TCON
由于gate
给了0
所以IE0
和IT0
不用配置了
不可位寻址寄存器只能整体赋值,可位寻址寄存器可以对其中某一位单独赋值
1.17 定时器(2)
摸鱼了两天+昨天出去玩 停了三天才继续上次的内容,年前应该是跟不完up主的内容了,看大纲轴真的可以很清晰地看出摸鱼的日子,SAD,不要太摆了惹~
接下来配置TH0
和TL0
选了12分频原默认值是:每隔1us
+1 计数到最大值(65535
)后中断 总定时间为65535us
想要实现的目标:每隔1s发生中断,上面的默认达不到1s,所以要不断地累计中断
配置中断路径
具体内部结构图
中断后的程序
test中断程序
以1ms中断为例子
实现手段:赋初值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #include <REGX52.H>
void timer0_Init(){ TMOD=0x01; TF0=0; TR0=1; TH0=64535/256; TL0=64535%256; ET0=1; EA=1; PT0=0; }
void main(){ timer0_Init(); while(1){ } }
void Timer0_Rountine() interrupt 1{ P2_0=0; }
|
以1s为目标
实现手段:赋初值+循环
注意:计数器溢出后会TH0
和TL0
会变成0,而不是原来给的初值,所以进入中断要重新赋初值
TMOD=0x01;
会把定时器1的状态默认刷新为0000
,所以要改成TMOD&=0xF0;TMOD|=0X01;
,注意不能直接TMOD&=0xF1;
如果原来最低位为0的话就不能实现功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include <REGX52.H>
void timer0_Init(){
TMOD &= 0xF0; TMOD |= 0X01; TF0 = 0; TR0 = 1; TH0 = 64535/256; TL0 = 64535%256+1; ET0 = 1; EA = 1; PT0 = 0; }
void main(){ timer0_Init(); while(1){ } }
unsigned int count; void Timer0_Rountine() interrupt 1{ TH0 = 64535/256; TL0 = 64535%256; count++; if(count>=1000){ count = 0; P2_0 = ~P2_0; } }
|
软件里也有现成的代码配置:
但是中断系统配置要自己写一下:
1 2 3
| ET0 = 1; EA = 1; PT0 = 0;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| #include <REGX52.H>
void timer0_Init(){
TMOD &= 0xF0; TMOD |= 0x01; TL0 = 0x18; TH0 = 0xFC; TF0 = 0; TR0 = 1;
ET0 = 1; EA = 1; PT0 = 0; }
void main(){ timer0_Init(); while(1){ } }
unsigned int count; void Timer0_Rountine() interrupt 1{ TH0 = 0xFC; TL0 = 0x18; count++; if(count>=1000){ count = 0; P2_0 = ~P2_0; } }
|
将1s中断程序模块化
Timer0.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| #include <REGX52.H>
void timer0_Init(){
TMOD &= 0xF0; TMOD |= 0x01; TL0 = 0x18; TH0 = 0xFC; TF0 = 0; TR0 = 1;
ET0 = 1; EA = 1; PT0 = 0; }
|
Timer0.h
1 2 3 4 5 6
| #ifndef __Timer0_H__ #define __Timer0_H__
void timer0_Init();
#endif
|
main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #include <REGX52.H> #include "Timer0.h"
void main(){ timer0_Init(); while(1){ } }
void Timer0_Rountine() interrupt 1{ static unsigned int count; TH0 = 0xFC; TL0 = 0x18; count++; if(count>=1000){ count = 0; P2_0 = ~P2_0; } }
|
按键控制LED流水灯模式
获取独立按键的模块化编程:
Key.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <REGX52.H> #include "Delay.h"
unsigned char key(){ unsigned char keynumber=0; if(P3_1==0){Delay(20);while(P3_1==0);Delay(20);keynumber=1;} if(P3_0==0){Delay(20);while(P3_0==0);Delay(20);keynumber=2;} if(P3_2==0){Delay(20);while(P3_2==0);Delay(20);keynumber=3;} if(P3_3==0){Delay(20);while(P3_3==0);Delay(20);keynumber=4;} return keynumber; }
|
Key.h
1 2 3 4 5 6
| #ifndef __Key_H__ #define __Key_H__
unsigned char key();
#endif
|
写完模块最好立即测试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <REGX52.H> #include "Timer0.h" #include "Key.h" #include <INTRINS.H>
unsigned char keynum,ledmode;
void main(){ P2 = 0xFE; timer0_Init(); while(1){ keynum = key(); if(keynum==1){ ledmode++; if(ledmode>=2)ledmode = 0; } } }
void Timer0_Rountine() interrupt 1{ static unsigned int count; TH0 = 0xFC; TL0 = 0x18; count++; if(count>=500){ count = 0; if(ledmode==0) P2=_crol_(P2,1); else P2=_cror_(P2,1); } }
|
定时器时钟
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| #include <REGX52.H> #include "Delay.h" #include "LCD1602.h" #include "Timer0.h"
unsigned char Sec,Min,Hour;
void main(){ LCD_Init(); Timer0_Init(); LCD_ShowString(1,1,"Clock:"); LCD_ShowString(2,1," : :");
while(1){ LCD_ShowNum(2,1,Hour,2); LCD_ShowNum(2,4,Min,2); LCD_ShowNum(2,7,Sec,2); } }
void Timer0_Rountine() interrupt 1{ static unsigned int count; TH0 = 0xFC; TL0 = 0x18; count++; if(count>=1000){ count = 0; Sec++; if(Sec>=60) { Sec = 0; Min++; if(Min>=60) { Min = 0; Hour++; if(Hour>=24){ Hour = 0; } } } } }
|
其他的事情:
关于中断系统up主讲的比较少 要另外找视频再学一下
不能再摸鱼了 剩下还有10个章节 春节前是看不完了 争取在二月前看完哇!
1.18 串口
串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信。
单片机的串口可以使单片机与单片机、单片机与电脑、单片机与各式各样的模块互相通信,极大的扩展了单片机的应用范围,增强了单片机系统的硬件实力。
51单片机内部自带UART(Universal Asynchronous Receiver Transmitter,通用异步收发器),可实现单片机的串口通信。
单片机-电脑使用usb转换接口
电平标准:人为规定的电压与数据的对应关系
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 |
半双工、异步 |
可挂载多个设备 |
I²C :单片机写入读出依靠的通信口
通信方式:
- 全双工:通信双方可以在同一时刻互相传输数据
- 半双工:通信双方可以互相传输数据,但必须分时复用一根数据线
- 单工:通信只能有一方发送到另一方,不能反向传输(例如:遥控器)
- 异步:通信双方各自约定通信速率(双方要自己计时)
- 同步:通信双方靠一根时钟线来约定通信速率
- 总线:连接各个设备的数据传输线路(类似于一条马路,把路边各住户连接起来,使住户可以相互交流)
单片机的UART
只有一个,四种工作模式
与电脑的通信接口:
RXD、TXD(串口)和P3口(IO口)共用
MCU发送信号到usb自动下载电路 转化为usb信号传给电脑(串口通信)
串口参数及时序图
到中断后:
串口相关寄存器
SMOD和SMOD0是属于串口的
串口向电脑发送数据
配置控制寄存器SCON
应用模式一
配置PCON
up主说中断不用配置?不太明白欸
似乎是说中断默认打开,相当于已经配置好了,不用自己配置
选择波特率
串口只能用定时器1,且是8位自动重装载定时器
用软件的波特率计算器
要选波特率倍速,波特率加倍是因为分频了
程序
要发送信息到单片机,需要写信息到缓存SBUF
但连续发会出错,所以要通过发送控制器的标志位TI
检测
当串行在停止位开始发送时由内部硬件自动置位1,必须由软件来复位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <REGX52.H> #include "Delay.h"
void UART_Init(){ SCON = 0x40; PCON |= 0x80; TMOD &= 0x0F; TMOD |= 0x20; TL1 = 0xF3; TH1 = 0xF3; ET1 = 0;
TR1 = 1;
}
void UART_SendByte(unsigned char Byte){ SBUF = Byte; while(TI==0); TI = 0; }
void main(){ UART_Init(); UART_SendByte(0x66); while(1){ } }
|
打开单片机,发现一直是00,原来是软件的波特率没有改,要改成4800
按照原来的9600的话,发出去一个数据会被采集成两个数据
但是又E6和66交错,可能是因为我的单片机晶振是11.0596,修改定时器
1 2
| TL1 = 0xF4; TH1 = 0xF4;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <REGX52.H> #include "Delay.h"
void UART_Init(){ SCON = 0x40; PCON |= 0x80; TMOD &= 0x0F; TMOD |= 0x20; TL1 = 0xF4; TH1 = 0xF4; ET1 = 0;
TR1 = 1;
}
void UART_SendByte(unsigned char Byte){ SBUF = Byte; while(TI==0); TI = 0; }
void main(){ UART_Init(); UART_SendByte(0x11); while(1){ } }
|
如果把UART_SendByte(0x11);
放到while里可能晶振会有误差,可以用以下delay
只发送不接收
1 2 3 4 5 6 7 8 9
| void main(){ UART_Init(); while(1){ UART_SendByte(Sec); Sec++; Delay(1000);
} }
|
将串口模块化
UART.h
1 2 3 4 5 6 7
| #ifndef __UART_H__ #define __UART_H__
void UART_Init(); void UART_SendByte(unsigned char Byte);
#endif
|
UART.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include <REGX52.H>
void UART_Init(){ SCON = 0x40; PCON |= 0x80; TMOD &= 0x0F; TMOD |= 0x20; TL1 = 0xF4; TH1 = 0xF4; ET1 = 0; TR1 = 1;
}
void UART_SendByte(unsigned char Byte){ SBUF = Byte; while(TI==0); TI = 0; }
|
电脑控制LED流水灯
由于不知道电脑发送信号的时间,所以单片接收需要中断系统,在中断系统中进行数据处理与提取
SCON
中一个REN
(接收位),置1
还要打开串口的中断
防止单片机发送数据的时候进入中断,要先做一下测试
将单片机的信息发送回电脑
UART.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| #include <REGX52.H>
void UART_Init(){ SCON = 0x50; PCON |= 0x80; TMOD &= 0x0F; TMOD |= 0x20; TL1 = 0xF4; TH1 = 0xF4; ET1 = 0; TR1 = 1; EA = 1; ES = 1; }
void UART_SendByte(unsigned char Byte){ SBUF = Byte; while(TI==0); TI = 0; }
|
main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include <REGX52.H> #include "Delay.h" #include "UART.h"
unsigned char Sec;
void main(){ UART_Init(); while(1){ } }
void UART_Routine() interrupt 4{ if(RI==1) { RI = 0; } }
|
计算波特率
发现选择11.0596MHz后
1 2
| TL1 = 0xF4; TH1 = 0xF4;
|
0xF4
是244
,每隔256
溢出一次,计数值位12,每计数12就溢出一次(每隔12us)
所以定时器的溢出率为1/12us=0.08333Mhz
真正到发送控制器(SMOD为1)的频率为0.08333/16=0.005208Mhz=5208hz
8.33/5200约等于0.16%的误差
如果不选倍数
真正到发送控制器(SMOD为1)的频率为0.08333/2/16=0.002604Mhz=2604hz
也可以让UART发送以编码形式呈现(要转化为ASCII码)
例如发送UART_SendByte(0x41)
以编码形式呈现就是A
也可以直接发字符UART_SendByte('A')
但是如果发送A的时候选的是HEX编码 则是不可见的(在ASCII码中0x0a是不可见的)
(因为这里是8位重整模式,一次给寄存器赋值最高只能存储8位二进制数据,而8位二进制数据正好对应一个字符,所以只能一个一个发送字符)
嗯发现哪止定时器,串口也是很迷糊的,说是郭天祥老师讲的也不错,到时候可以查漏补缺一下┭┮﹏┭┮