江科大51单片机入门学习笔记合集

博主文档下载

最近有很多童鞋来问需要文档的事情。博主也很开心自己的记录能被大家认可。所以最近就提取出来了 pdf 的永久链接,链接如下:
【超级会员V3】通过百度网盘分享的文件:51单片机入门_…
链接:https://pan.baidu.com/s/14bsuiwUYGbWqq3ZWOuuu1A?pwd=rsy8
提取码:rsy8
复制这段内容打开「百度网盘APP 即可获取」
另外,博主现在也是在学习嵌入式的阶段,有感兴趣想一起学习的同学可以加博主的公众号一起交流沟通~(公众号名:灰海宽松,见文章底部)
本文档仅为博主个人学习江协科技51单片机课程过程中记录的学习笔记,未做任何商业用途,侵删!

软件下载

开发:Keil。如果想要没乱码的中文注释,那么设置编码方式为 UTF8 或 GB2312。

程序文件下载到单片机:STC/普中(STC需要冷启动,先点击下载再开启单片机电源)

介绍

Micro Controller Unit, MCU 单片机,其中包含了CPU RAM ROM 输入输出设备 等一系列电脑硬件常用功能。

功能:通过传感器采集数据,通过CPU处理数据,控制硬件。

可以说是一个性能低的小电脑,是了解计算机原理的很好的学习方法。

右上角的跳线帽使用数码管时跳到VCC,使用点阵时跳到GND。

STC89C52RC 命名规则

STC:芯片为 STC 公司生产的产品。

8:该芯片为 8051 内核芯片。

9:表示内部含有 Flash EEPROM 存储器,还有如 80C51 中 0 表内部含有 MaskROM(掩模 ROM)存储器;如 87C51 中 7 表示内部含有 EPROM(紫外线可擦除 ROM)存储器。

C–表示该器件为 CMOS 产品。还有如 89LV52 和 89LE58 中的 LV 和 LE 都表示 该芯片为低电压产品(通常为 3.3V 电压供电);而 89S52 中 S 表示该芯片含有 可串行下载功能的 Flash 存储器,即具有 ISP 可在线编程功能。

5–固定不变。

2:表示该芯片内部程序存储(FLASH)空间大小,1 为 4KB,2 为 8KB,3 为 12KB,即该数乘以 4KB 就是芯片内部的程序存储空间大小。程序空间大小决定了 一个芯片所能装入执行代码的多少。一般来说,程序存储空间越大,芯片价格也 越高,所以我们再选择芯片的时候要根据自己需求选择合适芯片。 RC–STC 单片机内部 RAM(随机读写存储器)为 512B。还有如 RD+表示内部 RAM 为 1280B。还有芯片会省略此部分

image-20220828144814559

芯片介绍

芯片在 PDIP 里。黑色的部分 PDIP 是一种封装方式,可能还有 LQFP 等(一个正方形的形状)封装方式。

8051 内核基本上都是中间绿色块的样子,只是外设、封装等方式不同。

image-20220828145334929

管脚图:

Vcc 是电源,XTAL 管时钟,RST 是复位,等等。

image-20220828145345268

image-20230125214429628

整个是一个总线结构,所有外设都挂在上面。如最下面一行左边是晶振,右边是外部引脚。

只有这一个单片机是不能运行的,看我们的开发板上面还外接了好多好多外设呢。能让单片机运行的最小应用系统如下:

image-20230125214745143

三角是正极,三线符号是负极。

首先需要 Vcc 接正,GND 接负。

然后需要接晶振。没有晶振单片机程序无法一条条往下执行,有了晶振按照固定的周期才能一条条往下执行。晶振就是板子上银色的椭圆形的一个东西,频率写在上面,一般是有12MHz和11.多MHz的两种(有的芯片自带晶振。不过很明显我们的芯片并不自带)。

然后还有复位电路,让程序回到第一条的位置。

开发板介绍

image-20220903113237968

中间黑色的是刚刚介绍的单片机。拉起拉杆,可以取下单片机,但放回时一定不能放反。单片机有缺口的一端左侧从01开始,逆时针逐渐增大到40。

右侧中间有8个 LED 灯,我是点灯大师!

下面是一个矩阵按键,用户可以通过按按键输入。

最下面一行右侧有个红外接收传感器,接收红外线的。

左边无线模块,8个插孔的,做无线模块(如2.4G)用的。

再左边四个独立按键。

最左下是 USB 自动下载模块,插上 USB 线后按开关就会自动下载程序,不用了解。

DS1302 时钟芯片,可以做一个小时钟,读取时间。

红色按钮是 RST 按钮。

AD/DA 模数转换器,使单片机在数字与模拟领域之间转化。

74H595 可以扩展出更多的 IO 口。

步进电机可以精确控制脚步(转一圈、转半圈)。比如空调会用。

超声波模块可以测距。

蜂鸣器模块可以放歌。但我()()()()。

138译码器控制数码管,也可以扩展 IO 口。

24c02 也是一种 ROM,还是 EEP ROM(掉电不丢失)。其实单片机自带的 Flash ROM 更先进,但是只能用来存储程序。

温度传感器可以用来检测温度。

74HC245 可以驱动数码管(我的单片机是 HC138)。

左上角的电位器和排座用于接显示屏。电位器可以调整显示屏的亮度。

最大的黑色方阵是一个 LED 点阵。可以点亮8*8的方阵,甚至用来做动画。

之后的课程中还会详细介绍每一个模块,以及对应的电路图。

逻辑运算

&与,|或,!非,⊙同或(相同结果才=1),异或⊕(不同结果才为1)

C语言语法

int 16位,char 8位。

image-20220904115124141

image-20220904115224027

基本语法其他的都好说,再复习一下位运算。

image-20220904145323095

左右移补0.

位运算符也可以参与成为复合赋值运算符,如^=, <<=

逗号运算符=最后一个表达式的值

image-20220904145638619

函数在C语言基础上做的拓展

重入函数

在函数形参括号后加修饰符 reentrant,代表这个函数是重入函数,可以被递归调用,但这样就不能有bit变量,也不能进行位运算。

中断函数

在函数形参括号后加修饰符 interrupt m,系统编译时把对应函数转化为中断函数,自动加上程序头段和尾段,并按 51 系 统中断的处理方式自动把它安排在程序存储器中的相应位置。

在该修饰符中,m 的取值为 0~31,对应的中断情况如下:

0——外部中断 0

1——定时/计数器 T0

2——外部中断 1

3——定时/计数器 T1

4——串行口中断

5——定时/计数器 T2

其它值预留。

外部函数

如果要调用的函数不在本文件内,在其他文件内,定义函数时函数开头要加 extern 修饰符。

sfr sbit

用于定义特殊功能寄存器或特殊位。

sfr P0=0x80;//把地址 0x80 处的寄存器定义为 P0
sbit P0_1=P0^1;//取第一位定义为 P0_1

其实头文件 regx52.h 中都有。

能不能给位单独赋值要看是不是可位寻址。因为物理地址有限,每8个寄存器只能有一个可位寻址。

51单片机最小系统组成

  • 晶振电路,提供时钟,相当于心脏
  • 复位电路,系统运行不正常时可以重启
  • 电源电路,注意单片机的供电电压要求
  • 下载电路,烧入程序

另外注意,单片机的P0口是漏级开路,输出高电平会导致高阻态,因此输出高电平时要接上拉电阻,通常选择 4.7K~10K 阻值。

程序编写前言

新建项目 new μversion project

选择 CPU 型号:Keil 中没有完全对应的 STC89C52 版本,用Atmel 中的 AT89C52 即可,不用把8051启动文件添加到工程中。

AT 和 STC 是两种型号的单片机。有的 STC 单片机上面还有 AT 接口,AT 使用那个接口烧录程序。STC 就用 USB 下载。

新建好后有一个文件夹:source group,代码文件都在其中。

选中该文件夹,右键新建new item,新建c语言文件。可以选c/cpp/asm

在魔术棒 Output 选项中添加 “ create HEX file".

程序框架

#include "reg52.h"
void main()
{
    while(1)
    {
    }
}

编译:translate按钮

建立:build按钮,也有编译的作用,只编译发生变动的文件。

重新建立:rebuild,编译所有文件(速度慢不建议)。

报错如果显示:缺少root segment根段,即没有找到主函数。

头文件作用

#include<reg52.h>和`#include “reg52.h"都可以。区别在于<>直接去软件安装处搜索头文件,而”"先在该项目下查找头文件,找不到再去软件安装处,再找不到就报错。

查看头文件可以在左侧的结构树对应的c文件目录下打开,或者右键“reg52.h" open 打开。

该头文件中定义了52单片机内部所有功能寄存器,把地址值如0x80赋值给P0等端口。

程序烧录

程序编译建立没有错误,也开启了魔术棒创建 HEX 文件选项,那么 build 后就会在对应路径中找到生成的 HEX 文件。

在 STC-ISP 中选定单片机型号、串口、晶振频率(可以直接看开发板上的晶振上面有写),选择对应的 HEX 文件,先断电开发板,再点击下载,再开机,就可以查看呈现在开发板上的效果。

HELLO WORLD——LED部分

LED 发光二极管。

image-20230125222255962

image-20230125222332854

下面两个黑色的方块就是8个电阻。电阻是限流作用,防止电流过大烧毁 LED。

电阻上面写着小小的“102”,代表10*10^2,即1kΩ。

每个 LED 正极是一定通电流的,如果负极接地,那么这个 LED 被点亮。否则两头都是高电平点不亮(这里的电平是 TTL 电平,高5低0)。

单片机如何驱动高低电平?在 MCU 内,CPU 接到指令(如P2^0口赋1,即高电平)CPU 把数据写入寄存器,寄存器数据通过驱动器放大后变为5V/0V 电平输出。

点亮 LED

GPIO(general purpose input output) 即通用输入输出端口,可以通过软件控制其输入和输出.

image-20220905111036720

  • 电源引脚: Vcc, GND
  • 晶振引脚:XTAL1 2
  • 复位引脚:RST VPD,不做其他功能。
  • 下载引脚:TXD RXD
  • GPIO引脚:Px.x的都是 GPIO 引脚,大致分为P0 P1 P2 P3,每组8个IO,P3还有附加功能,比如串口、外部中 断、计数器等。每个引脚每次只能使用一个功能。
#include "reg52.h"
sbit LED1=P2^0; //将 P2.0 管脚定义为 LED1
//我们也可以直接给P2整个赋值。比如P2=0xFE,即1111 1110,就只会点亮最后一个 LED 灯,和 P2^0=0 效果是一样的。
//另,我们的这种做法只是寻找特殊寄存器P2的第几位。而头文件 REGX52.H 中是真正包含所有引脚信息的,如P2_0 就是2.0引脚,也能起到一样的效果。
void main()
{
    LED1=0; //LED1 端口设置为低电平,就会被点亮
    while(1)//单片机默认不断执行主程序。如果没有这个死循环,单片机就会不断点亮点亮点亮点亮……不如点亮一次之后无限延时。
    {
    }
}

编译结果里面的几个数据的意义:

code:表示程序所占用 FLASH 的大小。

data:数据储存器内部 RAM 占用大小。

xdata:数据储存器外部 RAM 占用大小。

LED 闪烁

只需要点亮——延时——熄灭——延时循环即可。

单片机频率单位是 MHz 兆赫兹,所以只是单纯的亮灭亮灭肉眼看不出亮灭的效果。所以需要延时。

延时可以写一个这样的函数:

typedef unsigned int u16
void delay(u16 ten_us){
    while(ten_us--);
}

u16 代表16位的无符号整型数据。这是一个比较常用的定义,unsigned char 定义为 u8, unsigned int 定义为 u16。当 ten_us 超出 u16 的范围后,跳出 while 循环。

然后就LED1=0;delay(50000);LED1=1;delay(50000);循环即可.

但是,STC-ISP 可以根据晶振频率和要延时的时间生成延时函数,真的牛!不过注意软件上标明的适用系列版本。

image-20230125224132185

其中 _nop_() 函数包括在 INTRINS.H 头文件中,是一个空语句,就只会产生延时的效果。

不过 STC-ISP 只能生成固定时长的延时函数。如果想要像自己写的那个 delay() 函数一样传入参数,延时对应长度的毫秒/微秒呢?

很简单,我们先生成延时1毫秒/微秒的函数,然后把函数中的内容重复执行传入参数遍。

void Delay1ms(unsigned int xms)		//@11.0592MHz
{
	unsigned char i, j;
 	while(xms--){//这里是修改过的
        _nop_();
        _nop_();
        _nop_();
        i = 11;
        j = 190;
        do
        {
            while (--j);
        } while (--i);
     }
}

Keil 软件仿真

使用仿真功能查看 LED 闪烁案例中的实际延时时间。

  1. 点击魔术棒,选择 Target 选项卡,设置 Xtal 为12M或11.0592M,根据开发板晶振修改对应值。
  2. 点击黑色放大镜中有红色d的仿真按钮,进入仿真页面

image-20220905153847511

我们要关注的参数是sec。

  1. 点击RST按钮重新复位系统参数,sec 变为0。然后在要调试的行前双击,就会出现红色块的断点,点击8运行时就会直接运行到断点处。再次点击就会运行到下一处断点处。
  2. 点击红色标记8运行,运行到36行时显示用时:0.00039s,再次点击运行到37行,用时:0.45s
  3. 可见delay花费时间约为0.45s

LED 流水灯

学会了点亮和延时,流水灯的原理就很好懂了。就是给P2的所有端口赋值为:1111 1110,每次只有一个为0即点亮,这个点亮的0从最高位逐渐降到最低位。

取反后即为:

1000 0000

0100 0000

0010 0000

0001 0000

0000 1000

0000 0100

0000 0010

0000 0001

也就是一个移位运算,0x01<<i的循环。

#include "reg52.h"
# define LED P2
void delay(unsigned int i){
	while(i--){}
}
void main()
{
    while(1)
    {
			int i=0;
			for(i;i<8;i++){
				LED=~(0x01<<i);
				delay(50000);
			}
    }
}

移位函数

