news 2026/10/14 1:33:02

UART串口通信原理与嵌入式实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
UART串口通信原理与嵌入式实战指南

1. 串口通讯不是“古董”,而是嵌入式世界的呼吸节奏

很多人第一次听说“串口通讯”,脑子里自动跳出老式电脑后面那一排9针D型接口,或者单片机开发板上标着TX/RX的两个小焊点。这种印象不算错,但严重低估了它的生命力——它不是博物馆里的展品,而是每天在智能电表里抄表、在工业PLC中同步产线节拍、在医疗监护仪里实时回传心电波形、甚至在你刚拆开的蓝牙耳机充电盒PCB背面,依然稳稳跑着38400波特率的调试日志。我做过一个统计:过去三年参与过的27个量产级嵌入式项目,100%用到了串口,其中19个把它作为主通信通道,剩下8个虽用CAN或以太网做主干,但串口仍是固件升级、参数配置和故障诊断的“生命线”。它不炫技,不堆协议栈,靠的是极低的硬件资源占用(几KB Flash+几百字节RAM)、确定性的时序响应(微秒级中断延迟)、以及近乎零的学习门槛。你不需要懂TCP三次握手,也不用研究HTTP状态码,只要搞懂“一帧数据怎么发、怎么收、怎么不出错”,就能让两个设备真正“说上话”。这篇文章不讲教科书定义,不列RFC文档编号,只讲我在产线调试现场、在深夜改bug时、在帮某高校实验室学生调通温湿度传感器时,反复验证过的真实逻辑、踩过的坑、和那些手册里不会写的“手感”。

2. 串口通讯的本质:把并行思维切成时间切片

2.1 为什么非得“串”?并行不是更快吗?

先破一个常见误解:串口慢,并行快。这在CPU内部总线层面成立,但在设备间通信场景下,恰恰相反。我拿手边一块STM32F4开发板和一块ESP32模块实测过:用8位并行总线(需8根数据线+控制线)连接,布线长度超过15cm后,信号反射导致误码率飙升到12%;而换成UART串口(仅TX/RX两根线),同样距离下,波特率升到2Mbps仍稳定无误。原因在于物理层——并行传输要求所有数据线电气特性严格一致,走线长度差哪怕1mm,高速翻转时就会产生“歪斜(skew)”,接收端看到的8位数据就可能错位。串口把8位(或9位)数据压成一个时间序列,一根线按固定节奏“滴答、滴答”发出去,接收端用本地时钟对齐采样,天然规避了线间延时差异。这就像一群人过独木桥,必须排队走(串行),比八个人并排挤上去(并行)安全得多。所以,“串”的本质不是妥协,而是面向长距离、低成本、高可靠场景的主动设计选择。

2.2 一帧数据的完整生命周期:起始位、数据位、校验位、停止位

串口通信的最小单位是“一帧”,它不是单纯的数据字节,而是一个带严格时序结构的“信封”。我们以最常用的8N1格式(8位数据、无校验、1位停止位)为例,拆解一帧从诞生到消亡的全过程:

  • 起始位(Start Bit):恒为逻辑0,持续1位时间。它的唯一使命是“敲门”——告诉接收方:“注意!新数据来了!” 接收端持续监测RX线,一旦检测到从1变0的跳变,立刻启动内部定时器,在中间时刻采样后续每一位。这个“中间时刻”是关键,它能最大程度容忍发送/接收时钟的微小偏差(通常允许±5%)。我见过太多新手把示波器探头接在TX线上,看到一长串0就以为没数据,其实那正是起始位在宣告开始。

  • 数据位(Data Bits):紧随起始位之后,通常是5~9位,主流是8位。数据低位(LSB)在前,高位(MSB)在后。比如要发字符‘A’(ASCII码0x41,二进制01000001),线路上实际传输顺序是:0→1→0→0→0→0→0→1。这个顺序是硬件强制的,软件无需反转,但如果你用GPIO模拟串口(bit-banging),就必须手动按此顺序输出。

  • 校验位(Parity Bit):可选,用于简单错误检测。奇校验(Odd Parity)要求整帧(数据位+校验位)中1的个数为奇数;偶校验(Even Parity)则要求为偶数。例如数据位01000001中1的个数是2(偶数),若用偶校验,校验位填0;若用奇校验,则填1。它的缺陷很明显:只能检测奇数个位错误,无法定位、无法纠正。在现代应用中,CRC校验已全面取代它,但某些老旧工业设备(如部分PLC)仍强制要求奇校验,这是兼容性绕不开的坎。

  • 停止位(Stop Bit):恒为逻辑1,持续1位或2位时间。它像一个“句号”,明确标识本帧结束,并为下一帧的起始位提供必要的高电平间隔。停止位过短(如强制设为0.5位),会导致连续发送多帧时,接收端无法可靠识别新的起始位,引发“粘连帧”错误。某次调试一款国产电机驱动器,对方固件要求2位停止位,我按常规设1位,结果每发10帧就有1帧被丢弃,示波器抓出来就是停止位不够长,RX线还没来得及拉高,下一帧起始位就来了。

