这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 嵌入式开发 » MCU » 「AI编程记录--含源码」用一晚上的时间写一个esp32的示波器

共1条 1/1 1 跳转至

「AI编程记录--含源码」用一晚上的时间写一个esp32的示波器

助工
2026-06-12 23:13:11     打赏


叠甲!叠甲!叠甲! 没啥厉害的,就是没事儿干叫AI写个示波器demo,开源的代码在这里。AI的执行计划在docs目录里。

lvgl和屏幕驱动原先就适配好了,示波器代码叫AI从零开始写的。

https://gitee.com/walker2048/eetree-occurrence/tree/master/oscilloscope-c6


ESP32-C6 LCD 示波器 — 从零实现方案

背景

已有条件:ESP32-C6 + ST7789T LCD (320×172 横屏已配好) + LVGL 已能正常显示。 目标:实现简易示波器,ADC 采样率 50KHz,GPIO0 模拟输入。

数据流

ADC 采样(50KHz) → 触发(稳住波形) → 测量(频率+幅度) → LVGL 显示(波形+数字)

步骤 1:ADC 采样 + 校准

做什么

用 ESP-IDF 连续 ADC 驱动持续采集 GPIO0 的模拟信号,校准后以 mV 单位输出。

具体步骤

  1. 新建 components/oscilloscope/ 组件,包含 include/osc_adc.h 和 osc_adc.c

  2. 配置连续 ADC:ADC1_CH0 (GPIO0),12bit,ADC_ATTEN_DB_12,50KHz

  3. 分配环形缓冲区 8192 采样点(163ms @50KHz,兼顾 10Hz~25kHz)

  4. 初始化时调用 adc_cali_create_scheme_curve_fitting() 创建校准句柄

  5. 提供 osc_adc_snapshot(buf, max) 接口:拷贝缓冲区并逐样本调用 adc_cali_raw_to_voltage() 转为 mV

  6. 在 app_main 中初始化 ADC,循环调用 snapshot,通过 ESP_LOGI 打印前 20 个值

验收

  • [x] 编译烧录,串口持续输出采样值

  • [x] GPIO0 接 GND → 打印值 ≈ 0mV

  • [x] GPIO0 接 3.3V → 打印值 ≈ 2500~3300mV(C6 满量程不是 3.3V)

  • [x] 接 1kHz 正弦波 → 数据有明显周期性高低变化

注意

  • 组件 CMakeLists.txt 需添加 esp_adc 依赖


步骤 2:边沿触发

做什么

在采样数据中找到上升过中点的位置,让波形从固定相位开始,显示不漂移。

具体步骤

  1. 在 oscilloscope 组件中新建 include/osc_trigger.h 和 osc_trigger.c

  2. 实现函数:遍历数据找第一个 data[i] >= mid && data[i-1] < mid 的索引

  3. mid 由调用者传入(通常为 (min + max) / 2)

  4. 找不到时返回 found=false

  5. 在主循环中:触发成功则从触发点取数据,失败则取缓冲区末尾

验收

  • [x] 主循环:snapshot → 触发 → 打印触发位置

  • [x] 接正弦波,串口持续输出触发索引,值应在合理范围内跳动

  • [x] 后续接上波形显示后,波形应稳定不左右滚动


步骤 3:Vpp 测量

做什么

从一段采样数据中算出峰峰值电压。

具体步骤

  1. 在 oscilloscope 组件中新建 include/osc_measure.h 和 osc_measure.c

  2. 实现测量函数:遍历数据找 min_mV 和 max_mV

  3. Vpp = (max_mV - min_mV) / 1000.0,单位为伏特

  4. 在主循环中调用,通过 ESP_LOGI 打印 Vpp

验收

  • [x] 接 1kHz 正弦波(信号发生器设置 1Vpp)→ 打印 Vpp ≈ 1.0V

  • [x] 接 1kHz 方波(信号发生器设置 3Vpp)→ 打印 Vpp ≈ 3.0V

  • [x] 误差 < 10%


步骤 4:频率测量 — 过零点法

做什么

统计上升过中点的次数和间距,计算基波频率。

具体步骤

  1. 在 osc_measure.c 中增加过零点测频函数

  2. 找信号 min/max,计算中点 mid

  3. 收集最多 64 个上升过零点位置(data[i] >= mid && data[i-1] < mid)

  4. 计算相邻过零点间距的平均值 → 平均周期

  5. 频率 = 采样率 / 平均周期

  6. 不足 2 个过零点时返回 0(无效)

  7. 在主循环中调用,ESP_LOGI 打印频率

验收

  • [x] 1kHz 正弦波 → 打印 ≈ 1000Hz,误差 < 5%

  • [x] 100Hz 正弦波 → 打印 ≈ 100Hz

  • [x] 10kHz 正弦波 → 打印 ≈ 10000Hz

  • [x] 535Hz 方波 90%占空比 → 打印 ≈ 535Hz(不会误判为 1070Hz 二次谐波)

注意

  • < 200Hz 时频率读数会跳变(163ms 缓冲区内周期数太少),这是正常的,后续可用多帧平滑改善


步骤 5:频率测量 — FFT(可选 fallback)

做什么

为过零点失败的场景提供备选频率检测。此步骤可选,过零点法已覆盖绝大多数场景。

具体步骤

  1. 在 oscilloscope 组件中新建 include/osc_fft.h 和 osc_fft.c

  2. 实现 512 点 Cooley-Tukey FFT:bit_reverse + 蝶形运算

  3. 实现 magnitude 计算:sqrt(re² + im²)

  4. 在 measure 中:对数据加 Hann 窗后送入 FFT

  5. 搜索峰值时跳过 bin 0-3(DC 泄漏区)

  6. 抛物线插值提高频率精度

  7. 测量策略:过零点优先,FFT 仅在过零点返回 0 时使用

