单片机综合小项目
一、单片机做项目常识
1.行业常识
2.方案选型
3.此项目定位和思路
二、单片机的小项目介绍
1.项目名称:基于51单片机的温度报警器
(1)主控:stc51;
(2)编程语言:C语言
(3)开发环境:Keil
(4)1602屏显示时间和温度;当温度超过预定值时蜂鸣器和电机工作报警
(5)系统人机界面:矩阵按键或者红外遥控器:修改时间
2.硬件资源分配
优先满足硬件上已经接好的引脚
1602屏幕 P0;P2^7 ;P2^5;P2^6
4*4按键 P2
串口 P3.0 P3.1
IR(红外遥控器):P3^2
传感器:DS18B20 P3.7
DS1302 P3.4 P3.6
步进电机(四线双极性)P1.0---P1.5
蜂鸣器 P1.7
3.项目流程
(1)编写,移植,封装,测试顶层硬件模块操作库
(2)梳理,定义应用层功能
(3)逐个实现各功能,并联合调试,测试功能是否正常
(4)实现测试使用,并解决bug持续维护
4.一些小问题
三、构成建立及框架搭建
1.基本搭建
lst
src
app:高层时序【main存放】
driver :低层时序
include:基本的全局变量
obj
2.端口分配检查确定
1602屏幕 P0;P1^4 ; P1.5 ; P 1.6
4*4按键 P2
串口 P3.0 P3.1
IR(红外遥控器):P3^2
传感器:DS18B20 P3.7
DS1302 P3.4 P3.6 P3.5
步进电机(四线双极性)P1.0---P1.3
蜂鸣器 P1.6
四、第一个模块:串口
1.移植并调试确认基本功能
uart.c
#include"uart.h"//串口初始化函数
//预设计一个串口条件:8位数据位,1位停止位,0校验位,波特率9600
//初始化的主要工作是设置相关的寄存器
//使用晶振为11.0592MHz
//CPU工作在12T模式下void uart_init(){//使用8bit串行接口SCON=0x50;//波特率不加倍PCON=0x00;//波特率相关设置TMOD=0x20;//设置T1在模式2TL1=249; //设定定时初值TH1 = 249; //设定定时器重装值TR1=1;//开启T1,开始工作ES=1;//开启串行中断允许位EA=1;//开启全部中断}//串口发送单个字符
void uart_send_byte(unsigned char a){//发送一个字节SBUF=a;//查看当然串口是否在忙//根据SCON中的TI位可以判断当前串口是否在忙//如果数据8位发送结束,则硬件自动将TI=1,则TI=0表示程序还没有发送结束if(!TI)//软件复位TITI=0;
}void uart_send_string(unsigned char *str)
{while (*str != '\0'){uart_send_byte(*str); // 发送1个字符str++; // 指针指向下一个字符}
}
2.封装
1.何为封装
(1)隐藏
(2)保护
2.封装低层接口实践
uart.h
//开头2行和最后1行加起来构成一种格式,这种格式利用了c语言的预处理中的条件编译技术,
//实现的效果就是防止该头文件被重复包含构成的错误
#ifndef __UART__H__
#define __UART__H__
#include<reg51.h>//串口初始化函数
//预设计一个串口条件:8位数据位,1位停止位,0校验位,波特率9600
//初始化的主要工作是设置相关的寄存器
//使用晶振为11.0592MHz
//CPU工作在12T模式下
void uart_init();//串口发送单个字符
void uart_send_byte(unsigned char a);
//串口发送字符串
void uart_send_string(unsigned char *str);//延时函数
void Delay2000ms(); //@11.0592MHz#endif
五、DS18B20移植(温度显示)
1.static
static void Delay750us():表示只能在该文件内部使用
2.高层时序
初始化函数将复位和检测是否存在分为两个函数,方便封装
/*******************************************************************************
* 函 数 名 : ds18b20_reset
* 函数功能 : 复位DS18B20
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void ds18b20_reset(void)
{DS18B20_PORT=0; //拉低DQdelay_10us(75); //拉低750usDS18B20_PORT=1; //DQ=1delay_10us(2); //20US
}/*******************************************************************************
* 函 数 名 : ds18b20_check
* 函数功能 : 检测DS18B20是否存在
* 输 入 : 无
* 输 出 : 1:未检测到DS18B20的存在,0:存在
*******************************************************************************/
u8 ds18b20_check(void)
{u8 time_temp=0;while(DS18B20_PORT&&time_temp<20) //等待DQ为低电平{time_temp++;delay_10us(1); }if(time_temp>=20)return 1; //如果超时则强制返回1else time_temp=0;while((!DS18B20_PORT)&&time_temp<20) //等待DQ为高电平{time_temp++;delay_10us(1);}if(time_temp>=20)return 1; //如果超时则强制返回1return 0;
}/*******************************************************************************
* 函 数 名 : ds18b20_start
* 函数功能 : 开始温度转换
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
//转换命令
void ds18b20_start(void)
{ds18b20_reset();//复位ds18b20_check();//检查DS18B20ds18b20_write_byte(0xcc);//SKIP ROMds18b20_write_byte(0x44);//转换命令
}/*******************************************************************************
* 函 数 名 : ds18b20_init
* 函数功能 : 初始化DS18B20的IO口 DQ 同时检测DS的存在
* 输 入 : 无
* 输 出 : 1:不存在,0:存在
*******************************************************************************/
u8 ds18b20_init(void)
{ds18b20_reset();return ds18b20_check();
}/*******************************************************************************
* 函 数 名 : ds18b20_read_temperture
* 函数功能 : 从ds18b20得到温度值
* 输 入 : 无
* 输 出 : 温度数据
*******************************************************************************/
float ds18b20_read_temperture(void)
{float temp;u8 dath=0;u8 datl=0;u16 value=0;//开始转换:开启转换命令ds18b20_start();//将各个电线置为默认电平ds18b20_reset();//复位//判断当前程序是否在忙ds18b20_check();//发送读取温度命令ds18b20_write_byte(0xcc);//SKIP ROMds18b20_write_byte(0xbe);//读存储器datl=ds18b20_read_byte();//低字节dath=ds18b20_read_byte();//高字节value=(dath<<8)+datl;//合并为16位数据if((value&0xf800)==0xf800)//判断符号位,负温度{value=(~value)+1; //数据取反再加1temp=value*(-0.0625);//乘以精度 }else //正温度{temp=value*0.0625; }return temp;
}
3.遇到的问题
问题:double t=24.5;要用串口把24.5打印出来给串口助手去显示
串口助手显示方式有2种:二进制方式和文本方式。文本方式最直观,但是需要通过串口去发送的不是double,不是int,而是ASCII码的字符串
意思是:想要看到25.4,的uart_send_string("25.4");
所以我们需要一个函数,能够把double类型的t,给转成对应的字符串来去给串口显示
lcd1602.c
// 显示类似于24.5这种的double类型的数字
void LcdShowDouble(unsigned char x, unsigned char y, double d)
{// 第一步:将double d转成字符串strunsigned char str[5] = {0};// 第1步:先由double的25.4得到uint的254unsigned int tmp = (unsigned int)(d * 10); unsigned char c = 0;// 第2步:由/和%操作来得到2、5、4// 第3步:将2、5、4对应的ASCII码放到字符串中去,完成c = (unsigned char)(tmp / 100);str[0] = c + 48;tmp = tmp % 100; // 运算后tmp=54c = (unsigned char)(tmp / 10); // c = 5str[1] = c + 48;str[2] = '.';tmp = tmp % 10; // 运算后tmp=4c = (unsigned char)(tmp / 1); // c = 4str[3] = c + 48;str[4] = '\0';// 第二步:显示strLcdShowStr(x, y, str);
}
ds18b20
#include "ds18b20.h"
#include "intrins.h"/*******************************************************************************
* 函 数 名 : delay_10us
* 函数功能 : 延时函数,ten_us=1时,大约延时10us
* 输 入 : ten_us
* 输 出 : 无
*******************************************************************************/
void delay_10us(u16 ten_us)
{while(ten_us--);
}/*******************************************************************************
* 函 数 名 : delay_ms
* 函数功能 : ms延时函数,ms=1时,大约延时1ms
* 输 入 : ms:ms延时时间
* 输 出 : 无
*******************************************************************************/
void delay_ms(u16 ms)
{u16 i,j;for(i=ms;i>0;i--)for(j=110;j>0;j--);
}
/*******************************************************************************
* 函 数 名 : ds18b20_reset
* 函数功能 : 复位DS18B20
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void ds18b20_reset(void)
{DS18B20_PORT=0; //拉低DQdelay_10us(75); //拉低750usDS18B20_PORT=1; //DQ=1delay_10us(2); //20US
}/*******************************************************************************
* 函 数 名 : ds18b20_check
* 函数功能 : 检测DS18B20是否存在
* 输 入 : 无
* 输 出 : 1:未检测到DS18B20的存在,0:存在
*******************************************************************************/
u8 ds18b20_check(void)
{u8 time_temp=0;while(DS18B20_PORT&&time_temp<20) //等待DQ为低电平{time_temp++;delay_10us(1); }if(time_temp>=20)return 1; //如果超时则强制返回1else time_temp=0;while((!DS18B20_PORT)&&time_temp<20) //等待DQ为高电平{time_temp++;delay_10us(1);}if(time_temp>=20)return 1; //如果超时则强制返回1return 0;
}
/*******************************************************************************
* 函 数 名 : ds18b20_read_bit
* 函数功能 : 从DS18B20读取一个位
* 输 入 : 无
* 输 出 : 1/0
*******************************************************************************/
u8 ds18b20_read_bit(void)
{u8 dat=0;DS18B20_PORT=0;_nop_();_nop_();DS18B20_PORT=1; _nop_();_nop_(); //该段时间不能过长,必须在15us内读取数据if(DS18B20_PORT)dat=1; //如果总线上为1则数据dat为1,否则为0else dat=0;delay_10us(5);return dat;
} /*******************************************************************************
* 函 数 名 : ds18b20_read_byte
* 函数功能 : 从DS18B20读取一个字节
* 输 入 : 无
* 输 出 : 一个字节数据
*******************************************************************************/
u8 ds18b20_read_byte(void)
{u8 i=0;u8 dat=0;u8 temp=0;for(i=0;i<8;i++)//循环8次,每次读取一位,且先读低位再读高位{temp=ds18b20_read_bit();dat=(temp<<7)|(dat>>1);}return dat;
}/*******************************************************************************
* 函 数 名 : ds18b20_write_byte
* 函数功能 : 写一个字节到DS18B20
* 输 入 : dat:要写入的字节
* 输 出 : 无
*******************************************************************************/
void ds18b20_write_byte(u8 dat)
{u8 i=0;u8 temp=0;for(i=0;i<8;i++)//循环8次,每次写一位,且先写低位再写高位{temp=dat&0x01;//选择低位准备写入dat>>=1;//将次高位移到低位if(temp)//此时表示读入位为“1”{DS18B20_PORT=0;_nop_();_nop_();DS18B20_PORT=1; delay_10us(6);}else{DS18B20_PORT=0;delay_10us(6);DS18B20_PORT=1;_nop_();_nop_(); } }
}/*******************************************************************************
* 函 数 名 : ds18b20_start
* 函数功能 : 开始温度转换
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
//转换命令
void ds18b20_start(void)
{ds18b20_reset();//复位ds18b20_check();//检查DS18B20ds18b20_write_byte(0xcc);//SKIP ROMds18b20_write_byte(0x44);//转换命令
}/*******************************************************************************
* 函 数 名 : ds18b20_init
* 函数功能 : 初始化DS18B20的IO口 DQ 同时检测DS的存在
* 输 入 : 无
* 输 出 : 1:不存在,0:存在
*******************************************************************************/
u8 ds18b20_init(void)
{ds18b20_reset();return ds18b20_check();
}/*******************************************************************************
* 函 数 名 : ds18b20_read_temperture
* 函数功能 : 从ds18b20得到温度值
* 输 入 : 无
* 输 出 : 温度数据
*******************************************************************************/
float ds18b20_read_temperture(void)
{float temp;u8 dath=0;u8 datl=0;u16 value=0;//开始转换:开启转换命令ds18b20_start();//将各个电线置为默认电平ds18b20_reset();//复位//判断当前程序是否在忙ds18b20_check();//发送读取温度命令ds18b20_write_byte(0xcc);//SKIP ROMds18b20_write_byte(0xbe);//读存储器datl=ds18b20_read_byte();//低字节dath=ds18b20_read_byte();//高字节value=(dath<<8)+datl;//合并为16位数据if((value&0xf800)==0xf800)//判断符号位,负温度{value=(~value)+1; //数据取反再加1temp=value*(-0.0625);//乘以精度 }else //正温度{temp=value*0.0625; }return temp;
}/*
double Ds18b20ReadTemp(void)
{unsigned int temp = 0;unsigned char tmh = 0, tml = 0;double t = 0;Ds18b20ChangTemp(); //先写入转换命令Ds18b20ReadTempCom(); //然后等待转换完后发送读取温度命令tml = Ds18b20ReadByte(); //读取温度值共16位,先读低字节tmh = Ds18b20ReadByte(); //再读高字节
// temp = tmh;
// temp <<= 8;
// temp |= tml;temp = tml | (tmh << 8);t = temp * 0.0625;return t;
}*/
六、LCD1602移植
1.1602的接线
1602的引脚是事先接好的,所以不能改变
1602屏幕 P0;P2^7 ;P2^5;P2^6
2.lcd1602.c
#include <reg51.h>
#include "lcd1602.h"
// 对LCD1602的底层以及高层时序做封装/************ 低层时序 ********************************/
static void Read_Busy() //忙检测函数,判断bit7是0,允许执行;1禁止
{unsigned char sta; //LCD1602_DB = 0xff;LCD1602_RS = 0;LCD1602_RW = 1;do{LCD1602_EN = 1;sta = LCD1602_DB;LCD1602_EN = 0; //使能,用完就拉低,释放总线}while(sta & 0x80);
}static void Lcd1602_Write_Cmd(unsigned char cmd) //写命令
{Read_Busy();LCD1602_RS = 0;LCD1602_RW = 0; LCD1602_DB = cmd;LCD1602_EN = 1;LCD1602_EN = 0;
}static void Lcd1602_Write_Data(unsigned char dat) //写数据
{Read_Busy();LCD1602_RS = 1;LCD1602_RW = 0;LCD1602_DB = dat;LCD1602_EN = 1;LCD1602_EN = 0;
}/************* 高层时序 ******************************/
// 本函数用来设置当前光标位置,其实就是设置当前正在编辑的位置,
// 其实就是内部的数据地址指针,其实就是RAM显存的偏移量
// x范围是0-15,y=0表示上面一行,y=1表示下面一行
static void LcdSetCursor(unsigned char x,unsigned char y) //坐标显示
{unsigned char addr;if(y == 0)addr = 0x00 + x;elseaddr = 0x40 + x;Lcd1602_Write_Cmd(addr|0x80);
}// 函数功能是:从坐标(x,y)开始显示字符串str
// 注意这个函数不能跨行显示,因为显存地址是不连续的
// 其实我们可以封装出一个能够折行显示的函数的
void LcdShowStr(unsigned char x,unsigned char y,unsigned char *str) //显示字符串
{LcdSetCursor(x,y); //当前字符的坐标while(*str != '\0'){Lcd1602_Write_Data(*str++);}
}// 初始化LCD,使之能够开始正常工作
void InitLcd1602() //1602初始化
{Lcd1602_Write_Cmd(0x38); //打开,5*8,8位数据//Lcd1602_Write_Cmd(0x0c); // 打开显示并且无光标Lcd1602_Write_Cmd(0x0f); // 打开显示并且光标闪烁Lcd1602_Write_Cmd(0x06);Lcd1602_Write_Cmd(0x01); //清屏
}
3.lcd1602.h
#ifndef __lcd1602__H__
#define __lcd1602__H__#include<reg51.h>// IO接口定义
#define LCD1602_DB P0 //data bus 数据总线
// 控制总线
sbit LCD1602_RS = P2^6; //选择读取数据/命令
sbit LCD1602_RW = P2^5; //选择读/写
sbit LCD1602_EN = P2^7; //使能#define u8 unsigned char //只需要声明高层时序即可,而底层时序是不需要声明
//因为我们在头文件中声明这个函数,目的是为了让别的文件去包含这个
//从而调用这个头文件中声明的函数,所以我们只需要声明1602.c中将来
//会被外部.c文件调用的哪些函数即可,而且1602.c中自己使用的内部函数将来也
//不会被外部.c文件调用,因此就不用声明了。/************* 高层时序 ******************************/
// 本函数用来设置当前光标位置,其实就是设置当前正在编辑的位置,
// 其实就是内部的数据地址指针,其实就是RAM显存的偏移量
// x范围是0-15,y=0表示上面一行,y=1表示下面一行
void LcdSetCursor(unsigned char x,unsigned char y);// 函数功能是:从坐标(x,y)开始显示字符串str
// 注意这个函数不能跨行显示,因为显存地址是不连续的
// 其实我们可以封装出一个能够折行显示的函数的
void LcdShowStr(unsigned char x,unsigned char y,unsigned char *str);
//void lcd1602_show_string(u8 x,u8 y,u8 *str);
// 初始化LCD,使之能够开始正常工作
void InitLcd1602();#endif
4.lcd与测温联调
1.将double转换为字符串
因为LCD中的显示函数要求输入的是字符串
此处,我们为了设置方便,要求温度只能精确到小数点后一位。
将数值转换为字符串的实质,其实就是将单独一个数值强制类型转化为unsigned char
//显示类似于24.5这种的double类型的数字
/**思路:1)先将其一位一位显示出来2)然后将其强制类型转换为unsigned char3)记得最后有一个'\0'
*/
void LcdShowDouble(unsigned char x,unsigned char y,double d){// 第一步:将double d转成字符串strunsigned char str[5] = {0};// 第1步:先由double的25.4得到uint的254unsigned int tmp = (unsigned int)(d * 10); unsigned char c = 0;// 第2步:由/和%操作来得到2、5、4// 第3步:将2、5、4对应的ASCII码放到字符串中去,完成c = (unsigned char)(tmp / 100);str[0] = c + 48;tmp = tmp % 100; // 运算后tmp=54c = (unsigned char)(tmp / 10); // c = 5str[1] = c + 48;str[2] = '.';tmp = tmp % 10; // 运算后tmp=4c = (unsigned char)(tmp / 1); // c = 4str[3] = c + 48;str[4] = '\0';// 第二步:显示strLcdShowStr(x, y, str);
}
void main(void){double t=35.4;InitLcd1602();//LcdShowStr(0,0,"nihaoliaoxiaoyi");LcdShowDouble(0,0,t);
}
2.输出摄氏度符号
3.注意点
我们在定义摄氏度符号的时候发现,在最后的位置出现奇怪的符号,是因为我们没有手动的添加结束符【‘\0’】
5.完整代码
lcd1602.c
#include"lcd1602.h"
/**显示屏:显示温度和时间
*/void Delay2000ms() //@11.0592MHz
{unsigned char i, j, k;_nop_();_nop_();i = 85;j = 12;k = 155;do{do{while (--k);} while (--j);} while (--i);
}/************ 低层时序 ********************************/
void Read_Busy() //忙检测函数,判断bit7是0,允许执行;1禁止
{unsigned char sta; //LCD1602_DB = 0xff;LCD1602_RS = 0;LCD1602_RW = 1;do{LCD1602_EN = 1;sta = LCD1602_DB;LCD1602_EN = 0; //使能,用完就拉低,释放总线}while(sta & 0x80);
}void Lcd1602_Write_Cmd(unsigned char cmd) //写命令
{Read_Busy();LCD1602_RS = 0;LCD1602_RW = 0; LCD1602_DB = cmd;LCD1602_EN = 1;LCD1602_EN = 0;
}void Lcd1602_Write_Data(unsigned char dat) //写数据
{Read_Busy();LCD1602_RS = 1;LCD1602_RW = 0;LCD1602_DB = dat;LCD1602_EN = 1;LCD1602_EN = 0;
}/************* 高层时序 ******************************/
// 本函数用来设置当前光标位置,其实就是设置当前正在编辑的位置,
// 其实就是内部的数据地址指针,其实就是RAM显存的偏移量
// x范围是0-15,y=0表示上面一行,y=1表示下面一行
void LcdSetCursor(unsigned char x,unsigned char y) //坐标显示
{unsigned char addr;if(y == 0)addr = 0x00 + x;elseaddr = 0x40 + x;Lcd1602_Write_Cmd(addr|0x80);
}// 函数功能是:从坐标(x,y)开始显示字符串str
// 注意这个函数不能跨行显示,因为显存地址是不连续的
// 其实我们可以封装出一个能够折行显示的函数的
void LcdShowStr(unsigned char x,unsigned char y,unsigned char *str) //显示字符串
{LcdSetCursor(x,y); //当前字符的坐标while(*str != '\0'){Lcd1602_Write_Data(*str++);}}// 初始化LCD,使之能够开始正常工作
void InitLcd1602() //1602初始化
{Lcd1602_Write_Cmd(0x38); //打开,5*8,8位数据Lcd1602_Write_Cmd(0x0c); // 打开显示并且无光标//Lcd1602_Write_Cmd(0x0f); // 打开显示并且光标闪烁Lcd1602_Write_Cmd(0x06);Lcd1602_Write_Cmd(0x01); //清屏
}/**
为了显示ds18b20中获取到的浮点数温度
使其可以在显示屏上显示
*/
// 显示类似于24.5这种的double类型的数字
void LcdShowFloat(unsigned char x, unsigned char y, float d)
{// 第一步:将double d转成字符串strunsigned char str[5] = {0};// 第1步:先由double的25.4得到uint的254unsigned int tmp = (unsigned int)(d * 10); unsigned char c = 0;// 第2步:由/和%操作来得到2、5、4// 第3步:将2、5、4对应的ASCII码放到字符串中去,完成c = (unsigned char)(tmp / 100);str[0] = c + 48;tmp = tmp % 100; // 运算后tmp=54c = (unsigned char)(tmp / 10); // c = 5str[1] = c + 48;str[2] = '.';tmp = tmp % 10; // 运算后tmp=4c = (unsigned char)(tmp / 1); // c = 4str[3] = c + 48;str[4] = '\0';// 第二步:显示strLcdShowStr(x, y, str);}
七、DS1302的移植和联调(实时时钟)
1.原理图和接线
DS1302 P3.4 P3.6
2.时间的封装(使用结构体)
(1)一个时间=年 月 日 分 秒 周几(相比于温度是一个简单变量)
(2)C语言提供结构体这种技巧,来处理复杂变量
(3)区分2个概念:结构体类型【不占内存】和结构体变量【占内存】
(4)结构体这种语法使用时有套路:
第一步:先定义结构体类型
第二步:用类型去生产结构体变量
第三步:使用结构体变量(其实就是使用结构体变量肚子里包着的内容)
3.使用结构体读取时间前的准备工作
1.结构体的定义
//封装出来的一个表示时间的结构体类型
//类型不占内存,也不表示一个具体时间,但是类型可以用来生成时间
//每一个时间变量占一定的内存,每一个时间变量就代表一个具体的时间
struct time_t
{unsigned int year;//2023unsigned char mon;//1-12unsigned char date;//1-31【几号】unsigned char hour;//0-23unsigned char min;//0-59unsigned char sec;//0-59unsigned char day;//0-6【星期几】
};
2.宏定义
之前我们使用DS1302的时候,是使用数组来记录寄存器地址。此处,我们使用结构体,则使用宏定义来实现使得CPU运行时间得到提升
// 用来存储读取的时间的,格式是:秒分时日月周年
//unsigned char code READ_RTC_ADDR[7] = {0x81, 0x83, 0x85, 0x87, 0x89, 0x8b, 0x8d};
//unsigned char code WRITE_RTC_ADDR[7] = {0x80, 0x82, 0x84, 0x86, 0x88, 0x8a, 0x8c};
//用宏定义的方式来定义时间的寄存器地址
// 用宏定义的方式来定义时间的寄存器地址 格式是:秒分时日月周年
//写地址
#define REG_ADDR_YEAR_WRITE 0x8c
#define REG_ADDR_MON_WRITE 0x88
#define REG_ADDR_DATE_WRITE 0x86
#define REG_ADDR_HOUR_WRITE 0x84
#define REG_ADDR_MIN_WRITE 0x82
#define REG_ADDR_SEC_WRITE 0x80
#define REG_ADDR_DAY_WRITE 0x8a//读地址
#define REG_ADDR_YEAR_READ (REG_ADDR_YEAR_WRITE+1)
#define REG_ADDR_MON_READ (REG_ADDR_MON_WRITE+1)
#define REG_ADDR_DATE_READ (REG_ADDR_DATE_WRITE+1)
#define REG_ADDR_HOUR_READ (REG_ADDR_HOUR_WRITE+1)
#define REG_ADDR_MIN_READ (REG_ADDR_MIN_WRITE+1)
#define REG_ADDR_SEC_READ (REG_ADDR_SEC_WRITE+1)
#define REG_ADDR_DAY_READ (REG_ADDR_DAY_WRITE+1)
3.读取函数
// 从ds1302的内部寄存器addr读出一个值,作为返回值
static unsigned char ds1302_read_reg(unsigned char addr)
{unsigned char i = 0;unsigned char dat = 0; // 用来存储读取到的一字节数据的unsigned char tmp = 0;// 第1部分: 时序起始SCLK = 0;delay();RST = 0;delay();RST = 1; // SCLK为低时,RST由低变高,意味着一个大的周期的开始delay();// 第2部分: 写入要读取的寄存器地址,addrfor (i=0; i<8; i++){dat = addr & 0x01; // SPI是从低位开始传输的DSIO = dat; // 把要发送的bit数据丢到IO引脚上去准备好SCLK = 1; // 制造上升沿,让DS1302把IO上的值读走delay(); // 读走之后,一个小周期就完了SCLK = 0; // 把SCLK拉低,是为了给下一个小周期做准备delay();addr >>= 1; // 把addr右移一位}// 第3部分: 读出一字节DS1302返回给我们的值dat = 0;for (i=0; i<8; i++){// 在前面向ds1302写入addr的最后一个bit后,ds1302就会将读取到的寄存器值// 的第一个bit放入到IO引脚上,所以我们应该先读取IO再制造下降沿然后继续// 读取下一个bittmp = DSIO;dat |= (tmp << i); // 读出来的数值是低位在前的SCLK = 1; // 由于上面SCLK是低,所以要先拉到高delay();SCLK = 0; // 拉低SCLK制造一个下降沿delay();}// 第4部分: 时序结束SCLK = 0; // SCLK拉低为了后面的周期时初始状态是对的delay();RST = 0; // RST拉低意味着一个大周期的结束delay();// 第5部分:解决读取时间是ff的问题DSIO = 0;return dat;
}// 用结构体方式来实现的读取时间的函数
// READ_RTC_ADDR格式是:秒分时日月周年
void ds1302_read_time_struct(void)
{mytime.year = ds1302_read_reg(REG_ADDR_YEAR_READ) + 2000;mytime.mon = ds1302_read_reg(REG_ADDR_MON_READ);mytime.date = ds1302_read_reg(REG_ADDR_DATE_READ);mytime.hour = ds1302_read_reg(REG_ADDR_HOUR_READ);mytime.min = ds1302_read_reg(REG_ADDR_MIN_READ);mytime.sec = ds1302_read_reg(REG_ADDR_SEC_READ);mytime.day = ds1302_read_reg(REG_ADDR_DAY_READ);
}
4.日期转换为字符串
因为我们获取到的数值是一串字符串,而我们要的是4位的年,2位的日期
所以我们为了把它区分开来,同时将其转换为字符串输出到lcd1602上
lcd1602.c
我们封装了2个函数分别将数值转换为字符串
// 实现一个子函数,将十进制的4位整数转成一个字符串
void Int2Str4(unsigned int dat, unsigned char str[], unsigned char index)
{unsigned char c = 0;// 假设dat=2017c = dat / 1000; // c = 2str[index+0] = c + '0'; // 第1位入库dat %= 1000; // dat = 017c = dat / 100; // c = 0str[index+1] = c + '0'; // 第2位入库dat %= 100; // dat = 17c = dat / 10; // c = 1str[index+2] = c + '0'; // 第3位入库dat %= 10; // dat = 7c = dat / 1; // c = 7str[index+3] = c + '0'; // 第4位入库
}// 实现一个子函数,将十进制的2位整数转成一个字符串
void Int2Str2(unsigned int dat, unsigned char str[], unsigned char index)
{unsigned char c = 0;// 假设dat=17c = dat / 10; // c = 1str[index+0] = c + '0'; // 第1位入库dat %= 10; // dat = 7c = dat / 1; // c = 7str[index+1] = c + '0'; // 第2位入库
}// LCD1602上显示time_t
void LcdShowTimeT(unsigned char x, unsigned char y, struct time_t ti)
{// 第一步:将struct time_t ti转成字符串str// 格式:20170406113515-4unsigned char str[17] = {0};// 格式化time_t里面的各个时间,然后填充str// 年的格式化,str[0]-str[3]放年字符串Int2Str4(ti.year, str, 0);// 月的格式化 ,str[4]-str[5]Int2Str2(ti.mon, str, 4);// 日的格式化 ,str[6]-str[7]Int2Str2(ti.date, str, 6);// 时的格式化 ,str[8]-str[9]Int2Str2(ti.hour, str, 8);// 分的格式化 ,str[10]-str[11]Int2Str2(ti.min, str, 10);// 秒的格式化 ,str[12]-str[13]Int2Str2(ti.sec, str, 12);// 填充了一个'-' str[14]str[14] = '-';// 周几的格式化 ,str[15]str[15] = ti.day + '0';str[16] = '\0';// 第二步:显示strLcdShowStr(x, y, str);
}
main.c
unsigned char c=0;InitLcd1602();LcdShowStr(0,0,"temp=");//0-4//显示:°CLcdShowStr(9,0,du);while(1){//读取温度并显示t=Ds18b20ReadTemp2();//显示温度值LcdShowDouble(5,0,t);//读取时间并且显示ds1302_read_time_struct();LcdShowTimeT(0,1,mytime);}
5.进制转换
【单片机】13-实时时钟DS1302-CSDN博客
BCD码与十进制数间转换_bcd码转十进制-CSDN博客
BCD码:看起来像十进制,但是实际上是十六进制
我们想要机器可以识别十六进制,则应该将其外观修改为真正的十六进制【也就是把外观和本质都修改为十六进制】bcd---》hex
比如:我们通过ds1302获得原始数据就为BCD码【此时:0x24实际上是十进制,所以我们要将其转换为0x18(这个才是真正的十六进制)】
我们在学习ds1302的时候说到:读出的其实是BCD码,所以才会产生乱码。故我们要进行转换才可以正确的显示。
// 实现2个子函数,分别实现从bcd码转十六进制,和十六进制转bcd码
unsigned char bcd2hex(unsigned char bcd)
{// 譬如我们现在要把bcd码0x24转成24(0x18)// 思路就是分2步// 第1步,先从0x24得到2和4// ((bcd & 0xf0) >> 4) 高4位,也就是2// (bcd & 0x0f) 低4位,也就是4// 第2步,由2*10+4得到24return (((bcd & 0xf0) >> 4) * 10 + (bcd & 0x0f));
}unsigned char hex2bcd(unsigned char hex)
{// 就是要把24转成0x24// 第一步,先由24得到2和4// (24 / 10) 就是2, (24 % 10)就是4// 第二步,再组合成0x24return (((hex / 10) << 4) | (hex % 10));
}
// 用结构体方式来实现的读取时间的函数
// READ_RTC_ADDR格式是:秒分时日月周年
void ds1302_read_time_struct(void)
{mytime.year = bcd2hex(ds1302_read_reg(REG_ADDR_YEAR_READ)) + 2000;mytime.mon = bcd2hex(ds1302_read_reg(REG_ADDR_MON_READ));mytime.date = bcd2hex(ds1302_read_reg(REG_ADDR_DATE_READ));mytime.hour = bcd2hex(ds1302_read_reg(REG_ADDR_HOUR_READ));mytime.min = bcd2hex(ds1302_read_reg(REG_ADDR_MIN_READ));mytime.sec = bcd2hex(ds1302_read_reg(REG_ADDR_SEC_READ));mytime.day = bcd2hex(ds1302_read_reg(REG_ADDR_DAY_READ));
}
6.时间设置
我们要向DS1302中写入时间,让其按照这个时间接着往下走
因为我们传入的时间是十进制,要被DS1302所识别着应该转换为BCD码才可以,所以使用hex2bcd。【因为BCD码只能识别0-256,所以我们需要在“年”后面-2000】
ds1302.c
// 向ds1302的内部寄存器addr写入一个值value
static void ds1302_write_reg(unsigned char addr, unsigned char value)
{unsigned char i = 0;unsigned char dat = 0;// 第1部分: 时序起始SCLK = 0;delay();RST = 0;delay();RST = 1; // SCLK为低时,RST由低变高,意味着一个大的周期的开始delay();// 第2部分: 写入第1字节,addrfor (i=0; i<8; i++){dat = addr & 0x01; // SPI是从低位开始传输的DSIO = dat; // 把要发送的bit数据丢到IO引脚上去准备好SCLK = 1; // 制造上升沿,让DS1302把IO上的值读走delay(); // 读走之后,一个小周期就完了SCLK = 0; // 把SCLK拉低,是为了给下一个小周期做准备delay();addr >>= 1; // 把addr右移一位}// 第3部分: 写入第2字节,valuefor (i=0; i<8; i++){dat = value & 0x01; // SPI是从低位开始传输的DSIO = dat; // 把要发送的bit数据丢到IO引脚上去准备好SCLK = 1; // 制造上升沿,让DS1302把IO上的值读走delay(); // 读走之后,一个小周期就完了SCLK = 0; // 把SCLK拉低,是为了给下一个小周期做准备delay();value = value >> 1; // 把addr右移一位}// 第4部分: 时序结束SCLK = 0; // SCLK拉低为了后面的周期时初始状态是对的delay();RST = 0; // RST拉低意味着一个大周期的结束delay();
}//用结构体方式在实现时间的修改函数
//本函数用于向DS1302中写入一个时间t1
void ds1302_write_time_struct(struct time_t t1){ds1302_write_reg(0x8E, 0x00); // 去掉写保护// 依次写各个时间寄存器ds1302_write_reg(REG_ADDR_YEAR_WRITE, (hex2bcd(t1.year - 2000)));ds1302_write_reg(REG_ADDR_MON_WRITE, (hex2bcd(t1.mon)));ds1302_write_reg(REG_ADDR_DATE_WRITE, (hex2bcd(t1.date)));ds1302_write_reg(REG_ADDR_HOUR_WRITE, (hex2bcd(t1.hour)));ds1302_write_reg(REG_ADDR_MIN_WRITE, (hex2bcd(t1.min)));ds1302_write_reg(REG_ADDR_SEC_WRITE, (hex2bcd(t1.sec)));ds1302_write_reg(REG_ADDR_DAY_WRITE, (hex2bcd(t1.day)));ds1302_write_reg(0x8E, 0x80); // 打开写保护}
main.c
struct time_t t1;float t;InitLcd1602();LcdShowStr(0,0,"temp=");LcdShowStr(9,0,du);t1.year = 2023;t1.mon = 10;t1.date = 14;t1.hour = 21;t1.min = 6;t1.sec = 45;t1.day = 6;//将时间写入寄存器ds1302_write_time_struct(t1);while(1){//显示温度t=ds18b20_read_temperture();LcdShowFloat(5,0,t);// 读取时间并显示ds1302_read_time_struct();LcdShowTimeT(0, 1, mytime);}
7.单独修改时间
//只修改年份
void ds1302_write_time_year(unsigned int year){ds1302_write_reg(0x8E, 0x00); // 去掉写保护// 依次写各个时间寄存器ds1302_write_reg(REG_ADDR_YEAR_WRITE, (hex2bcd(year - 2000)));ds1302_write_reg(0x8E, 0x80); // 打开写保护}//只修改月份
void ds1302_write_time_month(unsigned char month){ds1302_write_reg(0x8E, 0x00); // 去掉写保护// 依次写各个时间寄存器ds1302_write_reg(REG_ADDR_MON_WRITE, (hex2bcd(month - 2000)));ds1302_write_reg(0x8E, 0x80); // 打开写保护}//只修改日
void ds1302_write_time_date(unsigned char date){ds1302_write_reg(0x8E, 0x00); // 去掉写保护// 依次写各个时间寄存器ds1302_write_reg(REG_ADDR_DATE_WRITE, (hex2bcd(date - 2000)));ds1302_write_reg(0x8E, 0x80); // 打开写保护}//只修改时
void ds1302_write_time_hour(unsigned char hour){ds1302_write_reg(0x8E, 0x00); // 去掉写保护// 依次写各个时间寄存器ds1302_write_reg(REG_ADDR_HOUR_WRITE, (hex2bcd(hour - 2000)));ds1302_write_reg(0x8E, 0x80); // 打开写保护}//只修改分
void ds1302_write_time_min(unsigned char min){ds1302_write_reg(0x8E, 0x00); // 去掉写保护// 依次写各个时间寄存器ds1302_write_reg(REG_ADDR_MIN_WRITE, (hex2bcd(min - 2000)));ds1302_write_reg(0x8E, 0x80); // 打开写保护}//只修改秒
void ds1302_write_time_sec(unsigned char sec){ds1302_write_reg(0x8E, 0x00); // 去掉写保护// 依次写各个时间寄存器ds1302_write_reg(REG_ADDR_SEC_WRITE, (hex2bcd(sec - 2000)));ds1302_write_reg(0x8E, 0x80); // 打开写保护
9.注意点:
1)从DS1302中获取到的数据是unsigned char ,所以不能超过255,但是我们年份是四位数,所以我们在读取时,应该直接在后面加上2000
2)我们从DS1302中获取到数据是BCD码,所以我们需要将其转换为十六进制
3)我们在显示屏上显示数据,因为我们获取到的是数值,但是要转换为字符串才可以进行展示,所以我们要将获取到的时间转换为字符串
// LCD1602上显示time_t
void LcdShowTimeT(unsigned char x, unsigned char y, struct time_t ti)
{// 第一步:将struct time_t ti转成字符串str// 格式:20170406113515-4unsigned char str[17] = {0};// 格式化time_t里面的各个时间,然后填充str// 年的格式化,str[0]-str[3]放年字符串Int2Str4(ti.year, str, 0);// 月的格式化 ,str[4]-str[5]Int2Str2(ti.mon, str, 4);// 日的格式化 ,str[6]-str[7]Int2Str2(ti.date, str, 6);// 时的格式化 ,str[8]-str[9]Int2Str2(ti.hour, str, 8);// 分的格式化 ,str[10]-str[11]Int2Str2(ti.min, str, 10);// 秒的格式化 ,str[12]-str[13]Int2Str2(ti.sec, str, 12);// 填充了一个'-' str[14]str[14] = '-';// 周几的格式化 ,str[15]str[15] = ti.day + '0';str[16] = '\0';// 第二步:显示strLcdShowStr(x, y, str);
}
4)因为年份和其他时间单位的位数不同,所以我们需要将其分开,写成2个函数
// 实现一个子函数,将十进制的4位整数转成一个字符串
void Int2Str4(unsigned int dat, unsigned char str[], unsigned char index)
{unsigned char c = 0;// 假设dat=2017c = dat / 1000; // c = 2str[index+0] = c + '0'; // 第1位入库dat %= 1000; // dat = 017c = dat / 100; // c = 0str[index+1] = c + '0'; // 第2位入库dat %= 100; // dat = 17c = dat / 10; // c = 1str[index+2] = c + '0'; // 第3位入库dat %= 10; // dat = 7c = dat / 1; // c = 7str[index+3] = c + '0'; // 第4位入库
}// 实现一个子函数,将十进制的2位整数转成一个字符串
void Int2Str2(unsigned int dat, unsigned char str[], unsigned char index)
{unsigned char c = 0;// 假设dat=17c = dat / 10; // c = 1str[index+0] = c + '0'; // 第1位入库dat %= 10; // dat = 7c = dat / 1; // c = 7str[index+1] = c + '0'; // 第2位入库
}
5)写入时间,因为我们传入的是十六进制,但是为了让DS1302识别,所以需要将其转换为bcd码
//十六进制转换为bcd码
unsigned char hex2bcd(unsigned char hex)
{// 就是要把24转成0x24// 第一步,先由24得到2和4// (24 / 10) 就是2, (24 % 10)就是4// 第二步,再组合成0x24return (((hex / 10) << 4) | (hex % 10));
}
6)写入时间函数
//用结构体方式在实现时间的修改函数
//本函数用于向DS1302中写入一个时间t1
void ds1302_write_time_struct(struct time_t t1){ds1302_write_reg(0x8E, 0x00); // 去掉写保护// 依次写各个时间寄存器ds1302_write_reg(REG_ADDR_YEAR_WRITE, (hex2bcd(t1.year - 2000)));ds1302_write_reg(REG_ADDR_MON_WRITE, (hex2bcd(t1.mon)));ds1302_write_reg(REG_ADDR_DATE_WRITE, (hex2bcd(t1.date)));ds1302_write_reg(REG_ADDR_HOUR_WRITE, (hex2bcd(t1.hour)));ds1302_write_reg(REG_ADDR_MIN_WRITE, (hex2bcd(t1.min)));ds1302_write_reg(REG_ADDR_SEC_WRITE, (hex2bcd(t1.sec)));ds1302_write_reg(REG_ADDR_DAY_WRITE, (hex2bcd(t1.day)));ds1302_write_reg(0x8E, 0x80); // 打开写保护}
八、蜂鸣器
1.接线和原理图
P1^7
2.函数封装
1.初始化
void buzzer_init(){TMOD = 0x01; // T0使用16bit定时器//决定中断的时间TL0 = N % 256;TH0 = N / 256;TR0 = 1; // T0打开开始计数ET0 = 1; // T0中断允许EA = 1; // 总中断允许BUZZER = 1;// 设置响和不响的周期时间/**我们初始化频率为:4KHZ则应该是1/4000HZ的周期1s=1000ms=1000 000us所以1/4000=1000 000/4000=1000/4=256us*/count = TIMELEN; //600*256usflag = 0;
}
2.中断函数
//timer0的isr,在这里对引脚进行电平反反转以让蜂鸣器响
void timer0_isr(void) interrupt 1 using 1
{TL0 = N % 256;TH0 = N / 256;if (count-- == 0){// 说明到了翻转的时候了// count = 600;if (flag == 0){// 之前是处于有声音的,说明本次是从有声音到无声音的翻转flag = 1;//*10:表示不响的时间是响的时间的10倍count = TIMELEN*10; //这里的count数量决定蜂鸣器【不响】的时间长短}else{// 之前是处于没声音的,说明本次是从没声音到有声音的翻转flag = 0;BUZZER = !BUZZER;count = TIMELEN; //这里的count数量决定蜂鸣器【响】的时间长短}}//时间未到else{// 常规情况,也就是不反转时if (flag == 0){BUZZER = !BUZZER; // 4999次声音}else{// 空的就可以,因为不进行任何IO操作就是没声音}}}
3.提升
1.中断函数优化
//宏定义
//设置蜂鸣器的输出频率为XKHZ
#define XKHZ 4 // 要定多少Khz,就直接写这里
#define US (500/XKHZ)
#define N (65535-US)//N=(65535-(500/XKHZ))static unsigned char xKHZ=0; //用于获取N的数值,可以计算响或者不响
将初始化函数和中断函数中的N修改,并且将count删除
//让蜂鸣器一直响的isr
void timer0_isr(void) interrupt 1 using 1
{//N=(65535-(500/XKHZ))TL0 =(65535-(500/xKHZ)) % 256;TH0 = (65535-(500/xKHZ)) / 256;// 常规情况,也就是不反转时if (flag == 0) //flag=0表示响{BUZZER = !BUZZER; // 4999次声音}
}
//buzzer的初始化
void buzzer_init(){TMOD = 0x01; // T0使用16bit定时器//决定中断的时间
//N=(65535-(500/XKHZ))TL0 =(65535-(500/xKHZ)) % 256;TH0 = (65535-(500/xKHZ)) / 256;TR0 = 1; // T0打开开始计数ET0 = 1; // T0中断允许EA = 1; // 总中断允许BUZZER = 1;flag = 0; // flag = 0表示有声音,flag = 1表示没声音xKHZ=4;//默认是4khz
}
2.一直响/不响
//让蜂鸣器开始响
void buzzer_start(void){flag=0;
}//让蜂鸣器停止响
void buzzer_stop(void){flag=1;
}//设置蜂鸣器响的频率
void buzzer_freq_set(unsigned char tmp){xKHZ=tmp;//修改蜂鸣器的频率
}
3.完整代码
#include"buzzer.h"//buzzer的初始化
void buzzer_init(){xKHZ=4;//默认是4khzTMOD = 0x01; // T0使用16bit定时器//决定中断的时间
//N=(65535-(500/XKHZ))TL0 =(65535-(500/xKHZ)) % 256;TH0 = (65535-(500/xKHZ)) / 256;TR0 = 1; // T0打开开始计数ET0 = 1; // T0中断允许EA = 1; // 总中断允许BUZZER = 1;flag = 1; // flag = 0表示有声音,flag = 1表示没声音}//让蜂鸣器一直响的isr
void timer0_isr(void) interrupt 1 using 1
{//N=(65535-(500/XKHZ))TL0 =(65535-(500/xKHZ)) % 256;TH0 = (65535-(500/xKHZ)) / 256;// 常规情况,也就是不反转时if (flag == 0) //flag=0表示响{BUZZER = !BUZZER; // 4999次声音}
}//让蜂鸣器开始响
void buzzer_start(void){flag=0;
}//让蜂鸣器停止响
void buzzer_stop(void){flag=1;
}//设置蜂鸣器响的频率
void buzzer_freq_set(unsigned char tmp){xKHZ=tmp;//修改蜂鸣器的频率
}
4.蜂鸣器频率设置
上面的代码我们无法修改频率