嵌入式入门教学——C51(下)

目录

十三、AT24C02(I2C总线)

十四、DS18B20温度传感器(单总线)

十五、LCD1602液晶显示屏

十六、直流电机(PWM) 

十七、AD/DA

十八、红外遥控(外部中断)


十三、AT24C02(I2C总线)

1、存储器
  • RAM、ROM各有优势,所以需要结合使用。
1.1、AT24C02简介
  • AT24C02是一种可以实现掉电不丢失的存储器,可用于保存单片机运行时想要永久保存的数据信息。
  • 存储介质:E2PROM
  • 通讯接口:I2C总线
  • 容量:256字节
2、AT24C02原理图
  • VDD、VSS:电源(1.8V~5.5V)
  • WE:写使能(低电平有效)
  • SCL、SDA:I2C接口
  • E0、E1、E2:I2C地址
3、I2C总线
3.1、I2C简介
  • 12C总线 (InterIC BUS) 是由Philips公司开发的一种通用数据总线。
  • 两根通信线:SCL (Serial Clock)、SDA(Serial Data )
  • 特点:同步,半双工,带数据应答
  • 通用的I2C总线,可以使各种设备的通信标准统一,对于厂家来说使用成熟的方案可以缩短芯片设计周期、提高稳定性,对于应用着来说,使用通用的通信协议可以避免学习各种各样的自定义协议降低了学习和应用的难度。
3.2、I2C电路规范
  • 所有I2C设备的SCL连在一起,SDA连在一起。
  • 设备的SCL和SDA均要配置成开漏输出模式
    • 【注】开漏输出模式:输出电平只能被拉低,而不能被拉高。开关断开时,处于一种浮空的状态;开关连接时,输出低电平。
  • SCL和SDA各添加一个上拉电阻(把一个信号通过一个电阻接到电源),阻值一般为4.7KQ左右。
    • 【注】为什么开漏输出又要加上拉电阻?
      • 接上拉电阻是因为I2C通信需要输出高电平的能力。一般开漏输出无法输出高电平,如果在漏极接上拉电阻,则可以进行电平转换。
  • 开漏输出和上拉电阻的共同作用实现了“线与”的功能,此设计主要是为了解决多机通信互相干扰的问题。
3.3、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)。
3.4、I2C数据帧
  • 发送一帧数据:在起始条件开始后,第一个字节一定是发送从机地址+写标志0。从机地址前四位是固定的,后三位是可配置的,即E0,E1,E2。每发送一个字节的数据,主机需要接收从机的应答。
  • 接受一帧数据:第一个字节一定是发送从机地址+读标志1。第一次发送从机地址后,主机需要接收从机的应答。之后每接收一个字节的数据,主机需要向从机发送应答。
  • 先发送再接收数据帧:
4、AT24C02数据帧
  • 字节写:在WORD ADDRESS(字地址)处写入数据DATA。
  • 随机读:读出在WORD ADDRESS处的数据DATA。
  • AT24C02的固定地址为1010,可配置地址本开发板上为000,所以SLAVE ADDRESS+W为0xA0,SLAVE ADDRESS(从机地址)+R为0xA1。
  • 【注】每次写入需要延时5ms,因为AT24C02的写周期为5ms。连续读不用。
5、AT24C02数据存储(LCD1602显示)
  • 内容:按下按键1,Num加1;按下按键2,Num减1;按下按键3,将十六位的Num拆开写入AT24C02存储器中;按下按键4,AT24C02存储器中读出Num。(数据是写入芯片中的,掉电后仍会保留)
  • 新建工程,在工程目录下新建Functions、Objects、Listings文件夹,将延时函数、独立按键和LCD1602模块放入Functions文件夹中。设置生成.hex文件并将其存放到Objects中,将.lst文件存放到Listings中。新建main.c文件,添加Delay.c、Key.c和LCD1602.c到工程中,并设置其引入路径。
  • 代码结构:
5.1、I2C模块
  • I2C.c
  • #include <REGX52.H>
    sbit I2C_SCL=P2^1;
    sbit I2C_SDA=P2^0;
    /**
    	*	@brief	I2C开始
    	* 	@param	无
    	*	@retval	无
    	*/
    void I2C_Start(void){
    	I2C_SDA=1;
    	I2C_SCL=1;
    	I2C_SDA=0;
    	I2C_SCL=0;
    }
    /**
    	*	@brief	I2C停止
    	* 	@param	无
    	*	@retval	无
    	*/
    void I2C_Stop(void){
    	I2C_SDA=0;
    	I2C_SCL=1;
    	I2C_SDA=1;
    }
    /**
    	*	@brief	I2C发送一个字节
    	* 	@param	Byte 要发送的字节
    	*	@retval	无
    	*/
    void I2C_SendByte(unsigned char Byte){
    	unsigned char i;
    	for(i=0;i<8;i++){
    		I2C_SDA=Byte&(0x80>>i);
    		I2C_SCL=1; // 读取
    		I2C_SCL=0;
    	}
    }
    /**
    	*	@brief	I2C接收一个字节
    	* 	@param	无
    	*	@retval	接收到的一个字节数据
    	*/
    unsigned char I2C_ReceiveByte(void){
    	unsigned char i,Byte=0x00;
    	I2C_SDA=1; // 主机释放总线,让从机使用
    	for(i=0;i<8;i++){
    		I2C_SCL=1;
    		if(I2C_SDA)	Byte|=(0x80>>i);
    		I2C_SCL=0;
    	}
    	return Byte;
    }
    /**
    	*	@brief	I2C发送应答
    	* 	@param	AckBit 应答位,0为应答,1为非应答
    	*	@retval	无
    	*/
    void I2C_SendAck(unsigned char AckBit){
    	I2C_SDA=AckBit;
    	I2C_SCL=1;
    	I2C_SCL=0;
    }
    /**
    	*	@brief	I2C接收应答
    	* 	@param	无
    	*	@retval	接收到的应答位,0为应答,1为非应答
    	*/
    unsigned char I2C_ReceiveAck(void){
    	unsigned char AckBit;
    	I2C_SDA=1; // 主机释放总线,让从机使用
    	I2C_SCL=1;
    	AckBit=I2C_SDA;
    	I2C_SCL=0;
    	return AckBit;
    }
  • I2C.h
  • #ifndef __I2C_H__
    #define __I2C_H__
    void I2C_Start(void);
    void I2C_Stop(void);
    void I2C_SendByte(unsigned char Byte);
    unsigned char I2C_ReceiveByte(void);
    void I2C_SendAck(unsigned char AckBit);
    unsigned char I2C_ReceiveAck(void);
    #endif
  • 将I2C总线模块放入Functions文件夹中,添加I2C.c到工程中,并设置其引入路径。 
5.2、AT24C02模块
  • AT24C02.c
  • #include <REGX52.H>
    #include "I2C.h"
    #define AT24C02_ADDRESS 0xA0
    /**
    	*	@brief	AT24C02写入一个字节
    	* 	@param	WordAddress 要写入字节的地址
    	* 	@param	Data 要写入的数据	
    	*	@retval	无
    	*/
    void AT24C02_WriteByte(unsigned char WordAddress,Data){
    	I2C_Start();
    	I2C_SendByte(AT24C02_ADDRESS); // 发送从机地址,写
    	I2C_ReceiveAck(); // 接收应答
    	I2C_SendByte(WordAddress); // 发送字地址
    	I2C_ReceiveAck();
    	I2C_SendByte(Data); // 发送数据
    	I2C_ReceiveAck();
    	I2C_Stop();
    }
    /**
    	*	@brief	AT24C02读取一个字节
    	* 	@param	WordAddress 要读出字节的地址
    	*	@retval	读出的数据
    	*/
    unsigned char AT24C02_ReadByte(unsigned char WordAddress){
    	unsigned char Data;
    	I2C_Start();
    	I2C_SendByte(AT24C02_ADDRESS); // 发送从机地址
    	I2C_ReceiveAck(); // 接收应答
    	I2C_SendByte(WordAddress); // 发送字地址
    	I2C_ReceiveAck();
    	I2C_Start();
    	I2C_SendByte(AT24C02_ADDRESS|0x01); // 发送从机地址,读
    	I2C_ReceiveAck(); // 接收应答
    	Data=I2C_ReceiveByte();
    	I2C_SendAck(1); // 发送应答
    	I2C_Stop();
    	return Data;
    }
  • AT24C02.h 
  • #ifndef __AT24C02_H__
    #define __AT24C02_H__
    void AT24C02_WriteByte(unsigned char WordAddress,Data);
    unsigned char AT24C02_ReadByte(unsigned char WordAddress);
    #endif
  • 将AT24C02存储器模块放入Functions文件夹中,添加AT24C02.c到工程中,并设置其引入路径。  
