news 2026/10/7 14:59:32

STM32嵌入式开发全解析:从内核架构到实战避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32嵌入式开发全解析:从内核架构到实战避坑指南

STM32 这个名字,在嵌入式圈子里几乎是绕不开的。不管你是刚入行的电子专业学生,还是做了几年硬件转软件的工程师,只要碰过 MCU,大概率第一块板子就是 STM32。但很多人对它的理解停留在“库函数能跑就行”的层面,一旦遇到换芯片、调时钟、排查通信故障,就抓瞎了。这篇内容我打算从实际使用的角度,把 STM32 到底是什么、为什么它成了事实标准、以及围绕它的一整套开发体系讲清楚。适合刚接触嵌入式的新手建立全局认知,也适合有经验的开发者查漏补缺——尤其是那些从标准库转到 HAL 库、或者从 STM32F1 迁移到 F4/H7 系列时踩过坑的人。

1. STM32 到底是个什么东西:从芯片架构说起

1.1 Cortex-M 内核与 STM32 的关系

很多人第一次听到“STM32 是基于 ARM Cortex-M 内核”这句话时,并不真正理解它的含义。简单说,ARM 是一家设计处理器内核架构的公司,它自己不生产芯片,而是把内核设计授权给各家半导体厂商。ST(意法半导体)就是拿到授权后,在内核外面加上自己的闪存、SRAM、外设(GPIO、UART、SPI、I2C、ADC、定时器等),再封装成一颗完整的 MCU,这就是 STM32。

所以你会看到,同样是 Cortex-M3 内核,ST 有 STM32F1,NXP 有 LPC1700,TI 有 Stellaris。它们的内核指令集一样,但外设设计、时钟树、寄存器映射完全不同。这意味着你从 STM32F1 换到另一家的 M3 芯片,内核相关的代码(比如中断向量表的结构)概念相通,但外设驱动基本要重写。

Cortex-M 系列目前主流的有 M0、M0+、M3、M4、M7、M33 等。STM32 的产品线也据此划分:

内核典型系列主频范围特点
Cortex-M0/M0+STM32F0、L0、G048MHz 左右低成本、低功耗,适合简单控制
Cortex-M3STM32F1、L172-120MHz经典通用型,资料最丰富
Cortex-M4STM32F3、F4、L4、G480-180MHz带 FPU 和 DSP 指令,适合信号处理
Cortex-M7STM32F7、H7200-480MHz+高性能,带 Cache,适合复杂算法
Cortex-M33STM32L5、U5110-160MHz带 TrustZone,适合安全场景

选型时不要只看主频。比如你要做电机 FOC 控制,M4 的 FPU 和 DSP 指令能大幅加速运算,比单纯堆主频的 M7 更划算;如果只是读几个传感器、控制继电器,M0+ 就够了,没必要上 F4。

1.2 冯诺依曼与哈佛架构在 STM32 上的体现

Cortex-M3/M4/M7 采用哈佛架构的变体——指令总线和数据总线分开,但地址空间统一编址。这带来的实际好处是:取指令和读数据可以并行,执行效率比纯冯诺依曼架构高。你在写代码时感知不到这个区别,但在做高频中断处理或 DMA 传输时,总线仲裁机制会影响实际性能。

举个例子,当 DMA 在搬运 ADC 采样数据到内存时,CPU 同时从 Flash 取指令执行,这两条总线不冲突,所以 DMA 不会明显拖慢 CPU。但如果 DMA 和 CPU 同时访问同一块 SRAM,就会产生总线等待周期。这也是为什么在高性能场景下,ST 会把 SRAM 分成多个块(比如 F4 有 CCM RAM、SRAM1、SRAM2),不同总线矩阵连接不同区域,减少冲突。

1.3 存储器映射与启动模式

STM32 的 4GB 地址空间被划分成几个固定区域:Flash 从 0x08000000 开始,SRAM 从 0x20000000 开始,外设寄存器从 0x40000000 开始。这个映射是内核规定的,所有 Cortex-M 芯片都遵循类似布局。