提示:波特率计算公式Baud Rate = f_CPU / (16 × (UBRR + 1))中的16,正是源于经典8051/AVR架构中,每个位周期被划分为16个时钟周期进行采样。现代MCU(如STM32)虽支持分数波特率发生器,但底层采样逻辑仍沿袭此思想——用更高频时钟对RX线进行多次采样(如3次),再取多数表决,以抑制噪声干扰。

2.3 波特率:不是越快越好,而是“刚刚好”

波特率(Baud Rate)常被误认为“速度”,其实它定义的是“每秒传输的符号(Symbol)数量”。在UART中,一个符号就是一个比特(bit),所以常与比特率(bps)混用。但选型绝非“越高越好”。我整理了一份不同波特率在典型场景下的实测表现对比表:

波特率典型应用场景1米双绞线实测最大稳定距离关键制约因素我的实操建议
9600调试日志、传感器读数>50米无新手入门首选,容错率最高
115200高速传感器(IMU)、固件升级~5米线缆分布电容、MCU UART外设时钟精度量产项目主力,需关注PCB走线
921600实时音视频流(简易)、高速ADC采样<1米信号完整性、MCU处理能力仅限板内通信,必须用屏蔽线
2Mbps工业现场总线替代方案<0.3米EMI抗扰度、电源噪声需专业EMC设计,慎用

为什么115200成为事实标准?因为它是2的整数次幂(2^17=131072)的近似值,MCU内部波特率发生器分频计算误差最小。我用STM32H7实测过:设115200,实际误差仅0.15%;设120000,误差跳到2.3%,在长距离传输时直接导致帧丢失。另一个隐形杀手是“时钟源”。很多开发者直接用HSI(内部RC振荡器)跑UART,其温漂可达±1%,远超UART容忍的±5%。某次交付给某高校实验室的温控模块,夏天实验室空调一关,设备就频繁丢指令——最后发现是HSI时钟漂移,换用8MHz外部晶振后问题消失。

3. 硬件电路:从电平标准到隔离保护的实战细节

3.1 RS-232、TTL、RS-485:不是三种串口,而是三种“方言”

