4. ESP32 GPIO与ADC编程实现多传感器轮询采集

在农业远程监控系统中,土壤湿度、空气温湿度、光照强度、水位等传感器往往分散在不同引脚上,其中既有输出数字高低电平的开关量器件,也有输出连续模拟电压的模拟量器件。ESP32 的 GPIO 与 ADC 正是连接这些传感器的两条基本通道:GPIO 负责数字输入输出与电平判断,ADC 负责把模拟电压转换为 0~4095 的数字量。本节要解决的核心问题是:如何用一套非阻塞轮询程序,按固定节拍依次采集多个传感器,并把原始数据整理成可上传的格式化字符串。

一、先明确两类通道的编程差异

数字输入适合读取土壤湿度开关、浮球液位开关、雨滴开关、按键等器件。以 GPIO 数字输入为例,需要先配置引脚模式与内部上拉/下拉,再读取电平。ESP32 常用 pinMode(pin, INPUT_PULLUP) 配置上拉输入,开关另一端接 GND,则闭合时读到低电平 0,断开时读到高电平 1。这里要特别注意:低电平有效是农业现场常见的接线约定,程序中应统一按此判断,避免同一系统里有的传感器高有效、有的低有效造成混乱。

模拟输入则通过 ADC 读取。ESP32 的 ADC1 通道对应 GPIO32~GPIO39,ADC2 在启用 Wi-Fi 时存在资源冲突,因此多传感器模拟采集应优先使用 ADC1。ADC 分辨率为 12 位,读数范围 0~4095,参考电压约 3.3 V,可近似按 电压 ≈ 读数 × 3.3 / 4095 换算。需要注意 ADC 存在非线性,靠近 0 V 和 3.3 V 两端误差较大,农业传感器通常输出 0.5~3.0 V,正好落在相对线性区间内。

二、非阻塞轮询的整体结构

很多初学者会用 delay(1000) 实现“每秒采集一次”,但 delay 期间 CPU 被完全占用,无法响应上传、显示或按键。非阻塞轮询改用 millis() 记录时间戳,每次 loop 只判断“是否到达采集时刻”,到达才执行一次采集,否则立即返回。这样主循环可以同时处理网络、串口等其他任务。

多传感器轮询有两种常见组织方式:一是同一时刻采集全部传感器,适合数据需要严格同步的场景;二是分时轮询,每个节拍只采一个传感器,轮流进行,适合传感器数量多、单个转换时间较长的场景。农业监控对同步性要求不高,分时轮询更节省时间,也便于给每个传感器设置不同的采样周期。下图展示了一个典型的分时轮询数据流:定时器节拍驱动通道选择,各通道采集后进入格式化缓冲区,最终输出一行 CSV 数据。

millis() 节拍 每 200 ms 触发 通道轮询指针 ch = (ch+1) % 4 非阻塞判断 now - last ≥ 周期? GPIO 数字输入 土壤开关 / 液位开关 ADC1 模拟采样 GPIO34 / GPIO35 ADC 电压换算 读数 × 3.3 / 4095 格式化缓冲区 snprintf 拼接 时间戳,通道,数值 单位,状态 串口 / 上传 CSV 一行 MQTT 主题 本地缓存 每个节拍只处理一个通道,主循环不阻塞,网络与显示任务可并行执行

三、关键代码实现

下面给出一个可直接参考的骨架。假设系统有 4 个通道:通道 0 为土壤湿度开关(GPIO25,上拉输入,低电平表示湿润),通道 1 为液位开关(GPIO26,上拉输入),通道 2 为土壤湿度模拟量(GPIO34,ADC1),通道 3 为光照模拟量(GPIO35,ADC1)。每个通道采样周期设为 1000 ms,轮询节拍 200 ms,即每 5 个节拍完成一轮。

const uint8_t CH_NUM = 4;
const uint32_t SAMPLE_MS = 1000;
const uint32_t TICK_MS   = 200;

uint32_t lastTick = 0;
uint32_t lastSample[CH_NUM] = {0};
uint8_t  ch = 0;

void setup() {
  Serial.begin(115200);
  pinMode(25, INPUT_PULLUP);   // 土壤开关
  pinMode(26, INPUT_PULLUP);   // 液位开关
  analogReadResolution(12);    // 12 位
  analogSetAttenuation(ADC_11db); // 量程约 0~3.3V
}

void loop() {
  uint32_t now = millis();
  if (now - lastTick < TICK_MS) return;  // 未到节拍,立即返回
  lastTick = now;

  if (now - lastSample[ch] >= SAMPLE_MS) {
    lastSample[ch] = now;
    sampleChannel(ch);
  }
  ch = (ch + 1) % CH_NUM;   // 指针轮转
}

其中 sampleChannel() 按通道分支处理,数字通道直接 digitalRead,模拟通道先 analogRead 再换算电压。格式化时用 snprintf 把时间戳、通道号、数值、单位拼成一行 CSV,例如 12345,2,1.87,V。这样上位机或 MQTT 订阅端可以直接按逗号切分解析,不需要额外协议。

四、采样质量与工程细节

模拟量采集最容易忽视的是多次采样取平均。ESP32 ADC 单次读数存在数毫伏抖动,对土壤湿度这类缓慢变化的量,连续读 8 次求平均即可明显平滑。数字量则要注意消抖:开关在临界状态可能快速跳变,可在连续两次轮询中读到相同值才确认状态变化,或对状态变化加 50 ms 的确认窗口。

另一个细节是采样周期与传感器响应时间匹配。土壤湿度电阻式传感器插入后需要几百毫秒稳定,若周期设得过短,读数会受前一次激励影响;光照传感器响应快,可以设更短周期。分时轮询的好处正在于此:每个通道可以独立设置 SAMPLE_MS,不必强行统一。

调试阶段建议先把格式化结果同时输出到串口,用串口监视器观察每个通道的数值范围是否合理。例如土壤模拟量在空气中读数接近 4095,插入水中明显下降;若某通道始终为 0 或 4095,应优先检查接线、上拉配置和 ADC 通道是否选错。
ESP32 的 ADC2(GPIO0、2、4、12~15、25~27)在 Wi-Fi 工作时不可用。若系统需要联网上传,模拟传感器务必接到 ADC1 引脚(GPIO32~39),否则会出现读数失败或返回固定值。

五、结果判读与验证

程序跑起来后,判断是否达成目标可以看三点:第一,串口是否按约 1 秒的节奏稳定输出,且四个通道轮流出现,说明轮询指针与节拍逻辑正确;第二,数字通道在人为触发开关时状态是否随之翻转,模拟通道在改变光照或湿度时数值是否单调变化;第三,主循环中若加入一个每 100 ms 翻转的 LED 或心跳打印,应不受采集影响,说明程序确实是非阻塞的。满足这三点,多传感器轮询采集的基本框架就已经可用,后续只需在此基础上接入网络上传与阈值告警。