RP2040裸金属串口通讯

这篇文章将讨论如何在裸金属应用中使用RP2040串口通讯。

--by Captdam @ Mar 29, 2026 Mar 27, 2026

Index

这篇文章的针对读者为32位RP2040与ARM Cortex-M0+新手,但是熟悉使用汇编与C语言开发8位单片裸金属应用的机开发者。

因为我们将开发裸金属应用,我们将会直接读写单片机和外部闪存设备的控制寄存器,并不使用任何库。

我们将大量依靠文档。文档中包括了我们所需要知道的所有单片机与外部闪存设备控制寄存器的信息。

因为RP2040文档更新的缘故,而且他们还决定把老文档的链接直接重定向到新文档,我决定在本地保存当前版本的副本(2025-02-20)。你可以通过原始链接(取样于2026-02-10)获取该文档。

RP2040文档版权页
RP2040文档版权页

这篇文章将讨论如何在裸金属应用中使用RP2040串口通讯。

我们会顺便总结之前所有文章的主题:

我们将写一个简单的双核裸金属程序:

链接脚本与编译命令(复习)

让我们回顾之前关于编译与链接的文章:

编译


arm-none-eabi-as --warn --fatal-warnings -g *.s -o s.o
arm-none-eabi-objdump --disassembler-options=force-thumb -Dxs s.o > s.list

arm-none-eabi-gcc -mcpu=cortex-m0plus -c -O3 *.c -o c.o
arm-none-eabi-objdump --disassembler-options=force-thumb -Dxs c.o > c.list

arm-none-eabi-ld -nostdlib -nostartfiles -T main.ld *.o -o main.elf
arm-none-eabi-objdump --disassembler-options=force-thumb -dxs main.elf > main.list
pico-elf2uf2 main.elf main.uf2
		

这些指令将会:

  1. 汇编所有的汇编代码文件(.s)为对象文件s.o
  2. 编译(但是不链接)所有的C语言代码文件(.c)为对象文件c.o
  3. 根据链接脚本main.ld链接所有的对象文件为可执行可链接(elf)文件。这不仅包括了我们刚从汇编代码与C语言代码创建的两个对象文件,还包括当前目录下所有已经生成的对象文件,例如SDK启动引导程序(boot2.o)。
  4. 使用elf文件main.elf生成uf2文件main.uf2,该文件用于通过USB下载程序到RP2040。

链接


MEMORY {
	FLASH(rwx) : ORIGIN = 0x10000000, LENGTH = 2048k
	SRAM(rwx) : ORIGIN = 0x20000000, LENGTH = 256k
	SRAM_4(rwx) : ORIGIN = 0x20040000, LENGTH = 4k
	SRAM_5(rwx) : ORIGIN = 0x20041000, LENGTH = 4k
	SRAM_0(rwx) : ORIGIN = 0x21000000, LENGTH = 64k
	SRAM_1(rwx) : ORIGIN = 0x21010000, LENGTH = 64k
	SRAM_2(rwx) : ORIGIN = 0x21020000, LENGTH = 64k
	SRAM_3(rwx) : ORIGIN = 0x21030000, LENGTH = 64k
}

ENTRY(_boot_start)

SECTIONS {
	.boot : {
		*(.boot2)
		*(.boot3)
	} > FLASH
	_boot_start = ORIGIN(FLASH);
	_boot_end = _boot_start + SIZEOF(.boot);

	.core0 : {
		. = ALIGN (256);
		*(.c0_vector)
		*(.c0_data)
		*(.c0_text)
	} > SRAM_4 AT > FLASH
	_core0_dest = ORIGIN(SRAM_4);
	_core0_start = _boot_end;
	_core0_end = _core0_start + SIZEOF(.core0);

	.core1 : {
		. = ALIGN (256);
		*(.c1_vector)
		*(.c1_data)
		*(.c1_text)
	} > SRAM_5 AT > FLASH
	_core1_dest = ORIGIN(SRAM_5);
	_core1_start = _core0_end;
	_core1_end = _core1_start + SIZEOF(.core1);

	.unspecified : {
		*(.text)
		*(.data)
		*(.bss)
	}
	ASSERT(!(SIZEOF(.unspecified)), "Unspecified text, data, and/or bss section")
}
		

