第3章

传感器接口与编程




本章从基本传感器模块入手,介绍树莓派GPIO接口与各种传感器的连接与编程,学习树莓派的硬件资源与接口设计。本章使用的各种传感器模块价格便宜,购买方便。
3.1GPIO接口简介 
树莓派拥有40个可编程的GPIO(通用输入/输出端口)引脚。GPIO应用非常广泛,掌握了GPIO的使用,就掌握了树莓派硬件设计的能力。使用者可以通过GPIO输出高低电平来控制LED、蜂鸣器、电动机等各种外设工作,也可以通过它们实现树莓派和外接传感器模块之间的交互。树莓派3B/3B+的GPIO接口及引脚分布如图31所示,除了包括多个5V、3.3V以及接地引脚以外,还具有I2C、SPI和UART


图31树莓派GPIO引脚

接口等双重功能。需要说明的是,电源和接地引脚可用于给外部模块或元器件供电,但过大的工作电流或峰值电压可能会损坏树莓派。

 GPIO接口有以下两种常用的编码方式:
(1)  BOARD编码,按照树莓派主板上引脚的物理位置进行编号,分别对应1~40号引脚。




(2) BCM编码,属于更底层的工作方式,它和Broadcom片上系统中信道编号相对应,在使用引脚时需要查找信道编号和物理引脚编号的对应关系。
树莓派操作系统里包含了RPi.GPIO库,使用该库可以指定GPIO接口的编码方式,代码如下: 



import RPi.GPIO as GPIO#导入RPi.GPIO模块



GPIO.setmode(GPIO.BCM)  #引脚采用BCM编码方式

GPIO.setmode(GPIO.BOARD)  #引脚采用BOARD编码方式






使用RPi.GPIO库也可以轻松实现对GPIO引脚功能的设置,例如,



import RPi.GPIO as GPIO



GPIO.setmode(GPIO.BOARD)

GPIO.setup(11, GPIO.IN)  #将引脚11设置为输入模式

GPIO.input(11)  #读取输入引脚的值

GPIO.setup(12, GPIO.OUT)  #将引脚12设置为输出模式



#可以通过软件实现输入引脚的上拉/下拉

GPIO.setup(11, GPIO.IN, pull_up_down=GPIO.PUD_UP)  #输入引脚11上拉

GPIO.setup(11, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)  #输入引脚11下拉

'''也可以设置输出引脚的初始状态,输出引脚12的初始状态为高电平,状态可以表示为

0/GPIO.LOW/False或者1/GPIO.HIGH/True'''

GPIO.setup(12, GPIO.OUT, initial=GPIO.HIGH)






程序运行结束后,需要释放硬件资源,同时避免因意外损坏树莓派。使用GPIO.cleanup()会释放使用的GPIO引脚,并清除设置的引脚编码方式。


串口配置与
GPS模块
编程


3.2GPS定位
3.2.1树莓派串口配置

树莓派3B/3B+提供了两类串口,即硬件串口(/dev/ttyAMA0)和mini串口(/dev/ttyS0)。硬件串口由硬件实现,有单独的时钟源,性能高、工作可靠; 而mini串口性能低,功能简单,波特率受到内核时钟的影响。树莓派3B/3B+新增了板载蓝牙模块,硬件串口被默认分配给与蓝牙模块通信,而把由内核提供时钟参考源的mini串口分配给了GPIO接口中的TXD和RXD引脚。在终端输入ls l /dev,查看当前的串口映射关系,如图32所示,UART接口映射的串口serial0默认是mini串口。



图32树莓派默认串口映射关系


由于mini串口速率不稳定,通过UART接口外接模块时可能会出现无法正常工作的情况。为了通过GPIO使用高性能的硬件串口,需要将树莓派3B/3B+的蓝牙模块切换到mini串口,并将硬件串口恢复到GPIO引脚中,步骤如下: 
(1)  终端输入sudo raspiconfig,如图33所示,依次选择Interfacing Options→Serial选项,回车后选择“否”,禁用串口的控制台功能(树莓派GPIO引出的串口默认用来做控制台使用,需要禁用该功能,使得串口可以自由使用),随后选择“是”,使能树莓派串口,如图34所示。


图33树莓派串口配置




图34使能树莓派串口



(2) 终端输入sudo nano /boot/config.txt打开配置文件,在文件最后一行添加“dtoverlay=pi3disablebt”释放蓝牙占用的串口,保存后退出,重启树莓派使上述修改生效。
(3) 在终端输入ls l /dev再次查看当前的串口映射关系,如图35所示,树莓派已经恢复了硬件串口与GPIO的映射关系。



图35恢复硬件串口与GPIO的映射关系



注意:  
禁用串口的控制台功能也可以通过编辑cmdline.txt文件来实现。输入sudo nano /boot/cmdline.txt打开文件,将/dev/ttyAMA0有关的配置去掉,例如,原cmdline.txt的内容为: dwc_otg.lpm_enable=0 console= ttyAMA0,115200 console=tty1 root=
…,只需将其中的“console= ttyAMA0,115200”删掉即可。



3.2.2GPS模块接口与编程
选用的GPS模块型号为ATGM336H,它基于中科微低功耗GNSS SOC芯片AT6558,支持GPS/BDS/GLONASS卫星导航系统,具有高灵敏度、低功耗、低成本的特点。该模块供电电压为3.3~5V,采用IPX陶瓷有源天线,定位精度2.5m,冷启动捕获灵敏度为-148dBm,跟踪灵敏度为-162dBm,工作电流小于25mA,通信方式为串口通信,波特率默认为9600bps。GPS模块及其与树莓派的引脚连接如图36所示,4个引脚VCC、GND、TX和RX分别与树莓派GPIO接口的1脚(3.3V)、6脚(GND)、10脚(RXD)
和8脚(TXD)相连。


图36树莓派连接GPS模块


