STM32自动浇花系统源码解析:从土壤湿度采集到水泵控制与Flash阈值存储
简介STM32自动浇花系统是一套面向嵌入式初学者与电子爱好者的完整智能硬件实践项目以STM32F103C8T6为核心结合土壤温湿度、光敏、空气温湿度等传感器与OLED显示、DC-DC电源、水泵控制模块实现按湿度阈值或定时策略自动灌溉帮助读者理解从传感器采集到执行机构驱动的完整闭环。压缩包共535个文件约27.77MB以86个c源文件与70个h头文件为主体配合Keil工程文件、hex固件、PCB设计资料、pdf文档与png图片等覆盖代码、原理图与调试记录。目前已有3779人学习下载。项目涉及STM32CubeMX初始化、HAL库驱动、GPIO控制OLED、继电器驱动水泵及PCB布局与元器件封装等环节适合作为课程设计、毕业设计或DIY智能养护方案的参考便于读者快速搭建硬件平台并移植控制逻辑。1. 从一份 STM32 自动浇花系统源码包说起它到底能解决什么阳台那几盆绿萝出差三天回来叶子全耷拉了——这事我干过不止一次。后来我拿到一份 STM32 自动浇花系统.zip拆开跑了一遍发现它解决的正是这种“人不在、花要死”的场景。核心逻辑不复杂STM32 采集土壤湿度低于阈值就驱动水泵补水高于阈值就停顺带在 OLED 上显示当前湿度百分比。它适合三类人正在找 STM32 综合练手项目的学生、想给家里做个小装置但不想从零画板的开发者、以及需要一套可改可扩的自动浇花系统参考实现的工程师。整包是标准 Keil 工程结构外设驱动、主循环、阈值判断都拆得比较清楚不是那种一个 main.c 写到底的“教学 demo”。下面我按“硬件怎么接、代码怎么读、参数怎么调、坑在哪”的顺序把这份资源拆一遍。2. 硬件链路与工程结构先看清信号从土壤到水泵走了哪条路2.1 传感器选型与 ADC 采集链路自动浇花系统的第一环是土壤湿度检测。这份工程用的是模拟量土壤湿度传感器常见做法是电阻式叉形探头输出 03.3V 模拟电压接到 STM32 的 ADC 通道上。为什么不用数字量模块因为数字量模块只给一个可调电位器决定的开关量你没法在代码里做平滑滤波也没法显示具体湿度百分比调阈值只能拧螺丝很玄学。模拟量方案虽然要多写几行 ADC 采样和换算但阈值可以在代码里改还能做多点平均稳定性高不少。工程里 ADC 的配置大致是规则通道单次转换采样时间设到 55.5 个周期以上因为土壤湿度变化慢不需要高速采样长采样时间反而能降低噪声。下面这段是常见的 ADC 初始化骨架我按工程里的写法整理了一下// ADC1 初始化PA0 作为模拟输入对应土壤湿度传感器 void Soil_ADC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); // ADC 时钟 72M/6 12MHz不超过 14MHz GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 模拟输入必须设 AIN GPIO_Init(GPIOA, GPIO_InitStructure); ADC_DeInit(ADC1); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; // 单通道不用扫描 ADC_InitStructure.ADC_ContinuousConvMode DISABLE; // 单次转换主循环里手动触发 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_Cmd(ADC1, ENABLE); // ADC 校准别省这一步不然首读偏差能到几十个 LSB ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); }逻辑说明这段代码做了三件事——开时钟、配 GPIO 为模拟输入、配 ADC 为单通道单次转换。参数上最需要注意的是RCC_ADCCLKConfig(RCC_PCLK2_Div6)STM32F103 的 ADC 时钟不能超过 14MHz72MHz 主频下分频系数至少选 6。ADC_SampleTime_55Cycles5是采样时间土壤传感器输出阻抗不低采样时间太短会导致读数偏低55.5 周期是稳妥值。校准那两行很多人会漏漏了之后同一块板子换传感器读数能差出 5% 以上。2.2 水泵驱动与继电器/ MOS 管选择STM32 的 GPIO 输出电流最大 20mA 左右带不动水泵必须加驱动级。工程里用的是继电器模块低电平触发。为什么不是 MOS 管继电器模块即插即用隔离性好缺点是寿命有限、切换有声音MOS 管方案更安静、寿命长但要自己算栅极电阻和续流二极管对新手不友好。这份资源定位是快速复现所以选继电器是合理的。接线逻辑STM32 的 PB1 接继电器 IN 脚继电器常开触点串在水泵电源回路里。代码里就是一句 GPIO 置位/复位。但这里有个容易翻车的点继电器模块如果是 5V 供电而 STM32 是 3.3V 电平部分继电器模块的高电平触发阈值是 3.5V 以上3.3V 可能触发不了。常见做法是选低电平触发的继电器模块或者加一个三极管做电平转换。工程里默认低电平触发所以代码里是GPIO_ResetBits开水泵别写反了。2.3 工程目录与关键文件定位解压后目录结构大致是CORE、HARDWARE、SYSTEM、USER四个文件夹。HARDWARE下放adc.c、relay.c、oled.cUSER下是main.c。读代码的顺序建议是先看main.c的主循环搞清楚状态机怎么跑再回头看adc.c里的换算函数最后看oled.c的显示刷新。不要一上来就啃 OLED 驱动那部分和浇花逻辑无关容易陷进去。3. 主循环逻辑与阈值参数湿度百分比是怎么算出来的3.1 ADC 原始值到湿度百分比的换算ADC 读出来是 04095 的原始值要变成“湿度百分比”得做一次映射。这里有个反直觉的地方电阻式土壤传感器的输出电压和湿度是反相关的——越湿输出电圧越低。所以换算时不能直接raw/4095*100得反过来。工程里的做法是先测两个校准点传感器完全干燥时的 ADC 值记为 DRY和泡在水里的 ADC 值记为 WET然后线性映射。// 土壤湿度换算返回 0~100 的百分比 // DRY_VAL 和 WET_VAL 需要根据实际传感器实测后修改 #define DRY_VAL 3800 // 传感器在空气中时的 ADC 读数 #define WET_VAL 1200 // 传感器泡水时的 ADC 读数 u8 Soil_GetPercent(void) { u16 raw 0; u8 i; u32 sum 0; for (i 0; i 10; i) { // 连续采 10 次 ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); sum ADC_GetConversionValue(ADC1); } raw sum / 10; // 取平均滤掉抖动 if (raw DRY_VAL) return 0; if (raw WET_VAL) return 100; return (u8)((DRY_VAL - raw) * 100 / (DRY_VAL - WET_VAL)); }逻辑说明先做 10 次采样取平均这是最省事的软件滤波能压掉大部分随机噪声。参数DRY_VAL和WET_VAL是整份代码里最需要你动手改的地方不同厂家、不同批次的传感器这两个值能差几百个 LSB。我一般会先把传感器晾干读一次串口再泡水里读一次把两个值填回去。如果你不校准直接用默认值显示出来的湿度可能永远卡在 30% 或者 90%然后你就开始怀疑代码其实是传感器个体差异。3.2 阈值判断与水泵启停的迟滞设计主循环里判断逻辑看起来简单湿度低于阈值开水泵高于阈值关水泵。但如果只设一个阈值比如 40%那湿度在 40% 附近抖动时水泵会频繁启停继电器咔咔响寿命掉得很快。工程里用了迟滞区间低于 35% 开高于 55% 关。中间 20% 的区间是“死区”湿度在这区间内水泵保持当前状态。// 主循环中的浇花控制逻辑 #define HUMI_LOW 35 // 低于此值开始浇水 #define HUMI_HIGH 55 // 高于此值停止浇水 void Water_Control(u8 humi) { static u8 pump_on 0; if (humi HUMI_LOW) { pump_on 1; } else if (humi HUMI_HIGH) { pump_on 0; } // 在 LOW 和 HIGH 之间时pump_on 保持上一次的值 if (pump_on) { Relay_On(); // 低电平触发实际是 GPIO_ResetBits } else { Relay_Off(); } }逻辑说明static u8 pump_on保存上一次的状态这就是迟滞的核心。参数HUMI_LOW和HUMI_HIGH的差值建议至少 15%太小了还是会抖。另外浇水时最好加一个最大连续运行时间保护比如水泵连续开 30 秒强制停 10 秒防止传感器故障导致水泵一直抽水把花淹了。这个保护工程里没写但属于我强烈建议你补上的。3.3 OLED 显示刷新与串口调试OLED 部分用的是 I2C 接口的 0.96 寸屏显示内容一般是两行第一行当前湿度百分比第二行水泵状态。刷新频率不用太高主循环里每 500ms 刷一次就够了刷太快反而闪。串口调试更实用把原始 ADC 值、换算后的百分比、水泵状态一起打印出来校准传感器的时候盯着串口看比看 OLED 方便。// 串口打印调试信息波特率 115200 printf(ADC_RAW%d, HUMI%d%%, PUMP%s\r\n, raw, humi, pump_on ? ON : OFF);逻辑说明printf重定向到串口需要实现fputc工程里SYSTEM文件夹下一般有usart.c已经做好了。参数上注意波特率一致115200 是常用值。如果你打印出来是乱码先查波特率再查晶振频率宏定义是不是 8MHz 外部晶振这两个地方错了串口必乱。4. 避坑与排查这份源码跑不起来时先查这五处4.1 现象OLED 不亮但串口有数据原因I2C 地址不对或者接线反了。0.96 寸 OLED 常见地址是 0x78 和 0x7A有些模块背面电阻跳线决定地址。另外 SCL 和 SDA 接反了也会不亮但不会烧只是没反应。解决先用 I2C 扫描代码确认地址再核对模块背面的地址跳线。SCL/SDA 对调一下试试成本最低。4.2 现象水泵一直转湿度显示 0%原因传感器没接好或者 ADC 通道配错。如果 PA0 悬空ADC 读到的值会飘到接近 4095换算后湿度接近 0%触发浇水。另外传感器供电如果是 5V 而 ADC 参考电压是 3.3V输出超过 3.3V 会读满量程也会显示 0%。解决先确认传感器供电和信号线接对用万用表量传感器输出脚电压。如果传感器是 5V 供电加一个分压电阻再进 ADC或者换成 3.3V 供电的传感器。4.3 现象湿度数值跳变超过 10%原因ADC 采样没有做滤波或者传感器探头接触不良。电阻式传感器对接触电阻很敏感探头插在土里如果松动读数就会跳。解决软件上把采样次数从 10 次加到 20 次或者加一阶低通滤波filtered filtered*0.8 new*0.2。硬件上把探头插紧线用杜邦线的话换成长度短一点的减少干扰。4.4 现象继电器吸合瞬间 STM32 复位原因继电器线圈断电时产生反向电动势通过电源线耦合把 MCU 拉复位了。这是血泪经验很多人以为是代码问题其实是电源问题。解决继电器模块的 VCC 和 GND 并一个 100uF 电解电容加 0.1uF 瓷片电容线圈两端加续流二极管如果是裸继电器。另外 STM32 和继电器不要共用一条细杜邦线供电分开走线。4.5 现象编译报错 “cannot open source input file stm32f10x.h”原因Keil 工程里的头文件路径没设对或者你换了一台电脑绝对路径失效。解决在 Keil 的 Options for Target → C/C → Include Paths 里把CORE、HARDWARE、SYSTEM、USER以及标准库的inc目录都加进去。路径用相对路径别用绝对路径不然换电脑就翻车。5. 进阶改造把阈值写进 Flash断电不用重烧5.1 为什么要把阈值存进 Flash现在改阈值要重新编译、重新烧录如果你把装置装到阳台上了想调一下阈值还得把板子拆下来插电脑很麻烦。常见做法是把HUMI_LOW和HUMI_HIGH存到 STM32 内部 Flash 的末尾页上电时读出来运行时可以通过串口命令修改。这样调阈值只需要串口发一条指令不用拆机。5.2 Flash 读写的最小实现STM32F103 的 Flash 页大小是 1KB 或 2KB选最后一页操作别和代码区冲突。下面是一个最小可用的读写封装// 把阈值存到 Flash 最后一页以 STM32F103C8T6 为例最后一页起始 0x0800FC00 #define FLASH_SAVE_ADDR 0x0800FC00 #define FLASH_PAGE_SIZE 1024 void Flash_Save_Threshold(u8 low, u8 high) { u16 buf[2] {low, high}; FLASH_Unlock(); FLASH_ErasePage(FLASH_SAVE_ADDR); // 先擦除整页 FLASH_ProgramHalfWord(FLASH_SAVE_ADDR, buf[0]); FLASH_ProgramHalfWord(FLASH_SAVE_ADDR 2, buf[1]); FLASH_Lock(); } void Flash_Load_Threshold(u8 *low, u8 *high) { *low *(vu16*)FLASH_SAVE_ADDR; *high *(vu16*)(FLASH_SAVE_ADDR 2); // 如果读出来是 0xFF说明从没写过用默认值 if (*low 0xFF || *high 0xFF) { *low 35; *high 55; } }逻辑说明FLASH_ErasePage必须整页擦除擦完所有字节变 0xFF。FLASH_ProgramHalfWord按半字写所以地址每次加 2。参数上注意FLASH_SAVE_ADDR要选在你代码占用空间之外的页如果你工程编译出来超过 63KB就得往前挪一页。读的时候判断 0xFF 是必要的因为新芯片 Flash 擦除后就是 0xFF不判断会读到 255 导致逻辑异常。5.3 串口命令解析与在线调参有了 Flash 读写再加一个简单的串口命令解析比如收到SET 40 60就把低阈值设为 40、高阈值设为 60然后存 Flash。解析逻辑用sscanf最省事// 串口接收缓冲区里收到 SET 40 60 时调用 void Cmd_Parse(char *cmd) { u8 low, high; if (sscanf(cmd, SET %hhu %hhu, low, high) 2) { if (low high high 100) { Flash_Save_Threshold(low, high); printf(OK: LOW%d HIGH%d\r\n, low, high); } else { printf(ERR: invalid range\r\n); } } }逻辑说明%hhu对应unsigned char别写成%d不然会覆盖相邻内存。参数校验里low high是必须的否则迟滞逻辑会失效。调参时我一般会先发SET 35 55确认返回 OK再断电重启看阈值有没有丢验证 Flash 确实写进去了。5.4 验证方法与我的习惯验证 Flash 是否写入成功最直接的办法是断电再上电看串口打印的阈值是不是你设置的值。如果每次上电都回到默认值先查FLASH_SAVE_ADDR是不是和代码区重叠了再查有没有调用FLASH_Unlock。从那以后我每次改 Flash 相关代码都强制走一遍“写→断电→上电→读”的流程不省这一步。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

