news 2026/9/16 1:32:08

STM32F103标准库UART串口通信实验详解:从初始化到中断接收

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103标准库UART串口通信实验详解:从初始化到中断接收

简介:面向嵌入式开发初学者,这是一份基于STM32F103C8T6芯片的UART串口通信标准库实验工程,通过串口输入1、2或3触发不同输出,直观演示串口初始化、参数配置以及数据收发等完整流程。工程在Keil环境下可直接编译下载,适合课堂实验、毕业设计或自学练手,也可作为移植到其他STM32F1系列项目的参考模板。资源包共175个文件,约5.78MB,按标准库工程结构组织,包括29个C源文件、30个头文件,以及启动文件、链接脚本、Keil工程配置和编译中间文件、hex烧录文件等,便于源码阅读与直接烧录验证。目前已有2645人学习下载,通过对照代码与实际运行,可掌握USART的GPIO复用配置、波特率设置、中断或轮询收发方式,并了解标准库工程的文件组织与构建流程,为串口类应用开发打下基础。整个实验体量适中,便于反复调试和扩展,也可作为后续学习其他外设驱动的参考骨架。

1. STM32F103 标准库 Uart 实验开篇:串口是嵌入式联调的地基

刚把 stm32f103 最小系统点亮一颗 LED,下一个实验几乎必然是 Uart 串口通信。串口这跟线承担着调试打印、传感器读取、上位机指令交互这些最基础的通信任务,STM32 标准库 V3.5 下配置串口的套路在 F1 全系列里高度统一,学会一次就能迁移到 F407、F429 上。很多人以为串口实验就是把波特率配对,真正上手才发现乱码、丢字节、收不到数据,问题往往不在波特率,而在 GPIO 复用模式配错、RCC 时钟总线使能错,或者 PC 端的 USB 转串口驱动没装对。

下面按“硬件准备 → 参数初始化 → 轮询收发 → 中断接收 → 排错验证”的实际开发顺序展开,所有代码基于 STM32 标准库,可以直接抄进自己的工程。适合刚建好标准库工程的新手,也适合从寄存器或 HAL 库转过来想对照差异的工程师。

2. STM32F103 最小系统与标准库工程的串口硬件准备

2.1 USART1 引脚映射与交叉接线

STM32F103 系列最常用的调试串口是 USART1,默认引脚映射在 PA9(TX)和 PA10(RX)。绝大多数最小系统板把这组引脚单独引出,接 USB 转 TTL 模块时有一个最高频的接错点:模块的 TXD 必须接板子的 RXD(PA10),模块的 RXD 接板子的 TXD(PA9),也就是“发对收、收对发”。接成同名的 TX 对 TX,用示波器量电平完全正常,数据就是出不去。

F103 的串口资源不止 USART1 一个,做多串口项目前先对照一下外设挂载和引脚映射,避免初始化时张冠李戴。

串口外设挂载总线默认 TX/RX 引脚重映射 TX/RX时钟使能函数
USART1APB2,72MHzPA9 / PA10PB6 / PB7RCC_APB2PeriphClockCmd
USART2APB1,36MHzPA2 / PA3PD5 / PD6RCC_APB1PeriphClockCmd
USART3APB1,36MHzPB10 / PB11PD8 / PD9RCC_APB1PeriphClockCmd

在这个表里能看到 stm32f103 串口1和串口3使用差异的根源:USART1 挂在 APB2 总线上,主频 72MHz;USART2 和 USART3 挂在 APB1 总线上,主频最大 36MHz。同样配置 115200 波特率,USART1 和 USART3 在 BRR 寄存器里写入的分频值完全不同。初始化时如果把 USART1 的时钟使能误写成 RCC_APB1PeriphClockCmd,向时钟位写值会失败,后续所有 USART 寄存器操作都无效,表现就是串口完全没反应。

2.2 标准库 V3.5 的工程结构与系统时钟