minicom是运行在Linux系统下的轻量级串口调试工具,类似Windows系统中的串口调试助手,在命令行输入sudo aptget install minicom即可完成安装。将GPS模块与树莓派连接好后,在终端输入minicom b 9600 D /dev/ttyAMA0打开minicom获取串口数据,其中b设定波特率,视模块参数而定; D指定的是接口。随后可以看到树莓派通过串口接收到GPS模块的定位数据,如图37所示。测试时需将GPS模块置于室外或者窗户边,有利于GPS搜星与定位。


图37串口读取GPS模块的数据



GPS模块按照NMEA0183协议格式输出数据,包括GPS定位信息(GGA)、当前卫星信息(GSA)、可见卫星信息(GSV)、推荐定位信息(RMC)和地面速度信息(VTG)等内容。通常根据推荐定位信息($GNRMC开头的数据行)来获取有用数据,$GNRMC语句的基本格式与数据详解如图38所示。
以图37中标记的$GNRMC语句为例,选用其中<1><3><4><5><6><9>这6个数据项就可以得到时间和经纬度信息。从<9>和<1>可知,当前的时间是2021年3月6日10时28分32秒; <3>和<4>表明当前位置是北纬30度31.5378分,即北纬30.525630度(31.5378分/60可以转化为度); 类似地,<5>和<6>表明当前位置是东经114度23.5745分,即东经114.392908度(23.5745分/60转化为度)。为了从GPS原始数据中解析出时间




$GPRMC,<1>,<2>,<3>,<4>,<5>,<6>,<7>,<8>,<9>,<10>,<11>,<12>*hh

<1> UTC时间,hhmmss.sss(时分秒)格式

<2> 定位状态,A=有效定位,V=无效定位

<3> 纬度ddmm.mmmm(度分)格式

<4> 纬度半球N(北半球)或S(南半球)

<5> 经度dddmm.mmmm(度分)格式,其中的分/60可以转化为度

<6> 经度半球E(东经)或W(西经)

<7> 地面速率(000.0~999.9节,前面的0也将被传输)

<8> 地面航向(000.0~359.9度,以真北为参考基准)

<9>UTC日期,ddmmyy(日月年)格式

<10> 磁偏角(000.0~180.0度,前面的0也将被传输)

<11> 磁偏角方向,E(东)或W(西)

<12> 模式指示(A=自主定位,D=差分,E=估算,N=数据无效),后面的*hh表示校验值


图38$GNRMC语句的基本格式与数据详解


与位置信息,新建gps_test.py脚本文件,输入以下代码: 



import serial  #导入串口库,以便通过串口访问GPS模块

import time



ser= serial.Serial("/dev/ttyAMA0",9600)  #使用/dev/ttyAMA0建立串口,波特率9600



def GPS(): 

str_gps= ser.read(1200)  #从串口读取1200字节数据,包括完整的GPS数据集合

#转换成UTF-8编码输出,避免乱码

str_gps=str_gps.decode(encoding = 'utf-8', errors = 'ignore') 

pos1= str_gps.find("$GNRMC")  #找到$GNRMC字符串首次出现的位置

pos2= str_gps.find("\n",pos1)  #找到$GNRMC行的结尾处

loc= str_gps[pos1:pos2]  #提取完整的$GNRMC行数据

data=loc.split(",")  #以逗号为分隔符,将$GNRMC行进行分割,分解到data列表中



if data[2]=='V':  #GPS数据无效

print("No location found")  

else:

position_lat=float(data[3][0:2]) + float(data[3][2:9]) / 60.0  #计算纬度

position_lng = float(data[5][0:3]) + float(data[5][3:10]) / 60.0  #计算经度

time = data[1]

time_h = int(time[0:2])+8  #调整为北京时间,北京时间=UTC + 时区差8

time_m = int(time[2:4])

time_s = int(time[4:6])  

print("纬度: %f %s" % (position_lat, data[4]))

print("经度: %f %s" % (position_lng, data[6]))

print("时间: %d h %d m %d s\n" % (time_h, time_m, time_s))

#返回的经纬度只取小数点后面6位








return [round(position_lng,6), data[6],round(position_lat,6), data[4]] 



if __name__ == "__main__":

try:

while True:

GPS()

time.sleep(5)

except KeyboardInterrupt:  #按Ctrl+C组合键退出

ser.close()  # 释放串口






此外,还可以通过第三方库访问GPS。首先,运行命令sudo aptget install gpsd gpsdclients安装gpsd软件。接着输入sudo gpsd /dev/ttyAMA0 F /var/run/gpsd.sock运行gpsd软件,该命令会开启一个后台程序并由它负责与GPS模块通信。然后输入命令cgps开启GPS客户端,显示gpsd接收的数据信息,如图3
9所示。如果运行软件时客户端一直没有数据显示或出现超时错误,则可以使用命令sudo systemctl stop gpsd.socket以及sudo systemctl disable gpsd.socket禁用gpsd系统服务,再执行命令sudo killall gpsd结束gpsd进程并通过命令sudo gpsd /dev/ttyAMA0 F /var/run/gpsd.sock重启gpsd软件。



图39客户端窗口显示数据信息



图38中的标注框中包含了经纬度、海拔、航向等信息,借助gps3库可以解析并获取这些数据。运行sudo pip3 install gps3命令安装gps3库(负责提供gpsd接口,默认为host='127.0.0.1',port=2947),新建Python脚本gps_test2.py,输入以下代码: 



from gps3 import gps3  #用来访问gpsd

import time



def GPS3():

gps_socket = gps3.GPSDSocket() #创建gpsd套接字连接并请求gpsd输出

data_stream = gps3.DataStream() #将流式gpsd数据解压到字典中

gps_socket.connect() #建立连接

gps_socket.watch() #寻找新的GPS数据

for new_data in gps_socket:

if new_data: #数据非空

data_stream.unpack(new_data) #将字节流转换成数据

if not (isinstance(data_stream.TPV['alt'],str)|

isinstance(data_stream.TPV['lat'],str)|

isinstance(data_stream.TPV['lon'],str)|

isinstance(data_stream.TPV['track'],str)): #防止出现n/a