位运算的移位操作只能补0,但是 Keil C51 软件内有对应的移位库函数,左移_crol_(),右移_cror_(),包含在 intrins.h 库中

移位函数会把移出去的位补到空位,一个循环。

#include "reg52.h"
#include "intrins.h"
# define LED P2
void delay(unsigned int i){
	while(i--){}
}
void main()
{
    LED=~(0x01);
	delay(50000);
    while(1)
    {
			LED=_crol_(LED,1);
			delay(50000);
    }
}

蜂鸣器实验

蜂鸣器简单地说,就是电磁线圈和磁铁对振动膜的作用。

单片机的是无源蜂鸣器,不能一直充电,需要外部控制器发送震荡信号,可以改变频率产生不同的音色、音调。

大多数有源蜂鸣器则没有这个效果,有源蜂鸣器外观与之相同,内部自带震荡源,接上电就能响,但不能改变频率。

image-20230207154444964

我们知道三极管的作用是不用单片机自己直接驱动单片机。

另一种方法是步进电机。

image-20230207155956609

ULN 2003,高电压 高电流驱动器,给信号就被驱动。IN 取反输出 OUT。

简谱

image-20230207163112788

首先整个谱大概分为几个区。大字组、小字组、小字1组、小字2组。每个组之间差8度,每相邻的两个键(如黑白)差半音,相邻的两个同色键差一个全音。

几个白键的表示方法就是下面的简谱,差半音的黑键用左上角的#表示升半音,b表示降半音。

演奏两大要素:音高和时值。

img

谱上一个数字是1/4 音符,二分是其两倍,数字加个横线 - 。全音符就是(2 - - -)。这个线叫增时线。

八分是其1/2,数字下加一条线(2).再/2就再加一条,叫减时线。

试着识一个完整的谱:

image-20230207233115087

4/4:以四分音符为一拍,每小节有四拍。

第二节 1 ˙ \dot{1} 1˙ · 上面的点我们知道代表高音,后面的点代表:前一位音符延长1/2长度,即四分音符+1/2的四分音符。也就是3/8哈哈哈。

看第一节,一般连着两个八分音符就把 underline 连起来。但是这种哪怕是一个音,中间也要先断开再重响。比如右上角的3 3

升音和降音在本小节中有效。如第三行的 7 #4 4 7 ,两个4都是升音。

不过如果顶端画了延音线,就是连起来的不用断开。如中间的 77 ^ \widehat{7 7} 77 ,拆开写是为了好读谱。

接下来就是如何把谱转化为单片机代码。左上角 1=c 说明是c调的。d大调会出现黑键,c调只有白键。

image-20230208000727170

音具体是怎么定义的?首先以中音a为基准,高音a是其2倍,低音a是其1/2。

中间每次升音都是等比数列递增的,即*2的1/12次方

使用蜂鸣器

响起来很简单:不断反转 P1^5 口(是不是这个口得看自己的板子型号)。

void main()
{
	u16 i=2000;//决定时值
	while(1){
		while(i--)
		{
			BEEP=!BEEP;
			delay10Us(100);//决定音高
		}
        i=2000;
        BEEP=0;
	}
}

时值还好确认,音高怎么说?

首先我们有上图的音符与频率对照表。我们把频率转化为周期,即1/频率。这里周期单位是us。

image-20230208093509168

然后周期时长转化为机器周期,即记一个数需要的时间。我们看看需要多少机器周期。

1机器周期=12时钟周期,时钟周期=1/单片机晶振。比如对于我的11.0592MHZ 晶振,机器周期=12/11.0592MHZ (单位:us)。

据此把“需要切换的周期时长”转化为“需要切换的周期需要执行几次指令”。即周期/机器周期。如果是12MHZ 晶振这一步相当于没有。

image-20230208094537336

然后电平从低到高,从高到低才是一个周期。所以实际电平反转一次的周期是周期的一半。

image-20230208095158230

我们知道定时器原理是 TH TL 加至65536触发中断。因此重装载值(定时器初值)=65536-取整值。

音符频率周期需要的机器周期数需要的机器周期数/2取整重装载值
12623816.7943517.5572521758.778626175963777
1#2773610.1083327.0758121663.537906166463872
22943401.3613134.6938781567.346939156763969
2#3113215.4342963.3440511481.672026148264054
33303030.3032792.7272731396.363636139664140
43492865.332640.6876791320.34384132064216
4#3702702.7032490.8108111245.405405124564291
53922551.022351.0204081175.510204117664360
5#4152409.6392220.7228921110.361446111064426
64402272.7272094.5454551047.272727104764489
6#4662145.9231977.682403988.841201798964547
74942024.2911865.587045932.793522393364603
15231912.0461762.141491881.070745788164655
1#5541805.0541663.537906831.768953183264704
25871703.5781570.017036785.008517978564751
2#6221607.7171481.672026740.836012974164795
36591517.4511398.482549699.241274769964837
46981432.6651320.34384660.171919866064876
4#7401351.3511245.405405622.702702762364913
57841275.511175.510204587.75510258864948
5#8311203.3691109.025271554.512635455564981
68801136.3641047.272727523.636363652465012
6#9321072.961988.8412017494.420600949465042
79881012.146932.7935223466.396761146665070
11046956.0229881.0707457440.535372844165095
1#1109901.7133831.018936415.50946841665120
21175851.0638784.3404255392.170212839265144
2#1245803.2129740.2409639370.120481937065166
31318758.7253699.2412747349.620637335065186
41397715.8196659.6993558329.849677933065206
4#1480675.6757622.7027027311.351351431165225
51568637.7551587.755102293.87755129465242
5#1661602.047554.846478277.42323927765259
61760568.1818523.6363636261.818181826265274
6#1865536.193494.155496247.07774824765289
71976506.0729466.3967611233.198380623365303

使用方法:TH=重装载值/256,TL=重装载值%256.

音高从低到高逐位响起代码:

#include "reg52.h"
#include "Delay.h"
#include "Timer0.h"
sbit beep=P1^5;

unsigned int beep_table[]={//可以加个0代表不响的0
	63777,63872,63969,64054,64140,64216,64291,64360,64426,64489,64547,64603,
	64655,64704,64751,64795,64837,64876,64913,64948,64981,65012,65042,65070,
	65095,65120,65144,65166,65186,65206,65225,65242,65259,65274,65289,65303	
};

unsigned char beep_select=0;

void main(){																	 
	unsigned char i;
	timer0Init();
	while(1){
		beep_select++;
		delayMs(50);//时值		
	}
}

void timer0Interrupt() interrupt 1
{
    TH0 = beep_table[beep_select]/256; // 因为触发中断时,TH TL 归零,所以记得赋初值!
    TL0 = beep_table[beep_select]%256;
	beep=!beep;
}

编曲:

image-20230208125043337

根据乐谱写一个数组。

unsigned int little_star[]={12, 12, 19, 19,
                            21, 21, 19,    //增时线
                            17, 17, 16, 16,
                            14, 14, 12,
                            19, 19, 17, 17,
                            16, 16, 14,
                            19, 19, 17, 17,
                            16, 16, 14,
                            12, 12, 19, 19,
                            21, 21, 19, 
                            17, 17, 16, 16,
                            14, 14, 12
                           };

遍历数组,得到的音高再去 beep_table 中获取重装载值。

TH0 = beep_table[little_star[beep_select]]/256; // 因为触发中断时,TH TL 归零,所以记得赋初值!
TL0 = beep_table[little_star[beep_select]]%256;

但是播放起来都是连着的,听起来效果并不好。可以每次播完一个音先关闭中断并延时一段时间,再继续播放。

while(1){
		beep_select++;
		delayMs(50);
		TR0=0;
		delayUs(1);
		TR0=1;		
	}

増时线如何处理?中间是不断开一直想的,因此需要几个特定的音符delay时间更长一些。怎么区分哪些音符加长哪些不加呢?

最好还是存储乐谱时搞一个二维数组(逻辑上物理上都可以),既能存储音高,也能存储时值。

unsigned int little_star[]={12, 4,
                            12, 4,
                            19, 4,
                            19, 4,
                            21, 4,
                            21, 4,
                            19, 8,  //增时线
                            17, 4,
                            17, 4,
                            16, 4,
                            16, 4,
                            14, 4,
                            14, 4,
                            12, 8,
                            19, 4,
                            19, 4,
                            17, 4,
                            17, 4,
                            16, 4,
                            16, 4,
                            14, 8,
                            19, 4,
                            19, 4,
                            17, 4,
                            17, 4,
                            16, 4,
                            16, 4,
                            14, 8,
                            12, 4,
                            12, 4,
                            19, 4,
                            19, 4,
                            21, 4,
                            21, 4,
                            19, 8,
                            17, 4,
                            17, 4,
                            16, 4,
                            16, 4,
                            14, 4,
                            14, 4,
                            12, 8,
                            0xFF,4//终止标志防越界
                           };

如果数组大小超限,在魔术棒-Target-Memory Model 中选择第三个。不过这只是治标不治本,因为 RAM 只有512字节所以存不下太长。可以在定义数组时加上关键词 code 来存在 ROM 8K 的闪存中。不过这样的数组是只读的。

当然这样找索引比较麻烦。最好是索引全部重新宏定义。

//音符与索引对应表,P:休止符,L:低音,M:中音,H:高音,下划线:升半音符号#
#define P	0
#define L1	1
#define L1_	2
#define L2	3
#define L2_	4
#define L3	5
#define L4	6
#define L4_	7
#define L5	8
#define L5_	9
#define L6	10
#define L6_	11
#define L7	12
#define M1	13
#define M1_	14
#define M2	15
#define M2_	16
#define M3	17
#define M4	18
#define M4_	19
#define M5	20
#define M5_	21
#define M6	22
#define M6_	23
#define M7	24
#define H1	25
#define H1_	26
#define H2	27
#define H2_	28
#define H3	29
#define H4	30
#define H4_	31
#define H5	32
#define H5_	33
#define H6	34
#define H6_	35
#define H7	36

数码管

动态数码管原理是什么?首先我们知道每个数码管都有 abcdefg 七个段。

8个数码管那我们按理来说是需要8*7个引脚,很浪费。

于是设计了动态数码管。首先所有数码管是共阴极的。然后我们选中哪一个数码管阴极赋0,就会启动哪一个数码管,传入的 abcdefg 就会点亮该数码管的对应段。

然后8个数码管像流水灯一样,以极高频率依次点亮,肉眼看到的就是8个数码管都被点亮且呈现出不同的图案。

image-20230126130808285

开发板上的数码管是共阴极数码管,所有位共接一个阴极,给对应ABCDEF输入高电平点亮。

直接引脚:

直接引脚

image-20230126125258985

74译码器使用3位 bit 输入表示8种状态,调整 LED1~8 哪一个输出低电平,代表要启动8个数码管的哪一个的公共端。

输入的三位从最低位到最高位分别是P2^2, P2^3, P2^4,代表数码管从左到右的第几位是输入取反。

比如P2^4=1, P2^3=1, P2^2=0, 输入就是110,取反后就是001,就是从左到右第1位数码管(从第0位开始)。

VCC 和 GND 是使能,接到译码器上一上电就工作。

image-20230126005938488

P00~P07 代表控制当前数码管的 a~g 显示形式,接到 74HC245 缓冲器上而不是直接接到数码管上,使得单片机不用直接驱动数码管,Ai 连到 Bi 上。

OE 是使能,接地工作不接地不工作的原理。

DIR 是规定方向,高电平从左边读取数据传输到右边。低电平从右边读数据到左边。开发板上有个 J21 跳线帽,可以调整是 GND 与 LE 相连还是 VCC 与 LE 相连,也就是高电平输入 DIR 还是低电平输入 DIR。

数码管上面的 COM 是公共端,选中哪一个公共端(使得其=0,因为是共阴极)就是调整哪一个数码管的点亮方式。

点亮一位数码管代码:

#include "reg52.h"
#define Display P0 
unsigned char display_code[17]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};
/*具体显示什么图案是怎么推算出来的呢?首先 abcdefg 和 dp 小数点段一共有8个要控制的段。
比如我们要呈现数字6,就是 acdefg 亮,b和小数点不亮。
因为数码管是共阴极,所以我们想让哪个段亮哪个段就输入高电平,和 LED 相反。
所以P00~P07 的输入应该是 1011 1110
然后我们直接给 P0 赋值的话,是 P0_7 在最高位,P0_0 在最低位,所以输入应该正好反过来,0111 1101,即0x7d。*/
void main()
{
    Display=display_code[0];
    while(1)
    {}
}
#define SMG_A_DP_PORT P0 //使用宏定义数码管段码口
sbit LSA=P2^2;
sbit LSB=P2^3;
sbit LSC=P2^4;

for(i=0;i<8;i++){
    //这个程序是从左到右显示12345678.当然完全也可以把确定哪一位数码管和确定要显示的数字封装成一个函数,点亮会更方便。
		switch(i){
			case 0: LSC=1;LSB=1;LSA=1;break;//从左往右第0个数码管
			case 1: LSC=1;LSB=1;LSA=0;break;
			case 2: LSC=1;LSB=0;LSA=1;break;
			case 3: LSC=1;LSB=0;LSA=0;break;
			case 4: LSC=0;LSB=1;LSA=1;break;
			case 5: LSC=0;LSB=1;LSA=0;break;
			case 6: LSC=0;LSB=0;LSA=1;break;
			case 7: LSC=0;LSB=0;LSA=0;break;
			break;
		}
		SMG_A_DP_PORT=gsmg_code[i];
		delay_10us(100);
		SMG_A_DP_PORT=0x00;//消影
    //为什么要消影? 不延时的话动态数码管会反复重复 位选 段选 位选 段选……位选之后不一定及时段选,可能前一位的位选就会赋给后一位。因此需要消影。
}

我们这种动态数码管扫描方式是单片机直接扫描,硬件会简单很多,但是会占据大量的 CPU 时间。有的动态数码管自带显存和扫描电路,只要告诉他要显示什么他会自动扫描显示。

按键

内部有一个金属片,按下后电路接通。

image-20230125234043319

