第24章:综合项目实践
本章通过几个综合项目,整合前面所学的传感器和执行器知识。
前面各章把传感器和执行器逐个跑通,这一章把它们装到同一个程序里。综合项目的难点不在单个模块,而在时序和异常:传感器偶尔读失败怎么办、报警要不要去抖、显示刷新和采样会不会互相拖慢、断电后数据还在不在。下面三个项目覆盖了三种典型组合:多传感器加报警输出、双电机加运动控制、定时采集加数据落盘。
24.1 智能家居监控系统
24.1.1 系统功能
- 温湿度监测
- 光照检测
- 烟雾报警
- LCD 显示
- LED 指示
这个系统要同时采集温湿度、光照和烟雾三类数据,并输出声光报警。温湿度用 DHT11,光照和烟雾用模拟量,烟雾模块另外引一路数字比较器输出,用于快速判断阈值;显示留给 LCD1602,调试阶段先用串口代替。功能上多了一个容易被忽略的点:各类告警的优先级不同,烟雾最高,高温和湿度其次,处理顺序应按这个来。
24.1.2 硬件连接
DHT11 → GP19
光敏传感器 → GP26 (ADC0)
MQ-2 → GP27 (ADC1), GP22 (数字)
LCD1602 → I2C (GP20, GP21)
蜂鸣器 → GP15
LED → GP0
接线表里有两处需要核对。第一是 I2C:GP20/GP21 是 I2C0 的默认 SDA/SCL,LCD1602 的 I2C 转接板地址通常是 0x27 或 0x3F,扫描总线才能确定。第二是模拟输入:GP26、GP27 分别对应 ADC0 和 ADC1,而片内温度传感器占用 ADC4,不要和外部通道混用。DHT11 的数据脚接 GP19,模块自带上拉;MQ-2 需要预热,刚上电的几十秒读数偏高,程序启动阶段应把它排除在报警判断之外。
24.1.3 完整代码
'''
智能家居监控系统
'''
from machine import Pin, ADC, I2C
import dht
import time
# 初始化传感器
dht_sensor = dht.DHT11(Pin(19))
light_sensor = ADC(26)
smoke_analog = ADC(27)
smoke_digital = Pin(22, Pin.IN)
# 初始化执行器
buzzer = Pin(15, Pin.OUT)
led = Pin(0, Pin.OUT)
# 初始化 LCD
i2c = I2C(0, sda=Pin(20), scl=Pin(21))
# lcd = LCD1602_I2C(i2c) # 需要导入 LCD 驱动
class SmartHomeMonitor:
def __init__(self):
self.temp = 0
self.humidity = 0
self.light = 0
self.smoke = 0
self.smoke_alert = False
def read_sensors(self):
"""读取所有传感器"""
# DHT11
try:
dht_sensor.measure()
self.temp = dht_sensor.temperature()
self.humidity = dht_sensor.humidity()
except:
pass
# 光照
raw = light_sensor.read_u16()
self.light = 100 - (raw / 65535 * 100)
# 烟雾
self.smoke = smoke_analog.read_u16()
self.smoke_alert = smoke_digital.value() == 0
def check_alerts(self):
"""检查报警条件"""
alerts = []
if self.temp > 35:
alerts.append("高温警报!")
if self.humidity > 80:
alerts.append("湿度过高!")
if self.smoke_alert:
alerts.append("烟雾警报!")
return alerts
def update_display(self, alerts):
"""更新显示"""
# LCD 显示
# lcd.clear()
# lcd.set_cursor(0, 0)
# lcd.print(f"T:{self.temp}C H:{self.humidity}%")
# lcd.set_cursor(0, 1)
# if alerts:
# lcd.print(alerts[0][:16])
# else:
# lcd.print(f"L:{self.light:.0f}% OK")
# 串口输出
print(f"温度: {self.temp}°C 湿度: {self.humidity}% "
f"光照: {self.light:.0f}% 烟雾: {self.smoke}")
if alerts:
print(f"⚠️ 警报: {', '.join(alerts)}")
def handle_alerts(self, alerts):
"""处理报警"""
if alerts:
buzzer.value(1)
led.value(1)
time.sleep(0.2)
buzzer.value(0)
led.value(0)
time.sleep(0.2)
else:
buzzer.value(0)
led.value(0)
def run(self):
"""主循环"""
while True:
self.read_sensors()
alerts = self.check_alerts()
self.update_display(alerts)
self.handle_alerts(alerts)
time.sleep(2)
# 运行
monitor = SmartHomeMonitor()
monitor.run()
代码分三块:初始化、传感器读取、报警处理。初始化部分创建 DHT11 对象、两个 ADC、一个数字输入、蜂鸣器和 LED,I2C 对象留给 LCD。SmartHomeMonitor 类把状态存在实例属性里,主循环每轮先读传感器再判断再输出。读 DHT11 用 try/except 包住,失败时保留上一次的值——DHT11 的时序容错较差,偶尔一次读取失败属于正常现象,直接崩溃不可取,但连续失败超过若干次就应该标记传感器故障,而不是一直显示旧数据。 光照换算成百分比时用 100 减去归一化读数,这是因为多数光敏模块的输出电压随光照增强而升高,而这里希望「百分比越大越亮」,方向和模块输出相反。烟雾判断用数字口:模块的比较器输出低电平时表示超过阈值,与模拟量配合可以做趋势加阈值的双重判断。
check_alerts 把三类条件分别判断,结果放进列表,主循环据此决定显示内容。这段逻辑简单,但有两处工程细节值得改进。其一,报警输出只持续 0.2 秒,而主循环末尾 sleep 了 2 秒,实际听感是「滴一声、等两秒」,烟雾这类需要持续提醒的告警应该改成长鸣或按节奏反复,可以把鸣叫放到定时器中断里,或者缩短主循环周期、用计数器控制节奏。 其二,阈值判断没有迟滞,温度在 35°C 附近波动时会反复进入和退出告警,加一个回差(比如 35°C 触发、33°C 解除)能避免抖动。LCD 相关代码目前被注释掉,因为驱动依赖具体的 I2C 转接芯片,调试阶段用串口打印代替是合理做法,接上屏幕后记得把 clear、set_cursor、print 的顺序补全。
24.2 智能小车
24.2.1 功能
- 红外遥控
- 避障行驶
- 循线行驶
- 超声波测距
24.2.2 硬件连接
左电机 → GP14, GP15
右电机 → GP16, GP17
左避障 → GP2
右避障 → GP3
循线传感器 → GP4, GP5
超声波 → GP6 (TRIG), GP7 (ECHO)
红外接收 → GP11
小车要同时处理电机、避障、循线和测距,引脚占用比前两个项目多。电机四个引脚分左右两组,每组两个 GPIO 接 H 桥输入;避障和循线都是数字输入,传感器输出低电平表示检测到障碍或黑线;超声波用一对 TRIG/ECHO;红外接收留了 GP11,但示例代码里没有实现解码。上电前先单独确认每个模块的方向:避障传感器有电位器可以调检测距离,循线传感器要调到黑线时输出稳定翻转,否则后面的逻辑全是猜。
24.2.3 小车类
'''
智能小车控制类
'''
from machine import Pin, PWM
import time
class SmartCar:
def __init__(self):
# 电机
self.motor_left_1 = PWM(Pin(14))
self.motor_left_2 = PWM(Pin(15))
self.motor_right_1 = PWM(Pin(16))
self.motor_right_2 = PWM(Pin(17))
for m in [self.motor_left_1, self.motor_left_2,
self.motor_right_1, self.motor_right_2]:
m.freq(1000)
# 传感器
self.obs_left = Pin(2, Pin.IN)
self.obs_right = Pin(3, Pin.IN)
self.line_left = Pin(4, Pin.IN)
self.line_right = Pin(5, Pin.IN)
# 超声波
self.trig = Pin(6, Pin.OUT)
self.echo = Pin(7, Pin.IN)
def set_motors(self, left, right):
"""设置电机速度 (-100 到 100)"""
def set_motor(m1, m2, speed):
if speed > 0:
m1.duty_u16(int(speed / 100 * 65535))
m2.duty_u16(0)
elif speed < 0:
m1.duty_u16(0)
m2.duty_u16(int(-speed / 100 * 65535))
else:
m1.duty_u16(0)
m2.duty_u16(0)
set_motor(self.motor_left_1, self.motor_left_2, left)
set_motor(self.motor_right_1, self.motor_right_2, right)
def forward(self, speed=60):
self.set_motors(speed, speed)
def backward(self, speed=60):
self.set_motors(-speed, -speed)
def turn_left(self, speed=60):
self.set_motors(-speed, speed)
def turn_right(self, speed=60):
self.set_motors(speed, -speed)
def stop(self):
self.set_motors(0, 0)
def get_distance(self):
"""超声波测距"""
self.trig.low()
time.sleep_us(2)
self.trig.high()
time.sleep_us(10)
self.trig.low()
while self.echo.value() == 0:
start = time.ticks_us()
while self.echo.value() == 1:
end = time.ticks_us()
return time.ticks_diff(end, start) * 0.0343 / 2
def avoid_obstacle(self):
"""避障模式"""
left = self.obs_left.value() == 0
right = self.obs_right.value() == 0
if left and right:
self.backward()
time.sleep(0.5)
self.turn_left()
time.sleep(0.5)
elif left:
self.turn_right()
elif right:
self.turn_left()
else:
dist = self.get_distance()
if dist < 20:
self.turn_left()
else:
self.forward()
def follow_line(self):
"""循线模式"""
left = self.line_left.value() == 1 # 黑线
right = self.line_right.value() == 1 # 黑线
if left and right:
self.forward(50)
elif left:
self.turn_left(40)
elif right:
self.turn_right(40)
else:
# 丢失线路,搜索
self.turn_left(30)
# 使用
car = SmartCar()
# 避障模式
while True:
car.avoid_obstacle()
time.sleep(0.1)
SmartCar 把电机控制统一成 set_motors(left, right),两个参数的范围是 -100 到 100,正负号表示方向,内部再拆成两个 H 桥输入的高低电平,这样上层逻辑只关心速度和转向。forward、backward、turn_left、turn_right、stop 都是对它的封装。超声波测距先拉低 TRIG 保证起始电平干净,再给 10 微秒高脉冲,然后等 ECHO 的上升沿记开始时间、下降沿记结束时间,距离等于时间差乘 0.0343 再除 2:0.0343 厘米每微秒是声速 343 米每秒换算来的,除 2 是因为声音走了一个来回。 这里有一个必须补的防御——两个 while 等待回波都没有超时,传感器没接好或者超出量程时会永远卡住,主循环直接停摆。正确写法是记录起始时刻,等待超过 30 毫秒就返回一个无效值,让上层按「测不到」处理。避障逻辑先读左右两个数字传感器,两侧都有障碍就先后退再转向,单侧有障碍向反方向转,都没有才看超声波距离,二十厘米以内转向避让。循线逻辑按左右探头是否压到黑线组合判断:两侧都压线直行,单侧压线向该侧转,两侧都没压到说明脱离线路,沿固定方向搜索。
24.3 气象站
24.3.1 功能
- 温湿度采集
- 气压测量(如有)
- 数据记录
- OLED 显示
- 定时上报
气象站是三个项目里最接近「长期运行」的一个:定时采样、写文件、可选显示。硬件上 DHT11 接 GP19,RTC 用 Pico 内部模块,OLED 走 I2C。内部 RTC 没有后备电池,断电后时间会回到默认值,所以每次上电要么手动设置,要么外接 RTC 模块。这一点直接决定了日志里的时间戳是否可信,长期部署必须处理。
24.3.2 代码框架
'''
微型气象站
'''
from machine import Pin, I2C, RTC
import dht
import time
class WeatherStation:
def __init__(self):
self.dht = dht.DHT11(Pin(19))
self.rtc = RTC()
# self.oled = SSD1306_I2C(128, 32, I2C(0))
self.data_log = []
def read_sensors(self):
"""读取传感器"""
try:
self.dht.measure()
return {
'temp': self.dht.temperature(),
'humidity': self.dht.humidity(),
'timestamp': self.rtc.datetime()
}
except:
return None
def log_data(self, data):
"""记录数据"""
self.data_log.append(data)
# 保存到文件
with open('weather_log.csv', 'a') as f:
t = data['timestamp']
f.write(f"{t[0]}-{t[1]:02d}-{t[2]:02d} "
f"{t[4]:02d}:{t[5]:02d},"
f"{data['temp']},{data['humidity']}\n")
def display(self, data):
"""显示数据"""
print(f"温度: {data['temp']}°C 湿度: {data['humidity']}%")
# OLED 显示
# self.oled.fill(0)
# self.oled.text(f"Temp: {data['temp']}C", 0, 0)
# self.oled.text(f"Hum: {data['humidity']}%", 0, 12)
# self.oled.show()
def run(self, interval=60):
"""主循环"""
while True:
data = self.read_sensors()
if data:
self.display(data)
self.log_data(data)
time.sleep(interval)
# 运行
station = WeatherStation()
station.run(60) # 每分钟采集一次
WeatherStation 的思路是采样、记录、显示三段分离。read_sensors 返回一个字典,包含温度、湿度和 RTC 时间,读取失败返回 None,主循环据此跳过本轮。log_data 把时间格式化成 CSV 行追加写入文件,年、月、日、时、分分别取 RTC 元组的对应位置,月和日补零对齐。这里有几个实际部署才会遇到的问题:Pico 的 Flash 写入寿命有限,每分钟写一次、长年运行会很快消耗完,要么降低频率、批量写,要么外接 SD 卡;文件打开失败(存储满、路径不对)会直接抛异常,应该包在 try/except 里;RTC 时间没设置时写出来的日志日期没有意义,程序启动时应检查年份是否合理。display 部分同样留了 OLED 的注释,128×32 的屏幕放两行足够。run 方法的间隔用 sleep 实现,采样间隔会随着前面的处理时间累积漂移,对分钟级记录影响不大,对秒级要求就得改用 ticks_ms 计算下个采样点。
24.4 本章小结
本章展示了综合项目实践:
- 智能家居监控:
- 多传感器融合
- 报警处理
- 显示输出
- 智能小车:
- 电机控制
- 避障循线
- 模式切换
- 气象站:
- 数据采集
- 文件存储
- 定时任务
下一章将学习常见问题与故障排除。
三个项目都在演示同一件事:把「读传感器—做判断—驱动执行器」这条循环写稳。能跑通只是第一步,真正决定可用性的是异常处理、时序安排和电源设计。做完一个项目后回头量一下各环节的耗时,往往能发现瓶颈不在你以为的地方。