return [data_stream.TPV['alt'],data_stream.TPV['lat'],

data_stream.TPV['lon'],data_stream.TPV['track']]



if __name__ == "__main__":

altitude,latitude,longitude,heading = GPS3() #提取海拔、纬度、经度和航向

print('海拔: ', altitude) #输出结果

print('纬度: ', latitude)

print('经度: ', longitude)

print('航向: ', heading)






3.2.3百度地图GPS定位
通过百度地图拾取坐标系统(http://api.map.baidu.com/lbsapi/getpoint/index.html)坐标反查发现,GPS模块获取的数据在地图上显示的位置与实际位置有较大偏差,如图310(a)所示。主要原因是坐标系之间不兼容,GPS坐标遵循WGS84标准,而百度对外接口的坐标系并不是GPS采集的经纬度。为了在百度地图上精准定位,需要对GPS坐标进行转换。具体过程是: 先将WGS84标准转换为GCJ02标准(国内Google、高德以及腾讯地图遵循该标准),再进行BD09标准加密转换为百度地图坐标系。新建名为gps_transform.py的文件,输入如下代码: 



import math



pi = 3.14159265358979324

a = 6378245.0 #长半轴

ee = 0.00669342162296594323 #偏心率平方









x_pi = 3.14159265358979324 * 3000.0 / 180.0



def transformlat(lng, lat):

ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat +

0.2 * math.sqrt(math.fabs(lng))

ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 * math.sin(2.0 * lng * pi)) * 2.0 / 3.0

ret += (20.0 * math.sin(lat * pi) + 40.0 * math.sin(lat / 3.0 * pi)) * 2.0 / 3.0

ret += (160.0 * math.sin(lat / 12.0 * pi) + 320 * math.sin(lat * pi / 30.0)) * 2.0 / 3.0

return ret



def transformlng(lng, lat):

ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat +

 0.1 * math.sqrt(math.fabs(lng))

ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 * math.sin(2.0 * lng * pi)) * 2.0 / 3.0

ret += (20.0 * math.sin(lng * pi) + 40.0 * math.sin(lng / 3.0 * pi)) * 2.0 / 3.0

ret += (150.0 * math.sin(lng / 12.0 * pi) + 300.0 * math.sin(lng / 30.0 * pi)) * 2.0 / 3.0

return ret



def wgs84_to_gcj02(lng, lat):

dlat = transformlat(lng - 105.0, lat - 35.0)

dlng = transformlng(lng - 105.0, lat - 35.0)

radlat = lat / 180.0 * pi

magic = math.sin(radlat)

magic = 1 - ee * magic * magic

sqrtmagic = math.sqrt(magic)

dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi)

dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi)

mglat = lat + dlat

mglng = lng + dlng

return [mglng, mglat]



def gcj02_to_bd09(lng, lat):

z = math.sqrt(lng * lng + lat * lat) + 0.00002 * math.sin(lat * x_pi)

theta = math.atan2(lat, lng) + 0.000003 * math.cos(lng * x_pi)

bd_lng = z * math.cos(theta) + 0.0065

bd_lat = z * math.sin(theta) + 0.006

return [bd_lng, bd_lat]






修改gps_test.py代码,调用wgs84_to_gcj02()和gcj02_to_bd09()两个转换函数对读取到的经纬度数据进行处理,另存为gps.py。运行该脚本,将得到的经纬度再次通过百度地图拾取坐标系统坐标反查,就能够准确定位到真实位置,结果如图310(b)所示。


图310百度地图GPS定位


3.3烟雾/可燃气体检测
MQ2属于二氧化锡半导体气敏材料,适用于可燃性气体、酒精、烟雾等的探测。MQ2传感器模块如图311所示,4个接口从上到下分别为VCC、GND、DO和AO(具有TTL电平输出和模拟量输出),烟雾/可燃气体浓度越大,输出的模拟信号越大; 输入电压为5V,AO输出0.1~0.3V相对无污染,最高浓度电压4V左右; 使用前必须预热20s左右,使测量的数据稳定,使用中传感器发热属于正常现象。


图311MQ2气敏传感器模块



为了获取浓度,树莓派需要外接模数转换器读取MQ2模块AO引脚的输出值,这里选用模数转换芯片MCP3002,如图312所示。该芯片是双通道10位A/D转换器,采用2.7~5.5V电源和参考电压输入,通过SPI串行总线与树莓派GPIO接口直接相连。具体的电路连接如下,MCP3002的VDD/VREF和VSS分


图312A/D转换器MCP3002 


别连接树莓派的17脚(3.3V)和9脚(GND),DIN、DOUT、CLK和CS/SHDN引脚分别连接树莓派GPIO接口的19脚(MOSI)、21脚(MISO)、23脚(SCLK)和24脚(CE0); MQ2传感器的VCC连接树莓派GPIO接口的2脚(5V),GND连接MCP3002的VSS,A0引脚通过330Ω和470Ω电阻串联分压后连到MCP3002的CH0引脚(保证输入的模拟电压不超过3.3V)。


为了访问MCP3002,先要启动树莓派的SPI硬件接口。操作过程如下: 在终端输入sudo raspiconfig进入配置界面,依次选择Interfacing Options →SPI选项,使能SPI接口。通过ls l /dev命令可以看到两个SPI设备(spidev0.0和spidev0.1),GPIO引脚CE0和CE1分别对应spidev0.0和spidev0.1。MCP3002使用SPI通信协议,可以使用spidev库来驱动SPI接口,简化程序设计。新建名为mcp3002.py的Python脚本,开启SPI总线设备并通过其获取MQ2模块AO引脚输出的电压值,代码如下: 



import spidev #导入spidev库

import time



def read_Analog(channel):

spi = spidev.SpiDev() #创建SPI总线设备对象

spi.open(0, 0)  #打开SPI总线设备,此处设备为/dev/spidev0.0

spi.max_speed_hz = 15200  #设置最大总线速度

reply = spi.xfer2([(((6 + channel)<< 1 )+ 1 )<< 3, 0]) 
#向spi设备发送命令,见数据手册

