news 2026/9/18 6:42:05

RK3568 SPI LCD驱动实战:FrameBuffer替代DRM的完整方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RK3568 SPI LCD驱动实战:FrameBuffer替代DRM的完整方案

在RK3568平台上用SPI接口驱动一块LCD屏,很多人第一反应是直接套DRM的panel驱动框架,然后被各种KMS概念绕得头晕。实际上如果屏本身分辨率不高、对刷新率也没那么苛刻,FrameBuffer模式反而是最简单直接、也最容易跑通的一条路。这篇文章我以RK3568 + 一块2.0寸320x240的ST7789 SPI屏为例,把从硬件接线、设备树配置、驱动核心实现到性能优化和排障的完整思路拆开讲清楚。

我最早接到这个需求时,客户要的是一块小尺寸SPI LCD,用于显示设备状态和简单参数,不需要跑视频,也没有复杂的合成需求。当时评估了几种方案,最终选了FrameBuffer + SPI的方式,主要原因是这套组合在低分辨率、低刷新率场景下足够成熟,Linux核心对fbdev的支持非常稳定,驱动代码量小,排查问题也更直观。下面整个过程就是我实际调试下来的完整记录,希望能帮准备上这块方案的人少走几步弯路。

1. SPI LCD方案与FrameBuffer架构的适配逻辑

1.1 为什么选择FrameBuffer而不是DRM

RK3568的显示链路默认走的是DRM框架,MIPI DSI、RGB、eDP这些主流接口都有现成驱动。但SPI接口的小屏比较特殊,它不依赖RK3568自带的显示控制器,而是完全靠CPU侧通过SPI总线把像素数据写进LCD模组自带的GRAM里。屏本身自带显存,只要CPU能把数据送进去,剩下的刷新工作都由屏控制芯片完成。

这种屏如果硬塞进DRM框架,就得走panel-simple这类驱动,但SPI屏的数据通道并不是标准panel接口,驱动需要自己实现一整条数据通路。就算实现了,DRM带来的plane、crtc、connector、encoder这套抽象对SPI小屏来说几乎是纯开销。FrameBuffer架构在这个场景里就是降维打击:内核里直接注册一个fb_info,用户态通过/dev/fb0打开、mmap、写数据,驱动只要把数据经SPI搬到GRAM里就完事。

选择FrameBuffer的另一个原因是生态成熟。老的BSP、开发板资料、甚至网上的开源项目,大量SPI LCD方案都是基于fbdev写的,踩坑资料多,遇到问题也好搜。DRM虽然更现代,但在低端小屏场景下,它的优势完全发挥不出来,反而把问题复杂化。

1.2 SPI LCD在RK3568上的资源盘点

RK3568本身有多个SPI控制器,带FIFO也支持DMA,这给SPI LCD留了不错的性能基础。接线方面,标准的4线SPI只需要SCLK、MOSI、CS,MISO在很多LCD模组上可以不接,因为LCD是单方向写数据的设备。除了SPI三根线,还必须引出D/C引脚(区分命令还是数据)和RESET引脚,另外背光控制一般用PWM引脚,这样方便调节亮度。

RK3568的SPI控制器在设备树里有多个iomux组可以选择,比如SPI2可以mux到不同引脚组,这就是为了嵌入式系统里引脚资源紧张准备的。我在项目中把SPI2m1用到了LCD上,CS、CLK、MOSI、MISO各占一个引脚,再加一个GPIO管DC、一个GPIO管复位、一路PWM管背光,总共占不到10个引脚,对RK3568这种接口丰富的芯片来说非常宽裕。

2. 硬件设计与接线注意事项

2.1 最小系统接线方案

这里以ST7789V3控制芯片的2.0寸屏为例,它的驱动IC可以支持到320x240分辨率,RGB565格式,内置GRAM 240x320x16bit,用SPI接口时最高可以跑几十MHz时钟。实际项目中不会真跑那么快,因为GPIO控制的D/C引脚和SPI之间需要时间配合,时钟太快反而容易采错。

接线建议如下:

信号屏端引脚RK3568侧说明
SCLKSCK/SCLSPI2_CLKSPI时钟
MOSISDA/SDISPI2_MOSI主出从入
CSCSSPI2_CS0或GPIO片选,建议硬件CS
D/CDC/RSGPIO3_C0数据/命令选择
RESETRESETGPIO3_C1复位引脚,低电平有效
BLLEDAPWM4PWM背光,频率建议1kHz以上
VCCVDD3.3V逻辑电源,多数SPI屏是3.3V
GNDGNDGND共地

有个细节很容易忽略:SPI LCD的供电能力问题。小屏模组一般逻辑电压3.3V、背光LED串联后再加限流,背光全亮时电流可能到40mA以上。如果直接从RK3568的GPIO或某个弱供电轨取电,容易出现背光闪烁或者屏幕亮度不稳定的现象。我一般习惯把屏的VCC和背光分开供,VCC直接接到3.3V电源轨,背光用PWM控制MOS管驱动,不要用GPIO直驱背光。

2.2 D/C引脚与SPI时序配合

SPI LCD和普通SPI设备最大的区别就是多了一个D/C引脚,它决定了当前SPI总线上传输的字节是命令还是数据。有些屏控制芯片支持9-bit SPI模式,把第9个bit当作命令/数据标志,但大多数情况下都用最通用的方案:D/C引脚拉低时发命令,拉高时发数据。

时序上,D/C信号必须在SPI传输的字节期间保持稳定,不能用软件反复翻转。实际上Linux SPI框架发送数据时,整个spi_message是一次性传输出去的,所以驱动里需要在发起传输前把D/C引脚设好,传输过程中保持不动。这点用GPIO操作很方便:调用spi_sync_transfer之前用gpiod_set_value设置DC引脚电平,然后开始传输。

这里我踩过一个大坑:如果SPI频率比较高,超过了20MHz,GPIO翻转D/C引脚的速度可能跟不上,导致帧首的字节被当成错误类型处理,表现出来就是屏幕显示错乱。所以要么降低SPI频率,要么利用SPI控制器支持的spi_transferbits_per_word = 9来实现硬件级D/C切换,但后者对驱动兼容性要求更高,不如直接控制GPIO来得可靠。

2.3 背光控制与供电策略

背光这一路最简单也最容易出问题。用PWM控制背光亮度时,PWM频率最好不要低于1kHz,否则耳朵灵敏的人能听到背光驱动电感发出的啸叫。RK3568的PWM控制器在设备树里配置很方便,周期和占空比通过pwm-backlight框架管理,用户态通过sysfs接口就可以调亮度。

供电方面,SPI屏的driver IC内部会产生多路电压(如VCOM、VGH、VGL),这些电压都是内部DCDC或电荷泵产生的,对外部电源的纹波比较敏感。如果板子上3.3V纹波太大,屏幕容易出现水波纹或者刷新时亮度抖动。实际调试中我遇到过类似问题,最后在屏的VCC输入处加了一颗10uF陶瓷电容和1uF电容并联,问题就消失了。给正在做硬件的朋友一个建议:LCD的电源输入端预留电容位置,永远不要省。

3. 软件框架搭建:设备树、驱动核心与FrameBuffer注册

3.1 设备树配置详解

设备树的配置决定了内核能不能正确识别SPI设备和背光设备。以SPI2为例,先要确认RK3568的pinctrl中SPI2的引脚mux设置。我这边用到的是SPI2的m1组,也就是spi2m1_cs0spi2m1_clkspi2m1_misospi2m1_mosi这组引脚。

设备树的完整节点如下:

/ { backlight: backlight { compatible = "pwm-backlight"; pwms = <&pwm4 0 1000000 0>; brightness-levels = <0 4 8 16 32 64 128 255>; default-brightness-level = <6>; status = "okay"; }; }; &spi2 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&spi2m1_cs0 &spi2m1_clk &spi2m1_miso &spi2m1_mosi>; st7789: st7789@0 { compatible = "mycomp,st7789-fb"; reg = <0>; spi-max-frequency = <16000000>; spi-cpol = <0>; spi-cpha = <0>; reset-gpios = <&gpio3 RK_PC1 GPIO_ACTIVE_LOW>; dc-gpios = <&gpio3 RK_PC0 GPIO_ACTIVE_HIGH>; backlight = <&backlight>; width = <320>; height = <240>; rotate = <0>; bpp = <16>; status = "okay"; }; };

