很多嵌入式开发者在入门时都会被同一个问题拦住:想调一个 I2C 通信、看 PWM 占空比或者确认电源纹波是否异常,手边却没有示波器。买一台入门级台式示波器通常要大几千,于是 “用 ESP32-C3 自己做一个示波器” 就成了一个看起来很诱人的方案。不过先给结论:ESP32-C3 示波器的价值,不在于替代台式示波器,而在于它把采样、触发、幅值校准、信号调理这些示波器底层概念全部摊开,让你用一块几十块钱的开发板亲手跑通一条完整测量链路。
这篇文章会围绕 ESP32-C3 示波器方案讲清楚几件事:ESP32-C3 内置 ADC 到底能测什么、不能测什么;信号进入 ADC 之前为什么要做调理和保护;固件端如何采集数据,上位机又如何实时画波形;以及当你看到波形不准、上升沿明显变大时,问题出在哪一步。读完以后,你可以判断这个方案适不适合自己手头的项目,也能照着代码搭出一个能用的低频虚拟示波器。
需要说明的是,这篇文章不打算把 ESP32-C3 示波器包装成“平替泰克”的方案。真正调试高速数字信号、测量电源纹波或者做严格的上升时间分析时,台式示波器和隔离探头仍然是不可替代的工具。但如果目标是理解信号数字化流程、验证低速协议时序、观察传感器输出,ESP32-C3 是一个成本很低并且足够灵活的起点。
1. ESP32-C3 做示波器,先想清楚边界
网上关于“ESP32 示波器”的搜索结果很多,但大多数只展示效果,没有说明方案的真实边界。很多人把 ESP32-C3 的 ADC 引脚直接接到被测信号上,发现波形抖动、读数不准、甚至芯片发烫,就开始怀疑是代码问题。实际上,问题通常出在方案定位不清晰。
ESP32-C3 是一颗 RISC-V 内核的单片机,集成 2.4G Wi-Fi 和 BLE,内置一个 12 位 SAR ADC。这意味着它可以完成“模拟信号采样 + 数字信号处理 + 无线/串口传输”的完整链路。单片机和 Arduino 环境配合,几分钟就能让 ADC 读数通过串口输出。但“能采样”不等于“能当示波器用”,因为示波器是一个完整的模拟前端、采集、触发、显示系统,ESP32-C3 只覆盖了采集和传输这一小段。
如果只看芯片手册里“12-bit SAR ADC”这个参数,很容易误以为垂直分辨率很高。实际上,ADC 的分辨率只是把输入电压分成 4096 级,真正决定测量质量的是有效位数、采样率、输入带宽、噪声和前端阻抗。ESP32-C3 的 ADC 更适合观察缓慢变化的模拟信号,例如电池电压、光敏电阻输出、低频传感器波形,而不是捕捉几十纳秒的上升沿。
1.1 适用场景和不适用场景
从实际工程经验看,ESP32-C3 示波器方案比较适合以下几类任务:
- 观察 kHz 级别的音频信号、PWM 波形、传感器输出。
- 学习 ADC 采样、触发、串口传输和上位机绘图。
- 验证 I2C、UART 这类低速协议的电平变化是否存在明显异常。
- 做低成本教学实验,让更多人理解“示波器内部发生了什么”。
相对地,以下场景不建议使用这个方案:
- 测量几十 MHz 的时钟信号、高速总线。
- 精确测量电源纹波,尤其是 mV 级别的高频噪声。
- 测量上升沿只有几十纳秒的数字信号。
- 任何超过 3.3V 或者没有安全隔离的电路。
这里有一个容易被忽略的点:示波器探头输入阻抗通常为 1MΩ,同时并联十几 pF 电容。ESP32-C3 开发板的 ADC 引脚虽然也是高阻抗输入,但没有经过精心设计的探头补偿网络,直接探测高频信号时,引脚的寄生电容和走线电感会改变被测电路的工作状态。换句话说,你看到的不一定是真实信号,可能是开发板自身负载造成的影响。
1.2 如何判断你是否需要这个方案
如果在做电池供电设备、传感器数据采集、电机驱动波形观察、低速通信协议学习,ESP32-C3 示波器是一个高性价比的工具。如果你需要测量电源模块的开关纹波、DDR 时序、射频信号,那应该继续使用正规台式示波器。这个方案最大的价值是学习和原理验证,而不是替代实验室设备。
2. 示波器的关键指标,和 ESP32-C3 差在哪
很多人在做 ESP32-C3 示波器时,只关注代码能不能把波形画出来,忽略了示波器的核心参数。理解这几个参数,才知道为什么有些波形明明存在,却显示不出来。
2.1 采样率决定“能不能看到信号”
采样定理告诉我们,要还原一个信号,采样率至少要高于信号最高频率的两倍。实际工程中,为了保证波形幅值可信,采样率通常要是信号频率的 5 到 10 倍以上。ESP32-C3 的内置 ADC 采样能力受限于 ADC 控制器、时钟配置和固件读取方式。在 Arduino 环境下用analogRead()加串口打印,实际有效采样率可能只有几千到几万次每秒,连 10kHz 的方波都很难还原。想要看更高频信号,必须改用 ESP-IDF 的连续采样模式,并且配合合理的缓冲区和传输协议。
2.2 带宽和上升时间的关系
示波器的带宽决定了它能多准确地显示信号的快速变化。如果一个信号的上升沿是 20ns,测它的示波器带宽至少需要几百 MHz。ESP32-C3 的模拟输入链路本身没有明确带宽指标,但受 ADC 采样率和前端 RC 电路限制,实际可用带宽很低。这也是为什么网上经常出现“示波器测上升时间比平时大了一倍”的问题——测量系统本身的带宽不足,会把快沿拉成慢沿。用 ESP32-C3 DIY 方案测一个高速信号时,呈现出来的上升时间可能被放大几十倍,这并不代表芯片坏了,而是采集链路跟不上信号变化。
2.3 垂直分辨率不等于精度
12 位 ADC 意味着理论上可以把 0~3.3V 分成 4096 级,每级大约 0.8mV。但实际测量中,ADC 的差分非线性、偏移误差、电源噪声和参考电压波动都会影响精度。许多 ESP32 系列芯片的 ADC 有效位数低于标称分辨率,实际可能只有 9~10 位有效信息。如果不对 ADC 做校准,直接用analogRead()原始值换算电压,误差可能在几十 mV 甚至更大。这就是为什么正规示波器需要校准,为什么自制的 ESP32-C3 示波器也要做两点校准。
理解以上三个指标后,再回头看第 1 章的适用场景就非常清楚:ESP32-C3 示波器是一个低频、低精度、用于观察波形形状和相对变化的工具。只有接受这个定位,后续的信号调理和软件设计才不会跑偏。
3. 信号链路设计:从探头接口到 ADC 引脚
常规示波器前端包含衰减器、交流/直流耦合、阻抗变换和驱动放大电路。ESP32-C3 示波器即使简化,也不能省略最基本的安全保护和阻抗匹配。下面这部分是整个方案最容易被忽略、但最影响可靠性的环节。
3.1 为什么不能把信号直接接到 ADC 引脚
ESP32-C3 的 ADC 输入引脚耐压范围有限。开发板上的 3.3V 供电系统,如果直接测量一个 5V TTL 信号,超过供电轨的电压可能通过引脚内部的 ESD 二极管泄漏到电源轨,轻则读数异常,重则损坏芯片。另外,未经处理的信号源可能有较大的输出阻抗,与 ADC 引脚内部采样电容相互作用,导致采样值不稳定。
一个稳妥的做法是在 ADC 引脚之前加入限流电阻和钳位保护。最简单的电路是在信号源和 ADC 引脚之间串联一个 1kΩ~10kΩ 电阻,同时在引脚对地并联一个 3.3V 稳压二极管或者使用两个二极管做钳位。这样即使外部瞬态电压略高于 3.3V,也能把电流限制在安全范围。需要注意,串联电阻会与 ADC 内部采样电容构成 RC 低通滤波,电阻过大会降低高频响应,所以需要在保护和带宽之间做平衡。
3.2 一个最小可用的模拟前端
对于电池供电、传感器输出这类低压信号,一个实用的 ESP32-C3 示波器前端可以这样设计:
- 输入端串联 1kΩ 限流电阻。
- 在 ADC 引脚对地并联一个 3.3V TVS 管或肖特基钳位二极管,防止过压。
- 如果信号源输出阻抗较高,增加一级单位增益运放缓冲器。运放建议选择单电源、轨到轨输出类型,电源由开发板的 3.3V 提供。
- 如果被测信号最高超过 3.3V,先用电阻分压网络将电压降到 ADC 量程内,分压比例要计入软件换算。
不要直接测量市电或者任何高压电路。ESP32-C3 开发板没有隔离电源,USB 地线可能和被检系统共地,一旦接错会带来安全风险。
3.3 软件校准:比追求高精度硬件更实际
即使加了前端电路,ADC 的绝对精度仍然有限。更可行的做法是在软件里做两点校准。先用一个已知电压源输入 0V,记录 ADC 读数;再输入一个准确电压,例如 3.000V,记录 ADC 读数。然后用两点公式计算每个 ADC 原始值对应的电压。常见形式是:
float adc_to_voltage(int raw) { // offset 和 scale 通过两点校准得到 return (raw - offset) * scale; }这样可以在不更换硬件的情况下把测量误差控制在一个可以接受的范围。每次更换开发板或供电方式后,建议重新校准一次。
4. 固件采集:用 Arduino 先跑通最小数据通路
在深入 ESP-IDF 连续采样之前,先用 Arduino 环境跑通一条最简单的采集链路,能帮你快速验证硬件连接、串口通信和上位机绘图是否正常。下面的代码基于 Arduino-ESP32 core,适用于大多数 ESP32-C3 开发板。
4.1 环境准备
你需要准备以下内容:
- 一块 ESP32-C3 开发板,确认板载串口芯片或 USB 转串口电路正常。
- Arduino IDE 或 Arduino CLI,安装 esp32 by Espressif Systems 开发板支持包。
- 开发板选择:ESP32C3 Dev Module。
- 一根数据线,最好支持数据传输而不是只充电。
安装完成后,在 Arduino IDE 中选择对应串口,烧录下面的示例。
4.2 最小 ADC 采样和串口输出示例
这个程序的逻辑很简单:初始化 12 位 ADC,将输入引脚衰减设置为 11dB,使可测量范围接近 0~3.1V,然后在主循环中不断读取 ADC 原始值,通过串口发送出去。
// 文件路径:esp32c3_oscillo_sampler.ino const int adcPin = 0; // 不同开发板丝印不同,请对照原理图选择 ADC 引脚 void setup() { Serial.begin(921600); // 使用较高波特率,减少传输瓶颈 analogReadResolution(12); // 配置为 12 位分辨率 analogSetPinAttenuation(adcPin, ADC_11db); // 量程约 0~3.1V } void loop() { int raw = analogRead(adcPin); Serial.println(raw); // 输出原始 ADC 值,方便上位机换算 }这里的核心是输出原始值而不是直接输出电压。因为不同开发板的参考电压和衰减档位可能存在差异,在上位机做换算和校准会更灵活。ADC_11db对应量程约 0~3.1V,适合测量大多数 3.3V 系统的信号。如果你的被测信号始终在 0~2.2V 以内,可以改用更低的衰减档位,牺牲量程换取更好的小信号分辨率。
需要注意,analogRead()加Serial.println()的方式效率并不高。每打印一行都会占用串口时间,实际采样率很可能不超过几 kS/s,但这对于验证数据链路已经足够。如果运行后串口没有任何输出,先检查串口波特率是否匹配,再检查引脚编号是否选错。
4.3 用串口绘图软件快速看波形
如果你的代码运行正常,在 Arduino IDE 的串口监视器中应该能看到连续滚动的数字。这时可以打开 Arduino IDE 自带的串口绘图器,或者使用 SerialPlot、VOFA+ 等第三方工具。选择 921600 波特率后,软件会把每一行数字当成一个数据点显示出来。
给 ADC 引脚输入一个低频正弦波信号,就能看到一条类似正弦波的曲线。如果只看到一条水平直线,可能是输入信号超出了量程,或者信号源没有与开发板共地。许多初学者在这里会犯一个错误:信号发生器输出和开发板没有连接参考地,导致 ADC 看到的电压悬浮,波形自然不正常。
5. 上位机实时显示:从串口数据到动态波形
串口绘图工具虽然方便,但功能有限。如果你想加入触发、保存数据和自定义显示界面,用 Python 写一个简单的上位机是更灵活的选择。下面是一个基于 pyserial 和 matplotlib 的实时绘图示例。它读取串口数据,每一行解析成一个整数,然后维护一个固定长度的采样窗口并实时刷新图形。
# 文件路径:esp32c3_oscillo_viewer.py import sys import serial import matplotlib.pyplot as plt from collections import deque PORT = "COM3" # Linux 下通常是 /dev/ttyACM0 或 /dev/ttyUSB0 BAUD = 921600 WINDOW = 500 # 显示最近 500 个样本 ser = serial.Serial(PORT, BAUD, timeout=0.1) samples = deque(maxlen=WINDOW) fig, ax = plt.subplots() line, = ax.plot([], [], lw=1) ax.set_xlim(0, WINDOW) ax.set_ylim(0, 4096) def update(frame): while ser.in_waiting: raw_line = ser.readline().decode("utf-8", errors="ignore").strip() try: val = int(raw_line) samples.append(val) except ValueError: pass data = list(samples) line.set_data(range(len(data)), data) return line, ani = matplotlib.animation.FuncAnimation(fig, update, interval=30) plt.show()运行前需要安装依赖:
pip install pyserial matplotlib将代码中的PORT改成你的实际串口号。运行后,如果数据链路正常,图表会持续刷新。要注意的是,这个脚本没有做严格的采样率控制,显示的时间轴是“样本序号”而不是真实时间。想要换算成真实时间,需要在固件中记录采样周期,或者在 Python 中根据预期的采样率重新映射横轴。
5.1 软件触发:让波形稳定下来
没有触发功能的示波器,在观察重复信号时会感觉波形一直在水平漂移。解决方法是加入软件触发。原理很简单:在样本序列中寻找一个从低电平跨过阈值电平的位置,把这个位置作为波形起点。
# trigger_demo.py —— 软件上升沿触发思路 def find_rising_edge(data, threshold=1500): for i in range(1, len(data)): if data[i - 1] < threshold <= data[i]: return i return -1 samples = [...] # 固件一次采集到的原始 ADC 样本 idx = find_rising_edge(samples) if idx >= 0: # 以上升沿之前 100 个样本作为起点,截取一段波形 start = max(0, idx - 100) segment = samples[start:start + 512] print("触发位置:", idx, "截取样本数:", len(segment)) else: print("未在缓冲区中找到上升沿")实际工程中,下降沿触发、脉冲触发和迟滞比较都能用类似思路扩展。需要理解的是,触发并不改变 ADC 数据本身,而是决定从哪个时刻开始“稳定地展示”波形。对于 ESP32-C3 这种低频设备,软件触发是最可行的方式。
6. 用已知信号验证你的“示波器”是否准确
完成最小采集和上位机显示之后,不要急于去测量复杂电路。先用一个已知信号验证测量链路的准确性,这是避免后续误判的关键。推荐用 ESP32-C3 自己的 PWM 输出作为测试信号,因为信号源与采集系统共地,不需要额外信号发生器。
6.1 用 LEDC 输出 PWM 测试波形
// 文件路径:esp32c3_pwm_test_signal.ino const int pwmPin = 4; // 选择一个不会被 ADC 占用的引脚 void setup() { ledcSetup(0, 5000, 8); // 5kHz PWM,8 位分辨率 ledcAttachPin(pwmPin, 0); ledcWrite(0, 127); // 约 50% 占空比 } void loop() { }把 PWM 输出引脚,经过一个 1kΩ 串联电阻后连接到 ADC 输入引脚。注意两个引脚必须共地。此时应当能看到一个频率为 5kHz 的方波。如果你的采集系统实际采样率不够,看到的不再是干净方波,而是轮廓甚至混叠现象。这是学习采样定理的直观素材:当采样率低于信号频率的 2 倍时,显示的频率会完全错误。
如果发现占空比测量结果与 50% 偏差很大,排查方向是采样点的均匀性。loop()中的串口打印不是精确等间隔的,这会导致某些样本停留时间更长,从而改变统计出的占空比。这也是为什么真正的高性能采集通常要使用定时器或者 DMA,而不是在loop()中轮询读取。
6.2 直流电压校准
用一节 1.5V 干电池或一个稳定的可调电源,经过已知比例的分压电阻把电压降到 ADC 量程内,然后测量记录值。与万用表读数对比,如果误差较大,说明需要校准。校准后的换算参数可以写在前面的adc_to_voltage函数中。
校准操作属于低电压安全操作,但如果使用可调电源,务必先调低电压再接入开发板,避免误操作导致过压损坏。
6.3 如何判断方案是否满足项目需要
观察 PWM 波形时,如果频率、占空比趋势能和程序设定对得上,说明 ESP32-C3 示波器足以应对这类低频逻辑检查。如果测量结果连方波的基本形状都看不出来,就不要勉强去测更高频的信号。把方案用在合适的地方,才能发挥最大价值。
7. 想跑更高采样率:从 Arduino 到 ESP-IDF 连续采样
Arduino 示例的价值在于快速验证链路,但性能上限明显。Serial.println()阻塞式输出、每次采样都要进入函数调用,都无法发挥 ESP32-C3 的极限。如果你想把 ESP32-C3 示波器做成一个真正能用的工具,需要切换到 ESP-IDF,使用 ADC 连续采样模式和 DMA 缓冲区。
7.1 为什么 Arduino 串口打印会成为瓶颈
在 Arduino 主循环中执行Serial.println()时,CPU 需要等待串口外设把数据发送完,这个过程中无法同时做下一次 ADC 采样。即便是缓冲式输出,高频打印也会让循环周期不稳定。解决的思路是让 ADC 连续采样并写入 DMA 缓冲区,CPU 在后台批量搬移数据,而不是每次转换都打断 CPU。
7.2 ESP-IDF 连续采样模式简介
在 ESP-IDF 中,ADC 连续采样通常使用adc_continuous驱动。你需要先配置 ADC 通道、采样周期和 DMA 缓冲区,然后启动连续转换,再周期性读取缓冲区数据。下面是一个流程示意:
// 伪代码/示意:实际 API 请以当前 ESP-IDF 版本的官方示例为准 #include "esp_adc/adc_continuous.h" adc_continuous_handle_t adc_handle; adc_continuous_handle_cfg_t handle_cfg = { .max_store_buf_size = 4096, .conv_frame_size = 512, }; adc_continuous_new_handle(&handle_cfg, &adc_handle); // 配置 ADC 通道和模式后启动 adc_continuous_start(adc_handle); for (;;) { uint8_t result[512]; uint32_t bytes_read = 0; adc_continuous_read(adc_handle, result, sizeof(result), &bytes_read, 1000); // 在这里解析 result 中的 ADC 原始数据,并通过 USB CDC 批量发送 }真实项目中还需要加入结果解析、校准和传输帧协议。因为不同版本的 ESP-IDF API 有差异,最稳妥的方法不是记忆接口名,而是打开官方例程,找到adc_continuous相关示例,在此基础上修改。重点理解两件事:第一,采样由硬件驱动,而不是loop()轮询控制;第二,数据从 DMA 缓冲区搬到上位机时,需要成帧发送,避免每个样本都占用一个串口包。
7.3 硬件层面还能怎么提升
如果连续采样依然不满足需求,可以考虑外置 ADC。ESP32-C3 的 SPI 接口可以外接更高采样率的 ADC 芯片,但成本和复杂度会上升。对于多数学习场景,先把 ESP32-C3 内置 ADC 的行为吃透,比盲目堆硬件更能帮助你理解示波器原理。类似的入门项目还可以扩展到 ESP32-S3、树莓派 Pico 等平台,但不同芯片的 ADC 特性和驱动差异很大,切换平台后需要重新验证。
8. ESP32-C3 示波器常见问题与排查
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| ADC 读数始终为 4095 或 0 | 输入电压超出量程,或引脚悬空 | 用万用表测量 ADC 引脚电压 | 调整分压比例,确保输入在量程内 |
| 读数波动很大 | 信号源共地不良,或引脚未加滤波 | 检查信号源地线是否连接到开发板 GND | 确保共地;必要时在 ADC 引脚并联小电容 |
| 串口输出乱码 | 波特率不匹配,或串口工具设置错误 | 检查 Serial.begin 与上位机波特率 | 统一为 921600 或 115200 |
| 上位机无数据 | 串口号错误或开发板未识别 | 查看设备管理器/系统设备列表 | 更换 USB 口,重新安装驱动 |
| 波形水平漂移 | 没有触发功能 | 在数据中寻找上升沿/下降沿作为起点 | 加入软件触发逻辑 |
| 方波显示为三角波 | 采样率不足或信号带宽太高 | 降低被测信号频率,或用更高采样率模式 | 使用 ESP-IDF 连续采样或只测低频信号 |
| 测量上升时间比实际大很多 | 采集链路带宽不足 | 检查前端 RC 滤波和采样率 | 接受系统限制,不要用于高速信号测量 |
| 电压读数与万用表不一致 | ADC 未校准或衰减配置不对 | 用已知电压做两点校准 | 在软件中加增益和偏移校准 |
每个问题背后都对应一个具体的测量链路环节。建议把排查顺序固定为:先确认输入电压在安全量程内,再确认共地,然后用直流电压校准,最后再进行波形观察。不要跳过校准直接测高频信号,否则很难判断误差来自 ADC、前端电路还是传输协议。
9. 最佳实践与工程建议
把 ESP32-C3 示波器当作工程工具来用,而不是一次性的实验代码,需要在硬件、软件、测试流程三个层面养成规范。
9.1 硬件层面的建议
输入保护是长期可靠性的底线。无论被测信号是电池还是开发板上的 3.3V 逻辑,尽量在 ADC 引脚前保留串联电阻。只测量低压、隔离良好的信号,不要试图测量市电或者未隔离的开关电源。如果项目需要测多个通道,可以规划一个可插拔的前端小板,把衰减、限流、钳位电路固定下来,而不是每次都在面包板上飞线。
电源去耦同样重要。ESP32-C3 开启 Wi-Fi 时电流波动较大,可能影响 ADC 参考电压。为了获得更稳定的测量结果,尽量使用稳压电源供电,并在开发板电源引脚附近增加电容。如果必须使用 USB 供电,注意电脑 USB 口输出电压会在负载变化时轻微波动,这种波动会直接叠加到 ADC 测量中。
9.2 软件层面的建议
固件与上位机之间的数据协议尽量简单清晰。入门阶段可以只输出原始 ADC 值,但进入正式项目时,建议定义一帧包含通道号、样本序号、ADC 原始值和校验字段。这样上位机可以检测丢包和乱序,避免把坏数据显示成波形。
采样数据最好批量传输。每行一个样本在低频时没问题,但采样率提高后会产生大量串口中断。更合理的做法是在固件中填充缓冲区,攒够一帧再发送。Python 端按帧解析,而不是逐行解析,这样 CPU 占用更低,波形也更平滑。
9.3 测试流程的建议
给 ESP32-C3 示波器做“自检”是一个非常实用的习惯。保留一个 PWM 测试程序,每次改动硬件或软件后,先输出一个 1kHz 方波验证波形是否正常。自检通过后再去测实际项目信号,可以极大减少误判。如果发现测量结果异常,优先检查测试环境,而不是直接怀疑被测设备。
10. 总结与后续学习方向
ESP32-C3 示波器是一个很好的嵌入式学习项目,但它教会你的不是“省钱替代泰克”,而是完整理解示波器的链路:模拟信号的调理与保护、ADC 采样与量化、触发算法、数据传输、上位机可视化以及误差校准。把这些环节走通一遍,比单纯使用一台商用示波器更能帮助你建立信号完整性直觉。
如果你想继续深入,下一步不是马上画 PCB,而是先把三件事做好: 第一,用本文第 6 章的 PWM 信号验证当前系统的采样率上限; 第二,把串口打印的阻塞式采集改成批量缓冲输出,观察采样率是否显著提升; 第三,用外接分压器和已知直流电压完成两点校准,把测量误差降低到可接受范围。
然后可以阅读 ESP-IDF 官方 ADC 连续模式示例,尝试自己在例程基础上增加触发和上位机协议。等到你不再关心“怎么画一条波形”,而是开始思考“这条波形为什么是这样”的时候,再回头看 ESP32-C3 示波器项目,它的目的就已经达成了。