news 2026/9/5 8:46:16

51单片机交通灯仿真:定时器、状态机与Proteus实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
51单片机交通灯仿真:定时器、状态机与Proteus实战

简介:本资源是一套面向单片机初学者与电子类课程实践者的51单片机C语言教学实例,聚焦定时器中断驱动的交通灯控制系统设计与Proteus仿真验证。资源完整覆盖从Keil C编程、定时器初始化配置(Timer0工作模式1)、中断服务逻辑到I/O口动态控制红绿黄灯时序的核心技能,适用于嵌入式入门、课程设计及实训项目开发。压缩包共15个文件,含Proteus电路图(.dsn)、Keil工程配置(.uv2、.opt)、C源码(.c)、编译输出(.hex、.obj、.lst)、仿真日志(.plg)及备份文件(.bak、.dbk),总大小仅29KB,结构紧凑、即开即用。已有180人学习下载,读者可直接加载Proteus工程观察实时灯控效果,结合源码理解定时器重载值计算、状态机切换逻辑与端口电平控制细节,是掌握51单片机定时器应用与软硬协同仿真的高实用性入门范例。

1. 项目概述与核心价值

最近在整理以前做过的单片机项目,翻到了一个经典的交通灯仿真。这个项目虽然基础,但麻雀虽小五脏俱全,它把51单片机的定时器、I/O口控制、状态机编程以及Proteus仿真这几个核心技能点都串起来了。对于刚学完51单片机基础语法,想找个综合项目练手的朋友来说,这个“定时器控制交通指示灯”的仿真实例,是个非常合适的“毕业设计”。

这个项目要解决的问题很直观:模拟一个十字路口的交通信号灯系统。东西方向和南北方向的红、黄、绿灯按预设的时间规则交替点亮和熄灭。听起来简单,但实现起来,你需要考虑如何用一块芯片的有限资源(比如只有两个定时器)来管理多个时间任务,如何设计清晰的状态切换逻辑,以及如何在仿真环境中验证你的程序逻辑是否正确。这不仅仅是点亮几个LED,而是学习如何将一个实际需求,拆解成单片机可以理解和执行的代码与电路。

通过这个项目,你能得到的不只是一份能跑起来的代码。你会深刻理解定时器中断如何作为单片机系统的“心跳”,驱动整个程序有条不紊地运行;你会掌握状态机这种极其重要的编程思想,它能让复杂流程的逻辑变得清晰可控;你还会熟悉Proteus这个强大的仿真工具,实现从代码编写、电路搭建到功能验证的完整闭环,这比单纯在开发板上实验更能锻炼你的系统设计能力。无论你是电子信息类专业的学生,还是刚入行的嵌入式爱好者,这个项目都能帮你把书本上的知识点,凝结成实实在在的工程能力。

2. 项目整体设计与思路拆解

2.1 系统需求与功能定义

首先,我们需要把“交通灯”这个模糊的概念,转化为明确的技术指标。一个标准的十字路口交通灯系统,其核心需求可以拆解如下:

  1. 双向控制:系统需独立控制东西方向和南北方向两组信号灯(每组含红、黄、绿三个LED)。
  2. 时序循环:两个方向的信号灯按固定的周期循环切换。一个典型的周期包括:
    • 东西绿灯,南北红灯:允许东西方向通行。
    • 东西黄灯,南北红灯:东西方向通行警告。
    • 东西红灯,南北绿灯:允许南北方向通行。
    • 东西红灯,南北黄灯:南北方向通行警告。
    • 之后循环回第一个状态。
  3. 时间可配置:每个状态的持续时间应该是可灵活设置的,例如绿灯时长30秒,黄灯时长3秒。这要求我们的程序不能使用简单的delay函数,而必须基于定时器实现可维护的时间管理。
  4. 实时性与可靠性:状态切换必须准时,不能因为程序执行其他代码而延误。这要求我们使用中断机制来确保计时的准确性。
  5. 可视化仿真:在无实物硬件的情况下,通过Proteus软件搭建电路模型,并将编译好的程序加载到单片机模型中运行,直观地观察信号灯的变化是否符合预期。