标准库 V3.5(网上常搜到的 stm32f103 库 v3.50 下载就是这个版本)新建工程的骨架由几个固定文件组成:main.c 写应用逻辑,stm32f10x_it.c 放中断服务函数,stm32f10x_conf.h 做外设头文件包含,system_stm32f10x.c 负责时钟初始化,启动文件根据芯片容量选择,F103ZET6 这类高密度芯片用 startup_stm32f10x_hd.s。main 函数里由 SystemInit() 把系统时钟配置到 72MHz,SystemCoreClock 全局变量等于 72000000。

分频计算直接依赖这个 72MHz。USART 的 BRR 寄存器计算公式是 USARTDIV = PCLK / (16 * 波特率),当 PCLK=72MHz、波特率=115200 时,USARTDIV=39.0625,BRR 写成 0x271。如果外部晶振不是 8MHz,或者板子用了内部 HSI 时钟,PCLK 就不是 72MHz,同样的 BRR 值实际产生的波特率会偏移,对端自然收出乱码。排查串口乱码时先确认板子晶振频率,再看 system_stm32f10x.c 里的 PLL 倍频配置。

2.3 GPIO 复用推挽与浮空输入的初始化代码

#include "stm32f10x.h" void UART1_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; /* 使能 GPIOA 与 USART1 时钟。 串口1挂在 APB2 总线上,主频 72MHz,所以用 APB2 的时钟使能函数。 如果换成 USART3,这行要改成 RCC_APB1PeriphClockCmd(...) */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); /* TX 引脚 PA9 配置为复用推挽输出 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); /* RX 引脚 PA10 配置为浮空输入 */ GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); }

TX 引脚用 GPIO_Mode_AF_PP 而不是 GPIO_Mode_Out_PP,原因是复用推挽模式把引脚控制权交给片上外设,USART 移位寄存器输出的 TX 信号才能接到引脚上;普通推挽输出由 ODR 寄存器人工控制,配成这种模式后 USART 发送的数据根本到不了 PA9。RX 配浮空输入是官方例程的默认做法,如果外部设备输出端是开漏结构,也可以改用上拉输入,但会轻微影响电平边沿质量,调试期建议先保持浮空。

提示:如果最小系统的串口引脚不是 PA9/PA10,而是 PB6/PB7,除了 GPIO 时钟外还要单独使能 AFIO 时钟,并调用 GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE),否则重映射后的引脚同样不工作。

3. 标准库 Uart 通信实验:USART 初始化的参数与轮询收发

3.1 USART_Init 的六个结构体成员拆解

void UART1_Init(uint32_t baudrate) { USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate = baudrate; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); USART_Cmd(USART1, ENABLE); } int main(void) { SystemInit(); UART1_GPIO_Config(); UART1_Init(115200); while (1) { /* 主循环里做轮询收发 */ } }

每个成员单独说清楚:

  • USART_BaudRate = 115200。库内部根据 PCLK 自动计算 BRR 寄存器值,但 PCLK 取值来自 SystemCoreClock 全局变量。如果工程里手动改过时钟初始化,比如把外部晶振倍频数从 9 改成 18,SystemCoreClock 没有同步更新,算出来的 BRR 就是错的。手动改时钟后要检查 system_stm32f10x.c 里的 SystemCoreClock 更新逻辑。
  • USART_WordLength = 8b,指 8 个数据位。开启奇偶校验时这里必须改成 9b,原因见 3.2 的参数表。
  • USART_StopBits = 1,常规配置。对端停止位设 2,F103 这边仍按 1 位停止位发送也能通信,但反过来不行。
  • USART_Parity = No。串口帧的校验位会占用数据位长度,这一点最容易踩。
  • USART_HardwareFlowControl = None。只有板子引出 RTS/CTS 引脚时才需要打开,普通三线制串口直接关闭。
  • USART_Mode = Rx | Tx,收发都开。只调发送可以只写 USART_Mode_Tx,但实际调试中几乎没有只发不收的场景。

3.2 数据位、停止位、校验位的参数组合