5.3、编写main.c文件
  • #include <REGX52.H>
    #include "Delay.h"
    #include "Key.h"
    #include "LCD1602.h"
    #include "AT24C02.h"
    unsigned char KeyNum;
    unsigned int Num; // Num为十六位数据,所以需要拆开存放
    void main(){
    	LCD_Init();
    	LCD_ShowNum(1,1,Num,5);
    	while(1){
    		KeyNum=Key();
    		if(KeyNum==1){
    			Num++;
    			LCD_ShowNum(1,1,Num,5);
    		}
    		if(KeyNum==2){
    			Num--;
    			LCD_ShowNum(1,1,Num,5);
    		}
    		if(KeyNum==3){
    			AT24C02_WriteByte(0,Num%256); // 低八位
    			Delay(5); // 写周期
    			AT24C02_WriteByte(1,Num/256); // 高低位
    			Delay(5);
    			LCD_ShowString(2,1,"Write OK");
    			Delay(1000);
    			LCD_ShowString(2,1,"        ");
    		}
    		if(KeyNum==4){
    			Num=AT24C02_ReadByte(0);
    			Num|=AT24C02_ReadByte(1)<<8;
    			LCD_ShowNum(1,1,Num,5);
    			LCD_ShowString(2,1,"Read OK");
    			Delay(1000);
    			LCD_ShowString(2,1,"        ");
    		}
    	}
    }
6、秒表(定时器扫描按键和数码管) 
  • 内容:使用数码管显示秒表,按下按键1,秒表开始计时;按下按键2,秒表停止计时;按下按键3,将秒表的时间写入AT24C02存储器中;按下按键4,从AT24C02存储器中读出时间。(重点是使用定时器扫描按键和数码管)
  • 新建工程,在工程目录下新建Functions、Objects、Listings文件夹,将延时函数、I2C总线、AT24C02存储器和定时器0模块放入Functions文件夹中。设置生成.hex文件并将其存放到Objects中,将.lst文件存放到Listings中。新建main.c文件,添加Delay.c、I2C.c、AT24C02.c和Timer0.c到工程中,并设置其引入路径。
  • 代码结构:
6.1、独立按键模块
  • 因为使用的是定时器去扫描按键,速度可以控制在20ms扫描一次,所以可以不用消抖。需要在独立按键中编写一个函数供定时器调用,故重新编写定时器模块。
  • Key_Timer.c
  • #include <REGX52.H>
    /**
    	*	@brief	使用定时器扫描按键,获取独立按键键码
    	* 	@param	无
    	*	@retval	按下按键的键码,范围:0~4,无按键按下时,返回0
    	*/
    unsigned char Key_KeyNumber;
    unsigned char Key(void){
    	unsigned char Temp=0;
    	Temp=Key_KeyNumber;
    	Key_KeyNumber=0;
    	return Temp;
    }
    unsigned char Key_GetState(){ // 定时器每隔20秒扫描一次,所以不用消抖
    	unsigned char KeyNumber=0;
    	if(P3_1==0)	KeyNumber=1;
    	if(P3_0==0)	KeyNumber=2;
    	if(P3_2==0)	KeyNumber=3;
    	if(P3_3==0)	KeyNumber=4;
    	return KeyNumber;
    }
    void Key_Loop(void){ // 在定时器中调用
    	static unsigned char NowState,LastState;
    	LastState=NowState;
    	NowState=Key_GetState();
    	if(LastState==1 && NowState==0){ // 按下按键1并松手
    		Key_KeyNumber=1;
    	}
    	if(LastState==2 && NowState==0){ // 按下按键2并松手
    		Key_KeyNumber=2;
    	}
    	if(LastState==3 && NowState==0){ // 按下按键3并松手
    		Key_KeyNumber=3;
    	}
    	if(LastState==4 && NowState==0){ // 按下按键4并松手
    		Key_KeyNumber=4;
    	}
    }
  • Key_Timer.h 
  • #ifndef __KEY_H__
    #define __KEY_H__
    unsigned char Key(void);
    void Key_Loop(void);
    #endif
    
  • 将新的独立按键模块放入Functions文件夹中,添加Key_Timer.c到工程中,并设置其引入路径。   
6.2、数码管模块
  • 因为使用的是定时器去扫描数码管,速度可以控制在2ms扫描一次,所以不用管数码管多久刷新一次,不需要延时函数。需要在数码管中定义一个函数去控制它显示的内容,故重新编写定时器模块。
  • Nixie_Timer.c
  • #include <REGX52.H>
    /**
    	*	@brief	使用定时器扫描数码管,数码管显示函数
    	* 	@param	Location 显示位置 范围:1~6
    	* 	@param	Number 显示数值 范围:0~16
    	*	@retval	无
    	*/
    //段选
    unsigned char NixieTable[]={ // 只显示0~9和黑屏
        0x3F, 0x06, 0x5B, 0x4F,
        0x66, 0x6D, 0x7D, 0x07,
        0x7F, 0x6F, 0x00
    };
    unsigned char Nixie_Buf[9]={0,10,10,10,10,10,10,10,10}; // 显示缓存
    // 修改缓存
    void Nixie_SetBuf(unsigned char Location,Number){
    	Nixie_Buf[Location]=Number;
    }
    void Nixie_Scan(unsigned char Location, int Number){
        P0=0x00; // 段选清零,消影
        switch(Location){
            case 1: P2_4=1; P2_3=1; P2_2=1; break;
            case 2: P2_4=1; P2_3=1; P2_2=0; break;
            case 3: P2_4=1; P2_3=0; P2_2=1; break;
            case 4: P2_4=1; P2_3=0; P2_2=0; break;
            case 5: P2_4=0; P2_3=1; P2_2=1; break;
            case 6: P2_4=0; P2_3=1; P2_2=0; break;
            case 7: P2_4=0; P2_3=0; P2_2=1; break;
            case 8: P2_4=0; P2_3=0; P2_2=0; break;
        }
        P0=NixieTable[Number]; // 段选
    }
    void Nixie_Loop(void){ // 供定时器调用,不能有Delay
    	static unsigned char i=1;
    	Nixie_Scan(i,Nixie_Buf[i]);
    	i++;
    	if(i>=9) i=1;
    }
  • Nixie_Timer.h 
  • #ifndef __NIXIE_H__
    #define __NIXIE_H__
    void Nixie_SetBuf(unsigned char Location,Number);
    void Nixie_Scan(unsigned char Location, int Number);
    void Nixie_Loop(void);
    #endif
  • 将新的数码管模块放入Functions文件夹中,添加Nixie_Timer.c到工程中,并设置其引入路径。
6.2、编写main.c文件
  • #include <REGX52.H>
    #include "AT24C02.h"
    #include "Key_Timer.h"
    #include "Nixie_Timer.h"
    #include "Timer0.h"
    #include "Delay.h"
    unsigned char KeyNum;
    unsigned char Min,Sec,MinSec;
    unsigned char RunFlag;
    void main(){
    	Timer0Init();
    	while(1){
    		KeyNum=Key();
    		if(KeyNum==1){
    			RunFlag=!RunFlag;
    		}
    		if(KeyNum==2){
    			Min=0,Sec=0,MinSec=0;
    		}
    		if(KeyNum==3){ // 写入存储器
    			AT24C02_WriteByte(0,Min);
    			Delay(5); // 写周期
    			AT24C02_WriteByte(1,Sec);
    			Delay(5);
    			AT24C02_WriteByte(2,MinSec);
    			Delay(5);
    		}
    		if(KeyNum==4){ // 从存储器读出
    			Min=AT24C02_ReadByte(0);
    			Sec=AT24C02_ReadByte(1);
    			MinSec=AT24C02_ReadByte(2);
    		}
    		Nixie_SetBuf(1,Min/10); // 分钟的十位
    		Nixie_SetBuf(2,Min%10); // 分钟的个位
    		Nixie_SetBuf(3,11); // 杠-
    		Nixie_SetBuf(4,Sec/10); // 秒钟的十位
    		Nixie_SetBuf(5,Sec%10); // 秒钟的个位
    		Nixie_SetBuf(6,11); // 杠-
    		Nixie_SetBuf(7,MinSec/10); // 100为1秒
    		Nixie_SetBuf(8,MinSec%10);
    	}
    }
    void Sec_Loop(void){
    	if(RunFlag){
    		MinSec++; // 到100加1秒
    		if(MinSec>=100){
    			MinSec=0;
    			Sec++;
    			if(Sec>=60){
    				Sec=0;
    				Min++;
    				if(Min>=60) Min=0;
    			}
    		}
    	}
    }
    void Timer0_Routine() interrupt 1{
    	static unsigned int T0Count1,T0Count2,T0Count3;
    	TL0 = 0x18;		//设置定时初值
    	TH0 = 0xFC;		//设置定时初值
    	T0Count1++;
    	if(T0Count1>=20){ // 20ms
    		T0Count1=0;
    		Key_Loop(); // 独立键盘
    	}
    	T0Count2++;
    	if(T0Count2>=2){ // 2ms
    		T0Count2=0;
    		Nixie_Loop(); // 数码管
    	}
    	T0Count3++;
    	if(T0Count3>=10){ // 10ms
    		T0Count3=0;
    		Sec_Loop(); // 秒表计时
    	}
    }

十四、DS18B20温度传感器(单总线)

