第一次被树莓派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、GPIO28UART0 RXGPIO1、GPIO13、GPIO17、GPIO29UART1 TXGPIO4、GPIO8、GPIO20、GPIO24UART1 RXGPIO5、GPIO9、GPIO21、GPIO25这个映射关系在RP2040数据手册的GPIO功能表里写得清清楚楚。引脚复用由IO_BANK0基地址下的GPIOx_CTRL寄存器控制每个GPIO控制寄存器的低5位是FUNCSEL字段对应不同的外设功能。比如GPIO0_CTRL的地址是0x40014004把FUNCSEL设为2GPIO0就切换成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寄存器用来设置帧格式比如8N18个数据位、无校验、1个停止位就是把WLEN字段设为0b11、PEN位清除、STP2位清除。UARTCR寄存器则是总开关UARTEN位使能整个UARTTXE和RXE分别控制发送和接收通道。2.2 波特率参数的数学推导看起来是小学除法实际全是坑RP2040的UART波特率由以下公式决定baud fck / (16 * (IBRD FBRD/64))其中fck是UART外设时钟IBRD是16位整数除数FBRD是6位小数除数范围0到63。这里的16是16倍过采样系数也就是说UART需要一个16倍波特率的内部时钟。比如fck125MHz目标波特率115200先算总除数125000000 / (16 * 115200) 67.8168整数部分IBRD取67小数部分0.8168乘以64是52.28四舍五入得到FBRD52。代入公式验证125000000 / (16 * (67 52/64)) ≈ 115079实际波特率是115079偏差只有大约0.1%完全在UART容错范围内。再来一个fck48MHz的例子。48MHz是很多系统给外设分配的低频时钟计算过程48000000 / (16 * 115200) 26.0417IBRD26小数部分0.0417乘以64约等于2.67四舍五入得到FBRD3。实际波特率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 TXGPIO1为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波特率 // IBRD67, FBRD52 *(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的数据手册标注的绝对最大额定值表明超过VDD0.5V就可能损坏引脚。但在实际项目中经常遇到1.8V逻辑的传感器或者模块比如某些GPS模块、音频芯片它们的工作电压是1.8V。这时候直接连Pico的3.3V引脚轻则通信失败重则烧坏模块的IO。如果只做单向转换最简单的方案是电阻分压。Pico的TX输出3.3V经过两个电阻分压得到1.8V比如用10K和18K电阻串联分压比就是18/(1018)0.6433.3V乘上这个系数大约是2.12V再微调一下电阻比例R110K、R220K时分压得到2.2V还是偏高。要精确的话用R18.2K、R210K分压比0.549得到1.81V接近目标。双向电平转换更好用的是业界经典的BSS138方案用一颗N-MOS管和两个电阻实现双向转换。电路结构是Pico侧接3.3V通过一个100K上拉电阻把漏极拉到3.3V模块侧接1.8V同样通过100K上拉电阻拉到1.8VBSS138的栅极接1.8V。这个电路能自动处理双向信号关键是栅极电压必须等于低压侧的电源电压这样高压侧信号进来时MOS管截止低压侧信号拉低时MOS管导通实现双向电平转换。3.3 FT231x和FT232RUSB转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 RTUPico在工业现场的正确打开方式把Pico接入工业设备最经济的方案是加一颗RS-485收发器比如MAX3485。这颗芯片是3.3V供电的正好和Pico电平匹配。接线方式Pico的UART TX接MAX3485的DIRX接ROGPIO控制一个方向引脚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 2d0; localparam START 2d1; localparam DATA 2d2; localparam STOP 2d3; // 115200波特率分频参数50MHz时钟下约434 localparam BAUD_DIV 16d434; 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 1b1; tx_busy 1b0; clk_cnt 0; bit_cnt 0; end else begin case (state) IDLE: begin txd 1b1; tx_busy 1b0; if (tx_start) begin data_buf tx_data; state START; tx_busy 1b1; clk_cnt 0; end end START: begin txd 1b0; if (clk_cnt BAUD_DIV - 1) begin clk_cnt 0; state DATA; bit_cnt 0; end else begin clk_cnt clk_cnt 1b1; end end DATA: begin txd data_buf[0]; if (clk_cnt BAUD_DIV - 1) begin clk_cnt 0; data_buf {1b0, data_buf[7:1]}; if (bit_cnt 3d7) begin state STOP; end else begin bit_cnt bit_cnt 1b1; end end else begin clk_cnt clk_cnt 1b1; end end STOP: begin txd 1b1; if (clk_cnt BAUD_DIV - 1) begin clk_cnt 0; state IDLE; tx_busy 1b0; end else begin clk_cnt clk_cnt 1b1; 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.3VFPGA引脚也是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你会发现那些封装函数的每一行都能对上号那时候才算真正掌握了这块芯片的串口。