和学习 LED 时类似,所有 IO 口一开始都是高电平,我们给其接地就变成低电平了。

按下按键后一段时间内电平会高低抖动。

image-20230125235945530

1,先设置 IO 口为高电平(由于开发板 IO 都有上拉电阻,所以默认 IO 为高 电平)。

2,读取 IO 口电平确认是否有按键按下。

3,如有 IO 电平为低电平后,延时几个毫秒。

4,再读取该 IO 电平,如果仍然为低电平,说明按键按下。

5,执行按键控制程序。

#include "reg52.h"
typedef unsigned int u16; //对系统默认数据类型进行重定义
typedef unsigned char u8;
//定义独立按键控制脚
sbit KEY1=P3^1;
sbit KEY2=P3^0;
sbit KEY3=P3^2;
sbit KEY4=P3^3;
//定义 LED1 控制脚
sbit LED1=P2^0;
sbit LED2=P2^1;
sbit LED3=P2^2;
sbit LED4=P2^3;
//使用宏定义独立按键按下的键值
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4
#define KEY_UNPRESS 0

void delay_10us(u16 ten_us)
{
while(ten_us--);
}

u8 key_scan(u8 mode)
{
	static u8 key=1;
	if(mode)key=1;//连续扫描按键
	if(key==1&&(KEY1==0||KEY2==0||KEY3==0||KEY4==0))//任意按键按下
	{
		delay_10us(1000);//10ms 消抖
        /*另一种消抖的方法:
        if(KEY==0){
        	delay_ms(20);
        	while(KEY==0);
        	delay(20ms);
        }
        这个是针对按下按钮又抬起按钮之后的执行。*/
		key=0;
		if(KEY1==0)
		return KEY1_PRESS;
		else if(KEY2==0)
		return KEY2_PRESS;
		else if(KEY3==0)
		return KEY3_PRESS;
		else if(KEY4==0)
		return KEY4_PRESS;
	}
	else if(KEY1==1&&KEY2==1&&KEY3==1&&KEY4==1) //无按键按下
	{
		key=1;
	}
	return KEY_UNPRESS;
}
void main()
{
	u8 key=0;
	while(1)
	{
		key=key_scan(0);
		if(key==KEY1_PRESS)//检测按键 K1 是否按下
		LED1=!LED1;//LED1 状态翻转
		if(key==KEY2_PRESS)//检测按键 K2 是否按下
		LED2=!LED2;//LED1 状态翻转
		if(key==KEY3_PRESS)//检测按键 K3 是否按下
		LED3=!LED3;//LED1 状态翻转
		if(key==KEY4_PRESS)//检测按键 K4 是否按下
		LED4=!LED4;//LED1 状态翻转
	}
}

矩阵按键

为了减少 IO 口的占用,用4个 IO 口代表行,4个 IO 口代表列。

类似动态数码管快速扫描实现几乎同时点亮的效果,矩阵键盘也是快速扫描。

image-20230126221349232

主要有两种扫描方法

行列式扫描法:每次给某一列赋值为0,然后检测这一列有无按钮按下。

​ 按行扫描:通过设置 P17 16 15 14 中的一个为低电平来选择扫描哪一行。根据 P10 P11 P12 P13 的输入判断是哪一列。但是 P15 口是蜂鸣器,不断反转会响。所以最好还是用按列扫描。

线翻转扫描方法:给所有列赋1,给所有行赋0,先判断在哪一行;然后用同样的方法判断在哪一列。

这里有一点问题就是:本单片机是准双向口输出,每个口既能做输入也能做输出而不用重新配置口线输出状态。其实这样相当于单片机一个引脚输出高电平,直接与另一个为低电平的引脚相连接。不会短路吗?

单片机的处理方法是这样的:内部 VCC 电源还附带一个下拉电阻。低电平的驱动力比高电平强,高电平直接接低电平就会被变为低电平,而不会短路。

#include "reg52.h"
typedef unsigned int u16; //对系统默认数据类型进行重定义
typedef unsigned char u8;
#define KEY_MATRIX_PORT P1 //使用宏定义矩阵按键控制口
#define SMG_A_DP_PORT P0 //使用宏定义数码管段码口
//共阴极数码管显示 0~F 的段码数据
u8 gsmg_code[17]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};

void delay_10us(u16 ten_us)
{
    while(ten_us--);
}

u8 key_matrix_ranks_scan(void)
{
    u8 key_value=0;
    KEY_MATRIX_PORT=0xf7;//1111 0111 给第一列也就是 P1_3 赋值 0,其余全为 1
    if(KEY_MATRIX_PORT!=0xf7)//判断第一列按键是否按下
    {
        delay_10us(1000);//消抖。因为这里用了类似动态数码管的循环扫描的方法。
        switch(KEY_MATRIX_PORT)//保存第一列按键按下后的键值
        {
            case 0x77: key_value=1;break;//0111 0111
            case 0xb7: key_value=5;break;//1011 0111
            case 0xd7: key_value=9;break;//1101 0111
            case 0xe7: key_value=13;break;//1110 0111
        }
    }
    while(KEY_MATRIX_PORT!=0xf7);//等待按键松开
    KEY_MATRIX_PORT=0xfb;//给第二列赋值 0,其余全为 1
    if(KEY_MATRIX_PORT!=0xfb)//判断第二列按键是否按下
    {
        delay_10us(1000);//消抖
        switch(KEY_MATRIX_PORT)//保存第二列按键按下后的键值
        {
            case 0x7b: key_value=2;break;
            case 0xbb: key_value=6;break;
            case 0xdb: key_value=10;break;
            case 0xeb: key_value=14;break;
        }
    }
    while(KEY_MATRIX_PORT!=0xfb);//等待按键松开
    KEY_MATRIX_PORT=0xfd;//给第三列赋值 0,其余全为 1
    if(KEY_MATRIX_PORT!=0xfd)//判断第三列按键是否按下
    {
        delay_10us(1000);//消抖
        switch(KEY_MATRIX_PORT)//保存第三列按键按下后的键值
        {
            case 0x7d: key_value=3;break;
            case 0xbd: key_value=7;break;
            case 0xdd: key_value=11;break;
            case 0xed: key_value=15;break;
        }
    }
    while(KEY_MATRIX_PORT!=0xfd);//等待按键松开
    KEY_MATRIX_PORT=0xfe;//给第四列赋值 0,其余全为 1
    if(KEY_MATRIX_PORT!=0xfe)//判断第四列按键是否按下
    {
        delay_10us(1000);//消抖
        switch(KEY_MATRIX_PORT)//保存第四列按键按下后的键值
        {
            case 0x7e: key_value=4;break;
            case 0xbe: key_value=8;break;
            case 0xde: key_value=12;break;
            case 0xee: key_value=16;break;
        }
    }
    while(KEY_MATRIX_PORT!=0xfe);//等待按键松开
    return key_value;
}

u8 key_matrix_flip_scan(void)//另一种扫描方式,这个函数暂时没有投入使用
{
    static u8 key_value=0;
    KEY_MATRIX_PORT=0x0f;//给所有行赋值 0,列全为 1
    if(KEY_MATRIX_PORT!=0x0f)//判断按键是否按下
    {
        delay_10us(1000);//消抖
        if(KEY_MATRIX_PORT!=0x0f)
        {
            //测试列
            KEY_MATRIX_PORT=0x0f;
            switch(KEY_MATRIX_PORT)//保存行为 0,按键按下后的列值
            {
                case 0x07: key_value=1;break;
                case 0x0b: key_value=2;break;
                case 0x0d: key_value=3;break;
                case 0x0e: key_value=4;break;
            }
            //测试行
            KEY_MATRIX_PORT=0xf0;
            switch(KEY_MATRIX_PORT)//保存列为 0,按键按下后的键值
            {
                case 0x70: key_value=key_value;break;
                case 0xb0: key_value=key_value+4;break;
                case 0xd0: key_value=key_value+8;break;
                case 0xe0: key_value=key_value+12;break;
            }
            while(KEY_MATRIX_PORT!=0xf0);//等待按键松开
        }
    }
    else
    key_value=0;
	return key_value;
}

void main()
{
    u8 key=0;
    while(1)
    {
        key=key_matrix_ranks_scan();
        if(key!=0)
        SMG_A_DP_PORT=gsmg_code[key-1];//得到的按键值减 1 换算成数组下标
    }
}

IO 扩展(串转并)-74HC595

前面接的一些输入输出设备都是直接连接的单片机 IO 口,单片机仅有的 IO 口非常有限。而使用 IO 扩展可以大量增加可使用的端口。比如后面要使用的 LED 点阵,8*8个格子,使用扩展 IO 输入就更为合适。如果多级联一个,就又有了8位输出,能实现16*16的点阵。

由图可知,OE 低电平有效,因此 LED 点阵旁的跳线帽一定要接到 OE-GND 一端。

74HC595 是一个位移缓存器,有8位串行输入、并行输出,并行输出是三态输出(高电平、低电平、高阻抗)。比如一次输入一个比特,输入八次,并行输出可以输出一个8位的字 1010 1010.

输出是由 VCC 驱动的,原理有那么一点像三极管。因为单片机内部是弱上拉,输出不足以点亮 LED 点阵,所以抛开 IO 口不够的问题,也不能直接接到 LED 点阵上,至少要有三极管。

点亮 LED 点阵

image-20230130004758073

image-20230130004829583

传入数据如列是0100 0000,行是0000 0001,则代表最后一行第二列的点会被点亮。

SRCLK:移位寄存器,数据先传输到移位寄存器中。移位寄存器上升沿时移位,再接收下一次数据。

RCLK:存储寄存器。存储寄存器上升沿时把寄存器中所有数据都通过端口输出。

相当于手枪,每次 SRCLK 上升时我们填入一枚子弹,RCLK 上升时把弹夹塞入。

QH 是级联用的。

列数据直接输入引脚,行数据输入 IO 拓展。

#include "reg52.h"
//编写程序先定义管脚和端口。管脚用sbit,端口宏定义
#define LED_MATRIX P0
sbit SRCLK=P3^6;
//因为 RCLK 是关键字不能被复用了
sbit rCLK=P3^5;
sbit SER=P3^4;
unsigned char hc_led_arr[8]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};
void delay(int i){while(i--);}

void hc_write_data(unsigned char c){
	//要传入8个输入,需要循环
	int i=0;
	for(i;i<8;i++){
		//注意 芯片传数据先传高位 再传低位,所以要反着写。这个问题在write函数内部解决,传入的数据和想要的形式一样就好。
		//通过移位获取
		SER=c>>7;
		//想获取下一位寄存器,需要移位寄存器移位。需要创造上升沿
		SRCLK=0;
		//芯片一般给定一个延时时间,经过这个时间之后才能处理完毕
		delay(1);
		SRCLK=1;
		delay(1);
		//让传入数据的次高位变为下次循环的高位
		c<<=1;
	}
	//最后通过存储寄存器的上升沿,传输全部数据
	rCLK=0;
	delay(1);
	rCLK=1;
}
void main(){
	LED_MATRIX=0x00;
	while(1){
		 int i=0;
		 for(i;i<8;i++){
		 	hc_write_data(0x00);//消隐
		 	hc_write_data(hc_led_arr[i]);
			delay(500000);
		 }
	}
}

比如0000 0001,传入LED阵列的数据是:每轮循环传入最高位的值,并且所有数据向左移动一位。因此前7轮 SER 传入都是0,最后一轮 SER 传入1,最下面一行全亮。

LED 点阵实验

上面的方法只能确定某一具体的行被点亮。可不可以具体确定哪些点点亮的方法?

我们让想被点亮的点列为低电平,行为高电平,就会被点亮。如果我们只想点亮第一行第一列的点,只需行脚只有第一行接高电平,列脚只有第一列接低电平即可。

所以只要先让第一列为低电平,其他列为高天平来只读取第一列,遍历所有行检查第一列哪些点应该被点亮;然后第二列,第三列……一次类推,每轮循环不用消除上次的结果即可。

#include "reg52.h"
//编写程序先定义管脚和端口。管脚用sbit,端口宏定义
#define LED_MATRIX P0
sbit SRCLK=P3^6;
sbit rCLK=P3^5;
sbit SER=P3^4;
unsigned char hc_led_arr[8]={0x38,0x7C,0x7E,0x3F,0x3F,0x7E,0x7C,0x38};
unsigned char col[8]={0x7f,0xbf,0xdf,0xef,0xf7,0xfb,0xfd,0xfe};
void delay(int i){while(i--);}

void hc_write_data(unsigned char c){
	//要传入8个输入,需要循环
	int i=0;
	for(i;i<8;i++){
		//注意 芯片传数据先传高位 再传低位,所以要反着写
		//通过移位获取
		SER=c>>7;
		//想获取下一位寄存器,需要移位寄存器移位。需要创造上升沿
		SRCLK=0;
		//芯片一般给定一个延时时间,经过这个时间之后才能处理完毕
		delay(1);
		SRCLK=1;
		delay(1);
		//让传入数据的次高位变为下次循环的高位
		c<<=1;
	}
	//最后通过存储寄存器的上升沿,传输全部数据
	rCLK=0;
	delay(1);
	rCLK=1;
}
void main(){
	LED_MATRIX=0x00;
	while(1){
		 int i=0;
		 for(i;i<8;i++){
		 	LED_MATRIX=col[i];
		 	hc_write_data(hc_led_arr[i]);
             //不知道为什么,下面两部分不写图形会偏移。不知道会不会有大佬解答一下
			delay(100);
			hc_write_data(0x00);
		 }
	}
}

点阵的具体图案生成方法:字模提取软件。

image-20220908234813283

步进电机

电脉冲信号转化为角位移。

**注意步进电机红色线接到5V的地方。**以下程序意为:启动步进电机后,按按钮1旋转方向改变,按按钮2加速,按按钮3减速。

#include "reg52.h"
typedef unsigned int u16;
typedef unsigned char u8;
sbit IN1_A=P1^0;
sbit IN2_B=P1^1;
sbit IN3_C=P1^2;
sbit IN4_D=P1^3;