我们只之前的文章:对比RP2040可执行内存与从闪存中加载程序到SRAM已经详细讨论过了,故不再赘述。总结一下:

  • 将SDK二级启动引导程序放在闪存的开头(地址0x10000000),之后紧接着我们的三级启动引导程序。
  • 接下来,将核心0的内容放在闪存中,向量表.c0_vector在最前面(以保证其在载入SRAM后256字节对齐),然后是数据.c0_data和程序指令.c0_text。分配地址为SRAM区块4(0x20040000)。
  • 再然后,将核心1的内容放在闪存中,向量表.c1_vector在最前面,然后是数据.c1_data和程序指令.c1_text。分配地址为SRAM区块5(0x20041000)。

启动引导程序(复习)

让我们回顾以下这个程序将使用的两个引导程序:

SDK二级启动引导程序 - boot2.o

将我们在之前的文章:W25Q闪存与裸金属RP2040 SDK引导程序中生成的SDK二级启动引导程序复制过来。

总结来说,该启动引导程序:

我们的三级启动引导程序 - boot3.o

将我们在之前的文章:对比RP2040可执行内存与从闪存中加载程序到SRAM中生成的三级启动引导程序复制过来。

总结来说,该启动引导程序:

汇编代码文件


.cpu cortex-m0plus
.thumb
.align 2
.thumb_func
		

在这个例子中,我们将不使用任何汇编代码。但是,我们仍需保留一个“空的”汇编文件以满足我们的编译指令。

C语言文件 - 向量表

表结构

让我们先来定义向量表。两个核心使用相同的表结构。

ARM向量表
ARM向量表 ©ARM Cortex-M0+ Devices Generic User Guide
RP2040 IRQs
RP2040 IRQs @RP2040

最前面的16个向量为ARM Cortex M0+ CPU使用。

紧接着一系列的IRQ响应程序地址。具体结构根据不同的单片机有所不同,因为不同的单片机带有不同的外围设备。具体可以参考RP2040的文档。

我们可以创建一个头文件vector.h来定义每个向量的地址:


#define vector_sp			0
#define vector_reset		1
#define vector_nmi		2
#define vector_hardfault	3
#define vector_svcall		11
#define vector_pendsv		14
#define vector_systick		15
#define vector_irq(n)		(16 + n)

#define irq_timer0		0
#define irq_timer1		1
#define irq_timer2		2
#define irq_timer3		3
#define irq_pwmwrap		4
#define irq_usbctrl		5
#define irq_xip			6
#define irq_pio00			7
#define irq_pio01			8
#define irq_pio10			9
#define irq_pio11			10
#define irq_dma0			11
#define irq_dma1			12
#define irq_io_bank0		13
#define irq_io_qspi		14
#define irq_sio_proc0		15
#define irq_sio_proc1		16
#define irq_clocks		17
#define irq_spi0			18
#define irq_spi1			19
#define irq_uart0			20
#define irq_uart1			21
#define irq_adc			22
#define irq_i2c0			23
#define irq_i2c1			24
#define irq_rtc			25
			

向量表代码

本例中,我们只使用第一个向量(初始SP)和第二个向量(进入点):


uint32_t c0_vector[48] __attribute__((section(".c0_vector"))) = {
	[vector_sp] = 0x20041000,
	[vector_reset] = (uint32_t)c0_reset
};

uint32_t c1_vector[48] __attribute__((section(".c1_vector"))) = {
	[vector_sp] = 0x20042000,
	[vector_reset] = (uint32_t)c1_reset
};
	

注意,在C语言中,编译器已经为我们设置好了Thumb-bit(地址最低位)。