1、DS18B20
1.1、DS18B20简介
  • DS18B20是一种常见的数字温度传感器,其控制命令和数据都是以数字信号的方式输入输出,相比较于模拟温度传感器,具有功能强大、硬件简单、易扩展、抗干扰性强等特点。
  • 测温范围:-55℃ ~ +125℃
  • 通信接口:1-Wire(单总线)
  • 其它特征:可形成总线结构、内置温度报警功能、可寄生供电
1.2、DS18B20原理图
  • VCC:电源(3.0V~5.5V)
  • GND:电源地
  • IO:单总线接口
1.3、DS18B20内部结构框图
  • 64-BIT ROM:作为器件地址,用于总线通信的寻址。
  • SCRATCHPAD(暂存器):用于总线的数据交互。
  • TEMPERATURE:用于保存温度触发值和配置参数。
1.4、暂存器结构
  • 先把数据写入暂存器,再把暂存器的内容复制到EEPRAM中。
2、单总线
2.1、单总线简介
  • 单总线 (1-Wire BUS) 是由Dallas公司开发的一种通用数据总线。
  • 一根通信线:DQ
  • 特点:异步、半双工
  • 单总线只需要一根通信线即可实现数据的双向传输,当采用寄生供电时,还可以省去设备的VDD线路,此时,供电加通信只需要DQ和GND两根线。
2.2、单总线电路规范
  • 设备的DQ均要配置成开漏输出模式。
  • DQ添加一个上拉电阻,阻值一般为4.7KQ左右。(与I2C相似,为了多机通信)
  • 若此总线的从机采取寄生供电,则主机还应配一个强上拉输出电路。
2.3、单总线时序结构 
  • 初始化:主机将总线拉低至少480us,然后释放总线,等待15~60us后,从机会拉低总线60~240us以响应主机,之后从机将释放总线。后一段也至少要480us。
  • 发送一位:主机将总线拉低60~120us,然后释放总线,表示发送0;主机将总线拉低1~15us,然后释放总线,表示发送1。从机将在总线拉低30us后(典型值) 读取电平,整个时间片应大于60us。
  • 接收一位:主机将总线拉低1~15us,然后释放总线,并在拉低后15us内读取总线电平(尽量贴近15us的末尾),读取为低电平则为接收0,读取为高电平则为接收1,整个时间片应大于60us。
  • 发送一个字节:连续调用8次发送一位的时序,依次发送一个字节的8位(低位在前)
  • 接收一个字节:连续调用8次接收一位的时序,依次接收一个字节的8位(低位在前)
3、DS18B20数据帧
3.1、DS18B20操作流程
  • 初始化:从机复位,主机判断从机是否响应。
  • ROM操作:ROM指令+本指令需要的读写操作。
  • 功能操作:功能指令+本指令需要的读写操作。
  • 【注】详细描述,看DS18B20手册。
3.2、数据帧
  • 温度变换:初始化 -> 跳过ROM -> 开始温度变换。(只有一个从机时可使用跳过ROM)
  • 温度读取:初始化 -> 跳过ROM -> 读暂存器 -> 连续的读操作。
4、温度存储格式
  • 符号位全1,即为负;全0,即为正。
5、DS18B20温度读取(LCD1602显示)
  • 内容:使用DS18B20测量温度,并时刻读取温度显示在LCD1602上。
  • 新建工程,在工程目录下新建Functions、Objects、Listings文件夹,将延时函数和LCD1602模块放入Functions文件夹中。设置生成.hex文件并将其存放到Objects中,将.lst文件存放到Listings中。新建main.c文件,添加Delay.c和LCD1602.c到工程中,并设置其引入路径。
  • 代码结构:
5.1、单总线模块
  • Onewire.c(结合时序图)
  • #include <REGX52.H>
    sbit OneWire_DQ=P3^7;
    /**
    	*	@brief	总线初始化
    	* 	@param	无
    	*	@retval	AckBit 从机应答标志位
    	*/
    unsigned char OneWire_Init(void){
    	unsigned char i;
    	unsigned char AckBit;
    	OneWire_DQ=1;
    	OneWire_DQ=0;
    	i=247; while(--i); // 延时500us,根据STC-ISP生成的
    	OneWire_DQ=1; // 释放总线
    	i=32; while(--i); // 延时70us,根据STC-ISP生成的
    	AckBit=OneWire_DQ; // 从机响应主机
    	i=247; while(--i); // 延时500us,根据STC-ISP生成的
    	return AckBit;
    }
    /**
    	*	@brief	发送一位
    	* 	@param	Bit 要发送的一位数据
    	*	@retval	无
    	*/
    void OneWire_SendBit(unsigned char Bit){ // 根据发送1来设定,这样就不用区分0和1
    	unsigned char i;
    	OneWire_DQ=0;
    	i=4; while(--i); // 延时10us,根据STC-ISP生成的
    	OneWire_DQ=Bit;
    	i=24; while(--i); // 延时50us,根据STC-ISP生成的
    	OneWire_DQ=1;
    }
    /**
    	*	@brief	发送一位
    	* 	@param	无
    	*	@retval	Bit 接收到的一位数据
    	*/
    unsigned char OneWire_ReceiveBit(void){ // 根据接收1来设置
    	unsigned char i;
    	unsigned char Bit;
    	OneWire_DQ=0;
    	i=2; while(--i); // 延时5us,根据STC-ISP生成的
    	OneWire_DQ=1;
    	i=2; while(--i); // 延时5us,根据STC-ISP生成的
    	Bit=OneWire_DQ;
    	i=24; while(--i); // 延时50us,根据STC-ISP生成的
    	return Bit;
    }
    /**
    	*	@brief	发送一个字节
    	* 	@param	Byte 要发送的一个字节
    	*	@retval	无
    	*/
    void OneWire_SendByte(unsigned char Byte){
    	unsigned char i;
    	for(i=0;i<8;i++){
    		OneWire_SendBit(Byte&(0x01<<i)); // 从低位开始
    	}
    }
    /**
    	*	@brief	接收一个字节
    	* 	@param	无
    	*	@retval	Byte 接收到的一个字节
    	*/
    unsigned char OneWire_ReceiveByte(void){
    	unsigned char Byte=0x00;
    	unsigned char i;
    	for(i=0;i<8;i++){
    		if(OneWire_ReceiveBit())	Byte|=(0x01<<i); // 从低位开始
    	}
    	return Byte;
    }
  • OneWire.h
  • #ifndef __ONEWIRE_H__
    #define __ONEWIRE_H__
    unsigned char OneWire_Init(void);
    void OneWire_SendBit(unsigned char Bit);
    unsigned char OneWire_ReceiveBit(void);
    void OneWire_SendByte(unsigned char Byte);
    unsigned char OneWire_ReceiveByte(void);
    #endif
  • 将单总线模块放入Functions文件夹中,添加Onewire.c到工程中,并设置其引入路径。
5.2、DS18B20模块
  • DS18B20.c
  • #include <REGX52.H>
    #include "OneWire.h"
    
    #define DS18B20_SKIP_ROM 0xCC
    #define DS18B20_CONVERT_T 0x44
    #define DS18B20_READ_SCRATCHPAD 0xBE
    /**
    	*	@brief	变换温度
    	* 	@param	无
    	*	@retval	无
    	*/
    void DS18B20_ConvertT(void){
    	OneWire_Init();
    	OneWire_SendByte(DS18B20_SKIP_ROM); // 跳过ROM
    	OneWire_SendByte(DS18B20_CONVERT_T); // 变换温度
    }
    /**
    	*	@brief	读取温度
    	* 	@param	无
    	*	@retval	T 读取到的温度
    	*/
    float DS18B20_ReadT(void){
    	unsigned char TLSB,TMSB;
    	int Temp;
    	float T;
    	OneWire_Init();
    	OneWire_SendByte(DS18B20_SKIP_ROM); // 跳过ROM
    	OneWire_SendByte(DS18B20_READ_SCRATCHPAD); // 读暂存器
    	TLSB=OneWire_ReceiveByte();
    	TMSB=OneWire_ReceiveByte();
    	Temp=(TMSB<<8)|TLSB;
    	T=Temp/16.0; // 左移四位,无符号转有符号
    	return T;
    }
  • DS18B20.h 
  • #ifndef __DS18B20_H__
    #define __DS18B20_H__
    void DS18B20_ConvertT(void);
    float DS18B20_ReadT(void);
    #endif
  •  将温度传感器模块放入Functions文件夹中,添加DS18B20.c到工程中,并设置其引入路径。