//定义独立按键控制脚
sbit KEY1=P3^1;
sbit KEY2=P3^0;
sbit KEY3=P3^2;
sbit KEY4=P3^3;
//使用宏定义独立按键按下的键值
#define KEY1_PRESS 1
#define KEY2_PRESS 2
#define KEY3_PRESS 3
#define KEY4_PRESS 4
#define KEY_UNPRESS 0
#define STEPMOTOR_MINSPEED 1
#define STEPMOTOR_MAXSPEED 5

void delay(u16 ten_us)
{
	while(ten_us--);
}

void step_motor_28BYJ48_send_pulse(u8 step,u8 dir){
	u8 temp=step;
	if(dir==0)temp=7-step;//逆时针旋转
	switch(temp)//8 个节拍控制:A->AB->B->BC->C->CD->D->DA
	{
		case 0: IN1_A=1;IN2_B=0;IN3_C=0;IN4_D=0;break;
		case 1: IN1_A=1;IN2_B=1;IN3_C=0;IN4_D=0;break;
		case 2: IN1_A=0;IN2_B=1;IN3_C=0;IN4_D=0;break;
		case 3: IN1_A=0;IN2_B=1;IN3_C=1;IN4_D=0;break;
		case 4: IN1_A=0;IN2_B=0;IN3_C=1;IN4_D=0;break;
		case 5: IN1_A=0;IN2_B=0;IN3_C=1;IN4_D=1;break;
		case 6: IN1_A=0;IN2_B=0;IN3_C=0;IN4_D=1;break;
		case 7: IN1_A=1;IN2_B=0;IN3_C=0;IN4_D=1;break;
		default:break;
	}
}


u8 key_scan(u8 mode)
{
	static u8 key=1;
	if(mode)key=1;//连续扫描按键
	if(key==1&&(KEY1==0||KEY2==0||KEY3==0||KEY4==0))//任意按键按下
	{
	delay(1000);//消抖
	key=0;
	if(KEY1==0)
	return KEY1_PRESS;
	else if(KEY2==0)
	return KEY2_PRESS;
	else if(KEY3==0)
	return KEY3_PRESS;
	else if(KEY4==0)
	return KEY4_PRESS;
	}
	else if(KEY1==1&&KEY2==1&&KEY3==1&&KEY4==1) //无按键按下
	{
	key=1;
	}
	return KEY_UNPRESS;
}

void main(){
	u8 key=0;
	u8 dir=0;
	u8 speed=STEPMOTOR_MAXSPEED;
	u8 step=0;
	while(1){
		key=key_scan(0);
		switch(key)
		{
			case KEY1_PRESS:dir=!dir;break;
			case KEY2_PRESS:if(speed>STEPMOTOR_MINSPEED)speed-=1;break;
			case KEY3_PRESS:if(speed<STEPMOTOR_MAXSPEED)speed+=1;break;
			default:break;
		}
		step_motor_28BYJ48_send_pulse(step++,dir);
		step%=8;
		delay(speed*1000);
	}
}

中断

使单片机能对外部或者内部随机发生的事件实时处理。

分时操作,实时响应,可靠性高。

中断相应条件:首先我们要确保相关配置都准备好了,CPU 允许中断,中断源允许中断,然后发生中断事件时才会正确触发中断。

中断可能还会被优先级更高的中断打断,支持这种操作的系统叫多级中断系统。

STC89C52 有8个中断,4外部,3定时器,1串口。

image-20230127002315074

这是传统51单片机定时器中断结构,原理与 STC89C52 相近。

image-20230127002418325

通过配置寄存器控制线路连接。比如上图中的开关就是由寄存器控制。

EA ENABLE ALL:即使能所有中断。

ET:中断允许位。

PT:中断优先级。只有一个 PT 只能决定是高或低两种优先级。更多的中断优先级寄存器可以决定更多中断优先级。

TCON 部分: time controller,不属于 CPU 部分,等到定时器部分展开叙述。

代码编写:主程序中需要包含:

EA=1;//总中断开关:打开
EX0=1;//外部中断0开关:打开。
IT0=0/1;//外部中断触发方式的选择。如下降沿触发,或低电平触发。
//如果要配置外部中断1,则改为EX1和IT1

//中断服务函数
void int0() interrupt 0 using 1//using 1 可省略
{
    
}

定时器中断实验

本章利用单片机自带的定时器来实现之前做过的操作:LED灯间歇闪烁。一直以来实现的方法都是借助while循环来拖延时间。定时器不仅更加准确,还可以节省下 CPU 的资源。

STC89C52 有3个定时器。

CPU时序的相关知识

振荡周期:为单片机提供信号的振荡源的周期(晶振周期)。12MHZ 的晶振振荡周期就是1/12us, 求倒数。

状态周期:两个振荡周期=1状态周期s(时钟周期)。

机器周期:6状态周期=1机器周期。

指令周期:完成一条指令所用的全部时间,以机器周期为单位。

定时器的相关知识

定时器又可以计数,也叫计数器。不需要CPU参与自己就能完成任务,根据脉冲信号对寄存器中数据+1。来一个脉冲定时器+1,加到全为1后输出一个脉冲并归0.同时,向CPU发出计时器中断信息。

一般有四种工作模式:

13位定时器,16位定时器,8位自动重装模式,双8位计数器。

我们的工作模式用的是16位。

串口通信中为了精度考虑要使用自动重装模式。因为我们要自己设置每次溢出产生中断后 TH TL 的初值,这样吧比较容易出错且有点慢。8位自动重装模式就是舍弃了16位存储数据(到65535),只采用后八位 TL 计数(到255),初始值TH TL 赋一样的初值。TL 每次溢出,TH 把自己的值赋给 TL,这样就不用我们自己手动重新赋初值了,初值一直保存在 TH 中。

而且,在本例中我们使用定时器中断就是为了产生中断时给定时器赋初值并让灯切换状态。使用8位自动重装模式后,不用手动给定时器赋初值了;如果没有其他的需要定时器溢出时必须做的操作,定时器可以不用设置中断,起到一个可以看时间但是不会响的闹钟的功能,即 ET=0。

image-20230126235200709

左上角支路是时钟功能,左下角支路是计数功能,最终实现中断功能。

TH TL 寄存器最大能存储到65535.每来一次脉冲+1,加到最大值时 flag 申请中断。

SYSCLK 是晶振周期。另一个时钟是 T0 引脚,如果启用 T0 引脚定时器就变成计数器了,每来一个脉冲+1。

默认使用12T 的分频,把 12MHZ 分成12份,每一份就是1us。这个单片机上是没有对应调整的寄存器的,如果想使用 6T 的分频需要在 STC-ISP 中选择使能 6T 模式。

CT 是一位寄存器,赋1为C,即计数器;赋0为T,即时钟(T上面的横线就代表0时)。

每个定时器主要有两种寄存器:TCON TMOD。

TCON 包括:TF, TR, IE, IT。

image-20230127114845707

​ TF 可见上图主路,TH TL 被允许计数后周期性+1计数,加到最大值时 TF=1,并发起中断。处理完中断后恢复为0.

​ TR 可见上图支路,是开启中断的条件之一。

​ IE 是外部中断。

​ IT 是设置中断触发模式,比如设置为0是低电平触发,设置为1是下降沿触发。

TMOD 是不可寻址的寄存器,也就是只能整体赋值,不能像 P2 一样分开给每个变量赋值。包含:GATE, CT, M0, M1.

image-20230127114908062

​ GATE 用于开启定时器。当 GATE 打开, TR=1(timer reset)且 I N T 1 / I N T 0 ‾ \overline{INT1/INT0} INT1/INT0 为高(即打开中断引脚)时定时器开始工作。这一部分内容对应上图电路中的左下角。

​ M0M1 用于选定时钟的4个模式。比如16 位就是01. 两者包含一个叫做 TMOD 的不可位寻址的变量里,

​ CT就是打开定时器的计时还是时钟功能。

开启定时器计数功能及总中断:

void time0_init(void)
{
    TMOD&=0xF0;//设置定时器模式
    TMOD|=0X01;//我们知道 TMOD 是不可位寻址,也就是里面虽然既包含了定时器1和0的寄存器,但是我们赋值只能一整个赋值。
    //如果直接给 TMOD=0x01,就会影响定时器1的值。因此我们用这两部先清空后四位,再单独给后四位赋值为0x01。
    
    //TCON 中的 IE IT 对应就是支路图中的 INT0,因为我们开启了 GATE,或电路,因此 IE IT 不用设置也行。
    //我们知道 TH TL 合起来达到65535,也就是过了 65535个机器周期 后会触发中断。
    //比如我们现在想1ms触发一次中断,怎么处理呢?
    //12MHZ 下1us一个周期,1ms 1000个周期,因此我们每次设置初始值为64535,变为65535正好需要1ms。
    //11.0592MHz除12为921600Hz,就是1us 0.9216 个机器周期,因此初值为65535-922=64613.
    //我们可以给 TH TL 赋初始值,64535,这样只要过 1000us 就会触发中断。
    //然后因为 TH 和 TL 拼接变为一整个16位的寄存器,所以 TH 是高8位,TL 是低8位,分别用计算出的初值/256 %256得到最终结果。
    TH0=0XFC; //64613/256=252=0xFC,我的单片机是 11.0592 MHZ
    TL0=0X65;//64613%256=0x65
    
    TF0=0;//归零,防止刚配置好就产生中断。可有可无
    TR0=1;//打开定时器,开启中断条件之一
    ET0=1;//打开定时器 0 中断允许
    EA=1;//打开总中断
    PT0=0;//设置中断优先级为低。默认也是低,不写也没关系。
}

STC-ISP 上也有生成定时器函数。不过 AUXR 设置定时器时钟那一步是针对最新版本可以调整单片机定时器使能而添加的,我们的单片机加上会报错,需要删掉。

另外需要手动添加 ET EA PT。

image-20230127164307914

LED灯间隔1s闪烁代码:

#include "reg52.h"
sbit LED=P2^0;
void time0_init(void);//不再重写了~

void  time0() interrupt 1{
	static int i;
	TH0=0XFC; //因为触发中断时,TH TL 归零,所以记得赋初值!
	TL0=0X65;
	i++;
	if(i==1000)
	{
		i=0;
		LED=!LED;//闪烁
	}
}
void main(){
	time0_init();
	while(1){}
}

外部中断实验

运行程序前,请摘下红外接收传感器。因为共用P3^2引脚,会干扰实验结果。

51单片机都有2个外部中断。STC89C5X系列有INT0~INT3四个。

对于三个参数的初始化,一般用一个init函数执行,在main的最开头。

本例中,我们用按键3作为外部中断源。按下按键3就会产生中断。中断执行的指令就是点亮或熄灭LED灯。

#include "reg52.h"
sbit LED=P2^0;
sbit KEY3=P3^2;
void delay(int ten_us)
{
while(ten_us--);
}
void interrupt_init(){
	EA=1;//总中断开关:打开
	EX0=1;//外部中断0开关:打开。
	IT0=1;//外部中断触发方式的选择。如下降沿触发,或低电平触发。	
}

void int0() interrupt 0{
	delay(1000);
	if(KEY3==0){
		LED=!LED;
	}
}

void main(){
	interrupt_init();
	while(1){
		
	}
}

通信

通信基础知识

单片机还可以通过IO口实现多种通信。

串行通信:一条数据线,一次发1bit,发很久。

并行通信:多条数据线,同时发送,发的速度快多了但是费用高、接收困难、抗干扰性差。

异步通信:发送和接收方时钟可以不用完全一致。

同步通信 :发送和接收方时钟要完全一致。

单工、半双工、全双工通信:数据的传输方式,略。

比特率:位/s。

波特率:码元/s。

溢出率:比如13us溢出一次。溢出率就是1/13us。

校验位:如奇偶校验位。

停止位:分隔数据帧。

(有那么一点点感谢通原了~)

串口

串口通信,指外设和计算机之间通过数据线等传输数据的一种通信方式。比如RS-系列,大多数计算机应该都有对应的梯形接口。51单片机内自带UART(Universal Asynchronous Receiver Transmitter,通用异步收发器),可实现与计算机之间的串口通信!

单片机串口通信的管脚:VCC TXD(发送数据,串行输出)RXD(接收数据,串行输入)SGND(信号接地)。

电脑的串口还有很多管脚,如 RTS CTS,单片机的相对简单很多。

TXD和RXD用正负电压表示逻辑1和0,51单片机采用TTL 晶体管-晶体管逻辑集成电路,用高低电平表示逻辑状态(+5V:1;0V:0),所以需要转换型时候才能与计算机串口通信。

还有两种电平状态:RS232,3~15V 表示低(注意),-15~-3V 表示高。RS485,两线压差(差分信号)2~6V 表示高,-6~-2V 表示低。

image-20230128231105461

STC89C52系列有一个通用异步收发器(UART P30 P31端口),有四种工作模式。

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

image-20230129013008311

TXD RXD 直接接到单片机 P30 P31 上. 另一端是单片机上自带的,我们把数据线连到电脑上就接上了。

image-20230129014312317

溢出率到波特率的计算见图。

串口助手和单片机要规定好发送数据的形式。

image-20230129193307966

串口数据缓存寄存器:SBUF。物理上是接收和发送两个寄存器,实际上共用相同的地址,只是使用时有不同的意义。我们只需要把数据放入其中就行,发送原理暂不用弄明白。

SCON:串口控制寄存器。控制电路。包含:

​ SM0,SM1:设置工作方式。比如我们采用8位 UART,就赋值01.

​ SM2:与工作方式1无关。

​ REN:是否允许串行接收状态。1允许接收。

​ TB8 RB8:接收到的第9位数据,与工作方式1无关。

​ TI RI:发送接收中断请求标志位。代表发送完了。硬件赋1,需要用软件复位。

赋值的话只有 SM0 SM1=01,和 REN 需要注意,其他的初始值都=0。

PCON:电源管理。包含:

​ SMOD:可见支路图,用于设置波特率是否加倍。

​ SMOD1:纠错或协助 SM0 设置工作方式。

IE:打开中断。

移位寄存器会触发对应中断。在中断图中的 TI RI,触发的是同一个中断。

