51单片机学习(下)

本节是C51单片机系列学习的最后一节,本节的重点是I2C总线通信,数模转换与模数转换。I2C与串口通信一样,在MCU通信里占据非常大的一部分。基本上算是通过C51系列单片机学完了的一些基本操作与概念,回顾这段时间的学习,收获最大部分还是学会看原理图、GPIO、定时器、中断资源、串口通信、I2C总线、AD/DA(PWM)、还有各种芯片模块等内容,后面会通过STC 89C51做一些小型项目,或者继续学习STM32吧~

I2C 总线

I2C是Inter-Integrated Circuit的简称,I2C是由Philips公司开发的一种通用数据总线,其中包括两根通信线:串行数据线:SDA,及串行时钟线:SCL。I2C通信的主要特征是同步、半双工,并且带数据应答。通用的I2C总线,可以使各种设备的通信标准统一,对于厂家来说,使用成熟的方案可以缩短芯片设计周期、提高稳定性,对于应用者来说,使用通用的通信协议可以避免学习各种各样的自定义协议,降低了学习和应用的难度。

I2C主要应用场景就是SOC和周边外设间的通信(如:EEPROM,电容触摸芯片,各种Sensor等)。

I2C电路规范

所有I2C设备的SCL连在一起,SDA连在一起;设备的SCL和SDA均要配置成开漏输出模式;SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右;开漏输出和上拉电阻的共同作用实现了“线与”的功能,此设计主要是为了解决多机通信互相干扰的问题。下面是I2C总线挂接多个设备示意图:

I2C总线内部结构图:

I2C使用一个7bit的设备地址,一组总线最多和112个节点通信。最大通信数量受限于地址空间及400pF的总线电容。常见的I2C总线以传输速率的不同分为不同的模式:标准模式(100Kbit/s)、低速模式(10Kbit/s)、快速模式(400Kbit/s)、高速模式(3.4Mbit/s),时钟频率可以被下降到零,即暂停通信。

该总线是一种多主控总线,即可以在总线上放置多个主设备节点,在停止位(P)发出后,即通讯结束后,主设备节点可以成为从设备节点。主设备节点 就是产生时钟并发起通信的设备节点;从设备节点 就是接收时钟并响应主设备节点寻址的设备节点。

总的来说I2C通信的主要特点如下:

1、I2C通信双方地位不对等,通信由主设备发起,并主导传输过程,从设备按I2C协议接收主设备发送的数据,并及时给出响应。

2、主设备、从设备由通信双方决定,既能当主设备,也能当从设备(需要软件进行配置)。

3、主设备负责调度总线,决定某一时刻和哪个从设备通信。在同一时刻,I2C总线上只能有一对主设备、从设备通信。

4、每个I2C从设备在I2C总线通讯中有一个I2C从设备地址,该地址具有唯一性,是从设备的固有属性,通信中主设备通过从设备地址来找到从设备,可以理解为网卡Mac地址。

I2C时序结构

起始条件:SCL高电平期间,SDA从高电平切换到低电平。

终止条件:SCL高电平期间,SDA从低电平切换到高电平。

发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位在前),然后拉高SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。

接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位在前),然后拉高SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)。

发送应答:在接收完一个字节之后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答

接收应答:在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)。

I2C数据帧

发送一帧数据,解决的问题是向谁发什么:

接收一帧数据,向谁收什么:

先发送再接收数据帧(复合格式),解决的问题是向谁收指定的什么:

C51的I2C操作封装

I2C.h

 1#ifndef __I2C_H__
 2#define __I2C_H__
 3
 4void I2C_Start(void);
 5void I2C_Stop(void);
 6void I2C_SendByte(unsigned char Byte);
 7unsigned char I2C_ReceiveByte(void);
 8void I2C_SendAck(unsigned char AckBit);
 9unsigned char I2C_ReceiveAck(void);
10
11#endif

I2C.c

 1#include <REGX52.H>
 2
 3sbit I2C_SCL=P2^1;
 4sbit I2C_SDA=P2^0;
 5
 6/**
 7  * @brief  I2C开始
 8  * @param  无
 9  * @retval 无
10  */
11void I2C_Start(void)
12{
13	I2C_SDA=1;
14	I2C_SCL=1;
15	I2C_SDA=0;
16	I2C_SCL=0;
17}
18
19/**
20  * @brief  I2C停止
21  * @param  无
22  * @retval 无
23  */
24void I2C_Stop(void)
25{
26	I2C_SDA=0;
27	I2C_SCL=1;
28	I2C_SDA=1;
29}
30
31/**
32  * @brief  I2C发送一个字节
33  * @param  Byte 要发送的字节
34  * @retval 无
35  */
36void I2C_SendByte(unsigned char Byte)
37{
38	unsigned char i;
39	for(i=0;i<8;i++)
40	{
41		I2C_SDA=Byte&(0x80>>i);
42		I2C_SCL=1;
43		I2C_SCL=0;
44	}
45}
46
47/**
48  * @brief  I2C接收一个字节
49  * @param  无
50  * @retval 接收到的一个字节数据
51  */
52unsigned char I2C_ReceiveByte(void)
53{
54	unsigned char i,Byte=0x00;
55	I2C_SDA=1;
56	for(i=0;i<8;i++)
57	{
58		I2C_SCL=1;
59		if(I2C_SDA){Byte|=(0x80>>i);}
60		I2C_SCL=0;
61	}
62	return Byte;
63}
64
65/**
66  * @brief  I2C发送应答
67  * @param  AckBit 应答位,0为应答,1为非应答
68  * @retval 无
69  */
70void I2C_SendAck(unsigned char AckBit)
71{
72	I2C_SDA=AckBit;
73	I2C_SCL=1;
74	I2C_SCL=0;
75}
76
77/**
78  * @brief  I2C接收应答位
79  * @param  无
80  * @retval 接收到的应答位,0为应答,1为非应答
81  */
82unsigned char I2C_ReceiveAck(void)
83{
84	unsigned char AckBit;
85	I2C_SDA=1;
86	I2C_SCL=1;
87	AckBit=I2C_SDA;
88	I2C_SCL=0;
89	return AckBit;
90}

