深度解析 RP2040 DMA 应用:MicroPython 下 UART 零 CPU 干预数据传输
1.实验前的准备
在以下代码中,我们自定义了一个 DMA_UART_Tx 类,它用于在树莓派 Pico 上通过 UART 和 DMA 进行高效的串口数据发送,DMA 将数据从源地址(内存)传输到目的地址( UART 数据寄存器)中。
在下面实验中,我们需要将风雅一号板-Grove接口扩展板插入到风雅一号板-通用兼容扩展板上,同时使用 HY2.0-4P 接线连接风雅一号板-Grove接口扩展板的 UART1 接口和 GraftSense-基于CH340K的USB转TTL模块 上的 UART1 接口:

接着使用 USB-A 转 MiniUSB 数据线连接GraftSense-基于CH340K的USB转TTL模块上的 USB 接口和电脑,实物连接图如下所示:

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

插入后实物图如下:

引脚连接情况如下所示:

2.自定义串口 DMA 传输类的实现
这里,我们首先将自定义 UART 串口 DMA 传输类放到一个单独的文件中:
# Python env : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @Time : 2024/9/7 下午11:56
# @Author : 李清水
# @File : dma_uart_tx.py
# @Description : 自定义UART串口DMA传输
# ======================================== 导入相关模块 =========================================
# 从 rp2 库导入 DMA 控制器
from rp2 import DMA
# 导入时间相关的模块
import time
# 导入硬件相关的模块
from machine import UART
# 导入读写32位内存的模块
from machine import mem32
# 导入 addressof 函数,用于获取数据的内存地址
from uctypes import addressof
# ======================================== 全局变量 ============================================
# ======================================== 功能函数 ============================================
# ======================================== 自定义类 ============================================
# 自定义DMA串口发送类:使用DMA传输数据到UART完成数据发送
class DMA_UART_Tx:
"""
DMA_UART_Tx类,用于通过DMA(直接内存访问)将数据传输到UART完成数据发送。
该类封装了DMA和UART的配置与操作,支持8个数据位、无校验位、1个停止位的传输模式。
默认使用UART0,波特率115200,TX引脚0,RX引脚1。
Attributes:
uart_num (int): UART编号,0表示UART0,1表示UART1。
baudrate (int): 波特率,默认为115200。
tx_pin (int): TX引脚编号,默认为0。
rx_pin (int): RX引脚编号,默认为1。
uart (UART): UART实例,用于配置和操作UART。
dma (DMA): DMA实例,用于配置和操作DMA。
UART_BASE (int): UART寄存器基地址。
UART_UARTDR (int): UART数据寄存器地址。
UART_UARTFR (int): UART标志寄存器地址。
UART_UARTDMACR (int): UART DMA控制寄存器地址。
Methods:
__init__(self, uart_num=0, baudrate=115200, tx_pin=0, rx_pin=1):
初始化DMA UART类。
is_transmit_fifo_full(self):
检查UART传输FIFO是否已满。
is_transmit_fifo_empty(self):
检查UART传输FIFO是否为空。
is_buffer_protocol(obj):
判断对象是否为缓冲区协议对象。
enable_uart_tx_dma(self):
启用UART发送DMA功能。
dma_transmit(self, buf, wait_func=None, callback=None, blocking=False):
使用DMA传输数据到UART。
"""
def __init__(self, uart_num: int = 0, baudrate: int = 115200, tx_pin: int = 0, rx_pin: int = 1) -> None:
"""
初始化DMA UART类,暂时只支持8个数据位、无校验位、1个停止位的传输模式。
默认使用UART0,波特率115200,TX引脚0,RX引脚1。
Args:
uart_num (int): 选择UART0或UART1,默认为0。
baudrate (int): 波特率,默认为115200。
tx_pin (int): TX引脚编号,默认为0。
rx_pin (int): RX引脚编号,默认为1。
Raises:
ValueError: 如果UART编号或波特率不合法。
"""
# 检测UART是否可用
if uart_num not in (0, 1):
raise ValueError("UART number must be 0 or 1")
# 检查波特率是不是合法值
if baudrate not in (9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600):
raise ValueError("Invalid baudrate")
self.uart_num = uart_num
self.baudrate = baudrate
self.tx_pin = tx_pin
self.rx_pin = rx_pin
# 初始化 UART
self.uart = UART(self.uart_num, baudrate=self.baudrate, tx=self.tx_pin, rx=self.rx_pin)
self.uart.init(baudrate=self.baudrate, bits=8, parity=None, stop=1, timeout=100)
# 初始化 DMA
self.dma = DMA()
# 定义 UART 寄存器地址:若是UART0,那么地址为0x40034000,否则为0x40038000
self.UART_BASE = 0x40034000 if self.uart_num == 0 else 0x40038000
# UART外设中 UARTDR、UARTFR、UARTCR和UARTDMACR寄存器的相关地址信息
self.UARTDR_OFFSET = 0x000
self.UARTFR_OFFSET = 0x018
self.UARTDMACR_OFFSET = 0x048
self.UART_UARTDR = self.UART_BASE + self.UARTDR_OFFSET
self.UART_UARTFR = self.UART_BASE + self.UARTFR_OFFSET
self.UART_UARTDMACR = self.UART_BASE + self.UARTDMACR_OFFSET
# 使能DMA传输功能
self.enable_uart_tx_dma()
def is_transmit_fifo_full(self) -> bool:
"""
检查 UART 传输 FIFO 是否已满。
Returns:
bool: True 为满,False 为未满。
"""
# 读取 UARTFR 寄存器的值
reg_value = mem32[self.UART_UARTFR]
# 提取 TXFF 位的值 (第 5 位)
txff_bit = (reg_value >> 5) & 0x1
# 判断 TXFF 位是否为 1(FIFO 是否已满)
return txff_bit == 1
def is_transmit_fifo_empty(self) -> bool:
"""
检查 UART 传输 FIFO 是否为空。
Returns:
bool: True 为空,False 为不为空。
"""
# 读取 UARTFR 寄存器的值
reg_value = mem32[self.UART_UARTFR]
# 提取 TXFE 位的值 (第 7 位)
txfe_bit = (reg_value >> 7) & 0x1
# 判断 TXFE 位是否为 1(FIFO 是否为空)
return txfe_bit == 1
@staticmethod
def is_buffer_protocol(obj: object) -> bool:
"""
判断对象是否为缓冲区协议对象。
Args:
obj (object): 传入对象。
Returns:
bool: 若为缓冲区协议对象,返回True,否则返回False。
"""
try:
# 尝试使用 memoryview 来创建对象的缓冲区视图
memoryview(obj)
return True
except TypeError:
return False
def enable_uart_tx_dma(self) -> None:
"""
启用 UART 发送 DMA 功能,设置 UARTDMACR 寄存器的 TXDMAE 位。
"""
reg_value = mem32[self.UART_UARTDMACR]
reg_value |= (1 << 1)
mem32[self.UART_UARTDMACR] = reg_value
def dma_transmit(self, buf: object, wait_func: callable = None, callback: callable = None,
blocking: bool = False) -> int:
"""
使用DMA传输数据到UART。
Args:
buf (object): 待传输的字节数据,需要为缓冲区协议对象。
wait_func (callable): 传输进行中时的回调函数,可选。
callback (callable): 传输完成时的回调函数,可选。
blocking (bool): 是否阻塞等待传输完成,可选,默认为 False。
Returns:
int: 耗时,单位为微秒。
Raises:
Exception: 如果非阻塞模式下传入了 wait_func,或者 buf 不是缓冲区协议对象。
"""
# 如果选择非阻塞模式,那么不应该传入wait_func
if blocking == False and wait_func is not None:
raise Exception("Blocking mode should not have wait_func!")
# 判断buf是否为缓冲区协议对象
if DMA_UART_Tx.is_buffer_protocol(buf) == False:
raise Exception("buf must be a buffer protocol object!")
# 20为DREQ_UARTO_TX请求信号的编号,22为DREQ_UART1_TX请求信号的编号
UART_DREQ = 20 if self.uart_num == 0 else 22
# 设置DMA控制寄存器
ctrl = self.dma.pack_ctrl(enable=True, # 启用 DMA
size=0, # 单次数据传输大小8-bit (byte)
inc_read=True, # 读取地址递增
inc_write=False, # 写入地址不递增
treq_sel=UART_DREQ # 设置 DMA 触发信号
)
# 配置 DMA
self.dma.config(read=addressof(buf), # 源地址,即数据缓冲区的内存地址
write=self.UART_UARTDR, # 目标地址,即 UART 数据寄存器的地址
count=len(buf), # 数据传输的字节数
ctrl=ctrl, # DMA 控制寄存器配置
trigger=True # 立即触发 DMA 传输
)
# 记录开始时间
start_time = time.ticks_us()
# 启动 DMA 传输
self.dma.active(1)
# 阻塞模式,等待 DMA 传输完成
if blocking == True:
# 等待 DMA 传输完成
while self.dma.active():
# 检查 UARTFR 寄存器的 TXFE 位是否置位
if not self.is_transmit_fifo_empty():
# 如果 TXFE 位不为 1,说明 FIFO 不为空
if wait_func is not None:
# 执行用户自定义回调函数
wait_func()
if callback is not None:
# 等待DMA传输完毕,执行用户自定义回调callback
callback()
# 记录结束时间
end_time = time.ticks_us() # 记录结束时间
# 返回耗时
return time.ticks_diff(end_time, start_time)
# ======================================== 初始化配置 ==========================================
# ======================================== 主程序 ===========================================
DMA_UART_Tx 类提供了如下方法和属性:
-
DMA_UART_Tx 类的属性:

-
DMA_UART_Tx 类的方法:


这里,我们首先查看树莓派 Pico 中串口外设的结构:

这里,图中的 32x8 transmit FIFO 和 32x12 receive FIFO 是 UART 硬件模块的一部分,但并不以直接的独立寄存器形式暴露给用户。我们需要通过 UARTDR(数据寄存器)来间接操作 FIFO,这个寄存器允许读写数据,并通过内部的硬件管理 FIFO 缓冲区的队列操作:

- 写入
UARTDR寄存器的 DATA 数据位:可以实现串口发送 - 读取
UARTDR寄存器的 DATA 数据位:可以实现读取串口接收的数据
而通过配置 UARTLCR_H 寄存器,则可以使能串口的 FIFO:

实际上,在 MicroPython 中创建并配置 UART 类的实例后,则已经使能了 FIFO 和 FIFO 中断触发阈值:

FIFO 的状态通常是通过标志寄存器 UARTFR (Flag Register) 来监控的,UARTFR 包含关于 FIFO 是否为空或已满的标志位。这样,通过读取标志位可以判断 FIFO 的状态,从而进行相应的数据传输操作:

串口外设中的 DMA 通过 UARTDMACR 来控制和协调数据传输,我们这里需要使能其 TXDMAE 数据位:

这里,使用 DMA 实现串口外设 DMA 数据发送的流程为:首先使能串口外设发送 DMA 中的 TXDMAE 数据位,接着使用 DMA 不断开启数据从内存到串口外设 UARTDR 寄存器的 DATA 数据位。
我们在初始化 DMA_UART_Tx 类时,进行如下工作:

- 初始化 UART 配置,指定所使用的 UART 接口(
uart_num),通信的波特率(baudrate),以及发送(tx_pin)和接收引脚(rx_pin)。 - 使用 MicroPython 中的
UART类初始化串口。self.uart_num决定使用哪个 UART 接口,baudrate设置通信速率,tx_pin和rx_pin则确定使用的引脚,接着使用初始化方法配置串口外设参数为 8 个数据位、不使用奇偶校验位、1 个停止位并设置超时时间为 100ms。 - 创建一个 DMA 对象,准备在后续的传输中使用 DMA 进行内存到 UART 的传输操作。
- 定义 UART 寄存器地址,若是使用
UART0,则基地址为0x40034000,而UART1基地址为0x40038000,并计算出UARTDR寄存器、UARTFR寄存器和UARTDMACR寄存器的偏移量用以进行访问。 - 调用
enable_uart_tx_dma()函数,将UARTDMACR寄存器的TXDMAE位(位 1)置 1,启用 UART 的 DMA 发送功能。
在读写寄存器数据时,我们使用machine.mem32来读写 32 位的寄存器数据,向其中传入寄存器地址值,即可像访问内存一样,访问和配置寄存器。
我们使用 DMA 实现串口数据发送的主要方法为 dma_transmit:
def dma_transmit(self, buf: object, wait_func: callable = None, callback: callable = None,
blocking: bool = False) -> int:
"""
使用DMA传输数据到UART。
Args:
buf (object): 待传输的字节数据,需要为缓冲区协议对象。
wait_func (callable): 传输进行中时的回调函数,可选。
callback (callable): 传输完成时的回调函数,可选。
blocking (bool): 是否阻塞等待传输完成,可选,默认为 False。
Returns:
int: 耗时,单位为微秒。
Raises:
Exception: 如果非阻塞模式下传入了 wait_func,或者 buf 不是缓冲区协议对象。
"""
... ...
这里,我们需要向其中传入四个参数:传输的字节数据、传输进行中的回调函数、传输完成后的回调函数和是否阻塞等待传输完成标志量。
当选择阻塞模式时,则方法会阻塞直到 DMA 数据传输完成才返回;反之,设置为非阻塞模式时,开启 DMA 传输数据后即返回:
# 阻塞模式,等待 DMA 传输完成
if blocking == True:
# 等待 DMA 传输完成
while self.dma.active():
# 检查 UARTFR 寄存器的 TXFE 位是否置位
if not self.is_transmit_fifo_empty():
# 如果 TXFE 位不为 1,说明 FIFO 不为空
if wait_func is not None:
# 执行用户自定义回调函数
wait_func()
在该方法中,我们首先进行了入口参数验证:
- 如果选择了非阻塞模式(
blocking=False),则wait_func参数不应传入
# 如果选择非阻塞模式,那么不应该传入wait_func
if blocking == False and wait_func is not None:
raise Exception("Blocking mode should not have wait_func!")
- 验证
buf是否为缓冲区协议对象,如果不是,抛出异常
# 判断buf是否为缓冲区协议对象
if DMA_UART_Tx.is_buffer_protocol(buf) == False:
raise Exception("buf must be a buffer protocol object!")
这里,我们定义了一个静态方法判断其是否为缓冲区协议对象:
@staticmethod
def is_buffer_protocol(obj: object) -> bool:
"""
判断对象是否为缓冲区协议对象。
Args:
obj (object): 传入对象。
Returns:
bool: 若为缓冲区协议对象,返回True,否则返回False。
"""
try:
# 尝试使用 memoryview 来创建对象的缓冲区视图
memoryview(obj)
return True
except TypeError:
return False
我们会根据使用的串口外设编号配置 DMA 请求信号:
# 20为DREQ_UARTO_TX请求信号的编号,22为DREQ_UART1_TX请求信号的编号
UART_DREQ = 20 if self.uart_num == 0 else 22
DMA 请求信号编号如下图所示:

接着,我们配置 DMA 控制寄存器和通道:
# 设置DMA控制寄存器
ctrl = self.dma.pack_ctrl(enable=True, # 启用 DMA
size=0, # 单次数据传输大小8-bit (byte)
inc_read=True, # 读取地址递增
inc_write=False, # 写入地址不递增
treq_sel=UART_DREQ # 设置 DMA 触发信号
)
# 配置 DMA
self.dma.config(read=addressof(buf), # 源地址,即数据缓冲区的内存地址
write=self.UART_UARTDR, # 目标地址,即 UART 数据寄存器的地址
count=len(buf), # 数据传输的字节数
ctrl=ctrl, # DMA 控制寄存器配置
trigger=True # 立即触发 DMA 传输
)
设置 DMA 的源地址为数据缓冲区的内存地址,目标地址为 UART 数据寄存器的地址,单次数据大小为 8 位(即一个字节),读取地址递增以便能够不断读取 buf 中的数据。
在启动 DMA 数据传输前,记录程序开始时间,在 DMA 传输完毕或 DMA 启动完成后,记录结束时间,返回该部分所耗费时间:
# 记录开始时间
start_time = time.ticks_us()
# 启动 DMA 传输
self.dma.active(1)
# 阻塞模式,等待 DMA 传输完成
if blocking == True:
# 等待 DMA 传输完成
while self.dma.active():
# 检查 UARTFR 寄存器的 TXFE 位是否置位
if not self.is_transmit_fifo_empty():
# 如果 TXFE 位不为 1,说明 FIFO 不为空
if wait_func is not None:
# 执行用户自定义回调函数
wait_func()
if callback is not None:
# 等待DMA传输完毕,执行用户自定义回调callback
callback()
# 记录结束时间
end_time = time.ticks_us() # 记录结束时间
# 返回耗时
return time.ticks_diff(end_time, start_time)
这里,我们使用 is_transmit_fifo_empty 方法判断 UART 传输中 FIFO 是否已空,提取 TXFE 位来判断 FIFO 是否为空,从而判断当前串口数据的发送状态:
def is_transmit_fifo_empty(self) -> bool:
"""
检查 UART 传输 FIFO 是否为空。
Returns:
bool: True 为空,False 为不为空。
"""
# 读取 UARTFR 寄存器的值
reg_value = mem32[self.UART_UARTFR]
# 提取 TXFE 位的值 (第 7 位)
txfe_bit = (reg_value >> 7) & 0x1
# 判断 TXFE 位是否为 1(FIFO 是否为空)
return txfe_bit == 1
dma_transmit() 方法完整时序图如下所示:

3.实验例程
以下代码可以在我们提供的资料包中的 elegance-devkit v1\Demo\67 DMA_MemoryToPeripheral 文件夹中找到。
我们测试 DMA_UART_Tx 类的主程序如下:
# Python env : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @Time : 2024/9/7 上午11:26
# @Author : 李清水
# @File : main.py
# @Description : DMA类实验,内存到外设传输:DMA从内存写入数据到UART
# ======================================== 导入相关模块 =========================================
# 导入数学计算相关模块
import math
# 导入时间相关模块
import time
# 导入自定义串口DMA传输类
from dma_uart_tx import DMA_UART_Tx
# 导入硬件相关模块
from machine import UART
# ======================================== 全局变量 ============================================
# 数据缓冲区,待发送的数据:为一个正弦波数据
# 配置正弦波参数
amplitude = 127 # 幅度,正弦波最大值(0-255),127 对应无符号8位范围的一半,峰值
offset = 128 # 偏移量,保证波形在 0 到 255 之间
num_samples = 1000 # 正弦波采样点数
frequency = 1 # 频率,可以自行调整
# 生成正弦波数据,并将其放入 bytearray
sin_wave = bytearray(num_samples)
for i in range(num_samples):
# 计算当前采样点的角度
angle = 2 * math.pi * frequency * (i / num_samples)
# 生成对应的正弦波值,放大并加上偏移量以适应 bytearray (0-255) 范围
sine_value = int(amplitude * math.sin(angle) + offset)
# 将正弦值放入 bytearray 缓冲区
sin_wave[i] = sine_value
# 串口使用DMA发送数据的运行时间
uart_dma_time = 0
# 串口不使用DMA发送数据的运行时间
uart_non_dma_time = 0
# ======================================== 功能函数 ============================================
# DMA传输等待回调函数
def uart_dma_tx_wait_func() -> None:
"""
DMA传输等待回调函数。
Returns:
None
"""
print("wait dma transmit complete")
# DMA传输完成回调函数
def uart_dma_tx_complete_callback() -> None:
"""
DMA传输完成回调函数。
Returns:
None
"""
print("uart dma transmit complete")
# 在 bytearray 每个元素后添加换行符 '\r\n'
def add_newline_after_each_byte(buf: bytearray) -> bytearray:
"""
在 bytearray 每个元素后添加换行符 '\r\n'。
Args:
buf (bytearray): 输入的 bytearray。
Returns:
bytearray: 返回一个新的包含 '\r\n' 的 bytearray。
Raises:
TypeError: 如果输入参数不是 bytearray 类型。
"""
# 检查输入参数是否为 bytearray
if not isinstance(buf, bytearray):
raise TypeError("buf must be a bytearray")
# 新建一个新的 bytearray 用于存放结果
expanded_buf = bytearray()
# 遍历输入缓冲区的每个字节
for byte in buf:
# 添加原始数据字节
expanded_buf.append(byte)
# 追加 '\r' 的 ASCII 值 (13)
expanded_buf.append(13)
# 追加 '\n' 的 ASCII 值 (10)
expanded_buf.append(10)
return expanded_buf
# ======================================== 自定义类 ============================================
# ======================================== 初始化配置 ==========================================
# 上电延时3s
time.sleep(3)
# 打印调试信息,表示开始主程序
print("FreakStudio: DMA Memory to Peripheral Test ")
# 实例化 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)
# 创建串口对象,设置波特率为115200
uart = UART(1, 115200)
# 初始化uart对象,波特率为115200,数据位为8,无校验位,停止位为1
# 设置接收引脚为GPIO5,发送引脚为GPIO4
# 设置串口超时时间为100ms
uart.init(baudrate = 115200,
bits = 8,
parity = None,
stop = 1,
tx = 4,
rx = 5,
timeout = 100)
# ======================================== 主程序 ===========================================
# 串口传输数据,使用DMA
# 传输数据,使用阻塞模式
dma_uart.dma_transmit(buf=sin_wave, wait_func=uart_dma_tx_wait_func, callback=uart_dma_tx_complete_callback, blocking=True)
# 传输数据,使用非阻塞模式,记录运行时间
uart_dma_time = dma_uart.dma_transmit(buf=sin_wave, blocking=False)
# 传输数据,不使用DMA
# 记录开始时间点
start_time = time.ticks_us()
# 传输数据
uart.write(sin_wave)
# 记录结束时间点
end_time = time.ticks_us()
# 计算运行时间
uart_non_dma_time = time.ticks_diff(end_time, start_time)
# 打印调试数据,表示传输完成
print("DMA Finished,run time: %d us" % uart_dma_time)
print("Non-DMA Finished,run time: %d us" % uart_non_dma_time)
这里,我们首先声明了一个缓冲区,向其中写入幅度为 127、直流偏移为 128、采样点为 1000 而频率为 1Hz 的正弦波数据;接着定义了 uart_dma_time 和 uart_non_dma_time 变量记录使用 DMA 和 不使用 DMA 传输数据的时间,同时,我们设置了两个回调函数,用于在 DMA 传输期间和传输结束后输出调试信息。
整个程序时序如下所示:

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

数据格式设置如下:

烧录代码,打开终端,显示如下:


可以看到,使用 DMA 实现串口发送数据的 CPU 占用远远小于普通的串口数据发送,非阻塞模式下,DMA 传输完成后会调用回调函数,无需 CPU 轮询。
我们也可以导出数据的 CSV 文件,查看是不是所有数据全部接收完成:

暂时无法在飞书文档外展示此内容


在以下代码中,我们自定义了一个 `DMA_UART_Tx` 类,它用于在树莓派 Pico 上通过 `UART` 和 `DMA` 进行高效的串口数据发送,`DMA` 将数据从源地址(内存)传输到目的地址( `UART` 数据寄存器)中。
浙公网安备 33010602011771号