初学者常混淆这些名词,以为它们是不同类型的串口协议。其实UART是“语法”(如何组织一帧),而RS-232/RS-485/TTL是“发音规则”(电压怎么表示0和1)。它们可以共存于同一套UART逻辑中,只需更换电平转换芯片。

  • TTL电平:MCU原生电平,逻辑1≈3.3V或5V,逻辑0≈0V。特点是成本低、速度快(可达数Mbps),但抗干扰差、传输距离短(<1米)。所有开发板上的TX/RX引脚都是TTL电平。我习惯在调试阶段直接用USB-TTL转换器(CH340/CP2102)连电脑,省去电平转换烦恼。

  • RS-232电平:经典PC串口标准,逻辑1=-3V~-15V,逻辑0=+3V~+15V。用正负电压表示,天生抗共模干扰,理论距离15米。但它需要±12V电源,现代MCU无法直接驱动。所以必须用MAX232这类芯片,它内部有电荷泵,能把单5V电源升压生成±10V。某次维修一台老式数控机床,对方坚持要用RS-232,我带了MAX3232模块,现场焊接三根线(TX/RX/GND)就搞定,比找原厂线缆快十倍。

  • RS-485电平:工业现场王者,采用差分信号(A/B两线,电压差≥200mV为1,≤-200mV为0)。它不关心单线对地电压,只看两线压差,因此抗共模干扰能力极强(可承受数千伏浪涌),理论距离1200米。但它需要终端匹配电阻(120Ω)和使能控制(DE/RE引脚)。我曾在一个污水处理厂项目中,用RS-485组网连接23个水质传感器节点,总线长达800米,全程未加中继器。秘诀就在:所有节点统一用SN65HVD72芯片(集成失效保护),总线两端各焊一个120Ω贴片电阻,且严格要求施工队用双绞屏蔽线(STP),屏蔽层单端接地。

注意:RS-485是半双工,同一时刻只能发或收。很多新手忘记控制DE引脚,导致发送时接收中断被触发,程序陷入死循环。我的硬性规定:所有RS-485驱动代码,必须在HAL_UART_Transmit()前拉高DE,在HAL_UART_Transmit_IT()回调函数中拉低DE,绝不依赖延时。

3.2 电平转换芯片选型:不只是看价格,更要看“脾气”

市面上电平转换芯片琳琅满目,但选错一颗,足以毁掉整个项目。我总结了四类核心参数,必须逐项核对:

  1. 驱动能力(Drive Strength):指芯片能输出多大电流。RS-232芯片如MAX3232,典型驱动电流±30mA,足够驱动15米线缆;而廉价的SP3232,驱动能力仅±10mA,超过5米就开始误码。某次为某智能家居网关选型,为省钱用了SP3232,量产测试时发现连接3米以上USB延长线就丢包,最终全部返工更换。

  2. ESD防护等级:工业现场静电无处不在。TI的SN65HVD72标称±16kV HBM,而某国产芯片标称±8kV,实测在干燥冬季,后者在插拔瞬间烧毁率达12%。我的原则:凡涉及人工插拔的接口,ESD防护必须≥±12kV。

  3. 失效保护(Fail-Safe Biasing):RS-485总线空闲时,A/B线应保持确定电平(如A>B),否则接收器会随机输出0或1。带失效保护的芯片(如THVD1550)内部集成偏置电阻,无需外部电路。某次调试一个农业大棚环境监测系统,因未用失效保护芯片,阴雨天湿度大,总线浮空导致网关误判所有节点离线,折腾两天才发现是这个细节。

  4. 功耗与温度范围:汽车电子要求-40℃~125℃,而普通工业芯片仅-40℃~85℃。某次为某新能源车企的BMS模块选型,供应商推荐了一款“工业级”芯片,但其数据手册温度范围只到85℃,车辆在新疆夏季暴晒下,PCB表面温度超100℃,芯片批量失效。最终选用ADI的ADM2483,-40℃~125℃全温域保证。

3.3 PCB布局:两条线的学问,决定成败的5%

