DIY PM 2.5 监测仪 (1): 采集和显示
如今北京的空气,嗯,关注点空气质量自我保护吧,废话不多说了,开工吧。
基本原理
有一定精确性但成本比较低的颗粒物检测方法差不多都用的是同一个原理——由于细颗粒物的直径(微米级)和光的波长(可见光的波长是 0.39-0.7 微米)比较接近,所以细颗粒物会对光产生比较强烈的散射,这也就是 PM 2.5 高的时候,空气的能见度一定不好的原因。当然,PM 2.5 高对能见度不好是充分非必要条件,其他因素也可能导致空气能见度不佳。
基于这个原理,发射一束激光,在一定角度上观测散射光的强度,就可以判断出颗粒物的浓度,当然,这个方法要求对空气中颗粒物直径分布模型、产生散射的物质的成分等有一个假设,同时还受限于光强度测量的精确性,因此,这个测量的精确度是受限的,对于科研可能不太够,但对日常生活的空气检测来说,应该是比较有效和准确的。这次我们的监测仪使用的就是采用这个原理制成的一个模块。
准备材料
- raspberry pi 或 arduino,目前测试用的是 rpi 2 和 arduino uno,订的 arduino nano 正在路上,nano 就又小又便宜了。
- 传感器,我用的是淘宝上买的 PMS1003,大家自行搜索 PM2.5 传感器吧,关键字是“激光”或 laser。不同的传感器接口啥的可能不太一样,我这个传感器自带 UART 输出,所以可以从串口直接读取数据。
- LCD1602 液晶显示,我用了一块 8574 转换电路,转成了 I2C 接口,这样比较省接口。可以直接买一个带 I2C 的 LCD,也可以分别买了自己连,嗯,我是分两次买自己连的,因为我最早(去年?)不知道有 I2C 的……
- 连接线
硬件链接
因为插接件不太兼容,我把传感器的连接线一端截开焊在了一块板子上,连上插针,方便连接,如果手头有合适插接件的话,可能不用动烙铁了。具体连接很简单:
传感器连接
- 5V 供电
- 地
- Setting 管脚,这个接在 Pi 的一个 GPIO 接口上(比如 GPIO 17)或 Arduino 的一个接口上,比如这次用的是 IO 2。这个管脚拉高,模块处于工作状态
- TXD 连接到 Pi 的 RXD 或 Arduino 的 RXD 上,做采集
LCD
- SDA/SCL I2C 的数据和时钟,接在 Pi 或者 Arduino 的 数据和时钟上
- Vcc/GND 接 5V 电源和地,实测接 3.3V 会供电不足
软件环境
Pi 的话需要准备下,要把缺省关闭的 I2C 打开,并且把 console 连接的串口去掉,空出串口来给我们采集数据用。
其他的就是要查一下 LCD 的 I2C 地址。
主体流程
- 初始化
- 液晶初始化:设置合适的格式和背光
- 模块初始化:初始化串口,找到
0x42
,0x4d
作为帧的开始,一帧 32 个字节(不同模块参考相应的手册)
- 采集
- 采集对应字节的数据,从大结尾转为短整型,然后显示在液晶屏上
如此而已,非常简单,以后我们再考虑用蓝牙讲数据发送出来。
源码清单
Pi 和 Arduino 的源码如下:
Pi 源码
主程序:
#!/usr/bin/env python import RPi.GPIO as GPIO import serial import pylcdlib # pylcdlib from https://gist.github.com/gnawux/4f68b8e301b203489336 def readbe16(s, pos): return ( ord(s[pos])<<8) + ord(s[pos+1]) GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) GPIO.setup(17, GPIO.OUT) GPIO.output(17, True) # define bus address, port, and pins display = pylcdlib.lcd(0x27,1, Rs=0, Rw=1, En=2, Backlight=3, D4=4, D5=5, D6=6, D7=7) display.lcd_clear() ser = serial.Serial("/dev/ttyAMA0") ser.baudrate=9600 data = ser.read(32) pos = 0 while True: pos = data[pos:].index(chr(0x42)) if ord(data[(pos+1)&31]) == 0x4d: print('found BM at %d' % (pos)) ser.read(pos) break pos = pos + 1 if pos > 31: print('frame format error') exit(1) l1d=-1 l2d=-1 while True: data = ser.read(32) if data[0] != 'B' or data[1] != 'M': print('frame format error while reading data: %s, %s' % (data[:2], data)) exit(1) length = readbe16(data, 2) if length != 28: print('length error : %d' % (length)) exit(1) pm25std = readbe16(data, 6) pm25atm = readbe16(data, 12) if l1d != pm25std: l1d = pm25std display.lcd_puts('std: %d ug/m3' % l1d,1) if l2d != pm25atm: l2d = pm25atm display.lcd_puts('atm: %d ug/m3'% l2d,2)
LCD显示库
#!/usr/bin/env python """ original from http://www.rpiblog.com/2012/07/interfacing-16x2-lcd-with-raspberry-pi.html I modified it, thus you can set customized pin defines, such as lcd(0x27,1, Rs=0, Rw=1, En=2, Backlight=3, D4=4, D5=5, D6=6, D7=7) address 0x27 port 1 P0 - Rs P1 - RW P2 - En P3 - Backlight P4-P7 - D4-D7 """ import smbus from time import * # General i2c device class so that other devices can be added easily class i2c_device: def __init__(self, addr, port): self.addr = addr self.bus = smbus.SMBus(port) def write(self, byte): self.bus.write_byte(self.addr, byte) def read(self): return self.bus.read_byte(self.addr) def read_nbytes_data(self, data, n): # For sequential reads > 1 byte return self.bus.read_i2c_block_data(self.addr, data, n) class lcd: #initializes objects and lcd def __init__(self, addr, port, **kwarg): self.pin = {} self.lcd_device = i2c_device(addr, port) self.config(**kwarg) self.Backlight = True self.low_level_write(En=False, Rs=False, Rw=False, data=3) self.lcd_strobe() sleep(0.0005) self.lcd_strobe() sleep(0.0005) self.lcd_strobe() sleep(0.0005) self.low_level_write(En=False, Rs=False, Rw=False, data=2) self.lcd_strobe() sleep(0.0005) self.lcd_write(0x28) self.lcd_write(0x08) self.lcd_write(0x01) self.lcd_write(0x06) self.lcd_write(0x0C) self.lcd_write(0x0F) def config(self, **kwarg): self.pin['Rs'] = kwarg.get('Rs', 4) self.pin['Rw'] = kwarg.get('Rw', 5) self.pin['En'] = kwarg.get('En', 6) self.pin['Backlight'] = kwarg.get('Backlight', 7) self.pin['D4'] = kwarg.get('D4', 0) self.pin['D5'] = kwarg.get('D5', 1) self.pin['D6'] = kwarg.get('D6', 2) self.pin['D7'] = kwarg.get('D7', 3) def backlight(self, on=True): self.backlight = on def low_level_write(self, En, Rs, Rw, data): byte = 0 if En: byte = byte | 1 << self.pin['En'] if Rs: byte = byte | 1 << self.pin['Rs'] if Rw: byte = byte | 1 << self.pin['Rw'] if self.backlight: byte = byte | 1 << self.pin['Backlight'] if data & 0x01 : byte = byte | 1 << self.pin['D4'] if data & 0x02 : byte = byte | 1 << self.pin['D5'] if data & 0x04 : byte = byte | 1 << self.pin['D6'] if data & 0x08 : byte = byte | 1 << self.pin['D7'] self.lcd_device.write(byte) # clocks EN to latch command def lcd_strobe(self): self.lcd_device.write(self.lcd_device.read() | 1 << self.pin['En'] ) self.lcd_device.write(self.lcd_device.read() & (0xFF - (1<< self.pin['En'])) ) # write a command to lcd def lcd_write(self, cmd): self.low_level_write(En=False, Rs=False, Rw=False, data=(cmd>>4)) self.lcd_strobe() self.low_level_write(En=False, Rs=False, Rw=False, data=(cmd&0x0F)) self.lcd_strobe() self.low_level_write(En=False, Rs=False, Rw=False, data=0) # write a character to lcd (or character rom) def lcd_write_char(self, charvalue): self.low_level_write(En=False, Rs=True, Rw=False, data=(charvalue>>4)) self.lcd_strobe() self.low_level_write(En=False, Rs=True, Rw=False, data=(charvalue&0x0F)) self.lcd_strobe() self.low_level_write(En=False, Rs=False, Rw=False, data=0) # put char function def lcd_putc(self, char): self.lcd_write_char(ord(char)) # put string function def lcd_puts(self, string, line): if line == 1: self.lcd_write(0x80) if line == 2: self.lcd_write(0xC0) if line == 3: self.lcd_write(0x94) if line == 4: self.lcd_write(0xD4) for char in string: self.lcd_putc(char) # clear lcd and set to home def lcd_clear(self): self.lcd_write(0x1) self.lcd_write(0x2) # add custom characters (0 - 7) def lcd_load_custon_chars(self, fontdata): self.lcd_device.bus.write(0x40); for char in fontdata: for line in char: self.lcd_write_char(line) if __name__ == '__main__': # define bus address, port, and pins display = lcd(0x27,1, Rs=0, Rw=1, En=2, Backlight=3, D4=4, D5=5, D6=6, D7=7) display.lcd_clear() # line 1 and line 2 display.lcd_puts("I2C LCD on R-Pi",1) display.lcd_puts(" by gnawux",2)
Arduino 源码
主程序:
#include#include #include // F Malpartida's NewLiquidCrystal library #define I2C_ADDR 0x27 // Define I2C Address for controller #define BACKLIGHT_PIN 3 #define En_pin 2 #define Rw_pin 1 #define Rs_pin 0 #define D4_pin 4 #define D5_pin 5 #define D6_pin 6 #define D7_pin 7 #define LED_OFF 0 #define LED_ON 1 const int SetPin = 2; char frame[32]; LiquidCrystal_I2C lcd(I2C_ADDR,En_pin,Rw_pin,Rs_pin,D4_pin,D5_pin,D6_pin,D7_pin,BACKLIGHT_PIN,POSITIVE); char banner1[17]; char banner2[17]; void banner_init() { for(int i = 0; i<16;i++) { banner1[i] = ' '; banner2[i] = ' '; } banner1[16] = '\0'; banner2[16] = '\0'; } void banner_display() { lcd.setCursor(0,0); lcd.print(banner1); lcd.setCursor(0,1); lcd.print(banner2); } void debug(const char* str) { lcd.setCursor(0,0); lcd.print(str); } void initPanTower() { debug("begin"); while (1) { if (!Serial.available()) { delay(100); debug("wating serial"); continue; } debug("got serial"); int len = 0; int nr = Serial.readBytes(frame, 32); while (len + nr < 32) { len = len + nr; nr = Serial.readBytes(frame + len, 32 - len); } debug("got frame"); for (int diff = 0; diff < 32 ; diff++ ) { if (frame[diff] == 'B' && frame[(diff+1)%32] == 'M') { nr = len = 0; while (len + nr < diff) { len = len +nr; nr = Serial.readBytes(frame + len, diff - len); } digitalWrite(LED_BUILTIN, HIGH); return; } } } } void setup() { //Initialize LCD lcd.begin (16,2); // initialize the lcd lcd.backlight(); banner_init(); //Enable Monitor Module pinMode(SetPin, OUTPUT); digitalWrite(SetPin, HIGH); //shut the led pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, LOW); //Start Serial Port Serial.begin(9600); initPanTower(); } void loop() { if (Serial.available()) { int len = 0; int nr = Serial.readBytes(frame, 32); while (len + nr < 32) { len = len + nr; nr = Serial.readBytes(frame + len, 32 - len); } int d1 = ((int)frame[6] << 8) + frame[7]; int d2 = ((int)frame[12] << 8) + frame[13]; snprintf(banner1, 16, "std: %d ug/m3 ", d1); snprintf(banner2, 16, "atm: %d ug/m3 ", d2); banner_display(); } }