实施串口通信

STC-ISP自带一个串口调试助手。

image-20230103020705868

串口选择左侧和串口号一致的选项。

STC89C52串口初始化函数:

void uart_init(void)
{
    TMOD|=0X20; //设置计数器工作方式 2
    SCON=0X50; //设置为工作方式 1。40是 REN 关闭,50是打开,代表单片机是否可以接收数据
    PCON=0X80; //波特率加倍,0就是不加倍
    TH1=0XFA; //计数器初始值设置,根据波特率为9600
    TL1=0XFA;
    ES=1; //打开接收中断
    EA=1; //打开总中断
    TR1=1; //打开计数器1
}

初始化函数也可以在 STC-ISP 中生成。这里会发现 12MHZ 的晶振相较 11.0592 MHZ 的晶振误差较大,要通过波特率加倍才能减少一些。这就是 11.0592 MHZ 晶振的设计原因。

单片机向电脑发送数据:给SBUF赋值即可。

在程序中发送可以直接SBUF=0X11;单片机就会收到11的信息,点击复位按钮后可以在串口助手的接收缓冲区中看到。

赋值后需要一段时间才能发送完成,发送完成后TI不再是0.

void main(){
    uart_init();
    SBUF=0x11;
    while(TI==0);TI=0;//=1说明发送完成,然后手动复位
    while(1){}
}

这样就在程序中发送了11信息。

电脑给单片机发送数据:通过串口助手发信息可以通过串口中断interrupt 4实现。

void uart() interrupt 4 //串口通信中断函数
{
    
    u8 rec_data;
    P2=0x00;//这一句使得函数成功触发时LED灯全亮,便于调试
    if(RI==1){P2=~SBUF;RI=0;}//因为发送和接收中断共用4中断,这句用于区分具体是发送还是接收中断
    //如果是接收中断,RI=1,那么只简单执行这两句即可。
    SBUF=rec_data; //将接收到的数据放入到发送寄存器
    while(!TI); //等待发送数据完成
    TI=0; //清除发送完成标志位
}

以上程序可以将发送缓冲区中输入的数据发给单片机,单片机再在接收缓冲区中原封不动地呈现出来。

文本模式和 HEX 模式就是文本和 ASCII 码的转换。

封装头文件;绘制LED动画

类似C语言的语法,部分函数等内容可以封装到头文件里,需要的时候引入到source file 中,再在 include 中指明即可正常使用。

编写.h文件:如:

image-20230103162731330

引入.h文件:右键左侧的.c文件→options for file→C51→include path→找到.h文件所在的文件夹并选中,注意一定不能有中文路径。然后就可以使用.h文件中定义的变量和函数了,注意不能重复定义

什么内容封装到函数里呢?静态的方法待调用的封装进去。逻辑判断后决定使用哪个方法这类的就不用放进函数里了,因为逻辑判断很可能经常改。

接下来就自己试着先把delay函数和矩阵LED绘制函数写入头文件,然后制作矩阵动画,这样动起来也会更方便一些。

//h file
#include "reg52.h"
typedef unsigned char u8;
typedef unsigned int u16;

	sbit SRCLK = P3 ^ 6;
	sbit rCLK = P3 ^ 5;
	sbit SER = P3 ^ 4;
	
/**
 * Function name: time0_init
 * Function paremeter: void
 * Function performance: 初始化定时器。只有执行此函数后才能使用单片机的定时器功能
 */
void time0_init(void)
{
	TMOD |= 0X01; // 选择为定时器 0 模式,工作方式 1
	TH0 = 0XFC;	  // 给定时器赋初值,定时 1ms
	TL0 = 0X18;
	ET0 = 1; // 打开定时器 0 中断允许
	EA = 1;	 // 打开总中断
	TR0 = 1; // 打开定时器
}
/**
 * Function name:time0
 * Function paremeter: void
 * Function performance: 启动定时器。定时器计数器time_counter在0~10000之间循环。
 */
/*
void time0() interrupt 1{
	TH0=0XFC; //给定时器赋初值,定时 1ms
	TL0=0X18;
	time_counter++;
	bit pass_1s=0;
	if(time_counter==1000)
	{
		time_counter=0;
		pass_1s=~pass_1s;
	}
}
*/
void delay(u16 delay_10us){
	while(delay_10us--);
}

void hc_write_data(unsigned char c)
{
	// 要传入8个输入,需要循环
	int i = 0;
	for (i; i < 8; i++)
	{
		// 注意 芯片传数据先传高位 再传低位,所以要反着写
		// 通过移位获取
		SER = c >> 7;
		// 想获取下一位寄存器,需要移位寄存器移位。需要创造上升沿
		SRCLK = 0;
		// 芯片一般给定一个延时时间,经过这个时间之后才能处理完毕
		delay(1);
		SRCLK = 1;
		delay(1);
		// 让传入数据的次高位变为下次循环的高位
		c <<= 1;
	}
	// 最后通过存储寄存器的上升沿,传输全部数据
	rCLK = 0;
	delay(1);
	rCLK = 1;
}

void matrix_led_animation(u8 hc_led_arr[])
{
	unsigned char col[8]={0x7f,0xbf,0xdf,0xef,0xf7,0xfb,0xfd,0xfe};
	int i=0;
	P0=0x00;
	
	for(i;i<8;i++){
		P0=col[i];
		hc_write_data(hc_led_arr[i]);
        //不知道为什么,下面两部分不写图形会偏移。不知道会不会有大佬解答一下
		delay(1);
		hc_write_data(0x00);
	}
	
}

然后就是利用取模软件得到要绘制的图案的字模。这里我选定的图案是之前圣诞节临摹过的像素画中”Merry Christmas“的字体。参照来源:圣诞节 像素画 圣诞树🎄_哔哩哔哩_bilibili

image-20230104123321341

绘制部分代码:

#include "reg52.h"
#include <MyHFile.H>
u8 anime_row[]={
 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C,
 0x40, 0x7C, 0x40, 0x7C, 0x00, 0x1C, 0x14, 0x18,
 0x00, 0x1C, 0x10, 0x00, 0x1C, 0x10, 0x00, 0x1D,
 0x05, 0x1F, 0x00, 0x7C, 0x44, 0x44, 0x44, 0x00,
 0x7C, 0x10, 0x1C, 0x00, 0x1C, 0x10, 0x00, 0x5C,
 0x00, 0x04, 0x1C, 0x10, 0x00, 0x7C, 0x14, 0x00,
 0x1C, 0x10, 0x1C, 0x10, 0x1C, 0x00, 0x0C, 0x14,
 0x1C, 0x00, 0x04, 0x1C, 0x10, 0x00, 0x00, 0x00,
 0x00, 0x00, 0x00, 0x00, 0x00
 };
void main()
{
	int cnt = 0;
	while (1)
	{
	for(cnt;cnt<62;cnt++){
		u8 row[8]={0,0,0,0,0,0,0,0};
		row[0]=anime_row[cnt];
		row[1]=anime_row[cnt+1];
		row[2]=anime_row[cnt+2];
		row[3]=anime_row[cnt+3];
		row[4]=anime_row[cnt+4];
		row[5]=anime_row[cnt+5];
		row[6]=anime_row[cnt+6];
		row[7]=anime_row[cnt+7];
		matrix_led_animation(row);
		delay(5000);
	}
	cnt=0;
	}
}

虽然因为每次绘制完成必须擦除再重新绘制,导致看起来一闪一闪的。

不过最终的动画效果还是不错的~

IIC, AT24C02

掉电不丢失的存储器。存储结构是 E2PROM,通讯方式是 I2C。

EEPROM

存储器主要分为 RAM 易失性存储器和 ROM 非易失性存储器,RAM 存取快但掉电丢失,ROM 正相反。

image-20230217001811596

SRAM:锁存器,速度最快。

DRAM:电容,充电1放电0. 但是电容漏电比较严重,需要扫描器每隔一段时间检查一下是否漏电,漏电再充。

ROM 名字来源于 read-only,但是后期的非易失性存储器都可以写入,只是沿用了名字。

MASK ROM:不可写入。

PROM:只能写入一次。

EPROM:可擦除,但是形式比较麻烦,要用紫外线照射很久。

E2PROM:电擦除更加方便,我们现在的单片机所用。但是容量小。

FLASH:应用广泛。

image-20230217060513510

这只是一个有助理解的简化模型~

选定一些结点导通,比如我们读取第一行地址1处的数据,发现第1/2/3个结点导通,其他节点都未导通,则从最下端读到的该处地址的数据为:1110 0000。

MASK ROM 中,两条线中没有二极管,则两条线都通电流就会导通。有二极管后,数据总线无法流下来,为低电平。

PROM 中如何实现可编程写入一次?蓝色的二极管容易被击穿。被击穿后二极管的通路变为断路,导通;否则和 MASK ROM 低电平原理一样。

EPROM 是二极管可以恢复。

选中哪条总线可以通过三位138译码器的输入决定。

image-20230217062720840

VCC 范围:1.8~5.5v

WE/WR:写保护,WR 高电平保护,不能写入。可以看到 WE 标明低电平启用,而且单片机上的 EEPROM 直接 WE 接地了。

SCL SDA:I2C 接口。

E1 E2:I2C 地址。

image-20230217063431839

大概是能看懂的。SCL SDA 传入数据后,看是 R/W 哪个地址里的数据,通过 DEC 译码器找到地址 R/W,通过串行多路复用器 serial MUX 把并行数据转化为串行并输出。

I2C

I2C(Inter-Integrated Circuit BUS)总线是由 PHILIPS 公司开发的两线式 串行总线,用于连接微控制器及其外围设备。因为各家自己开发的数据总线可能不通用,PHILIPS 公司开发了这款统一通信标准。这样不仅方便了芯片开发,也便于大家学习。

I2C 结构

I2C 只有两根双向信号线,一根是 SDA 数据线,一根是 SCL 时钟线。是同步、半双工的通信线,带数据应答。

所有设备的 SCL 和 SDA 都连在总线上。设备的 SCL SDA 是开漏输出的(高电平直接接地),SCL 和 SDA 又要添加弱上拉电阻。这两共同作用实现了“线与”的功能,避免各个设备通信相互干扰。

如下图所示,设备上的 SCLK SDA 是开漏输出。如果给高电平导通开关, IN 也是接地,该设备就无法读取输入数据不会被干扰。否则如果是低电平关闭开关,数据会流出 DATA IN。

image-20230217072355835

主机:启动数据传送并产生时钟信号的设备;

从机:被主机寻址的器件;

多主机:同时有多于一个主机尝试控制总线但不破坏传输;

主模式:用 I2CNDAT 支持自动字节计数的模式; 位 I2CRM,I2CSTT,I2CSTP 控制数据的接收和发送;

从模式:发送和接收操作都是由 I2C 模块自动控制的;

仲裁:是一个在有多个主机同时尝试控制总线但只允许其中一个控制总线并 使传输不被破坏的过程;

同步:两个或多个器件同步时钟信号的过程;

发送器:发送数据到总线的器件;

接收器:从总线接收数据的器件。

I2C 协议

①时钟信号周期性的高电平低电平。只有时钟信号为低电平时,数据信号才能变化。

②起始和停止信号:SCLK 高电平时,SDA 由高→低为开始,低→高为结束。

④发送数据:

image-20230217111133747

先发 start 标识,后跟一个字节,第一个字节是地址+读写位。前四位固定,比如 24C02 是1010. 后三位是地址。收到接收应答后就开始发送数据。读写位 R / W ‾ R/\overline{W} R/W .

SCLK 低电平时,SDA 先设置好要传的数据。然后 SCLK 变为高电平后 SDA 不允许再数据变化,SCLK 先读取数据。这样发送一位,SCLK 再变为低电平,如此循环。

发送器件传输完一个字节8位数据后,后面必须紧跟一个ACK/NACK校验位,判断接受是否完成,数据传送是否可以继续。接收方会让主机在发完的下一个时钟信号收到接收应答。

image-20230104204527296

image-20230217111807804

复合数据帧格式:先发再收。

image-20230217113542179

字节写:规定一个字节数据地址,写入和读取都去那里。

image-20230217113728972

随机读:

image-20230217192714230

③总线寻址方式

从机有自己的地址,总线寻址采用7或10位的寻址位数。如7位:位定义D0表示数据传送方向位(是主机从从机读取数据?还是主机向从机写数据?代表这个从机是接收器还是发送器),D7~D1是从机地址位。

主机发送一个地址后,所有从机前7位地址和主机比较,如果相同再判断从机是接收还是发送器。

从机地址包含固定位和可编程位,可编程位决定了这个部件可以最多有多少个接入总线。如4位固定位,3位可编程位,说明2^3=8个最多可以接入总线。

④数据传输

起始信号 S+7位从机地址+数据方向位+ACK+数据+NACK+终止信号。如果主机还想要新的数据传送,可以不终止,继续发出起始信号向另一从机寻址。

数据方向位=0:主机向从机发送数据。主机一直发到从机返回 NACK 为止。

数据方向位=1:从机向主机发送数据,发送到主机返回 NACK 为止。

AT24C02

是一个2k位串行CMOS,主板上的主板上的一块可读写的并行或串行FLASH芯片。该芯片有 I2C 接口,是个从机。而且有写保护功能,其中写入的数据断电不丢失。

我们可以通过单片机的模拟 IIO 功能,将数据写入该芯片永久存储,下次断电时也能访问。

创建多文件工程

下面编写的程序要求:

设计一个系统,可以写入、读取 AT24C02 中的数据,并将其中的数据用数码管显示出来。

这个系统涉及之前学过的按键、数码管信息,还涉及新的 AT24C02 的使用,三部分代码。由于内容太多,所以这次我们创建一个多文件系统,以后也会用这个系统模板更好的管理文件。

本项目主要包含:

App 文件夹:存储各类函数

Obj 文件夹:存放编译产生的 hex 文件、列表清单等。

Public 文件:存放公共文件,如延时、变量重定义。

User:存放 main.c 等主函数文件。

创建步骤:

①新建一个项目文件夹,在该项目文件夹中新建以上四个文件夹。

