51单片机学习笔记
KEIL使用
KEIL安装包及相关资源,视频教程见b站江科大视频
https://pan.baidu.com/s/1Fao9VfrM67TIFeIsusnSIw?pwd=7rx1#list/path=%2F
烧录代码(生成hex文件)
魔术棒->output->勾选create hex file(每次新建工程都要配置)
->编译,在object文件夹找到生成的hex文件(每次新建工程后要换成当前工程的文件夹,不然默认烧的是上个工程的hex文件)
->选择单片机型号为STC89C52RC 若选成STC89C52无法下载(用的普中的这个单片机丝印上写的有RC)
->单片机关机,点击下载程序,显示正在检测目标单片机后开机即可下载
当勾选左下角“当目标文件变化……”,我们在keil中编译后STC-ISP软件会自动发送下载指令,我们只需要对单片机断电,再次开机即可。
字体设置
我们在使用Keil的时候编译器默认是使用ANSI进行编码的,在ANSI中对于英文是使用一个字节来表示,但是对于中文,在GB2312的编码中是利用两个字节来表示的,所以如果我们按一次backspace键在ANSI下只会删掉一个字节,所以出现乱码,因为中文还有一个字节没有删掉。
按此设置后输入中文不会出现乱码
创建多文件
https://www.bilibili.com/video/BV1RB4y1i71i?spm_id_from=333.788.videopod.episodes&vd_source=a9d487fcf1a579639c6348eb5a9321db&p=157
keil主题配置
非常nice的一个主题配色
https://blog.csdn.net/wsstony579/article/details/53128206
模块化编程
1.新建文件夹
2.在文件夹内新建.c .h文件
3.添加.c文件
4.将.h文件路径包含进去,否则编译器找不到
魔术棒->c51->Include Paths 添加.h存放位置
5.在.c文件中包含对应.h,在.h里写如下代码:
1 2 3 4 5 6
| #ifndef __DELAY_H__ #define __DELAY_H__
void Delay_ms(unsigned int ms) ;
#endif
|
模板
可以把一些固定的代码当作模板,后面需要用的时候直接双击即可,不用重复自己敲代码,提升效率。
C51语言基础
数据类型
sfr(特殊功能寄存器)
8051单片机的特殊功能寄存器分布在内部数据存储区的地址单元80H~FFH中。sfr数据类型占用一个内存单元(一个字节)。利用它可以访问单片机内部的所有特殊功能寄存器。eg”sfr P1=0x90”定义了P1口在内部的寄存器中,在程序后续的语句中可以用”P1=0xff”语句,使P1口的所有引脚输出为高电平,来操作特殊功能寄存器
为什么P2 = 0xfe;
能直接赋值
- 在C语言中,
P2
被定义为SFR,编译器会将其视为一个8位寄存器。
- 当你对
P2
赋值时,编译器会生成对应的机器指令,将值写入0xA0
地址的寄存器。
- 硬件会根据写入的值,直接控制P2端口的状态。
赋值过程
当你执行P2 = 0xfe;
时,实际发生了以下过程:
- 编译器处理:
- 编译器知道
P2
对应地址0xA0
,因此将P2 = 0xfe;
翻译为:**将值0xfe
写入地址0xA0
**。
- 硬件执行:
- 单片机的硬件会将值
0xfe
(二进制1111 1110
)写入P2端口的输出寄存器。
- P2端口的每个引脚(P2.0到P2.7)会根据这个值设置电平状态:
- 因此,P2.0输出低电平,P2.1到P2.7输出高电平。
sbit(特殊功能位)
在8051系列单片机(如STC89C52)中:
- SFR的地址是8位的,范围是
0x80
到0xFF
。
- 每个SFR占用一个字节(8位),例如
P2
的地址是0xA0
。
- 位地址是对SFR的每一位单独寻址的地址。8051单片机支持位寻址,因此每个SFR的每一位都有一个独立的位地址
- SFR地址(如
0xA0
)是对整个8位寄存器的操作地址。
- 位地址(如
0xA0
到0xA7
)是对SFR中某一位的操作地址。
eg:
字节操作(字节地址0xA0
)
位操作(位地址0xA0
)
1 2
| sbit P2_0 = P2^0; P2_0 = 0;
|
- 操作的是位地址
0xA0
,只影响P2.0
这一位。
- **字节地址
0xA0
**:用于操作整个P2
端口的8位。
- **位地址
0xA0
**:用于操作P2
端口的第0位(P2.0)。
- 虽然它们的地址值相同,但它们的用途和操作对象完全不同。
位寻址
可位寻址的寄存器可以对它的每一位单独赋值,不可位寻址的寄存器只能整体赋值
数据运算
123÷10=12
123%10=3 %取余可以用来判断一个数是否可被另一个数整除。
0011 1100<<1 -> 0111 1000
0011 1100>>2 -> 0000 1111
0001 1000&0010 1010 -> 0000 1000
0001 1000|0010 1010 -> 0011 1010
0001 1000^0010 1010 -> 0011 0010 相同取0 不同取1
~0001 1000 -> 1110 0111
1 2 3 4 5 6 7 8 9 10 11 12 13
| void TIM0_Init(void) { TMOD&=0xF0; TMOD|=0x01; TH0=0xFC; TL0=0x18; ET0=1; EA=1; TR0=1; }
|
基本语句
数组
子函数
预编译
1 2
| #include <REG52.H> #include "delay.h"
|
代码
LED
8个led对应P2寄存器的八个I/O口,置低电平则可使LED导通发光。
2-1 点亮第一个LED
在REGX52.H中已经定义了P2寄存器,可以直接对其赋值
1 2 3 4 5 6 7 8 9
| #include <REGX52.H>
void main() { P2=0xfe; while(1) { } }
|
P2各位在头文件已经被定义,故也可单独对P2的某一位进行赋值操作:
1 2 3 4 5 6 7 8 9
| #include <REGX52.H>
void main() { P2_0=0; while(1) { } }
|
2-2 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
| #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(); } }
|
延时函数由STC-ISP软件生成
2-3 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 41 42
| #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(); } }
|
延迟函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void Delay_ms(unsigned int ms) { unsigned char i, j; while(ms) { i = 2; j = 239; do { while (--j); } while (--i); ms--; } }
|
独立按键
单片机上电后默认为高电平,按下按键后,I/O口接地,变为低电平。检测I/O口高低电平状态即可知道按键是否被按下。
3-1独立按键控制LED亮灭
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <REGX52.H>
void main() { while(1) { if(P3_1==0) { P2_0=0; } else { P2_0=1; } } }
|
3-2独立按键控制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
| #include <REGX52.H> #include <INTRINS.H>
void Delay_ms(unsigned int ms) { unsigned char i, j; while(ms) { i = 2; j = 239; do { while (--j); } while (--i); ms--; } }
void main() { while(1) { if(P3_1==0) { Delay_ms(20); while(P3_1==0); Delay_ms(20); P2_0=~P2_0; } } }
|
3-2独立按键控制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
| #include <REGX52.H> #include <INTRINS.H>
void Delay_ms(unsigned int ms) { unsigned char i, j; while(ms) { i = 2; j = 239; do { while (--j); } while (--i); ms--; } }
void main() { unsigned char LEDNum=0; while(1) { if(P3_1==0) { Delay_ms(20); while(P3_1==0); Delay_ms(20); LEDNum++; P2=~LEDNum; } } }
|
3-3独立按键控制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 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| #include <REGX52.H> #include <INTRINS.H>
unsigned char LEDNum=0;
void Delay_ms(unsigned int ms) { unsigned char i, j; while(ms) { i = 2; j = 239; do { while (--j); } while (--i); ms--; } }
void main() { P2=~0x01; while(1) { if(P3_0==0) { Delay_ms(20); while(P3_0==0); Delay_ms(20); LEDNum++; if(LEDNum>=8) LEDNum=0; P2=~(0x01<<LEDNum); } if(P3_1==0) { Delay_ms(20); while(P3_1==0); Delay_ms(20); if(LEDNum==0) LEDNum=7; else LEDNum--; P2=~(0x01<<LEDNum); } } }
|
数码管
数码管是由多个发光二极管封装在一起组成的“8”字型的器件。
对于四位一体数码管,eg:共阴,让第三个数码管亮其余灭,则位选1101,第三个数码管显示数字1,让7,4端口高电平,即给整个数码管01100000,若位选时为0000,则四个数码管都显示1,共阴极这种设计是四个数码管的A,B,C….分别在同一条线上,可以节省单片机I/O资源
该单片机数码管为公阴极。74HC245是一个信号缓冲器,由于单片机引脚的驱动能力较弱,通过该缓冲器后,输出的电流更大(利用它自己接的VCC输出)
LED1-8接到138译码器的输出端
138译码器输入端为A,B,C(P22,P23,P24),输出端为Y0-Y7(LED1-8),由三个输入端控制8个输出端。 G1,G2A,G2B为使能端(此电路设计时已经接好,单片机上电就可以用该译码器)。
给CBA(C为高位)写二进制,转换成的十进制即要让输出端哪一位亮。eg:给CBA 101,101转换成十进制即5,即让Y5为0
总结:驱动方式:用138译码器选中哪个数码管亮,再用245缓冲器给段码数据使数码管显示对应数字
4-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
| #include <REGX52.H>
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0X6D,0X7D,0X07,0x7F,0X6F}; void NixieTube(unsigned char Location,Number) { switch(Location) { case 1:P2_4=0;P2_3=0;P2_2=0;break; case 2:P2_4=0;P2_3=0;P2_2=1;break; case 3:P2_4=0;P2_3=1;P2_2=0;break; case 4:P2_4=0;P2_3=1;P2_2=1;break; case 5:P2_4=1;P2_3=0;P2_2=0;break; case 6:P2_4=1;P2_3=0;P2_2=1;break; case 7:P2_4=1;P2_3=1;P2_2=0;break; case 8:P2_4=1;P2_3=1;P2_2=1;break; } P0=NixieTable[Number]; }
void main() { NixieTube(7,3); while(1) { } }
|
4-2动态数码管显示
利用人眼视觉暂留和数码管显示的余晖(先让第一个数码管显示1,第二关显示2,第三个显示3,不断地扫描,由于视觉暂留可以看到123)
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
| #include <REGX52.H> #include <INTRINS.H>
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0X6D,0X7D,0X07,0x7F,0X6F};
void Delay_ms(unsigned int ms) { unsigned char i, j; while(ms) { i = 2; j = 239; do { while (--j); } while (--i); ms--; } }
void NixieTube(unsigned char Location,Number) { switch(Location) { case 1:P2_4=0;P2_3=0;P2_2=0;break; case 2:P2_4=0;P2_3=0;P2_2=1;break; case 3:P2_4=0;P2_3=1;P2_2=0;break; case 4:P2_4=0;P2_3=1;P2_2=1;break; case 5:P2_4=1;P2_3=0;P2_2=0;break; case 6:P2_4=1;P2_3=0;P2_2=1;break; case 7:P2_4=1;P2_3=1;P2_2=0;break; case 8:P2_4=1;P2_3=1;P2_2=1;break; } P0=NixieTable[Number]; }
void main() { while(1) { NixieTube(1,5); Delay_ms(1); NixieTube(2,5); Delay_ms(1); NixieTube(3,3); Delay_ms(1); } }
|
LCD1602显示屏
普中、51单片机LCD引脚与数码管和D3,D4,D5冲突,与其它引脚不冲突
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void LCD_Init(); void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char); void LCD_ShowString(unsigned char Line,unsigned char Column,char *String); void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length); void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length); void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length); void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
LCD_Init(); LCD_ShowChar(1,1,'A'); LCD_ShowString(1,3,"Hello word"); LCD_ShowNum(1,9,123,3); LCD_ShowSignedNum(1,13,-66,2); LCD_ShowHexNum(2,1,0xA8,2); LCD_ShowBinNum(2,4,0xAA,8);
|
5-1 LCD
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <REGX52.H> #include "delay.h" #include "LCD1602.h"
int result=0; void main() { LCD_Init(); LCD_ShowBinNum(2,4,0xAA,8); while(1) { result++; Delay_ms(1000); LCD_ShowNum(1,1,result,3); } }
|
矩阵键盘
按键以矩阵的形式连接在I/O上。
检测方法
1.行列式扫描法(将矩阵按键拆分为独立按键) 2.线翻转法
行列式扫描法一行一行扫描,检测的次数不定,线翻转法只用检测两次,一次定行一次定列。
行列式扫描法(编程最简单最无脑,但相比线翻转法效率低)
原理:P17,P16,P15,P14为矩阵的4行,P13,P12,P11,P10为矩阵的4列,给行赋1011,则是单独看第二行,此时它们一端接地,只用检测P13,P12,P11,P10的电平状态即可知道该行有没有被按下的……以此类推,可以逐行/列扫描,由于此开发板引脚冲突,蜂鸣器会一直响,所以在此用逐列扫描。
普通按键是直接给它一端接地,矩阵键盘两端连的是两个I/O,行列式扫描法是通过令一个I/O为低电平达到和普通键盘一样的效果。
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
| unsigned char MatrixKey() { unsigned char KeyNumber=0; P1=0xFF; P1_3=0; if(P1_7==0){Delay_ms(20);while(P1_7==0);Delay_ms(20);KeyNumber=1;} if(P1_6==0){Delay_ms(20);while(P1_6==0);Delay_ms(20);KeyNumber=5;} if(P1_5==0){Delay_ms(20);while(P1_5==0);Delay_ms(20);KeyNumber=9;} if(P1_4==0){Delay_ms(20);while(P1_4==0);Delay_ms(20);KeyNumber=13;} P1=0xFF; P1_2=0; if(P1_7==0){Delay_ms(20);while(P1_7==0);Delay_ms(20);KeyNumber=2;} if(P1_6==0){Delay_ms(20);while(P1_6==0);Delay_ms(20);KeyNumber=6;} if(P1_5==0){Delay_ms(20);while(P1_5==0);Delay_ms(20);KeyNumber=10;} if(P1_4==0){Delay_ms(20);while(P1_4==0);Delay_ms(20);KeyNumber=14;} P1=0xFF; P1_1=0; if(P1_7==0){Delay_ms(20);while(P1_7==0);Delay_ms(20);KeyNumber=3;} if(P1_6==0){Delay_ms(20);while(P1_6==0);Delay_ms(20);KeyNumber=7;} if(P1_5==0){Delay_ms(20);while(P1_5==0);Delay_ms(20);KeyNumber=11;} if(P1_4==0){Delay_ms(20);while(P1_4==0);Delay_ms(20);KeyNumber=15;} P1=0xFF; P1_0=0; if(P1_7==0){Delay_ms(20);while(P1_7==0);Delay_ms(20);KeyNumber=4;} if(P1_6==0){Delay_ms(20);while(P1_6==0);Delay_ms(20);KeyNumber=8;} if(P1_5==0){Delay_ms(20);while(P1_5==0);Delay_ms(20);KeyNumber=12;} if(P1_4==0){Delay_ms(20);while(P1_4==0);Delay_ms(20);KeyNumber=16;} return KeyNumber; }
|
线翻转法
先让四行为0,检测哪一列被按下(该列上任何一个按键被按下都会导致该列代表的I/O为低电平)
再让四列为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
| unsigned char MatrixKey_flip_scan(void) { unsigned char KeyNumber=0; P1=0x0f; if(P1!=0x0f) { Delay_ms(20); if(P1!=0x0f) { P1=0x0f; switch(P1) { case(0X07): KeyNumber=1;break; case(0X0b): KeyNumber=2;break; case(0X0d): KeyNumber=3;break; case(0X0e): KeyNumber=4;break; } P1=0Xf0; switch(P1) { case(0X70): KeyNumber=KeyNumber;break; case(0Xb0): KeyNumber=KeyNumber+4;break; case(0Xd0): KeyNumber=KeyNumber+8;break; case(0Xe0): KeyNumber=KeyNumber+12;break; } while(P1!=0xf0); } } else KeyNumber=0; return KeyNumber; }
|
6-1 矩阵键盘读取并显示在LCD
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" #include "LCD1602.h" #include "MatrixKey.h"
unsigned char KeyNum=0; void main() { LCD_Init(); LCD_ShowString(1,1,"zzxnb666"); while(1) {
KeyNum=MatrixKey_flip_scan(); if(KeyNum) { LCD_ShowNum(2,1,KeyNum,2); } } }
|
6-2 矩阵键盘密码锁
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
| #include <REGX52.H> #include "delay.h" #include "LCD1602.h" #include "MatrixKey.h"
unsigned char KeyNum,Count=0; unsigned int PassWord=0; void main() { LCD_Init(); LCD_ShowString(1,1,"Password:"); while(1) {
KeyNum=MatrixKey_flip_scan(); if(KeyNum) { if(KeyNum<=10) { if(Count<4) { PassWord*=10; PassWord+=KeyNum%10; } Count++; LCD_ShowNum(2,1,PassWord,4); } if(KeyNum==11) { if(PassWord==2345) { LCD_ShowString(1,14,"OK "); PassWord=0; Count=0; LCD_ShowNum(2,1,PassWord,4); } else { LCD_ShowString(1,14,"ERR"); PassWord=0; Count=0; LCD_ShowNum(2,1,PassWord,4); } } if(KeyNum==12) { PassWord=0; Count=0; LCD_ShowNum(2,1,PassWord,4); } } } }
|
中断系统(重要)
中断概念
中断源:引起中断的源头
中断优先级:中断允许多个中断源(外部中断,串口中断,定时器中断……)存在,当多个中断源同时出现时,谁的中断优先级高就先相应谁,先执行高的再执行低的,然后再返回主程序。若两个中断优先级相同,通过查询次序来决定谁先(有一个固定的顺序)。
中断嵌套:当执行一个中断时,若此时出现了一个比它优先级更高的中断,则要转向执行高优先级的,然后再返回优先级低的那个继续执行,然后再返回主程序。对于51来说比较少,对于STM32,DSP等中断更复杂,则更容易出现中断嵌套。
中断的开启关闭,使用哪一个中断等等 都是有特殊功能寄存器来设置的
中断结构
8个中断请求源:INT0,INT1,INT2,INT3,TIM0,TIM1,TIM2,UART 加粗部分对于所有51内核的单片机都有
所有中断都有4个中断优先级
INT0的IT0决定的是下降沿触发还是低电平触发,IE0是中断标志位(当中断源到来时由单片机自动置1),EA为全局总中断,IP是用来设置中断优先级
TCON(中断请求标志),IE(中断允许控制),IP都是寄存器
中断寄存器
TCON(中断请求标志)
IE(中断允许控制)
中断响应条件
中断优先级
中断号
外部中断
51内核的单片机都有INT0,INT1;STC89C5X提供了4个外部中断,INT0,INT1,INT2,INT3
INT0的IT0决定的是下降沿触发还是低电平触发,IE0是中断标志位(当中断源到来时由单片机自动置1),EA为全局总中断,IP是用来设置中断优先级
1 2 3 4 5 6 7
| EA=1; EX0=1; IT0=0/1; void int0() interrupt 0 { }
|
对于STC89C52单片机,INT0,INT1对应P3.2,P3.3 这里我们使用按键模拟外部中断触发
外部中断实验
通过独立按键K3,K4控制LED1,LED2亮灭。
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
| #include <REGX52.H> #include "delay.h"
#define KEY3 P3_2 #define KEY4 P3_3 #define LED1 P2_0 #define LED2 P2_1
void exti0_init() { EA=1; EX0=1; IT0=1; }
void exti1_init() { EA=1; EX1=1; IT1=1; }
void main() { exti0_init(); exti1_init();
while(1) { } }
void exti0() interrupt 0 { Delay_ms(20); if(KEY3==0) { LED1=!LED1; } }
void exti1() interrupt 2 { Delay_ms(20); if(KEY4==0) { LED2=!LED2; } }
|
对其模块化封装:
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
|
#include <REGX52.H> #include "exti.h"
void exti0_init() { EA=1; EX0=1; IT0=1; }
void exti1_init() { EA=1; EX1=1; IT1=1; }
void exti0() interrupt 0 { }
void exti1() interrupt 2 { }
|
1 2 3 4 5 6 7
| #ifndef __EXTI_H__ #define __EXTI_H__
void exti0_init(); void exti1_init();
#endif
|
定时器(重要)
定时器作用:
1.可用于计时系统,实现软件计时,或者使程序每隔一固定时间完成一项操作
2.替代长时间delay,提高CPU的运行效率和处理速度
51内核的定时器都有T0,T1,对于STCC9852单片机,还有T3
51单片机有两组定时计数器,既可以定时又可以计数;
定时器计数器与单片机CPU相互独立,工作过程自动完成,不需要CPU参与;
定时计数器是根据机器内部的时钟(使用定时功能)或外部的脉冲信号(使用计数功能)来对寄存器进行加1;
CPU时序周期相关知识
时钟周期(振荡周期):单片机控制信号的基本时间单位。若时钟晶体震荡频率为fosc,则时钟周期Tosc=1/fosc.
机器周期:CPU完成一个基本操作所需要的时间为机器周期。单片机通常把执行一条指令的过程分为几个机器周期,AT89S51单片机每12个时钟周期为一个机器周期。Tcy=12Tosc=12/fosc。eg:fosc=12MHZ,Tcy=12/12=1us.
指令周期:执行一条指令所需要的时间。
寄存器
详细的每一位介绍可以看参考手册。
不可位寻址 只能对寄存器整体赋值
一般用方式1,方式2(串口波特率生成)。TMOD高四位控制T1,低四位控制T0
GATE:门控位 1:(只需TR0/TR1为1来决定定时计数器工作)0:(除了TR0/TR1还需INT0/INT1为1来决定定时计数器工作)
C/T: 1(计数模式)0(定时模式)
M1,M0:工作方式
可位寻址 可对寄存器中的每一位单独赋值
TF1:T1溢出标志位,溢出时自动置1,向CPU发出中断请求
TR1: T1定时计数器运行控制位 1:开始工作 0:停止工作
工作方式(原理)
方式0:
C/T 若为1 计数器模式 将开关打到1 ,若为0 定时器模式 将开关打到0
方式1(常用):
不会自动装载初值 每次溢出进入中断后需要我们手动装载
每来一个脉冲,16位(最大为65535)的计数器(TH,TL)里面的值就会自动加1,当计数达到最大值65535后,再+1就会溢出,TF0置1,向中断系统申请中断
方式2:
自动重装载(初值)
方式3:
定时器配置(重要)
其实就是根据工作方式的图把相应的寄存器配一下
外部晶振12MHZ,则机器周期=1us,若想让定时器定时1ms
1ms/1us=1000次 初值=65536-1000=64536 将其转换为16进制为0xFC18,高八位写入TH,低八位写入TL
当要计时的时间比较大,次数超过65536的话,如500ms,我们可以设置定时器1ms,然后在定时器中断里设置一个变量cnt,每次进入中断时加一,当cnt=500时即为500ms.
也可以这样算:
2^16=65536 2^8=256
高八位=65535/(2^8),低八位=65535%(2^8)) 类比十进制:1880 取高2位和低2位,高二位=1880/(10^2)=18,低二位=1880%(10^2)=80)
1 2
| TH=65536/256; TL=65536%256;
|
此外,在掌握了计算方法后,也可以使用定时器计算工具提高效率:
STC-ISP
1 2 3 4 5 6 7 8 9 10 11 12 13
| void TIM0_Init(void) { TMOD&=0xF0; TMOD|=0x01; TH0=0xFC; TL0=0x18; ET0=1; EA=1; TR0=1; }
|
可位寻址的寄存器可以对它的每一位单独赋值,不可位寻址的寄存器只能整体赋值
定时器实验
1.通过定时器0中断控制D1指示灯间隔1s闪烁,定时器1中断控制D2指示灯间隔0.5s闪烁。
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 70 71 72 73 74 75 76
| #include <REGX52.H> #include "delay.h"
#define LED1 P2_0 #define LED2 P2_1 typedef unsigned char u8; typedef unsigned int u16;
void TIM0_Init(void) { TMOD&=0xF0; TMOD|=0x01; TH0=0xFC; TL0=0x18; ET0=1; EA=1; TR0=1; }
void TIM1_Init(void) { TMOD&=0x0F; TMOD|=0x10; TH1=0xFC; TL1=0x18; ET1=1; EA=1; TR1=1; }
void main() { TIM0_Init(); TIM1_Init(); while(1) { } }
void TIM0() interrupt 1 { static u16 cnt=0; TH0=0xFC; TL0=0x18; cnt++; if(cnt==1000) { LED1=!LED1; cnt=0; } }
void TIM1() interrupt 3 { static u16 cnt=0; TH1=0xFC; TL1=0x18; cnt++; if(cnt==500) { LED2=!LED2; cnt=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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
|
#include <REGX52.H> #include "tim.h"
#define LED1 P2_0 #define LED2 P2_1
void TIM0_Init(void) { TMOD&=0xF0; TMOD|=0x01; TH0=0xFC; TL0=0x18; ET0=1; EA=1; TR0=1; }
void TIM1_Init(void) { TMOD&=0x0F; TMOD|=0x10; TH1=0xFC; TL1=0x18; ET1=1; EA=1; TR1=1; }
void TIM0() interrupt 1 { static unsigned int cnt=0; TH0=0xFC; TL0=0x18; cnt++; if(cnt==1000) { LED1=!LED1; cnt=0; } }
void TIM1() interrupt 3 { static unsigned int cnt=0; TH1=0xFC; TL1=0x18; cnt++; if(cnt==500) { LED2=!LED2; cnt=0; } }
|
1 2 3 4 5 6 7 8
| #ifndef __TIM_H__ #define __TIM_H__
void TIM0_Init(void); void TIM1_Init(void);
#endif
|
2.定时器时钟
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
| #include <REGX52.H> #include "delay.h" #include "tim.h" #include "key.h" #include "LCD1602.h"
extern unsigned char Sec; extern unsigned char Min; extern unsigned char Hour;
void main() { TIM0_Init(); LCD_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); } }
|
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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
| #include <REGX52.H> #include "tim.h" #include "INTRINS.H"
#define LED1 P2_0 #define LED2 P2_1 unsigned char Sec=55; unsigned char Min=59; unsigned char Hour=23;
void TIM0_Init(void) { TMOD&=0xF0; TMOD|=0x01; TH0=0xFC; TL0=0x18; ET0=1; EA=1; TR0=1; }
void TIM1_Init(void) { TMOD&=0x0F; TMOD|=0x10; TH1=0xFC; TL1=0x18; ET1=1; EA=1; TR1=1; }
void TIM0() interrupt 1 { static unsigned int cnt=0; TH0=0xFC; TL0=0x18; cnt++; if(cnt==1000) { cnt=0; Sec++; if(Sec>=60) { Sec=0; Min++; if(Min>=60) { Min=0; Hour++; if(Hour>=24) { Hour=0; } } } } }
void TIM1() interrupt 3 { static unsigned int cnt=0; TH1=0xFC; TL1=0x18; cnt++; if(cnt==500) { LED2=!LED2; cnt=0; } }
|
PWM
直流电机
直流有刷电机主要由永磁体(定子),线圈(转子),换向器组成;直流无刷电机主要由永磁体(转子),绕组线圈(定子),少了碳刷和换向器的摩擦。
PWM介绍
电机调速不能和LED呼吸灯一样接一个滑动变阻器就完事,因为在驱动电机的过程中会产生很大电流,对于电机来说会转化为机械能没事,但对于滑动变阻器,电流会转化为热能使其损坏。
最新的单片机TIM定时器都有输出PWM的功能,但STC89C52没有,我们用定时器中断来实现,同时也会方便后面的理解。
该结构与最新单片机TIM定时器PWM硬件结构相似,在这里我们用软件来模拟这一结构
实验1: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
| #include <REGX52.H> #include <INTRINS.H> #include "delay.h"
#define LED P2_0
void main() { unsigned char Time,i; while(1) { for(Time=0;Time<100;Time++) { for(i=0;i<20;i++) { LED=0; Delay_us(Time); LED=1; Delay_us(100-Time); } } for(Time=100;Time>0;Time--) { for(i=0;i<20;i++) { LED=0; Delay_us(Time); LED=1; Delay_us(100-Time); } } } }
|
实验2:直流电机调速
pwm周期:100us*100=10ms 每100us进一次定时器中断
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 "tim.h" #include "key.h" #include "nixietube.h"
unsigned char KeyNum=0,Speed=0; extern unsigned char Compare;
void main() { P2=0xfe; TIM0_Init(); while(1) { KeyNum=Key(); if(KeyNum==1) { Speed++; if(Speed>=4)Speed=0; switch (Speed) { case 0: Compare=0; break; case 1: Compare=60; break; case 2: Compare=80; break; case 3: Compare=100; break; default: break; } } NixieTube(1,Speed); } }
|
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
| #include <REGX52.H> #include "tim.h" #include "INTRINS.H"
#define LED1 P2_0 #define LED2 P2_1 #define MOTOR P1_0 unsigned char LedMode=0; unsigned char Counter,Compare=0;
void TIM0_Init(void) { TMOD&=0xF0; TMOD|=0x01; TH0=0xFF; TL0=0x9C; ET0=1; EA=1; TR0=1; }
void TIM0() interrupt 1 { TH0=0xFF; TL0=0x9C; Counter++; if(Counter>=100)Counter=0; if(Counter<Compare) { LED1=0; MOTOR=1; } else { LED1=1; MOTOR=0; } }
|
串口通信