adc_out = ((reply[0]& 3) << 8)+ reply[1] #读取10位转换数据

value = adc_out*3.3/1024 #转化为电压值

value = value/4.7*(3.3+4.7) #将串联分压折算为MQ-2的输出电压

return value	



if __name__ == "__main__":

try:

while True:

value = read_Analog(0)  #读取A/D转换结果

print("AO_voltage = %f" % value)

time.sleep(5)

except KeyboardInterrupt:

spi.close() #中断退出关闭SPI设备






在终端输入python3 mcp3002.py运行程序,将点燃的蚊香靠近MQ2传感器并不断吹气,测试结果如图313所示。




图313气敏传感器模块测试结果



注意:  
spi.open(bus,device)用于开启SPI总线设备,bus和device分别对应设备/dev/spidev0.0(或者spidev0.1)后面的两个数字。此外,树莓派SPI接口默认的最大总线速度是125.0MHz(spi.max_speed_hz =125000000),工作时应根据需要设置为合适的值,否则有可能会读不到正确的数据。


3.4温湿度检测
DHT11是一款含有已校准数字信号输出的温湿度传感器。如图314所示,该传感器模块体积小、功耗低,采用单线制串行接口,3个接口分别为VCC、OUT和 GND。其主要参数特性包括: 供电电压为3.3~5.5V,相对湿度测量范围为20%~95%(测量误差±5%),温度测量范围为0~50℃(测量误差±2℃)。DHT11与树莓派接口简单,VCC和GND分别连接树莓派GPIO的17脚(3.3V)、20脚(GND),OUT连接GPIO的18引脚。


图314数字温湿度传感

器DHT11


DHT11遵循单总线通信协议,对时序有较为严格的要求。一次完整的工作流程如下: 首先树莓派发送开始信号,将总线由高电平拉低,时长至少需要18ms,以保证被DHT11检测到。待开始信号结束后,树莓派释放总线,总线从输出模式变为输入模式,保持高电平延时等待20~40μs后,DHT11发送80μs低电平响应信号,紧接着输出80μs的高电平通知树莓派准备接收数据。DHT11一次传送40b的数据(高位先出),1b数据都以50μs低电平时隙开始,电平的长短决定了数据位
是0还是1。具体来说,数据位0的格式是50μs的低电平和26~28μs的高电平; 数据位1的格式是50μs的低电平和70μs的高电平。40b数据格式为: 8b湿度整数数据+8b湿度小数数据+8b温度整数数据+8b温度小数数据+8b校验和。数据传送正确时校验和等于前面4个8b数据之和。如果数据接收不正确,则放弃本次数据,重新接收。

编写树莓派读取DHT11温湿度数据的代码时,有两点需要说明: 一是传感器上电后,要等待1s以越过不稳定状态; 二是树莓派的实时性较弱,不像单片机那样严格可控,在读取DHT11的数据脉冲时要注意控制时序,否则可能无法正确读取到数据。新建dht11.py脚本,输入以下代码: 



import RPi.GPIO as GPIO

import time









def init():

GPIO.setmode(GPIO.BOARD)

time.sleep(1) #时延1s,越过不稳定状态



def get_readings(ch): #ch:DHT11数据引脚

data = [] #存储温湿度值

j = 0 #数据位计数器

OUT = ch

GPIO.setup(OUT , GPIO.OUT) #设置引脚为输出

GPIO.output(OUT, GPIO.LOW) #发送开始信号,将总线由高拉低

time.sleep(0.02) #时长需要超过18ms

GPIO.output(OUT, GPIO.HIGH) #释放总线,变为高电平

GPIO.setup(OUT, GPIO.IN,pull_up_down=GPIO.PUD_UP) #设置引脚为输入,上拉

while GPIO.input(OUT) == GPIO.LOW: #等待DHT11发送的低电平响应信号

continue

while GPIO.input(OUT) == GPIO.HIGH: #等待DHT11拉高总线结束

continue

while j < 40: #开始接收40bit数据

k = 0 #通过计数的方式判断数据位高电平的时间

while GPIO.input(OUT) == GPIO.LOW:

continue

while GPIO.input(OUT) == GPIO.HIGH:

k += 1

if k > 100: #数据线为高时间过长,放弃本次数据

break

if k < 8: #数据位为0

data.append(0)

else: #数据位为1

data.append(1)

j += 1

return data



def data_check(data):

humidity_bit = data[0:8] #湿度整数

humidity_point_bit = data[8:16] #湿度小数

temperature_bit = data[16:24] #温度整数

temperature_point_bit = data[24:32] #温度小数

check_bit = data[32:40] #检验位

humidity = 0

humidity_point = 0

temperature = 0

temperature_point = 0

check = 0



for i in range(8):

humidity += humidity_bit[i] * 2 ** (7-i) #转换成十进制数据








humidity_point += humidity_point_bit[i] * 2 ** (7-i)

temperature += temperature_bit[i] * 2 ** (7-i)

temperature_point += temperature_point_bit[i] * 2 ** (7-i)

check += check_bit[i] * 2 ** (7-i)



return [humidity,humidity_point,temperature,temperature_point,check]



if __name__ == "__main__":

init()

while True:

dat = get_readings(18)

humidity,humidity_point,temperature,temperature_point,check = data_check(dat)

tmp = humidity + humidity_point + temperature + temperature_point

if check == tmp: #数据校验

T_value = str(temperature)+"."+str(temperature_point)#温度的整数与小数结合

H_value = str(humidity)+"."+str(humidity_point) #湿度的整数与小数结合

print ("temperature :", T_value, "*C,  humidity :", H_value, "%")

time.sleep(5)






在树莓派终端输入python3 dht11.py运行程序,可以得到从DHT11读取的温湿度数据,结果如图315所示。


图315温湿度传感器测试结果