②在 Keil 中新建项目,选中这个项目文件夹。选择不复制 StartUp 代码。

③点击魔术棒右边的三色方块,创建三个分组 Group。这里创建的分组是一个逻辑上的分组,并不是指选中这三个具体的文件夹。与三个文件夹起名相同是为了方便管理.

image-20230105115404825

④编写程序.

清楚了我们的需求,我们想一下要写那些代码,放在什么地方。

公共内容:public 中。

使用按钮代码:App中。

使用数码管代码:App中。

使用 AT24C02 代码:App 中。

调用以上几部分内容代码:main 中。

image-20230105134459205

把普中部分代码复制到单片机中,大意:按键1将当前数据写入芯片,按键2读取芯片中存储的数据,按键3将当前数据+1,按键4将当前数据清零。

iic:定义i2c总线的一些方法。如开始等待接收数据、结束关闭、ack、nack、wait ack、读取写入数据等方法。

/iic.h
#ifndef _iic_H
#define _iic_H

#include "public.h"

//定义EEPROM控制脚
sbit IIC_SCL=P2^1;//SCL时钟线
sbit IIC_SDA=P2^0;//SDA数据线


//IIC所有操作函数				 
void iic_start(void);			//发送IIC开始信号
void iic_stop(void);	  		//发送IIC停止信号
void iic_write_byte(u8 txd);	//IIC发送一个字节
u8 iic_read_byte(u8 ack);		//IIC读取一个字节
u8 iic_wait_ack(void); 			//IIC等待ACK信号
void iic_ack(void);				//IIC发送ACK信号
void iic_nack(void);			//IIC不发送ACK信号

#endif


///iic.c
#include "public.h"
#include "iic.h"

void iic_start()
{
    IIC_SCL = 1; // SCL为高电平时,SDA的数据才有效
    IIC_SDA = 1;
    delay(10);
    IIC_SDA = 0; // SCL SDA 都由高变低,是起始标志
    
	delay(10);
    IIC_SCL = 0;
}

void iic_stop()
{
    IIC_SCL = 1;
    IIC_SDA = 0;
    delay(10);
    IIC_SDA = 1;
    delay(10);
}

void iic_ack()
{
    IIC_SCL = 0;
    IIC_SDA = 0;
    delay(10);
    IIC_SCL = 1;
    delay(10);
    IIC_SCL = 0;
}

void iic_nack()
{
    IIC_SCL = 0;
    IIC_SDA = 1;
    delay(10);
    IIC_SCL = 1;
    delay(10);
    IIC_SCL = 0;
}

u8 iic_wait_ack()
{
    u16 time_temp = 0;
    IIC_SCL = 1;
    while (IIC_SDA)
    { // 等待数据变成0
        if (++time_temp > 100)
        {
            iic_stop();
            return 1;
        }
    }
    IIC_SCL = 0;
    return 0;
}

u8 iic_read_byte(u8 ack)
{
    u8 i = 0, receive = 0;
    for (i; i < 8; i++)
    {
        IIC_SCL = 0;
        delay(1);
        IIC_SCL = 1;
        receive <<= 1;
        if (IIC_SDA)
            receive += 1;
        delay(1);
    }
    if (!ack)
        iic_nack();
    else
        iic_ack();
    return receive;
}

void iic_write_byte(u8 dat)
{
    u8 i = 0;
    IIC_SCL = 0;
    for (i; i < 8; i++)
    {
        if ((dat & 0x80) > 0)
            IIC_SDA = 1;
        else
            IIC_SDA = 0;
        dat <<= 1;
        delay(1);
        IIC_SCL = 1;
        delay(1);
        IIC_SCL = 0;
        delay(1);
    }
}

24c02:主要就是芯片调用iic来写入或取出数据的函数。看上去倒没有多少自己的东西。

/24c02.h
#ifndef _24c02_H
#define _24c02_H

#include "public.h"
void at24c02_write_one_byte(u8 addr, u8 dat);
u8 at24c02_read_one_byte(u8 addr);
#endif

/24c02.c
#include "public.h"
#include "24c02.h"
#include "iic.h"

void at24c02_write_one_byte(u8 addr, u8 dat){
    iic_start();
    iic_write_byte(0xA0);//写命令
    iic_wait_ack();
    iic_write_byte(addr);//发送写地址
    iic_wait_ack();
    iic_write_byte(dat);
    iic_wait_ack();
    iic_stop();//停止条件
    delay(1);
}

u8 at24c02_read_one_byte(u8 addr){
	u8 temp;
    iic_start();
    iic_write_byte(0xA0);//写命令
    iic_wait_ack();
    iic_write_byte(addr);//发送写地址
    iic_wait_ack();
    iic_write_byte(0xA1);//进入接收模式
    iic_wait_ack();
    temp=iic_read_byte(0);//读取条件
    iic_stop();//停止条件
    return temp;
}

public.h和.c里面就是装了u8 u16 和 delay 函数的定义。不用多说。

main.c:

#include <reg52.h>
#include <public.h>
#include <key.h>
#include <display.h>
#include <24c02.h>
#define EEPROM_ADDRESS 0//数据存入EEPROM 的起始地址
void main(){
    u8 key_temp=0;
    u8 save_value=0;//每次刚打开单片机时,起始值都=0
    u8 save_buf[3];
    while(1){
        key_temp=key_scan(0);//单词扫描按键获取当前按键
        switch(key_temp){//按键1:写入
            
			case KEY1_PRESS:
                at24c02_write_one_byte(EEPROM_ADDRESS,save_value);
                break;
            case KEY2_PRESS:
                save_value=at24c02_read_one_byte(EEPROM_ADDRESS);
                break;
            case KEY3_PRESS:
                if(save_value<255)save_value++;
                break;
            case KEY4_PRESS:
                save_value=0;
                break;
            default:
                break;
        }
        save_buf[0]=save_value/100;
        save_buf[1]=save_value/10%10;
        save_buf[2]=save_value%10;
        smg_display(save_buf,5);
    }
}

main.c:

void main(){
	unsigned char dat=0,KeyNum;
	LCD_Init();
	//LCD_ShowString(1,1,"Hello!");
	//AT24C02_WriteByte(1,66);//256 位字节,存入数据不 >256
	//delayMs(50);//两次写之间需要的时间间隔
	//AT24C02_WriteByte(2,34);//256 位字节
	//delayMs(50);
	//dat=AT24C02_ReadByte(1);
	//delayMs(50);
	//dat1=AT24C02_ReadByte(2);
	LCD_ShowNum(2,1,dat,3);
	while(1){
		KeyNum=key_scan(0);
		switch(KeyNum){
			case KEY1_PRESS:
				if(dat<255)dat+=1;
				LCD_ShowString(1,1,"++");
				LCD_ShowNum(2,1,dat,3);	
				break;
			case KEY2_PRESS:
				if(dat>0)dat-=1;
				LCD_ShowString(1,1,"--");
				LCD_ShowNum(2,1,dat,3);	
				break;
			case KEY3_PRESS:
				AT24C02_WriteByte(1,dat);
				LCD_ShowString(1,1,"Write Complete!");
				delayMs(50);
				LCD_ShowNum(2,1,dat,3);	
				break;
			case KEY4_PRESS:
				dat=AT24C02_ReadByte(1);
				LCD_ShowString(1,1,"Read Complete!");
				LCD_ShowNum(2,1,dat,3);	
				break;
		}	
	}
}

运行效果:

一开始运行,显示当前数字000.

按下按钮1,数字++。

按下按钮2,数字–。

按下按钮3,当前数字会写入。

按下按钮4,会读取 AT24C02 中存储的数字显示出来。断电也能保存。

一个字节确实只能存到 255. 我们可以拿 12 两个存储地址合起来存储一个数据,一个存储高八位 /256,一个存储低八位 %256. 取数据的时候 datl|=dath<<8;

写入多位时,延时间隔应该>5ms!因为 at24c02 的写入还是很慢的。

温度传感器 DS18B20

单片机上自带的温度传感器模块:DS18B20。是一种常见的数字温度传感器。不用 ADC,自己处理好数据后放到 RAM 中,我们取出来就能用。

总线结构,可以把很多设备挂到总线上,省 IO 口;有温度报警装置;寄生供电(总线高电平可以供电).

寄生供电需要添加强上拉电阻。

传感器很简单,只有三个引脚:

image-20230322222620666

DQ 是 P37。

image-20230322232422937

具体结构如上图。寄生供电使得 DQ 也可以供电。

ROM 是寻址用,可以跳过。

SCRATCHPAD:暂存器,读取的数据在其中。

EEPROM:存储一些配置信息,比如报警阈值,精度。

image-20230322235814438

DS18B20 采用异步半双工的单总线,加上寄生供电,使得只要两根线就能驱动。不过这种结构并不常见。

我们将检测到的温度通过数码管显示出来。数码管的传入函数和IIC的一样,将外部dat数组传给数码管。

init

主机输出低电平拉低总线 480~960 us,释放总线;外部上拉电阻把总线拉高,延时 15~60 us 进入接收模式。接着 DS18B20 拉低总线 60~240 us。

我们要在主机释放总线后,检测一下一定时间内总线是否被拉低了,拉低了说明有从机。然后再检测一定时间内从机是否把总线又释放了。

发送一位数据

主机拉低总线 60~120 us 后释放:0.

主机拉低总线 1~15 us :1.

一般发送间隔 1ms。

从机拉低 30us 读取数据。

读取一位数据

主机拉低总线 15us,接近结束时释放,看一下总线是否释放。

如果释放了,说明从机在发送数据1,在 15us 内就释放了。

如果没释放,说明从机还在拉着总线,是打算拉 60us 以上的0.

至于发送读取是否会混淆,无需担心,因为总线发送给主机指令来决定其作用。

发送 接收字节

连续读或发1个字节 8次。低位在前。

整体流程

从机复位,主机检测从机是否响应;

ROM 指令+本指令是 R/W;

功能指令+本指令是 R/W。

image-20230324152054926

因为只有一个从机,所以这里用 skip rom 指令 跳过地址校验。

功能指令:

  • 转换温度:该指令一发出,立刻转换温度后放在暂存器中。
  • 写入寄存器:只写3个字节。TH TL configuration register。
  • 读寄存器:读出寄存器数据。我们知道温度值只是前2个字节,所以简单处理只读2个字节即可。
  • copy:寄存器指令写入 EEPROM。RECALL 相反。
  • read power supply:检测是否寄生供电,这里我们也不需要。

温度变换:init-跳过 ROM-转换温度。

温度读取:init-跳过 ROM-读取温度-持续读取。

温度>0:*0.625

温度<0:取反+1,再*0.625

具体代码解释在下文的注释中。

#include "intrins.h"
sbit DS18B20_PORT=P3^7;

//初始化序列:首先拉低总线至少480us,以产生复位脉冲。接着主机释放总线,延时至少15us,进入接收模式
void ds18b20_reset(){
	DS18B20_PORT=0;//拉低DQ
	delay(75);//拉低750us
	DS18B20_PORT=1;//DQ=1
	delay(2);
}

u8 ds18b20_check(){
	u8 time_temp=0;
	while(DS18B20_PORT&&time_temp<20){
		time_temp++;
		delay(1);
	}
	if(time_temp>=20)return 1;//超时跳出,而不是因为DQ变低跳出
	else time_temp=0;
	while((!DS18B20_PORT)&&time_temp<20){
		time_temp++;
		delay(1);
	}
	if(time_temp>=20)return 1;
	return 0;
}

u8 ds18b20_init(){
	ds18b20_reset();
	return ds18b20_check();
}

/*写时序:写1时序:拉低后延时2us,释放后延时60us.
写0时序:拉低后延时60us,释放后延时2us。*/
void ds18b20_write_byte(u8 dat){
	u8 i=0;
	u8 temp=0;
	for(i;i<8;i++){
		temp=dat&0x01;dat>>=1;
		if(temp){
			DS18B20_PORT=0;
			_nop_();_nop_();
			DS18B20_PORT=1;	
			delay(6);
		}
		else{
			DS18B20_PORT=0;
			delay(6);
			DS18B20_PORT=1;
			_nop_();_nop_();	
		}
	}
}

//读时序:至少需要60us。时序起始后15us内读数据(采样总线)。
//典型的读时序过程为:主机输出低电平延时 2us,然后主机转入输入模式延时12us,然后读取单总线当前的电平,然后延时 50us。
u8 ds18b20_read_bit(){
	u8 dat=0;
	DS18B20_PORT=0;
	_nop_();_nop_();
	DS18B20_PORT=1;	
	_nop_();_nop_(); //该段时间不能过长,必须在15us内读取数据
	if(DS18B20_PORT)dat=1;	//如果总线上为1则数据dat为1,否则为0
	else dat=0;
	delay(5);
	return dat;	
}

u8 ds18b20_read_byte(){
	u8 i=0;
	u8 dat=0;
	u8 temp=0;
	for(i;i<8;i++){
		//先低位后高位
		temp=ds18b20_read_bit();
		dat=(temp<<7)|(dat>>1);//读取到的先放到最高位,然后逐渐向低位移动
	}
	return dat;
}

void ds18b20_start(){
	ds18b20_init();
	ds18b20_write_byte(0xcc);//跳过ROM命令,此命令通过允许总线主机不提供64位ROM编码而访问存储器操作来节省时间。
	ds18b20_write_byte(0x44);//转换命令
}
float ds18b20_read_temperature(){
	u8 datl=0,dath=0;
	u16 val=0;
	float temp;
	ds18b20_start();
	ds18b20_init();
	ds18b20_write_byte(0xcc);
	ds18b20_write_byte(0xbe);//读存储器命令,读出温度
	datl=ds18b20_read_byte();//低字节
	dath=ds18b20_read_byte();//高字节
	val=(dath<<8)+datl;
	if((val&0xf800)==0xf800)//负温度
	{
		val=(~val)+1;//取反+1
		temp=val*(-0.0625);//计算方法如此。*0.0625,如果是负数要先取反+1。
	}
	else temp=(0.0625*val);
	return temp;
}