验收

  • [x] 过零点失败时(如噪声信号),FFT 仍能给出近似频率

  • [x] 1kHz 正弦波 FFT 结果 ≈ 1000Hz

注意

  • fft_len 必须是 2 的幂,否则 bit_reverse 死循环。用 while (n*2 <= limit) n*=2 计算

  • ESP32-C6 无 FPU,2048 点 FFT 会触发看门狗,限制在 512 点

  • 方波的二次谐波比基波还强时 FFT 会误判,所以过零点必须是主方案


步骤 6:波形显示 — 基础版

做什么

在 LVGL 上画出波形,先实现最简单的版本:直接显示采样数据,不做周期缩放。

具体步骤

  1. 在 oscilloscope 组件中新建 include/osc_ui.h 和 osc_ui.c

  2. 初始化:设置屏幕黑色背景,创建 lv_line 对象

  3. 分配 lv_point_t[320] 静态数组

  4. 更新函数:将 320 个采样点映射到屏幕坐标

    • X = 采样索引 (0~319)

    • Y = 翻转映射:y = disp_h - 1 - (mV × disp_h / 3300),高电压在顶部

  5. 底部预留 16px 不画波形(留给文字)

  6. 主循环中调用 osc_ui_update() + lv_timer_handler(),间隔 50ms

验收

  • [x] 接 1kHz 正弦波 → 屏幕上能看到绿色波形

  • [x] 波形幅度随信号发生器电压变化而变化

  • [x] 接 5kHz 信号 → 波形变密(周期多),但仍能看到


步骤 7:固定 3 周期显示

做什么

根据测量的周期,自动缩放波形,始终显示 3 个完整周期。

具体步骤

  1. 在 osc_ui.h 的 info 结构体中增加 period_samples 字段

  2. 在 osc_measure.c 中计算:period_samples = sample_rate / frequency + 0.5

  3. UI 更新函数中:

    • 计算要显示的采样数:src_len = period_samples × 3

    • 如果 src_len > wave_len,则 src_len = wave_len(数据不够就全显示)

  4. 用线性插值将 src_len 个采样重采样到 320 像素:

    • 对每个像素 i,计算对应的采样索引 frac = i × (src_len-1) / 319

    • 取相邻两个采样点线性插值

  5. 将插值结果映射到 Y 坐标

验收

  • [x] 1kHz 正弦波 → 屏幕上恰好 3 个完整周期

  • [x] 切换到 5kHz → 仍然 3 个周期

  • [x] 切换到 100Hz → 仍然 3 个周期

  • [x] 10kHz 信号 → 仍然 3 个周期

注意

  • period_samples × 3 可能小于 320(高频时),这是正常的,插值会拉伸,不需要加 >= 320 的限制


步骤 8:参考线 + 信息文字

做什么

叠加 1.65V 参考线和频率/Vpp 数字显示。

具体步骤

  1. 创建第二条 lv_line 作为参考线,水平画在 y = disp_h/2 位置(1.65V),颜色暗青色

  2. 创建 lv_label 底部居中对齐,绿色文字,字体 lv_font_montserrat_14

  3. 格式:"%.0fHz    %.2fVpp"(频率和 Vpp 之间 4 个空格)

  4. 频率 >= 1000 时用 "%.2fkHz" 格式

  5. 无效频率时显示 "---"

验收

  • [x] 屏幕上有一条暗色水平参考线

  • [x] 底部居中显示绿色文字,如 1.00kHz    1.50Vpp

  • [x] 频率和幅度随信号变化实时更新


步骤 9:整合主循环

做什么

将所有模块串联到 main/main.c 的主循环中。

具体步骤

  1. app_main 中依次初始化:LCD → LVGL → ADC → UI

  2. 延迟 100ms 等待 ADC 缓冲区填满

  3. 主循环:

    • 调用 osc_adc_snapshot() 获取一帧 mV 数据

    • 调用 osc_trigger_rising() 找触发点

    • 从触发点取后续数据,调用 osc_measure() 测量频率和 Vpp

    • 调用 osc_ui_update() 更新波形和文字

    • 调用 lv_timer_handler() 刷新显示

    • vTaskDelay(50ms) 间隔

验收

  • [x] 接信号发生器,波形稳定显示 3 个周期

  • [x] 频率和 Vpp 数字合理

  • [x] 切换波形类型(正弦/方/三角)和频率,1 秒内稳定

  • [x] 长时间运行不死机(看门狗不触发)


踩坑记录

坑说明
ADC 校准C6 的 ADC_ATTEN_DB_12 满量程约 2.5V 不是 3.3V,不校准 3V 信号只显示 2.4V
FFT 长度fft_len 必须是 2 的幂,否则 bit_reverse 死循环
LVGL 旋转不要用 disp_drv.rotated,在 ST7789T 驱动层直接硬件旋转
横屏偏移物理屏 column 从 34 开始,横屏后偏移从 X 轴变到 Y 轴,用 set_gap(0, 34)
FFT 看门狗C6 无 FPU,2048 点就触发 task_wdt,限制 512 点
方波谐波FFT 会把二次谐波当主峰,必须用过零点法直接测基波周期
重采样条件3 周期的 wanted 值可能 < 320(高频时),不应加 >= SCREEN_W 限制
Y 轴满量程数据是 mV,满量程 3300 不是 4095
符号冲突ESP-IDF 的 esp_adc 组件已内置 adc_dma_init 等函数名,自定义驱动需加前缀(如 osc_adc_dma_*)避免链接冲突



共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]