【树莓派 002】 RP2040 实现示波器 PIO来驱动 ADC10080 并抓取数据方案+ 内置12-bitADC&DMA&网页前端可视化方案

文章目录

纯python版本

RP2040 内置 12-bit ADC

  • RP2040 内置 12-bit ADC,有 4 个通道(GPIO26~29),还有温度传感器。

  • 采样可以 单次触发或 连续 DMA 采样。

  • 内部有 FIFO,ADC 将采样结果放入 FIFO,CPU 或 DMA 读取。

ADC处理

  • MicroPython 本身对 ADC 提供了简单接口:

    python 复制代码
    import machine
    adc = machine.ADC(26)
    val = adc.read_u16()
    • 这是轮询单次采样,没有硬件触发和 FIFO 支持
    • 采样速度有限:Python 层每次调用都要进入解释器 → CPU 周期浪费
python 复制代码
# simple_adc_test.py
from machine import ADC, Pin
import time

# ADC 输入引脚定义
# RP2040 的 ADC 引脚:
# ADC 0 -> GPIO 26
# ADC 1 -> GPIO 27
# ADC 2 -> GPIO 28
# ADC 3 -> 温度传感器 (Temperature Sensor)
ADC_PIN = 0  # 对应 ADC 0 (GPIO 26)

# ---------------- 初始化 ----------------
try:
    # 初始化 ADC 对象,传入 ADC 编号
    # 注意:在 MicroPython 中,ADC(0) 对应 GPIO 26
    adc = ADC(ADC_PIN) 
    print(f" ADC {ADC_PIN} (GPIO 26) 初始化成功。")
    
except ValueError as e:
    print(f" 错误: 无法初始化 ADC {ADC_PIN}。请检查引脚编号。")
    sys.exit()

# ---------------- 主循环 ----------------
while True:
    # 1. 读取原始值
    # read_u16() 返回一个 0 到 65535 (16位) 的值。
    # RP2040 ADC 实际是 12 位的,MicroPython 会将其自动扩展到 16 位。
    raw_value = adc.read_u16()

    # 2. 转换为电压值
    # RP2040 的 ADC 参考电压是 3.3V。
    # 电压 = 原始值 * (参考电压 / 最大原始值)
    # 最大原始值 = 65535 (2^16 - 1)
    voltage = raw_value * (3.3 / 65535)

    # 3. 打印结果
    print(f"--- 采样值 ---")
    print(f"原始值 (16-bit): {raw_value:5d}")
    print(f"电压值 (V):     {voltage:.3f} V")
    
    # 延迟 1 秒
    time.sleep(1)

纯python版本

python 复制代码
# rp2040_adc_for_html_scope.py
from machine import ADC
import sys
import time

# ---------------- config ----------------
ADC_CH = 26             # GPIO26 对应 ADC0
SAMPLE_RATE = 5000      # 5000 Hz
CAPTURE_DEPTH = 1024    # 每帧点数

# ---------------- ADC init ----------------
adc = ADC(ADC_CH)

# ---------------- read samples ----------------
def read_samples(n):
    buf = []
    for _ in range(n):
        val = adc.read_u16()  # 读取 16-bit ADC 值 (0-65535)
        # 可选:把 16-bit 缩放到 8-bit
        buf.append(val >> 8)  # 0-255
    return buf

# ---------------- format message ----------------
def send_frame(samples, msg_id):
    trigger_index = 0
    extra1 = 0
    extra2 = 0

    out = bytearray()
    out += b'/*m'
    out += bytes([48 + msg_id])

    out += bytes(f"{trigger_index:04X}", 'ascii')
    out += bytes(f"{extra1:04X}", 'ascii')
    out += bytes(f"{extra2:04X}", 'ascii')

    for v in samples:
        out += bytes(f"{v:02X}", 'ascii')

    out += b'*/'
    sys.stdout.buffer.write(out)