内核空间初始化:从引导程序到start_kernel的完整流程解析

内核空间初始化:从引导程序到start_kernel的完整流程解析

1. 内核空间初始化的整体蓝图1.1 初始化到底在做什么很多做应用开发的同事一听到"内核空间初始化",第一反应是"那不就是开机跑一段汇编把硬件摸一遍嘛"。这个理解不算错,但跟实际的内核初始化工作相比,显得太过乐观了。我…

2026/10/19 7:40:22 阅读更多 →
升维与降维互为因果:构建永续认知螺旋

升维与降维互为因果:构建永续认知螺旋

先说一个我最近的经历。某个产品功能上线不到两周,数据就出现明显下滑,团队开会复盘时,现场出现了两种截然不同的声音:一种说,要想清楚这个功能到底在服务谁、解决什么问题,别让短期指标牵着走;…

2026/10/19 7:40:21 阅读更多 →
RAG落地实战:检索增强生成的原理、架构与参数调优全解析

RAG落地实战:检索增强生成的原理、架构与参数调优全解析

最近大模型圈子里最绕不开的一个词就是RAG(Retrieval-Augmented Generation,检索增强生成)。不管是做知识库问答、企业私有数据助手,还是给大模型配上"长期记忆",背后基本都是这套架构。我自己在过去一段时间…