AT24C02存储器

存储器分类

AT24C02简介

AT24C02是一种可以实现掉电不丢失的存储器,可用于保存单片机运行时想要永久保存的数据信息,其存储介质:E2PROM。通讯接口是I2C总线,容量256字节。

存储器简化模型:

AT24C02引脚及应用电路:

AT24C02数据帧

字节写:在WORD ADDRESS处写入数据DATA

随机读:读出在WORD ADDRESS处的数据DATA

AT24C02的固定地址为1010,可配置地址本开发板上为000。所以SLAVE ADDRESS+W为0xA0,SLAVE ADDRESS+R为0xA1。

字节写:在"字地址"处写入"数据"

随机读:读出在"字地址"处的"数据"

I2C操作AT24C02示例

依托于之前封装的I2C的代码,下面是AT24C02.h

1#ifndef __AT24C02_H__
2#define __AT24C02_H__
3
4void AT24C02_WriteByte(unsigned char WordAddress,Data);
5unsigned char AT24C02_ReadByte(unsigned char WordAddress);
6
7
8#endif

AT24C02.c

 1#include <REGX52.H>
 2#include "I2C.h"
 3
 4#define AT24C02_ADDRESS		0xA0
 5
 6/**
 7  * @brief  AT24C02写入一个字节
 8  * @param  WordAddress 要写入字节的地址
 9  * @param  Data 要写入的数据
10  * @retval 无
11  */
12void AT24C02_WriteByte(unsigned char WordAddress,Data)
13{
14	I2C_Start();
15	I2C_SendByte(AT24C02_ADDRESS);
16	I2C_ReceiveAck();
17	I2C_SendByte(WordAddress);
18	I2C_ReceiveAck();
19	I2C_SendByte(Data);
20	I2C_ReceiveAck();
21	I2C_Stop();
22}
23
24/**
25  * @brief  AT24C02读取一个字节
26  * @param  WordAddress 要读出字节的地址
27  * @retval 读出的数据
28  */
29unsigned char AT24C02_ReadByte(unsigned char WordAddress)
30{
31	unsigned char Data;
32	I2C_Start();
33	I2C_SendByte(AT24C02_ADDRESS);
34	I2C_ReceiveAck();
35	I2C_SendByte(WordAddress);
36	I2C_ReceiveAck();
37	I2C_Start();
38	I2C_SendByte(AT24C02_ADDRESS|0x01);
39	I2C_ReceiveAck();
40	Data=I2C_ReceiveByte();
41	I2C_SendAck(1);
42	I2C_Stop();
43	return Data;
44}

main.c

 1#include <REGX51.H>
 2#include "Delay.h"
 3#include "LCD1602.h"
 4#include "AT24C02.h"
 5
 6void main(void){
 7	unsigned char retData;
 8	
 9	LCD_Init();
10	LCD_ShowString(1, 1, "Hello I2C");
11	// 写入
12	AT24C02_WriteByte(1, 188);
13	// 写入后等待5毫秒,相当于同步的IO等待
14	Delay(5); 
15	// 读取
16	retData = AT24C02_ReadByte(1);
17	LCD_ShowNum(2, 1, retData, 3);
18	while(1){
19		
20	}
21}

温度传感器

DS18B02

DS18B20是一种常见的数字温度传感器,其控制命令和数据都是以数字信号的方式输入输出,相比较于模拟温度传感器,具有功能强大、硬件简单、易扩展、抗干扰性强等特点。其测温范围是-55°C 到 +125°C,通信方式为1-Wire(单总线),可形成总线结构、内置温度报警功能、可寄生供电。

DS18B02的内部结构框图:

64-BIT ROM:作为器件地址,用于总线通信的寻址

SCRATCHPAD(暂存器):用于总线的数据交互

EEPROM:用于保存温度触发阈值和配置参数

DS18B02的存储器结构如下,如果只是读取温度的话那么只需要前两个字节就好了:

单总线(1-Wire BUS)

单总线(1-Wire BUS)是由Dallas公司开发的一种通用数据总线,它只有一根通信线:DQ。通信是异步、半双工的方式。单总线只需要一根通信线即可实现数据的双向传输,当采用寄生供电时,还可以省去设备的VDD线路,此时,供电加通信只需要DQ和GND两根线。