再完美的电路设计,败在PCB走线上。UART对布线敏感度极高,尤其是高速(>115200)或长距离场景。我坚持的五条铁律:

  1. TX/RX必须等长:长度差≤50mil(约1.27mm)。我用Altium Designer的Length Tuning工具强制约束,避免因延时差异导致采样点偏移。某次为某无人机飞控板Layout,因疏忽让RX线比TX长200mil,地面站发指令时,飞控偶尔失联,示波器显示RX采样点落在数据位边缘。

  2. 远离干扰源:TX/RX线严禁与开关电源电感、MOSFET驱动线、电机线平行走线>1cm。必须垂直跨越,且在交叉处用地平面隔开。某次调试一款激光雕刻机,雕刻时主轴电机启停,串口通信必丢帧。最终发现UART线与电机驱动线在PCB顶层平行走了3cm,加铺地铜并打过孔后解决。

  3. 参考地平面必须完整:TX/RX下方必须是连续的地平面,不能有分割槽。我见过最惨案例:某工程师为“隔离”数字地和模拟地,在UART线正下方开了地分割缝,结果该路串口成为整个系统EMI辐射源,CE认证失败。

  4. 终端匹配(仅RS-485):120Ω电阻必须放在总线物理末端,而非控制器端。我见过太多项目把电阻焊在主控板上,而最后一个节点离主控300米,导致信号反射严重。正确做法:在最远端的节点PCB上,预留120Ω电阻焊盘,出厂时焊接。

  5. 过孔数量最少化:每根信号线过孔≤2个。过孔引入寄生电感,高速信号下形成阻抗不连续点。某次将1Mbps UART从4层板改为2层板,因空间紧张被迫增加过孔,结果误码率从0飙升至8%,最终通过优化布局减少过孔解决。

4. 软件实现:从裸机轮询到RTOS消息队列的演进路径

4.1 裸机开发:轮询、中断、DMA,三种模式的生死抉择

在资源受限的MCU(如STM32F0/Cortex-M0)上,UART软件架构直接决定系统健壮性。没有银弹,只有权衡。

  • 轮询模式(Polling):主循环中不断查询USART_GetFlagStatus(USARTx, USART_FLAG_RXNE)。优点是代码简单,无中断开销;缺点致命——CPU被完全占用,无法响应其他事件。我只在两种场景用它:一是Bootloader中,为确保固件升级绝对可靠,宁可牺牲实时性;二是超低功耗待机唤醒后,用轮询快速读取一次配置命令,然后立即休眠。某次为某水表项目写低功耗代码,客户要求“收到指令后10ms内响应”,我若用轮询,CPU永远无法进入STOP模式,电池半年就耗尽。

  • 中断模式(Interrupt):RXNE中断触发时,将接收到的字节存入环形缓冲区(Ring Buffer)。这是最主流方案。关键在缓冲区大小设计:太小(如32字节)易溢出,太大(如2KB)浪费RAM。我的经验公式:Buffer Size = (Expected Max Frame Length) × 2 + 32。例如Modbus RTU帧最长256字节,则缓冲区至少544字节。更关键的是中断服务程序(ISR)必须极简——只做“收”和“存”,绝不做解析、计算、发指令。某次在某电梯控制板上,ISR里直接调用浮点运算库,导致中断嵌套时栈溢出,整机重启。

  • DMA模式(Direct Memory Access):让DMA控制器直接搬运数据,CPU全程不参与。适合大数据量、高实时性场景(如高速ADC串口上传)。但陷阱极深:DMA传输完成中断(TC)和半传输中断(HT)的配合、缓冲区切换的原子性、以及最重要的——DMA和CPU对同一内存区域的访问冲突。我曾用STM32F4的UART+DMA传图像数据,因未开启DMA的Memory Increment Mode,DMA始终往同一地址写,结果只收到第一行像素。解决方案:启用DMA_MemoryInc_Enable,并用双缓冲(Ping-Pong Buffer)机制,一个缓冲区DMA填充时,CPU解析另一个。

实操心得:无论哪种模式,必须实现“超时帧结束判断”。UART没有帧头帧尾,靠什么知道一帧完了?我的方法:在环形缓冲区中,记录每个字节接收的时间戳(SysTick计数器),当新字节与前一字节时间差 >1.5 × (1/BaudRate) × (DataBits + StopBits)时,即判定为新帧开始。这比单纯查“接收完成标志”可靠十倍,尤其在数据流不连续时。

4.2 协议解析:从原始字节流到可执行指令的魔法

收到字节流只是开始,解析出有效指令才是目的。我摒弃了复杂的协议栈,坚持“状态机+超时”这一古老而可靠的范式。以解析一个自定义的设备控制指令为例(格式:[SOH][CMD][LEN][DATA][CHK]):

