简介
在上一篇文章中我对当前系统的整个构成进行了概述, 虽然系统的传感器使用的比较多,但是使用的是Arduino 平台,所以对每个传感器的驱动并不复杂, 因此在本篇文章中我将完成所有组件的构建.
传感器外围部件清单
1 - 土壤湿度传感器(Analog)
2- 水流量传感器YF-S401
3- OLED 0.96
4- SHT30
5- LTR-329
6- NMOS 用于PWM驱动LED灯板
7- LED灯板
8- 继电器 (如果使用5V可以使用NMOS控制开关, 不需要使用继电器)
9- 12V 水泵 (可以调整为5V)
实物俯视图

实物平视图

OLED 显示
Flask上位机监控界面
图图表一为系统上电时 PID动态调整灯光亮度的曲线图.
系统稳定时即PID调整后的光照输出 (稳定在了亮度200)

Arduino程序设计
#include <Wire.h>
#include "Adafruit_SSD1306.h"
#include "Adafruit_GFX.h"
#include "Adafruit_LTR329_LTR303.h"
#include "Adafruit_SHT4x.h"
#include <ArduinoJson.h>
// OLED 屏幕配置
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire1, OLED_RESET);
// 传感器对象
Adafruit_LTR329 ltr = Adafruit_LTR329();
Adafruit_SHT4x sht4;
// PID 控制相关
const int pwmPin = 3; // IO2 PWM 输出
const int targetCh0 = 200; // 目标 CH0 值
float Kp = 0.18, Ki = 0.0825, Kd = 0.002;
float integral = 0, last_error = 0;
unsigned long lastTime = 0;
volatile unsigned int pulseCount = 0;
unsigned long lastFlowCalcTime = 0;
float flowRate_mLps = 0; // 毫升每秒
float totalMilliLiters = 0;
const int pumpPin = 4; // IO4 控制继电器
const int soilThreshold = 600; // 土壤湿度阈值(需根据实际情况调整)
bool pumping = false;
float pumpStartVolume = 0;
volatile bool isTrunonLight = true;
#define FLOW_SENSOR_PIN 2
void flowPulseISR() {
pulseCount++;
}
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("Initializing...");
pinMode(pwmPin, OUTPUT);
// 初始化 OLED
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 initialization failed!"));
while (true);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
// 初始化 LTR329
if (!ltr.begin(&Wire1)) {
Serial.println("Couldn't find LTR sensor!");
while (1) delay(10);
}
ltr.setGain(LTR3XX_GAIN_2);
ltr.setIntegrationTime(LTR3XX_INTEGTIME_100);
ltr.setMeasurementRate(LTR3XX_MEASRATE_200);
// 初始化 SHT40
if (!sht4.begin(&Wire1)) {
Serial.println(F("SHT40 sensor not found!"));
while (1);
}
sht4.setPrecision(SHT4X_HIGH_PRECISION);
sht4.setHeater(SHT4X_NO_HEATER);
pinMode(FLOW_SENSOR_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(FLOW_SENSOR_PIN), flowPulseISR, RISING);
pinMode(pumpPin, OUTPUT);
digitalWrite(pumpPin, LOW); // 默认关闭水泵
}
void loop() {
if(Serial.available() >0)
{
char c = Serial.read();
Serial.println(c);
if(c == '1')
{
isTrunonLight = !isTrunonLight;
}
}
uint16_t ch0 = 0, ch1 = 0;
float temperature = 0, humidity = 0;
// 读取光照
if (ltr.newDataAvailable()) {
StaticJsonDocument<256> doc;
bool valid = ltr.readBothChannels(ch0, ch1);
if (valid && isTrunonLight) {
// PID 计算
float error = targetCh0 - ch0;
unsigned long now = millis();
float dt = (now - lastTime) / 1000.0;
lastTime = now;
integral += error * dt;
float derivative = (error - last_error) / dt;
last_error = error;
float output = Kp * error + Ki * integral + Kd * derivative;
output = constrain(output, 0, 255);
analogWrite(pwmPin, (int)output);
// 每 1000ms 计算一次水流量
if (millis() - lastFlowCalcTime >= 1000) {
noInterrupts();
unsigned int pulses = pulseCount;
pulseCount = 0;
interrupts();
// 每脉冲 ≈ 2.22 毫升(YF401)
flowRate_mLps = pulses * 2.22;
totalMilliLiters += flowRate_mLps;
lastFlowCalcTime = millis();
}
display.print("PWM: ");
display.println((int)output);
doc["pwm"] = (int)output;
}else{
analogWrite(pwmPin, 0);
}
// 读取温湿度
sensors_event_t temp_event, hum_event;
sht4.getEvent(&hum_event, &temp_event);
temperature = temp_event.temperature;
humidity = hum_event.relative_humidity;
// 读取土壤湿度
int soilValue = analogRead(A5);
// 显示到 OLED
display.clearDisplay();
display.setCursor(0, 0);
display.print("Temp: ");
display.print(temperature, 1);
display.println(" C");
display.print("Humidity: ");
display.print(humidity, 1);
display.println(" %");
display.print("Light CH0: ");
display.println(ch0);
display.print("Target: ");
display.println(targetCh0);
display.print("Flow: ");
display.print(flowRate_mLps, 1);
display.println(" mL/s");
display.print("Total: ");
display.print(totalMilliLiters, 0);
display.println(" mL");
if (!pumping && soilValue > soilThreshold) {
digitalWrite(pumpPin, HIGH);
pumping = true;
pumpStartVolume = totalMilliLiters;
}
// 已在泵水 -> 检查是否达到100mL
if (pumping && (totalMilliLiters - pumpStartVolume >= 100.0)) {
digitalWrite(pumpPin, LOW);
pumping = false;
}
int barLength = map(soilValue, 1023, 0, 0, 100); // 显示湿度条(反映湿度)
display.drawRect(0, 58, 100, 5, SSD1306_WHITE); // 条边框
display.fillRect(0, 58, barLength, 5, SSD1306_WHITE); // 填充条
display.display();
doc["temperature"] = temperature;
doc["humidity"] = humidity;
doc["light_ch0"] = ch0;
doc["light_ch1"] = ch1;
doc["target"] = targetCh0;
doc["flow_mLps"] = flowRate_mLps;
doc["total_mL"] = totalMilliLiters;
doc["soil"] = soilValue;
doc["pump"] = pumping;
serializeJson(doc, Serial);
Serial.println();
}
delay(500); // 避免刷新过快
}在上述的程序中对PID的调整稍微有一点麻烦, 这里有一个调整的思路就是, 首先将KI和KD都设置为0, 首先调整KP, 这里调整KP的时候可能会出现抖动的情况. 当抖动并不是很大的时候(LED闪烁频率没有那么大), 然后调整KI用来消除稳态误差. 当光照逐渐变稳定的时候再尝试调整KD来减缓超调与振荡. 此时LED的亮度已经已经稳定了. 如果觉得LED的亮度上升太慢或者下降太慢的话再逐渐改变KP的值. 到最后我系统中所调整的值为:
float Kp = 0.18, Ki = 0.0825, Kd = 0.002;
使其程序在运行的时候光照强度(可见光 + 红外光) 的亮度稳定在了 200. (上述的PID算法同样可以用于温控系统, 比如说恒温箱等)
核心代码如下所示
float error = targetCh0 - ch0; unsigned long now = millis(); float dt = (now - lastTime) / 1000.0; lastTime = now; integral += error * dt; float derivative = (error - last_error) / dt; last_error = error; float output = Kp * error + Ki * integral + Kd * derivative; output = constrain(output, 0, 255); analogWrite(pwmPin, (int)output);
Python 上位机程序设计
python的上位机是运行在虚拟的树莓派OS中的, 其中和Arduino的通讯主要是采用串口通讯. 其后端框架采用的是Flask, 支持将数据保存到数据库中.