5.3、编写main.c文件
  • #include <REGX52.H>
    #include "LCD1602.h"
    #include "DS18B20.h"
    #include "Delay.h"
    float T;
    void main(){
    	LCD_Init();
    	LCD_ShowString(1,1,"Temperature:");
    	while(1){
    		DS18B20_ConvertT(); // 温度转换
    		Delay(1000); // 读出的不是默认值
    		T=DS18B20_ReadT();
    		if(T<0){
    			LCD_ShowChar(2,1,'-');
    			T=-T;
    		}else{
    			LCD_ShowChar(2,1,'+');
    		}
    		LCD_ShowNum(2,2,T,3);
    		LCD_ShowChar(2,5,'.');
    		LCD_ShowNum(2,6,(unsigned long)(T*10000)%10000,4); // 取小数部分
    	}
    }
 6、DS18B20温度报警器(LCD1602显示)
  • 内容:使用DS18B20测量温度,并时刻读取温度显示在LCD1602上。设置温度上限和下限,将其存储在AT24C02中。按下按键1,温度上限加一;按下按键2,温度上限减一;按下按键3,温度下限加一;按下按键4,温度下限减一。
  • 新建工程,在工程目录下新建Functions、Objects、Listings文件夹,将AT24C02存储器、I2C总线、DS18B02温度传感器、单总线、定时器0、定时器扫描按键、延时函数和LCD1602模块放入Functions文件夹中。设置生成.hex文件并将其存放到Objects中,将.lst文件存放到Listings中。新建main.c文件,添加AT24C02.c、I2C.c、DS18B02.c、OneWire.c、Timer0.c、Key_Timer.c、Delay.c和LCD1602.c到工程中,并设置其引入路径。
6.1、修改单总线模块
  • 本实验引入了定时器,定时器的介入会影响单总线的时序。所以,需要给单总线模块中添加开关中断。
  • OneWire.c
  • #include <REGX52.H>
    sbit OneWire_DQ=P3^7;
    /**
    	*	@brief	总线初始化
    	* 	@param	无
    	*	@retval	AckBit 从机应答标志位
    	*/
    unsigned char OneWire_Init(void){
    	unsigned char i;
    	unsigned char AckBit;
    	EA=0; // 屏蔽中断
    	OneWire_DQ=1;
    	OneWire_DQ=0;
    	i=247; while(--i); // 延时500us,根据STC-ISP生成的
    	OneWire_DQ=1; // 释放总线
    	i=32; while(--i); // 延时70us,根据STC-ISP生成的
    	AckBit=OneWire_DQ; // 从机响应主机
    	i=247; while(--i); // 延时500us,根据STC-ISP生成的
    	EA=1; // 打开中断
    	return AckBit;
    }
    /**
    	*	@brief	发送一位
    	* 	@param	Bit 要发送的一位数据
    	*	@retval	无
    	*/
    void OneWire_SendBit(unsigned char Bit){ // 根据发送1来设定,这样就不用区分0和1
    	unsigned char i;
    	EA=0; // 屏蔽中断
    	OneWire_DQ=0;
    	i=4; while(--i); // 延时10us,根据STC-ISP生成的
    	OneWire_DQ=Bit;
    	i=24; while(--i); // 延时50us,根据STC-ISP生成的
    	OneWire_DQ=1;
    	EA=1; // 打开中断
    }
    /**
    	*	@brief	发送一位
    	* 	@param	无
    	*	@retval	Bit 接收到的一位数据
    	*/
    unsigned char OneWire_ReceiveBit(void){ // 根据接收1来设置
    	unsigned char i;
    	unsigned char Bit;
    	EA=0; // 屏蔽中断
    	OneWire_DQ=0;
    	i=2; while(--i); // 延时5us,根据STC-ISP生成的
    	OneWire_DQ=1;
    	i=2; while(--i); // 延时5us,根据STC-ISP生成的
    	Bit=OneWire_DQ;
    	i=24; while(--i); // 延时50us,根据STC-ISP生成的
    	EA=1; // 打开中断
    	return Bit;
    }
    /**
    	*	@brief	发送一个字节
    	* 	@param	Byte 要发送的一个字节
    	*	@retval	无
    	*/
    void OneWire_SendByte(unsigned char Byte){
    	unsigned char i;
    	for(i=0;i<8;i++){
    		OneWire_SendBit(Byte&(0x01<<i)); // 从低位开始
    	}
    }
    /**
    	*	@brief	接收一个字节
    	* 	@param	无
    	*	@retval	Byte 接收到的一个字节
    	*/
    unsigned char OneWire_ReceiveByte(void){
    	unsigned char Byte=0x00;
    	unsigned char i;
    	for(i=0;i<8;i++){
    		if(OneWire_ReceiveBit())	Byte|=(0x01<<i); // 从低位开始
    	}
    	return Byte;
    }
6.2、编写main.c文件
  • #include <REGX52.H>
    #include "DS18B20.h"
    #include "Delay.h"
    #include "LCD1602.h"
    #include "AT24C02.h"
    #include "Key_Timer.h"
    #include "Timer0.h"
    float T,TShow;
    char TLow,THigh;
    unsigned char KeyNum;
    void main(){
    	DS18B20_ConvertT();
    	Delay(1000);
    	THigh=AT24C02_ReadByte(0);
    	TLow=AT24C02_ReadByte(1);
    	if(THigh>125 || TLow<-55 || THigh<=TLow){
    		THigh=20;
    		TLow=15;
    	}
    	LCD_Init();
    	LCD_ShowString(1,1,"T:");
    	LCD_ShowString(2,1,"TH:");
    	LCD_ShowString(2,9,"TL:");
    	LCD_ShowSignedNum(2,4,THigh,3);
    	LCD_ShowSignedNum(2,12,TLow,3);
    	Timer0Init();
    	while(1){
    		KeyNum=Key();
    		// 温度读取及显示
    		DS18B20_ConvertT();
    		T=DS18B20_ReadT();
    		if(T<0){
    			LCD_ShowChar(1,3,'-');
    			TShow=-T;
    		}else{
    			LCD_ShowChar(1,3,'+');
    			TShow=T;
    		}
    		LCD_ShowNum(1,4,TShow,3);
    		LCD_ShowChar(1,7,'.');
    		LCD_ShowNum(1,8,(unsigned long)(TShow*100)%100,2);
    		// 阈值判断及显示
    		if(KeyNum){
    			if(KeyNum==1){ //K1按键,THigh自增
    				THigh++;
    				if(THigh>125){THigh=125;}
    			}
    			if(KeyNum==2){ //K2按键,THigh自减
    				THigh--;
    				if(THigh<=TLow){THigh++;}
    			}
    			if(KeyNum==3){ //K3按键,TLow自增
    				TLow++;
    				if(TLow>=THigh){TLow--;}
    			}
    			if(KeyNum==4){ //K4按键,TLow自减
    				TLow--;
    				if(TLow<-55){TLow=-55;}
    			}
    			LCD_ShowSignedNum(2,4,THigh,3);
    			LCD_ShowSignedNum(2,12,TLow,3);
    			// 存储阈值
    			AT24C02_WriteByte(0,THigh); //写入到At24C02中保存
    			Delay(5);
    			AT24C02_WriteByte(1,TLow);
    			Delay(5);
    		}
    		if(T>THigh){ //越界判断
    			LCD_ShowString(1,13,"OV:H");
    		}else if(T<TLow){
    			LCD_ShowString(1,13,"OV:L");
    		}else{
    			LCD_ShowString(1,13,"    ");
    		}
    	}
    }
    void Timer0_Routine() interrupt 1{
    	static unsigned int T0Count;
    	TL0 = 0x18;		//设置定时初值
    	TH0 = 0xFC;		//设置定时初值
    	T0Count++;
    	if(T0Count>=20){
    		T0Count=0;
    		Key_Loop();	//每20ms调用一次按键驱动函数
    	}
    }

十五、LCD1602液晶显示屏

1、LCD1602
  • LCD1602液晶显示屏是一种字符型液晶显示模块,可以显示ASCII码的标准字符和其它的一些内置特殊字符,还可以有8个自定义字符。
  • 显示容量:16x2个字符,每个字符为5*7点阵。
1.1、LCD1602原理图
  • GND:地
  • VCC:电源正极(4.5-5.5V)
  • VO:对比度调节电压
  • RS:数据/指令选择,1为数据,0为指令
  • RW:读/写选择,1为读,0为写
  • E:使能,1为数据有效,下降沿执行命令
  • DB0~DB7:数据输入/输出
  • BG VCC:背光灯电源正极
  • BG GND:背光灯电源负极
1.2、LCD1602内部结构框图
  • 首先在DDRAM对应位置写入数据,通过字模库找到对应的段码,然后在屏幕对应位置上显示对应的字符。
1.3、LCD1602存储器结构
  • DDRAM(数据显示区)
  • CGRAM+CGROM(字模库)
  • 例,显示字符A,0100 0001(先列后行),及0x41。
  • 【注】行列位置表示数值与字符的ASCII的十六进制一致,故直接传入字符即可。
    • 有些不同,具体以字符库为主。
  • 【注】显示特殊字符时需要加上反斜杠。
    • 如,显示 ',需要加上 \'。
1.4、LCD1602时序结构
1.5、LCD1602指令集
1.6、LCD1602操作流程
  • 初始化
    • 发送指令0x38  // 八位数据接口,两行显示,5*7点阵
    • 发送指令0x0C  // 显示开,光标关,闪烁关
    • 发送指令0x06  // 数据读写操作后,光标自动加一,画面不动
    • 发送指令0x01  // 清屏
  • 显示字符
    • 发送指令0x80|AC  // 设置光标位置(设置DDRAM地址时,最高位为1)
    • 发送数据  // 发送要显示的字符数据
    • 发送数据  // 发送要显示的字符数据
