1.实验前的准备
在下面实验中,我们需要将**风雅一号板-Grove接口扩展板插入到 风雅一号板-通用兼容扩展板上,同时使用 PH2.0-4P 接线连接 风雅一号板-Grove接口扩展板的 AIN0 接口和 GraftSense-滑动变阻器模块**上的模拟输出接口:
除此之外,我们首先需要将 MiniUSB 数据线连接到**风雅一号板-通用兼容扩展板**上的串口,这里,GP0 和 GP1 是树莓派 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 系统数据请求信号控制,配置流程如下:
- 启用 FIFO (
FCS.EN):FIFO 必须启用,以便将 ADC 转换结果写入 FIFO。默认情况下,FIFO 是禁用的,以防止在执行一次性转换时意外填充 FIFO
2. 启用 ADC 的 DMA 请求 (FCS.DREQ_EN):必须启用 ADC 的 DMA 请求信号 DREQ,以便 DMA 可以根据 ADC 的请求信号从 FIFO 读取数据

- 选择 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 样本右移变为一个字节的数据

- 配置 ADC 采样率(
DIV):在启动 ADC 之前,应该先配置好 ADC 采样率

- **设置自由采样模式(
CS.START_MANY)**:即不断进行 ADC 采样

- 启动 DMA 通道,开始数据传输;DMA 传输完成一轮数据后,可以停止 ADC 或迅速启动新的 DMA 传输
- 清除
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()
在初始化方法中,我们主要进行以下工作:

- 参数检查: 检查
buf是否为bytearray类型,检查adc_id是否合法(0/1/2),检查sample_rate是否在有效范围(>1000Hz); - 配置 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_REG的EN位(位 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)。
- 启用 FIFO:
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_REG的START_MANY位(位 3),ADC 停止采样。 - 轮询
READY位(位 8),确保最后一次转换完成。
- 清除
- DMA 自动停止 :
- 由于 DMA 依赖 ADC 的 FIFO 触发,ADC 停止后 DMA 也会自动停止。
- 停止 ADC 采样 :
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 传输并在传输完成后执行用户输入的回调函数:

- 首先进行参数检查: 检查非阻塞模式 (
blocking=False) 下是否非法传入了wait_func,若传入complete_callback则注册 DMA 中断回调。 - 设置 DMA 控制寄存器:启用 DMA 通道,单次传输 8-bit 数据,ADC FIFO 地址固定,缓冲区地址自动递增,使用 ADC FIFO 作为触发源,同时每次传输完成触发中断。
- 设置传输参数: DMA 源地址设置为
FIFO_REG(ADC 数据寄存器地址0x4004C00C),写地址为addressof(buf)(用户缓冲区物理地址),总传输次数为缓冲区长度,手动触发传输。 - 判断是否为阻塞模式: 如果是阻塞模式并且传入了
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))
在上面例程中,我们进行以下操作:
- 首先使用阻塞模式运行
dma_adc.start_dma_transfer(),等待传输完成,然后计算 ADC DMA 运行时间 并打印。 - 接着调用
dma_uart.dma_transmit(buf1, blocking=True)阻塞模式发送 buf1 数据,计算 UART DMA 运行时间 并打印。 - 进行双缓冲 DMA 采样与传输,首先创建两个 ADC DMA 传输实例
dma_adc_1和dma_adc_2,分别使用buf1和buf2,先启动dma_adc_1进行 ADC 采样,并设定uart_dma_trans_buf1_isr作为回调。 - 在 主循环中轮询:
- 如果
dma_adc_2传输完成,则 启动dma_adc_1,并清除标志位。 - 如果
dma_adc_1传输完成,则 启动dma_adc_2,并清除标志位。
- 如果
以上例程整体时序图如下所示:
以上例程核心流程主要实现使用 DMA 从 ADC 外设传输数据到内存,并利用另一个 DMA 通道将数据通过串口发送出去,并且我们利用双缓冲机制来实现连续的数据采集和传输。
在设置采样率为 2000Hz、串口波特率为 115200、传输字节数为 256 时,串口 DMA 传输时间为 19ms 左右,而 ADC 的 DMA 传输时间为 124ms,因此,我们可以定义两个缓冲区(buf1 和 buf2)轮流存储 ADC 的 DMA 传输过来的数据,在 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 数据传输到 buf1,dma_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_flag 和 dma_1_adc_complete_flag 标志位来决定是否启动新的 DMA 传输:
- 如果
dma_2_adc_complete_flag被设置为True,则说明dma_adc_2对buf2的数据传输已经完成,此时启动dma_adc_1对buf1的新的数据传输 - 如果
dma_1_adc_complete_flag被设置为True,则说明dma_adc_1对buf1的数据传输已经完成,此时启动dma_adc_2对buf2的新的数据传输
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 的采样速率和串口波特率,并增大缓冲区大小,缓冲区越大,处理数据的频率可能越低,因为一次可以处理更多的数据,这有助于减少数据处理的开销;对于需要高数据吞吐量的应用,较大的缓冲区能够更好地支持数据的高效传输。