2026/10/19 7:40:21 阅读更多 →

最新新闻

Flutter组件鸿蒙适配实战:Windows与IANA时区转换

Flutter组件鸿蒙适配实战:Windows与IANA时区转换

做全球化的产品,时间问题永远比想象中麻烦。我维护的 Flutter 组件 win2iana_tz_converter,解决的就是 Windows 时区 ID 与 IANA 时区 ID 互转这件事。最近一个阶段,我把它完整适配到了鸿蒙 HarmonyOS 平台上,一边踩坑一边把方案沉…

2026/10/19 8:29:12 阅读更多 →
CentOS 7.7升级内核后卡在grub rescue?手把手教你修复引导

CentOS 7.7升级内核后卡在grub rescue?手把手教你修复引导

升级内核本身一步到位,结果重启后屏幕不是系统登录界面,而是一段冷冰冰的英文,最后停在一个grub rescue>的光秃秃提示符上。这种事CentOS运维多少都碰过,尤其是在CentOS 7.7这样的老版本上升级内核,概率还不低。如果…

2026/10/19 8:29:12 阅读更多 →
diagram-design 方法论:从草图到可评审架构图的视觉语法与实操指南

diagram-design 方法论:从草图到可评审架构图的视觉语法与实操指南

1. 从一张草图到一套系统:diagram-design 到底在解决什么问题第一次看到 “diagram-design” 这个项目名,很多人会以为它只是某个画图工具的皮肤或者模板集合。但真正上手之后你会发现,它其实是一套关于“如何把脑子里模糊的结构变成别人一眼…