有几个点要重点解释:

  • spi-max-frequency设多少合适?我最后稳定在16MHz,这个值在绝大多数线长和GPIO DC方案下都能稳定工作。如果你用的是硬件D/C引脚切换方式,可以再往上拉,但16MHz对320x240的屏来说已经够用了。
  • reg = <0>表示挂在CS0上,如果你的SPI控制器硬件CS不够用,可以把CS也改成GPIO软片选,通过cs-gpios属性指定,后面会提到。
  • spi-cpolspi-cpha决定SPI模式。ST7789和大部分SPI LCD都支持模式0,即空闲时时钟低电平、首个边沿采样。如果模式配错,屏幕上会出现极规律的雪花噪点,而且完全无法通过软件修正。

3.2 SPI软件片选与硬件片选的选择

RK3568的SPI控制器自带硬件CS输出,设备树里用reg指定的索引会映射到对应CS引脚。但实际项目中,有时候SPI控制器某组引脚的CS被其他外设复用,或者想灵活控制片选时序,就可以改用GPIO软片选。

Linux SPI框架支持通过cs-gpios属性定义一组GPIO来控制CS:

&spi2 { status = "okay"; cs-gpios = <&gpio3 RK_PB6 GPIO_ACTIVE_LOW>; pinctrl-names = "default"; pinctrl-0 = <&spi2m1_clk &spi2m1_miso &spi2m1_mosi>; };

需要注意的是,当cs-gpios存在时,reg就不再直接映射到硬件CS了,而是映射到cs-gpios数组的索引。比如reg = <0>就代表使用cs-gpios里的第0个GPIO作为片选。

软件片选的好处是CS拉低和拉高的时机完全由SPI框架控制,配合SPI传输消息时可以精确做到先拉低再传输再拉高。缺点则是当SPI频率较高时,GPIO翻转速度可能不如硬件CS平滑。对于LCD这种多字节连续传输的场景,20MHz以下频率用GPIO片选没什么问题,如果追求更高频率,还是用硬件CS更稳。

3.3 驱动核心结构与fb_info注册流程

驱动这边的核心工作就是创建fb_info结构体,实现必要的fb_ops,然后注册到内核。整个过程可以简化为几个步骤:

  1. 在probe函数里解析设备树,获取DC、复位等GPIO,初始化SPI设备。
  2. 分配fb_info结构体:framebuffer_alloc
  3. 填充fb_fix_screeninfofb_var_screeninfo,包括分辨率、颜色位深、行字节数、显存地址等。
  4. 实现fb_ops中的fb_set_parfb_blankfb_fillrectfb_copyareafb_imageblitfb_mmap等回调。
  5. 分配显存,推荐用dma_alloc_coherent分配,这样可以保证SPI DMA读取显存时缓存是一致的。
  6. 调用register_framebuffer注册设备。

这里把fb_var_screeninfo的初始化的一个例子贴出来:

static int lcd_fb_probe(struct spi_device *spi) { struct fb_info *info; struct lcd_fbdev *fb; fb = kzalloc(sizeof(*fb), GFP_KERNEL); info = framebuffer_alloc(0, &spi->dev); ... fb->info = info; fb->spi = spi; strcpy(info->fix.id, "spi_lcd"); info->fix.type = FB_TYPE_PACKED_PIXELS; info->fix.visual = FB_VISUAL_TRUECOLOR; info->fix.line_length = 320 * 2; info->fix.smem_len = 320 * 240 * 2; info->var.xres = 320; info->var.yres = 240; info->var.xres_virtual = 320; info->var.yres_virtual = 240; info->var.bits_per_pixel = 16; info->var.red.offset = 11; info->var.red.length = 5; info->var.green.offset = 5; info->var.green.length = 6; info->var.blue.offset = 0; info->var.blue.length = 5; ... }

RGB565的位域定义这里绝对不能写错。如果RGB的offset写乱了,屏幕上就会出现颜色莫名其妙的偏色,比如红色和蓝色对调、绿色忽亮忽暗。这类问题还不报错,最难排查。