# ---------------- main ----------------
def main():
    msg_id = 0
    while True:
        samples = read_samples(CAPTURE_DEPTH)
        send_frame(samples, msg_id)
        msg_id = (msg_id + 1) % 10
        time.sleep_ms(5) 

main() # 代码应该还有BUG

前端代码

  • 前端代码来自本文的"C语言版本"部分。
代码结构
复制代码
WebSerial
   ↓
receive() → processMessage()
                ↓
          decodeRxData()
                ↓
          alignADCValues()
                ↓
              drawWave()
             ↙        ↘
   signalFrequency()   measureLinesDistance()
数据格式
复制代码
function processMessage(data){ // general message format: /,*,command,data, ..... ,*,/ // measurement data: /,*,m,msg_id,trigger_index[4hex], hex,...hex,*,/

/*m<id><trigger_index:4hex><extra1:4hex><extra2:4hex><data...>* /
  • sys.stdout.buffer.write(out)在串口输出的二进制数据:

    /m400000000000001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101/

    /m500000000000001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101/

  • 尝试输出数据:

    复制代码
      	console.log(data);
      	console.log(data[3]);
decodeRxData
javascript 复制代码
/**
 * 将 ASCII HEX 数据解码为数值
 */
function decodeRxData(data){
    var values = [];

    // 跳过 /*mX         
    for (var i = 4; i < data.length - 2; i += 2){
        var d1 = data[i]   - 48; if (d1 > 9) d1 -= 7;
        var d2 = data[i+1] - 48; if (d2 > 9) d2 -= 7;
        values.push(d1 * 16 + d2); // 两个 ASCII 十六进制字符,合成为 1 个字节(0--255)
    }

    // 前 6 个字节是控制信息
    var trigger_index = values[0] * 256 + values[1];
    var extra1        = values[2] * 256 + values[3];
    var extra2        = values[4] * 256 + values[5];

    // 去掉控制信息,仅保留 ADC 数据
    values = values.slice(6);

    return { trigger_index, values };
}
字节序号 内容 说明
0 / 起始 , ASCII对应值为47
1 * ASCII对应值为42
2 m measurement
3 '0'..'9' msg_id ,48~57的循环,'0'ASCII对应值48
4--7 trigger_index 4 hex
8--11 extra1 4 hex(保留)
12--15 extra2 4 hex(保留)
16... ADC 数据 每个样本 2 hex(0--255)
n-2 *
n-1 / 结束
javascript 复制代码
/**
 * 处理一整帧 Pico 返回的数据
 * 帧格式: /*m<id><trigger><extra1><extra2><data>*/
 */
function processMessage(data){
    datareceived = true;

    // 消息编号(字符 '0'..'9' → 数字)
    var msg_id = data[3] - 48;

    // 采集耗时显示
    document.getElementById('info4').innerHTML =
        `acquiring time: ${(performance.now() - txtimes[msg_id]).toFixed(0)} ms`;

    // 可选:显示原始 HEX 数据
    if (document.getElementById("showhex").checked) {
        var s = `rxlen=${data.length}<br>`;
        var count = 0;
        for (var i = 0; i < data.length; i += 2) {
            s += String.fromCharCode(data[i], data[i+1]) + ' ';
            if (count % 32 == 31) s += '<br>';
            count++;
        }
        document.getElementById('data').innerHTML = s;
    } else {
        document.getElementById('data').innerHTML = "";
    }

    // 解析数据
    var {trigger_index, values} = decodeRxData(data);

    // 按触发点对齐
    adcSamples = alignADCValues(trigger_index, values);
    triggerIndex = trigger_index;

    drawWave();
}
javascript 复制代码
	function decodeRxData(data){
		// convert hex to array of values
		var values = []
		// skip data 0..3 : /,*,m,0
		for (var i = 4; i < data.length - 2; i+=2){
			var d1 = data[i] - 48   // 0..9, 
			if (d1 > 9) d1 -= 7     // 10..15
			var d2 = data[i+1] - 48 // 0..9, 
			if (d2 > 9) d2 -= 7		// 10..15
			values.push(d1 * 16 + d2)
		}
		var trigger_index = values[0] * 256 + values[1]
		var extra1 = values[2] * 256 + values[3]
		var extra2 = values[4] * 256 + values[5]
		//document.getElementById("debug").innerHTML = `trigger_index=${trigger_index}, extra1=${extra1}, extra2=${extra2}`

		// remove trigger_index + 2 extra items from data
		values = values.slice(6)
		
		return {trigger_index, values};
	}

优化方向

  • MicroPython 本身速度有限:Python 解释器循环开销,ADC 硬件转换时间,Python 调用 read_u16,MicroPython + USB CDC速度大约可能 50--100 kB/s。

  • 没有使用 DMA API

    • Pi Pico ADC input using DMA and MicroPython
    • DMA 需要设置触发源,设置读地址 ,RP2040的直接内存访问(DMA) 控制器,提供在内存块和/或输入输出寄存器之间移动数据的能力。The DMA 控制器拥有独立的读写总线主连接,连接到总线结构, 每个DMA信道可以独立地从一个地址读取数据并写回另一个地址 地址,可选择递增一个或两个指针,使其能够代表执行传输 处理器执行其他任务或进入低功耗状态时。
  • FIFO?
    • MicroPython 封装:标准的 MicroPython 没有直接暴露 硬件 FIFO 的 API。
    • 或许可以尝试使用 _thread 模块配合共享内存和锁来实现提升采样率。
    • machine.mem32

PIO版本

项目架构

  • 但是,PIO 不能直接访问 ADC ,但可以用它做 GPIO 采样 / PWM 生成 / 数字触发
  • 所以采用额外的模拟 ADC 采样
    • 外部 ADC → PIO 读数字信号
  • 触发逻辑放 PIO 内 → FIFO → MicroPython 读取

  • MicroPython 只负责:

    • 数据收集

    • USB CDC 发送

    • PWM 控制

      复制代码
              ┌──────────────┐

      GPIO26 → │ ADC/PWM │
      └──────────────┘


      ┌──────────────┐
      │ PIO 核心 │
      │ (触发 + FIFO)│
      └──────────────┘


      MicroPython CPU
      (读取 FIFO + USB CDC + 控制 PWM)


      PC / 上位机 GUI

优化方向

  • 可用 内置 ADC + PIO 定时触发采样 (RP2040 PIO + ADC 需要 C 扩展,MP 里可以用 machine.mem32 访问 ADC FIFO)
text 复制代码
PIO 定时触发 ----> ADC 启动采样 ----> ADC FIFO 缓冲 ----> MCU 读取数据
  • PIO 可以生成精准的时钟和触发信号:例如 500 kS/s(每 2 µs)采样

  • ADC FIFO 用硬件缓存数据

  • MCU 需要从 FIFO 高速读取:

  • 数据是 12 位,多个样本连续,理想情况下通过 DMA 或直接 内存访问

    • 在 MicroPython 中, 直接访问寄存器速度慢,解释器每次访问有解析开销,无法处理高采样率(>100 kS/s):
    python 复制代码
    import machine
    val = machine.mem32[0x4004C000]  # 直接访问寄存器
  • DMA 可以自动将 ADC FIFO 数据写入 RAM,但是MicroPython 无法直接启动 DMA。可利用 C 扩展实现,初始化 ADC + PIO,并配置 DMA 自动搬运。直接返回缓冲区给 Python 层(创建一个 native module 或 C扩展返回缓冲区指针或创建 bytearray 供Python 使用)。

C语言版本

  • DMA + C 版本 可以达到 500 kS/s。
  • 26、27引脚为信号采集引脚。
  • pwm输出使用引脚22(可能用于信号发生器)。

项目架构

复制代码
┌──────────┐
│   PC     │
│ WEB程序  │
└────┬─────┘
     │ USB CDC
┌────▼─────┐
│ TinyUSB  │
│  CDC栈   │
└────┬─────┘
     │
┌────▼──────────────┐
│   你的应用代码     │
│  cdc_task()       │
│  capture()        │
└────┬──────────────┘
     │
┌────▼─────┐
│ ADC + DMA│
└──────────┘

烧录

  • 项目给出了 UF2USB Flashing Format)固件文件。用于方便通过 拖拽方式刷写 microcontroller(MCU)闪存。

  • Raspberry Pi Pico 有一个 Bootloader(引导程序) :-按住 BOOTSEL ,然后将USB连接到电脑 → Pico 显示为 USB 大容量存储

  • 可以把 UF2 文件直接拖进去

  • Bootloader 会:

    1. 读取 UF2 文件
    2. 写入闪存对应地址
    3. 自动重启 MCU 运行固件

