RP2040裸金属串口通讯
这篇文章将讨论如何在裸金属应用中使用RP2040串口通讯。
树莓派Pico, RP2040, ARM, Cortex M0+, 裸金属, UART, 串口通讯, 单片机
--by Captdam @ Mar 29, 2026Index
这篇文章的针对读者为32位RP2040与ARM Cortex-M0+新手,但是熟悉使用汇编与C语言开发8位单片裸金属应用的机开发者。
因为我们将开发裸金属应用,我们将会直接读写单片机和外部闪存设备的控制寄存器,并不使用任何库。
我们将大量依靠文档。文档中包括了我们所需要知道的所有单片机与外部闪存设备控制寄存器的信息。
因为RP2040文档更新的缘故,而且他们还决定把老文档的链接直接重定向到新文档,我决定在本地保存当前版本的副本(2025-02-20)。你可以通过原始链接(取样于2026-02-10)获取该文档。
这篇文章将讨论如何在裸金属应用中使用RP2040串口通讯。
我们会顺便总结之前所有文章的主题:
- 使用SDK启动引导程序来从闪存中执行三级启动引导程序。见W25Q闪存与裸金属RP2040 SDK引导程序。
- 从核心0启动核心1。见RP2040裸金属双核应用与核心1启动协议。
- 将系统时钟源从ROSC切换到132MHz的PLL。见裸金属切换RP2040时钟源:ROSC,XOSC与PLL。
- 将程序与数据从闪存(用于储存)载入SRAM(防止闪存缓存未命中)。见对比RP2040可执行内存与从闪存中加载程序到SRAM。
- 手动链接二进制对象文件、汇编代码文件与C语言代码文件。
我们将写一个简单的双核裸金属程序:
- 核心0设置UART。
- 核心0通过UART从PC接收一条数据(一个字)。
- 核心0将该字发送给核心1。
- 核心1修改这个字,为其ASCII代码+1。比如说,A + 1得到B。
- 核心1将修改过后的字和其它信息通过UART发给PC。
链接脚本与编译命令(复习)
让我们回顾之前关于编译与链接的文章:
编译
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
这些指令将会:
- 汇编所有的汇编代码文件(.s)为对象文件
s.o。 - 编译(但是不链接)所有的C语言代码文件(.c)为对象文件
c.o。 - 根据链接脚本
main.ld链接所有的对象文件为可执行可链接(elf)文件。这不仅包括了我们刚从汇编代码与C语言代码创建的两个对象文件,还包括当前目录下所有已经生成的对象文件,例如SDK启动引导程序(boot2.o)。 - 使用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二级启动引导程序复制过来。
总结来说,该启动引导程序:
- 必须放在闪存的开头,地址0x10000000,且必须为256字节大小。这是片上一级引导程序所要求的。
- 配置SSI XIP(将外部闪存映射到内存XIP空间)以支持CPU直接读取外部闪存的数据。
- 启动储存在SDK二级启动引导程序后面的用户程序(即我们的三级启动引导程序)。
我们的三级启动引导程序 - boot3.o
将我们在之前的文章:对比RP2040可执行内存与从闪存中加载程序到SRAM中生成的三级启动引导程序复制过来。
总结来说,该启动引导程序:
- 启动比默认时钟环形振荡器ROSC更精确的晶体振荡器XOSC。
- 使用XOSC作为参考时钟源启动系统PLL,运行频率132MHz。然后,切换系统时钟源到系统PLL。
- 将程序(向量表、数据、程序指令)从闪存(用于存储)复制到SRAM(以避免缓存未命中)。将核心0的内容复制到SRAM区块4,地址0x20040000,将核心1的内容复制到SRAM区块5,地址0x20041000。
- 启动核心1,使用SRAM区块5为栈。开始核心0,使用SRAM区块4为栈。
汇编代码文件
.cpu cortex-m0plus
.thumb
.align 2
.thumb_func
在这个例子中,我们将不使用任何汇编代码。但是,我们仍需保留一个“空的”汇编文件以满足我们的编译指令。
C语言文件 - 向量表
表结构
让我们先来定义向量表。两个核心使用相同的表结构。
最前面的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来储存控制寄存器的地址。这有助于让我们的代码更易读。
#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收发数据,我们要先启用它。要做到这点:
重置
#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功能
#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。
外围设备时钟
#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设置 - 波特率
#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设置 - 格式
#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设置 - 启动
#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接收
#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
#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接收数据
#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前,我们希望先打印一条信息。
__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板接上USB-UART-TTL转接器,使用GPIO0为Tx,GPIO1为Rx。
然后,将转接器连上PC,并打开串口终端。在这个例子中,我使用Putty的Serial模式。设置波特率为9600。
随便在键盘上敲个字符,将会把该字符通过UART发送给RP2040单片机。例如,敲击'1',就会发送它的ASCII代码0x31。
RP2040将会回复包括该字符('1')和它的下一个字符('2')的信息,如截图所示。
注意,核心0或核心1将可能早于另一个核心启动/进入主程序。在单片机刚启动的一定时间内(通常几毫秒),将无法响应我们发送的数据。要么是核心0还未配置好UART,要么是核心1还在清空信箱。