RP2040 DMA: Zero‑CPU UART Transfer With MicroPython
RP2040 DMA principle, implementation and testing for zero‑CPU‑intervention UART transmission under MicroPython
- Pre‑experiment Preparation
The following code implements a custom DMA_UART_Tx class for efficient UART data transmission on Raspberry‑Pi Pico using DMA. DMA moves data from source memory address to the destination UART data register.
For this experiment, insert Elegance‑One Grove‑Interface Expansion Board onto Elegance‑One Universal Compatible Expansion Board. Use a HY2.0‑4P cable to connect the UART1 port on the Elegance‑One Grove‑Interface Expansion Board to the UART1 port of the GraftSense‑CH340K‑based USB‑to‑TTL module.

Connect the USB port of GraftSense‑CH340K‑based USB‑to‑TTL module to PC with a USB‑A‑to‑Mini‑USB cable. Physical connection diagram:

Additionally connect Mini‑USB cable to serial port on Elegance‑One Universal Compatible Expansion Board. GP0 and GP1 are default hardware UART0 pins for Raspberry‑Pi Pico.

Physical photo after assembly:
Pin assignment table:

- Implementation of Custom UART DMA‑Transfer Class
Put the custom UART DMA‑transfer class into a separate source file:
# Python env : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @Time : 2024/9/7 11:56 PM
# @Author : Li Qingshui
# @File : dma_uart_tx.py
# @Description : Custom UART DMA transmission
# ======================================== Import related modules =========================================
# Import DMA controller from rp2 library
from rp2 import DMA
# Import time‑related modules
import time
# Import hardware‑related modules
from machine import UART
# Import 32‑bit memory access
from machine import mem32
# Import addressof to get memory address of objects
from uctypes import addressof
# ======================================== Global variables ============================================
# ======================================== Function definitions ============================================
# ======================================== Custom classes ============================================
# Custom DMA UART transmit class: use DMA to transfer data to UART for sending
class DMA_UART_Tx:
"""
DMA_UART_Tx class transmits data to UART peripheral via DMA (Direct Memory Access).
This class encapsulates DMA and UART configuration and operations. It supports 8‑data‑bit, no‑parity, 1‑stop‑bit mode.
Default settings: UART0, baudrate 115200, TX pin 0, RX pin 1.
Attributes:
uart_num (int): UART instance number, 0 for UART0, 1 for UART1.
baudrate (int): Baud rate, default 115200.
tx_pin (int): TX pin number, default 0.
rx_pin (int): RX pin number, default 1.
uart (UART): UART instance for configuration and operation.
dma (DMA): DMA instance for DMA configuration and operation.
UART_BASE (int): Base address of UART registers.
UART_UARTDR (int): Address of UART data register.
UART_UARTFR (int): Address of UART flag register.
UART_UARTDMACR (int): Address of UART DMA control register.
Methods:
__init__(self, uart_num=0, baudrate=115200, tx_pin=0, rx_pin=1):
Initialize DMA UART class.
is_transmit_fifo_full(self):
Check whether UART transmit FIFO is full.
is_transmit_fifo_empty(self):
Check whether UART transmit FIFO is empty.
is_buffer_protocol(obj):
Judge whether object supports buffer protocol.
enable_uart_tx_dma(self):
Enable UART transmit DMA function.
dma_transmit(self, buf, wait_func=None, callback=None, blocking=False):
Transfer data to UART using DMA.
"""
def __init__(self, uart_num: int = 0, baudrate: int = 115200, tx_pin: int = 0, rx_pin: int = 1) -> None:
"""
Initialize DMA UART class. Only 8‑data‑bit, no‑parity, 1‑stop‑bit mode is supported.
Default: UART0, baudrate 115200, TX pin 0, RX pin 1.
Args:
uart_num (int): Select UART0 or UART1, default 0.
baudrate (int): Baud rate, default 115200.
tx_pin (int): TX pin number, default 0.
rx_pin (int): RX pin number, default 1.
Raises:
ValueError: Raised if UART number or baud rate is invalid.
"""
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
# Initialize 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)
# Initialize DMA
self.dma = DMA()
# Define UART register base address: UART0 = 0x40034000; UART1 = 0x40038000
self.UART_BASE = 0x40034000 if self.uart_num == 0 else 0x40038000
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
# Enable UART transmit DMA
self.enable_uart_tx_dma()
def is_transmit_fifo_full(self) -> bool:
"""
Check whether UART transmit FIFO is full.
Returns:
bool: True if full, False otherwise.
"""
reg_value = mem32[self.UART_UARTFR]
txff_bit = (reg_value >> 5) & 0x1
return txff_bit == 1
def is_transmit_fifo_empty(self) -> bool:
"""
Check whether UART transmit FIFO is empty.
Returns:
bool: True if empty, False otherwise.
"""
reg_value = mem32[self.UART_UARTFR]
txfe_bit = (reg_value >> 7) & 0x1
return txfe_bit == 1
@staticmethod
def is_buffer_protocol(obj: object) -> bool:
"""
Judge whether input object supports buffer protocol.
Args:
obj (object): Input object.
Returns:
bool: True for buffer‑protocol‑compatible object, False otherwise.
"""
try:
memoryview(obj)
return True
except TypeError:
return False
def enable_uart_tx_dma(self) -> None:
"""
Enable UART transmit DMA by setting TXDMAE bit inside UARTDMACR register.
"""
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:
"""
Transfer data to UART via DMA.
Args:
buf (object): Byte buffer object which must support buffer protocol.
wait_func (callable): Optional callback invoked during ongoing transfer.
callback (callable): Optional callback invoked on transfer completion.
blocking (bool): Whether to block thread until transfer finishes, default False.
Returns:
int: Elapsed time in microseconds.
Raises:
Exception: wait_func supplied under non‑blocking mode; or buf does not support buffer protocol.
"""
if blocking == False and wait_func is not None:
raise Exception("Blocking mode should not have wait_func!")
if DMA_UART_Tx.is_buffer_protocol(buf) == False:
raise Exception("buf must be a buffer protocol object!")
# DREQ_UART0_TX = 20; DREQ_UART1_TX =22
UART_DREQ = 20 if self.uart_num == 0 else 22
ctrl = self.dma.pack_ctrl(enable=True,
size=0,
inc_read=True,
inc_write=False,
treq_sel=UART_DREQ
)
self.dma.config(read=addressof(buf),
write=self.UART_UARTDR,
count=len(buf),
ctrl=ctrl,
trigger=True
)
start_time = time.ticks_us()
self.dma.active(1)
if blocking == True:
while self.dma.active():
if not self.is_transmit_fifo_empty():
if wait_func is not None:
wait_func()
if callback is not None:
callback()
end_time = time.ticks_us()
return time.ticks_diff(end_time, start_time)
# ======================================== Initialization ==========================================
# ======================================== Main program ===========================================
Attributes exposed by DMA_UART_Tx class:

Methods exposed by DMA_UART_Tx class:

Overview of Raspberry‑Pi Pico UART peripheral hardware:

The 32×8 transmit FIFO and 32×12 receive FIFO are internal hardware resources of UART block and are not exposed as independent CPU‑accessible registers. Program interacts with FIFO indirectly via UARTDR data register; hardware manages FIFO queue internally.

Write DATA bits of UARTDR register → perform UART transmission. Read DATA bits of UARTDR register → read received UART data.
FIFO can be enabled by configuring UARTLCR_H register.

When you create UART object inside MicroPython, FIFO and FIFO interrupt thresholds are already enabled.
FIFO status flags are provided inside UARTFR flag register for checking empty / full conditions.
UART peripheral DMA behaviour is controlled by UARTDMACR register; we need to set TXDMAE bit to enable transmit DMA.
Workflow for UART transmit DMA: enable TXDMAE bit in UARTDMACR, then use DMA to continuously move bytes from memory to DATA field of UARTDR register.
Initialization steps inside DMA_UART_Tx constructor:

Initialize UART with selected instance number, baud rate, TX / RX pin assignments. Call UART init method to set 8‑bit data, no parity, 1 stop bit and 100 ms timeout. Instantiate DMA object for memory‑to‑peripheral transfers. Calculate UART base register addresses for UART0 / UART1 and compute offset addresses for UARTDR, UARTFR and UARTDMACR registers. Invoke enable_uart_tx_dma() to set TXDMAE bit (bit 1) inside UARTDMACR register and activate transmit DMA function.
Note: use
machine.mem32to read‑write 32‑bit peripheral registers, supply register memory address for memory‑mapped register access.
Core transmit method is dma_transmit.
def dma_transmit(self, buf: object, wait_func: callable = None, callback: callable = None,
blocking: bool = False) -> int:
"""
Use DMA to transfer data to UART.
Args:
buf (object): Byte buffer object supporting buffer‑protocol.
wait_func (callable): Optional callback invoked during ongoing transfer.
callback (callable): Optional callback invoked on transfer completion.
blocking (bool): Whether block until transfer finishes, optional, default False.
Returns:
int: Elapsed time in microseconds.
Raises:
Exception: wait_func present under non‑blocking mode; or buf not buffer‑protocol object.
"""
... ...
Four input arguments: byte buffer, ongoing‑transfer callback, completion callback, blocking flag. Blocking mode: function returns only after DMA finishes. Non‑blocking mode: returns immediately after starting DMA transfer.
# Blocking mode: wait for DMA transfer completion
if blocking == True:
while self.dma.active():
if not self.is_transmit_fifo_empty():
if wait_func is not None:
wait_func()
Input parameter validation:
- Non‑blocking mode cannot supply wait_func callback.
if blocking == False and wait_func is not None:
raise Exception("Blocking mode should not have wait_func!")
- Check if input buffer supports buffer‑protocol.
if DMA_UART_Tx.is_buffer_protocol(buf) == False:
raise Exception("buf must be a buffer protocol object!")
Static helper method for buffer‑protocol detection:
@staticmethod
def is_buffer_protocol(obj: object) -> bool:
"""
Judge whether object supports buffer protocol.
Args:
obj (object): Input object.
Returns:
bool: True for buffer‑protocol‑compatible object, False otherwise.
"""
try:
memoryview(obj)
return True
except TypeError:
return False
Select DREQ signal number for corresponding UART peripheral:
# DREQ_UART0_TX =20, DREQ_UART1_TX =22
UART_DREQ = 20 if self.uart_num == 0 else 22
Configure DMA control register and channel parameters:
ctrl = self.dma.pack_ctrl(enable=True,
size=0,
inc_read=True,
inc_write=False,
treq_sel=UART_DREQ
)
self.dma.config(read=addressof(buf),
write=self.UART_UARTDR,
count=len(buf),
ctrl=ctrl,
trigger=True
)
DMA source = memory address of input buffer; destination = UART data register; 8‑bit per transfer; read‑address auto‑increment; write‑address fixed pointing to UARTDR.
Record timestamp before DMA activation, record timestamp after DMA finishes or starts, return elapsed microseconds.
start_time = time.ticks_us()
self.dma.active(1)
if blocking == True:
while self.dma.active():
if not self.is_transmit_fifo_empty():
if wait_func is not None:
wait_func()
if callback is not None:
callback()
end_time = time.ticks_us()
return time.ticks_diff(end_time, start_time)
is_transmit_fifo_empty reads TXFE bit inside UARTFR register to judge transmit‑FIFO status.
def is_transmit_fifo_empty(self) -> bool:
"""
Check whether UART transmit FIFO is empty.
Returns:
bool: True if empty, False otherwise.
"""
reg_value = mem32[self.UART_UARTFR]
txfe_bit = (reg_value >> 7) & 0x1
return txfe_bit == 1
Timing diagram for dma_transmit():
- Experiment Sample Code
Source code is located in resource‑package path elegance‑devkit v1\Demo\67 DMA_MemoryToPeripheral.
Main program for testing DMA_UART_Tx class:
# Python env : MicroPython v1.23.0
# -*- coding: utf-8 -*-
# @Time : 2024/9/7 11:26 AM
# @Author : Li Qingshui
# @File : main.py
# @Description : DMA experiment, memory‑to‑peripheral: DMA writes data from memory to UART
# ======================================== Import related modules =========================================
# Import math library
import math
# Import time‑related modules
import time
# Import custom DMA‑UART‑TX class
from dma_uart_tx import DMA_UART_Tx
# Import hardware‑related modules
from machine import UART
# ======================================== Global variables ============================================
# Data buffer for sine‑wave samples
amplitude = 127
offset = 128
num_samples = 1000
frequency = 1
sin_wave = bytearray(num_samples)
for i in range(num_samples):
angle = 2 * math.pi * frequency * (i / num_samples)
sine_value = int(amplitude * math.sin(angle) + offset)
sin_wave[i] = sine_value
uart_dma_time = 0
uart_non_dma_time = 0
# ======================================== Function definitions ============================================
def uart_dma_tx_wait_func() -> None:
"""
Callback invoked during DMA transmission.
Returns:
None
"""
print("wait dma transmit complete")
def uart_dma_tx_complete_callback() -> None:
"""
Callback invoked when DMA transmission finishes.
Returns:
None
"""
print("uart dma transmit complete")
def add_newline_after_each_byte(buf: bytearray) -> bytearray:
"""
Append '\r\n' after each byte inside bytearray buffer.
Args:
buf (bytearray): Input bytearray.
Returns:
bytearray: New bytearray with CRLF inserted.
Raises:
TypeError: Input is not bytearray.
"""
if not isinstance(buf, bytearray):
raise TypeError("buf must be a bytearray")
expanded_buf = bytearray()
for byte in buf:
expanded_buf.append(byte)
expanded_buf.append(13)
expanded_buf.append(10)
return expanded_buf
# ======================================== Custom classes ============================================
# ======================================== Initialization ==========================================
time.sleep(3)
print("FreakStudio: DMA Memory to Peripheral Test ")
# Instantiate DMA_UART_Tx: UART0, baudrate 115200, TX=GP0, RX=GP1
dma_uart = DMA_UART_Tx(uart_num=0, baudrate=115200, tx_pin=0, rx_pin=1)
# Create ordinary UART1 object
uart = UART(1, 115200)
uart.init(baudrate = 115200,
bits = 8,
parity = None,
stop = 1,
tx = 4,
rx = 5,
timeout = 100)
# ======================================== Main program ===========================================
# DMA transmit, blocking mode
dma_uart.dma_transmit(buf=sin_wave, wait_func=uart_dma_tx_wait_func, callback=uart_dma_tx_complete_callback, blocking=True)
# DMA transmit, non‑blocking mode, measure time
uart_dma_time = dma_uart.dma_transmit(buf=sin_wave, blocking=False)
# Ordinary UART write without DMA, measure time
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)
Program workflow: prepare sine‑wave sample bytearray with amplitude 127, DC offset 128, 1000 samples, frequency 1 Hz. Define timing variables for DMA / non‑DMA transmit time and two debug callback functions.
Overall program timing diagram:

Launch SerialPlot software and select PC COM port connected to Pico board.
Data format configuration:

Flash firmware and open serial terminal:


DMA‑driven UART transmission consumes far less CPU resource than ordinary UART write. Under non‑blocking mode DMA invokes completion callback without continuous CPU polling. You can also export CSV data file from SerialPlot to verify complete data reception.