2、LCD1602液晶显示屏
  • 内容:编写LCD1602模块,在LCD1602液晶显示屏上显示对应的字符,并且移动显示。
  • 新建工程,在工程目录下新建Functions、Objects、Listings文件夹。将延时函数模块放入Functions文件夹中。设置生成.hex文件并将其存放到Objects中,将.lst文件存放到Listings中。新建main.c文件,添加Delay.c到工程中,并设置其引入路径。
2.1、LCD1602模块
  • LCD1602.c
  • #include <REGX52.H>
    //引脚配置:
    sbit LCD_RS=P2^6;
    sbit LCD_RW=P2^5;
    sbit LCD_EN=P2^7;
    #define LCD_DataPort P0
    //函数定义:
    /**
      * @brief  LCD1602延时函数,12MHz调用可延时1ms
      * @param  无
      * @retval 无
      */
    void LCD_Delay()
    {
    	unsigned char i, j;
    
    	i = 2;
    	j = 239;
    	do
    	{
    		while (--j);
    	} while (--i);
    }
    /**
      * @brief  LCD1602写命令
      * @param  Command 要写入的命令
      * @retval 无
      */
    void LCD_WriteCommand(unsigned char Command)
    {
    	LCD_RS=0;
    	LCD_RW=0;
    	LCD_DataPort=Command;
    	LCD_EN=1;
    	LCD_Delay();
    	LCD_EN=0;
    	LCD_Delay();
    }
    /**
      * @brief  LCD1602写数据
      * @param  Data 要写入的数据
      * @retval 无
      */
    void LCD_WriteData(unsigned char Data)
    {
    	LCD_RS=1;
    	LCD_RW=0;
    	LCD_DataPort=Data;
    	LCD_EN=1;
    	LCD_Delay();
    	LCD_EN=0;
    	LCD_Delay();
    }
    /**
      * @brief  LCD1602设置光标位置
      * @param  Line 行位置,范围:1~2
      * @param  Column 列位置,范围:1~16
      * @retval 无
      */
    void LCD_SetCursor(unsigned char Line,unsigned char Column)
    {
    	if(Line==1)
    	{
    		LCD_WriteCommand(0x80|(Column-1));
    	}
    	else if(Line==2)
    	{
    		LCD_WriteCommand(0x80|(Column-1+0x40));
    	}
    }
    /**
      * @brief  LCD1602初始化函数
      * @param  无
      * @retval 无
      */
    void LCD_Init()
    {
    	LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵
    	LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关
    	LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动
    	LCD_WriteCommand(0x01);//光标复位,清屏
    }
    
    /**
      * @brief  在LCD1602指定位置上显示一个字符
      * @param  Line 行位置,范围:1~2
      * @param  Column 列位置,范围:1~16
      * @param  Char 要显示的字符
      * @retval 无
      */
    void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
    {
    	LCD_SetCursor(Line,Column);
    	LCD_WriteData(Char);
    }
    /**
      * @brief  在LCD1602指定位置开始显示所给字符串
      * @param  Line 起始行位置,范围:1~2
      * @param  Column 起始列位置,范围:1~16
      * @param  String 要显示的字符串
      * @retval 无
      */
    void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
    {
    	unsigned char i;
    	LCD_SetCursor(Line,Column);
    	for(i=0;String[i]!='\0';i++)
    	{
    		LCD_WriteData(String[i]);
    	}
    }
    /**
      * @brief  返回值=X的Y次方
      */
    int LCD_Pow(int X,int Y)
    {
    	unsigned char i;
    	int Result=1;
    	for(i=0;i<Y;i++)
    	{
    		Result*=X;
    	}
    	return Result;
    }
    /**
      * @brief  在LCD1602指定位置开始显示所给数字
      * @param  Line 起始行位置,范围:1~2
      * @param  Column 起始列位置,范围:1~16
      * @param  Number 要显示的数字,范围:0~65535
      * @param  Length 要显示数字的长度,范围:1~5
      * @retval 无
      */
    void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
    {
    	unsigned char i;
    	LCD_SetCursor(Line,Column);
    	for(i=Length;i>0;i--)
    	{
    		LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
    	}
    }
    /**
      * @brief  在LCD1602指定位置开始以有符号十进制显示所给数字
      * @param  Line 起始行位置,范围:1~2
      * @param  Column 起始列位置,范围:1~16
      * @param  Number 要显示的数字,范围:-32768~32767
      * @param  Length 要显示数字的长度,范围:1~5
      * @retval 无
      */
    void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
    {
    	unsigned char i;
    	unsigned int Number1;
    	LCD_SetCursor(Line,Column);
    	if(Number>=0)
    	{
    		LCD_WriteData('+');
    		Number1=Number;
    	}
    	else
    	{
    		LCD_WriteData('-');
    		Number1=-Number;
    	}
    	for(i=Length;i>0;i--)
    	{
    		LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
    	}
    }
    /**
      * @brief  在LCD1602指定位置开始以十六进制显示所给数字
      * @param  Line 起始行位置,范围:1~2
      * @param  Column 起始列位置,范围:1~16
      * @param  Number 要显示的数字,范围:0~0xFFFF
      * @param  Length 要显示数字的长度,范围:1~4
      * @retval 无
      */
    void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
    {
    	unsigned char i,SingleNumber;
    	LCD_SetCursor(Line,Column);
    	for(i=Length;i>0;i--)
    	{
    		SingleNumber=Number/LCD_Pow(16,i-1)%16;
    		if(SingleNumber<10)
    		{
    			LCD_WriteData(SingleNumber+'0');
    		}
    		else
    		{
    			LCD_WriteData(SingleNumber-10+'A');
    		}
    	}
    }
    /**
      * @brief  在LCD1602指定位置开始以二进制显示所给数字
      * @param  Line 起始行位置,范围:1~2
      * @param  Column 起始列位置,范围:1~16
      * @param  Number 要显示的数字,范围:0~1111 1111 1111 1111
      * @param  Length 要显示数字的长度,范围:1~16
      * @retval 无
      */
    void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
    {
    	unsigned char i;
    	LCD_SetCursor(Line,Column);
    	for(i=Length;i>0;i--)
    	{
    		LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
    	}
    }
    
  • LCD1602.h
  • #ifndef __LCD1602_H__
    #define __LCD1602_H__
    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);
    void LCD_WriteCommand(unsigned char Command);
    #endif
    
  •  将LCD1602模块放入Functions文件夹中,添加LCD1602.c到工程中,并设置其引入路径。
2.2、编写main.c文件
  • #include <REGX52.H>
    #include "LCD1602.h"
    #include "Delay.h"
    void main(){
    	LCD_Init();
    	LCD_ShowChar(1,1,'A');
    	LCD_ShowString(1,3,"Hello");
    	LCD_ShowNum(1,9,66,2);
    	LCD_ShowSignedNum(1,12,-88,2);
    	LCD_ShowHexNum(2,1,0xA5,2);
    	LCD_ShowBinNum(2,4,0xA5,8);
    	LCD_ShowChar(2,13,0xDF);
    	LCD_ShowChar(2,14,'C');
    	LCD_ShowString(1,16,"Welcome to China!");
    	while(1){
    		LCD_WriteCommand(0x18); // 移屏
    		Delay(500);
    	}
    }

十六、直流电机(PWM) 

1、直流电机
1.1、直流电机简介
  • 直流电机是一种将电能转换为机械能的装置。一般的直流电机有两个电极,当电极正接时,电机正转,当电极反接时,电机反转。
  • 直流电机主要由永磁体(定子)、线圈(转子)和换向器组成。
  • 除直流电机外,常见的电机还有步进电机、舵机、无刷电机、空心杯电机等。
1.2、直流电机驱动电路
  • ,电机按图接后,P10输入1,OUT1输出0,电机通电旋转;P10输入0,OUT1相当于断开,电机不工作。
  • 大功率器件直接驱动(不能控制电机旋转方向)
  • ,使用三极管,利用I/O口间接控制电源开关。
  • H桥驱动(可以控制电机旋转方向)
  • ,Q1、Q4导通,Q2、Q3断开,经过B1的电流向右;Q2、Q3导通,Q1、Q4断开,经过B1的电流向左。
2、PWM
2.1、PWM简介
  • PWM(Pulse Width Modulation)即脉冲宽度调制,在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速、开关电源等领域。
  • PWM重要参数:
    • 频率 = 1 / Ts
    • 占空比 = Ton / Ts(打开时间 / 一个周期)
    • 精度 = 占空比变化步距
2.2、PWM产生方法
  • 模型结构
  • 波形
3、LED呼吸灯
  • 内容:通过控制LED亮的时间,让LED灯逐渐亮灭。
  • 新建工程,在工程目录下新建Functions、Objects、Listings文件夹。设置生成.hex文件并将其存放到Objects中,将.lst文件存放到Listings中。新建main.c文件。
  • 编写main.c文件
    • #include <REGX52.H>
      sbit LED=P2^0; // 第一个LED灯
      void Delay(unsigned int t){
      	while(t--);
      }
      void main(){
      	unsigned char Time,i;
      	while(1){
      		for(Time=0;Time<100;Time++){
      			for(i=0;i<20;i++){ // 这种状态停留20次
      				LED=0;
      				Delay(Time); // 亮的时间逐渐增加
      				LED=1;
      				Delay(100-Time);
      			}
      		}
      		for(Time=100;Time>0;Time--){
      			for(i=0;i<20;i++){ // 这种状态停留20次
      				LED=0;
      				Delay(Time); // 亮的时间逐渐减少
      				LED=1;
      				Delay(100-Time);
      			}
      		}
      	}
      }