网页端

  • 网页是 一个基于 Web 的示波器前端,通过 Web Serial API 与 MCU 通信,采集 ADC 数据并绘制波形。

  • USB CDC 提供 一个虚拟串口(Virtual COM Port),浏览器通过它与 MCU 进行双向通信。

  • 串口初始化

js 复制代码
async function start(){
    ports = await navigator.serial.getPorts();  // 获取已连接的 CDC 设备
    if(ports.length==1){
        await ports[0].open({baudRate:1500000, bufferSize:4096})  // 打开串口
        reader1 = ports[0].readable.getReader(); // 读取器
        writer1 = ports[0].writable.getWriter(); // 写入器
        setTimeout(run,1)
        receive() // 启动数据接收循环
    }
    else setTimeout(start,333)
}
  • navigator.serial.getPorts():获取已授权的串口设备。
  • ports[0].open():打开串口,设置波特率(1.5M)和缓冲区大小。
  • reader1 / writer1:用于 读写 CDC 数据
  • 发送命令到 MCU
js 复制代码
var measureCommand = new Uint8Array([42, 109, message_id + 48, 47]); // [*, m, 0, /]
var parameterCommand = new Uint8Array([42,112, 0,0,...,47])          // *, p, ..., /
await writer1.write(parameterCommand) // 发送参数
await writer1.write(measureCommand)   // 启动采样
  • 接收 MCU 数据
js 复制代码
async function receive(){
    var data = [];
    do {
        var { value, done } = await reader1.read(); // 循环读取 CDC 数据
        data = [ ...data, ...value]; // 追加接收数据
        for(var i=0; i<data.length-1; i++){
            if(data[i]==starcode && data[i+1]==slashcode) endOfMessage = i;
        }
        if(endOfMessage >= 0){
            var message = data.slice(0,endOfMessage + 2);
            data = data.slice(endOfMessage + 2);
            processMessage(message); // 解析完整消息
            endOfMessage = -1;
        }
    } while(true)
}
  • 数据的处理可见"纯python"版本部分。

CG

相关推荐
DJ斯特拉5 小时前
Vue工程化
前端·javascript·vue.js
秋深枫叶红5 小时前
嵌入式第三十五篇——linux系统编程——exec族函数
linux·前端·学习
LinDon_5 小时前
【vue2form表单中的动态表单校验】
前端·javascript·vue.js
一水鉴天5 小时前
整体设计 之28 整体设计 架构表表述总表的 完整程序(之27 的Q268 )(codebuddy)
java·前端·javascript
DsirNg6 小时前
使用 SSE 单向推送实现 系统通知功能
前端·javascript
IT_陈寒6 小时前
SpringBoot 3.2 实战:用这5个新特性让你的API性能提升40%
前端·人工智能·后端
霍理迪6 小时前
HTML初相识
前端·html
恋猫de小郭6 小时前
Android 宣布 Runtime 编译速度史诗级提升:在编译时间上优化了 18%
android·前端·flutter
莓莓儿~6 小时前
Next.js 14 App Router数据获取开发手册
开发语言·前端·javascript