应用场景WordLengthParityStopBits实际帧结构
常规调试,8N18bNo11起始 + 8数据 + 1停止
Modbus RTU,8E19bEven11起始 + 8数据 + 1校验 + 1停止
与老式设备对接8bNo21起始 + 8数据 + 2停止
9 位多机通信9bNo11起始 + 8数据 + 1地址/标志位

USART_Parity 配成 Even 或 Odd 时,标准库不会自动调整 WordLength,得手动把 WordLength 改成 9b。9 位里低 8 位是数据,第 9 位是校验位,硬件自动计算和插入。如果只改 Parity 不改 WordLength,发送端会产生 10 位帧,接收端按 9 位解析,帧边界全部错位,表现就是偶发乱码且无法通过调整波特率解决。上电联调第一个动作是确认 PC 端串口助手的校验位和停止位设置,和单片机完全一致后再谈乱码问题。

3.3 轮询发送:判断 TXE 还是 TC

void UART1_SendByte(uint8_t byte) { /* 等待发送数据寄存器空。TXE=1 说明 TDR 已空,可以写入下一字节 */ while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, byte); }

USART 有两个发送相关标志位最容易混。TXE 表示 TDR 里的数据已经搬到移位寄存器,TDR 空出来了;TC 表示移位寄存器把这一帧的停止位也发送完毕。发单个字节等 TXE 就够,因为 TDR 空出后立刻写入下一字节,不会覆盖当前正在移位的数据。但半双工总线场景必须等 TC,典型例子是 RS485 方向控制:

void UART1_SendByte_RS485(uint8_t byte) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); USART_SendData(USART1, byte); /* RS485 方向引脚必须在整帧发送完后才能释放总线, 只等 TXE 会导致最后一位还在移位寄存器里时总线已被切换 */ while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); GPIO_SetBits(GPIOC, GPIO_Pin_4); /* 释放总线 */ }

这里的 GPIO_SetBits 只是示意,实际方向控制引脚的拉高拉低电平取决于收发器芯片。核心逻辑是:TXE 置位时数据还没发完,TC 置位才代表物理层发送完毕。需要精确控制总线切换时间的场景,一律等 TC。

3.4 轮询接收与 ORE 溢出丢帧

uint8_t UART1_ReceiveByte(void) { /* 等待接收数据寄存器非空。RXNE=1 表示 RDR 已收到一帧数据 */ while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET); return (uint8_t)USART_ReceiveData(USART1); }

轮询接收的硬伤是等待期间 CPU 被完全占住。上位机连续发来两帧,第一帧被读走之后,软件还没回到 while 循环开头,第二帧已经到达 RDR,此时 RXNE 尚未清除,新的数据把旧数据覆盖,硬件置 ORE 溢出标志。读 DR 读到的还是上一帧数据,新帧直接丢失。

这一点决定了轮询方式只适合一问一答式的短交互,比如 Modbus 主站查询后从站应答。连续高速数据流必须换中断接收。

提示:排查丢帧时先看 ORE 标志是否被置位,在接收到预期数据后执行 if (USART_GetFlagStatus(USART1, USART_FLAG_ORE) != RESET) 并计数,连续出现 ORE 说明数据到达速率超过消费速率,调高主循环处理效率或改中断接收才是正路。

4. STM32F103 Uart 通信实验进阶:中断接收与不定长帧解析

4.1 NVIC 初始化与 USART_ITConfig 的先后顺序

NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); /* 2位抢占优先级 + 2位子优先级 */ NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); /* 必须在 NVIC 配置完成后使能串口中断 */ USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

NVIC_PriorityGroupConfig 必须在任何 NVIC_Init 之前调用,否则分组设置无效。抢占优先级和子优先级的分配取决于整个工程的中断负载:如果同时有定时器中断和 DMA 中断在跑,串口中断的抢占优先级建议放高,抢占优先级数值越小优先级越高,给 0 或 1 都可以,定时器放后面。中断里如果还做了浮点运算或长耗时操作,抢占优先级再高也会拖慢主循环,所以中断里只做数据搬运,业务处理放主循环。