3.5大气压检测
BMP180是一款性能优越的数字气压传感器,具有高精度、体积小和超低能耗的特点。该模块采用
I2C接口,如图316所示,4个引脚分别是VIN、GND、SCL和SDA。其主要特点如下: 与BMP085兼容,电源电压1.8~3.6V(VIN需5V供电,该模块上带有电源转换芯片,可将5V转化为3.3V),低功耗(标准模式下电流仅为5μA),压力范围为
300~1100hPa(海拔9000m~-500m),低功耗模式下分辨率为0.06hPa(0.5m)。


图316数字气压传感器BMP180



除了测量大气压力,BMP180还能测量温度,同时还可以根据式(31)推测出当前海拔高度。


altitude=44330×1-1-pp01/5.255(31)



式中,p为测得的大气压值,p0是海平面大气压力,默认值取1013.25hPa。
为了通过GPIO接口访问外接I2C设备,要先启动树莓派的I2C硬件接口。输入sudo raspiconfig打开配置界面,依次选择Interfacing Options →I2C选项,使能I2C接口。通过ls l /dev命令可以查看到I2C设备i2c1。使用smbus库对I2C设备进行读写操作,可以避免编写烦琐的I2C时序,简化程序设计。另外,使用i2cdetect工具可以查看连接到树莓派上的I2C设备,这二者的安装命令分别为sudo aptget install pythonsmbus和sudo aptget install i2ctools。
树莓派3脚和5脚分别预设为I2C总线的数据信号SDA和时钟信号SCL,能够与外接I2C设备进行通信。I2C总线可以同时连接多个I2C设备,每个设备都有一个唯一的7位地址。树莓派与某个特定I2C设备通信时是通过地址进行区分的,只有被呼叫的设备会做出响应。将BMP180的VIN、GND、SDA、SCL引脚分别与树莓派GPIO的4脚(5V)、14脚(GND)、3脚(SDA)和5脚(SCL)连接。当BMP180连接好后,输入命令i2cdetect y 1,可以看到BMP180的设备地址为0x77,如图317所示。


图317i2cdetect查看已连接的I2C设备


BMP180的工作流程大致如下: 首先读取相关寄存器获得校准数据,然后分别对相应寄存器进行读写,获取原始温度和气压数据; 接下来通过前面得到的校准数据和原始数据计算出实际的温度和气压值; 最后再根据气压值推测出海拔高度。读者可自行查看数据手册了解BMP180的工作模式、寄存器设置以及具体计算公式等内容。下面编写测试程序,创建名为bmp180.py的脚本,输入以下内容: 



import time

import smbus #导入smbus库实现树莓派和BMP180的I2C通信



class BMP180(): #定义BMP180类








def __init__(self, address=0x77, mode=1): #默认OSS=1(标准模式),设备地址0x77

self._mode = mode #单下画线开头的表示伪私有变量

self._address = address

self._bus = smbus.SMBus(1)  #创建smbus实例,1代表/dev/i2c-1



def read_u16(self,cmd): #读16位无符号数据

MSB =self._bus.read_byte_data(self._address,cmd)

LSB =self._bus.read_byte_data(self._address,cmd+1)

return (MSB << 8) + LSB



def read_s16(self,cmd): #读16位有符号数据

result =self.read_u16(cmd)

if result > 32767:

result -= 65536

return result



def write_byte(self,cmd,val):

self._bus.write_byte_data(self._address, cmd, val) #I2C总线写字节操作



def read_byte(self,cmd):

return self._bus.read_byte_data(self._address,cmd) #I2C总线读字节操作



def read_Calibration(self): #从22个寄存器读取校准数据

caldata = []

caldata.append(self.read_s16(0xAA))

caldata.append(self.read_s16(0xAC))

caldata.append(self.read_s16(0xAE))

caldata.append(self.read_u16(0xB0))

caldata.append(self.read_u16(0xB2))

caldata.append(self.read_u16(0xB4))

caldata.append(self.read_s16(0xB6))

caldata.append(self.read_s16(0xB8))

caldata.append(self.read_s16(0xBA))

caldata.append(self.read_s16(0xBC))

caldata.append(self.read_s16(0xBE))

return caldata



def read_rawTemperature(self):  #读取原始温度数据

self.write_byte(0xF4, 0x2E) #向控制寄存器0xF4发送读取温度命令0x2E

time.sleep(0.005) #等待测量完毕,延时至少4.5ms

rawTemp = self.read_u16(0xF6)

return rawTemp



def read_rawPressure(self): #读取原始气压数据

self.write_byte(0xF4, 0x34 + (self._mode << 6))

time.sleep(0.008) #OSS=1,延时不少于7.5ms








MSB = self.read_byte(0xF6)

LSB = self.read_byte(0xF7)

XLSB = self.read_byte(0xF8)

rawPressure = ((MSB << 16) + (LSB << 8) + XLSB) >> (8 - self._mode)

return rawPressure



def read_Temperature(self,caldata): #计算实际温度值,公式参见数据手册

UT = self.read_rawTemperature()

X1 = ((UT - caldata[5]) * caldata[4]) >> 15

X2 = (caldata[9] << 11) / (X1 + caldata[10])

B5 = X1 + X2

temp = (B5 + 8) / 16

return temp / 10.0



def read_Pressure(self,caldata): #计算实际气压值,公式参见数据手册

UT = self.read_rawTemperature()

UP = self.read_rawPressure()

X1 = ((UT - caldata[5]) * caldata[4]) >> 15

X2 = (caldata[9] << 11) / (X1 + caldata[10])

B5 = X1 + X2

B6 = B5 - 4000

X1 = (caldata[7] * (B6 * B6) / 2**12) / 2**11

X2 = (caldata[1] * B6) / 2**11

X3 = X1 + X2

B3 = (((int(caldata[0] * 4 + X3)) << self._mode) + 2) / 4

X1 = (caldata[2] * B6) / 2**13

X2 = (caldata[6] * (B6 * B6) / 2**12) / 2**16

X3 = ((X1 + X2) + 2) / 2**2

B4 = (caldata[3] * (X3 + 32768)) / 2**15