基于以上需求,我们的技术方案就清晰了:以STC89C52单片机为核心控制器,利用其内部定时器产生精确时基,通过中断服务程序更新状态和时间,驱动I/O口控制Proteus中的LED模型,模拟交通灯运行。

2.2 核心方案选型与考量

为什么选择这样的方案?每一个技术选型背后都有其考量。

主控芯片选择STC89C52:对于这个项目而言,AT89C51或STC89C52这类经典的51内核单片机完全够用。它们拥有足够的I/O口(32个)来控制6个LED,内置两个16位定时器/计数器(Timer0和Timer1),价格低廉,资料丰富。选择STC89C52而非AT89C51,主要是因为STC系列支持ISP在线编程,开发调试更方便,且其增强型型号资源更丰富,为后续功能扩展(如增加倒计时显示)留有余地。

定时器而非延时函数:这是本项目最重要的设计决策。很多初学者喜欢用for循环或while循环实现的delay函数来做延时。这种做法在简单任务中可行,但存在致命缺陷:CPU被无限期占用,无法执行其他任务。在交通灯系统中,如果我们用delay(30000)来实现30秒绿灯,那么在这30秒内,单片机无法响应任何其他事件(比如检测紧急按钮)。而使用定时器中断,每隔一个固定时间(如10ms)产生一次中断,在中断里对一个计数器加1。主程序只需要判断这个计数器是否达到预设值(如3000次对应30秒)。这样,在等待的30秒内,CPU绝大部分时间都在执行主循环,可以轻松处理其他逻辑,系统实时性和多任务处理能力大大提升。

状态机编程思想:交通灯的工作流程是典型的“状态-迁移”模型。我们可以定义几个明确的状态(如STATE_EW_GREEN)。每个状态下,灯的输出是固定的,并且在该状态持续一段时间后,会条件跳转到下一个状态。用switch-case语句或函数指针数组来实现状态机,能使程序逻辑层次分明,易于调试和修改。例如,如果想增加一个“全红闪烁”的故障状态,只需要增加一个新的状态定义和迁移逻辑即可,不会影响原有代码结构。

Proteus作为仿真平台:在硬件制作之前进行仿真,可以极大降低开发成本和风险。Proteus能够模拟单片机执行机器码,并实时显示电路中各点的电压、电流以及器件的工作状态。对于这个项目,我们可以验证: * 程序逻辑是否正确,状态切换是否准时。 * 单片机I/O口驱动能力是否足够点亮LED(虽然仿真中理想器件无需担心)。 * 电路连接有无错误,如限流电阻是否接入。 成功仿真意味着你的软硬件设计在原理上是完全可行的,接下来制作实物就是水到渠成的事情。

3. 核心细节解析与实操要点

3.1 定时器配置精讲

51单片机的定时器是项目的“心脏”。我们以最常用的模式1(16位自动重装模式)为例,进行详细配置。假设我们使用11.0592MHz的晶振(这个频率非常常见,因为它能产生精确的串口波特率)。

目标:让定时器每50ms产生一次中断。

计算过程

  1. 机器周期:对于标准51架构,1个机器周期 = 12个时钟周期。因此,机器周期T_machine = 12 / 11.0592MHz ≈ 1.085μs
  2. 定时器计数原理:定时器从初始值开始向上加1,计满65536(2^16)后溢出并产生中断。我们需要定时50ms,即50000μs。
  3. 计算需要计数的机器周期数:N = 50000μs / 1.085μs ≈ 46080次。
  4. 计算定时器初始值:初始值TH0/TL0 = 65536 - 46080 = 19456
  5. 将19456转换为十六进制:19456 = 0x4C00。所以TH0 = 0x4C; TL0 = 0x00;

C语言配置代码

void Timer0_Init(void) //50毫秒@11.0592MHz { TMOD &= 0xF0; //清除T0的模式位,不影响T1 TMOD |= 0x01; //设置T0为模式1:16位定时器,不自动重装 // 或者使用 TMOD = 0x01; 但更推荐位操作,避免影响T1 TL0 = 0x00; //设置定时初值低8位 TH0 = 0x4C; //设置定时初值高8位 TF0 = 0; //清除T0溢出中断标志 TR0 = 1; //启动T0 ET0 = 1; //允许T0中断 EA = 1; //开启总中断 }