中断源选择看具体需求。RXNE 负责收数据,TC 负责发送完成回调,ORE 负责溢出恢复。对帧丢失敏感的应用建议把 USART_IT_ORE 也打开,溢出中断里做状态复位。

4.2 中断服务函数的标志位清理规则

uint8_t rx_buf[128]; volatile uint16_t rx_len = 0; void USART1_IRQHandler(void) { uint8_t byte; if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { byte = (uint8_t)USART_ReceiveData(USART1); /* 读 DR 自动清 RXNE */ if (rx_len < sizeof(rx_buf)) { rx_buf[rx_len++] = byte; } } if (USART_GetITStatus(USART1, USART_IT_ORE) != RESET) { /* 溢出后先读 SR 再读 DR,把残留数据取走,清除 ORE */ (void)USART_GetFlagStatus(USART1, USART_FLAG_ORE); (void)USART_ReceiveData(USART1); rx_len = 0; } }

标准库里 USART_GetITStatus 和 USART_GetFlagStatus 的差别在于:IT 版本同时检查 SR 标志位和 CR 寄存器里的中断使能位,FLAG 版本只查 SR。中断服务里统一用 IT 版本,避免外设中断根本没开启时误入处理分支。

RXNE 标志的清除方式是读 DR,读完自动清。不要在中断里再调 USART_ClearITPendingBit(USART1, USART_IT_RXNE),标准库的这个清除操作对 RXNE 是无效的,还会多一次写 CR 操作。ORE 清除比较特殊,必须先读 SR 再读 DR,两者顺序反了清不掉。

中断源触发条件清除方式典型用途
RXNE接收数据寄存器非空读 DR 自动清除收数据
TC一帧发送完成读 SR 后写 DR,或 ClearFlag发送完成钩子
ORE接收溢出先读 SR 再读 DR溢出恢复
IDLE总线空闲超过一帧时间先读 SR 再读 DR不定长帧收尾

4.3 用状态机解析不定长数据帧

中断接收解决了不丢字节的问题,但字节流本身没有边界。上位机发来长度不定的命令帧时,常见做法是用状态机逐字节推进:帧头 0xAA 0x55,接下来是数据长度,然后数据区,最后是累加校验和。

typedef enum { FRAME_WAIT_HEAD1, FRAME_WAIT_HEAD2, FRAME_WAIT_LEN, FRAME_WAIT_DATA, FRAME_WAIT_CHECK } FrameState; FrameState state = FRAME_WAIT_HEAD1; uint8_t frame_buf[64]; uint8_t frame_cnt = 0; uint8_t frame_len = 0; uint8_t check_sum = 0; void process_byte(uint8_t b) { switch (state) { case FRAME_WAIT_HEAD1: if (b == 0xAA) state = FRAME_WAIT_HEAD2; break; case FRAME_WAIT_HEAD2: if (b == 0x55) state = FRAME_WAIT_LEN; else if (b == 0xAA) state = FRAME_WAIT_HEAD2; /* 连续两个 0xAA 时保持等待 */ else state = FRAME_WAIT_HEAD1; break; case FRAME_WAIT_LEN: if (b > 0 && b <= 60) { frame_len = b; frame_cnt = 0; check_sum = b; state = FRAME_WAIT_DATA; } else { state = FRAME_WAIT_HEAD1; /* 长度非法,重新找帧头 */ } break; case FRAME_WAIT_DATA: frame_buf[frame_cnt++] = b; check_sum += b; if (frame_cnt == frame_len) state = FRAME_WAIT_CHECK; break; case FRAME_WAIT_CHECK: if (check_sum == b) { /* 帧校验通过,在 frame_buf 里解析业务数据 */ } state = FRAME_WAIT_HEAD1; break; } }

