文章目录
1、前言
设计了一个基于51单片机的室内空气净化系统,所选用单片机型号为STC89C52,硬件部分包括字符型液晶显示屏lcd1602,空气检测传感器MQ135,小功率步进电机,排气扇,蜂鸣警报器,独立按键等。将程序烧入单片机进行验证,达到了预期的效果。
2、设计思路
本系统主要由4个模块所组成:
(1)数据采集模块
(2)数据/阈值显示模块
(3)阈值设置模块
(4)蜂鸣报警模块
系统设计简图如下图所示:
系统工作的主要流程如下:传感器MQ135采集数据接入单片机上的外部模拟量数据采集引脚(AIN3),经由xpt2046芯片进行AD模数转换,STC89C52对采集的数据作进一步处理转化成气体浓度(ppm)。当前环境下的气体浓度会与程序设计的阈值一并显示在LCD1602字符型液晶显示屏上。当所监测气体浓度超出阈值时,蜂鸣报警模块开始工作,发出蜂鸣警报声并开启排气扇进行排气。此外也可以人为地对阈值进行设置,以满足不同环境下的需求。
3、 系统硬件设计
3.1、MQ135传感器
1、简述:NQ135是一款对氨气、硫化物、苯系蒸汽等均有着高灵敏度的半导体气敏元件。其凭借着成本低、电路结构简单、功耗低等优点被广泛应用于日常生活生产中。
MQ135传感器实物图如下(背面):
2、重要技术指标:MQ135传感器的重要技术指标如下图所示:
3、引脚功能:从上文关于MQ135的实物图我们可以看出,MQ135一共有4个外接引脚,这4个引脚的功能分别是:
VCC:接5V直流电源输入
GND:接地
DO:模拟信号输出
AO:数字信号(TTL)输出
在本项目中,需要用杜邦线将VCC和GND引脚与单片机电源的正负极相接,DO引脚与开发板AIN3引脚相接(只采集模拟量),AO引脚不需要与任何引脚相接。
4、使用注意事项:在实际使用中应该考虑以下两点:(1)VCC与GND引脚不用与电源正负极反接,否则容易烧坏芯片。(2)从上文标准测试条件中可以看出,在使用MQ135之前应该先将MQ135传感器进行预热(即插电工作一段时间)后方可正常工作。
3.2、LCD1602液晶
1、简述:作为各类单片机实物设计的常用显示屏,LCD1602是一种专门用于显示字母、数字、符号等点阵式LCD,其1602是指LCD显示的内容为16X2,即可以显示两行,每行16个字符(1个汉字占用2个字符)。
2、LCD1602关键引脚说明:
RS :0=输入指令,1=输入数据
RW :0=向LCD写入指令或数据;1=从LCD读取信息
E :使能信号,1时读取信息,1->0(下降沿)执行指令
3、LCD1602基本操作时序:
1.1读状态:输入:RS=L,RW=H,E=H
----输出:D0~D7=状态字
1.2写指令:输入:RS=L,RW=L,D0~D7=指令码,E=高脉冲—>低脉冲
----输出:无
1.3读数据:输入:RS=H,RW=H,E=H
----输出:D0~D7=数据
1.4写数据:输入:RS=H,RW=L,D0~D7=数据码,E=高脉冲—>低脉冲
----输出:无
控制时序图:
(1)读操作时序图:
(2)写操作时序图:
4、LCD1602常用指令
LCD在使用的过程中,可以在RS=0、RW=0的情况下,向LCM写入一个字节的控制指令。使用的控制指令一共六个类别
(1)01H:清除DDRAM(显示数据存储器)的所有单元,光标被移动到屏幕左上角。
(2)02H:DDRAM所有单元的内容不变,光标移至左上角。
(3)输入方式设置(EnterModeSet),这些指令规定了两个方面:一是写入一个DDRAM单元后,地址指针如何改变(加一还是减一);二是屏幕上的内容是否滚动。
04H:写入DDRAM后,地址指针减一,比如第一个字符写入8FH,则下一个字符会写入8EH;屏幕上的内容不滚动。
05H:写入DDRAM后,地址指针减一,同上一种情况;每一个字符写入以后,屏幕上的内容向右滚动一个字符位。
06H:写入DDRAM后,地址指针加一,比如第一个字符写入80H,则下一个字符会写入81H;屏幕上的内容也是不滚动。这应该是最常用的一种显示方式。
07H:写入DDRAM后,地址指针加一,同上一种情况;每一个字符写入以后,屏幕上的内容向左滚动一个字符位。
(4)屏幕开关、光标开关、闪烁开关。
08H、09H、0AH、0BH:关闭显示屏,实质上是不把DDRAM中的内容对应显示在屏幕上,对DDRAM的操作还是在进行的,执行这条指令,接着对 DDRAM进行写入,屏幕上没有任何内容,但是接着执行下面的某条指令,就能看到刚才屏幕关闭期间,对DDRAM操作的效果了。
0cH:打开显示屏,不显示光标,光标所在位置的字符不闪烁。
0dH:打开显示屏,不显示光标,光标所在位置的字符闪烁。
0eH:打开显示屏,显示光标,光标所在位置的字符不闪烁。
0fH:打开显示屏,显示光标,光标所在位置的字符闪烁。
关于光标的位置:光标所在的位置指示了下一个被写入的字符所处的位置,加入在写入下一个字符前没有通过指令设置DDRAM的地址,那么这个字符就应该显示在光标指定的地方。
(5)设置光标移动(本质就是AC的增加还是减少)、整体画面是否滚动。
10H:每输入一次该指令,AC就减一,对应了光标向左移动一格。整体的画面不滚动。
14H:每输入一次该指令,AC就加一,对应了光标向右移动一格。整体的画面不滚动。
18H:每输入一次该指令,整体的画面就向左滚动一个字符位。
1CH:每输入一次该指令,整体的画面就向右滚动一个字符位。画面在滚动的时候,每行的首尾是连在一起的,
也就是每行的第一个字符,若左移25次,就会显示在该行的最后一格。在画面滚动的过程中,AC的值也是变化的。
(6)显示模式设定指令,设定了显示几行,显示什么样的点阵字符,数据总线占用几位。
20H:4位总线,单行显示,显示5×7的点阵字符。
24H:4位总线,单行显示,显示5×10的点阵字符。
28H:4位总线,双行显示,显示5×7的点阵字符。
2CH:4位总线,双行显示,显示5×10的点阵字符。
30H:8位总线,单行显示,显示5×7的点阵字符。
34H:8位总线,单行显示,显示5×10的点阵字符。
38H:8位总线,双行显示,显示5×7的点阵字符。这是最常用的一种模式。
3CH:8位总线,双行显示,显示5×10的点阵字符。
3.3、XPT2046芯片
1、简述:XPT2046芯片是一款典型的逐次逼近型模数转换器(SAR ADC),包含了采样/保持、模数转换、串口数据输出等功能。
2、典型应用电路:
3、重要引脚功能描述:
CS:片选信号,控制转换时序和使能串行输入输出寄存器,当CS为低电平时芯片正常工作。
DIN:串行数据输入端,当CS为低电平时,数据在DCLK上升沿时串行输入。
DCLK:外部时钟信号输入
DOUT:串行数据输出端,当CS为低电平时,数据在DCLK下降沿时串行输出。
4、典型工作方式:
XPT2046芯片的典型工作方式如下图所示:
工作方式解析:前 8 个时钟用来通过DIN引脚输入控制字节,接着的12 个时钟周期将完成真正的模数转换,剩下的3个多时钟周期将用来完成被转换器忽略的最后字节(DOUT置低)。
5、外接典型应用电路:
有4个外部接口:
AIN0:检测转换电位器模拟信号,信号采集地址为0x94或者0xB4
AIN1:检测转换热敏电阻模拟信号,信号采集地址为0xD4
AIN2:检测转换光敏电阻模拟信号,信号采集地址为0xA4
AIN3:检测转换AIN3通道上模拟信号,信号采集地址为0xE4
4、 系统软件设计
4.1、算法状态机(ASM)图
针对上文关于系统模块的描述以及项目具体的功能需要,为了明晰编程思路,画出算法状态机(ASM)图如下所示:
4.2、传感器数据处理
从MQ135空气质量传感器所检测的数值需要经由模拟量—>数字量—>气体浓度的变换,其中数字量—>气体浓度遵循这如下规律:没有被测气体的环境,设定传感器输出电压值为参考电压,这时,aout端的电压在1v左右,当传感器检测到被测气体时,电压每升高0.1v,实际被测气体的浓度增加20ppm(简单的说:1ppm=1mg/kg=1mg/l=1×10-6 常用来表示气体浓度,或者溶液浓度),根据这个参数就可以在单片机里面将测得的模拟量电压值转换为浓度值。然后再经由AIN3所测得的数值与真实的数值进行线性拟合得出相应表达式:
value(ppm) = (int)(0.236 * (value(ad) - 12.8623));
由于所测数据点样本较少,得出的拟合结果公式可能会与真实值存在小范围的偏差,但经过实际测试发现并不影响实际使用,在一定0~5V的电压范围内所测的气体浓度与真实值接近。
4.3、工程具体代码
此项工程的具体代码如下所示,注意单片机型号!不同型号的单片机的相应功能所对应的具体引脚值会有所不同,具体请查看单片机开发原理图。
XPT2046.H
/**
*@file XPT2046.H
*@brief 封装模数转换器xpt2046芯片的相应功能引脚,声明相应函数
*@details 与51单片机采用串口通信的方式进行数据交换
*@author 青渡
*@version V1.1.0
*@date 2021-04-05
*/
#ifndef __xpt2046_H_
#define __xpt2046_H_
#include "reg52.h"
#include "intrins.h"
#ifndef u8
#define u8 unsigned char
#endif
#ifndef u16
#define u16 unsigned int
#endif
/**输出引脚*/
sbit DOUT = P3^7;
/**时钟引脚*/
sbit CLK = P3^6;
/**输入引脚*/
sbit DIN = P3^4;
/**片选引脚*/
sbit CS = P3^5;
u16 read_ad_data(u8 cmd);
u16 SPI_read();
void SPI_write(u8 dat);
#endif
LCD1602.H
/**
*@file LCD1602.H
*@brief 封装字符型液晶lcd1602的相应功能引脚,声明相应函数
*@details 注意所采用的lcd1602是4位输入还是8位输入,如果是4位输入
则应该添加语句#define LCD1602_4PINS
*@author 青渡
*@version V1.1.0
*@date 2021-04-05
*/
#ifndef __lcd1602_H_
#define __lcd1602_H_
#include"reg52.h"
/*#define LCD1602_4PINS*/
#ifndef uchar8
#define uchar8 unsigned char
#endif
#ifndef uint16
#define uint16 unsigned int
#endif
#define lcd_data P0
/**读写选择*/
sbit lcd_RW = P2^5;
sbit lcd_RS = P2^6;
/**使能端*/
sbit lcd_E = P2^7;
void delay(uint16 i);
void lcd_writecom(uchar8 com);
void lcd_writedat(uchar8 dat);
void lcdinit();
void lcd_stringwrite(uchar8 com, uchar8 *s);
#endif
xpt2046.c
/**
*@file xpt2046.c
*@brief 配置xpt2046芯片的读写函数,读取相应模拟量数据
*@details 无
*@author 青渡
*@version V1.1.0
*@date 2021-04-05
*/
#include "xpt2046.h"
/**
*@brief 编写xpt2046的写数据函数
*@details 写入的是采集模拟量的地址,数据进行串行输入
*@param dat 8位数据
*@retval 无
*/
void SPI_write(u8 dat)
{
u8 i = 0;
CLK = 0;
for(i = 0; i <= 7; i++)
{
DIN = dat>>7;
dat = dat<<1;
CLK = 0;
/**延迟一个机器周期以形成相应沿信号*/
_nop_();
CLK = 1;
}
}
/**
*@brief 编写xpt2046的读数据函数
*@details DOUT为串行数据输出,需要一位一位地进行读取
*@param 无
*@retval dat 16位处理后数据
*/
u16 SPI_read()
{
u8 i = 0;
u16 dat = 0;
for(i = 0; i <= 11; i++)
{
dat = dat<<1;
CLK = 1;
_nop_();
CLK = 0;
dat |= DOUT;
}
return dat;
}
/**
*@brief 根据xpt2046芯片的通信时序实例化输出数字信号
*@details 根据相应时序初始化相对应的标志位
*@param cmd 8位数据采集地址
*@retval value 16位输出数字量
*/
u16 read_ad_data(u8 cmd)//模拟AD模数转换过程
{
u8 i;
u16 value;
CLK = 0;
CS = 0;
SPI_write(cmd);
for(i = 6; i >0; i--);
CLK = 1;
_nop_();
_nop_();
CLK = 0;
_nop_();
_nop_();
value = SPI_read();
/**通信结束置CS为高电平*/
CS = 1;
return value;
}
lcd1602.c
/**
*@file lcd1602.c
*@brief 配置lcd1602的读写函数,实现显示功能
*@details 对于4位和8位的输入应编写相对应的函数,不能混用
*@author 青渡
*@version V1.1.0
*@date 2021-04-05
*/
#include "lcd1602.h"
/*基本延时函数*/
void delay(uint16 i)
{
while(i--);
}
/**
*@brief 编写lcd1602的写指令函数
*@details 注意操作时序RS=L,RW=L,D0~D7=指令码,E=高脉冲->低脉冲
*@param com 8位控制指令
*@retval 无
*/
#ifndef LCD1602_4PINS
void lcd_writecom(uchar8 com)
{
lcd_RS = 0;
lcd_RW = 0;
lcd_E = 0;
lcd_data = com;
delay(200);
lcd_E = 1;
delay(1000);
lcd_E = 0;
}
#else
void lcd_writecom(uchar8 com)
{
lcd_RS = 0;
lcd_RW = 0;
lcd_E = 0;
lcd_data = com;
delay(200);
lcd_E = 1;
delay(1000);
lcd_E = 0;
lcd_data = com<<4;
delay(200);
lcd_E = 1;
delay(1000);
lcd_E = 0;
}
#endif
/**
*@brief 编写lcd1602的写数据函数
*@details RS=H,RW=L,D0~D7=数据,E=高脉冲->低脉冲
*@param dat 8位数据
*@retval 无
*/
#ifndef LCD1602_4PINS
void lcd_writedat(uchar8 dat)
{
lcd_RS = 1;
lcd_RW = 0;
lcd_E = 0;
lcd_data = dat;
delay(200);
lcd_E = 1;
delay(400);
lcd_E = 0;
}
#else
void lcd_writedat(uchar8 dat)
{
lcd_RS = 1;
lcd_RW = 0;
lcd_E = 0;
lcd_data = dat;
delay(200);
lcd_E = 1;
delay(400);
lcd_E = 0;
lcd_data = dat<<4;
delay(200);
lcd_E = 1;
delay(400);
lcd_E = 0;
}
#endif
/**
*@brief 编写lcd1602的初始化函数,写入相应控制指令
*@details 无
*@param 无
*@retval 无
*/
#ifndef LCD1602_4PINS
void lcdinit()
{
lcd_writecom(0x38);
lcd_writecom(0x0c);
lcd_writecom(0x06);
lcd_writecom(0x01);
}
#else
void lcdinit()
{
/**将8位总线转化成4位总线*/
lcd_writecom(0x32);
lcd_writecom(0x28);
lcd_writecom(0x0c);
lcd_writecom(0x06);
lcd_writecom(0x01);
}
#endif
/**
*@brief 实例化lcd1602的写数据函数
*@details lcd1602是字符型液晶,只能接受char类型的数据输入
*@param com 8位控制指令 *s 输入数据的地址
*@retval 无
*/
void lcd_stringwrite(uchar8 com, uchar8 *s)
{
lcd_writecom(com);
while(*s > 0)
{
lcd_writedat(*s++);
}
}
main.c
/**
*@file main.c
*@brief 配置室内空气净化系统的相应功能
*@details 无
*@author 青渡
*@version V1.1.0
*@date 2021-04-05
*/
#include "reg52.h"
#include "lcd1602.h"
#include "xpt2046.h"
sbit beep = P1^5;
sbit power = P1^0;
sbit key1 = P3^1;
sbit key2 = P3^0;
sbit key3 = P3^2;
uint16 value;
uchar8 token,num;
uchar8 threshold;
uchar8 code str1[] = "GAS:";
uchar8 code str2[] = "SET:";
uchar8 code math[] = "0123456789";
/**定时器0的中断初始配置*/
void timecontrol()
{
EA = 1;
ET0 = 1;
TR0 = 0;
TMOD |= 0x01;
TH0 = 0xFC;
TL0 = 0x18;
}
/**定时器1的中断初始配置*/
void timecon()
{
EA = 1;
ET1 = 1;
TR1 = 0;
TMOD |= 0x10;
TH1 = 0xFC;
TL1 = 0x18;
}
/**定时器0的中断服务函数*/
void timecontrol1()interrupt 1
{
/**无功能用途可设置为空*/
}
/**定时器1的中断服务函数*/
void timecon1()interrupt 3
{
uchar8 i;
/**蜂鸣器配置*/
for(i = 10 ; i > 0; i--)
{
beep =~ beep;
delay(100);
}
}
/**
*@brief 实现通过按键设置阈值,暂停的功能
*@details key1用以暂停,key2用以增大阈值,key3用于减小阈值
*@param 无
*@retval 无
*/
void dataset()
{
/**将num设置成静态变量*/
static num = 0;
if(key1 == 0)
{
/**按键消抖,触发定时器0中断*/
TR0 = 1;
if(key1 == 0)
{
token = 0;
num = num + 1;
}
/**等待按键松开*/
while(!key1);
}
if(num == 2)
{
num = 0;
token = 1;
}
/**当程序处在暂停状态下*/
if(key2 == 0&&num == 1)
{
TR0 = 1;
if(key2 == 0&&num == 1)
{
threshold = threshold+1;
if(threshold > 200)
{
threshold = 200;
}
}
while(!key2);
}
if(key3 == 0&&num == 1)
{
TR0 = 1;
if(key3 == 0&&num == 1)
{
threshold = threshold - 1;
if(threshold < 0)
{
threshold = 0;
}
}
while(!key3);
}
}
/**
*@brief 实现对传感器数据的处理
*@details 实现对外部模拟量测量值——>电压值——>气体浓度值的转换
*@param 无
*@retval 无
*/
void datadispose()
{
/**0xE4是外部输入AD值的地址,value为实际检测气体浓度值*/
value = read_ad_data(0xE4);
value = (int)(0.236 * (value - 12.8623));
/**当程序处在正常运行状态下且气体浓度值超出阈值时,排气扇转动并发出蜂鸣声预警*/
if(value >= threshold&&token == 1)
{
/**触发定时器1中断*/
TR1 = 1;
power = 1;
}
else
{
TR1 = 0;
power = 0;
}
}
/**
*@brief 将相对应的数据显示在lcd1602液晶屏上
*@details 注意对应数据在液晶屏上的写入地址
*@param 无
*@retval 无
*/
void datadisplay()
{
uchar8 a, b, c, d, i, j, k, l;
a = value/1000;
b = value%1000/100;
c = value%1000%100/10;
d = value%1000%100%10;
i = threshold/1000;
j = threshold%1000/100;
k = threshold%1000%100/10;
l = threshold%1000%100%10;
/**第一行最左侧的写入地址是0x80,第二行最左侧的写入地址是0xc0*/
lcd_stringwrite(0x80, &str1);
lcd_stringwrite(0xc0, &str2);
/**一个字符占用1位存储地址*/
lcd_writecom(0x84);
lcd_writedat(math[a]);
lcd_writedat(math[b]);
lcd_writedat(math[c]);
lcd_writedat(math[d]);
lcd_writecom(0xc4);
lcd_writedat(math[i]);
lcd_writedat(math[j]);
lcd_writedat(math[k]);
lcd_writedat(math[l]);
}
/**
*@brief 主函数,集成模块功能
*@details 初值的设置以及相应的硬件配置初始化应放在while(1)循环外
*@param 无
*@retval 无
*/
void main()
{
beep = 1;
token = 1;
power = 0;
threshold = 80;
timecontrol();
timecon();
/*将lcd1602的初始化函数放在while循环外,避免占用时序导致屏幕闪烁*/
lcdinit();
while(1)
{
dataset();
datadisplay();
datadispose();
}
}
5、项目部分成果展示
写在最后:若对本文内容存在质疑或疑惑,欢迎提出指正,一起学习进步!