void main(){
	int cnt=0;//每50次循环读取一次温度
	int temp_value;
	u8 temp_buf[5];
	ds18b20_init();
	while(1){
		cnt++;
		if(cnt%50==0){
			cnt%=50;
			temp_value=ds18b20_read_temperature()*10;
			if(temp_value<0){
				temp_value=-temp_value;
				temp_buf[0]=0x40;//负号
			}
			else temp_buf[0]=0x00;//不显示负号
			temp_buf[1]=gsmg_code[temp_value/1000];
			temp_buf[2]=gsmg_code[temp_value%1000/100];
			temp_buf[3]=gsmg_code[temp_value%100/10]|0x80; //加小数点
			temp_buf[4]=gsmg_code[temp_value%10];
			smg_display(temp_buf,3);
		}
	}
}

时钟

DS1302 时钟芯片。开发板上已经集成的芯片,本次实验目的是用数码管显示出当前时间,以hh-mm-ss的格式。

image-20230130130648915

VCC2:电源

VCC1:备用电源,即单片机断电后维持时钟继续运行的。STC89C52 是没有的,即断电就停止运行。

X1X2:晶振。

CE:使能。

IO:输入输出。

SCLK:时钟。

操作流程就是将数据写入 DS1302 的寄存器来设置当前时间格式,然后 DS1302 时钟运作后我们再将寄存器中数据读出。

DS1302 中存储顺序是秒分时日月周年,存储格式是 BCD 码。

image-20230110142624173

首先,CE/RST在整个读写过程中要保持是高电平,一次字节读写完毕后要返回低电平。

然后,控制指令字输入后的下一个 SCLK 上升沿数据写入,下降沿数据读出,在图中可以看得出来,都是从低位0先读,最后出高位7. 第一位代表读或写,后四位代表地址(年、月、日、时、分、秒有着不同的地址),R/C 代表存取 RAM 数据还是读取时钟数据。最高位只有=1才能启用时钟。后面八位是读或写的数据。

读时序注意:因此写命令的第八个上升沿结束后,紧接着第八个下降沿就开始读数据了。

写时序注意:先关闭写保护 WP,1是只读,0才可以写。

image-20230206234252279

我们先试着写入一个秒初值,然后读取时钟里的秒数值,应该是1s一加。

记得 BCD 码和十进制的转换。

image-20230206015234658

#include"REG52.H"
#include"LCD1602.h"
sbit DS1302_CLK=P3^6;//时钟管脚
sbit DS1302_CE=P3^5;//复位管脚
sbit DS1302_IO=P3^4;//数据管脚

typedef unsigned char u8;
void ds1302Init(){
    DS1302_CE=0;
    DS1302_CLK=0;
}

void ds1302WriteByte(u8 command,u8 dat)
{
u8 i;
    DS1302_CE=1;
    
    for(i=0;i<8;i++)
    {
        DS1302_IO=command&(0x01<<i);//从低到高写入数据
        DS1302_CLK=1;
        DS1302_CLK=0;
    //需要查询最小执行时间。不过这里执行时间都大于最小时间了。
    }
    for(i=0;i<8;i++)
    {
        DS1302_IO=dat&(0x01<<i);//从低到高写入数据
        DS1302_CLK=1;
        DS1302_CLK=0;
    //需要查询最小执行时间。不过这里执行时间都大于最小时间了。
    }
    DS1302_CE=0;
}

u8 ds1302ReadByte(u8 command)
{
	u8 i;
	u8 dat=0x00;//全局变量会有初值0,局部变量不会。data 是要写入的数据
    DS1302_CE=1;
    for(i=0;i<8;i++)
    {
        DS1302_IO=command&(0x01<<i);//从低到高写入数据
        DS1302_CLK=0;
        DS1302_CLK=1;//这里为什么和 write 是相反的?因为我们注意到 read 是先上升沿读入8位,再切换为下降沿读入8位。、
        //如果还是先1后0,读入第8位时不仅会把上升沿读掉,下降沿也会读掉,导致错过第九位。
    //需要查询最小执行时间。不过这里执行时间都大于最小时间了。
    }
    for(i=0;i<8;i++)
    {
        
        DS1302_CLK=1;
        DS1302_CLK=0;
    //需要查询最小执行时间。不过这里执行时间都大于最小时间了。
		if(DS1302_IO)dat|=(0x01<<i);//从低到高写入数据
    }
    DS1302_CE=0;
	DS1302_IO=0;//读取前要归0,因为内部是以 BCD 码格式存储。
	return dat;
}

void main(){
    u8 second;
	LCD_Init();
	ds1302Init();
	//ds1302WriteByte(0x8E,0x00);//如果读出数据>59,可能是处于 wp 写保护,需要通过这句关闭
	ds1302WriteByte(0x80,0x01);//写入55s,就是0x55,与内部BCD码对应
	LCD_ShowString(1,1,"DS1302");
	second=ds1302ReadByte(0x81);
	while(1){LCD_ShowNum(2,1,second/16*10+second%16,3);}
}

写入其他变量时分秒都一样。就是更换不同的地址。

不过全部定义变量明显太麻烦。可以先 define 定义了所有地址,再定义一个数组存所有值。

u8 ds1302_address[7]={0x80, 0x82, 0x84, 0x86, 0x88, 0x8c};// second, minute, hour, date, month, year
u8 ds1302_time[7]={0x55, 0x59, 0x11, 0x07, 0x02, 0x23};

void main(){
	u8 i;
    u8 time_temp[6];
	u8 temp;
	LCD_Init();
	ds1302Init();
	//ds1302WriteByte(0x8E,0x00);//如果读出数据>59,可能是处于 wp 写保护,需要通过这句关闭
	ds1302WriteByte(0x80,0x01);
	ds1302WriteByte(0x82,0x37);
	for(i=0;i<6;i++){
		ds1302WriteByte(ds1302_address[i], ds1302_time[i]);
	}
	LCD_ShowString(1,1,"  -  -  ");
	LCD_ShowString(2,1,"  :  :  ");
	while(1){
		for(i=0;i<6;i++){
			temp=ds1302ReadByte(ds1302_address[i]+1);
			time_temp[i]=temp/16*10+temp%16;
		}
		for(i=0;i<6;i++){
			LCD_ShowNum(1+i/3,1+3*(i%3), time_temp[5-i],2);
		}
		//LCD_ShowNum(2,1,minute/16*10+minute%16,2);
		//LCD_ShowNum(2,3,second/16*10+second%16,2);
	}
}

然后我们让按键可以修改时钟值,这样用户可以手动更改时钟。

按键1切换切换要调整的位,按键2切换回第0位,按键3+1,按键4-1.

难点主要在于+ -的溢出情况。这里我们不考虑秒+1进位对分钟或小时等影响,大多数闹钟也是这样设计的。59+1就归零,0-1就变59.