启动模式由 BOOT0 和 BOOT1 引脚决定:

  • BOOT0=0:从主 Flash 启动,这是正常运行时的状态。
  • BOOT0=1, BOOT1=0:从系统存储器启动,也就是跑 ST 出厂固化的 Bootloader,用于串口下载程序。
  • BOOT0=1, BOOT1=1:从内嵌 SRAM 启动,一般用于调试。

实际项目中,我习惯把 BOOT0 通过 10k 电阻下拉到地,需要串口下载时再用跳线帽短接到 VCC。这样正常上电永远从 Flash 跑,不会因为引脚悬空导致意外进入 Bootloader。

2. 开发环境搭建:从 Keil 到 VSCode 的路线选择

2.1 Keil MDK 为什么仍是很多人的首选

Keil MDK 在国内嵌入式教学和中小企业的占有率极高,原因很实际:安装简单、器件包一键下载、调试器集成度高、中文资料多。但它的坑也很集中。

最常见的问题就是 ARM Compiler 版本。Keil MDK5 默认装的是 ARM Compiler 6(基于 Clang),但大量老工程用的是 ARM Compiler 5(基于 GCC 的 armcc)。当你打开一个旧工程,编译时可能报错:

*** error: 'e:\keil5\arm\bin\sarmcm3.dll' not found

这个错误的本质是工程配置里指定了 AC5 编译器,但你的 Keil 安装目录下没有 AC5 组件。解决办法有两个:一是通过 Keil 的 Pack Installer 安装 ARM Compiler 5(注意版本,常用 5.06 update 7);二是把工程迁移到 AC6,但这需要修改大量编译选项和汇编语法,老工程不建议轻易动。

另一个高频问题是芯片包(Device Family Pack)没装。新建工程时找不到 STM32F103C8 这个型号,就是因为对应的 DFP 没有安装。在 Keil 的 Pack Installer 里搜索 STM32F1,安装对应的 Device Family Pack 即可。

2.2 VSCode + 开源工具链的配置逻辑

越来越多的团队开始用 VSCode 搭配 arm-none-eabi-gcc、OpenOCD、Makefile/CMake 来开发 STM32。这套方案的优势是跨平台、可版本控制、不依赖特定 IDE 的工程文件格式。但配置门槛确实比 Keil 高。

核心组件包括:

  • 编译器:arm-none-eabi-gcc,用于把 C 代码编译成 ARM 机器码。
  • 调试服务器:OpenOCD 或 J-Link GDB Server,负责和硬件调试器通信。
  • 构建系统:Makefile 或 CMake,管理编译链接流程。
  • 链接脚本:.ld 文件,定义 Flash 和 SRAM 的地址分配、堆栈位置、中断向量表布局。

链接脚本是很多人卡住的地方。STM32 的 .ld 文件必须和具体芯片的存储器布局匹配。比如 STM32F103C8T6 有 64KB Flash、20KB SRAM,链接脚本里就要写:

MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K }

如果你换了芯片但忘了改链接脚本,程序可能编译通过但运行异常——比如变量被分配到不存在的 SRAM 地址,或者中断向量表偏移不对导致 HardFault。

2.3 调试器选型:ST-Link、J-Link 与 DAPLink

ST-Link 是 ST 官方调试器,价格便宜(山寨版十几块钱),配合 STM32CubeProgrammer 和 Keil 都能用。缺点是山寨版固件升级容易变砖,而且不支持非 ST 芯片。

J-Link 性能更强,支持芯片范围广,但正版价格高。教育版受限,商业项目要注意授权问题。

DAPLink 是 ARM 官方的开源调试器方案,很多国产开发板自带。它支持拖拽下载(把 bin 文件拖到虚拟 U 盘即可),对新手很友好。

