树莓派Pico UART深度解析:从寄存器操作到Verilog实战
2026/9/7 11:27:31 网站建设 项目流程

第一次被树莓派Pico的UART坑到,是我把它当成普通Cortex-M0开发板、准备照搬STM32那套串口代码的时候。翻RP2040数据手册的UART章节才发现,这里用的不是常见的USART外设,而是一套和Arm PrimeCell PL011高度兼容的设计,寄存器布局、位域定义跟ST、NXP的风格完全不一样。这篇就把树莓派Pico的UART从头到尾拆一遍:先看硬件架构里UART所在的位置和引脚复用关系,再把UARTDR、UARTFR、IBRD、FBRD、LCR_H、CR这些核心寄存器逐个过一遍,最后落到波特率计算、电平转换电路、USB转串口驱动安装和Verilog联调这些实际操作上。适合不满足于只调用SDK、想真正看懂串口收发过程的开发者。

1. 从协议到硬件:树莓派Pico的UART架构

1.1 UART的协议与时序:没有时钟线也能同步的秘密

UART全称是Universal Asynchronous Receiver/Transmitter,翻译过来就是通用异步收发器。和I2C、SPI这类带时钟线的同步通信不一样,UART只靠两根数据线(TX和RX)就能完成双向通信,发送方和接收方之间没有一根专门的时钟线来对齐数据。它的同步机制完全建立在“双方提前约定好传输速率”这个前提上。

打个比方,两个人不用对讲机、只靠约定好的语速聊天,你说一个字他听一个字,只要语速相同,聊天就能进行。UART里的“语速”就是波特率,单位是bps。一个字节的帧结构通常是:空闲时TX线保持高电平,要发数据时先把电平拉低一个位宽作为起始位,然后依次发送8个数据位(低位在前),最后拉高一个位宽作为停止位。

接收方在检测到起始位的下降沿后,会以波特率的16倍频率对后续每一位进行过采样,在每位的中点附近读取电平值。这就是为什么双方波特率必须匹配——如果差太多,采样点就会偏离数据位的中心,误码率急剧上升。RP2030这个译法严格说不对,RP2040的UART也是这么工作的。

1.2 RP2040里的两个UART外设:不止是“两个串口”这么简单

RP2040内部集成了两个完全相同的UART实例,分别叫UART0和UART1,基地址是0x40034000和0x40038000。每个UART的收发路径上各带一个16字节的FIFO,可以缓存数据,避免高速收发时CPU来不及处理导致丢字节。FIFO还有一个很有用的特性:可以设置触发阈值,比如FIFO里攒到8个字节才产生一次中断,这样CPU就不需要每收到一个字节都跑一趟中断服务函数。

在中断层面,UART0和UART1分别映射到NVIC的IRQ 17和IRQ 18。每个UART外设内部又细分了接收中断、发送中断、超时中断等多个中断源,都通过IMSC寄存器统一控制使能和屏蔽。到了DMA层面,RP2040的UART还支持DMA请求,DMACR寄存器可以配置接收和发送的DMA使能。这意味着数据量大的场景下,完全可以让DMA把FIFO里的数据搬到内存缓冲区,CPU去干别的活。

这里要特别强调一个Pico和普通8位单片机不一样的地方:RP2040的UART寄存器不是像C51、STM32那样“一个寄存器管一个功能”的紧凑布局,而是PL011这种扩展性很好的布局。很多状态标志位集中在FR标志寄存器里,控制位分散在CR、LCR_H等寄存器中,初次接触的人很容易搞混。

1.3 引脚复用与GPIO功能选择:为什么GPIO0既能点灯又能发数据

树莓派Pico的GPIO都是多功能复用的,同一个物理引脚可以由不同的外设使用。UART0和UART1可以被映射到多组引脚上,具体见下表:

功能可用引脚
UART0 TXGPIO0、GPIO12、GPIO16、GPIO28
UART0 RXGPIO1、GPIO13、GPIO17、GPIO29
UART1 TXGPIO4、GPIO8、GPIO20、GPIO24
UART1 RXGPIO5、GPIO9、GPIO21、GPIO25