B7 = (UP - B3) * (50000 >> self._mode)

if B7 < 0x80000000:

p = (B7 * 2) / B4

else:

p = (B7 / B4) * 2

X1 = (p / 2**8) * (p / 2**8)

X1 = (X1 * 3038) / 2**16

X2 = (-7357 * p) / 2**16

p = p + ((X1 + X2 + 3791) / 2**4)

return p / 100.0



def read_Altitude(self,sealevel_hpa,pressure):  #sealevel_hpa是海平面大气压

altitude = 44330 * (1.0 - pow(pressure / sealevel_hpa, (1.0/5.255)))

return altitude









def read_BMP180_data():

bmp = BMP180() #创建BMP180实例


while True:

caldata = bmp.read_Calibration()

temp = bmp.read_Temperature(caldata)	

pressure = bmp.read_Pressure(caldata)	

altitude = bmp.read_Altitude(1013.25,pressure)#sealevel_hpa = 1013.25hPa

print("Altitude : %.2f m" % altitude)

print("Pressure : %.2f hPa" % pressure)

print("Temperature : %.2f C\n" % temp)

time.sleep(5)



if __name__ == '__main__':

read_BMP180_data()






运行上面的程序可以发现,计算出来的海拔高度为负数,这与所在地区的真实海拔明显不一致。其原因是
式(31)中p0的默认值是0℃时的海平面大气压值,需要根据实际情况进行校正。
这里的做法是,在不同楼层通过树莓派读取BMP180输出的大气压值p,同时用智能手机内置的指南针工具获取对应的海拔高度,按式(32)反向计算当前的p0,然后再将p0代入式(31)即可计算出比较精准的海拔高度。



p0=p1-altitude443305.255(32)


如表31所示,根据当前环境下8个楼层测得的大气压值和对应的海拔高度,计算出的p0值比较稳定(平均值1023.66hPa)。由于p0的值会随着温度改变发生变化,所以实际应用时需要进行修正。将bmp180.py脚本中sealevel_hpa的值替换为1023.66,再次运行程序,可得到准确的海拔高度,结果如图318所示。


表31不同楼层大气压与海拔测量值的对应关系



楼层大气压测量值/hPa海拔高度/mp0计算值/hPa

11021.96151023.78
21021.26201023.68
31020.73241023.64
41020.24281023.63
51019.87321023.75
61019.35361023.71
71018.85391023.57
81018.16441023.49



图318BMP180测试结果 


3.6空气质量检测

空气质量检测采用集CO2、PM2.5、PM10、温湿度、总挥发性有机物(TVOC)及甲醛(CH2O)于一体的综合型传感器模块,如图319所示。该模块供电电压5V,工作温度为0~50℃,采用串口通信协议,波特率默认为9600bps,数据传输周期默认为1s(可通过指令修改)。每次传输的数据共19字节,格式为: 报文头(0x01)+功能码(0x03)+数据长度(0x0E)+7个双字节数据(CO2、TVOC、CH2O、PM2.5、湿度、温度、PM10)+2字节的CRC16校验。


图319综合型空气质量传感器


由于树莓派的UART接口已分配给GPS模块使用,所以这里通过USB外接串口模块(CH340E)实现树莓派与空气质量传感器模块的连接,如图320所示。传感器模块和CH340E的具体接口如下: 5V和GND引脚分别直接相连,TXD和RXD引脚交叉相连,此外,传感器模块的SET引脚连接树莓派的2脚(5V)。


图320树莓派连接空气质量传感器



树莓派系统集成了USB转串口驱动,将CH340E插入树莓派USB接口就可以使用。在命令行输入lsusb查看连接的USB设备,输入ls l /dev/tty*查看设备的串口号,结果分别如图321(a)和图321(b)所示。在树莓派系统中,USB串口设备一般是根据设备插入顺序进行命名,依次是/dev/ttyUSB0、/dev/ttyUSB1等。


图321查看USB串口设备


创建脚本文件air_quality_senor.py,输入如下代码: 



import serial

import time

import binascii #用于二进制(byte类型数据)和ASCII的转换



class Multisensor(): #定义Multisensor类

def __init__(self):

self.ser = serial.Serial("/dev/ttyUSB0", 9600) #打开USB串口,波特率设为9600bps

self.time_sent=bytes.fromhex('42 78 01 00 00 00 00 FF') #设置数据传输周期1s

self.ser.write(self.time_sent) #通过串口向传感器模块写入指令

self.ser.flushInput() #清空串口接收缓存中的数据



def serial_rec(self):

count = self.ser.inWaiting() #返回串口接收缓存中的字节数

while count != 0:

recv=self.ser.read(count) #从串口读入指定的字节数








'''下条语句返回二进制数据的十六进制表示形式,将串口接收的19字节转

换成38位十六进制字符串,其中[2:-1]表示截取该行从第三位到最后一个字符

(换行符)之间的部分,该部分对应真正的有效数据'''

recv= str(binascii.b2a_hex(recv))[2:-1]

self.ser.flushInput() #清空接收缓存区

return recv



#以下各项空气指标的具体计算公式参见传感器模块文档

def co2_count(self,recv):

recv_co2 = recv[6:10] #从38位十六进制字符串中截取CO2数据

recv_co2_h = int(recv_co2[0:2],16) #高字节十六进制转换成十进制

recv_co2_l = int(recv_co2[2:4],16) #低字节十六进制转换成十进制

co2 = recv_co2_h*256 + recv_co2_l

print('(1).CO2 :  %d ppm' %co2)

return co2



def tvoc_count(self,recv):

recv_tvoc = recv[10:14] #从38位十六进制字符串中截取TVOC数据

recv_tvoc_h = int(recv_tvoc[0:2],16)

recv_tvoc_l = int(recv_tvoc[2:4],16)

tvoc = float(recv_tvoc_h*256 + recv_tvoc_l)/10.0

print('(2).TVOC :  %f ug/m3' %tvoc)

return tvoc



def ch20_count(self,recv):