单总线线路规范:设备的DQ均要配置成开漏输出模式,DQ添加一个上拉电阻,阻值一般为4.7KΩ左右,若此总线的从机采取寄生供电,则主机还应配一个强上拉输出电路。

单总线时序结构

初始化:主机将总线拉低至少480us,然后释放总线,等待15~60us后,存在的从机会拉低总线60~240us以响应主机,之后从机将释放总线。

发送一位:主机将总线拉低60~120us,然后释放总线,表示发送0;主机将总线拉低1~15us,然后释放总线,表示发送1。从机将在总线拉低30us后(典型值)读取电平,整个时间片应大于60us

接收一位:主机将总线拉低1~15us,然后释放总线,并在拉低后15us内读取总线电平(尽量贴近15us的末尾),读取为低电平则为接收0,读取为高电平则为接收1 ,整个时间片应大于60us

发送一个字节:连续调用8次发送一位的时序,依次发送一个字节的8位(低位在前)

接收一个字节:连续调用8次接收一位的时序,依次接收一个字节的8位(低位在前)

C51单总线操作封装

OneWire.c

 1#include <REGX52.H>
 2
 3//引脚定义
 4sbit OneWire_DQ=P3^7;
 5
 6/**
 7  * @brief  单总线初始化
 8  * @param  无
 9  * @retval 从机响应位,0为响应,1为未响应
10  */
11unsigned char OneWire_Init(void)
12{
13	unsigned char i;
14	unsigned char AckBit;
15	OneWire_DQ=1;
16	OneWire_DQ=0;
17	i = 247;while (--i);		//Delay 500us
18	OneWire_DQ=1;
19	i = 32;while (--i);			//Delay 70us
20	AckBit=OneWire_DQ;
21	i = 247;while (--i);		//Delay 500us
22	return AckBit;
23}
24
25/**
26  * @brief  单总线发送一位
27  * @param  Bit 要发送的位
28  * @retval 无
29  */
30void OneWire_SendBit(unsigned char Bit)
31{
32	unsigned char i;
33	OneWire_DQ=0;
34	i = 4;while (--i);			//Delay 10us
35	OneWire_DQ=Bit;
36	i = 24;while (--i);			//Delay 50us
37	OneWire_DQ=1;
38}
39
40/**
41  * @brief  单总线接收一位
42  * @param  无
43  * @retval 读取的位
44  */
45unsigned char OneWire_ReceiveBit(void)
46{
47	unsigned char i;
48	unsigned char Bit;
49	OneWire_DQ=0;
50	i = 2;while (--i);			//Delay 5us
51	OneWire_DQ=1;
52	i = 2;while (--i);			//Delay 5us
53	Bit=OneWire_DQ;
54	i = 24;while (--i);			//Delay 50us
55	return Bit;
56}
57
58/**
59  * @brief  单总线发送一个字节
60  * @param  Byte 要发送的字节
61  * @retval 无
62  */
63void OneWire_SendByte(unsigned char Byte)
64{
65	unsigned char i;
66	for(i=0;i<8;i++)
67	{
68		OneWire_SendBit(Byte&(0x01<<i));
69	}
70}
71
72/**
73  * @brief  单总线接收一个字节
74  * @param  无
75  * @retval 接收的一个字节
76  */
77unsigned char OneWire_ReceiveByte(void)
78{
79	unsigned char i;
80	unsigned char Byte=0x00;
81	for(i=0;i<8;i++)
82	{
83		if(OneWire_ReceiveBit()){Byte|=(0x01<<i);}
84	}
85	return Byte;
86}

OneWire.h

 1#ifndef __ONEWIRE_H__
 2#define __ONEWIRE_H__
 3
 4unsigned char OneWire_Init(void);
 5void OneWire_SendBit(unsigned char Bit);
 6unsigned char OneWire_ReceiveBit(void);
 7void OneWire_SendByte(unsigned char Byte);
 8unsigned char OneWire_ReceiveByte(void);
 9
10#endif

DS18B20操作流程

初始化:从机复位,主机判断从机是否响应

ROM操作:ROM指令+本指令需要的读写操作

功能操作:功能指令+本指令需要的读写操作

DS18B20数据帧

温度变换:初始化→跳过ROM →开始温度变换

温度读取:初始化→跳过ROM →读暂存器→连续的读操作

读取DS18B20示例

代码基于单总线的代码: DS18B20.h

1#ifndef __DS18B20_H__
2#define __DS18B20_H__
3
4void DS18B20_ConvertT(void);
5float DS18B20_ReadT(void);
6
7#endif