把 process_byte 挂到中断服务里,RXNE 分支收到字节后直接调用。状态机的核心收益是:不管上位机以什么速度发帧,只要字节不丢、不乱序,状态就能正确推进。帧头出现错误字节时,通过状态回退保证重新同步,这是最基础但最实用的容错设计。

如果上位机发的是 ASCII 字符串命令,比如 AT+STATUS\r\n,也可以用 strcmp 直接比较。要注意 stm32f103 里 strcmp 依赖字符串结尾的 \0,而串口收到的原始数据没有结尾,必须在接收缓冲区末尾手动补 \0 再比较,否则 strcmp 会越界读到不确定内容。比较之前先做 \r\n 定界处理,比直接在整个缓冲区上 strcmp 更可靠。

4.4 环形缓冲区替代简单数组

简单数组接收有个边界问题:不定长帧什么时候算收完。一个常见做法是环形缓冲区加 IDLE 中断,空闲超时代表一帧结束。环形缓冲的实现用 256 字节配合掩码取模,比取模运算快很多。

#define RX_RING_SIZE 256 #define RX_RING_MASK 0xFF volatile uint8_t rx_ring[RX_RING_SIZE]; volatile uint16_t rx_head = 0; volatile uint16_t rx_tail = 0; /* 中断里写入,缓冲区被写满时覆盖最旧数据 */ void ring_push(uint8_t b) { rx_ring[rx_head] = b; rx_head = (rx_head + 1) & RX_RING_MASK; } /* 主循环里读取,返回 0 表示没有新数据 */ uint8_t ring_pop(uint8_t *byte) { if (rx_tail == rx_head) { return 0; } *byte = rx_ring[rx_tail]; rx_tail = (rx_tail + 1) & RX_RING_MASK; return 1; }

head 和 tail 相等代表缓冲区为空。这个实现不做防覆盖,适合帧速率不高的调试场景。要防覆盖,在 push 里判断 ((rx_head + 1) & RX_RING_MASK) == rx_tail,相等说明缓冲区已满,这时丢弃新字节并置一个溢出计数,供主循环查询。注意 RX_RING_SIZE 必须是 2 的整数次幂,掩码才能用 & 来代替 %,改成 512 或 1024 时掩码要同步改成 0x1FF、0x3FF。

5. Uart 串口通信实验的故障排查与波形验证技巧

5.1 回环测试定位是哪一端的问题

把 PA9 和 PA10 用杜邦线短接,程序上电后发送 0x5A,随后轮询接收,把收到的字节再发回 PC。短接时能自收自发,说明单片机侧的 GPIO 配置、USART 初始化、波特率都是对的,问题在单片机与 PC 之间的链路。接着去掉短接,接上 USB 转 TTL 模块。

USB 转 TTL 模块常用 FT232R 或 CP2102 芯片,这两类芯片在 Windows 下如果设备管理器看不到 COM 口,先装芯片厂商的官方驱动,Windows 10 以上系统通常能自动识别,但部分精简版系统需要手动指定驱动路径。CH340 芯片的驱动名和 FT232R 不一样,不要装错。设备管理器出现 COM 口后再打开串口助手,波特率、停止位、校验位全部与单片机一致,发送 0x5A,接收区能看到同样字节就说明整条链路通。

回环测试有个陷阱:PA9 和 PA10 短接后,发送端和接收端是同一个芯片的同一个 USART,哪怕两边波特率配置不一致也能收到数据,因为收发共用同一个 BRR。所以回环测试通过不能完全证明波特率准确,只能证明外设本身没坏。

5.2 用逻辑分析仪看波特率误差

逻辑分析仪夹在 PA9 上,抓一段实际发送波形。一帧的标准结构是 1 位起始位低电平、8 位数据位、1 位停止位高电平,共 10 个位时间。量出起始位下降沿到停止位结束的总时间,除以 10 得到单个位时间,做 1000000 / 位时间 换算就能得到实际波特率。