这个映射关系在RP2040数据手册的GPIO功能表里写得清清楚楚。引脚复用由IO_BANK0基地址下的GPIOx_CTRL寄存器控制,每个GPIO控制寄存器的低5位是FUNCSEL字段,对应不同的外设功能。比如GPIO0_CTRL的地址是0x40014004,把FUNCSEL设为2,GPIO0就切换成UART0的TX功能。

实际操作中很多人会掉进一个坑:以为设置了GPIO复用之后还要像操作普通IO一样配置输入输出方向。实际上,当引脚被切换到UART功能后,发送引脚的方向由UART硬件自动控制,接收引脚也自动变成输入,不需要再去动GPIO方向寄存器。这一点和STM32的复用推挽配置思路不同,理解之后能少走不少弯路。

1.4 UART、USART、SPI、I2C、CAN放在一起怎么选

做嵌入式开发,经常要在几种通信接口里做选择。整理一个直观的对比表:

特性UARTUSARTSPII2CCAN
时钟线同步模式有有(SCK)有(SCL)
数据线TX/RXTX/RXMOSI/MISOSDA/SCLCANH/CANL
通信方式全双工全双工全双工半双工半双工
常见速率115200bps量级可到数Mbps几十Mbps几Mbps1Mbps以下
接线复杂度
抗干扰能力

RP2040并没有独立的USART外设,它的UART本质上是异步模式,如果想做同步串行通信,更多是用SPI或者I2C。对于大多数场景,比如调试日志输出、和蓝牙模块通信、和GPS模块对接,UART就够用了,接线少、协议简单、调试方便。如果要接多个传感器,I2C可以用两根线挂一堆设备;如果追求速率,SPI是最佳选择;如果是工业现场、需要远距离抗干扰通信,CAN或者RS-485总线更适合。

2. 寄存器地图与波特率发生器

2.1 核心寄存器一览:先把地图记住再上路

要直接操作UART,第一步是把寄存器地图记熟。RP2040的UART寄存器布局如下:

偏移名称作用
0x00UARTDR数据寄存器,读和写都走这里
0x04UARTRSRECR接收状态/错误清除
0x18UARTFR标志寄存器,FIFO状态、忙标志
0x24UARTIBRD波特率整数除数
0x28UARTFBRD波特率小数除数
0x2CUARTLCR_H线路控制,数据位、校验、FIFO使能
0x30UARTCR控制寄存器,UART使能、收发使能
0x34UARTIFLSFIFO触发阈值选择
0x38UARTIMSC中断屏蔽置位/清除
0x3CUARTRIS原始中断状态
0x40UARTMIS屏蔽中断状态
0x44UARTICR中断清除
0x48UARTDMACRDMA控制

UARTFR这个标志寄存器非常关键,发送数据前要等TXFF(发送FIFO满)清零才写入,接收数据时要检查RXFE(接收FIFO空)是否为0,发送期间还要注意BUSY位。UARTLCR_H寄存器用来设置帧格式,比如8N1(8个数据位、无校验、1个停止位)就是把WLEN字段设为0b11、PEN位清除、STP2位清除。UARTCR寄存器则是总开关,UARTEN位使能整个UART,TXE和RXE分别控制发送和接收通道。

2.2 波特率参数的数学推导:看起来是小学除法,实际全是坑

RP2040的UART波特率由以下公式决定:

baud = fck / (16 * (IBRD + FBRD/64))

其中fck是UART外设时钟,IBRD是16位整数除数,FBRD是6位小数除数(范围0到63)。这里的16是16倍过采样系数,也就是说UART需要一个16倍波特率的内部时钟。

比如fck=125MHz,目标波特率115200,先算总除数:

125000000 / (16 * 115200) = 67.8168

整数部分IBRD取67,小数部分0.8168乘以64是52.28,四舍五入得到FBRD=52。代入公式验证:

125000000 / (16 * (67 + 52/64)) ≈ 115079

实际波特率是115079,偏差只有大约0.1%,完全在UART容错范围内。

再来一个fck=48MHz的例子。48MHz是很多系统给外设分配的低频时钟,计算过程:

48000000 / (16 * 115200) = 26.0417

