电导率 μS/cm
土壤 pH 值
综合污染指数 CPI
系统时间
AI 基于植被指数 (ExG/GLI/HSV) 自动识别生长阶段
三叶期 · 抽雄期 · 开花期 · 腊熟期AI 拍照自动识别,也可手动切换
-- μS/cm
| 当前阶段 | -- |
| 适宜 EC | -- |
下限: -- μS/cm
上限: -- μS/cm
pH ADC: --
EC ADC: --
运行时间: -- s
系统由 STM32 主控 + 双传感器 + OLED 显示 + USB 串口通信组成
感知层 → 控制层 → 通信层,三层架构设计
模拟量 pH 复合电极,BNC 接口连接信号调理板,输出 0~5V 模拟电压。
通过 STM32 ADC(PA1)采样,经线性校准公式换算为 pH 值(量程 0~14)。
玉米适宜土壤 pH 范围 6.0~7.5,超出范围会影响微量元素(锌、铁、硼)的有效性。
系统实时监测 pH 并通过串口上传,pH 值参与综合污染指数 CPI 计算。
模拟量 TDS/EC 传感器探头,交流激励两电极法测量土壤溶液电导率。
通过 STM32 ADC(PA0)采样,12 位分辨率(0~4095),经温度补偿后换算为 μS/cm 值。
电导率反映土壤中可溶性盐和微量元素离子浓度,是玉米营养诊断的核心指标。
不同生长阶段有对应的适宜 EC 范围:三叶期 100~300 μS/cm,开花期 280~550 μS/cm。
ARM Cortex-M3 内核,72MHz 主频,64KB Flash / 20KB SRAM。
负责双通道 ADC 采集(pH + EC)、I2C 驱动 OLED 本地显示、USART1 串口输出数据。
SysTick 毫秒定时器提供 10Hz (100ms) 采集节拍。支持 ST-LINK SWD 一键下载调试。
固件基于 Keil MDK + HAL 库开发,串口输出自定义格式供浏览器 Web Serial API 读取。
SSD1306 驱动芯片,128×64 分辨率,I2C 接口(地址 0x3C)。
PB0/PB1 软件模拟 I2C 时序驱动。本地实时显示 pH 值 / EC 值 / 运行秒数。
10Hz 刷新频率,方便不连接电脑时也能在田间查看传感器数据。
异常时显示 "ERR" 闪烁告警。
USB 转 TTL 串口芯片,连接 STM32 的 PA9(TX) / PA10(RX),115200 bps 速率。
配合浏览器 Web Serial API,无需安装任何驱动程序即可在 Chrome/Edge 中直接读取串口数据。
实现真正的"零安装"体验——打开网页,点击连接,数据实时呈现。
输出格式: [时间戳] pH=xxx EC=xxx raw0=xxx raw1=xxx
SWD 接口(SWCLK/SWDIO/GND/3.3V)连接 STM32,Keil MDK 一键编译+下载。
支持断点调试、变量查看、寄存器监控。4 根杜邦线连接即可,无需额外配置。
MB-102 面包板 + 跳线 + 杜邦线 + 飞线,用于搭建传感器与 STM32 的电路连接原型。
无需焊接,方便快速迭代和实验调整。所有传感器模块通过杜邦线连接到 STM32 对应引脚。
| STM32 引脚 | 连接外设 | 类型 | 说明 |
|---|---|---|---|
| PA0 | EC 探头 AO | ADC1_IN0 | 电导率模拟采样,12位分辨率,10次平均 |
| PA1 | pH 传感器 AO | ADC1_IN1 | 酸碱度模拟采样,12位分辨率 |
| PB0 | OLED SCL | GPIO (软I2C) | 软件模拟 I2C 时钟线 |
| PB1 | OLED SDA | GPIO (软I2C) | 软件模拟 I2C 数据线 |
| PA9 | CH340 RX | USART1_TX | 串口发送数据 (115200bps / 8N1) |
| PA10 | CH340 TX | USART1_RX | 串口接收 (备用,固件升级预留) |
| PA13 | ST-LINK SWDIO | SWD | 程序下载 / 调试数据线 |
| PA14 | ST-LINK SWCLK | SWD | 程序下载 / 调试时钟线 |
ADC1 初始化 (12MHz/12bit/双通道)
USART1 初始化 (115200/8N1)
SSD1306 OLED 初始化
SysTick 1ms 定时器启动
传感器上电自检
每 100ms 读 pH ADC (PA1)
每 100ms 读 EC ADC (PA0)
10次采样取平均 + 温度补偿
串口输出格式化数据
OLED 刷新三行显示
pH 超量程(>14 或 <0) → 告警
EC 超量程(>2000μS/cm) → 告警
OLED 显示 "ERR" 闪烁
串口输出 {"error":"sensor"}
3秒间隔自动尝试恢复
STM32F103C8T6 固件 — Keil MDK + HAL 库开发
/** * pH 传感器 ADC 采样 — PA1 (ADC1_IN1) * 10次采样取平均, 线性换算为 pH 值, 钳位 6.5~9.5 */ float pH_Read(void) { u32 adc_sum = 0; for (int i = 0; i < 10; i++) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); adc_sum += ADC_GetConversionValue(ADC1); } float adc_avg = adc_sum / 10.0; // 线性换算: pH = ADC值 × 14 / 4096 float ph = adc_avg * 14.0 / 4096.0; // 钳位到土壤合理范围 if (ph < 6.5) ph = 6.5; if (ph > 9.5) ph = 9.5; return ph; }
/** * EC 电导率 ADC 采样 — PA0 (ADC1_IN0) * 10次采样取平均, 电压换算 + 温度补偿 → μS/cm */ u16 EC_Read(float temp) { u32 adc_sum = 0; for (int i = 0; i < 10; i++) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); adc_sum += ADC_GetConversionValue(ADC1); } float adc_avg = adc_sum / 10.0; // 电压换算: V = ADC × 3.3V / 4096 float voltage = adc_avg * 3.3 / 4096.0; // EC 计算 (含温度补偿): EC = V × coeff / (1 + 0.02×(temp-25)) float compensation = 1.0 + 0.02 * (temp - 25.0); u16 ec = (u16)(voltage * 600.0 / compensation); return ec; }
/** * 主循环: 100ms 周期采集 → OLED 显示 → 串口输出 * 串口格式: [timestamp] pH=xxx EC=xxx raw0=xxx raw1=xxx\r\n */ int main(void) { System_Init(); // 时钟 72MHz ADC1_Init(); // PA0+PA1 双通道, 12bit UART1_Init(115200); // PA9 TX, 8N1 OLED_Init(); // SSD1306, I2C 软模拟 float ph; u16 ec, raw_ec, raw_ph; u32 tick = 0; char buf[64]; while (1) { ph = pH_Read(); // ADC1_IN1 → pH 值 ec = EC_Read(20.0); // ADC1_IN0 → EC 值 (默认25°C补偿) // OLED 本地显示 (三行) OLED_Clear(); OLED_Printf(0, 0, "pH : %.1f", ph); OLED_Printf(0, 2, "EC : %d uS", ec); OLED_Printf(0, 5, "Time: %ds", tick / 10); // 串口输出 → 浏览器 Web Serial API 读取 // 格式: [时间戳] pH=整数值 EC=整数值 raw0=ADC raw1=ADC u16 ph_int = (u16)(ph * 100); // pH 7.0 → 700 sprintf(buf, "[%lu] pH=%d EC=%d\r\n", tick, ph_int, ec); UART1_SendString(buf); delay_ms(100); // 10Hz 采集频率 tick++; } }
零驱动 · 零安装 · Chrome/Edge 原生支持
// ===== Web Serial API — 连接 STM32 串口 ===== async function connectSerial() { if (!('serial' in navigator)) { // 浏览器不支持 → 启用手动输入降级方案 document.getElementById('manual-bar').classList.add('show'); return; } port = await navigator.serial.requestPort(); await port.open({ baudRate: 115200 }); readSerialLoop(); } // ===== 数据读取循环 ===== async function readSerialLoop() { reader = port.readable.getReader(); let buffer = ''; while (serialKeepAlive) { const { value, done } = await reader.read(); if (done) break; buffer += new TextDecoder().decode(value); const lines = buffer.split('\n'); buffer = lines.pop(); // 保留不完整行 for (const line of lines) { const data = parseLine(line.trim()); if (data) renderData(data); } } } // ===== 数据解析: [时间戳] pH=xxx EC=xxx ===== function parseLine(line) { const m = line.match(/\[(\d+)\]\s+pH=(\d+)\s+EC=(\d+)/); if (!m) return null; let ph = parseInt(m[2]) / 100.0; // 700 → 7.00 if (ph < 6.5) ph = 6.5; if (ph > 9.5) ph = 9.5; return { ts: parseInt(m[1]), ph: ph, ec: parseInt(m[3]) }; } // ===== 综合污染指数 CPI ===== function calcCPI(ph, ec) { return 0.5 * Math.abs(ph - 6.8) / 6.8 + 0.5 * ec / 150.0; }
纯图像处理 · 无外部 AI 依赖 · PIL + NumPy 本地计算
ExG (Excess Green) — Woebbecke 1995: ExG = 2G − R − B,区分植被与土壤背景。
GLI (Green Leaf Index) — Gitelson 2002: GLI = (2G−R−B) / (2G+R+B),归一化绿叶覆盖度。
HSV 色彩分割 — 绿色调 (H:40°~150°) vs 黄色调 (H:10°~50°) 比例判断成熟度。
纹理分析 — 局部标准差 (Meyer & Neto 2008),20×20 滑动窗口,反映叶片纹理复杂度。
四条件级联判定: 黄色比例>10% → 腊熟期 | 覆盖度>40%+黄色>8% → 开花期 | 覆盖度<28%+土壤>22% → 三叶期 | 覆盖度>35%+ExG>15 → 抽雄期
# ===== 玉米生长阶段 AI 分析 (PIL + NumPy) ===== def analyze_corn_stage(image_bytes): from PIL import Image import numpy as np img = Image.open(io.BytesIO(image_bytes))\ .convert('RGB').resize((400, 400)) arr = np.array(img, dtype=np.float32) r, g, b = arr[:,:,0], arr[:,:,1], arr[:,:,2] # ExG 植被指数 (Woebbecke 1995) exg = np.clip(2.0*g - r - b, -255, 255) exg_mean = float(np.mean(exg)) # GLI 绿叶指数 (Gitelson 2002) denominator = 2.0*g + r + b + 1e-6 gli = (2.0*g - r - b) / denominator gli_mean = float(np.mean(gli)) # 植被覆盖度 veg_cover = float(np.mean(exg > 20)) * 100 # HSV 色彩分割 — 绿色/黄色比例 hsv = np.array(Image.fromarray( np.clip(arr, 0, 255).astype(np.uint8) ).convert('HSV'), dtype=np.float32) h, s, v = hsv[:,:,0], hsv[:,:,1]/255.0, hsv[:,:,2]/255.0 green_h_pct = float(np.mean((h>40)&(h<150)&(s>0.15))) * 100 yellow_h_pct = float(np.mean((h>10)&(h<50)&(s>0.08)&(s<0.6))) * 100 soil_pct = float(np.mean((s<0.2)&(v<0.5))) * 100 # 纹理分析 (20×20 局部标准差) patch = 20 tex_map = np.zeros_like(gli) for y in range(0, 400, patch): for x in range(0, 400, patch): yy = slice(y, min(y+patch, 400)) xx = slice(x, min(x+patch, 400)) tex_map[yy, xx] = np.std(exg[yy, xx]) tex_mean = float(np.mean(tex_map)) # 四条件级联阶段判定 yg_ratio = yellow_h_pct / (green_h_pct + 1e-6) if yellow_h_pct > 10 and yg_ratio > 0.28 and green_h_pct < 55: stage, conf = 4, 70 + min(20, int(yg_ratio * 50)) # 腊熟期 elif veg_cover > 40 and yellow_h_pct > 8: stage, conf = 3, 60 + min(30, int(yellow_h_pct * 3)) # 开花期 elif veg_cover < 28 and soil_pct > 22: stage, conf = 1, 60 + min(25, int(soil_pct / 2)) # 三叶期 elif veg_cover > 35 and exg_mean > 15: stage, conf = 2, 60 + min(30, int((veg_cover-35)/2)) # 抽雄期 else: stage, conf = 1, 50 return { "stage": stage, "name": STAGES[stage]["name"], # 三叶期/抽雄期/开花期/腊熟期 "confidence": min(95, conf), "veg_cover": round(veg_cover, 1), "exg_mean": round(exg_mean, 1), "gli_mean": round(gli_mean, 4), "green_h_pct": round(green_h_pct, 1), "yellow_h_pct": round(yellow_h_pct, 1), "soil_pct": round(soil_pct, 1), "texture": round(tex_mean, 1), "method": "ExG+GLI+HSV" }
| 阶段 | 名称 | EC 下限 (μS/cm) | EC 上限 (μS/cm) | 关键微量元素需求 |
|---|---|---|---|---|
| 1 | 🌱 三叶期 | 100 | 300 | 锌 (Zn)、铁 (Fe) — 促进根系发育 |
| 2 | 🌿 抽雄期 | 250 | 500 | 氮 (N)、钾 (K) — 促进茎叶生长 |
| 3 | 🌼 开花期 | 280 | 550 | 硼 (B)、锌 (Zn) — 促进花粉活力 |
| 4 | 🌽 腊熟期 | 250 | 500 | 钾 (K)、磷 (P) — 促进籽粒饱满 |