DS18B20.c

 1#include <REGX52.H>
 2#include "OneWire.h"
 3
 4//DS18B20指令
 5#define DS18B20_SKIP_ROM			0xCC
 6#define DS18B20_CONVERT_T			0x44
 7#define DS18B20_READ_SCRATCHPAD 	0xBE
 8
 9/**
10  * @brief  DS18B20开始温度变换
11  * @param  无
12  * @retval 无
13  */
14void DS18B20_ConvertT(void)
15{
16	OneWire_Init();
17	OneWire_SendByte(DS18B20_SKIP_ROM);
18	OneWire_SendByte(DS18B20_CONVERT_T);
19}
20
21/**
22  * @brief  DS18B20读取温度
23  * @param  无
24  * @retval 温度数值
25  */
26float DS18B20_ReadT(void)
27{
28	unsigned char TLSB,TMSB;
29	int Temp;
30	float T;
31	OneWire_Init();
32	OneWire_SendByte(DS18B20_SKIP_ROM);
33	OneWire_SendByte(DS18B20_READ_SCRATCHPAD);
34	TLSB=OneWire_ReceiveByte();
35	TMSB=OneWire_ReceiveByte();
36	Temp=(TMSB<<8)|TLSB;
37	T=Temp/16.0;
38	return T;
39}

LCD1602液晶屏

LCD1602简介

LCD1602(Liquid Crystal Display)液晶显示屏是一种字符型液晶显示模块,可以显示ASCII码的标准字符和其它的一些内置特殊字符,还可以有8个自定义字符,其显示容量为16×2个字符,每个字符为5*7点阵。引脚及应用电路如下图所示:

LCD1602内部结构框图:

LCD1602显示模块时序与参数图:

LCD1602操作流程

初始化:

发送指令0x38 //八位数据接口,两行显示,5*7点阵

发送指令0x0C //显示开,光标关,闪烁关

发送指令0x06 //数据读写操作后,光标自动加一,画面不动

发送指令0x01 //清屏

显示字符:

发送指令0x80|AC //设置光标位置

发送数据 //发送要显示的字符数据

发送数据 //发送要显示的字符数据

……

LCD1602操作封装

LCD1602.h

 1#ifndef __LCD1602_H__
 2#define __LCD1602_H__
 3
 4//用户调用函数:
 5void LCD_Init();
 6void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char);
 7void LCD_ShowString(unsigned char Line,unsigned char Column,char *String);
 8void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
 9void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
10void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
11void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
12
13#endif

LCD1602.c

  1#include <REGX52.H>
  2
  3//引脚配置:
  4sbit LCD_RS=P2^6;
  5sbit LCD_RW=P2^5;
  6sbit LCD_EN=P2^7;
  7#define LCD_DataPort P0
  8
  9//函数定义:
 10/**
 11  * @brief  LCD1602延时函数,12MHz调用可延时1ms
 12  * @param  无
 13  * @retval 无
 14  */
 15void LCD_Delay()
 16{
 17	unsigned char i, j;
 18
 19	i = 2;
 20	j = 239;
 21	do
 22	{
 23		while (--j);
 24	} while (--i);
 25}
 26
 27/**
 28  * @brief  LCD1602写命令
 29  * @param  Command 要写入的命令
 30  * @retval 无
 31  */
 32void LCD_WriteCommand(unsigned char Command)
 33{
 34	LCD_RS=0;
 35	LCD_RW=0;
 36	LCD_DataPort=Command;
 37	LCD_EN=1;
 38	LCD_Delay();
 39	LCD_EN=0;
 40	LCD_Delay();
 41}
 42
 43/**
 44  * @brief  LCD1602写数据
 45  * @param  Data 要写入的数据
 46  * @retval 无
 47  */
 48void LCD_WriteData(unsigned char Data)
 49{
 50	LCD_RS=1;
 51	LCD_RW=0;
 52	LCD_DataPort=Data;
 53	LCD_EN=1;
 54	LCD_Delay();
 55	LCD_EN=0;
 56	LCD_Delay();
 57}
 58
 59/**
 60  * @brief  LCD1602设置光标位置
 61  * @param  Line 行位置,范围:1~2
 62  * @param  Column 列位置,范围:1~16
 63  * @retval 无
 64  */
 65void LCD_SetCursor(unsigned char Line,unsigned char Column)
 66{
 67	if(Line==1)
 68	{
 69		LCD_WriteCommand(0x80|(Column-1));
 70	}
 71	else if(Line==2)
 72	{
 73		LCD_WriteCommand(0x80|(Column-1+0x40));
 74	}
 75}
 76
 77/**
 78  * @brief  LCD1602初始化函数
 79  * @param  无
 80  * @retval 无
 81  */
 82void LCD_Init()
 83{
 84	LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵
 85	LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关
 86	LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动
 87	LCD_WriteCommand(0x01);//光标复位,清屏
 88}
 89
 90/**
 91  * @brief  在LCD1602指定位置上显示一个字符
 92  * @param  Line 行位置,范围:1~2
 93  * @param  Column 列位置,范围:1~16
 94  * @param  Char 要显示的字符
 95  * @retval 无
 96  */
 97void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
 98{
 99	LCD_SetCursor(Line,Column);
100	LCD_WriteData(Char);
101}
102
103/**
104  * @brief  在LCD1602指定位置开始显示所给字符串
105  * @param  Line 起始行位置,范围:1~2
106  * @param  Column 起始列位置,范围:1~16
107  * @param  String 要显示的字符串
108  * @retval 无
109  */
110void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
111{
112	unsigned char i;
113	LCD_SetCursor(Line,Column);
114	for(i=0;String[i]!='\0';i++)
115	{
116		LCD_WriteData(String[i]);
117	}
118}
119
120/**
121  * @brief  返回值=X的Y次方
122  */
123int LCD_Pow(int X,int Y)
124{
125	unsigned char i;
126	int Result=1;
127	for(i=0;i<Y;i++)
128	{
129		Result*=X;
130	}
131	return Result;
132}
133
134/**
135  * @brief  在LCD1602指定位置开始显示所给数字
136  * @param  Line 起始行位置,范围:1~2
137  * @param  Column 起始列位置,范围:1~16
138  * @param  Number 要显示的数字,范围:0~65535
139  * @param  Length 要显示数字的长度,范围:1~5
140  * @retval 无
141  */
142void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
143{
144	unsigned char i;
145	LCD_SetCursor(Line,Column);
146	for(i=Length;i>0;i--)
147	{
148		LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
149	}
150}
151
152/**
153  * @brief  在LCD1602指定位置开始以有符号十进制显示所给数字
154  * @param  Line 起始行位置,范围:1~2
155  * @param  Column 起始列位置,范围:1~16
156  * @param  Number 要显示的数字,范围:-32768~32767
157  * @param  Length 要显示数字的长度,范围:1~5
158  * @retval 无
159  */
160void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
161{
162	unsigned char i;
163	unsigned int Number1;
164	LCD_SetCursor(Line,Column);
165	if(Number>=0)
166	{
167		LCD_WriteData('+');
168		Number1=Number;
169	}
170	else
171	{
172		LCD_WriteData('-');
173		Number1=-Number;
174	}
175	for(i=Length;i>0;i--)
176	{
177		LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
178	}
179}
180
181/**
182  * @brief  在LCD1602指定位置开始以十六进制显示所给数字
183  * @param  Line 起始行位置,范围:1~2
184  * @param  Column 起始列位置,范围:1~16
185  * @param  Number 要显示的数字,范围:0~0xFFFF
186  * @param  Length 要显示数字的长度,范围:1~4
187  * @retval 无
188  */
189void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
190{
191	unsigned char i,SingleNumber;
192	LCD_SetCursor(Line,Column);
193	for(i=Length;i>0;i--)
194	{
195		SingleNumber=Number/LCD_Pow(16,i-1)%16;
196		if(SingleNumber<10)
197		{
198			LCD_WriteData(SingleNumber+'0');
199		}
200		else
201		{
202			LCD_WriteData(SingleNumber-10+'A');
203		}
204	}
205}
206
207/**
208  * @brief  在LCD1602指定位置开始以二进制显示所给数字
209  * @param  Line 起始行位置,范围:1~2
210  * @param  Column 起始列位置,范围:1~16
211  * @param  Number 要显示的数字,范围:0~1111 1111 1111 1111
212  * @param  Length 要显示数字的长度,范围:1~16
213  * @retval 无
214  */
215void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
216{
217	unsigned char i;
218	LCD_SetCursor(Line,Column);
219	for(i=Length;i>0;i--)
220	{
221		LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
222	}
223}