IBRD=26,小数部分0.0417乘以64约等于2.67,四舍五入得到FBRD=3。实际波特率:

48000000 / (16 * (26 + 3/64)) ≈ 115273

偏差约0.06%,同样没问题。要注意FBRD的取值范围是0到63,如果四舍五入结果超过63,要把进位加到IBRD上。很多人栽在这里,算出来64直接写进去,硬件就会按一个错误的小数除数工作,波特率彻底跑偏。

2.3 不靠SDK:直接用C操作UART寄存器初始化

在这里给出一段不依赖Pico SDK库函数的寄存器初始化代码。先定义寄存器地址宏:

#define UART0_BASE 0x40034000UL #define UART_DR (UART0_BASE + 0x00) #define UART_FR (UART0_BASE + 0x18) #define UART_IBRD (UART0_BASE + 0x24) #define UART_FBRD (UART0_BASE + 0x28) #define UART_LCR_H (UART0_BASE + 0x2c) #define UART_CR (UART0_BASE + 0x30) #define UART_IFLS (UART0_BASE + 0x34) #define UART_IMSC (UART0_BASE + 0x38) #define UART_FR_TXFF (1 << 5) #define UART_FR_RXFE (1 << 4) #define UART_FR_BUSY (1 << 3) #define UART_LCR_H_WLEN_8 (0b11 << 5) #define UART_LCR_H_FEN (1 << 4) #define UART_CR_UARTEN (1 << 0) #define UART_CR_TXE (1 << 8) #define UART_CR_RXE (1 << 9)

GPIO复用寄存器:

#define IO_BANK0_BASE 0x40014000UL #define GPIO0_CTRL (IO_BANK0_BASE + 0x04) #define GPIO1_CTRL (IO_BANK0_BASE + 0x08) #define FUNCSEL_UART 2

初始化函数:

void uart0_init(void) { // 配置GPIO0为UART0 TX,GPIO1为UART0 RX volatile uint32_t *gpio0_ctrl = (volatile uint32_t *)GPIO0_CTRL; volatile uint32_t *gpio1_ctrl = (volatile uint32_t *)GPIO1_CTRL; *gpio0_ctrl = (*gpio0_ctrl & ~0x1f) | FUNCSEL_UART; *gpio1_ctrl = (*gpio1_ctrl & ~0x1f) | FUNCSEL_UART; // 先关闭UART,再配置参数 volatile uint32_t *cr = (volatile uint32_t *)UART_CR; *cr &= ~UART_CR_UARTEN; // 8N1帧格式,使能FIFO volatile uint32_t *lcr_h = (volatile uint32_t *)UART_LCR_H; *lcr_h = UART_LCR_H_WLEN_8 | UART_LCR_H_FEN; // 系统时钟125MHz下,设置115200波特率 // IBRD=67, FBRD=52 *(volatile uint32_t *)UART_IBRD = 67; *(volatile uint32_t *)UART_FBRD = 52; // FIFO触发阈值:1/2满时触发,即8字节 *(volatile uint32_t *)UART_IFLS = 0b010 << 3; // 使能UART和收发 *cr = UART_CR_UARTEN | UART_CR_TXE | UART_CR_RXE; }

发送和接收函数:

void uart_putc(char c) { // 等待发送FIFO不满 while (*(volatile uint32_t *)UART_FR & UART_FR_TXFF); *(volatile uint32_t *)UART_DR = c; } char uart_getc(void) { // 等待接收FIFO非空 while (*(volatile uint32_t *)UART_FR & UART_FR_RXFE); return (char)(*(volatile uint32_t *)UART_DR & 0xff); }

这段代码完全可以跑在树莓派Pico上。注意一个细节:在最后写UARTCR的时候,我直接整体赋值而不是先读再改,因为前面已经关闭了UART,此时CR寄存器里其他位都是默认值,整体赋值是安全的。如果后面想动态开关某个功能,记得用读-改-写的方式,别把其他位覆盖了。

2.4 寄存器操作能力还能用在哪:PHY调试、UVM验证和PLC通信

学会直接读寄存器这个能力,远不止是操作UART这么简单。做以太网开发时,PHY芯片的内部寄存器同样需要手动读写,Linux下用ethtool就能读取和修改PHY寄存器来排查链路协商问题;在FPGA验证领域,UVM寄存器模型的镜像值(mirror value)就是软件对硬件寄存器状态的影子,理解了底层寄存器才能正确配置predict和update机制;工业现场用的PLC,像施耐德ATV320变频器的RS-485通信地址、汇川PLC的日期时间寄存器,本质上都是Modbus协议里定义的一组寄存器地址。可以说,寄存器操作是嵌入式乃至软硬件交叉领域的一项底层通用能力。

3. 电平转换、USB转串口与Modbus链路搭建

3.1 电平标准不是一回事:TTL、RS-232、RS-485的区别

很多人把UART、TTL、RS-232、RS-485这几个概念搅在一起,其实它们是不同层面的东西。UART是通信协议,定义了帧格式和时序;而TTL、RS-232、RS-485是电气标准,规定了信号的电平大小和传输方式。

树莓派Pico的GPIO输出3.3V TTL电平,高电平是3.3V,低电平是0V。电脑串口通常是RS-232电平,高电平是-12V,低电平是+12V,和TTL完全不兼容,所以电脑原生串口不能直接接Pico的UART引脚。RS-485则是差分信号传输,用两根线之间的电压差表示逻辑1和0,抗干扰能力强,传输距离可以达到上千米。

在实际项目里,Pico最常见的通信对象就是电脑或者各种串口模块,这时候USB转TTL芯片是必备的,把电脑的USB信号转成3.3V TTL电平的UART信号。

3.2 3.3V与1.8V电平转换电路:别让Pico的引脚承受它不该承受的电压

RP2040的GPIO是不耐5V的,数据手册标注的绝对最大额定值表明,超过VDD+0.5V就可能损坏引脚。但在实际项目中经常遇到1.8V逻辑的传感器或者模块,比如某些GPS模块、音频芯片,它们的工作电压是1.8V。这时候直接连Pico的3.3V引脚,轻则通信失败,重则烧坏模块的IO。

如果只做单向转换,最简单的方案是电阻分压。Pico的TX输出3.3V,经过两个电阻分压得到1.8V,比如用10K和18K电阻串联,分压比就是18/(10+18)=0.643,3.3V乘上这个系数大约是2.12V,再微调一下电阻比例,R1=10K、R2=20K时分压得到2.2V,还是偏高。要精确的话用R1=8.2K、R2=10K,分压比0.549,得到1.81V,接近目标。

双向电平转换更好用的是业界经典的BSS138方案,用一颗N-MOS管和两个电阻实现双向转换。电路结构是:Pico侧接3.3V,通过一个100K上拉电阻把漏极拉到3.3V;模块侧接1.8V,同样通过100K上拉电阻拉到1.8V;BSS138的栅极接1.8V。这个电路能自动处理双向信号,关键是栅极电压必须等于低压侧的电源电压,这样高压侧信号进来时MOS管截止,低压侧信号拉低时MOS管导通,实现双向电平转换。

3.3 FT231x和FT232R:USB转UART芯片的选用和驱动坑

做USB转串口,FTDI家的芯片是绕不开的选择。FT231x和FT232R是两种常用的型号,区别在于FT231x的VCCIO引脚可以外接1.8V到3.3V电压,输出电平完全跟VCCIO走,所以接1.8V模块特别合适;FT232R的VCCIO虽然也可以接3.3V,但设计初衷是5V/3.3V系统,应用场景没那么灵活。

用这些芯片最折磨人的是Windows下的驱动问题。Win7时代插上就能用,到了Win10以上反而经常出妖蛾子:设备管理器里出现一个带感叹号的“USB Serial Converter”,而不是大家熟悉的“USB Serial Port”。我遇到过好几次,都是Windows Update自动更新驱动导致的,系统把驱动组件错误识别,串口设备根本不会出现在端口一栏里。

解决方法不复杂,但很多人不知道。右键带感叹号或者识别错的设备,选择“更新驱动程序”,再选“浏览我的电脑以查找驱动程序”,然后点“让我从计算机上的可用驱动程序列表中选取”,这时候会看到两个条目,一个是“USB Serial Converter”,一个是“USB Serial Port”。选后者,安装之后设备管理器里就会出现COM号。如果做过这个操作还是不稳定,建议直接去FTDI官网下载最新版VCP驱动,把旧驱动彻底卸载干净再装。

3.4 接RS-485跑Modbus RTU:Pico在工业现场的正确打开方式

把Pico接入工业设备,最经济的方案是加一颗RS-485收发器,比如MAX3485。这颗芯片是3.3V供电的,正好和Pico电平匹配。接线方式:Pico的UART TX接MAX3485的DI,RX接RO,GPIO控制一个方向引脚DE/RE,发送时拉高,接收时拉低。

Modbus RTU是一种应用层协议,报文由从站地址、功能码、数据域和CRC校验组成。比如读写施耐德ATV320变频器的寄存器,需要先查它的Modbus寄存器映射表,找到对应参数的寄存器地址和数据类型;和汇川PLC通信时,日期时间这类实时值通常存在保持寄存器区,地址从40001开始编号。用Pico做Modbus主机,最稳妥的起步方式是写一个只读保持寄存器的功能码0x03,从设备上读取几个连续地址的寄存器值,验证通过之后再扩展其他功能码。

这里有个很实用的经验:调试Modbus时,先用电脑上的串口调试工具配合一个USB转RS-485模块把报文打通,再用Pico去接替。这样能先把从站地址、波特率、校验位这些参数确认好,避免把问题全部堆在Pico侧一起排查。

3.5 用逻辑分析仪看UART时序:一帧数据长什么样

写再多代码不如亲眼看一次波形。调试UART时,逻辑分析仪是最趁手的工具,选择一款支持24MHz以上采样率的就行,十几个通道足以应对大多数场景。把探针夹在Pico的GPIO0上,地线接GND,触发方式设为下降沿触发,就可以捕获发送过程。

以发送一个字节0x55为例,0x55的二进制是01010101,最低位是1,所以线上波形会是这样:先是空闲高电平,然后拉低一个位宽作为起始位,接下来依次输出1、0、1、0、1、0、1、0这8个数据位,最后拉高一个位宽作为停止位。整个帧宽是10个位宽,时间长度取决于波特率,115200波特率下每一位大约是8.68微秒,一帧大约86.8微秒。

如果采集到的波形每个位都被拉宽或者变窄,说明波特率时钟有偏差,回去检查IBRD和FBRD的计算;如果数据位顺序不对,八成是把低位在前和高位在前搞反了;如果根本没有波形,先查GPIO复用有没有配好,再查UARTCR使能位。

4. Verilog实现UART与硬件排障实录

4.1 用Verilog写一个最简UART发送器:从另一个视角理解UART

写FPGA的时候,我经常需要自己用Verilog实现UART,这不光是因为IP核有时候不好用,更是因为自己写一遍能彻底理解UART的时序。一个最简的UART发送器,核心是一个状态机,状态包括空闲、起始位、数据位、停止位,外加一个波特率分频计数器。

module uart_tx_simple ( input clk, // 系统时钟,例如50MHz input rst_n, input [7:0] tx_data, input tx_start, output reg txd, output reg tx_busy ); localparam IDLE = 2'd0; localparam START = 2'd1; localparam DATA = 2'd2; localparam STOP = 2'd3; // 115200波特率分频参数,50MHz时钟下约434 localparam BAUD_DIV = 16'd434; reg [1:0] state; reg [8:0] clk_cnt; reg [2:0] bit_cnt; reg [7:0] data_buf; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; txd <= 1'b1; tx_busy <= 1'b0; clk_cnt <= 0; bit_cnt <= 0; end else begin case (state) IDLE: begin txd <= 1'b1; tx_busy <= 1'b0; if (tx_start) begin data_buf <= tx_data; state <= START; tx_busy <= 1'b1; clk_cnt <= 0; end end START: begin txd <= 1'b0; if (clk_cnt == BAUD_DIV - 1) begin clk_cnt <= 0; state <= DATA; bit_cnt <= 0; end else begin clk_cnt <= clk_cnt + 1'b1; end end DATA: begin txd <= data_buf[0]; if (clk_cnt == BAUD_DIV - 1) begin clk_cnt <= 0; data_buf <= {1'b0, data_buf[7:1]}; if (bit_cnt == 3'd7) begin state <= STOP; end else begin bit_cnt <= bit_cnt + 1'b1; end end else begin clk_cnt <= clk_cnt + 1'b1; end end STOP: begin txd <= 1'b1; if (clk_cnt == BAUD_DIV - 1) begin clk_cnt <= 0; state <= IDLE; tx_busy <= 1'b0; end else begin clk_cnt <= clk_cnt + 1'b1; end end endcase end end endmodule

这个模块在DATA状态用的是移位输出,每次发送最低位,然后右移一位。如果把RS-485收发器的方向控制引脚接在tx_busy上,发送期间保持高电平,空闲时拉低,就能直接驱动RS-485总线。一个小细节是,tx_busy在START状态就拉高,确保起始位发送期间方向引脚已经切换完成。

4.2 实战排障:Pico和FPGA之间的断连问题

有一次我在实验室里把Pico的UART和FPGA的UART接收模块对接,Pico每秒发送几百个字节,FPGA那边总是出现解析错误,偶尔还能收到乱码。排查过程走了三个阶段。

先确认共地。Pico的GND和FPGA开发板的GND没有接在一起,导致两边的参考地电位不同,信号电平判断出了问题。接上共地线之后,乱码现象明显减少。接着检查电平,Pico输出3.3V,FPGA引脚也是3.3V,但FPGA所在的bank被配置成了1.8V,这会直接导致高电平识别阈值过高,时序上产生严重偏移。把bank电压改成3.3V后,通信基本正常。

最后剩下间歇性丢字节,用逻辑分析仪抓波形发现每隔一段帧就会有一个字节被截断。查Pico侧代码才发现发送函数里没有等待BUSY位清除,FIFO空了就以为发完了,实际上最后一个字节可能还在移位寄存器里。修改发送逻辑,在发送完最后一个字节后等待BUSY拉低,问题彻底解决。这个坑在PL011架构的UART里非常典型,值得记下来。

4.3 常见问题速查表:十几条实战经验直接抄走

现象可能原因排查优先级
串口输出了乱码波特率不匹配、系统时钟不是预期值先验证时钟频率,再检查IBRD、FBRD计算
能发不能收RXE未使能、引脚复用错误检查CR寄存器、GPIO CTNL
能收不能发TXE未使能、发送等待条件错误检查CR寄存器、发送函数的忙等待逻辑
设备管理器识别为“USB Serial Converter”驱动被Windows Update替换手动选择“USB Serial Port”驱动
USB转串口设备感叹号驱动版本不匹配或安装顺序错误卸载重装FTDI官方VCP驱动
电脑完全没有反应转接板供电不足、USB数据线质量问题换线、插机箱后置USB口
信号线有波形但上位机收不到转接板RX/TX接反、共地问题交换TX/RX接线,检查GND
一帧数据里顺序错乱高低位顺序处理错误用0x55抓波形验证起始位和LSB
发送结束后马上发下一字节出错没等待移位寄存器发送完成发送完等待BUSY位清零
RS-485方向切换导致收发异常方向信号在起始位之前没有拉高用tx_busy提前拉高方向引脚
MODBUS通信偶发错误CRC计算错误、波特率偏差过大先读寄存器验证再写,抓完整报文分析

4.4 操作习惯的一点提醒

我自己踩过很多次坑之后,养成了一个雷打不动的习惯:凡是新板子第一次调UART,一定先用逻辑分析仪或者示波器看引脚波形,而不是直接连上位机软件。波形能说明到底有没有信号、帧格式对不对、波特率准不准,比盲猜代码省太多时间了。另外,操作寄存器之前一定把数据手册对应章节的寄存器图打印出来或者开一个窗口随时看,PL011这种寄存器架构虽然规律性强,但位域多,纯粹凭记忆容易出错。等把寄存器层面彻底搞清楚之后,再回头看Pico SDK的uart.c,你会发现那些封装函数的每一行都能对上号,那时候才算真正掌握了这块芯片的串口。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询