注意:这里计算出的50ms是理论值,存在微小误差。在实际高精度场合,可能需要使用定时器的自动重装模式(模式2)来减少误差,或进行软件校准。但对于交通灯这种秒级应用,50ms的误差累积可以忽略不计。

3.2 状态机设计与实现

状态机是控制逻辑的核心。我们定义4个状态,并用枚举类型增加代码可读性。

typedef enum { STATE_EW_GREEN_NS_RED, // 东西绿,南北红 STATE_EW_YELLOW_NS_RED, // 东西黄,南北红 STATE_EW_RED_NS_GREEN, // 东西红,南北绿 STATE_EW_RED_NS_YELLOW // 东西红,南北黄 } TrafficState; TrafficState g_current_state = STATE_EW_GREEN_NS_RED; // 全局变量,记录当前状态

每个状态需要关联两个信息:输出动作(哪些灯亮)和持续时间。我们可以用两个数组来管理:

// 各状态对应的持续时间(单位:50ms的倍数)。例如600对应600*50ms=30秒。 unsigned int state_duration[] = {600, 60, 600, 60}; // 绿30s,黄3s // 各状态对应的灯控信号(假设P1口控制,具体位定义见下文) unsigned char light_pattern[] = { 0xXX, // 东西绿南北红:例如,P1.0=1(东绿),P1.3=1(南红) 0xXX, // 东西黄南北红 0xXX, // 东西红南北绿 0xXX // 东西红南北黄 };

在主循环或定时器中断中,我们维护一个状态计时器。当计时器达到state_duration[g_current_state]时,就迁移到下一个状态,并重置计时器,同时更新I/O口输出为light_pattern[g_current_state]

3.3 Proteus电路设计关键点

在Proteus中搭建电路,需要注意以下几点:

  1. 元件选择

    • 单片机:搜索“AT89C52”或“STC89C52”。虽然Proteus库中没有STC,但内核兼容,用AT89C52仿真完全没问题。
    • LED:搜索“LED-RED”、“LED-YELLOW”、“LED-GREEN”。注意选择“LED”而不是“LAMP”。
    • 电阻:搜索“RES”。LED需要串联限流电阻,阻值通常在220Ω到1kΩ之间,仿真中330Ω即可,颜色会显示得更真实。
    • 晶振和电容:搜索“CRYSTAL”和“CAP”。在单片机XTAL1和XTAL2引脚接一个11.0592MHz晶振,两侧各接一个20-30pF的电容到地,构成振荡电路。
    • 复位电路:在RST引脚接一个10uF电容到VCC,接一个10k电阻到地,构成上电复位电路。
  2. 连线与布局

    • 将6个LED分别连接到单片机的I/O口,例如P1.0~P1.5。建议规划好:P1.0-东西绿,P1.1-东西黄,P1.2-东西红,P1.3-南北绿,P1.4-南北黄,P1.5-南北红。这样编程时位操作更清晰。
    • 务必为每个LED串联一个限流电阻,直接连接IO口到LED阳极,阴极接地,可能会在仿真中因电流过大报错或烧毁虚拟元件(虽然仿真不会真坏,但会提示错误)。
    • 将VCC和GND网络连接好,可以使用电源端子(POWER)和地端子(GROUND)。
  3. 程序加载

    • 双击单片机元件,在“Program File”一栏,选择你使用Keil编译生成的.hex文件。
    • 在“Clock Frequency”中设置为11.0592MHz,与你的程序设置匹配。

4. 实操过程与核心代码实现

4.1 软件开发环境搭建与项目创建

我们使用Keil uVision作为开发环境。创建新项目时,选择芯片型号为“AT89C52”(因为Keil可能没有STC的型号,但代码通用)。创建一个新的main.c文件。

首先,进行宏定义和全局变量声明,这是好习惯,便于修改和维护。

#include <REGX52.H> // 引脚定义,根据你的Proteus电路连接修改 #define EW_GREEN P1_0 // 东西绿灯 #define EW_YELLOW P1_1 // 东西黄灯 #define EW_RED P1_2 // 东西红灯 #define NS_GREEN P1_3 // 南北绿灯 #define NS_YELLOW P1_4 // 南北黄灯 #define NS_RED P1_5 // 南北红灯 // 状态枚举 typedef enum { ... } TrafficState; // 如前文定义 // 全局变量 TrafficState g_current_state; unsigned int g_timer_50ms_cnt = 0; // 50ms定时器累计次数 unsigned int g_state_timer = 0; // 当前状态已持续时间计数

实操心得:使用宏定义(#define)来管理引脚,而不是直接在代码中写P1_0,能让程序的可移植性大大增强。如果硬件电路改了,只需要修改宏定义一处即可。

4.2 定时器中断服务程序实现

这是整个系统的驱动引擎。

void Timer0_ISR(void) interrupt 1 // 定时器0中断号是1 { // 1. 重装初值,保证下一次50ms的精确性(模式1需手动重装) TH0 = 0x4C; TL0 = 0x00; // 2. 50ms计数器加1 g_timer_50ms_cnt++; // 3. 状态计时器加1 g_state_timer++; // 4. 状态时间判断与切换(也可放在主循环,但放中断里响应更及时) // 这里以状态0(东西绿)持续30秒为例:30s / 0.05s = 600次 if(g_state_timer >= 600) { g_state_timer = 0; // 重置状态计时器 g_current_state++; // 切换到下一个状态 if(g_current_state > STATE_EW_RED_NS_YELLOW) { g_current_state = STATE_EW_GREEN_NS_RED; // 状态循环 } update_lights(); // 更新灯的状态 } }

注意:中断服务函数(ISR)应该尽可能短小精悍,只做最必要的操作(更新计数、标志位)。像update_lights()这种涉及较多I/O操作和逻辑的函数,虽然这里直接调用了,但如果函数很复杂,更优的做法是在中断里设置一个标志位(如flag_light_update = 1),然后在主循环中检查这个标志位并执行update_lights(),这被称为“中断+轮询”机制,能保证中断快速退出,不影响其他中断响应。

4.3 主循环与状态输出函数

主程序非常简单,主要负责初始化,然后进入一个空循环,所有工作都由中断驱动。这是一种典型的“前后台系统”架构。

void update_lights(void) { // 先关闭所有灯,避免混乱(根据电路是共阳还是共阴,这里是共阴示例,输出0点亮) P1 = 0xFF; // 假设LED阴极接IO,阳极接VCC,输出1熄灭,输出0点亮。 switch(g_current_state) { case STATE_EW_GREEN_NS_RED: EW_GREEN = 0; // 点亮东西绿灯 NS_RED = 0; // 点亮南北红灯 break; case STATE_EW_YELLOW_NS_RED: EW_YELLOW = 0; NS_RED = 0; break; case STATE_EW_RED_NS_GREEN: EW_RED = 0; NS_GREEN = 0; break; case STATE_EW_RED_NS_YELLOW: EW_RED = 0; NS_YELLOW = 0; break; default: // 异常处理,例如全部闪烁 P1 = 0xFF; break; } } void main(void) { Timer0_Init(); // 初始化定时器 g_current_state = STATE_EW_GREEN_NS_RED; // 设置初始状态 update_lights(); // 更新初始灯光 g_state_timer = 0; while(1) { // 主循环可以处理其他任务,例如按键扫描(增加手动控制)、串口通信等。 // 对于当前项目,主循环可以为空,所有功能由中断处理。 // 这是一个“后台”任务。 } }

4.4 Proteus仿真运行与调试

  1. 编译与生成HEX:在Keil中确保编译0错误,0警告。然后进入“Options for Target” -> “Output”,勾选“Create HEX File”。重新编译,会在工程目录下生成.hex文件。
  2. 加载HEX文件:在Proteus中双击单片机,指定生成的.hex文件路径。
  3. 运行仿真:点击Proteus左下角的运行按钮。你应该能看到两组LED按照预设的时间(东西绿30秒 -> 东西黄3秒 -> 东西红30秒 -> 东西红3秒 -> ...)循环点亮。
  4. 调试技巧
    • 逻辑分析仪:Proteus内置了虚拟逻辑分析仪。你可以将单片机的P1口引脚添加到分析仪中,观察其电平变化的时序波形,精确测量每个状态的持续时间,这是验证定时器是否精准的利器。
    • 单步调试:虽然不如在真实硬件上方便,但你可以通过暂停仿真,查看单片机寄存器和变量的值,辅助排查逻辑错误。
    • 修改参数:尝试修改state_duration数组里的值,重新编译生成HEX,在Proteus中重新加载并运行,观察交通灯时序是否随之改变。这是检验你程序灵活性的好方法。

5. 功能扩展与优化思路

基础功能实现后,你可以尝试以下扩展,让项目更接近实际应用,也能学到更多。

5.1 增加倒计时数码管显示

这是非常实用的功能。可以增加两个2位的共阴数码管,分别显示东西和南北方向的剩余通行时间。

实现思路

  1. 硬件:使用单片机的P0口和P2口作为段选(通过74HC245驱动增加电流),P3口的几个引脚作为位选。
  2. 软件
    • 定义一个全局变量数组countdown[2],分别存储东西和南北方向的剩余秒数。
    • 在定时器中断中,每过1秒(累积20个50ms中断),就对当前通行方向(绿灯或黄灯)的countdown进行减1操作。
    • 在主循环中,实现数码管的动态扫描函数,将countdown中的数值解码成7段码,并轮流显示在两个数码管上。
    • 注意:倒计时在黄灯状态应从3开始,在绿灯状态应从30(或设定值)开始。需要在状态切换时重置countdown值。

5.2 增加紧急车辆优先通行模式

通过一个按键模拟紧急车辆传感器。

实现思路

  1. 硬件:将一个按键接到单片机的外部中断引脚(如INT0/P3.2)。
  2. 软件
    • 配置外部中断0为下降沿触发。
    • 在中断服务程序中,设置一个紧急标志emergency_flag = 1,并立即将状态强制切换到“东西红,南北红”(全红状态)。
    • 在主循环中,检测到emergency_flag为1时,进入紧急模式:让全红状态保持一段时间(如10秒),或者让红灯闪烁。
    • 紧急模式结束后,清除标志,并恢复到中断发生前的状态和剩余时间继续运行。这里就需要你在进入紧急模式前,备份g_current_stateg_state_timer

5.3 优化时间管理方式

当状态增多或时间精度要求高时,前文的g_state_timer直接自增的方式会使得中断服务程序变得冗长。可以引入“事件标志”机制。

优化思路

  1. 在定时器中断中,只做最核心的计时工作:g_timer_50ms_cnt++
  2. 在主循环中,不断检查g_timer_50ms_cnt
    unsigned int last_cnt = 0; while(1) { if(g_timer_50ms_cnt != last_cnt) { last_cnt = g_timer_50ms_cnt; // 每50ms执行一次的任务 state_machine_handler(); // 状态机处理函数 display_handler(); // 显示处理函数 // ... 其他任务 } // 可以在这里处理按键扫描等实时性要求不高的任务 }
    这样,中断函数极其简短,系统的实时性更好。状态机处理state_machine_handler()里再根据g_timer_50ms_cnt来计算状态是否该切换。

6. 常见问题与排查技巧实录

在实际操作和教学中,我遇到过学员们提出的各种各样的问题。这里总结几个最具代表性的:

问题1:Proteus仿真时,LED不亮或亮度异常。

  • 排查
    1. 检查电路:确认LED方向是否正确(三角形端为阳极)。确认限流电阻是否串联在电路中。
    2. 检查IO口电平:右键点击LED或连线,选择“Place Voltage Probe”或“Place Current Probe”,运行仿真后查看电压电流。如果单片机IO口输出为高电平,但LED两端电压差为0,可能是LED被短路或IO口模式设置错误(51单片机上电后IO口默认为高电平,输出1)。
    3. 检查程序:确认你的update_lights()函数确实在改变IO口输出。可以在Keil中软件仿真,单步执行看P1口的值是否变化。
  • 技巧:在Proteus中,LED旁边显示一个小红点表示当前有电流流过(亮),显示一个小蓝点表示没有电流(灭)。这是最直观的判断方式。

问题2:定时器时间不准,交通灯切换过快或过慢。

  • 排查
    1. 核对晶振频率:首先确认Keil项目设置和Proteus单片机属性中的晶振频率是否一致,都是11.0592MHz。
    2. 重算定时器初值:按照前文公式重新计算。特别注意你的单片机是否工作在12T模式(标准51是12T)。有些增强型51(如STC某些型号)可以配置为6T或1T模式,机器周期算法不同。
    3. 检查中断重装:如果使用定时器模式1(16位不自动重装),必须在中断服务程序开始处立即重装TH0和TL0。如果放在中断结束前重装,那么从溢出到重装这段时间的计数会被丢失,导致时间变长。
    4. 中断函数过长:如果中断服务程序执行时间过长,可能耽误下一次中断的响应。用逻辑分析仪看IO波形,如果周期不稳定,多是此原因。优化方法是精简ISR代码。
  • 技巧:使用Proteus的逻辑分析仪或示波器,测量一个IO口(例如用一个闲置口在中断里取反)输出的方波周期,这是最准确的验证方法。

问题3:状态切换混乱,不按预定顺序循环。

  • 排查
    1. 枚举变量越界:检查g_current_state++后,是否做了边界检查?如果超过了枚举的最大值,它会变成不可预测的值。确保有if(g_current_state > LAST_STATE) g_current_state = FIRST_STATE;这样的语句。
    2. 状态计时器重置时机:确保在状态切换时,g_state_timer被正确重置为0。
    3. 多个地方修改状态:确保g_current_state这个全局变量只在状态机逻辑的核心位置(如定时器中断或主循环的状态处理函数)被修改,避免在程序多个地方随意修改,导致逻辑冲突。
  • 技巧:在Keil的调试模式下,将g_current_stateg_state_timer添加到Watch窗口,单步或全速运行,观察它们的变化是否符合你的逻辑预期。

问题4:想增加功能(如按键、显示),但程序一加就卡死或运行异常。

  • 排查
    1. 堆栈溢出:51单片机RAM很小,局部变量过多或中断嵌套可能导致堆栈溢出。尽量减少函数调用层级和局部变量大小,特别是中断函数里。
    2. 中断冲突:如果使用了多个中断(如定时器0和外部中断0),要确保它们的中断优先级设置合理,且中断服务程序执行时间尽可能短。
    3. 主循环阻塞:如果你在主循环里用了while等待某个条件(如while(!key_pressed);),会导致程序卡死。必须使用非阻塞式的按键扫描方式。
  • 技巧:遵循“中断快进快出,主循环轮询处理”的原则。中断只设置标志位,主循环根据标志位执行具体任务。这是单片机编程保持系统响应性的黄金法则。

这个项目从电路到代码,完整地走完了一个嵌入式小系统的开发流程。它像一把钥匙,帮你打开了单片机应用开发的大门。理解了定时器、中断和状态机,你就能处理更复杂的任务,比如步进电机控制、温湿度采集、简单的通信协议等等。希望你在动手实现的过程中,不仅能得到一组闪烁的灯,更能收获那种从无到有、让想法在虚拟和现实中都运行起来的成就感。

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

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

技术项目资源优化:低配置环境下的开发与性能提升实战

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

作者头像 李华
网站建设 2026/9/5 8:40:30

用FFmpeg搭建美食探店短视频自动化生产线

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

作者头像 李华
网站建设 2026/9/5 8:38:50

QQ音乐mgg、mflac加密格式转MP3完整教程与工具推荐

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

作者头像 李华
网站建设 2026/9/5 8:37:46

技术文章标题的写法:让对的人一眼就点进来

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

作者头像 李华
网站建设 2026/9/5 8:37:07

HTML版BP制作指南:以阅读体验为核心的商业计划书架构

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

作者头像 李华