直流电机驱动

直流电机是一种将电能转换为机械能的装置。一般的直流电机有两个电极,当电极正接时,电机正转,当电极反接时,电机反转。直流电机主要由永磁体(定子)、线圈(转子)和换向器组成。除直流电机外,常见的电机还有步进电机、舵机、无刷电机、空心杯电机等。

电机驱动模块的驱动电路:

PWM 脉冲调制

PWM(Pulse Width Modulation)即脉冲宽度调制,在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速、开关电源等领域。

这种调制技术可用于对信息进行编码以进行传输。它也常用于控制向电气设备(尤其是电机等惯性负载)提供的电源。通过高速打开和关闭电源和负载之间的开关,可以控制提供给负载的电压(和电流)的平均值。与关闭时间相比,开启时间越长,提供给负载的电压(和电流)就越高。

占空比是指“开启”时间与固定间隔或“周期”的比率。低占空比对应于低功率,因为​​大部分时间都处于关闭状态。占空比通常以百分比表示,100% 表示开关完全打开。根据 PWM 波形的占空比,有一个如图所示的平均值。t_on小,平均值低,如果大,平均值高。通过控制占空比就可以控制这个平均值。在使用 PWM 时,我们通常关心的是这个平均值。

产生PWM方法

下面来实现一个LED呼吸灯

main.c

 1#include <REGX52.H>
 2
 3sbit LED=P2^0;
 4
 5void Delay(unsigned int t)
 6{
 7	while(t--);
 8}
 9
10void main()
11{
12	unsigned char Time,i;
13	while(1)
14	{
15		for(Time=0;Time<100;Time++)		// 改变亮灭时间,由暗到亮
16		{
17			for(i=0;i<20;i++)			// 计次延时
18			{
19				LED=0;					// LED亮
20				Delay(Time);			// 延时Time
21				LED=1;					// LED灭
22				Delay(100-Time);		// 延时100-Time
23			}
24		}
25		for(Time=100;Time>0;Time--)		// 改变亮灭时间,由亮到暗
26		{
27			for(i=0;i<20;i++)			// 计次延时
28			{
29				LED=0;					// LED亮
30				Delay(Time);			// 延时Time
31				LED=1;					// LED灭
32				Delay(100-Time);		// 延时100-Time
33			}
34		}
35	}
36}

下面通过比较值的方法实现电机调速