4、 直流电机调速
  • 内容:使用独立按键控制直流电机的旋转速度,并且在数码管上显示当前的速度,共有0~3个档位,按下按键1进行调节。
  • 新建工程,在工程目录下新建Functions、Objects、Listings文件夹。将延时函数、独立按键、数码管和定时器0模块放入Functions文件夹中。设置生成.hex文件并将其存放到Objects中,将.lst文件存放到Listings中。新建main.c文件,添加Delay.c、Key.c、Nixie.c和Timer0.c到工程中,并设置其引入路径。
4.1、接入电机
  • 电机应该接入步进电机模块的5V和D1口。(不区分正反)
4.2、修改定时器0模块
  • 原本的1ms进入一次中断太慢了,使用STC-ISP重新生成一个100us的定时器初始化函数。
  • Timer0.c
  • #include <REGX52.H>
    /**
    	*	@brief	定时器0初始化,100微秒@12.000MHz
    	* 	@param	无
    	*	@retval	无
    	*/
    void Timer0_Init(void)		//100微秒@12.000MHz
    {
    	TMOD &= 0xF0;		//设置定时器模式
    	TMOD |= 0x01;		//设置定时器模式
    	TL0 = 0x9C;		//设置定时初值
    	TH0 = 0xFF;		//设置定时初值
    	TF0 = 0;		//清除TF0标志
    	TR0 = 1;		//定时器0开始计时
    	// 配置中断
    	ET0=1;
    	EA=1; 
    	PT0=0;
    }
  •  Timer0.h
  • #ifndef __TIMER0_H__
    #define __TIMER0_H__
    void Timer0_Init(void);
    #endif
4.3、编写main.c文件
  • #include <REGX52.H>
    #include "Delay.h"
    #include "Key.h"
    #include "Nixie.h"
    #include "Timer0.h"
    sbit Motor=P1^0;
    unsigned char Counter,Compare; // 计数器和比较值
    unsigned char KeyNum,Speed;
    void main(){
    	Timer0_Init();
    	while(1){
    		KeyNum=Key();
    		if(KeyNum==1){ // 按下按键1调速
    			Speed++;
    			Speed%=4;
    			if(Speed==0) Compare=0;
    			if(Speed==1) Compare=50;
    			if(Speed==2) Compare=75;
    			if(Speed==3) Compare=100;
    		}
    		Nixie(1,Speed); // 数码管显示
    	}
    }
    void Timer0_Routine() interrupt 1{
    	TL0 = 0x9C;		//设置定时初值
    	TH0 = 0xFF;		//设置定时初值
    	Counter++;
    	Counter%=100;
    	if(Counter<Compare){
    		Motor=1; // 电机通电
    	}else Motor=0;
    }

十七、AD/DA

  • AD(Analog to Digital):模拟-数字转换,将模拟信号转换为计算机可操作的数字信号。
  • DA(Digital to Analog):数字-模拟转换,将计算机输出的数字信号转换为模拟信号。
  • 简单来说,AD将电压转换为内存中的数据,DA将数据转换为电压。
1、AD/DA硬件电路模型
  • AD转换通常有多个输入通道,用多路选择开关连接至AD转换器以实现AD多路复用的目的,提高硬件利用率。
  • AD/DA与单片机数据传送可使用并口(速度快、原理简单),也可使用串口(接线少、使用方便)。
  • 可将AD/DA模块直接集成在单片机内,这样直接写入/读出寄存器就可进行AD/DA转换,单片机的IO口可直接复用为AD/DA的通道。
2、AD/DA原理图
  • AD
  • DA
3、AD/DA工作原理
3.1、运算放大器
  • 运算放大器是具有很高放大倍数的放大电路单元内部集成了差分放大器、电压放大器、功率放大器三级放大电路是一个性能完备、功能强大的通用放大电路单元。
  • 运算放大器可构成的电路有:电压比较器、反相放大器、同相放大器、电压跟随器、加法器、积分器、微分器等。
  • 运算放大器电路的分析方法:虚短、虚断(负反馈条件下)。
  • 虚短:运算放大器的反相输入端接地时,会使反相输入端的电压跟随同相输入端,此时就像反相输入端被短接到地,所以称为虚短。
  • 虚断:当运算放大器的同相输入端和反相输入端接相同信号时,两输入端间没有电压差,此时就像放大器输入端断开一样,所以称为虚断。
3.1.1、电压比较器
3.1.2、反向放大器
  • R2=2k,R1=1k,此时电压放大两倍。
3.1.3、同向放大器
3.1.4、电压跟随器
3.2、DA(数字-模拟)原理
3.2.1、T型电阻网络DA转换器
  • 输出电压:
3.2.2、PWM型DA转换器
  • (本开发板所使用)
  • 低通滤波器的作用是过滤交流,保留直流。
  • 输出电压:,VH为高电平电压。
3.3、AD(模拟-数字)原理
3.3.1、逐次逼近型AD转换器
  • 使用DAC输出的电压与输入的电压比较,找到表示其的数字量。
  • 输出数字量:
4、AD/DA性能指标
  • 分辨率:指AD/DA数字量的精细程度,通常用位数表示。
    • 例如,对于5V电源系统来说,8位的AD可将5V等分为256份,即数字量变化最小一个单位时,模拟量变化5V/256=0.01953125V,所以,8位AD的电压分辨率为0.01953125V,AD/DA的位数越高,分辨率就越高。
  • 转换速度:表示AD/DA的最大采样/建立频率,通常用转换频率或者转换时间来表示,对于采样/输出高速信号,应注意AD/DA的转换速度。
5、XPT2046
5.1、XPT2046简介
  • XPT2046是一款4线制电阻式触摸屏控制器,内含12位分辨率125KHz转换速率逐步逼近型A/D转换器。XPT2046支持从1.5V到5.25V的低电压I/O接口。
5.2、XPT2046时序图
5.3、XPT2046控制位
  • SER/DFR:本芯片中一端输入,另一端接地,故为单端模式。
  • 掉电和内部参考电压选择:
  • 本次配置:
  • 通道选择:
    • XP:读可调电阻
    • YP:读光敏电阻
    • VBAT:读热敏电阻
  • 控制位宏定义
    • // 控制位宏定义
      #define XPT2046_XP		0x9C//0x8C 	// 测x正,可调电阻
      #define XPT2046_YP		0xDC 		// 测y正,光敏电阻
      #define XPT2046_VBAT	0xAC 		// 测VBAT,热敏电阻
      #define XPT2046_AUX		0xEC 		// 测AUX
 6、AD模数转换
  • 内容:通过调节可调电阻、光敏电阻、热敏电阻控制电压,将电压转换为数字显示在LCD1602上。
  • 新建工程,在工程目录下新建Functions、Objects、Listings文件夹。将延时函数和LCD1602模块放入Functions文件夹中。设置生成.hex文件并将其存放到Objects中,将.lst文件存放到Listings中。新建main.c文件,添加Delay.c和LCD1602.c到工程中,并设置其引入路径。
6.1、XPT2046模块
  • XPT2046.c
  • #include <REGX52.H>
    sbit XPT2046_CS=P3^5;
    sbit XPT2046_DCLK=P3^6;
    sbit XPT2046_DIN=P3^4;
    sbit XPT2046_DOUT=P3^7;
    /**
      * @brief  ZPT2046读取AD值
      * @param  Command 命令字,范围:头文件内定义的宏,结尾的数字表示转换的位数
      * @retval AD转换后的数字量,范围:8位为0~255,12位为0~4095
      */
    unsigned int XPT2046_ReadAD(unsigned char Command){
    	unsigned char i;
    	unsigned int ADValue=0;
    	XPT2046_DCLK=0;
    	XPT2046_CS=0;
    	for(i=0;i<8;i++){ // 发送命令字
    		XPT2046_DIN=Command&(0x80>>i);
    		XPT2046_DCLK=1; // 上升沿发送
    		XPT2046_DCLK=0;
    	}
    	for(i=0;i<16;i++){ 
    		XPT2046_DCLK=1;
    		XPT2046_DCLK=0; // 下降沿接收
    		if(XPT2046_DOUT) ADValue|=(0x8000>>i); // 读两个字节
    	}
    	XPT2046_CS=1;
    	if(Command&0x08){ // 模式1,精度为8,只需要前8位
    		return ADValue>>8;
    	}else{ // 模式0,精度为12,只需要前12位
    		return ADValue>>4;
    	}
    }
  • XPT2046.h
  • #ifndef __XPT2046_H__
    #define __XPT2046_H__
    // 控制位宏定义
    #define XPT2046_XP_8		0x9C	 	// 0x8C 测x正
    #define XPT2046_YP_8		0xDC 		// 测y正
    #define XPT2046_VBAT_8		0xAC 		// 测VBAT
    #define XPT2046_AUX_8		0xEC 		// 测AUX
    // 精度为12
    #define XPT2046_XP_12		0x94	 	// 0x8C 测x正
    #define XPT2046_YP_12		0xD4 		// 测y正
    #define XPT2046_VBAT_12		0xA4 		// 测VBAT
    #define XPT2046_AUX_12		0xE4 		// 测AUX
    unsigned int XPT2046_ReadAD(unsigned char Command);
    #endif
  •  将XPT2046模块放入Functions文件夹中,添加XPT2046.c到工程中,并设置其引入路径。