我们将需要为两个核心都定义向量表。将核心0的向量表放在分区.c0_vector,将核心1的向量表放在分区.c1_vector.这有助于我们在链接时指定向量表的地址。

向量表
向量 核心0 核心1
初始SP SRAM区块4顶端(0x20041000) SRAM区块5顶端(0x20042000)
进入点 函数c0_reset 函数c1_reset

C语言文件 - 核心0程序

现在,让我们来编写核心0运行的程序:


__attribute__((long_call)) extern void boot3_clearInterprocessorMailboxRx();

void c0_reset() __attribute__((section(".c0_text"))) __attribute__((naked));

void c0_reset() {
	(boot3_clearInterprocessorMailboxRx + 1)();
			

定义核心0的进入点,函数c0_reset

因为这个函数将不会返回,因此就没有必要保存调用栈。我们可以把这个函数设置为naked,这将防止编译器添加保存调用栈相关的代码。

将该函数放在分区.c0_text中。

在我们执行任何操作前,我们希望清空核心间通讯使用的信箱,因为这个信箱在启动核心1时可能被污染了。我们可以调用boot3.o提供的boot3_clearInterprocessorMailboxRx函数。

因为boot3_clearInterprocessorMailboxRx保存在闪存中,但c0_reset运行在SRAM中,它们之间的距离超过了16MiB。我们必须将该函数声明为lang_call。编译器默认使用更方便的bl offset指令,但是该指令调用距离有限。注意,在编译时(链接前),编译器并不知道调用者和被调用者的地址。要调用远距离的函数,我们需要将被调用函数地址载入一个寄存器,然后使用bx r指令。

另外,我们需要手动+1函数地址,因为该函数是Thumb函数。编译器不会自动为我们设置Thumb bit。因为在编译时,编译器并不知道外部函数为Thumb或ARM函数。

C宏

我们将创建一个头文件reg.h来储存控制寄存器的地址。这有助于让我们的代码更易读。

RP2040 寄存器原子操作
寄存器原子操作 ©RP2040

#define reg(reg_name)		(*(uint32_t volatile * const)(reg_name))
#define reg_xor(reg_name)	(*(uint32_t volatile * const)(reg_name + 0x1000))
#define reg_set(reg_name)	(*(uint32_t volatile * const)(reg_name + 0x2000))
#define reg_clr(reg_name)	(*(uint32_t volatile * const)(reg_name + 0x3000))
			

之前提到,AHB-Lite Crossbar支持原子操作。我们可以在寄存器地址上添加偏移量来达到这个效果。

配置外围设备

要使用UART收发数据,我们要先启用它。要做到这点:

重置

RP2040 RESETS_RESET
重置控制。比特为1表示该设备出于重置状态,0表示非重置状态。 @ 0x4000C000 ©RP2040

#define reg_resets_reset			0x4000c000
#define reg_resets_reset_usbctrl		24
#define reg_resets_reset_uart1		23
#define reg_resets_reset_uart0		22
#define reg_resets_reset_timer		21
#define reg_resets_reset_tbman		20
#define reg_resets_reset_sysinfo		19
#define reg_resets_reset_syscfg		18
#define reg_resets_reset_spi1			17
#define reg_resets_reset_spi0			16
#define reg_resets_reset_rtc			15
#define reg_resets_reset_pwm			14
#define reg_resets_reset_pll_usb		13
#define reg_resets_reset_pll_sys		12
#define reg_resets_reset_pio1			11
#define reg_resets_reset_pio0			10
#define reg_resets_reset_pads_qspi		9
#define reg_resets_reset_pads_bank0		8
#define reg_resets_reset_jtag			7
#define reg_resets_reset_io_qspi		6
#define reg_resets_reset_io_bank0		5
#define reg_resets_reset_i2c1			4
#define reg_resets_reset_i2c0			3
#define reg_resets_reset_dma			2
#define reg_resets_reset_busctrl		1
#define reg_resets_reset_adc			0
			

	reg_clr(reg_resets_reset)
		= (1<<reg_resets_reset_io_bank0)
		| (1<<reg_resets_reset_uart0);
			

将GPIO区块0与UART0带出重置状态。注意这里我们使用了原子清零操作来避免碰到其它功能的重置状态。

GPIO功能

树莓派Pico Rev3引脚
树莓派Pico Rev3引脚 ©Pico
RP2040 IO_BANK0_GPIO0_CTRL
GPIO 25控制,包括功能选择与覆盖。 @ 0x40014004 (GPIO0) ©RP2040
RP2040 Function Select
RP2040 GPIO功能选择 ©RP2040

#define reg_io_bank0_gpio_ctrl(io)	(0x40014004 + 8 * io)
#define reg_io_bank0_gpio_ctrl_irqover		29
#define reg_io_bank0_gpio_ctrl_irqover_normal	0
#define reg_io_bank0_gpio_ctrl_irqover_invert	1
#define reg_io_bank0_gpio_ctrl_irqover_low	2
#define reg_io_bank0_gpio_ctrl_irqover_high	3
#define reg_io_bank0_gpio_ctrl_inover		16
#define reg_io_bank0_gpio_ctrl_inover_normal	0
#define reg_io_bank0_gpio_ctrl_inover_invert	1
#define reg_io_bank0_gpio_ctrl_inover_low		2
#define reg_io_bank0_gpio_ctrl_inover_high	3
#define reg_io_bank0_gpio_ctrl_oeover		12
#define reg_io_bank0_gpio_ctrl_oeover_normal	0
#define reg_io_bank0_gpio_ctrl_oeover_invert	1
#define reg_io_bank0_gpio_ctrl_oeover_low		2
#define reg_io_bank0_gpio_ctrl_oeover_high	3
#define reg_io_bank0_gpio_ctrl_outover		8
#define reg_io_bank0_gpio_ctrl_outover_normal	0
#define reg_io_bank0_gpio_ctrl_outover_invert	1
#define reg_io_bank0_gpio_ctrl_outover_low	2
#define reg_io_bank0_gpio_ctrl_outover_high	3
#define reg_io_bank0_gpio_ctrl_funcsel		0
#define reg_io_bank0_gpio_ctrl_spi			1
#define reg_io_bank0_gpio_ctrl_uart			2
#define reg_io_bank0_gpio_ctrl_i2c			3
#define reg_io_bank0_gpio_ctrl_pwm			4
#define reg_io_bank0_gpio_ctrl_sio			5
#define reg_io_bank0_gpio_ctrl_pio0			6
#define reg_io_bank0_gpio_ctrl_pio1			7
#define reg_io_bank0_gpio_ctrl_clock		8
#define reg_io_bank0_gpio_ctrl_usb			9
			

	reg(reg_io_bank0_gpio_ctrl(0))
		= (reg_io_bank0_gpio_ctrl_uart<<reg_io_bank0_gpio_ctrl_funcsel);
	reg(reg_io_bank0_gpio_ctrl(1))
		= (reg_io_bank0_gpio_ctrl_uart<<reg_io_bank0_gpio_ctrl_funcsel);
			

设置GPIO 0和1的功能为UART。

外围设备时钟

RP2040时钟树
RP2040时钟树 ©RPI2040
RP2040外围设备时钟控制
外围设备时钟控制 @ 0x40008048 ©RP2040

#define reg_clk_peri_ctrl			0x40008048
#define reg_clk_peri_en				11
#define reg_clk_peri_kill			10
#define reg_clk_peri_auxsrc			5
#define reg_clk_peri_auxsrc_sys		0
#define reg_clk_peri_auxsrc_syspll		1
#define reg_clk_peri_auxsrc_usbpll		2
#define reg_clk_peri_auxsrc_roscph		3
#define reg_clk_peri_auxsrc_xosc		4
#define reg_clk_peri_auxsrc_gpin0		5
#define reg_clk_peri_auxsrc_gpin1		6
			

	reg(reg_clk_peri_ctrl)
		= (1<<reg_clk_peri_en)
		| (reg_clk_peri_auxsrc_sys<<reg_clk_peri_auxsrc);
			

外围设备时钟用于驱动UART,默认关闭。我们在使用UART前需要先启动该时钟。我们将使用系统时钟(使用系统PLL作为输入)来驱动外围设备时钟,频率132MHz

注意外围设备时钟使用AUX MUX,在切换时将产生毛刺,且需要2时钟周期停止和2时钟周期重启。

通常来说,我们在切换时钟时需要禁用使用该时钟的所有设备。因为单片机刚启动,我们可以确保目前没有任何设备使用该时钟。

UART设置 - 波特率

RP2040 UART_UARTIBRD and UART_UARTFBRD
整数波特率寄存器UARTIBRD @ 0x40034024 / 小数波特率寄存器UARTFBRD @ 0x40034028 (UART0) ©RP2040

#define reg_uart_uartibdr(n)		(0x40034024 + 0x4000 * n)
#define reg_uart_uartfbdr(n)		(0x40034028 + 0x4000 * n)
			

RP2040有两套UART。UART0的基础地址为0x40034000,UART1的基础地址为0x40038000。


	reg(reg_uart_uartibdr(0)) = 859;
	reg(reg_uart_uartfbdr(0)) = 24;
			

首先,设置波特率。因为系统时钟为132MHz且我们的PC期望UART信号为9600 BAUD,UART时钟分频器应该为:


132MHz / 9600BAUD / 16 = 859.375
			

也就是说整数部分为859。

UART时钟分频器支持6位小数部分,也就是说:


0.375 * (2^6) = 0.375 * 64 = 24
			

也就是说整数部分为24。

UART设置 - 格式

RP2040 UART_UARTLCR_H
格式控制寄存器UARTLCR_H @ 0x4003402c (UART0) ©RP2040

#define reg_uart_uartlcr_h(n)		(0x4003402c + 0x4000 * n)
#define reg_uart_uartlcr_h_sps		7
#define reg_uart_uartlcr_h_wlen		5
#define reg_uart_uartlcr_h_fen		4
#define reg_uart_uartlcr_h_stp2		3
#define reg_uart_uartlcr_h_eps		2
#define reg_uart_uartlcr_h_pen		1
#define reg_uart_uartlcr_h_brk		0
			

	reg(reg_uart_uartlcr_h(0))
		= ((8-5)<<reg_uart_uartlcr_h_wlen)
		| (1<<reg_uart_uartlcr_h_fen)
		| (1<<reg_uart_uartlcr_h_stp2);
			

接下来,数据格式。我们使用:

  • 8位数据长度。
  • 使用Tx/Rx FIFO(缓存)。(双向各32字)
  • 2位停止符。注意该设置只影响发送端。这有助于增加信号可靠性,因为更长的停止符提供了更多的时钟频率容错空间。

UART设置 - 启动

RP2040 UART_UARTCR
控制寄存器UARTCR @ 0x40034030 (UART0) ©RP2040

#define reg_uart_uartcr(n)		(0x40034030 + 0x4000 * n)
#define reg_uart_uartcr_ctsen			15
#define reg_uart_uartcr_rtsen			14
#define reg_uart_uartcr_out2			13
#define reg_uart_uartcr_out1			12
#define reg_uart_uartcr_rts			11
#define reg_uart_uartcr_dtr			10
#define reg_uart_uartcr_rxe			9
#define reg_uart_uartcr_txe			8
#define reg_uart_uartcr_lbe			7
#define reg_uart_uartcr_sirlp			2
#define reg_uart_uartcr_siren			1
#define reg_uart_uartcr_uarten		0
			

	reg(reg_uart_uartcr(0))
		= (1<<reg_uart_uartcr_rxe)
		| (1<<reg_uart_uartcr_txe)
		| (1<<reg_uart_uartcr_uarten);
			

最后,启用UART,包括Tx(发送端)与Rx(接收端)。

必须在完全配置好之后才能启动UART。

从PC接收数据

在一个死循环中:

通过UART接收

RP2040 UART_UARTDR
数据寄存器UARTDR @ 0x40034000 (UART0) ©RP2040
RP2040 UART_UARTFR
状态寄存器UARTFR @ 0x40034018 (UART0) ©RP2040

#define reg_uart_uartdr(n)		(0x40034000 + 0x4000 * n)
#define reg_uart_uartdr_oe		11 // Overrun
#define reg_uart_uartdr_be		10 // Break error
#define reg_uart_uartdr_pe		9 // Parity error
#define reg_uart_uartdr_fe		8 // Framing error
#define reg_uart_uartdr_data		0 // Tx/Rx data (FIFO)

#define reg_uart_uartfr(n)		(0x40034018 + 0x4000 * n)
#define reg_uart_uartfr_ri		8 // Ring indicator
#define reg_uart_uartfr_txfe		7 // Tx fifo empty
#define reg_uart_uartfr_rxff		6 // Rx fifo full
#define reg_uart_uartfr_txff		5 // Tx fifo full
#define reg_uart_uartfr_rxfe		4 // Rx fifo empty
#define reg_uart_uartfr_busy		3
#define reg_uart_uartfr_dcd		2 // Data carrier detect
#define reg_uart_uartfr_dsr		1 // Data set ready
#define reg_uart_uartfr_cts		0 // Clear to send
			

	for(;;) {
		while ( reg(reg_uart_uartfr(0)) & (<<reg_uart_uartfr_rxfe) );
		char received = reg(reg_uart_uartdr(0));
			

首先,轮询UART状态寄存器以查找新的接收数据。Rx FIFO是否为空?

如果不是,从Rx FIFO读取。

发送到核心1

RP2040 SIO_FIFO_*
核心间通讯信箱FIFO状态与读写 @ 0xD0000050 - 0xD000005B ©RPI2040

#define reg_sio_fifo_st			0xd0000050
#define reg_sio_fifo_st_roe		3		// Read on empty
#define reg_sio_fifo_st_wof		2		// Write on full
#define reg_sio_fifo_st_rdy		1		// Ready to write (not full)
#define reg_sio_fifo_st_vld		0		// Valid to read (not empty)
#define reg_sio_fifo_wr			0xd0000054
			

		while (!( reg(reg_sio_fifo_st) & (1<<reg_sio_fifo_st_rdy) ));
		reg(reg_sio_fifo_wr) = received;
	}
}
			