main.c

 1#include <REGX52.H>
 2#include "Delay.h"
 3#include "Key.h"
 4#include "Nixie.h"
 5#include "Timer0.h"
 6
 7sbit Motor=P1^0;
 8
 9unsigned char Counter,Compare;	// 计数值和比较值,用于输出PWM
10unsigned char KeyNum,Speed;
11
12void main()
13{
14	Timer0_Init();
15	while(1)
16	{
17		KeyNum=Key();
18		if(KeyNum==1)
19		{
20			Speed++;
21			Speed%=4;
22			if(Speed==0){Compare=0;}	// 设置比较值,改变PWM占空比
23			if(Speed==1){Compare=50;}
24			if(Speed==2){Compare=75;}
25			if(Speed==3){Compare=100;}
26		}
27		Nixie(1,Speed);
28	}
29}
30
31void Timer0_Routine() interrupt 1
32{
33	TL0 = 0x9C;		// 设置定时初值
34	TH0 = 0xFF;		// 设置定时初值
35	Counter++;
36	Counter%=100;	// 计数值变化范围限制在0~99
37	if(Counter<Compare)	// 计数值小于比较值
38	{
39		Motor=1;		// 输出1
40	}
41	else				// 计数值大于比较值
42	{
43		Motor=0;		// 输出0
44	}
45}
46

AD/DA

AD(Analog to Digital):模拟-数字转换,将模拟信号转换为计算机可操作的数字信号;

DA(Digital to Analog):数字-模拟转换,将计算机输出的数字信号转换为模拟信号;

AD/DA转换打开了计算机与模拟信号的大门,极大的提高了计算机系统的应用范围,也为模拟信号数字化处理提供了可能。

AD转换通常有多个输入通道,用多路选择开关连接至AD转换器,以实现AD多路复用的目的,提高硬件利用率。

AD/DA与单片机数据传送可使用并口(速度快、原理简单),也可使用串口(接线少、使用方便)。

可将AD/DA模块直接集成在单片机内,这样直接写入/读出寄存器就可进行AD/DA转换,单片机的IO口可直接复用为AD/DA的通道。下面是硬件电路图:

运算放大器

运算放大器是具有很高放大倍数的放大电路单元。内部集成了差分放大器、电压放大器、功率放大器三级放大电路,是一个性能完备、功能强大的通用放大电路单元,由于其应用十分广泛,现已作为基本的电路元件出现在电路图中。运算放大器可构成的电路有:电压比较器、反相放大器、同相放大器、电压跟随器、加法器、积分器、微分器等,运算放大器电路的分析方法:虚短、虚断(负反馈条件下)。

下面是用T型电阻网络DA转换器展示的AD演示图:

PWM型DA转换器:

逐次逼近型AD转换器:

AD/DA的性能指标

分辨率:指AD/DA数字量的精细程度,通常用位数表示。例如,对于5V电源系统来说,8位的AD可将5V等分为256份,即数字量变化最小一个单位时,模拟量变化5V/256=0.01953125V,所以,8位AD的电压分辨率为0.01953125V,AD/DA的位数越高,分辨率就越高。

转换速度:表示AD/DA的最大采样/建立频率,通常用转换频率或者转换时间来表示,对于采样/输出高速信号,应注意AD/DA的转换速度。

红外遥控

红外遥控简介

红外遥控是利用红外光进行通信的设备,由红外LED将调制后的信号发出,由专用的红外接收头进行解调输出。其通信方式为单工,异步。红外LED波长为940nm,通信协议标准为NEC。

由于红外线遥控不具有像无线电遥控那样穿过障碍物去控制被控对象的能力,所以在设计红外线遥控器时,不必要像无线电遥控器那样,每套(发射器和接收器)要有不同的遥控频率或编码否则就会隔墙控制或干扰邻居的家用电器),所以同类产品的红外线遥控器,可以有相同的遥控频率或编码,而不会出现遥控信号“串门”的情况。这对于大批量生产以及在家用电器上普及红外线遥控提供了极大的方便。由于红外线为不可见光,因此对环境影响很小,再由红外光波动波长远小于无线电波的波长,所以红外线遥控不会影响其他家用电器,也不会影响临近的无线电设备。红外遥控通信系统一般由红外发射装置和红外接收设备两大部分组成。

基本发送与接收:

空闲状态:红外LED不亮,接收头输出高电平

发送低电平:红外LED以38KHz频率闪烁发光,接收头输出低电平

发送高电平:红外LED不亮,接收头输出高电平

NEC编码(12MHz晶振),NEC遥控指令的数据格式为:引导码、地址码、地址反码、控制码、控制反码。引导码由一个9ms的低电平和一个 4.5ms的高电平组成,地址码、地址反码、控制码、控制反码均是8位数据格式。按照低位在前,高位在后的顺序发送。采用反码是为了增加传输的可靠性(可用于校验)。数据格式如下:

NEC码还规定了连发码(由9ms 低电平+2.5m高电平+0.56ms 低电平+97.94ms高电平组成),如果在一帧数据发送完毕之后,红外遥控器按键仍然没有放开,则发射连发码,可以通过统计连发码的次数来标记按键按下的长短或次数。至于如何解码呢?我们可以使用状态机来解决这个问题。