6.2、编写main.c文件
  • #include <REGX52.H>
    #include "LCD1602.h"
    #include "Delay.h"
    #include "XPT2046.h"
    unsigned int ADValue;
    void main(){
    	LCD_Init();
    	LCD_ShowString(1,1,"ADJ  NTC  RG");
    	while(1){
    		ADValue=XPT2046_ReadAD(XPT2046_XP_8); //读取AIN0,可调电阻
    		LCD_ShowNum(2,1,ADValue,3);
    		ADValue=XPT2046_ReadAD(XPT2046_YP_8); //读取AIN1,热敏电阻
    		LCD_ShowNum(2,6,ADValue,3);
    		ADValue=XPT2046_ReadAD(XPT2046_VBAT_8); //读取AIN2,光敏电阻
    		LCD_ShowNum(2,11,ADValue,3);
    		Delay(100);
    	}
    }
7、DA数模转换
  • 内容:使用PWM型DA转换器实现呼吸灯。
  • 将上一章直流电机调速实验复制一份,并修改名字。删除独立按键和数码管模块。
  • 修改mian.c文件
    • #include <REGX52.H>
      #include "Delay.h"
      #include "Key.h"
      #include "Nixie.h"
      #include "Timer0.h"
      sbit DA=P2^1;
      unsigned char Counter,Compare;
      unsigned char i;
      void main(){
      	Timer0_Init();
      	while(1){
      		for(i=0;i<100;i++){
      			Compare=i;
      			Delay(10);
      		}
      		for(i=100;i>0;i--){
      			Compare=i;
      			Delay(10);
      		}
      	}
      }
      void Timer0_Routine() interrupt 1{
      	TL0 = 0x9C;		//设置定时初值
      	TH0 = 0xFF;		//设置定时初值
      	Counter++;
      	Counter%=100;
      	if(Counter<Compare){
      		DA=1;
      	}else DA=0;
      }
  •  这里本身就是使用PWM实现DA转换,只不过在硬件上加了低通滤波。

十八、红外遥控(外部中断)

1、红外遥控
1.1、红外遥控简介
  • 红外遥控是利用红外光进行通信的设备,由红外LED将调制后的信号发出,由专用的红外接收头进行解调输出。
  • 通信方式:单工,异步
  • 红外LED波长:940nm
  • 通信协议标准:NEC标准
1.2、红外遥控硬件电路
  • 发送端和接收端

1.3、红外遥控原理图
1.4、基本发送与接收
  • 空闲状态:红外LED不亮,接收头输出高电平。
  • 发送低电平:红外LED以38KHz频率闪烁发光,接收头输出低电平。
  • 发送高电平:红外LED不亮,接收头输出高电平。
1.5、NEC编码
  •  Data格式:(4字节32位)
    • Address地址码:遥控器的标识符
    • 地址码反码:数据验证
1.6、遥控器键码
2、51单片机的外部中断
2.1、外部中断
  • STC89C52有4个外部中断。
  • STC89C52的外部中断有两种触发方式:下降沿触发和低电平触发。
    • 下降沿触发:给一次低电平,不恢复,外部中断只触发一次。
    • 低电平触发:给一次低电平,不恢复,外部中断一直触发。
  • 中断号:
2.2、外部中断寄存器
  • 要用到的寄存器:
  • IE
  • IP和IPH
  • TCON
3、红外遥控(LCD1602显示)
  • 内容:通过按下红外遥控,在LCD1602上显示接收到的地址和命令。按下VOL+,控制数字+1;按下VOL-,控制数字-1。
  • 新建工程,在工程目录下新建Functions、Objects、Listings文件夹。将延时函数和LCD1602和定时器0模块放入Functions文件夹中。设置生成.hex文件并将其存放到Objects中,将.lst文件存放到Listings中。新建main.c文件,添加Delay.c、LCD1602.c和Timer0.c到工程中,并设置其引入路径。
  • 代码结构:
  • 程序思路:
3.1、外部中断0模块
  • Int0.c
  • #include <REGX52.H>
    /**
      * @brief  外部中断0初始化
      * @param  无
      * @retval 无
      */
    void Int0_Init(void){
    	IT0=1; // 下降沿触发
    	IE0=0;
    	EX0=1;
    	EA=1;
    	PX0=1;
    }
    /* 外部中断0中断函数模板
    void Int0_Routine(void) interrupt 0{
    
    }
    */
  • Int0.h
  • #ifndef __INT0_H__
    #define __INT0_H__
    void Int0_Init(void);
    #endif
  •  将外部中断模块放入Functions文件夹中,添加Int0.c到工程中,并设置其引入路径。 
3.2、修改定时器0模块
  • 这里只需要使用定时器0进行程序间的计时,用来判断外部中断接收到的信号,不需要靠它中断。
  • Timer0.c
  • #include <REGX52.H>
    /**
      * @brief  定时器0初始化
      * @param  无
      * @retval 无
      */
    void Timer0_Init(void){
    	TMOD &= 0xF0;		//设置定时器模式
    	TMOD |= 0x01;		//设置定时器模式
    	TL0 = 0;		//设置定时初值
    	TH0 = 0;		//设置定时初值
    	TF0 = 0;		//清除TF0标志
    	TR0 = 0;		//定时器0不计时
    }
    /**
      * @brief  定时器0设置计数器值
      * @param  Value,要设置的计数器值,范围:0~65535
      * @retval 无
      */
    void Timer0_SetCounter(unsigned int Value){
    	TH0=Value/256;
    	TL0=Value%256;
    }
    /**
      * @brief  定时器0获取计数器值
      * @param  无
      * @retval 计数器值,范围:0~65535
      */
    unsigned int Timer0_GetCounter(void){
    	return (TH0<<8)|TL0;
    }
    /**
      * @brief  定时器0启动停止控制
      * @param  Flag 启动停止标志,1为启动,0为停止
      * @retval 无
      */
    void Timer0_Run(unsigned char Flag){
    	TR0=Flag;
    }
  • Timer0.h
  • #ifndef __TIMER0_H__
    #define __TIMER0_H__
    void Timer0_Init(void);
    void Timer0_SetCounter(unsigned int Value);
    unsigned int Timer0_GetCounter(void);
    void Timer0_Run(unsigned char Flag);
    #endif
