今天给大家介绍的是一个温湿度检测设计,基于51单片机、蓝牙模块、温湿度传感器、Android APP完成。首先先展示一下设计好的实物,接下来将从系统方案、硬件设计、软件设计这三个方面来阐述。
先来看一下整体的架构图:硬件部分由STC89C52单片机、DHT11温湿度传感器、BT08蓝牙串口模块和Android手机组成。传感器将采集到的温湿度数据传送给单片机,然后单片机通过蓝牙串口模块将数据发送到手机APP,从而将温湿度在APP显示出来。在APP上可以设置温湿度告警的阈值,超过阈值将显示“偏高”或者“偏低”的相关信息。
整个设计的原理图如下所示,由单片机最小系统、蓝牙串口模块、温湿度传感器组成。传感器的DATA管脚连接单片机的P2^0口,蓝牙串口模块的RXD、TXD分别连接单片机的TXD、RXD。
蓝牙串口模块的功能是串口协议和蓝牙协议之间的相互转换,在单片机上自己编写一套蓝牙驱动代码是非常复杂的,借助这个模块我们在编写单片机代码时只需要编写串口收发的代码即可,该模块得到串口数据后会转成蓝牙数据。对于APP它接收到的是蓝牙数据,开发APP时只需要编写蓝牙相关的代码,Android封装了蓝牙相关的API,所以开发起来简单。蓝牙串口模块的引脚图如下图所示,在这个设计中用到了四个引脚,VCC、GND接5V电源和地,蓝牙模块的TXD接单片机的RXD,RXD接TXD。
DHT11温湿度传感器负责采集环境中的温湿度数据,在单片机软件设计部分会详细的介绍该传感器的使用步骤。引脚说明:
VDD 供电3.3~5.5V DC
DATA 串行数据,单总线
NC 空脚
GND 接地,电源负极
单片机程序主要是两个点,一是读取DHT11传感器的温湿度数据,二是串口通信。DHT11的官方文档写的很规范,有关于读取数据的详细步骤,文档更新也比较及时,最新的更新日期是2017年3月31号,官网的下载地址:http://www.aosong.com/products-21.html
DHT11采用单总线通信,单总线即只有一根数据线,系统中的数据交换、控制均由单总线完成。
DATA 管脚用于DHT11与单片机之间的通讯和同步,采用单总线数据格式,一次传送40 位数据,高位先出。
数据格式:
8bit 湿度整数数据+ 8bit 湿度小数数据+ 8bit 温度整数数据+ 8bit 温度小数数据+ 8bit 校验位。
注:其中湿度小数部分为0。
8bit 湿度整数数据 + 8bit 湿度小数数据 + 8bit 温度整数数据 + 8bit 温度小数数据 = 8bit 校验位
如果以上等式成立,则本次传感器采集的数据有效,否则无效。
先看采集数据有效的示例,接收到的40 位数据为:
0011 0101 0000 0000 0001 1000 0000 0100 0101 0001
湿度高8 位 湿度低8 位 温度高8 位 温度低8 位 校验位
计算: 0011 0101 + 0000 0000 + 0001 1000 + 0000 0100 = 0101 0001,接收数据正确。
湿度:0011 0101(整数)=35H=53%RH 0000 0000(小数)=00H=0.0%RH =>53%RH + 0.0%RH = 53.0%RH
温度:0001 1000(整数)=18H=24℃ 0000 0100(小数)=04H=0.4℃ =>24℃ + 0.4℃ = 24.4℃
采集数据无效的示例,接收到的40 位数据为:
0011 0101 0000 0000 0001 1000 0000 0100 0100 1001
湿度高8 位 湿度低8 位 温度高8 位 温度低8 位 校验位
计算: 0011 0101 + 0000 0000 + 0001 1000 + 0000 0100 不等于0100 1001,本次接收的数据不正确,放弃,重新接收数据。
通过以上两个示例可以清楚DHT11数据格式以及数据如何去校验有效性。
用户主机(MCU)发送一次开始信号后,DHT11 从低功耗模式转换到高速模式,待主机开始信号结束后,DHT11 发送响应信号,送出40bit 的数据,并触发一次信采集。信号发送如图所示。这里的主机是指单片机,从机是指DHT11传感器。
下面这个图表罗列了时序图相关的参考时间,在读取数据的详细步骤中会用到这些数值。
根据时序图和表中的参考时间,我们可以得出读取传感器数据的步骤。
step1:单片机输出低电平保持20ms
step2:单片机拉高电平保持13us等待DHT11传感器的低电平响应信号
step3:判断DHT11是否给出低电平响应,如果有低电平响应则进入步骤4,否则等待下一轮的尝试。
step4:通过while语句等待83us的低电平响应时间结束
step5:通过while语句等待87us的高电平响应时间结束
step6:计算温湿度数据
step7:单片机输出高电平结束一次数据采集的读取
step8:校验数据
在时序图中可以看到,数据读取是每次一位进行的,数据0位和数据1位的低电平时间是相同的,即54us。数据0位的高电平时间是24us,而数据1为的高电平时间是71us,通过高电平时间的差异我们就可以判断出是数据0还是数据1。所以单独写了一个函数用来计算数据0位和1位,由于温湿度的整数和小数部分分别是由8位表示的,我们定义该函数得到8位数据之后给出返回值。步骤6对应的函数computeData() 用来完成上述工作。我们对步骤6进行详细的描述:
step 6.1:等待54us低电平结束
step 6.2:延时30us判断高电平是否结束,因为数据0位的电平最大时长是27us,如果超过27us之后高电平结束,则为数据0位,否则为数据1位。
step 6.3:通过while语句等待高电平结束
step 6.4:通过移位和或与的方式保存一个数据位
step 6.5:循环6.1到6.4步骤8次,得到一个字节的数据
//--------------------------------
//-----湿度读取子程序 ------------
//--------------------------------
//----以下变量均为全局变量--------
//----温度高8位== temperature_H------
//----温度低8位== temperature_L------
//----湿度高8位== humidity_H-----
//----湿度低8位== humidity_L-----
//----校验 8位 == checkdata-----
//--------------------------------
void readData()
{
U8 humidity_H_temp,humidity_L_temp,temperature_H_temp,temperature_L_temp,checkdata_temp;
//step1:单片机输出低电平保持20ms
P2_0=0;
delayms(20);
//step2:单片机拉高电平保持13us等待DHT11传感器的低电平响应信号
P2_0=1;
delay13us();
//step3:判断DHT11是否给出低电平响应,如果有低电平响应则进入步骤4,否则等待下一轮的尝试。
if(P2_0==0)
{
//step4:通过while语句等待83us的低电平响应时间结束
while(P2_0==0);
//step5:通过while语句等待87us的高电平响应时间结束
while(P2_0==1);
//step6:计算温湿度数据
humidity_H_temp = computeData();
humidity_L_temp = computeData();
temperature_H_temp = computeData();
temperature_L_temp = computeData();
checkdata_temp = computeData();
//step7:单片机输出高电平结束一次数据采集的读取
P2_0 = 1;
//step8:校验数据
if(checkdata_temp = humidity_H_temp + humidity_L_temp + temperature_H_temp + temperature_L_temp)
{
humidity_H = humidity_H_temp;
humidity_L = humidity_L_temp;
temperature_H = temperature_H_temp;
temperature_L = temperature_L_temp;
checkdata = checkdata_temp;
}
}
}
/**
*根据时序计算温湿度值
*/
U8 computeData()
{
U8 i,U8comdata;
for(i=0; i<8; i++)
{
//step 6.1:等待54us低电平结束
while(P2_0==0);
//step 6.2:延时30us判断高电平是否结束
Delay_10us();
Delay_10us();
Delay_10us();
U8temp=0;
if(P2_0==1)
{
U8temp=1;
}
//step 6.3:通过while语句等待高电平结束
while(P2_0==1);
//step 6.4:通过移位和或与的方式保存一个数据位
U8comdata<<=1;
U8comdata|=U8temp;
}
return U8comdata;
}
温湿度数据读取完毕,接下来就是通过串口发送出去,串口发送数据的代码相对简单了,我们在主函数中对串口通信进行初始化,然后在一个while语句中每隔2s读取数据然后发送。
//----------------------------------------------
//main()功能描述: STC89C52RC 11.0592MHz 串口发送温湿度数据,波特率 9600
//----------------------------------------------
void main()
{
U8 i;
TMOD = 0x20; //定时器T1使用工作方式2
TH1 = 253; // 设置初值
TL1 = 253;
TR1 = 1; // 开始计时
SCON = 0x50; //工作方式1,波特率9600bps,允许接收
ES = 1;
EA = 1; // 打开所以中断
TI = 0;
RI = 0;
Delay(1); //延时100US(12M晶振)
while(1)
{
//调用温湿度读取子程序
readData();
str[0]=humidity_H;
str[1]=humidity_L;
str[2]=temperature_H;
str[3]=temperature_L;
str[4]=checkdata;
//发送到串口
for(i=0; i<5; i++)
{
sendOneChar(str[i]);
}
//读取模块数据周期不易小于 2S
delayms(2000);
}
}
至此,单片机端的主要代码就讲解完了,可以看到核心代码是如何读取DHT11的数据。
4.手机APP软件设计
APP是用Android Studio(AS)开发的,不建议初学者学习Eclipse结合ADT(Android Eclipse Tools)插件的方式开发Android APP,这种方式已经过时并且以后会被淘汰,Google在2016年底已经停止了对ADT的更新,我之前所在的公司已经将Eclispe的代码全部迁移到AS平台了,推荐使用Google自家的AS集成开发环境。AS有很多优点,但是在使用时也有问题,AS借助gradle进行项目构建,至于为什么Google利用gradle进行Android app项目构建,读者可以自行上网搜索。gradle插件版本要和AS版本相对应,不同的开发者的gradle版本可能不同,所以当你拿到另外一个开发者的代码在自己的AS运行时时有可能会构建失败。这个现象对于国外开发者而言不是一个问题,AS可以自动去下载所需要的gradle插件版本,但是在国内,由于众所周知的原因,如果不会科学上网那么AS直接尝试下载gradle插件时会失败,会令很多初学者不知所措。在以后有时间我会单独写一篇blog来讲解如何去解决这个问题。最近听到Google要重返中国市场,如果能回归成功,对于国内的很多开发者和学术研究者而言是个好消息。
言归正传,本设计APP的代码主要分成两个部分,一是蓝牙数据的接收,二是图表显示。
蓝牙通信的三个基本步骤:搜索、配对、连接。这之后就可以进行数据传输了。
在蓝牙通信中需要获取Android系统的以下三个权限,如果不能给APP授予相关的权限会影响蓝牙的正常使用:
BLUETOOTH:允许配对的设备进行连接
BLUETOOTH_ADMIN:允许搜索和配对设备
ACCESS_COARSE_LOCATION:广播接收器接收BluetoothDevice.ACTION_FOUND广播需要改权限
在下文中还会提到在Android6.0及以上的版本中关于ACCESS_COARSE_LOCATION权限的申请。
建立蓝牙通信之前需要验证是否有蓝牙设备,以及蓝牙设备是否已经开启。对于一个Android系统而言只有一个蓝牙适配器,通过getDefaultAdapter()方法可以返回其一个实例,如果返回为null,则说明该设备不支持蓝牙。
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
// device doesn't support Bluetooth
}
接下来是检查蓝牙设备是否已经开启,如果没有开启,可以调用startActivityForResult()方法来弹出对话框让用户选择开启,这种方式不会停止当前的应用。
if (!mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
搜索设备可以分成两部分,一是查找已经与本机配对的设备,通过getBondedDevices()方法返回已经配对的设备信息:
Set pairedDevices = mBluetoothAdapter.getBondedDevices();
if (pariedDevices.size > 0) {
for (BluetoothDevice device: pairedDevices) {
String deviceName = device.getName();
String deviceMACAddress = device.getAddress();
}
}
二是搜索周围可用的但是还未配对的设备。
系统在发现蓝牙设备会通过广播的形式通知app,所以在搜索设备之前需要注册广播接收器来接收发现蓝牙设备的消息,在销毁Activity时注销广播接收器。
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
public void onReceiver(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
BluetoothDevie device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// Register for broadcasts when a device is discovered.
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter);
}
@Override
protected void onDestroy() {
super.onDestroy();
// Don't forget to unregister the ACTION_FOUND receiver.
unregisterReceiver(mReceiver);
}
BluetoothDevice.ACTION_FOUND广播需要ACCESS_COARSE_LOCATION权限,该权限是个危险权限,在Android 6.0及以上,除了在manifest中声明还需要在java代码中申请。获取了该权限之后,在搜索蓝牙设备时才能收到系统发出的蓝牙设备发现的广播。搜索设备调用startDiscovery()方法,当周围有可用设备时,系统会通过广播的形式通知应用。
//检查ACCESS_COARSE_LOCATION权限
if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_COARSE_LOCATION)
== PackageManager.PERMISSION_GRANTED){
Toast.makeText(MainActivity.this,"搜索回调权限已开启",Toast.LENGTH_SHORT).show();
if(mBluetoothAdapter.isDiscovering()){
mBluetoothAdapter.cancelDiscovery();
}
mBluetoothAdapter.startDiscovery();
}else{
Toast.makeText(MainActivity.this,"搜索回调权限未开启",Toast.LENGTH_SHORT).show();
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},REQUEST_ACCESS_COARSE_LOCATION);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(requestCode==REQUEST_ACCESS_COARSE_LOCATION){
if(grantResults.length>0 && grantResults[0]==PackageManager.PERMISSION_GRANTED){
mBluetoothAdapter.startDiscovery();
if(mBluetoothAdapter.isDiscovering()){
mBluetoothAdapter.cancelDiscovery();
}
mBluetoothAdapter.startDiscovery();
} else {
Toast.makeText(MainActivity.this,"action found is not granted.",Toast.LENGTH_LONG).show();
}
}
}
在建立连接时需要一个UUID,UUID是用来标识不同设备的ID,对于蓝牙串口设备而言其对应的UUID是“00001101-0000-1000-8000-00805F9B34FB”。
If you are connecting to a Bluetooth serial board then try using the well-known SPP UUID 00001101-0000-1000-8000-00805F9B34FB
https://developer.android.google.cn/reference/android/bluetooth/BluetoothDevice.html
手机端是作为客户端与蓝牙模块进行连接的。
在蓝牙socket进行connect之前,一定要调用BluetoothAdapter的cancelDiscovery()方法。连接的第一步是通过调用BluetoothDevice的createRfcommSocketToServiceRecord(UUID)获取BluetoothSocket.第二步是调用BluetoothSocket的connect()方法发起连接。由于connect()为阻塞调用,因此该连接过程应该在主线程之外的线程中执行。在调用connect()时,应始终确保设备未在执行设备发现。如果正在进行发现操作,则会大幅降低连接尝试的速度,并增加连接失败的可能性。
String macAddr = "20:15:05:25:02:43";
BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(macAddr);
UUID uuid = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb");
try {
mSocket = device.createRfcommSocketToServiceRecord(uuid);
} catch (IOException e) {
e.printStackTrace();
}
new Thread(){
@Override
public void run() {
mBluetoothAdapter.cancelDiscovery();
try {
mSocket.connect();
} catch (IOException e) {
try {
mSocket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}
super.run();
}
}.start();
确保在建立连接之前始终调用cancelDiscovery(),而且调用时无需实际检查其是否正在运行,如果确实想要执行检查,请调用isDiscovering()。
try {
OutputStream os = mSocket.getOutputStream();
os.write("发送的数据".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
系统的整个设计过程如上所述,我已经把该设计的实物挂在了淘宝上,如果想买来玩一玩,欢迎大家点击以下链接:
https://item.taobao.com/item.htm?spm=a1z10.1-c.w4004-3312521594.2.21332cf8bVg4fB&id=575320673715
物联网开发技术讨论群: