MicroPython ADC 采样拖慢主循环?试试 DMA + 乒乓缓冲,全程解放 CPU

1.实验前的准备

在下面实验中,我们需要将**风雅一号板-Grove接口扩展板插入到 风雅一号板-通用兼容扩展板上,同时使用 PH2.0-4P 接线连接 风雅一号板-Grove接口扩展板AIN0 接口和 GraftSense-滑动变阻器模块**上的模拟输出接口:

除此之外,我们首先需要将 MiniUSB 数据线连接到**风雅一号板-通用兼容扩展板**上的串口,这里,GP0GP1 是树莓派 Pico 的硬件串口 UART0 的默认引脚:

插入后实物图如下:

引脚连接情况如下表所示:

2.自定义模数转换 DMA 传输类的实现

这里,我们定义了一个 DMA_ADC_Transfer 类,用以在树莓派 Pico 中使用 DMA 实现 ADC 转换数据从 FIFO 寄存器到内存的传输。

示例代码如下:

Python 复制代码
# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/9/8 下午11:15   
# @Author  : 李清水            
# @File    : dma_adc_trans.py.py       
# @Description : 自定义ADC数据采集DMA传输类

# ======================================== 导入相关模块 =========================================

# 导入硬件相关模块
from machine import ADC
# 导入 addressof 函数,用于获取数据的内存地址
from uctypes import addressof
# 导入DMA相关模块
from rp2 import DMA
# 导入读写32位内存的模块
from machine import mem32

# ======================================== 全局变量 ============================================

# ======================================== 功能函数 ============================================

# ======================================== 自定义类 ============================================

# 自定义ADC类,通过DMA进行数据传输
class DMA_ADC_Transfer:
    """
​    DMA_ADC_Transfer 类,用于通过 DMA 进行高效的 ADC 数据传输。
​    该类封装了 ADC 配置、DMA 传输、FIFO 控制等功能,实现高速采样并存入用户提供的缓冲区。

​    该类支持自由采样模式,并可配置 ADC 采样率,同时提供阻塞和非阻塞模式进行 DMA 传输。

​    Attributes:
​        buf (bytearray): 用于存储 ADC 采样数据的缓冲区。
​        sample_rate (int): 期望的 ADC 采样率 (Hz)。
​        adc (ADC): ADC 实例对象,用于进行模数转换。
​        dma (DMA): DMA 控制器实例对象。

​    Constants:
​        DREQ_ADC (int): ADC 的 DMA 触发信号 ID。
​        ADC_BASE (int): ADC FIFO 寄存器的基地址。
​        CS_REG (int): 控制寄存器 (CS) 的地址偏移。
​        RESULT_REG (int): 结果寄存器 (RESULT) 的地址偏移。
​        FCS_REG (int): FIFO 控制寄存器 (FCS) 的地址偏移。
​        DIV_REG (int): 采样分频寄存器 (DIV) 的地址偏移。
​        FIFO_REG (int): FIFO 数据寄存器 (FIFO) 的地址偏移。
​        FIFO_THRESH (int): ADC FIFO 触发 DMA 传输的阈值。
​        ADC_CLOCK_FREQ (int): ADC 时钟频率,假设为 48MHz。

​    Methods:
​        __init__(self, buf: bytearray, sample_rate: int = 1000, adc_id: int = 0):
​            初始化 DMA_ADC_Transfer 类的实例,并配置 ADC 与 DMA。

​        configure_adc_fifo(self) -> None:
​            配置 ADC FIFO,使其能够配合 DMA 进行数据传输。

​        start_adc_continuous(self) -> None:
​            启动 ADC 自由采样模式,使其持续采集数据。

​        configure_adc_sample_rate(self, sample_rate: int) -> None:
​            配置 ADC 采样率,并设置相应的时钟分频。

​        stop_dma_adc_fifo(self) -> None:
​            停止 DMA 传输和 ADC 采样,确保采样过程安全结束。

​        start_dma_transfer(self, wait_func: callable = None, complete_callback: callable = None, blocking: bool = True) -> None:
​            启动 DMA 传输,并可选择是否阻塞等待完成或指定传输完成回调。

​        close(self) -> None:
​            关闭 DMA 传输,并释放 ADC 资源。
​    """

​    ​# ADC的DMA触发信号
    DREQ_ADC = 36

    # ADC FIFO寄存器的基地址
    ADC_BASE = 0x4004C000
    # CS 寄存器偏移量为0x00
    CS_REG = ADC_BASE + 0x00
    # RESULT 寄存器偏移量为0x04
    RESULT_REG = ADC_BASE + 0x04
    # FIFO 控制寄存器偏移量为0x08
    FCS_REG = ADC_BASE + 0x08
    # DIV 寄存器偏移量为0x10
    DIV_REG = ADC_BASE + 0x10
    # FIFO 寄存器偏移量为0x0C
    FIFO_REG = ADC_BASE + 0x0C

    # ADC的 FIFO 阈值
    FIFO_THRESH = 1

    # ADC 时钟频率,假设为48MHz
    ADC_CLOCK_FREQ = 48_000_000

    def __init__(self, buf: bytearray, sample_rate: int = 1000, adc_id: int = 0) -> None:
        """
​        初始化DMA_ADC_Transfer类的实例。

​        Args:
​            buf (bytearray): 用户传入的数据缓冲区。
​            sample_rate (int): 期望的采样率 (Hz),默认为1000。
​            adc_id (int): ADC的ID号(0、1或2),默认为0。

​        Raises:
​            TypeError: 如果buf不是bytearray类型。
​            ValueError: 如果adc_id不是0、1或2,或者采样率超出范围。
​        """
​        ​# 判断buf是否为bytearray类型
        if not isinstance(buf, bytearray):
            raise TypeError("buf must be bytearray")

        # 判断adc_id是否为0、1或2
        if adc_id not in (0, 1, 2):
            raise ValueError("adc_id must be 0, 1 or 2")

        # 缓冲区:用户传入的缓冲区
        self.buf = buf
        # 采样率
        self.sample_rate = sample_rate
        # 创建ADC对象
        self.adc = ADC(adc_id)
        # 创建DMA通道对象
        self.dma = DMA()
        # 配置ADC的FIFO
        self.configure_adc_fifo()
        # 配置ADC采样率
        self.configure_adc_sample_rate(self.sample_rate)
        # 启动自由采样模式
        self.start_adc_continuous()

    def configure_adc_fifo(self) -> None:
        """
​        配置ADC的FIFO,与DMA控制器相配合。
​        默认FIFO右移4位,即ADC数据为8位,最大值为255。

​        Returns:
​            None
​        """
​        ​# 启用FIFO (FCS.EN), 启用DMA请求信号 (FCS.DREQ_EN), 设置FIFO右移 (FCS.SHIFT)
        fcs_value = mem32[DMA_ADC_Transfer.FCS_REG]
        mem32[DMA_ADC_Transfer.FCS_REG] = fcs_value | (1 << 0) | (1 << 3) | (1 << 1)
        # 设置FIFO阈值 (FCS.THRESH)
        mem32[DMA_ADC_Transfer.FCS_REG] |= (DMA_ADC_Transfer.FIFO_THRESH << 24)

    def start_adc_continuous(self) -> None:
        """
​        启动自由采样模式,使能START_MANY数据位。
​        ADC将自动以固定的间隔启动新的转换。

​        Returns:
​            None
​        """
​        ​# 读取 CS 寄存器的当前值
        cs_value = mem32[DMA_ADC_Transfer.CS_REG]
        # 设置 START_MANY 位为1
        mem32[DMA_ADC_Transfer.CS_REG] = cs_value | (1 << 3)

    def configure_adc_sample_rate(self, sample_rate: int) -> None:
        """
​        配置ADC采样率。

​        Args:
​            sample_rate (int): 期望的采样率 (Hz)。

​        Returns:
​            None

​        Raises:
​            ValueError: 如果采样率超出范围。
​        """
​        ​# 确保采样率在合理范围内
        if sample_rate <= 0 or sample_rate > 48_000_000:
            raise ValueError("sample rate out of range")

        # 采样率在1000Hz以上时,才能充分发挥DMA的作用
        # 若采样率在1000Hz以下,使用普通软件定时器定时采样即可
        if sample_rate < 1000:
            raise ValueError("sample rate too low")

        # 计算分频器值
        total_period = 48_000_000 / sample_rate
        int_part = int(total_period) - 1
        frac_part = int((total_period - int_part - 1) * 256)

        # 写入采样周期到DIV寄存器
        mem32[DMA_ADC_Transfer.DIV_REG] = (int_part << 8) | frac_part

    def stop_dma_adc_fifo(self) -> None:
        """
​        停止DMA传输和ADC转换。

​        Returns:
​            None
​        """
​        ​# 清除 CS 寄存器中的 START_MANY 位,以停止 ADC 转换
        # 读取 CS 寄存器
        adc_cs_value = mem32[DMA_ADC_Transfer.CS_REG]
        # 清除 START_MANY 位
        adc_cs_value &= ~(1 << 3)
        # 写回 CS 寄存器
        mem32[DMA_ADC_Transfer.CS_REG] = adc_cs_value

        # 轮询 READY 位,确保最后一次转换已完成
        while not ((mem32[DMA_ADC_Transfer.CS_REG]>>8) & 0x1):
            pass

    def start_dma_transfer(self, wait_func: callable = None, complete_callback: callable = None, blocking: bool = True) -> None:
        """
​        启动DMA传输。

​        Args:
​            wait_func (callable): 等待DMA传输完成时调用的函数,可选。
​            complete_callback (callable): DMA传输完成回调函数,可选。
​            blocking (bool): 是否阻塞等待传输完成,默认为True。

​        Returns:
​            None

​        Raises:
​            ValueError: 如果非阻塞模式下传入了wait_func。
​        """

​        ​# 如果选择非阻塞模式,那么不应该传入wait_func
        if blocking == False and wait_func is not None:
            raise ValueError("wait_func must be None when blocking is False")

        # 若是 complete_callback 不为空,则执行用户自定义函数
        if complete_callback is not None:
            # 传输完成中断回调函数
            self.dma.irq(handler=complete_callback, hard=True)

        # 设置DMA控制寄存器
        ctrl = self.dma.pack_ctrl(
            enable=True,                         # 启用 DMA
            size=0,                              # 单次数据传输大小8-bit,单字节传输
            inc_read=False,                      # 读取地址不递增
            inc_write=True,                      # 写入地址递增
            treq_sel=DMA_ADC_Transfer.DREQ_ADC,  # 选择DMA触发源
            irq_quiet = False                    # 在每次传输结束时生成中断
        )

        # 配置DMA通道
        self.dma.config(
            read=self.FIFO_REG,         # 源地址,即ADC外设FIFO寄存器地址
            write=addressof(self.buf),  # 目标地址,即数据缓冲区地址
            count=len(self.buf),        # 数据传输总次数
            ctrl=ctrl,                  # DMA 控制寄存器配置
            trigger=False               # 不立即触发 DMA 传输
        )

        # 启动 DMA 传输
        self.dma.active(1)

        # 阻塞模式,等待 DMA 传输完成
        if blocking == True:
            # 等待DMA执行完毕
            while self.dma.active():
                # 若是 wait_func 不为空,则执行用户自定义函数
                if wait_func is not None:
                    # 执行用户自定义函数
                    wait_func()
        else:
            # 非阻塞模式,不等待DMA执行完毕
            return

    def close(self) -> None:
        """
​        关闭DMA通道,并停止ADC。

​        Returns:
​            None
​        """
​        ​# 释放DMA通道占用
        self.dma.close()
        # 停止ADC转换
        self.stop_dma_adc_fifo()

# ======================================== 初始化配置 ==========================================

# ========================================  主程序  ===========================================

这里,DMA 控制器通过读取 ADC 样本 FIFO 寄存器来获取 ADC 采集数据的过程由 ADC_DREQ 系统数据请求信号控制,配置流程如下:

  1. 启用 FIFO (FCS.EN​):​FIFO 必须启用,以便将 ADC 转换结果写入 FIFO。默认情况下,FIFO 是禁用的,以防止在执行一次性转换时意外填充 FIFO

2. ​启用 ADC 的 DMA 请求 (FCS.DREQ_EN​):​必须启用 ADC 的 DMA 请求信号 DREQ,以便 DMA 可以根据 ADC 的请求信号从 FIFO 读取数据

  1. ​选择 DMA 通道的 DREQ_ADC 数据请求信号:​使用的 DMA 通道必须选择 DREQ_ADC 数据请求信号,以确保 DMA 可以与 ADC 的数据请求信号同步工作

4. ​设置 DREQ 断言阈值 (FCS.THRESH​):​DREQ 断言阈值应设置为 1,以便当 FIFO 中有一个样本时,DMA 就开始传输。需要注意的是,这也是 IRQ 断言的阈值,因此在非 DMA 使用场景中,可能会需要更高的值来减少中断频率

5. ​8 位 DMA 传输的配置(FCS.SHIFT​):​如果 DMA 传输大小设置为 8 位(即将数据传输到内存中的字节数组),则必须启用 FCS.SHIFT,以预先将 FIFO 样本右移变为一个字节的数据

  1. 配置 ADC 采样率(DIV​):​在启动 ADC 之前,应该先配置好 ADC 采样率
  1. **设置自由采样模式(CS.START_MANY​​)**:即不断进行 ADC 采样
  1. 启动 DMA 通道,开始数据传输;DMA 传输完成一轮数据后,可以停止 ADC 或迅速启动新的 DMA 传输
  2. 清除 CS.START_MANY 数据位以停止 ADC 之后,软件应轮询 CS.READY 数据位确保最后一次转换已完成,并清空 FIFO 中的任何多余的采样数据

在自定义的 DMA_ADC_Transfer 类中,具有如下方法:

在初始化方法中,我们完成 ADC 外设关于 DMA 传输的相关配置:

Python 复制代码
# 自定义ADC类,通过DMA进行数据传输
class DMA_ADC_Transfer:
    """
​    DMA_ADC_Transfer 类,用于通过 DMA 进行高效的 ADC 数据传输。
​    该类封装了 ADC 配置、DMA 传输、FIFO 控制等功能,实现高速采样并存入用户提供的缓冲区。

​    该类支持自由采样模式,并可配置 ADC 采样率,同时提供阻塞和非阻塞模式进行 DMA 传输。

​    Attributes:
​        buf (bytearray): 用于存储 ADC 采样数据的缓冲区。
​        sample_rate (int): 期望的 ADC 采样率 (Hz)。
​        adc (ADC): ADC 实例对象,用于进行模数转换。
​        dma (DMA): DMA 控制器实例对象。

​    Constants:
​        DREQ_ADC (int): ADC 的 DMA 触发信号 ID。
​        ADC_BASE (int): ADC FIFO 寄存器的基地址。
​        CS_REG (int): 控制寄存器 (CS) 的地址偏移。
​        RESULT_REG (int): 结果寄存器 (RESULT) 的地址偏移。
​        FCS_REG (int): FIFO 控制寄存器 (FCS) 的地址偏移。
​        DIV_REG (int): 采样分频寄存器 (DIV) 的地址偏移。
​        FIFO_REG (int): FIFO 数据寄存器 (FIFO) 的地址偏移。
​        FIFO_THRESH (int): ADC FIFO 触发 DMA 传输的阈值。
​        ADC_CLOCK_FREQ (int): ADC 时钟频率,假设为 48MHz。

​    Methods:
​        __init__(self, buf: bytearray, sample_rate: int = 1000, adc_id: int = 0):
​            初始化 DMA_ADC_Transfer 类的实例,并配置 ADC 与 DMA。

​        configure_adc_fifo(self) -> None:
​            配置 ADC FIFO,使其能够配合 DMA 进行数据传输。

​        start_adc_continuous(self) -> None:
​            启动 ADC 自由采样模式,使其持续采集数据。

​        configure_adc_sample_rate(self, sample_rate: int) -> None:
​            配置 ADC 采样率,并设置相应的时钟分频。

​        stop_dma_adc_fifo(self) -> None:
​            停止 DMA 传输和 ADC 采样,确保采样过程安全结束。

​        start_dma_transfer(self, wait_func: callable = None, complete_callback: callable = None, blocking: bool = True) -> None:
​            启动 DMA 传输,并可选择是否阻塞等待完成或指定传输完成回调。

​        close(self) -> None:
​            关闭 DMA 传输,并释放 ADC 资源。
​    """

​    ​# ADC的DMA触发信号
    DREQ_ADC = 36

    # ADC FIFO寄存器的基地址
    ADC_BASE = 0x4004C000
    # CS 寄存器偏移量为0x00
    CS_REG = ADC_BASE + 0x00
    # RESULT 寄存器偏移量为0x04
    RESULT_REG = ADC_BASE + 0x04
    # FIFO 控制寄存器偏移量为0x08
    FCS_REG = ADC_BASE + 0x08
    # DIV 寄存器偏移量为0x10
    DIV_REG = ADC_BASE + 0x10
    # FIFO 寄存器偏移量为0x0C
    FIFO_REG = ADC_BASE + 0x0C

    # ADC的 FIFO 阈值
    FIFO_THRESH = 1

    # ADC 时钟频率,假设为48MHz
    ADC_CLOCK_FREQ = 48_000_000

    def __init__(self, buf: bytearray, sample_rate: int = 1000, adc_id: int = 0) -> None:
        """
​        初始化DMA_ADC_Transfer类的实例。

​        Args:
​            buf (bytearray): 用户传入的数据缓冲区。
​            sample_rate (int): 期望的采样率 (Hz),默认为1000。
​            adc_id (int): ADC的ID号(0、1或2),默认为0。

​        Raises:
​            TypeError: 如果buf不是bytearray类型。
​            ValueError: 如果adc_id不是0、1或2,或者采样率超出范围。
​        """
​        ​# 判断buf是否为bytearray类型
        if not isinstance(buf, bytearray):
            raise TypeError("buf must be bytearray")

        # 判断adc_id是否为0、1或2
        if adc_id not in (0, 1, 2):
            raise ValueError("adc_id must be 0, 1 or 2")

        # 缓冲区:用户传入的缓冲区
        self.buf = buf
        # 采样率
        self.sample_rate = sample_rate
        # 创建ADC对象
        self.adc = ADC(adc_id)
        # 创建DMA通道对象
        self.dma = DMA()
        # 配置ADC的FIFO
        self.configure_adc_fifo()
        # 配置ADC采样率
        self.configure_adc_sample_rate(self.sample_rate)
        # 启动自由采样模式
        self.start_adc_continuous()

在初始化方法中,我们主要进行以下工作:

  1. ​参数检查:​ 检查 buf 是否为 bytearray 类型,检查 adc_id 是否合法(0/1/2),检查 sample_rate 是否在有效范围(>1000Hz);
  2. ​配置 ADC 的 FIFO、采样率并启动 ADC 的自由采样模式:​ 主要通过 configure_adc_fifo 方法、configure_adc_sample_rate 方法、start_adc_continuous 方法和 stop_dma_adc_fifo 方法。

这里,configure_adc_fifo 方法、configure_adc_sample_rate 方法、start_adc_continuous 方法和 stop_dma_adc_fifo 方法主要通过读写对应寄存器实现,主要进行以下工作:

  • configure_adc_fifo()​方法:​ 配置 ADC 的 FIFO(先进先出队列),使其与 DMA 控制器配合,实现自动数据传输。
    • 启用 FIFO:
      • 设置 FCS_REGEN 位(位 0)为 1,启用 ADC FIFO。
      • 设置 DREQ_EN 位(位 3)为 1,允许 FIFO 触发 DMA 请求。
      • 设置 SHIFT 位(位 1)为 1,使 ADC 数据右移 4 位(默认 12 位 ADC 数据右移后变为 8 位,便于 DMA 传输)。
    • 设置 FIFO 阈值
      • THRESH(位 24-31)设置 FIFO 触发 DMA 的阈值(默认 FIFO_THRESH = 1,即 FIFO 中有 1 个数据时触发 DMA)。
  • configure_adc_sample_rate()​方法:​ 配置 ADC 的采样率,通过调整时钟分频器 (DIV_REG) 实现目标采样率。
    • 计算分频值
      • ADC 时钟频率固定为 48MHz。
      • 采样周期 = 48_000_000 / sample_rate
      • 分频值 = 采样周期 - 1,并拆分为整数部分 (int_part) 和小数部分 (frac_part)。
    • 写入 ​ DIV_REG
      • int_part 写入高 8 位(位 8-15)。
      • frac_part 写入低 8 位(位 0-7)。
  • start_adc_continuous()​方法:​ 启动 ADC 的自由采样模式,使 ADC 持续采样。
    • CS_REG(控制状态寄存器)中,START_MANY(位 3)置 1,ADC 进入自由采样模式。
    • ADC 会自动以设定的采样率持续转换,无需软件干预。
  • stop_dma_adc_fifo()​方法:​ 停止 DMA 传输和 ADC 采样,确保 ADC 和 DMA 安全关闭。
    • 停止 ADC 采样
      • 清除 CS_REGSTART_MANY 位(位 3),ADC 停止采样。
      • 轮询 READY 位(位 8),确保最后一次转换完成。
    • DMA 自动停止
      • 由于 DMA 依赖 ADC 的 FIFO 触发,ADC 停止后 DMA 也会自动停止。

DMA 实现数据传输的方法为 start_dma_transfer,示例代码如下:

Python 复制代码
def start_dma_transfer(self, wait_func = None, complete_callback = None, blocking = True):
    """
​    启动DMA传输
​    ​:param​ wait_func: 等待DMA传输完成时调用的函数
​    ​:param​ complete_callback: DMA传输完成回调函数
​    ​:return: None
​    """

​    ​# 如果选择非阻塞模式,那么不应该传入wait_func
    if blocking == False and wait_func is not None:
        raise ValueError("wait_func must be None when blocking is False")

    # 若是 complete_callback 不为空,则执行用户自定义函数
    if complete_callback is not None:
        # 传输完成中断回调函数
        self.dma.irq(handler=complete_callback, hard=True)

    # 设置DMA控制寄存器
    ctrl = self.dma.pack_ctrl(
        enable=True,                         # 启用 DMA
        size=0,                              # 单次数据传输大小8-bit,单字节传输
        inc_read=False,                      # 读取地址不递增
        inc_write=True,                      # 写入地址递增
        treq_sel=DMA_ADC_Transfer.DREQ_ADC,  # 选择DMA触发源
        irq_quiet = False                    # 在每次传输结束时生成中断
    )

    # 配置DMA通道
    self.dma.config(
        read=self.FIFO_REG,         # 源地址,即ADC外设FIFO寄存器地址
        write=addressof(self.buf),  # 目标地址,即数据缓冲区地址
        count=len(self.buf),        # 数据传输总次数
        ctrl=ctrl,                  # DMA 控制寄存器配置
        trigger=False               # 不立即触发 DMA 传输
    )

    # 启动 DMA 传输
    self.dma.active(1)

    # 阻塞模式,等待 DMA 传输完成
    if blocking == True:
        # 等待DMA执行完毕
        while self.dma.active():
            # 若是 wait_func 不为空,则执行用户自定义函数
            if wait_func is not None:
                # 执行用户自定义函数
                wait_func()
    else:
        # 非阻塞模式,不等待DMA执行完毕
        return

主要完成 DMA 控制寄存器和通道配置后,开启 DMA 传输并在传输完成后执行用户输入的回调函数:

  1. ​首先进行参数检查:​ 检查非阻塞模式 (blocking=False) 下是否非法传入了 wait_func,若传入 complete_callback 则注册 DMA 中断回调。
  2. ​设置 DMA 控制寄存器:​启用 DMA 通道,单次传输 8-bit 数据,ADC FIFO 地址固定,缓冲区地址自动递增,使用 ADC FIFO 作为触发源,同时每次传输完成触发中断。
  3. ​设置传输参数:​ DMA 源地址设置为 FIFO_REG (ADC 数据寄存器地址 0x4004C00C),写地址为 addressof(buf) (用户缓冲区物理地址),总传输次数为缓冲区长度,手动触发传输。
  4. ​判断是否为阻塞模式:​ 如果是阻塞模式并且传入了 wait_func,则在 DMA 还未传输完成时,循环执行 wait_func 用户定义回调函数。

3.实验例程

以下代码可以在我们提供的资料包中的 elegance-devkit v1\Demo\68 DMA_PeripheralToMemory 文件夹中找到。

测试 DMA_ADC_Transfer 类的代码如下:

Python 复制代码
# Python env   : MicroPython v1.23.0
# -*- coding: utf-8 -*-        
# @Time    : 2024/9/8 上午11:04   
# @Author  : 李清水            
# @File    : main.py       
# @Description : DMA类实验,外设到内存传输,DMA从ADC外设的FIFO传输数据到内存

# ======================================== 导入相关模块 =========================================

# 导入时间相关模块
import time
# 导入自定义ADC DMA类
from dma_adc_trans import DMA_ADC_Transfer
# 导入自定义串口DMA传输类
from dma_uart_tx import DMA_UART_Tx
# 导入micropython相关的模块
import micropython
# 导入硬件相关模块
from machine import Timer

# ======================================== 全局变量 ============================================

# 数据缓冲区:256个元素,每个元素8位(1个字节)
buf1 = bytearray(256)
# 第二个ADC数据缓冲区
buf2 = bytearray(256)

# 串口使用DMA发送数据的运行时间
uart_dma_time = 0
# ADC使用DMA传输数据的运行时间
adc_dma_time = 0

# DMA_ADC_Transfer类实例1对应ADC的DMA和串口的DMA数据传输完毕的标志位
dma_1_adc_complete_flag = False
# DMA_ADC_Transfer类实例2对应ADC的DMA和串口的DMA数据传输完毕的标志位
dma_2_adc_complete_flag = False

# ======================================== 功能函数 ============================================

def adc_wait_dma_complete() -> None:
    """
​    等待DMA传输ADC的FIFO中数据完成时调用的函数。

​    Args:
​        None

​    Returns:
​        None
​    """

​    ​# 打印调试信息
    print("DMA-ADC transmitting ADC data")

def adc_dma_complete_callback(d: object) -> None:
    """
​    DMA传输ADC的FIFO中数据完成时调用的函数。

​    Args:
​        d (object): 使用的DMA通道实例。

​    Returns:
​        None
​    """

​    ​# 打印调试信息
    print("DMA-ADC transfer complete")

def uart_dma_trans_buf1_isr(d: object) -> None:
    """
​    DMA传输ADC的FIFO中数据完成时的中断函数,尽快安排对应回调函数执行。

​    Args:
​        d (object): 使用的DMA通道实例。

​    Returns:
​        None
​    """

​    ​# 安排回调函数在稍后执行
    micropython.schedule(uart_dma_trans_buf1, d)

def uart_dma_trans_buf1(d: object) -> None:
    """
​    DMA传输串口数据,读地址为buf1。

​    Args:
​        d (object): 使用的DMA通道实例。

​    Returns:
​        None
​    """

​    ​# 声明全局变量
    global dma_uart, buf1, dma_1_adc_complete_flag

    # 等待ADC的DMA传输彻底完毕
    while d.active():
        pass

    # 使用DMA传输串口数据,读地址为buf1
    dma_uart.dma_transmit(buf=buf1, blocking=False)
    # 标志位置位
    dma_1_adc_complete_flag = True

def uart_dma_trans_buf2_isr(d: object) -> None:
    """
​    DMA传输ADC的FIFO中数据完成时的中断函数,尽快安排对应回调函数执行。

​    Args:
​        d (object): 使用的DMA通道实例。

​    Returns:
​        None
​    """

​    ​# 安排回调函数在稍后执行
    micropython.schedule(uart_dma_trans_buf2, d)

def uart_dma_trans_buf2(d: object) -> None:
    """
​    DMA传输串口数据,读地址为buf2。

​    Args:
​        d (object): 使用的DMA通道实例。

​    Returns:
​        None
​    """

​    ​# 声明全局变量
    global dma_uart, buf2, dma_2_adc_complete_flag

    # 等待ADC的DMA传输彻底完毕
    while d.active():
        pass

    # 使用DMA传输串口数据,读地址为buf2
    dma_uart.dma_transmit(buf=buf2, blocking=False)
    # 标志位置位
    dma_2_adc_complete_flag = True

# def dma_timer_check(timer: Timer) -> None:
#     """
#     DMA数据传输检查定时器中断函数,检查DMA传输是否完成,
#     如果完成则启动另一个缓冲区的DMA传输。
#
#     Args:
#         timer (machine.Timer): 触发中断的定时器实例。
#
#     Returns:
#         None
#     """
#     global dma_adc_1, dma_adc_2, dma_1_adc_complete_flag, dma_2_adc_complete_flag
#
#     if dma_2_adc_complete_flag:
#         dma_adc_1.start_dma_transfer(blocking=False, complete_callback=uart_dma_trans_buf1_isr)
#         dma_2_adc_complete_flag = False
#
#     if dma_1_adc_complete_flag:
#         dma_adc_2.start_dma_transfer(blocking=False, complete_callback=uart_dma_trans_buf2_isr)
#         dma_1_adc_complete_flag = False

# ======================================== 自定义类 ============================================

# ======================================== 初始化配置 ==========================================

# 上电延时3s
time.sleep(3)
# 打印调试信息,表示开始主程序
print("FreakStudio: DMA Peripheral to Memory Test")

# 初始化DMA_ADC_Transfer类实例,并传入数据缓冲区
dma_adc = DMA_ADC_Transfer(buf = buf1, sample_rate = 2000, adc_id = 0)
# 实例化 DMA_UART_Tx 类,假设使用 UART0,波特率115200,TX引脚为0,RX引脚为1
dma_uart = DMA_UART_Tx(uart_num=0, baudrate=115200, tx_pin=0, rx_pin=1)

# ========================================  主程序  ===========================================

# 记录开始时间
start_time = time.ticks_us()
# 启动DMA传输:阻塞模式,传输未完成时会打印调试信息,传输完成后也会打印调试信息
dma_adc.start_dma_transfer(wait_func = adc_wait_dma_complete,
                           complete_callback = adc_dma_complete_callback,
                           blocking = True)
# 记录结束时间
end_time = time.ticks_us()

# 关闭DMA通道并停止ADC
dma_adc.close()

# 计算DMA数据传输时间
adc_dma_time = time.ticks_diff(end_time, start_time) / 1000

# 打印DMA数据传输时间
print("DMA run time: {:.2f} ms".format(adc_dma_time))

# 打印调试信息
print("FreakStudio: Start DMA UART transmit")

# 串口传输数据,使用DMA
# 传输数据,使用阻塞模式,记录运行时间
uart_dma_time = dma_uart.dma_transmit(buf=buf1,blocking=True) / 1000

# 打印调试数据,表示传输完成
print("DMA UART Finished,run time: {:.2f} ms".format(uart_dma_time))

# 初始化DMA_ADC_Transfer类实例1,并传入数据缓冲区buf1
dma_adc_1 = DMA_ADC_Transfer(buf=buf1, sample_rate=2000, adc_id=0)
# 初始化DMA_ADC_Transfer类实例2,并传入数据缓冲区buf2
dma_adc_2 = DMA_ADC_Transfer(buf=buf2, sample_rate=2000, adc_id=0)

# 首先启动dma_adc_1的DMA传输:非阻塞模式
dma_adc_1.start_dma_transfer(blocking=False,complete_callback = uart_dma_trans_buf1_isr)

# # 初始化软件定时器
# dma_check_timer = Timer(-1)
# # 使用定时器每10ms检查一次DMA传输标志
# dma_check_timer.init(period=10, mode=Timer.PERIODIC, callback=dma_timer_check)

# 轮询开启双缓存不间断数据采集和传输
while True:
    # 记录开始时间
    start_time = time.ticks_us()

    # 判断DMA_ADC_Transfer类实例2的DMA传输是否完成
    if dma_2_adc_complete_flag == True:
        # 启动DMA传输:非阻塞模式
        dma_adc_1.start_dma_transfer(blocking=False,complete_callback = uart_dma_trans_buf1_isr)
        # 标志位不置位
        dma_2_adc_complete_flag = False

    # 判断DMA_ADC_Transfer类实例1的DMA传输是否完成
    if dma_1_adc_complete_flag == True:
        # 启动DMA传输:非阻塞模式
        dma_adc_2.start_dma_transfer(blocking=False,complete_callback = uart_dma_trans_buf2_isr)
        # 标志位不置位
        dma_1_adc_complete_flag = False

    # 记录结束时间
    end_time = time.ticks_us()
    # 计算两次DMA数据传输花费时间
    dma_time = time.ticks_diff(end_time, start_time) / 1000
    # 打印调试数据
    print("DMA ADC run time: {:.2f} ms".format(dma_time))

在上面例程中,我们进行以下操作:

  1. 首先使用阻塞模式运行 dma_adc.start_dma_transfer(),等待传输完成,然后计算 ADC DMA 运行时间 并打印。
  2. 接着调用 dma_uart.dma_transmit(buf1, blocking=True) 阻塞模式发送 buf1 数据,计算 UART DMA 运行时间 并打印。
  3. 进行双缓冲 DMA 采样与传输,首先创建两个 ADC DMA 传输实例 dma_adc_1dma_adc_2,分别使用 buf1buf2,先启动 dma_adc_1 进行 ADC 采样,并设定 uart_dma_trans_buf1_isr 作为回调。
  4. 在 主循环中轮询:
    1. 如果 dma_adc_2 传输完成,则 启动 dma_adc_1,并清除标志位。
    2. 如果 dma_adc_1 传输完成,则 启动 dma_adc_2,并清除标志位。

以上例程整体时序图如下所示:

以上例程核心流程主要实现使用 DMAADC 外设传输数据到内存,并利用另一个 DMA 通道将数据通过串口发送出去,并且我们利用双缓冲机制来实现连续的数据采集和传输。

在设置采样率为 2000Hz、串口波特率为 115200、传输字节数为 256 时,串口 DMA 传输时间为 19ms 左右,而 ADCDMA 传输时间为 124ms,因此,我们可以定义两个缓冲区(buf1buf2)轮流存储 ADCDMA 传输过来的数据,在 DMA 写入两个缓冲区的间隙启动串口 DMA 对已经写入好的缓冲区进行传输并发送到电脑上。通过这种方式,在第一个缓冲区 (buf1 ) 正在进行数据处理(例如通过 DMA 发送到串口)时,第二个缓冲区 (buf2) 继续进行新的数据采集,这样可以避免数据采集和数据处理的冲突,同时减少等待时间和数据处理延迟,提高数据采集和传输的效率。

这里,我们首先初始化两个缓冲区:

Python 复制代码
# 数据缓冲区:256个元素,每个元素8位(1个字节)
buf1 = bytearray(256)
# 第二个ADC数据缓冲区
buf2 = bytearray(256)

然后创建创建两个 DMA_ADC_Transfer 实例:

Python 复制代码
# 初始化DMA_ADC_Transfer类实例1,并传入数据缓冲区buf1
dma_adc_1 = DMA_ADC_Transfer(buf=buf1, sample_rate=2000, adc_id=0)
# 初始化DMA_ADC_Transfer类实例2,并传入数据缓冲区buf2
dma_adc_2 = DMA_ADC_Transfer(buf=buf2, sample_rate=2000, adc_id=0)

其中,dma_adc_1 用于将 ADC 数据传输到 buf1dma_adc_2 用于将 ADC 数据传输到 buf2

然后启动 DMA 传输并设置回调,以 dma_adc_1 为例:

Python 复制代码
# 首先启动dma_adc_1的DMA传输:非阻塞模式
dma_adc_1.start_dma_transfer(blocking=False,complete_callback = uart_dma_trans_buf1_isr)

启动 dma_adc_1 的 DMA 传输,buf1 被用来存储 ADC 数据。uart_dma_trans_buf1_isr 是完成传输后的中断服务例程(ISR),它会安排后续的数据传输操作,并在传输完毕后将 DMA 传输完毕标志位置位:

Python 复制代码
def uart_dma_trans_buf1_isr(d: object) -> None:
    """
​    DMA传输ADC的FIFO中数据完成时的中断函数,尽快安排对应回调函数执行。

​    Args:
​        d (object): 使用的DMA通道实例。

​    Returns:
​        None
​    """

​    ​# 安排回调函数在稍后执行
    micropython.schedule(uart_dma_trans_buf1, d)

def uart_dma_trans_buf1(d: object) -> None:
    """
​    DMA传输串口数据,读地址为buf1。

​    Args:
​        d (object): 使用的DMA通道实例。

​    Returns:
​        None
​    """

​    ​# 声明全局变量
    global dma_uart, buf1, dma_1_adc_complete_flag

    # 等待ADC的DMA传输彻底完毕
    while d.active():
        pass

    # 使用DMA传输串口数据,读地址为buf1
    dma_uart.dma_transmit(buf=buf1, blocking=False)
    # 标志位置位
    dma_1_adc_complete_flag = True