实际选型建议:如果只玩 STM32,ST-Link 足够;如果同时接触多家芯片,J-Link 或 DAPLink 更灵活。注意调试器的 SWD 接口只需要 SWCLK、SWDIO、GND、VCC 四根线,但 VCC 最好接上,让调试器能检测目标板电压。

3. 外设驱动开发中的典型问题与排查思路

3.1 GPIO 配置的隐藏陷阱

GPIO 看似简单,但配置不当会引发各种奇怪现象。STM32 的 GPIO 有 8 种模式:输入浮空、输入上拉、输入下拉、模拟输入、开漏输出、推挽输出、开漏复用、推挽复用。

一个常见错误是:把 LED 接在推挽输出上,但忘了使能对应 GPIO 端口的时钟。STM32 的外设时钟默认关闭,必须通过 RCC 寄存器使能。用 HAL 库的话,__HAL_RCC_GPIOA_CLK_ENABLE()这行不能少。标准库则是RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE)。

另一个坑是复用功能的重映射。比如 STM32F103 的 USART1 默认在 PA9/PA10,但可以重映射到 PB6/PB7。如果你用了重映射却没开启 AFIO 时钟,串口就不会工作。HAL 库中需要调用__HAL_RCC_AFIO_CLK_ENABLE(),然后配置__HAL_AFIO_REMAP_USART1_ENABLE()。

3.2 ADC 多通道切换的数据错乱问题

STM32 的 ADC 支持多通道扫描,但很多人发现切换通道后读到的数据不对。原因通常是采样时间不够。ADC 的采样保持电容需要时间充电,如果信号源内阻较大,采样时间太短会导致读数偏低。

解决办法是增大采样周期。HAL 库中通过ADC_ChannelConfTypeDef.SamplingTime设置,可选 1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5 个 ADC 时钟周期。对于高内阻信号源(比如光敏电阻分压),建议用 239.5 周期。

还有一个细节:切换通道后第一次转换的数据建议丢弃。因为通道切换后,采样电容上可能残留上一个通道的电荷,第一次转换结果不准确。可以在切换通道后先启动一次转换、读走数据丢弃,再启动正式转换。

3.3 CAN 通信突然断连的排查链路

CAN 总线在工业控制和汽车电子中用得很多,但调试时经常遇到“昨天还好好的,今天就连不上”的情况。我整理了一套排查顺序:

  1. 检查终端电阻:CAN 总线两端各需要 120Ω 终端电阻,总共 60Ω。用万用表量 CANH 和 CANL 之间的电阻,如果接近 120Ω,说明只有一端有电阻;如果接近 60Ω,正常;如果接近 0 或无穷大,线路有问题。

  2. 确认波特率一致:所有节点的波特率必须相同。STM32 的 CAN 波特率由 APB1 时钟、预分频、BS1、BS2 共同决定。换晶振或改时钟树后,波特率可能偏移。

  3. 检查 CAN 收发器供电:TJA1050、SN65HVD230 等收发器需要 5V 或 3.3V 供电,如果供电不稳,总线电平异常。

  4. 用示波器看波形:正常 CAN 波形是差分信号,CANH 和 CANL 幅度对称。如果波形畸变,可能是线太长、干扰大或阻抗不匹配。

  5. 查看错误计数器:STM32 的 CAN 外设有错误计数器,通过读取 CAN_ESR 寄存器可以知道是位错误、格式错误还是应答错误。如果 TEC 超过 255,节点会进入 Bus-Off 状态,需要软件恢复。

3.4 定时器捕获测频率的精度问题

用 STM32 定时器捕获模式测外部信号频率时,精度受限于定时器时钟和捕获分辨率。假设定时器时钟 72MHz,预分频 72,则计数频率 1MHz,即分辨率 1μs。测 1kHz 信号,周期 1000μs,误差 ±1μs,相对误差 0.1%,够用。但测 100kHz 信号,周期 10μs,误差 ±1μs,相对误差 10%,就不行了。