typedef enum { STATE_WAIT_SOH, STATE_GET_CMD, STATE_GET_LEN, STATE_GET_DATA, STATE_GET_CHK } ParseState; ParseState state = STATE_WAIT_SOH; uint8_t rx_buffer[256]; uint16_t rx_index = 0; uint8_t expected_len = 0; void UART_Parse_ISR(uint8_t byte) { switch(state) { case STATE_WAIT_SOH: if(byte == 0x01) { // SOH rx_index = 0; rx_buffer[rx_index++] = byte; state = STATE_GET_CMD; timeout_start(); // 启动超时计时器 } break; case STATE_GET_CMD: rx_buffer[rx_index++] = byte; state = STATE_GET_LEN; break; case STATE_GET_LEN: expected_len = byte; rx_buffer[rx_index++] = byte; if(expected_len == 0) { state = STATE_GET_CHK; } else { state = STATE_GET_DATA; } break; case STATE_GET_DATA: rx_buffer[rx_index++] = byte; if(--expected_len == 0) { state = STATE_GET_CHK; } break; case STATE_GET_CHK: rx_buffer[rx_index++] = byte; if(Checksum_OK(rx_buffer, rx_index)) { Execute_Command(rx_buffer); } state = STATE_WAIT_SOH; // 重置 break; } // 超时处理:若任意状态停留超时,强制回到STATE_WAIT_SOH if(timeout_expired()) { state = STATE_WAIT_SOH; rx_index = 0; } }

这个状态机的威力在于:它不依赖精确的帧间隔,能自动从乱码中恢复。某次某工厂产线设备受变频器干扰,串口线收到大量随机噪声,但此状态机仍能稳定捕获有效指令,而基于“固定帧长”的解析方案则完全崩溃。

4.3 RTOS环境:消息队列与信号量的协同艺术

在FreeRTOS/RT-Thread等系统中,UART常被拆分为“接收任务”和“协议处理任务”。关键是如何传递数据而不丢帧。我坚决反对“全局数组+标志位”这种裸机思维。正确姿势是:

  • 接收任务(UART_RX_Task):在UART ISR中,将接收到的字节xQueueSendFromISR()到一个高优先级队列(如uart_rx_queue),队列深度设为256。
  • 协议任务(Protocol_Task):xQueueReceive()从队列取字节,用前述状态机解析。但这里有个陷阱:xQueueReceive()是阻塞的,若队列为空,任务会挂起,导致无法及时响应其他事件。我的解法是:用xQueuePeek()非阻塞查看队列头部,结合vTaskDelay(1)做轻量级轮询,既保证实时性,又避免忙等耗电。
  • 流量控制(Flow Control):当协议任务处理不过来,队列即将满时,必须通知发送方暂停。硬件流控(RTS/CTS)最可靠,但需额外引脚。软件流控(XON/XOFF)更通用。我的实现:当uxQueueMessagesWaiting(uart_rx_queue) > QUEUE_HIGH_WATER_MARK(如200)时,向发送方发0x13(DC3, XOFF);当队列水位降至QUEUE_LOW_WATER_MARK(如50)时,发0x11(DC1, XON)。某次为某医疗设备做远程升级,正是靠这套流控,让手机APP能稳定推送2MB固件包,零丢包。

5. 调试与排障:示波器、逻辑分析仪和“人肉嗅探”的黄金组合

5.1 示波器:看懂波形,比读手册更重要

万用表只能告诉你“有电没电”,而示波器能告诉你“电是怎么变的”。调试串口,我必看三处波形:

  1. TX线空闲电平:应为稳定的高电平(TTL)或负电压(RS-232)。若看到缓慢漂移(如TTL电平从3.3V降到2.8V),说明电源带载能力不足或地线阻抗过大。某次某4G模块串口异常,示波器显示TX空闲电平仅2.1V,查PCB发现电源走线过细,加粗后解决。

  2. 起始位下降沿:应陡峭无过冲。若出现明显振铃(ringing),说明阻抗不匹配或线缆过长。此时需在TX端串联一个22Ω~100Ω的源端匹配电阻。我常用33Ω,它能吸收大部分反射能量,又不至于过度衰减信号。

  3. 数据位采样点:用示波器光标,测量每个数据位中间时刻的电压。它必须清晰落在高/低电平平台中央。若靠近跳变沿,说明波特率设置错误或时钟不准。某次为某客户定制板调试,客户坚持波特率是115200,但示波器显示采样点总在数据位边缘晃动,最后发现是客户提供的晶振标称频率8.000MHz,实测为7.992MHz,误差达0.1%,远超UART容忍极限。

5.2 逻辑分析仪:捕捉“看不见”的时序漏洞

示波器看模拟特性,逻辑分析仪(LA)看数字时序。它是我排查“间歇性丢帧”的终极武器。典型用法:

  • 触发条件设置:不设简单边沿触发,而用“协议触发”——例如“捕获所有包含0x55 0xAA 0xFF序列的帧”。某次某传感器偶发上报错误数据,用LA抓取10万帧,发现只有第8732帧的CHK字节是0x00(应为0x55),顺藤摸瓜找到是ADC采样时钟与UART发送时钟存在微妙相位差,导致DMA搬运时读到未更新的寄存器值。

  • 时序分析:LA能精确测量TX与RX之间的延迟。我曾用Saleae Logic Pro 16测过STM32与ESP32间的UART交互:从STM32发完最后一字节到ESP32 RX中断触发,平均延迟12.3μs,但有3%的样本延迟达45μs。追查发现是ESP32的WiFi任务抢占了UART中断,最终通过调整FreeRTOS任务优先级解决。

5.3 “人肉嗅探”:用最笨的方法,解决最诡异的问题

当仪器也束手无策时,回归原始。我称之为“人肉嗅探”:

  • 逐字节打印:在关键路径插入printf("RX: 0x%02X\r\n", byte);,把每个收到的字节都打出来。某次某Modbus从机响应异常,打印后发现主机发来的地址字节总是0x00,查PCB发现地址跳线帽虚焊,万用表导通档测是通的,但微小接触电阻在高速通信时造成压降。

  • 反向注入:不从MCU发,而用信号发生器或另一块开发板,按协议规范,手工构造一帧数据注入RX线,观察MCU是否正确解析。这能100%排除“发送端问题”,聚焦在接收逻辑。某次某客户坚称我们的模块有问题,我带一块Arduino Nano,用Serial.write()发标准Modbus帧,他们的MCU完美响应,问题自然指向他们自己的发送代码。

  • 环境复现:把设备搬到问题现场。某次某车载终端在实验室完美,上车后频繁断连。带到客户车上,用万用表测得GND与车身搭铁间有0.8V压差,原来客户把终端GND接到座椅金属架,而非蓄电池负极。加装DC-DC隔离模块后解决。

最后分享一个血泪教训:某次为某军工项目调试,所有测试都通过,交付前最后一次复测,发现高温老化后串口失灵。查了三天,最后发现是PCB上一颗0805封装的100nF退耦电容,在85℃时容量衰减50%,导致UART外设供电纹波超标。解决方案:换用X7R材质、1206封装的电容,高温下容量变化<15%。硬件的可靠性,永远藏在那些不起眼的“小东西”里。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 1:32:30

树莓派避障小车实战:从超声波选型到状态机调参全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:32:29

ALIENTEK MiniSTM32例程包深度解析:从跑马灯到工程模板的避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:32:14

电商后台系统稳定性实战:从架构到监控的全链路保障

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:31:50

物联网协议选型实战:MQTT、TCP、HTTP 深度对比与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:31:49

微信小微商户图片上传全链路解析:从失败率72%到98.7%

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:31:43

PAT-A-1024回文数题解:高精度加法与字符串模拟实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华