外部中断

STC89C52有4个外部中断,STC89C52的外部中断有两种触发方式:下降沿触发低电平触发。下降沿触发即发生电平下降的时候就会触发,也就是说如果一个按钮开关在理想情况下,按下这个按钮就会产生一次下降沿,即使按下不松开,也只会产生一次中断。如果是低电平触发,只要按下不松动开,就会一直触发,因为一直处于低电平的状态。下面是STC89C51的外部中断管脚与中断号:

外部中断寄存器:

Int0.h

1#ifndef __INT0_H__
2#define __INT0_H__
3
4void Int0_Init(void);
5
6#endif

Int0.c

 1#include <REGX52.H>
 2
 3/**
 4  * @brief  外部中断0初始化
 5  * @param  无
 6  * @retval 无
 7  */
 8void Int0_Init(void)
 9{
10	IT0=1;
11	IE0=0;
12	EX0=1;
13	EA=1;
14	PX0=1;
15}
16
17/*外部中断0中断函数模板
18void Int0_Routine(void) interrupt 0
19{
20	
21}
22*/

红外遥控Demo

IR.h

 1#ifndef __IR_H__
 2#define __IR_H__
 3
 4#define IR_POWER		0x45
 5#define IR_MODE			0x46
 6#define IR_MUTE			0x47
 7#define IR_START_STOP	0x44
 8#define IR_PREVIOUS		0x40
 9#define IR_NEXT			0x43
10#define IR_EQ			0x07
11#define IR_VOL_MINUS	0x15
12#define IR_VOL_ADD		0x09
13#define IR_0			0x16
14#define IR_RPT			0x19
15#define IR_USD			0x0D
16#define IR_1			0x0C
17#define IR_2			0x18
18#define IR_3			0x5E
19#define IR_4			0x08
20#define IR_5			0x1C
21#define IR_6			0x5A
22#define IR_7			0x42
23#define IR_8			0x52
24#define IR_9			0x4A
25
26void IR_Init(void);
27unsigned char IR_GetDataFlag(void);
28unsigned char IR_GetRepeatFlag(void);
29unsigned char IR_GetAddress(void);
30unsigned char IR_GetCommand(void);
31
32#endif

IR.c

  1#include <REGX52.H>
  2#include "Timer0.h"
  3#include "Int0.h"
  4
  5unsigned int IR_Time;
  6unsigned char IR_State;
  7
  8unsigned char IR_Data[4];
  9unsigned char IR_pData;
 10
 11unsigned char IR_DataFlag;
 12unsigned char IR_RepeatFlag;
 13unsigned char IR_Address;
 14unsigned char IR_Command;
 15
 16/**
 17  * @brief  红外遥控初始化
 18  * @param  无
 19  * @retval 无
 20  */
 21void IR_Init(void)
 22{
 23	Timer0_Init();
 24	Int0_Init();
 25}
 26
 27/**
 28  * @brief  红外遥控获取收到数据帧标志位
 29  * @param  无
 30  * @retval 是否收到数据帧,1为收到,0为未收到
 31  */
 32unsigned char IR_GetDataFlag(void)
 33{
 34	if(IR_DataFlag)
 35	{
 36		IR_DataFlag=0;
 37		return 1;
 38	}
 39	return 0;
 40}
 41
 42/**
 43  * @brief  红外遥控获取收到连发帧标志位
 44  * @param  无
 45  * @retval 是否收到连发帧,1为收到,0为未收到
 46  */
 47unsigned char IR_GetRepeatFlag(void)
 48{
 49	if(IR_RepeatFlag)
 50	{
 51		IR_RepeatFlag=0;
 52		return 1;
 53	}
 54	return 0;
 55}
 56
 57/**
 58  * @brief  红外遥控获取收到的地址数据
 59  * @param  无
 60  * @retval 收到的地址数据
 61  */
 62unsigned char IR_GetAddress(void)
 63{
 64	return IR_Address;
 65}
 66
 67/**
 68  * @brief  红外遥控获取收到的命令数据
 69  * @param  无
 70  * @retval 收到的命令数据
 71  */
 72unsigned char IR_GetCommand(void)
 73{
 74	return IR_Command;
 75}
 76
 77//外部中断0中断函数,下降沿触发执行
 78void Int0_Routine(void) interrupt 0
 79{
 80	if(IR_State==0)			//状态0,空闲状态
 81	{
 82		Timer0_SetCounter(0);	//定时计数器清0
 83		Timer0_Run(1);		//定时器启动
 84		IR_State=1;		//置状态为1
 85	}
 86	else if(IR_State==1)		//状态1,等待Start信号或Repeat信号
 87	{
 88		IR_Time=Timer0_GetCounter();	//获取上一次中断到此次中断的时间
 89		Timer0_SetCounter(0);	//定时计数器清0
 90		//如果计时为13.5ms,则接收到了Start信号(判定值在12MHz晶振下为13500,在11.0592MHz晶振下为12442)
 91		if(IR_Time>12442-500 && IR_Time<12442+500)
 92		{
 93			IR_State=2;		//置状态为2
 94		}
 95		//如果计时为11.25ms,则接收到了Repeat信号(判定值在12MHz晶振下为11250,在11.0592MHz晶振下为10368)
 96		else if(IR_Time>10368-500 && IR_Time<10368+500)
 97		{
 98			IR_RepeatFlag=1;	//置收到连发帧标志位为1
 99			Timer0_Run(0);		//定时器停止
100			IR_State=0;		//置状态为0
101		}
102		else				//接收出错
103		{
104			IR_State=1;		//置状态为1
105		}
106	}
107	else if(IR_State==2)		//状态2,接收数据
108	{
109		IR_Time=Timer0_GetCounter();	//获取上一次中断到此次中断的时间
110		Timer0_SetCounter(0);	//定时计数器清0
111		//如果计时为1120us,则接收到了数据0(判定值在12MHz晶振下为1120,在11.0592MHz晶振下为1032)
112		if(IR_Time>1032-500 && IR_Time<1032+500)
113		{
114			IR_Data[IR_pData/8]&=~(0x01<<(IR_pData%8));	//数据对应位清0
115			IR_pData++;			//数据位置指针自增
116		}
117		//如果计时为2250us,则接收到了数据1(判定值在12MHz晶振下为2250,在11.0592MHz晶振下为2074)
118		else if(IR_Time>2074-500 && IR_Time<2074+500)
119		{
120			IR_Data[IR_pData/8]|=(0x01<<(IR_pData%8));	//数据对应位置1
121			IR_pData++;		//数据位置指针自增
122		}
123		else				//接收出错
124		{
125			IR_pData=0;		//数据位置指针清0
126			IR_State=1;		//置状态为1
127		}
128		if(IR_pData>=32)		//如果接收到了32位数据
129		{
130			IR_pData=0;		//数据位置指针清0
131			if((IR_Data[0]==~IR_Data[1]) && (IR_Data[2]==~IR_Data[3]))	//数据验证
132			{
133				IR_Address=IR_Data[0];	//转存数据
134				IR_Command=IR_Data[2];
135				IR_DataFlag=1;	//置收到连发帧标志位为1
136			}
137			Timer0_Run(0);		//定时器停止
138			IR_State=0;		//置状态为0
139		}
140	}
141}