其主要的核心逻辑即读取串口的JSON输入, 然后通过HTML的定时器定时发送HTTP请求来读取串口数据,然后使用chart.js进行绘图.
串口线程(读取Arduino串口数据)
# 串口线程函数
def read_serial():
global latest_data
try:
while True:
line = ser.readline().decode('utf-8').strip()
if line:
try:
data = json.loads(line) # 解析 JSON 数据
latest_data = data # 更新最新的数据
print("Received:", data)
insert_data_to_db(data) # 插入数据到数据库
except json.JSONDecodeError:
print("Invalid JSON:", line)
except serial.SerialException as e:
print("Serial error:", e)
# 启动串口线程
serial_thread = threading.Thread(target=read_serial, daemon=True)
serial_thread.start()返回串口数据用于前端绘图
@app.route('/data')
def get_data():
return jsonify(latest_data)定时请求串口数据:
async function fetchData() {
try {
const res = await fetch('/data');
const data = await res.json();
document.getElementById("json").textContent = JSON.stringify(data, null, 2);
const now = new Date().toLocaleTimeString();
if (labels.length >= maxDataPoints) {
labels.shift();
lightData.shift();
tempData.shift();
humidityData.shift();
soilData.shift();
}
labels.push(now);
lightData.push(data.light_ch0);
tempData.push(data.temperature);
humidityData.push(data.humidity);
soilData.push(data.soil);
[lightChart, tempChart, humidityChart, soilChart].forEach((chart, i) => {
chart.data.labels = labels;
chart.data.datasets[0].data = [lightData, tempData, humidityData, soilData][i];
chart.update();
});
} catch (err) {
console.error("Failed to fetch data:", err);
}
}
setInterval(() => {
fetchData();
fetchStats();
}, 2000);附件
注意: 系统并不需要使用上位机程序进行监控, 不运行上位机对Arduino的执行没有任何影响
我要赚赏金