2026/10/19 8:29:12 阅读更多 →
简历文本分类升级:从规则匹配到TF-IDF+SVM的实战解析

简历文本分类升级:从规则匹配到TF-IDF+SVM的实战解析

接手“简历分类问题二”的时候,我就知道这不是简单地把简历按关键词归个类的事。上一版规则匹配上线后,效果只能说勉强能跑——遇到“项目经理”和“项目专员”这种描述高度重叠的岗位,基本靠猜;碰到“跨境电商运营”这种既像运营…

2026/10/19 8:29:12 阅读更多 →
Qt QFile文件读写深入解析:模式、编码、性能与踩坑实战

Qt QFile文件读写深入解析:模式、编码、性能与踩坑实战

1. 项目概述:从一个踩坑场景聊起先说说我为什么想写这篇文章。前阵子接了个活,帮某开发者的一个文本解析工具做性能优化,里面有个环节是把几个配置文件读进来、改完再写回去。代码本身看着没毛病:QFile file(path); file.open(QIO…

2026/10/19 8:29:12 阅读更多 →
公用桌面防数据泄露:从屏幕保护到锁定连接的完整加固方案

公用桌面防数据泄露:从屏幕保护到锁定连接的完整加固方案

公用桌面上的数据泄露,往往不是被外部黑客“攻”出来的,而是被人“顺便看”出来的。你随便在办公室或公共区域的共享电脑上看一眼,可能会发现上一个人走的时候,网盘还开着、邮箱还没退出、微信静默挂在后台,甚至连 Exc…

2026/10/19 8:28:11 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介:本资源为电源技术方向的学术研究文档,聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略,面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

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

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址: https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集,共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/18 12:16:16 阅读更多 →