recv_ch20 = recv[14:18] #从38位十六进制字符串中截取CH2O数据

recv_ch20_h = int(recv_ch20[0:2],16)

recv_ch20_l = int(recv_ch20[2:4],16)

ch20 = float(recv_ch20_h*256 + recv_ch20_l)/10.0

print('(3).CH20 :  %f ug/m3' %ch20)

return ch20



def pm25_count(self,recv):

recv_pm25 = recv[18:22] #从38位十六进制字符串中截取PM2.5数据

recv_pm25_h = int(recv_pm25[0:2],16)

recv_pm25_l = int(recv_pm25[2:4],16)

pm25 = recv_pm25_h*256 + recv_pm25_l

print('(4).PM2.5 :  %d ug/m3' %pm25)

return pm25



def humidity_count(self,recv):

recv_humidity = recv[22:26] #从38位十六进制字符串中截取湿度数据

recv_humidity_h = int(recv_humidity[0:2],16)

recv_humidity_l = int(recv_humidity[2:4],16)

srh = recv_humidity_h*256 + recv_humidity_l

humidity = -6 + 125*float(srh)/ 2**16









print('(6).Humidity :  %f %%RH' %humidity)

return humidity



def temp_count(self,recv):

recv_temp = recv[26:30] #从38位十六进制字符串中截取温度数据

recv_temp_h = int(recv_temp[0:2],16)

recv_temp_l = int(recv_temp[2:4],16)

stem = recv_temp_h*256 + recv_temp_l

temp = -46.85 + 175.72*float(stem)/ 2**16

print('(7).Temperature :  %f °C' %temp)

return temp



def pm10_count(self,recv):

recv_pm10 = recv[30:34] #从38位十六进制字符串中截取PM10数据

recv_pm10_h = int(recv_pm10[0:2],16)

recv_pm10_l = int(recv_pm10[2:4],16)

pm10 = recv_pm10_h*256 + recv_pm10_l

print('(5).PM10 :  %d ug/m3' %pm10)

return pm10



def read_sensor_data(self): #获取空气指标参数

while True:

recv = self.serial_rec()

#判断接收数据的格式是否正确

if recv != None and len(recv) == 38 and recv[0:6] == '01030e':

sto_co2 = self.co2_count(recv)

sto_tvoc = self.tvoc_count(recv)

sto_ch20 = self.ch20_count(recv)

sto_pm25 = self.pm25_count(recv)

sto_pm10 = self.pm10_count(recv)

sto_humidity = self.humidity_count(recv)

sto_temp = self.temp_count(recv)

break #直至接收到一次完整数据后退出本次循环 



return sto_co2,sto_tvoc,sto_ch20,sto_pm25,sto_pm10,sto_humidity,sto_temp



if __name__ == '__main__':  

try:

multisensor = Multisensor() #创建实例

while True:

multisensor.read_sensor_data() #调用Multisensor类的方法

print('-----------------------')

except KeyboardInterrupt:

if multisensor.ser != None:

multisensor.ser.close() #关闭USB串口






运行程序,可以同时监测7种空气指标参数,结果如图322所示。


图322空气质量传感器测试结果


3.7数字指南针

数字指南针也称作电子罗盘或磁力计,用于测量地球磁场的方向和大小。HMC5883L是一种带有数字接口的弱磁传感器芯片,采用各向异性磁阻(AMR)技术,灵敏度高、可靠性好,内置12位模数转换器,可以测量沿
X、Y和Z轴3个方向上的地球磁场值,测量范围从毫高斯到8高斯。HMC5883L模块及其引脚如图323所示,该模块工作电压为2.16~3.6V,工作电流100μA,罗盘航向精度1°~2°。


图323磁场传感器模块HMC5883L



和前面介绍的BMP180一样,HC5883L也遵循I2C协议。它和树莓派相连只需要4根线,即VCC、GND、SCL和SDA,具体连接如下: 将SDA和SCL引脚分别连接至树莓派GPIO接口的3脚和5脚,GND和VCC分别连接GPIO的20脚(GND)和17脚(3.3V)。连线接好后,在终端输入命令sudo i2cdetect y 1,如图324所示,可以看到在地址0x1e处检测到了一个设备,这就是外接的HMC5883L传感器。


图324查看HC5883L的地址



同样,树莓派使用smbus库对HC5883L模块进行读写操作。下面编写程序,通过树莓派读取HMC5883L模块沿X、Y和Z轴的磁场强度并计算其航向角。新建脚本文件hmc5883l.py,输入以下代码: 



import smbus

import time

import math



class HMC5883(): #定义HMC5883类

def __init__(self, address=0x1e,x_offset = 0.041304,y_offset = -0.132608):

'''HMC5883L设备地址0x1e, x_offset和y_offset分别为 x、y方向校准量'''

self._address = address

self._bus = smbus.SMBus(1) #创建smbus实例,1代表/dev/i2c-1

self.Magnetometer_config() #设置寄存器

self.x_offset = x_offset

self.y_offset = y_offset



def read_raw_data(self,addr): #addr为数据输出寄存器的高字节地址

high = self._bus.read_byte_data(address, addr) #读取高字节数据

low  = self._bus.read_byte_data(address, addr+1) #读取低字节数据

value = (high << 8) + low

if (value >= 0x8000): #两个字节以补码的形式存储

return -((65535 - value) + 1)

else:

return value



def Magnetometer_config(self): #设置配置寄存器A、B和模式寄存器,参看数据手册

self._bus.write_byte_data(self._address, 0, 0x74) #配置寄存器A地址0x00

self._bus.write_byte_data(self._address, 1, 0xe0) #配置寄存器B地址0x01

self._bus.write_byte_data(self._address, 2, 0) #模式寄存器地址0x02



'''XYZ轴数据输出寄存器高字节地址分别为0x03,0x07和0x05读取的原始数据除以增益'''

def get_magnetic_xyz(self):

x_data = self.read_raw_data(3)/230.0 #X轴输出数据

y_data = self.read_raw_data(7)/230.0 #Y轴输出数据