在主程序的循环中,通过检查 dma_2_adc_complete_flagdma_1_adc_complete_flag 标志位来决定是否启动新的 DMA 传输:

  • 如果 dma_2_adc_complete_flag 被设置为 True,则说明 dma_adc_2buf2 的数据传输已经完成,此时启动 dma_adc_1buf1 的新的数据传输
  • 如果 dma_1_adc_complete_flag 被设置为 True,则说明 dma_adc_1buf1 的数据传输已经完成,此时启动 dma_adc_2buf2 的新的数据传输
Python 复制代码
# 轮询开启双缓存不间断数据采集和传输
while True:
    # 记录开始时间
    start_time = time.ticks_us()

    # 判断DMA_ADC_Transfer类实例2的DMA传输是否完成
    if dma_2_adc_complete_flag == True:
        # 启动DMA传输:非阻塞模式
        dma_adc_1.start_dma_transfer(blocking=False,complete_callback = uart_dma_trans_buf1_isr)
        # 标志位不置位
        dma_2_adc_complete_flag = False

    # 判断DMA_ADC_Transfer类实例1的DMA传输是否完成
    if dma_1_adc_complete_flag == True:
        # 启动DMA传输:非阻塞模式
        dma_adc_2.start_dma_transfer(blocking=False,complete_callback = uart_dma_trans_buf2_isr)
        # 标志位不置位
        dma_1_adc_complete_flag = False

    # 记录结束时间
    end_time = time.ticks_us()
    # 计算两次DMA数据传输花费时间
    dma_time = time.ticks_diff(end_time, start_time) / 1000
    # 打印调试数据
    print("DMA ADC run time: {:.2f} ms".format(dma_time))

我们也可以将轮询标志位放到定时器中执行,从而减少 CPU 资源的占用:

Python 复制代码
def dma_timer_check(timer: Timer) -> None:
    """
​    DMA数据传输检查定时器中断函数,检查DMA传输是否完成,
​    如果完成则启动另一个缓冲区的DMA传输。

​    Args:
​        timer (machine.Timer): 触发中断的定时器实例。

​    Returns:
​        None
​    """
​    ​global dma_adc_1, dma_adc_2, dma_1_adc_complete_flag, dma_2_adc_complete_flag

    if dma_2_adc_complete_flag:
        dma_adc_1.start_dma_transfer(blocking=False, complete_callback=uart_dma_trans_buf1_isr)
        dma_2_adc_complete_flag = False

    if dma_1_adc_complete_flag:
        dma_adc_2.start_dma_transfer(blocking=False, complete_callback=uart_dma_trans_buf2_isr)
        dma_1_adc_complete_flag = False
        
# 初始化软件定时器
dma_check_timer = Timer(-1)
# 设置定时器每10ms触发一次dma_timer_check函数
dma_check_timer.init(period=10, mode=Timer.PERIODIC, callback=dma_timer_check)

接着,我们打开 SerialPlot 软件,选择树莓派 Pico 的串口对应的端口:

数据格式设置如下:

烧录程序,连接远程终端,输出如下:

可以看到,使用 DMA 和双缓存机制实现 ADC 数据通过串口传输时,CPU 耗费时间极少,仅 0.05ms 左右。

SerialPlot 软件显示如下,右下角显示了此时的数据更新速率,可以看到为 2000Hz 左右:

这里,在波特率设置为 921600,采样速率设置为:45K Hz 时,实际测试的 ADC 采集和串口发送数据的速率为 40K Hz 左右,主要是由于 DMA 传输和串口发送过程中有一定延迟:

若需要更高的数据传输速率,我们可以提高 ADC 的采样速率和串口波特率,并增大缓冲区大小,缓冲区越大,处理数据的频率可能越低,因为一次可以处理更多的数据,这有助于减少数据处理的开销;对于需要高数据吞吐量的应用,较大的缓冲区能够更好地支持数据的高效传输。

相关推荐
Freak嵌入式2 小时前
从代码到项目:MicroPython+MCP4725 自定义类实现信号发生器功能的完整流程
嵌入式
与你4032 小时前
STM32F103_AB_OTA_从零复现教程
嵌入式
Freak嵌入式2 小时前
一文搞懂 MCP4725 波形发生器:MicroPython 代码实现、定时器驱动与调试流程全梳理
嵌入式
慧都小项2 小时前
汽车工具链重复测试太耗人?用 TestComplete 自动跑
功能测试·回归·汽车·嵌入式·软件工程·测试覆盖率
Discipline~Hai4 小时前
Linux网络编程02-TCP协议
linux·网络·tcp/ip·嵌入式·linux网络编程
Jason_zhao_MR4 小时前
电能质量监测为什么需要PTP精确时钟同步
linux·单片机·嵌入式硬件·嵌入式
Freak嵌入式6 小时前
全网最详细|树莓派 Pico ADC 软件控制全解析:从machine.ADC API 到定时温度采集实战(含 ISR 避坑指南)
嵌入式
嵌入式阿蔡8 小时前
存储_14:嵌入式存储测试岗面试——可靠性/兼容/车规面试题与项目话术
单片机·嵌入式硬件·面试·嵌入式·存储
Freak嵌入式1 天前
嵌入式 MQTT 项目实战:Pico+MicroPython 向 EMQX 发布 JSON 消息全解析
嵌入式