接下来,轮询信箱状态寄存器。信箱中是否可以接收新的数据(未满)?

如果是,将接收到的数据写入信箱。

C语言文件 - 核心1程序

现在,让我们来编写核心1运行的程序:


void c1_reset() __attribute__((section(".c1_text"))) __attribute__((naked));

void c1_reset() {
	(boot3_clearInterprocessorMailboxRx + 1)();

	for(;;) {
			

定义核心1进入点,函数c1_reset。和c0_reset相似,将它设置为naked,并放在分区.c1_text

在函数最开始,调用boot3_clearInterprocessorMailboxRx来清空核心间通讯信箱。

创建一个死循环来执行以下操作:

从核心0接收数据

RP2040 SIO_FIFO_*
核心间通讯信箱FIFO状态与读写 @ 0xD0000050 - 0xD000005B ©RPI2040

#define reg_sio_fifo_rd			0xd0000058
			

		while (!( reg(reg_sio_fifo_st) & (1<<reg_sio_fifo_st_vld) ));
		char received = reg(reg_sio_fifo_rd);
			

轮询信箱状态寄存器。信箱中数据是否有效(不空)?

如果是,从信箱中读取数据。

打印第一条信息

在我们将接收到的数据通过UART返回给PC前,我们希望先打印一条信息。

RP2040 UART_UARTDR
数据寄存器UARTDR @ 0x40034000 (UART0) ©RP2040
RP2040 UART_UARTFR
状态寄存器UARTFR @ 0x40034018 (UART0) ©RP2040

		__attribute__((section(".c1_data"))) const static char input[] = "Received character is: ";
			

我们定义一条字符串:

  • __attribute__((section(".c1_data"))) - 将该字符串放在分区.c1_data中,该分区将和核心1运行的程序(.c1_text)使用相同的SRAM区块,以防止结构冒险(如果我们将这个字符串放在核心0使用的SRAM区块)和缓存未命中(如果放在闪存中)。
  • const - 该字符串将不会被修改。这只是编译器的提示,旨在我们错误地修改该字符串时编译失败(但是可以绕过)。该关键字不会影响编译器生成的二进制代码。
  • static - 不要把这个字符串放在局部变量(栈)里。

		for(const char* ptr = input; *ptr; ptr++) {
			while ( reg(reg_uart_uartfr(0)) & (11<<reg_uart_uartfr_txff) );
			reg(reg_uart_uartdr(0)) = *ptr;
		}
			

使用for循环打印该字符串:

  • 创建一个指向该字符串的指针。在C语言中,这个指针将指向字符串中的第一个字符。
  • 这个for循环会在指针指向值0时跳出。在C语言中,字符串末尾将会有一个截止符,其值为零。
  • 在每一次循环中,指针将移动一个字符(1字节)。

在这个for循环中,发送每个字符前都先轮询状态寄存器,确保Tx FIFO未满。如果满了就等等。

当有空间后(Tx FIFO不再满),将该字符写入UART数据寄存器以发送该字符。

返回接收的数据


		while (!( reg(reg_uart_uartfr(0)) & (1<<reg_uart_uartfr_txfe) ));
		reg(reg_uart_uartdr(0)) = received;
		reg(reg_uart_uartdr(0)) = '\r';
		reg(reg_uart_uartdr(0)) = '\n';
			

接下来,检查UART,等待直到Tx FIFO为空。这将给我们32个字的可写空间。

待Tx FIFO为空,雷霆狂暴写入接收到的数据和\r\n换行。共计写入3个字。

打印第二条信息并发送修改后的数据


		__attribute__((section(".c1_data"))) const static char output[] = "The next ASCII character is: ";
		for(const char* ptr = input; *ptr; ptr++) {
			while ( reg(reg_uart_uartfr(0)) & (11<<reg_uart_uartfr_txff) );
			reg(reg_uart_uartdr(0)) = *ptr;
		}

		while (!( reg(reg_uart_uartfr(0)) & (1<<reg_uart_uartfr_txfe) ));
		reg(reg_uart_uartdr(0)) = received;
		reg(reg_uart_uartdr(0)) = '\r';
		reg(reg_uart_uartdr(0)) = '\n';
	}
}
			

发送第二条消息和修改(ASCII代码+1)后的数据。

注意这条消息const char output[]比Tx FIFO的容量还要大。不过,我们发送每个字符前有检查FIFO状态以防止FIFO覆写。

观测结果

树莓派Pico Rev3引脚
树莓派Pico Rev3引脚 ©Pico
PC上串口终端
PC上串口终端

将Pico板接上USB-UART-TTL转接器,使用GPIO0为Tx,GPIO1为Rx。

然后,将转接器连上PC,并打开串口终端。在这个例子中,我使用Putty的Serial模式。设置波特率为9600。

随便在键盘上敲个字符,将会把该字符通过UART发送给RP2040单片机。例如,敲击'1',就会发送它的ASCII代码0x31。

RP2040将会回复包括该字符('1')和它的下一个字符('2')的信息,如截图所示。

注意,核心0或核心1将可能早于另一个核心启动/进入主程序。在单片机刚启动的一定时间内(通常几毫秒),将无法响应我们发送的数据。要么是核心0还未配置好UART,要么是核心1还在清空信箱。