定时器 Timer0.h 与 Timer0.c

 1// ------------------------------------------
 2#ifndef __TIMER0_H__
 3#define __TIMER0_H__
 4
 5void Timer0_Init(void);
 6void Timer0_SetCounter(unsigned int Value);
 7unsigned int Timer0_GetCounter(void);
 8void Timer0_Run(unsigned char Flag);
 9
10#endif
11// ------------------------------------------
12
13#include <REGX52.H>
14
15/**
16  * @brief  定时器0初始化
17  * @param  无
18  * @retval 无
19  */
20void Timer0_Init(void)
21{
22	TMOD &= 0xF0;		//设置定时器模式
23	TMOD |= 0x01;		//设置定时器模式
24	TL0 = 0;		//设置定时初值
25	TH0 = 0;		//设置定时初值
26	TF0 = 0;		//清除TF0标志
27	TR0 = 0;		//定时器0不计时
28}
29
30/**
31  * @brief  定时器0设置计数器值
32  * @param  Value,要设置的计数器值,范围:0~65535
33  * @retval 无
34  */
35void Timer0_SetCounter(unsigned int Value)
36{
37	TH0=Value/256;
38	TL0=Value%256;
39}
40
41/**
42  * @brief  定时器0获取计数器值
43  * @param  无
44  * @retval 计数器值,范围:0~65535
45  */
46unsigned int Timer0_GetCounter(void)
47{
48	return (TH0<<8)|TL0;
49}
50
51/**
52  * @brief  定时器0启动停止控制
53  * @param  Flag 启动停止标志,1为启动,0为停止
54  * @retval 无
55  */
56void Timer0_Run(unsigned char Flag)
57{
58	TR0=Flag;
59}

main.c

 1#include <REGX52.H>
 2#include "Delay.h"
 3#include "LCD1602.h"
 4#include "IR.h"
 5
 6unsigned char Num;
 7unsigned char Address;
 8unsigned char Command;
 9
10void main()
11{
12	LCD_Init();
13	LCD_ShowString(1,1,"ADDR  CMD  NUM");
14	LCD_ShowString(2,1,"00    00   000");
15	
16	IR_Init();
17	
18	while(1)
19	{
20		if(IR_GetDataFlag() || IR_GetRepeatFlag())	//如果收到数据帧或者收到连发帧
21		{
22			Address=IR_GetAddress();		//获取遥控器地址码
23			Command=IR_GetCommand();		//获取遥控器命令码
24			
25			LCD_ShowHexNum(2,1,Address,2);	//显示遥控器地址码
26			LCD_ShowHexNum(2,7,Command,2);	//显示遥控器命令码
27			
28			if(Command==IR_VOL_MINUS)		//如果遥控器VOL-按键按下
29			{
30				Num--;						//Num自减
31			}
32			if(Command==IR_VOL_ADD)			//如果遥控器VOL+按键按下
33			{
34				Num++;						//Num自增
35			}
36			
37			LCD_ShowNum(2,12,Num,3);		//显示Num
38		}
39	}
40}

CodeDemo:https://github.com/zouchanglin/mcu-code/tree/main/STC89C51