3.3、 红外遥控模块
  • 【注】注意晶振频率,才发现STM89C52RC的晶振频率是11.0592MHz。晶振错误接收不到信号。
  • IR.c
  • #include <REGX52.H>
    #include "Timer0.h"
    #include "Int0.h"
    unsigned int IR_Time; // 计时
    unsigned char IR_State; // 状态
    unsigned char IR_Data[4]; // 4字节数据缓存
    unsigned char IR_pData; // 记录数据位存储位置0~31
    unsigned char IR_DataFlag; // 数据帧标志位
    unsigned char IR_RepeatFlag; // 连发帧标志位
    unsigned char IR_Address; // 地址
    unsigned char IR_Command; // 命令
    /**
      * @brief  红外遥控初始化
      * @param  无
      * @retval 无
      */
    void IR_Init(void){
    	Timer0_Init();
    	Int0_Init();
    }
    /**
      * @brief  红外遥控获取收到数据帧标志位
      * @param  无
      * @retval 是否收到数据帧,1为收到,0为未收到
      */
    unsigned char IR_GetDataFlag(void){
    	if(IR_DataFlag){
    		IR_DataFlag=0;
    		return 1;
    	}
    	return 0;
    }
    /**
      * @brief  红外遥控获取收到连发帧标志位
      * @param  无
      * @retval 是否收到连发帧,1为收到,0为未收到
      */
    unsigned char IR_GetRepeatFlag(void){
    	if(IR_RepeatFlag)
    	{
    		IR_RepeatFlag=0;
    		return 1;
    	}
    	return 0;
    }
    /**
      * @brief  红外遥控获取收到的地址数据
      * @param  无
      * @retval 收到的地址数据
      */
    unsigned char IR_GetAddress(void){
    	return IR_Address;
    }
    
    /**
      * @brief  红外遥控获取收到的命令数据
      * @param  无
      * @retval 收到的命令数据
      */
    unsigned char IR_GetCommand(void){
    	return IR_Command;
    }
    //外部中断0中断函数,下降沿触发执行
    void Int0_Routine(void) interrupt 0{
    	if(IR_State==0){ // 状态0,空闲
    		Timer0_SetCounter(0); // 定时计数器清0
    		Timer0_Run(1); // 定时器启动,1us加1
    		IR_State=1; // 状态置1
    	}else if(IR_State==1){ //状态1,等待Start信号或Repeat信号
    		IR_Time=Timer0_GetCounter(); // 获取上一次中断到此次中断的时间
    		Timer0_SetCounter(0); // 定时计数器清0
    		//如果计时为13.5ms,则接收到了Start信号(判定值在12MHz晶振下为13500,在11.0592MHz晶振下为12442)
    		if(IR_Time>12442-500 && IR_Time<12442+500){ // 有些误差
    			IR_State=2;	// 状态置2
    		}
    		//如果计时为11.25ms,则接收到了Repeat信号(判定值在12MHz晶振下为11250,在11.0592MHz晶振下为10368)
    		else if(IR_Time>10368-500 && IR_Time<10368+500){
    			IR_RepeatFlag=1; // 置连发帧标志位为1
    			Timer0_Run(0); // 定时器停止
    			IR_State=0;	// 置状态为0
    		}
    		else{ // 接收出错
    			IR_State=1;	// 置状态为1
    		}
    	}else if(IR_State==2){ // 状态2,接收数据
    		IR_Time=Timer0_GetCounter(); // 获取上一次中断到此次中断的时间
    		Timer0_SetCounter(0); // 定时计数器清0
    		// 如果计时为1120us,则接收到了数据0(判定值在12MHz晶振下为1120,在11.0592MHz晶振下为1032)
    		if(IR_Time>1032-500 && IR_Time<1032+500){
    			IR_Data[IR_pData/8]&=~(0x01<<(IR_pData%8));	// 数据对应位清0
    			IR_pData++;	// 数据位置指针自增
    		}
    		// 如果计时为2250us,则接收到了数据1(判定值在12MHz晶振下为2250,在11.0592MHz晶振下为2074)
    		else if(IR_Time>2074-500 && IR_Time<2074+500){
    			IR_Data[IR_pData/8]|=(0x01<<(IR_pData%8)); // 数据对应位置1
    			IR_pData++;	// 数据位置指针自增
    		}else{ // 接收出错
    			IR_pData=0;	// 数据位置指针清0
    			IR_State=1;	// 置状态为1
    		}
    		if(IR_pData>=32){ // 如果接收到了32位数据
    			IR_pData=0;	 // 数据位置指针清0
    			if((IR_Data[0]==~IR_Data[1]) && (IR_Data[2]==~IR_Data[3])){ // 数据验证
    				IR_Address=IR_Data[0]; // 转存数据
    				IR_Command=IR_Data[2];
    				IR_DataFlag=1; // 置数据帧标志位为1
    			}
    			Timer0_Run(0);		//定时器停止
    			IR_State=0;			//置状态为0
    		}
    	}
    }
  • IR.h
  • #ifndef __IR_H__
    #define __IR_H__
    // 按键地址宏定义
    #define IR_POWER		0x45
    #define IR_MODE			0x46
    #define IR_MUTE			0x47
    #define IR_START_STOP	0x44
    #define IR_PREVIOUS		0x40
    #define IR_NEXT			0x43
    #define IR_EQ			0x07
    #define IR_VOL_MINUS	0x15
    #define IR_VOL_ADD		0x09
    #define IR_0			0x16
    #define IR_RPT			0x19
    #define IR_USD			0x0D
    #define IR_1			0x0C
    #define IR_2			0x18
    #define IR_3			0x5E
    #define IR_4			0x08
    #define IR_5			0x1C
    #define IR_6			0x5A
    #define IR_7			0x42
    #define IR_8			0x52
    #define IR_9			0x4A
    void IR_Init(void);
    unsigned char IR_GetDataFlag(void);
    unsigned char IR_GetRepeatFlag(void);
    unsigned char IR_GetAddress(void);
    unsigned char IR_GetCommand(void);
    #endif
  • 将红外遥控模块放入Functions文件夹中,添加IR.c到工程中,并设置其引入路径。 
3.4、编写main.c文件
  • #include <REGX52.H>
    #include "Delay.h"
    #include "LCD1602.h"
    #include "IR.h"
    unsigned char Num;
    unsigned char Address;
    unsigned char Command;
    void main()
    {
    	LCD_Init();
    	LCD_ShowString(1,1,"ADDR  CMD  NUM");
    	LCD_ShowString(2,1,"00    00   000");
    	IR_Init();
    	while(1)
    	{
    		if(IR_GetDataFlag() || IR_GetRepeatFlag()){ // 如果收到数据帧或者收到连发帧
    			Address=IR_GetAddress(); // 获取遥控器地址码
    			Command=IR_GetCommand(); // 获取遥控器命令码
    			LCD_ShowHexNum(2,1,Address,2); // 显示遥控器地址码
    			LCD_ShowHexNum(2,7,Command,2); // 显示遥控器命令码
    			if(Command==IR_VOL_MINUS){ // 如果遥控器VOL-按键按下
    				Num--; // Num自减
    			}
    			if(Command==IR_VOL_ADD){ // 如果遥控器VOL+按键按下
    				Num++; // Num自增
    			}
    			LCD_ShowNum(2,12,Num,3); // 显示Num
    		}
    	}
    }
    
4、红外遥控电机调速
  • 内容:通过红外遥控0~4控制电机转速。
  • 将上两章直流电机调速实验复制一份,并修改名字。删除独立按键,并将定时器0模块修改为上一节的。将红外遥控和外部中断0模块放入Functions文件夹中。添加IR.c和Int0.c到工程中,并设置其引入路径。
4.1、定时器1模块
  • 红外遥控模块也需要使用到定时器0,会与直流电机产生冲突,所以让直流电机模块使用定时器1。
  • Timer1.c
  • #include <REGX52.H>
    /**
    	*	@brief	定时器1初始化,100us@12.000MHz
    	* 	@param	无
    	*	@retval	无
    	*/
    void Timer1_Init(void)		//100us@12.000MHz
    {
    	TMOD &= 0x0F;		//设置定时器模式
    	TMOD |= 0x10;		//设置定时器模式
    	TL1 = 0x9C;		//设置定时初值
    	TH1 = 0xFF;		//设置定时初值
    	TF1 = 0;		//清除TF1标志
    	TR1 = 1;		//定时器1开始计时
    	// 配置中断
    	ET1=1;
    	EA=1; 
    	PT1=0;
    }
    
    /* 定时器1中断函数模板
    void Timer1_Routine() interrupt 3{
    	static unsigned int T1Count;
    	TL1 = 0x9C;		//设置定时初值
    	TH1 = 0xFF;		//设置定时初值
    	T1Count++;
    	if(T1Count>=1000){
    		T1Count=0;
    		P2_0=~P2_0;
    	}
    }
    */
  • Timer1.h
  • #ifndef __TIMER1_H__
    #define __TIMER1_H__
    void Timer1_Init(void);
    #endif
  • 将定时器1模块放入Functions文件夹中,添加Timer1.c到工程中,并设置其引入路径。  
4.2、直流电机模块化
  • Motor.c
  • #include <REGX52.H>
    #include "Timer1.h"
    //引脚定义
    sbit Motor=P1^0;
    unsigned char Counter,Compare;
    /**
      * @brief  电机初始化
      * @param  无
      * @retval 无
      */
    void Motor_Init(void){
    	Timer1_Init();
    }
    /**
      * @brief  电机设置速度
      * @param  Speed 要设置的速度,范围0~100
      * @retval 无
      */
    void Motor_SetSpeed(unsigned char Speed){
    	Compare=Speed;
    }
    //定时器1中断函数
    void Timer1_Routine() interrupt 3{
    	TL1 = 0x9C;		//设置定时初值
    	TH1 = 0xFF;		//设置定时初值
    	Counter++;
    	Counter%=100;	//计数值变化范围限制在0~99
    	if(Counter<Compare)	//计数值小于比较值
    	{
    		Motor=1;		//输出1
    	}
    	else				//计数值大于比较值
    	{
    		Motor=0;		//输出0
    	}
    }
  • Motor.h
  • #ifndef __MOTOR_H__
    #define __MOTOR_H__
    void Motor_Init(void);
    void Motor_SetSpeed(unsigned char Speed);
    #endif
  • 将直流电机模块放入Functions文件夹中,添加Motor.c到工程中,并设置其引入路径。  
4.3、编写main.c文件
  • #include <REGX52.H>
    #include "Nixie.h"
    #include "Motor.h"
    #include "IR.h"
    unsigned char Command,Speed;
    void main(){
    	Motor_Init();
    	IR_Init();
    	while(1){
    		if(IR_GetDataFlag()){ // 红外遥控获取收到数据帧标志位
    			Command=IR_GetCommand(); // 红外遥控获取收到的命令数据
    			if(Command==IR_0) Speed=0;
    			if(Command==IR_1) Speed=1;
    			if(Command==IR_2) Speed=2;
    			if(Command==IR_3) Speed=3;
    			if(Speed==0) Motor_SetSpeed(0); // 电机调速
    			if(Speed==1) Motor_SetSpeed(50);
    			if(Speed==2) Motor_SetSpeed(75);
    			if(Speed==3) Motor_SetSpeed(100);
    		}
    		Nixie(1,Speed); // 数码管显示速度
    	}
    }
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

恣睢s

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

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

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

打赏作者

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

抵扣说明:

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

余额充值