if(key){
			switch(key){
				case KEY1_PRESS: time_set_select=0;break;
				case KEY2_PRESS: time_set_select=(time_set_select+1)%6;break;
				case KEY3_PRESS:
					if(time_set_select<=1&&ds1302_time[time_set_select]==59){//second, minute 59-00
						ds1302_time[time_set_select]=0;
					}
					else if(time_set_select==2&&ds1302_time[time_set_select]==23)ds1302_time[time_set_select]=0;//hour 23-00
					else if(time_set_select==3&&ds1302_time[time_set_select]==maxDate(ds1302_time[3],ds1302_time[4],ds1302_time[5]))ds1302_time[time_set_select]=1;//date 超过最大日期 -01
					else if(time_set_select==4&&ds1302_time[time_set_select]==12)ds1302_time[time_set_select]=1;//month 12-00
					else if(time_set_select==5&&ds1302_time[time_set_select]==99)ds1302_time[time_set_select]=0;//year 99-00
					else ds1302_time[time_set_select]++;break;
				case KEY4_PRESS: 
					if((ds1302_time[time_set_select]&&time_set_select!=3&&time_set_select!=4)
					||ds1302_time[time_set_select]>1)ds1302_time[time_set_select]--;//date month!=0
					else if(time_set_select<=1)ds1302_time[time_set_select]=59;
					else if(time_set_select==2)ds1302_time[time_set_select]=23;
					else if(time_set_select==3)ds1302_time[time_set_select]=maxDate(ds1302_time[3],ds1302_time[4],ds1302_time[5]);
					else if(time_set_select==4)ds1302_time[time_set_select]=12;
					else if(time_set_select==5)ds1302_time[time_set_select]=99;

					break;
			}
			if(key>=3)timeSet();

计算日期最大值函数:

u8 maxDate(u8 date, u8 month,u8 year){
	if(month==1||month==3||month==5||month==7||month==8||month==10||month==12)return 31;
	else if((month==4||month==6||month==9||month==11)&&date==31)return 30;
	else if(year%4)return 28;
	else if(year%4==0&&year%100)return 28;
	else return 29;
}

有个弊端就是不知道现在在操作哪一位(只能自己记住)。可以利用定时器的定时闪烁,让在被操作的位不停闪烁。(重复写入空格或数值。)

还有就是按下按键,时钟会停走,不松开就一直停着。可以改按键触发条件为按下或弹起的上升沿或下降沿,避免按键处理函数死循环。

红外传感器

遥控器通过红外 LED 发送调制后的信号,开发板上的红外接收模块接收遥控器的红外线。

单工异步,940nm 波长(还有一种 250nm 的N,可见光),EC 通信标准。

image-20230328000312504

38KHz:红外线频率。

IN:发送的方波。

image-20230328001955152

红外接收模块中会自动帮我们滤出 In 部分。

image-20230328002724563

空闲状态:OUT 输出高电平。

发送高电平:OUT 输出高电平。

发送低电平:OUT输出低电平,代表有数据。

image-20230328004940943

38kHz 属于是底层信息,所以协议层不给予展示,类似类的封装。

9ms 低+4.5ms 高:start。

后面跟四个字节数据。反码用于数据验证。

560us 低+560us 高表示0,560us 低+1690us 高表示1. 结束的最后一个高电平后面要跟一个下降沿表示终止。

一直扫描是效率很低的做法,所以 out 是接在外部中断上的。

外部中断

image-20230328115258479

void main(){
    LCD_Init();
    LCD_ShowString(1,1,"A");
    IT0=1;
    IE0=0;
    EX0=1;
    EA=1;
    PX0=1;
    while(1){
        LCD_ShowNum(2,1,num,2);
    }
}

void Int0_Routine() interrupt 0 {
    num++;
}

中断和第三个按键 P3^2 接在一起,因此按下按键3时就会下降沿触发中断。

如果改为低电平触发,即 IT0=0,按下按键就一直触发。

接收到的数据会以2位的二进制位展现在数码管上,这里遥控器上的按钮并不是按几数码管就会显示几的注意一下。重点只是在于看遥控器不同的信号单片机能否区分和识别。

image-20230328145734093

//ired.h
#ifndef _ired_H
#define _ired_H

#include "public.h"

//管脚定义
sbit IRED=P3^2;

//声明变量
extern u8 gired_data[4];


//函数声明
void ired_init(void);

#endif

//ired.c
#include "ired.h"

u8 gired_data[4];//存储4个字节接收码(地址码+地址反码+控制码+控制反码)

/*******************************************************************************
* 函 数 名         : ired_init
* 函数功能		   : 红外端口初始化函数,外部中断0配置 
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
void ired_init(void)
{
	IT0=1;	//下降沿触发
	EX0=1;	//打开中断0允许
	EA=1;	//打开总中断
	IRED=1;	//初始化端口
}

void ired() interrupt 0	//外部中断0服务函数
{
	u8 ired_high_time=0;
	u16 time_cnt=0;
	u8 i=0,j=0;
	//引导信号有9ms的低电平和4.5ms的高电平,先把这两部分读掉,并且如果太长时间引导信号没有发生相应的变化就先跳出,省的系统死机。我们给引导信号10ms和5ms的机会。
	if(IRED==0)
	{
		time_cnt=1000;
		while((!IRED)&&(time_cnt))//等待引导信号9ms低电平结束,若超过10ms强制退出
		{
			delay_10us(1);//延时约10us
			time_cnt--;
			if(time_cnt==0)return;		
		}
		if(IRED)//引导信号9ms低电平已过,进入4.5ms高电平
		{
			time_cnt=500;
			while(IRED&&time_cnt)//等待引导信号4.5ms高电平结束,若超过5ms强制退出
			{
				delay_10us(1);
				time_cnt--;
				if(time_cnt==0)return;	
			}
            //接下来是读取地址、地址反码、控制、控制反码。
			for(i=0;i<4;i++)//循环4次,读取4个字节数据
			{
				for(j=0;j<8;j++)//循环8次读取每位数据即一个字节
				{
					time_cnt=600;
					while((IRED==0)&&time_cnt)//等待数据1或0前面的0.56ms结束,若超过6ms强制退出
					{
						delay_10us(1);
						time_cnt--;
						if(time_cnt==0)return;	
					}
					time_cnt=20;
					while(IRED)//等待数据1或0后面的高电平结束,若超过2ms强制退出
					{
						delay_10us(10);//约0.1ms
						ired_high_time++;
						if(ired_high_time>20)return;	
					}
					gired_data[i]>>=1;//先读取的为低位,然后是高位
					if(ired_high_time>=8)//如果高电平时间大于0.8ms,数据则为1,否则为0
						gired_data[i]|=0x80;//最高位赋1
					ired_high_time=0;//重新清零,等待下一次计算时间
				}
			}
		}
		if(gired_data[2]!=~gired_data[3])//校验控制码与反码,错误则清空后返回
		{
			for(i=0;i<4;i++)
				gired_data[i]=0;
			return;	
		}
	}		
}

main.c:

void main()
{	
	u8 ired_buf[3];

	ired_init();//红外初始化

	while(1)
	{				
		ired_buf[0]=gsmg_code[gired_data[2]/16];//将控制码高4位转换为数码管段码
		ired_buf[1]=gsmg_code[gired_data[2]%16];//将控制码低4位转换为数码管段码
		ired_buf[2]=0X76;//显示H的段码
		smg_display(ired_buf,6);	
	}		
}

LCD1602

liquid crystal display 液晶显示屏,一种字符型液晶显示模块,可以显示 16*2 个字符,每个字符是 5*7 点阵。

image-20230203004049189

P0 P2 会和数码管、LED 一定程度上冲突。

  1. 地。

  2. Vcc。

  3. 调对比度的。

  4. RS:数据指令端。1代表 DB 是数据,0代表是指令。

  5. RW:1读0写。

  6. E:类似时钟的使能。高电平有效,下降沿执行。

  7. DB:并行输入。一个字节长。

  8. BG:背光灯电源。

内部结构图

image-20230221024102411

类似 SMG,想显示1并不是直接输入1,而是操纵数码管右侧一竖被点亮。字模库起的就是这个作用,里面有固定的 ROM 和用户课自定义的 RAM。

DDRAM 长于屏幕,可以通过移平实现滚动效果。

AC address controller,可以自动移位写入数据。

字模库中的数据大多数和 ASCII 码是一样的。

指令

image-20230221195404983

image-20230221195434495

image-20230221195450094

image-20230221200656434

image-20230221200839017

image-20230221201026593

image-20230221201247867

image-20230221201355749

image-20230222162315672

image-20230221205604331

初始化指令

初始化要做哪些操作?

规定显示区域(如8位数据接口,2行显示,5*7点阵,即为0011 10xx,如果取0即为 0x38.)。

显示的模式设置(如开启显示,关闭光标,关闭光标闪烁:0000 1101,即 0x0D)。

进入模式设置(如读写后光标++,屏幕不动:0000 0110,0x06。如果是滚动屏幕则为)。

清屏(0x01)。

显示指令

先设置 DDRAM 初始地址,0x80|AC(开头的8是 DDRAM 固定指令信息不能改。后面的全是0,与 AC 光标位置做与,AC 不同位置的值见 DDRAM 地址表).

然后发送数据。

时序

image-20230221210035721 image-20230221210102393 image-20230221210614193

模块化

第一阶段我们先编写 LCD1602 模块化编程代码,这一部分主要显示静态内容,用于程序编写过程中显示变量进行调试。

目标模块化函数:

image-20230203004415880
//LCD1602.h
#ifndef __LCD1602_H__
#define __LCD1602_H__

#include "reg52.h"
#include "intrins.h"
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_E=P2^7;
#define LCD_DATAPORT P0

void LCD_Delay1ms();		//@11.0592MHz
void LCD_WriteCommand(unsigned char Command);
void LCD_WriteData(unsigned char Data);
void LCD_Init();
void LCD_SetCursor(unsigned char line, unsigned char column);
unsigned int LCD_Pow(unsigned char x, unsigned char y);
void LCD_ShowChar(unsigned char line, unsigned char column, unsigned char c);
void LCD_ShowString(unsigned char line, unsigned char column, unsigned char str[]);
void LCD_ShowNum(unsigned char line, unsigned char column, unsigned int num, unsigned char length);
void LCD_ShowSignedNum(unsigned char line, unsigned char column, int num, unsigned char length);
void LCD_ShowHexNum(unsigned char line, unsigned char column, unsigned int num, unsigned char length);
void LCD_ShowBinNum(unsigned char line, unsigned char column, unsigned int num, unsigned char length);
#endif
//LCD1602.c
#include "LCD1602.H"

void LCD_Delay1ms()		//@11.0592MHz
{
	unsigned char i, j;

	_nop_();
	_nop_();
	_nop_();
	i = 11;
	j = 190;
	do
	{
		while (--j);
	} while (--i);
}

void LCD_WriteCommand(unsigned char Command){
	LCD_RS=0;
	LCD_RW=0;
	LCD_E=0;
	LCD_DATAPORT=Command;
	LCD_Delay1ms();
	LCD_E=1;
	LCD_Delay1ms();
	LCD_E=0;
}

void LCD_WriteData(unsigned char Data){
	LCD_RS=1;
	LCD_RW=0;
	LCD_E=0;
	LCD_DATAPORT=Data;
	LCD_Delay1ms();
	LCD_E=1;
	LCD_Delay1ms();
	LCD_E=0;
}

void LCD_Init(){
	LCD_WriteCommand(0x38);
	LCD_WriteCommand(0x0C);
	LCD_WriteCommand(0x06);
	LCD_WriteCommand(0x01);
}

void LCD_SetCursor(unsigned char line, unsigned char column){
	if(line==1){
		LCD_WriteCommand(0x80|(column-1));
	}
	else{
		LCD_WriteCommand(0x80|(column-1)+0x40);
	}
}

unsigned int LCD_Pow(unsigned char x, unsigned char y){
	unsigned char i=y;
	unsigned int res=1;
	for(;i>0;i--)res*=x;
	return res;
}

void LCD_ShowChar(unsigned char line, unsigned char column, unsigned char c){
	LCD_SetCursor(line, column);
	LCD_WriteData(c);
}

void LCD_ShowString(unsigned char line, unsigned char column, unsigned char str[]){
	unsigned int i=0;
	LCD_SetCursor(line, column);
	while(str[i]!='\0'){
		LCD_WriteData(str[i]);
		i++;
	}
}

void LCD_ShowNum(unsigned char line, unsigned char column, unsigned int num, unsigned char length){
	unsigned char i;
	unsigned base;
	LCD_SetCursor(line, column);
	for(i=length;i>0;i--){
		base=LCD_Pow(10, i-1);
		LCD_WriteData(num/base%10+'0');	
	}
}

void LCD_ShowBinNum(unsigned char line, unsigned char column, unsigned int num, unsigned char length){
	unsigned char i;
	unsigned char base;
	unsigned char single_num;
	LCD_SetCursor(line, column);
	for(i=length;i>0;i--){
		base=LCD_Pow(2, i-1);
		single_num=num/base%2+'0';
		LCD_WriteData(single_num);	
	}
}

void LCD_ShowHexNum(unsigned char line, unsigned char column, unsigned int num, unsigned char length){
	unsigned char i;
	unsigned char base;
	unsigned char single_num;
	LCD_SetCursor(line, column);
	for(i=length;i>0;i--){
		base=LCD_Pow(16, i-1);
		single_num=num/base%16;
		if(single_num<=9)single_num+='0';
		else single_num+='A'-10;
		LCD_WriteData(single_num);	
	}
}

void LCD_ShowSignedNum(unsigned char line, unsigned char column, int num, unsigned char length){
	unsigned char i;
	unsigned base;
	LCD_SetCursor(line, column);
	if(num>0)LCD_WriteData('+');
	else {LCD_WriteData('-');num=-num;}
	for(i=length;i>0;i--){
		base=LCD_Pow(10, i-1);
		LCD_WriteData(num/base%10+'0');	
	}
}

如果希望屏幕滚动,一定时间执行一次LCD_WriteCommand(0x18); 屏幕左移指令。

直流电机

电能转机械能。

image-20230327001703813

第二种驱动方式可以双向,电机可以双向驱动,转向不同。

直接驱动电机还起到电感的作用,断开电源后电机产生电压,可以形成一个回路慢慢消耗掉。

PWM:脉冲信号调制。

image-20230327003516983

比如调 LED 灯,我们可以加一个有电位器的电阻(滑动变阻器)。

电机可能这种方式有局限性,比如电阻太大直接不转,驱动不起来,太小烧毁。

脉冲调制比如:“转2s”“停1s”“转2s”“停1s”……因为电机有惯性,所以可行。

示例:LED 流水灯:

sbit LED=P2^0;

void delay(unsigned char i)		//@11.0592MHz
{
	while (--i);
}

void main(){
 	unsigned char period=100;
	unsigned char ti;
	unsigned char i;
	while(1){
		for(ti=0;ti<100;ti++){
			for(i=0;i<20;i++){
				LED=0;
				delay(ti);
				LED=1;
				delay(period-ti);
			}
		}
		for(ti=100;ti>0;ti--){
			for(i=0;i<20;i++){
				LED=0;
				delay(ti);
				LED=1;
				delay(period-ti);
		}
		}	
	}
}

可以利用 timer 来计数,我们知道 timer 是一直增加的,我们可以设置一个比较值,当 timer 大于比较值时输出1,小于时输出0类似这样。

#include "regx52.h"
#include "delay.h"
#include "nixie.h"
#include "timer0.h"
#include "key.h"

sbit LED=P1^0 ;

unsigned char compare;
unsigned int T0Count;

void main(){
	unsigned char key;
	unsigned char level=1;
	time0Init();
	compare=5;
	while(1){
		key=key_scan(0);
		if(key==KEY1_PRESS)level++;
		else if(key==KEY2_PRESS)if(level>1)level--;
		level%=5;
		if(level==0)level++;
		if(level==1)compare=0;
		else if(level==2)compare=50;
		else if(level==3)compare=75;
		else if(level==4)compare=100;
		Nixie_Scan(1,level);	
	}
}

void Timer0_Routine() interrupt 1
{
  TL0 = 0xAE;
  TH0 = 0xFB;
  T0Count++;
  if(T0Count>=100)
  {
    T0Count=0;
  }
  if(T0Count<compare){
  	 LED=1;
  }
  else if(T0Count>compare)LED=0;
}

ADC

使得调节开发板上的电位器时,数码管上能够显示 AD 模块 采集电位器的电压值且随之变化。

开发板上有三个应用:光敏电阻,热敏电阻,电位器。

一般 AD 转换有多个输入,提高使用效率。

image-20230327170005984

ADC 通过地址锁存与译码判断采用哪个输入。

image-20230327172256586

运算放大器,可以作为电压比较器、同相反相放大器、电压跟随器

T 型电阻网络 DA 转换器:

image-20230327195138979

低通滤波器:输入是有直交流两个分量的,可以通过低通滤波器提取出直流。电压跟随器让驱动能力增加。

image-20230327195154405

da 简单些,因为d值是固定的,根据d调整a即可。

ad 怎么判断电压大小?我们用一个电压值和给定电压作比较,看大于还是小于,逐渐逼近来找近似值。

分辨率:精细程度。比如8位的 ad 可以把 5v 转换到 0~255 范围。

转换速度:最大采样/建立频率。

XPT2046 采用 SPI,的时序在上升沿输入,下降沿输出,可实现输入再输出。

image-20230327213950578

采用单端模式(触摸屏查分会更好一些)。

PD1=1 采用内部参考电压,内部电压为 2.5v,我们知道adc 映射范围是 0~5v,所以1采用内部电压不如0采用5v的外部电压。

A 地址配置:

image-20230327220710512

VBAT:电池电压。

AUX:辅助电压。

XP YP:XY 正极。

读取指令并 ad 转化:

#define XPT2046_XP_8 0x9C    // 1001 1100
#define XPT2046_YP_8 0xDC    // 1101 1100
#define XPT2046_VBAT_8 0xAC  // 1010 1100
#define XPT2046_AUX_8 0xEC   // 1110 1100
#define XPT2046_XP_12 0x94   // 1001 0100
#define XPT2046_YP_12 0xD4   // 1101 0100
#define XPT2046_VBAT_12 0xA4 // 1010 0100
#define XPT2046_AUX_12 0xE4  // 1110 0100

unsigned int XPT2046_ReadAD(unsigned char command)
{
    unsigned char i;
    unsigned char temp=command;
    unsigned int ADValue = 0;
    XPT2046_DCLK = 0;
    XPT2046_CS = 0;
    for (i = 0; i < 8; i++)
    {
        XPT2046_DIN = temp >> 7;
        temp <<= 1;
        XPT2046_DCLK = 1;
        XPT2046_DCLK = 0;
    }

    for (i = 0; i < 12; i++)
    {
        XPT2046_DCLK = 1;
        XPT2046_DCLK = 0;
        if (XPT2046_DOUT)
            ADValue |= (0x0800 >> i);
    }
    XPT2046_CS = 1;
    //for 12 bit:
    if(command&0x08)return ADValue>>4;
    else return ADValue;
}
void main()
{
	unsigned int ADValue = 0;
	LCD_Init();
	LCD_ShowString(1, 1, "ADC NTC RG");
	while (1)
	{
		ADValue = XPT2046_ReadAD(XPT2046_XP_8);//电位器
		LCD_ShowNum(2, 1, ADValue, 3);
		ADValue = XPT2046_ReadAD(XPT2046_YP_8);//热敏电阻
		LCD_ShowNum(2, 5, ADValue, 3);
		ADValue = XPT2046_ReadAD(XPT2046_VBAT_8);//光敏电阻
		LCD_ShowNum(2, 9, ADValue, 3);
		delayMs(10);
	}
}

dac 也是采用 pwm 的原理,改变01 叫错频率来模拟灯的亮度(输出口:P2^1)。所以 dac 用处不广泛,因为可以被代替。

完结:库函数封装说明

从12月开始决定入嵌入式开始,经历了5个月的学习,终于把入门模块基本弄懂并且跟着代码敲了一遍。

在这期间跟随普中课程老师学习到的封装习惯让我主键开始留意每做完一个模块后就进行调试并把该模块封装起来,供下次使用该模块时可以快捷导入。

后来浅了解了一些 git 的项目管理后,我又把封装好的函数发布到 github repository 上。

单片机版本:STC89C52RC

github 仓库地址:Jingqing3948/MySTC89C52RCFunction: 学习 STC89C52RC 单片机时的一些封装好的库函数。 (github.com)

函数包含:

image-20230328184524152

AT24C02:掉电不丢失存储器。

DS18B20:温度传感器。

delay:stc-isp 生成的基于 11.0592MHz 晶振的延时函数。

IIC:IIC 总线的使用,配合 AT24C02 模块使用。

IR:红外遥控模块,包括外部中断、定时器等内容。

Key:四个独立按键。

LCD1602:LCD1602 显示屏。

NiXie:数码管。

Timer:定时器中断函数。

public.h:无用,定义了 u8 u16 两个变量。

有问题欢迎随时与博主沟通。如侵犯他人权益会尽快删除!

  • 46
    点赞
  • 360
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 20
    评论
基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例 基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例 基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例 基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例 基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例基于51单片机开发的小程序,学习资料,用于学习51单片机的嵌入式开发,应用案例
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灰海宽松

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值