实际波特率与设定值偏差在 ±2% 以内基本能正常通信,超过 ±3% 就会出现偶发误码。自制最小系统的晶振不是 8MHz,或晶振负载电容不匹配导致频率偏移,在逻辑分析仪上一目了然。检查时把示波器探头或逻辑分析仪的地线夹在板子地,不要夹在 USB 转 TTL 模块的地上,两边参考地不一致时测出的波形是浮动的,数值没有参考意义。

5.3 十六进制打印对比字节序

调试多字节协议时,按 ASCII 字符串打印会把 0x00 显示成空字符,0xA0 这类数据被当成扩展字符,帧边界完全看不清。写一个十六进制打印函数,把缓冲区内容逐字节转成两个十六进制字符输出:

void uart_print_hex(uint8_t *buf, uint16_t len) { uint8_t hex[] = "0123456789ABCDEF"; uint16_t i; for (i = 0; i < len; i++) { UART1_SendByte((uint8_t)hex[(buf[i] >> 4) & 0x0F]); UART1_SendByte((uint8_t)hex[buf[i] & 0x0F]); UART1_SendByte(' '); } UART1_SendByte('\r'); UART1_SendByte('\n'); }

用这个方法观察收到的帧头、长度字段和校验和,能快速定位帧格式错误。另一个高频问题现象是串口收到的整数高低字节颠倒:F103 是 Cortex-M3 小端核,上位机按大端发送 0x12 0x34,单片机里按 uint16_t 读出来却是 0x3412。把发送端和接收端的打印都切到十六进制,两端对比字节顺序,这类问题几秒钟就能确认。串口协议里多字节整数建议统一按大端传输,也就是高字节先发,MCU 端解析时手动拼装,避免不同平台字节序不一致带来的移植问题。

本文还有配套的精品资源,点击获取

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

麒麟Kylin V10系统yum源更换教程:从阿里云到本地源全攻略

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

作者头像 李华
网站建设 2026/9/16 1:29:25

Linux上安装RustFS:DEB与RPM包完整实战指南

在 Linux 上装一个软件&#xff0c;听起来好像不是什么大事&#xff0c;下载、解压、运行&#xff0c;三步走完。可一旦这个软件是存储组件&#xff0c;是要塞进生产环境长期跑下去的&#xff0c;事情就没那么简单了。最近我在帮团队评估 RustFS&#xff0c;准备把它作为对象存…

作者头像 李华
网站建设 2026/9/16 1:29:17

用MATLAB构建海底地形模型:坐标提取、插值与重采样全流程

简介&#xff1a;面向涉海专业课程设计与毕业设计的MATLAB海底地形模拟器小型源码包&#xff0c;适合具备基础MATLAB操作经验、希望快速搭建水下地形三维仿真原型的读者。资源共6个文件&#xff0c;以4个M脚本为核心&#xff0c;分别承担地图坐标提取、分辨率转换、地形生成与主…

作者头像 李华
网站建设 2026/9/16 1:29:00

JWT 验签不过?Codex 连上 TaoToken 后能一次查清 Signature 和 exp

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

作者头像 李华
网站建设 2026/9/16 1:28:50

二叉树算法实战:遍历与构造技巧解析

1. 二叉树算法实战&#xff1a;从基础遍历到构造应用今天我想和大家分享几个二叉树相关的经典算法题目&#xff0c;这些题目在面试和日常编码中经常出现。作为一名经历过多次算法面试的老手&#xff0c;我深知掌握这些题目对提升编程能力的重要性。我们将从513题"找树左下…

作者头像 李华
网站建设 2026/9/16 1:28:36

DESeq2差异分析可视化:5分钟绘制发表级火山图与热图

拿到DESeq2的差异分析结果&#xff0c;不少人卡在最后一公里——表格里几万行基因&#xff0c;padj、log2FoldChange一堆数字&#xff0c;完全不知道从哪看起&#xff0c;更别说画出一张能放进文章里的图。其实差异分析本身只是第一步&#xff0c;把结果看懂、把图做出来才是真…

作者头像 李华