z_data = self.read_raw_data(5)/230.0 #Z轴输出数据

return [x_data,y_data,z_data]









def read_HMC5883_data(self):

x_data,y_data,z_data = self.get_magnetic_xyz()

x_data = x_data - self.x_offset #校正偏差

y_data = y_data - self.y_offset

print('x轴磁场强度: ', x_data, ' Gs')

print('y轴磁场强度: ', y_data, ' Gs')

print('z轴磁场强度: ', z_data, ' Gs')

#计算航向角

bearing = math.atan2(y_data, x_data)

if (bearing < 0):

bearing += 2 * math.pi

print("航向角: ", math.degrees(bearing),"\n") #将弧度转换为角度

return math.degrees(bearing)

#return round(math.degrees(bearing),2) #保留小数点后2位



if __name__ == '__main__':

hmc = HMC5883()

while True:

hmc.read_HMC5883_data()

time.sleep(5)






上例中,设置配置寄存器A的值为0x74,其功能是数据输出频率为30Hz,每次测量采样8个样本并将其平均值作为输出; 配置寄存器B的值为0xe0,其功能是将增益设置为230,输出数据的范围为0xF800~0x07FF; 模式寄存器的值为0x00,表示选择连续测量操作模式。读取X轴和Y轴数据寄存器的原始值,除以增益后得到各方向上的磁场强度,最后再计算出航向角。运行程序,以正北方为初始方向顺时针旋转传感器,航向角不断增大,结果如图325所示。


图325磁场强度与航向角测试结果



如果根据HMC5883L读取值计算出来的角度和指南针的角度有偏差,需要按如下步骤进行校正。首先将传感器模块水平放置,匀速旋转找出X轴和Y轴方向上磁场强度的最大值与最小值,即x_max、x_min、y_max、y_min,然后计算得到两个方向上的偏移量x_offset=(x_max+x_min)/2和y_offset=(y_max+y_min)/2。新建hmc5883l_calibration.py输入以下代码: 



import time

from hmc5883l import HMC5883



def calibrateMag(): #进行X轴和Y轴方向的校准,绕Z轴慢速转动

minx = 0

maxx = 0

miny = 0

maxy = 0



hmc = HMC5883() #创建实例

hmc.Magnetometer_config()



for i in range(0,200): #旋转过程中读取200个数据

x_out,y_out,z_out =hmc.get_magnetic_xyz()



if x_out < minx:

minx=x_out

if y_out < miny:

miny=y_out

if x_out > maxx:

maxx=x_out

if y_out > maxy:

maxy=y_out

time.sleep(0.1)

print("minx: ", minx)

print("miny: ", miny)

print("maxx: ", maxx)

print("maxy: ", maxy)

x_offset = (maxx + minx) / 2

y_offset = (maxy + miny) / 2

print("x_offset: ", x_offset)

print("y_offset: ", y_offset)



if __name__ == '__main__':

calibrateMag() #测试中,X箭头初始朝向北方,匀速旋转






校正测试结果如图326所示,其中的x_offset、y_offset即为hmc5883l.py中
X轴和Y轴磁场强度的校准量。实际应用时,读者需要根据当前位置进行校正并重新设定。


图326HMC5883l校正结果


3.8超声波测距
利用HCSR04超声波传感器可以检测前方的障碍物,实现超声波测距与避障功能。HCSR04模块如图327所示,包括超声波发射器、接收器与控制电路,4个接口从左到右分别为VCC、Trig(触发控制信号输入端)、Echo(回响信号输出端)和GND。该模块采用5V电压供电,工作电流15mA,工作频率40kHz,测量角度不大于15°,探测距离2~400cm,精度为0.3cm。


图327HCSR04超声波传感器



HCSR04模块使用简单,只需给Trig引脚至少10μs的高电平信号即可触发测距。超声波发射器会对外连续发送8个40kHz的脉冲。如果接收器检测到返回信号,则Echo引脚输出一个高电平,且该高电平的持续时间是超声波从发射到返回的时间。由此可以计算出前方障碍物的距离,即距离等于高电平持续时间乘以声速的积的一半。
将HCSR04的VCC和GND分别连接树莓派GPIO的4脚(5V)和30脚(GND),Trig端接树莓派GPIO的29脚,由于Echo端输出电压为5V,需要经过330Ω和470Ω电阻串联分压后连接到树莓派GPIO的31脚。新建脚本hcsr04.py,输入超声波测距程序,代码如下: 



import RPi.GPIO as GPIO

import time



class HCSR04(): #定义HCSR04类








def __init__(self,trigger=29,echo=31):#默认定义TRIG为29脚,ECHO为31脚

self.TRIG = trigger

self.ECHO = echo

GPIO.setwarnings(False) #禁用引脚设置的警告信息

GPIO.setmode(GPIO.BOARD)

GPIO.setup(self.TRIG, GPIO.OUT, initial = False)

GPIO.setup(self.ECHO, GPIO.IN)



def readDistanceCm(self):

GPIO.output(self.TRIG, True) #设置TRIG为高电平

time.sleep(0.00001) #等待10μs

GPIO.output(self.TRIG, False)

while GPIO.input(self.ECHO) == 0:

pass

start_time = time.time() #获取ECHO为高的起始时间

while GPIO.input(self.ECHO) == 1:

pass 

stop_time = time.time() #获取ECHO为高的终止时间

time_elapsed = stop_time - start_time #计算超声波发射到返回的时间

distance = (time_elapsed * 34000) / 2 #计算距离,单位为cm

return distance



if __name__ == "__main__":

try:

hcsr04 = HCSR04(29,31) #创建实例,可以根据需要替换为其他引脚

while True:

d = hcsr04.readDistanceCm()

print("Distance is %.2f cm" % d)

time.sleep(1)

except KeyboardInterrupt:

GPIO.cleanup() #释放GPIO资源






运行程序,以书本作为障碍物不断靠近超声波传感器模块,距离测量值不断变小,结果如图328所示。


图328超声波测距结果