显存分配用dma_alloc_coherent,把返回的虚拟地址赋给info->screen_base,物理地址赋给info->fix.smem_start。用户态mmap时我们在fb_mmap回调里把这块DMA缓冲区映射给用户,这样就保证了用户写显存、SPI DMA读到的是同一份数据,不会因为CPU cache没回写而出现花屏。

3.4 帧缓冲刷新与SPI数据搬运实现

FrameBuffer模式下,LCD屏幕不会自己从内存拉数据,它只接收SPI送来的像素。所以驱动里要有一个“把fb显存内容刷到屏幕GRAM”的机制。最简单的实现是后台线程定时全屏刷新,或者在有写操作触发的回调里局部刷新。考虑到SPI屏主要显示静态页面,我用了定时刷新加脏标记的方式:用户往显存里写了数据,置脏标记,刷新线程检测到标记后把整帧数据通过SPI发送给屏幕。

核心发送函数大概是这样的:

static void lcd_fb_update_display(struct lcd_fbdev *fb) { struct fb_info *info = fb->info; u8 *buf = (u8 *)info->screen_base; int xres = info->var.xres; int yres = info->var.yres; struct spi_transfer xfer; struct spi_message msg; lcd_set_dc(fb, 0); lcd_spi_write_cmd(fb, 0x2A); // column address set lcd_set_dc(fb, 1); lcd_spi_write_data(fb, 0x00); lcd_spi_write_data(fb, 0x00); lcd_spi_write_data(fb, 0); lcd_spi_write_data(fb, 239); lcd_set_dc(fb, 0); lcd_spi_write_cmd(fb, 0x2B); // row address set lcd_set_dc(fb, 1); lcd_spi_write_data(fb, 0x00); lcd_spi_write_data(fb, 0x00); lcd_spi_write_data(fb, 0); lcd_spi_write_data(fb, 319); lcd_set_dc(fb, 0); lcd_spi_write_cmd(fb, 0x2C); // memory write lcd_set_dc(fb, 1); memset(&xfer, 0, sizeof(xfer)); xfer.tx_buf = buf; xfer.len = xres * yres * 2; spi_message_init(&msg); spi_message_add_tail(&xfer, &msg); spi_sync(fb->spi, &msg); }

画窗口地址时注意,ST7789的列地址和行地址对应的是GRAM坐标,而有些屏模组出厂时物理方向和FB坐标系不一致,就需要通过设备树里的rotate和驱动里的GRAM offset来调整。这块在后面的问题排查里还会细说。

4. 编译配置与启动验证

4.1 内核菜单配置与驱动编译

把驱动源码放到内核树的drivers/video/fbdev/目录下,然后在Kconfig里加一个配置项:

config FB_SPI_ST7789 tristate "SPI LCD ST7789 framebuffer support" depends on FB && SPI help Say Y here to enable SPI LCD framebuffer support.

执行make menuconfig,按以下路径打开配置:

Device Drivers ---> Graphics support ---> <*> Support for frame buffer devices ---> <*> SPI LCD ST7789 framebuffer support

同时确认SPI框架已经打开:

Device Drivers ---> <*> SPI support ---> <*> Rockchip SPI controller

如果你的BSP默认把DRM显示开着,且不打算用DRM输出任何内容,那可以保留DRM,但要注意fb设备节点的分配。fbdev子系统按注册顺序分配/dev/fb0/dev/fb1。如果DRM那边注册了一个fbdev模拟设备,SPI LCD的fb设备可能变成/dev/fb1,这对应用层使用影响不大,只要用户程序指定正确的设备节点就行。

如果不想编进内核,也可以用module方式编译,加载后观察dmesg有没有正确probe。

4.2 烧录启动后的三分钟验证流程

系统起来后,可以先快速验证驱动是否工作正常,整个流程就是经典的Linux frame buffer调试三步走。

第一步,查看fb设备是否存在,读取基本信息:

cat /proc/fb fbset -fb /dev/fb0

如果fbset能正确显示320x240, 16bpp,说明fb_info注册成功。

第二步,往fb设备里写随机数据,看屏幕有没有动态雪花:

dd if=/dev/urandom of=/dev/fb0 bs=1024 count=800

如果屏幕出现彩色噪点,说明SPI通路和GRAM刷新都正常。如果屏幕没有任何变化,优先查SPI通信和初始化序列。

第三步,把一个已知图片的raw数据写到fb,查看显示效果:

./fb_write_img /dev/fb0 logo.raw

fb_write_img可以用简单的C程序实现,打开/dev/fb0,mmap后直接把raw像素数据拷贝进去。如果图片方向不对或者颜色不对,再回设备树调整rotate和驱动里的RGB顺序。

另外还有一个非常实用的功能:内核FrameBuffer console。如果想让启动日志直接显示在SPI小屏上,内核配置里使能CONFIG_FRAMEBUFFER_CONSOLE,然后在内核启动参数里加上fbcon=map:0,这样系统日志就能直接出现在LCD上,调试时相当直观。不过有一点要提前说,SPI屏的刷新率本来就不高,滚动日志时会明显感到刷新慢,这是正常现象,毕竟带宽上限摆在那里。

5. 性能瓶颈分析:SPI带宽与刷新率优化

5.1 SPI屏刷新率的数学账

很多人对SPI LCD的刷新率没概念,我直接算一笔账。以320x240 RGB565为例,每像素2字节,一帧数据量是:

  • 320 x 240 x 2 = 153600 字节
  • 每字节在SPI总线上占8个bit
  • SPI时钟16MHz时,每bit耗时62.5ns,一字节耗时0.5us
  • 一帧纯数据耗时 153600 x 0.5us = 76.8ms

再加上命令、地址设置、D/C翻转、传输间隙,实际一帧刷新耗时可能在90ms左右。也就是说,16MHz SPI下,全屏刷新理论帧率只有11fps左右。

如果换成240x240的1.3寸屏,一帧数据量只有240x240x2=115200字节,纯传数时间57.6ms,加上开销后大概能到15fps。这个帧率对显示状态、菜单、仪表盘这类静态内容完全够用,但对视频播放就彻底没戏,这点在方案选型时就要想清楚。

5.2 提升刷新效率的几种实用手段

既然带宽有限,那就想尽办法减少无谓的数据搬运。

第一,能局部刷新就不要整屏刷新。实现一个脏矩形追踪机制,用户写显存后,驱动记录脏区域,刷新线程只把脏区域的像素发送到GRAM。对于大部分应用场景,只是某个数值变化或者图标闪烁,局部刷新能把有效刷新率提升好几倍。

第二,使用struct spi_transfer批量传输,避免逐字节调用spi_write。我在驱动里全屏一帧数据只发起一次spi_sync,这样既减少了GPIO翻转和内核调用的开销,也让SPI控制器有机会走DMA通道。

第三,调整SPI控制器DMA支持。RK3568的SPI控制器支持DMA传输,当一个spi_transfer长度较大时,框架会自动启用DMA。但从实际经验看,小长度传输走DMA反而效益不高,只有几KB以上的大块数据才值得用DMA搬运。全屏刷新这种场景天然适合DMA。

第四,降低LCD控制芯片的内部等待时间。ST7789在收到大量GRAM写入命令后,内部处理速度跟不上时会自动拉低busy信号,但SPI协议本身没有busy反馈,只能延时等方式处理。可以尝试在初始化时把FRMCTR1PORCTRL等寄存器调优,减少内部blanking时间,不过这需要对比屏厂规格书,不建议盲目改。

优化后的实测效果:16MHz SPI下,320x240全屏刷新从最初的11fps提升到约12fps,提升不算大;但如果改成局部刷新,状态栏区域100x30刷新一次只需要5ms,人眼看到的效果就是即时响应,体感提升非常明显。

6. 常见问题排查与调试心得

6.1 白屏、黑屏与无背光

这是SPI LCD最常见的三大症状,但根因各不相同。

如果是白屏且背光亮,说明屏有供电、背光正常,但GRAM没有任何数据显示。优先检查复位时序:RESET引脚是否按规格书要求拉低再拉高,复位后是否等了120ms以上再发初始化命令。ST7789这类控制芯片对复位时间很敏感,复位脉宽不够会导致初始化序列全部无效。

如果是黑屏且背光不亮,先查背光供电和PWM控制。用echo 1 > /sys/class/backlight/backlight/brightness测试一下,如果没反应,再看看设备树backlight节点和PWM引脚绑定的gpio是否冲突。

如果屏幕完全不显示但背光亮,且SPI时钟和数据都能在逻辑分析仪上看到波形,那就要怀疑初始化序列是否正确。很多山寨屏的初始化代码是从别的驱动里抄过来的,寄存器参数不适配,表现就是上电后屏幕不亮或者颜色异常。拿到屏先跟供应商确认控制芯片型号,再对照驱动IC规格书核对初始化命令。

6.2 花屏、错位与随机噪点

花屏的原因比白屏多得多,我按照排查优先级列一个速查表:

现象可能原因排查方向
规律性条纹或错位SPI模式不对(CPOL/CPHA)设备树里调整spi-cpol/spi-cpha
画面随机雪花SPI时钟过高降低spi-max-frequency到8MHz验证
左右或上下颠倒GRAM方向与物理方向不一致调整MADCTL寄存器或fb rotate参数
显示偏移了半个屏幕窗口地址设置错误核对0x2A/0x2B发送的坐标范围
色彩混乱但形状正常RGB位域配置错误核对red/green/blue偏移
只在快速刷新时花屏DMA数据与cache不一致改用dma_alloc_coherent分配显存

花屏问题里,最容易误导人的是SPI模式。设备树里spi-cpolspi-cpha看似只影响电气时序,但对控制芯片来说,采样沿错误会导致数据错位。STM32、单片机经验多的人到这里容易带偏节奏,因为单片机SPI初始化时设置的模式和Linux设备树里不是同一套命名逻辑,建议直接用逻辑分析仪抓波形,然后对照控制芯片数据手册里的时序图去核对。

6.3 屏幕闪烁与刷新残留

刷新残留常见于整屏刷新过程中,GRAM被分多次写入,用户看到上半帧和下半帧内容不一致。解决办法是把整帧缓冲准备好后一次性连续写入GRAM,中间不要穿插耗时操作。

闪烁则有三种情况。第一种是背光PWM频率过低,这种情况下看到的是整屏亮度周期性跳变,把PWM频率调到1kHz以上就能解决。第二种是刷新线程的定时器间隔不稳定,导致画面时快时慢,尽量把刷新线程优先级提高,并且使用hrtimer或者内核deferred_work代替基础定时器。第三种是LCD电压配置问题,屏幕内部VCOM电压偏移导致轻微闪烁,这个只能通过调寄存器参数解决,一般可以尝试改PVGAMCTRLNVGAMCTRL这些gamma寄存器或VCOM setting寄存器。

6.4 初始化序列等待时间的血泪教训

最后说一个容易复发的问题:初始化序列里的延时砍不得。很多驱动为了加快启动速度,会把ST7789初始化序列里的msleep改短甚至去掉,结果屏幕能亮,但显示一段时间后会出现颜色漂移、残影、甚至偶发黑屏。

原因很简单,控制芯片内部的DCDC和电荷泵需要足够时间稳定输出各路电压,比如VGH、VGL这些用于TFT开关的电压,如果没稳定就开启显示,TFT特性就会异常。我在实际项目中初始化序列分了三段:复位后等120ms,SLPOUT后等120ms,最后一个命令后至少等20ms再开启显示。这些延时是控制芯片的正常工作条件,不是可以随便优化的死代码。

另一个相关经验是初始化命令和数据之间必须严格区分D/C状态。如果DC引脚电平没切换到位,比如本该发数据时发成了命令,GRAM写入可能变成未知状态,表现就是某一整块区域的颜色不对。建议在SPI发送函数入口加一个调试开关,通过dev_dbg打印当前DC状态和发送的命令值,初期调试时全打开,稳定后再关掉。

如果你也正好在RK3568上接SPI小屏,这套流程可以直接照着走。整个方案里最难的不是驱动本身,而是Timing和初始化序列,遇到问题多抓波形、多对照控制芯片手册,比盲目改代码排查得快得多。

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

Windows 10 + Office 2019 正版重装完整教程:从备份到激活避坑指南

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

作者头像 李华
网站建设 2026/9/18 6:39:07

MCU开发四阶实战路线:硬件认知→裸机→RTOS→工程化

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

作者头像 李华
网站建设 2026/9/18 6:38:38

Echarts中国地图可视化实战:从GeoJSON到交互下钻全指南

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

作者头像 李华