提高精度的方法有几种:一是降低预分频,提高计数频率;二是用 PWM 输入模式,同时捕获周期和占空比;三是用两个定时器,一个计数、一个定时,通过计数值和溢出次数计算频率。实际项目中,如果信号频率超过 100kHz,建议用外部计数器芯片或 FPGA 预处理。

4. 从标准库到 HAL 库的迁移实战

4.1 两套库的设计哲学差异

标准库(Standard Peripheral Library)是 ST 早期推出的,直接操作寄存器,代码量小、执行效率高,但可移植性差。HAL 库(Hardware Abstraction Layer)是 ST 现在主推的,抽象层次更高,配合 STM32CubeMX 可以图形化配置,跨系列移植方便,但代码体积大、执行效率略低。

举个例子,配置一个 GPIO 输出:

标准库:

GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure);

HAL 库:

__HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_5; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

看起来差不多,但 HAL 库多了Pull配置,而且时钟使能宏更统一。在复杂外设如 UART、SPI、ADC 上,HAL 库的初始化结构体嵌套更深,但 CubeMX 能自动生成,减少手写错误。

4.2 迁移中的中断处理差异

标准库的中断服务函数需要自己写,比如:

void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { uint8_t data = USART_ReceiveData(USART1); // 处理数据 USART_ClearITPendingBit(USART1, USART_IT_RXNE); } }

HAL 库则提供了统一的中断处理入口:

void USART1_IRQHandler(void) { HAL_UART_IRQHandler(&huart1); }

然后在HAL_UART_RxCpltCallback回调函数里处理数据。这种设计的好处是中断逻辑和业务逻辑分离,但要注意回调函数的执行上下文是中断,不能做耗时操作。

迁移时最容易出错的地方是中断优先级分组。标准库用NVIC_PriorityGroupConfig设置分组,HAL 库用HAL_NVIC_SetPriorityGrouping。如果分组设置不一致,抢占优先级和子优先级的解释就不同,可能导致中断嵌套行为异常。

4.3 代码体积与执行效率的权衡

HAL 库因为抽象层次高,编译出的代码比标准库大 20%-50%。对于 Flash 只有 32KB 的 STM32F030 这类低端芯片,可能装不下。这时候有几个选择:

  • 用 LL 库(Low Layer),它比 HAL 更接近寄存器,代码效率高,但可移植性差一些。
  • 手动裁剪 HAL 库,只保留用到的模块。
  • 关键代码直接用寄存器操作,非关键部分用 HAL。

执行效率方面,HAL 的函数调用层次多,在高速中断或紧密循环中可能成为瓶颈。比如在 1MHz 的 SPI 传输中,HAL_SPI_Transmit 的函数调用开销可能影响吞吐量。这时候可以用 DMA 传输,或者直接操作 SPI 数据寄存器。

5. 时钟树配置:系统稳定性的根基

5.1 时钟源选择与 PLL 计算

STM32 的时钟源有四种:HSI(内部高速 RC,通常 8MHz 或 16MHz)、HSE(外部晶振,常用 8MHz)、LSI(内部低速 RC,约 32kHz)、LSE(外部低速晶振,32.768kHz)。

系统时钟通常由 HSE 经过 PLL 倍频得到。以 STM32F103 为例,HSE=8MHz,目标系统时钟 72MHz:

  • HSE 先经过 PLLXTPRE 分频(不分频或 2 分频)
  • 然后经过 PLLSRC 选择输入
  • PLL 倍频系数 PLLMUL 可选 2-16 倍
  • 8MHz × 9 = 72MHz

配置时要注意 Flash 等待周期。系统时钟 48MHz 以下时,Flash 等待周期为 1;48-72MHz 时为 2。如果等待周期设置不对,CPU 读取 Flash 会出错,表现为程序跑飞或 HardFault。

5.2 外设时钟与总线频率的关系

STM32 的外设挂在不同的总线上:AHB、APB1、APB2。APB1 最高频率通常低于 APB2。比如 F103 的 APB1 最高 36MHz,APB2 最高 72MHz。USART1 挂在 APB2,USART2/3 挂在 APB1,所以同样配置下 USART1 的波特率上限更高。

配置外设时,必须确认其所在总线的时钟频率。比如配置 TIM2(挂在 APB1),如果 APB1 预分频系数不为 1,定时器时钟会是 APB1 频率的 2 倍。这个细节在计算定时器周期时经常被忽略,导致定时时间差一倍。

5.3 低功耗模式下的时钟管理

在电池供电项目中,低功耗是关键。STM32 提供 Sleep、Stop、Standby 三种低功耗模式。进入 Stop 模式后,HSE 和 HSI 关闭,只有 LSI 或 LSE 维持 RTC 运行。唤醒后需要重新配置系统时钟。

一个常见问题是:进入 Stop 模式前忘了把 GPIO 配置成模拟输入或浮空输入,导致引脚漏电流。尤其是接了外部上拉或下拉的引脚,如果保持推挽输出,可能形成电流通路。正确做法是把所有未使用的 GPIO 设为模拟输入模式,关闭时钟。

6. 嵌入式项目中的 STM32 实战经验

6.1 超声波测距模块的时序控制

HC-SR04 超声波模块用 Trig 触发、Echo 回响,通过测量 Echo 高电平时间计算距离。用 STM32 实现时,常见做法是用定时器输入捕获测 Echo 高电平宽度。

关键点:

  • Trig 需要至少 10μs 的高电平脉冲。用 HAL 库的HAL_GPIO_WritePin加HAL_Delay可能不准,因为 HAL_Delay 最小分辨率是 1ms。正确做法是用定时器或 DWT 延时实现微秒级延时。
  • Echo 高电平时间对应距离:距离 = 高电平时间 × 声速 / 2。声速取 340m/s,即 34000cm/s。如果定时器计数频率 1MHz,则距离 = 计数值 × 0.017cm。
  • 测量周期建议大于 60ms,避免上一次回响干扰下一次触发。

6.2 步进电机控制:五线四相与细分驱动

五线四相步进电机(如 28BYJ-48)常用 ULN2003 驱动。STM32 通过 GPIO 输出四相时序:

步序ABCD
11000
20100
30010
40001

这是单四拍模式,步距角大、扭矩小。还有双四拍(每次两相通电)和八拍(单双交替)模式。八拍模式步距角减半,运行更平滑。

如果要精确定位,建议用带细分驱动的步进电机驱动器(如 A4988、DRV8825),STM32 只需要输出脉冲和方向信号。脉冲频率决定转速,脉冲数量决定角度。注意加减速曲线,突然启停可能导致丢步。

6.3 嵌入式按键的非阻塞扫描

用HAL_Delay做按键消抖会阻塞整个程序。正确做法是用定时器中断定期扫描按键状态,实现非阻塞消抖。

思路:定时器每 10ms 中断一次,在中断里读取按键 GPIO,用状态机判断:

  • 状态 0:按键未按下,检测到低电平转到状态 1。
  • 状态 1:消抖确认,10ms 后再次检测,仍为低电平则确认按下,转到状态 2。
  • 状态 2:等待释放,检测到高电平转到状态 3。
  • 状态 3:释放消抖,10ms 后确认释放,回到状态 0。

这样主循环完全不被阻塞,可以同时处理其他任务。

6.4 串口通信中的 GBK 与 UTF-8 转换

STM32 默认用 ASCII 处理串口数据,但中文环境下经常需要显示 GBK 编码的汉字。如果上位机发 GBK,STM32 要转成 UTF-8 显示到 LCD,就需要编码转换表。

简单做法是只转换常用汉字,用查表法。完整做法是移植一个小的 GBK 到 UTF-8 转换库。注意 STM32 的 Flash 有限,完整字库可能占几十 KB,低端芯片放不下。可以用外部 SPI Flash 存字库,或者只支持 ASCII 加少量汉字。

7. 选型与学习路线的个人建议

7.1 不同阶段该选哪款 STM32

  • 入门:STM32F103C8T6(蓝板),资料最多,价格便宜,适合学 GPIO、UART、定时器、ADC。
  • 进阶:STM32F407VET6,带 FPU 和 DSP,适合学信号处理、电机控制、网络通信。
  • 低功耗:STM32L051C8T6,适合电池供电项目,学 Stop/Standby 模式、RTC 唤醒。
  • 高性能:STM32H743VIT6,480MHz,带 Cache 和大量外设,适合复杂算法和高速数据采集。

不要一上来就买最贵的板子。F103 虽然老,但它的外设覆盖了 80% 的常用场景,学透了再迁移到其他系列,成本很低。

7.2 学习路线中的关键节点

我见过太多人卡在“会点灯但不会做项目”的阶段。问题通常出在几个节点:

  1. 中断和时钟:不理解 NVIC 优先级分组、不熟悉时钟树,遇到问题无法排查。
  2. 通信协议:UART、I2C、SPI 只会调用库函数,不懂时序图,遇到从机不响应就束手无策。
  3. 调试手段:只会用 printf,不会用调试器看寄存器、看变量、设断点。
  4. 代码组织:所有代码堆在 main.c,不会分模块、不会用头文件。

突破方法:找一个完整的开源项目(比如平衡车、四轴飞控、温控器),把代码从头到尾读一遍,理解每个模块的作用和调用关系。然后自己从头写一遍,遇到问题用调试器逐步排查。

7.3 工装与量产测试的注意事项

产品开发到量产阶段,需要做产线测试工装。STM32 的工装通常通过 UART 或 USB 和上位机通信,测试内容包括:

  • GPIO 通断测试
  • ADC 通道校准
  • 通信接口回环测试
  • Flash 读写测试
  • 低功耗电流测试

工装固件和产品固件最好分开。工装固件可以精简,只保留测试相关功能,通过特定命令进入测试模式。产品固件则要加保护,防止误入测试模式。

另外,量产时要注意芯片的 Unique ID 和 Flash 保护。每颗 STM32 有 96 位唯一 ID,可以用来做加密授权。Flash 读保护可以防止代码被读出,但要注意设置读保护后调试接口会失效,需要先解除保护才能重新烧录。

STM32 的生态还在不断扩展,新的 G0、U5、H5 系列在成本和性能上都有新选择。但核心的学习方法没变:理解时钟和中断,掌握至少一种通信协议,会用调试工具,然后通过项目积累经验。这些底层能力,换任何芯片都用得上。

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

嵌入式AI编程实战:代码审查、板级调试与工作流固化

嵌入式软件这行有个特别拧巴的地方:代码跑在资源受限的板子上,调试靠串口打印和示波器,但写代码的方式却还停留在“手搓寄存器、翻数据手册、对着参考手册一行行抠”的阶段。我做了十多年嵌入式,从8位机裸跑到带RTOS的Cortex-M&am…

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

Agent Skills 实战指南:从原理到自动化测试应用

1. 从“skills”这个热词说起:它到底是什么,为什么突然火了最近几个月,不管是在技术社区、AI 工具群,还是在做前端、写论文、搞自动化测试的朋友圈子里,“skills”这个词出现的频率高得离谱。有人叫它 Agent Skills&am…

作者头像 李华
网站建设 2026/10/7 14:58:03

解决codex回复一直重连问题:把auth.json改到TaoToken的排查清单

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

作者头像 李华
网站建设 2026/10/7 14:58:02

谈谈DeepSeek-v3在算力约束下的出色工作:从MoE到FP8的AIInfra实践

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

作者头像 李华
网站建设 2026/10/7 14:57:44

Hermes Agent 从入门到精通:自托管 AI 智能体的持久记忆实战

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

作者头像 李华