news 2026/10/14 1:20:36

STM32寄存器白话指南:从GPIO到串口,手把手入门操作

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32寄存器白话指南:从GPIO到串口,手把手入门操作

刚接触STM32的时候,我抱着一本上千页的参考手册啃了一个礼拜,最大的感受不是“看不懂”,而是“不知道往哪儿看”。寄存器那一章全是地址、位号、读写属性,没有一句人话。后来靠着一块开发板反复试、反复查,才慢慢摸清门道:寄存器其实不复杂,它就是一排带名字的开关和旋钮,写在芯片里,你拨动它们,外设就干活。这本“白话手册”没有什么高深理论,就是把GPIO、串口、时钟这些最常见的寄存器用大白话拆开,告诉你怎么找、怎么配、怎么避坑。适合那些已经写过点库函数代码、但感觉底层还是黑盒的同学,也适合想从零开始理解寄存器开发思路的初学者。

现在网上的教程多半两种极端:要么直接扔代码让你抄,要么堆满术语让你背。所以我打算换一种写法,不抄手册原文,不列长表格,而是把你真正会用到的那几个寄存器拿出来,讲清楚“它是干什么的”“为什么要这样配”“写错了会发生什么”,再配上真实可跑的例程思路。

1. 为什么要“白话”讲寄存器

1.1 寄存器学习的两个真实痛点

第一个痛点是“多”,第二个痛点是“怕”。

先说说多。以常见的中容量STM32为例,参考手册里寄存器编号能排到几百个。你如果按顺序从头往后读,读不到一半就放弃了,因为绝大多数寄存器你这辈子可能都用不上。比如芯片唯一的96位ID号寄存器、温度传感器校准寄存器,这些不是入门阶段该花精力看的东西。

再说说怕。很多人学寄存器不是学不会,而是被吓住了。手册里每个寄存器都带一张位图,上面写着“Bits 31:16 Reserved”“Bits 3:2 MODE”“Bit 0 ODR”,还有一堆 RW、RO、RC_W0 这种缩写。新人一看就懵了——这些缩写是什么意思?我该往哪一位写什么?我写错了会不会把芯片烧了?

其实这些担心大多不必要。寄存器操作说白了就是“往固定地址的内存里写值”,芯片设计时已经做了保护,一般的配置错误顶多是功能不对,不会烧芯片。至于那些缩写,只需要记住几个常用的:RW(可读可写)、RO(只读)、RC_W0(读后自动清零,写0无效)、Reserved(保留位,必须保持默认值,别去动它)。

1.2 从库函数到寄存器:一次认知上的转换

如果你之前一直用标准库或者HAL库写代码,寄存器对你来说其实并不陌生。比如你用HAL_GPIO_WritePin点亮LED,库函数内部最终做的事情,就是给某个地址的某一位写0或写1。打开库函数的源码,你会看到类似这样的操作:

GPIOA->BSRR = (uint32_t)GPIO_PIN_5;

这里的GPIOA是一个指向GPIOA端口基地址的指针,BSRR是这个地址上的一个寄存器。所以库函数是“别人帮你把寄存器封装好了”,你点一下灯,背后就是一条寄存器赋值语句。

从库函数切换到寄存器,你的认知需要做一个转换:以前你调用的是“功能接口”,现在你操作的是“硬件本身”。同样点灯这件事,你不再关心有没有HAL_GPIO_WritePin这个名字,你只关心 GPIOA 的ODR寄存器第5位是1还是0。

这种转换一开始有点别扭,但一旦适应了,你会发现两个好处。好处一是代码量急剧减少,点灯只需要几行寄存器操作。好处二是排查问题的时候思路更清楚,芯片不工作,你能直接想到“是不是哪一位没配对”,而不是去库的深处翻一层层封装。

2. STM32寄存器体系速览

2.1 寄存器地址映射:外设的“门牌号”

要操作寄存器,首先要找到它。STM32的寄存器不是散落在内存里的,而是按外设分组,每组占据一段连续的地址空间。这个地址空间在芯片设计时就定死了,不会变。以常见的STM32F1系列为例,几个最重要的地址你最好背下来:

外设总线起始地址包含的外设
APB10x40000000USART2/3/4/5、SPI2/3、I2C1/2、TIM2~TIM7、PWR、DAC
APB20x40010000GPIOA~GPIOC、USART1、SPI1、TIM1、EXTI、ADC1/2/3
AHB10x40020000GPIOA~GPIOH(F4系列)、CRC、RCC、Flash接口
AHB20x40021000USB OTG FS、DCMI(F4系列)
RCC0x40021000复位与时钟控制

从这张表能看出一个大规律:所有外设寄存器都在 0x40000000 起步的外设地址空间里,不同总线对应不同的大网格,外设之间通过偏移量区分。手册里通常会给每个外设一个“基地址”和“偏移地址”,实际寄存器地址就是两者相加。比如 GPIOA 的基地址是 0x40010800(APB2总线上的F1),它内部的 GPIOA_CRL 寄存器偏移是 0x00,ODR 偏移是 0x0C,所以 ODR 的绝对地址就是 0x40010800 + 0x0C = 0x4001080C。

实际编程时我们当然不用手写这些绝对地址——难道你还去算?有问题。好在官方提供了头文件,里面对每个外设都定义好了结构体和指针。比如访问GPIOA的ODR寄存器,C语言里就是:

GPIOA->ODR &= ~(1 << 5); // 将GPIOA的第5位输出低电平

编译器会自动把GPIOA->ODR翻译成访问 0x4001080C 这个地址。但你需要理解这个换算过程,因为排查硬件问题时,有人会给你一张地址表,让你确认某寄存器到底落在哪个总线上,这时候你心里得有个数。

2.2 什么是最该先搞懂的三个寄存器组

整个STM32寄存器体系里,我建议你先盯住三个组:RCC组、GPIO组、还有你用到的外设组。RCC负责给外设供时钟,GPIO负责管引脚,外设组就是USART、TIM、SPI这些功能模块自己的寄存器。逻辑关系很简单:RCC里把某个外设的时钟打开,然后GPIO里把对应引脚配置成复用功能,最后在外设的寄存器里配置参数并启动。

很多新手一开始最容易犯的错,就是在RCC里忘了打开外设时钟,然后怀疑继电器为什么不动。时钟没开,你对这个外设的所有寄存器写入都是无效的,就像插座没通电,你插什么电器都不亮。所以记住这个顺序:先时钟,后配置。

以点亮LED最常用的 GPIOA 为例,第一步操作RCC的APB2外设时钟使能寄存器(RCC_APB2ENR),把 bit2(IOPAEN)置1,GPIOA 的时钟才开放。第二步才是配置 GPIOA 的CRL或者CRH寄存器,把引脚定义成输出模式。第三步才是操作ODR、BSRR来改变引脚电平。

GPIO相关的寄存器本身也不复杂,大体分两类:一类是配置寄存器(CRL、CRH),决定引脚是输入还是输出、跑多快、要不要上下拉;另一类是数据寄存器(ODR、IDR、BSRR、BRR),决定引脚电平是“0还是1”。理解了这一层,GPIO的“神秘感”基本就消失了。

2.3 为什总是绕不开“端口配置寄存器”

端口配置低寄存器(CRL)和配置高寄存器(CRH)是新手最容易一头雾水的两个寄存器。原因很简单:每个引脚在这个寄存器里占4位,而这4位又被拆成一个2位的MODE字段和一个2位的CNF字段。查表的时候,你要先看MODE,再看CNF,组合起来才是这个引脚的完整模式。

我写个例子。如果要让某个引脚(比如PA5)变成通用推挽输出、速度50MHz,那么:

  • MODE5[1:0] = 0b11,代表输出模式,最高速度50MHz
  • CNF5[1:0] = 0b00,代表通用推挽输出

CRL寄存器里,PA5对应的是第20到23位。所以代码这样写:

GPIOA->CRL &= ~(0xF << 20); // 先把这4位清零,防止旧配置影响 GPIOA->CRL |= (0x3 << 20); // 写入新的配置 0011

先清零再置位,这叫“读-改-写”操作,后面踩坑环节我会专门讲。现在市面上的教程很多直接写GPIOA->CRL |= 0x3 << 20,跳过了清零那一步,短期能跑,但一旦某个引脚之前被配置过其他模式,就会留下垃圾位,排查问题时会让你怀疑人生。

3. 手把手点亮一颗LED:寄存器操作完整拆解

3.1 第一步:拿到LED的原理图,算出“引脚坐标”

网上很多教程一上来就让你写代码,但真正的寄存器开发习惯应该是先看原理图。板子上LED通常接在某个GPIO引脚上,要么一端接引脚、另一端接电源,那么引脚输出低电平时LED亮;要么一端接引脚、另一端接地,需要输出高电平亮。这两种接法决定了你最后往ODR写0还是写1,别盲目抄代码。

比如某块开发板上LED1接在PB0上,LED2接在PB1上,都是引脚给低电平就亮。那么我的“引脚坐标”就是:端口B,引脚0和1。这意味着我要操作GPIOB,而不是GPIOA。

拿到这个坐标之后,后面所有代码都围绕GPIOB展开。先把GPIOB的时钟打开,再把PB0、PB1配成推挽输出,然后往ODR位0、位1写0,完事。就这么三步,每一步对应一组寄存器操作。

3.2 第二步:RCC时钟使能——为什么必须是它

RCC的英文全称是Reset and Clock Control,复位和时钟控制。GPIOB挂在APB2总线上,对应的使能位在RCC_APB2ENR的第3位(IOPBEN)。你找到这个位,置1,GPIOB就开始供时钟了。

代码只有一行:

RCC->APB2ENR |= (1 << 3);

但我还是想多说两句。如果你用的是F4系列,GPIO的时钟使能位在RCC_AHB1ENR里,因为GPIO从APB2移到了AHB1总线。这就是为什么网上的代码有时不通用——不是芯片坏了,是总线变了,寄存器也跟着变。你学习的时候要抓住“哪个外设挂在哪条总线上、对应哪个使能位”这个本质,而不是死记硬背某一行代码。

时钟使能之后,GPIO模块里的寄存器才“通电”。在这之前,你对CRL、ODR写任何值都会被忽略,或者说写进去了也不会生效。这个特性曾经坑过我一次,我调一个外部中断死活不触发,检查了半天发现是EXTI的时钟没开,中断请求根本进不了NVIC。所以我把这句话刻在脑门上:芯片里任何外设的第一句话,永远是打开它的时钟。

3.3 第三步:配置GPIO模式,注意CRL和CRH的分工

CRL管引脚0到7,CRH管引脚8到15,这是F1系列GPIO固定的规则。如果你的引脚编号小于8,去CRL找;大于等于8,去CRH找。这个“分界线”值8并不是随便定的,因为CRL和CRH都是32位寄存器,8个引脚每个占4位,正好8×4=32位填满。

对PB0来说,它落在CRL的第0到3位。MODE0占用第0到1位,CNF0占用第2到3位。LED是输出引脚,要推挽输出,速度选2MHz就足够了——LED翻转速度再快人眼也看不出来,选50MHz反而会增加电磁干扰。所以MODE0写0b01,CNF0写0b00,合起来一个nibble就是0b0001,也就是0x1。

于是配PB0和PB1的代码就是:

GPIOB->CRL &= ~(0xFF << 0); // 清零PB0和PB1对应的8个位 GPIOB->CRL |= (0x11 << 0); // PB0=0001, PB1=0001

这里用0xFF是因为要同时清两个引脚,每个4位。|= 0x11是分别在第0位和4位写入0b0001。写完之后,PB0和PB1就已经是2MHz推挽输出模式了。

3.4 第四步:输出电平,用BSRR而不是直接写ODR

现在到了点灯的最后一步:让PB0、PB1输出低电平。最直观的写法是往ODR相应位写0:

GPIOB->ODR &= ~(1 << 0); GPIOB->ODR &= ~(1 << 1);

这种写法能实现功能,但有一个隐患:ODR是“可读可写”寄存器,你执行“读-改-写”的期间,如果中断里也在操作同一个ODR,就可能出现竞态条件。比如主循环里读到的ODR是旧值,正准备写回时,中断把另一个引脚改成高电平了,可你写回去以后,那个引脚又变回低电平。

为了避免这种情况,芯片里设计了BSRR寄存器。它是个“写1生效”的特殊寄存器:往bit0写1,对应引脚输出高电平;往bit16写1,对应引脚输出低电平。写0没有任何作用。因为BSRR只响应写1,你去读它不会得到有效数据,所以它是“写1即生效,不需要读原值”。

用BSRR点灯,代码极度简洁:

GPIOB->BSRR = (1 << 16); // PB0输出低电平,LED亮 GPIOB->BSRR = (1 << 17); // PB1输出低电平,LED亮

这个写法不仅干净,还从根本上避免了读-改-写冲突问题。所以我个人建议,凡是“把某一位置0或置1”这类操作,FPIO的场合能用BSRR就用BSRR,不要碰ODR的“读-改-写”。

4. 串口发送一个字节:从寄存器到数据线的旅程

4.1 USART寄存器配置的关键顺序

点亮LED之后,第二个经典入门实验就是串口打印。串口的重要性不用多说,调试代码时printf输出是最常用的手段。但串口的寄存器配置比GPIO要复杂,因为它涉及两个硬件模块的配合:GPIO的复用功能配置和USART本身的工作模式配置。

在F1系列上,USART1的发送引脚TX默认是PA9,接收引脚RX是PA10,这两个引脚要配成复用推挽输出和复用浮空输入。GPIO部分要操作CRH寄存器,因为PA9、PA10都属于高8位引脚:

GPIOA->CRH &= ~(0xFF << 4); // 清空PA9、PA10的配置位 GPIOA->CRH |= (0xB << 4); // PA9: CNF=10(复用推挽), MODE=11(50MHz) // 注意这里0xB的二进制是1011,对应CNF=10, MODE=11 GPIOA->CRH |= (0x4 << 8); // PA10: CNF=01(浮空输入), MODE=00(输入模式)

PA9的配置里 CNF=10、MODE=11,组合是0b1011,也就是0xB。PA10作为输入引脚,只要把CNF设置成0b01浮空输入、MODE设成0b00即可,0b0100就是0x4。

配好引脚之后,再配USART1本体。步骤是:先设置波特率寄存器BRR,再设置控制寄存器CR1。CR1是USART的总开关,里面有几个关键的位:UE(USART使能)、TE(发送使能)、RE(接收使能)。这三个人位你要按顺序来,顺序搞反了可能不工作,老一点的参考手册里特别强调要先配好其他参数再使能UE。

4.2 波特率计算:一个值得亲手算的数

BRR寄存器的计算,是串口配置里最有“技术含量”的一步,其实也就是一次除法取整再取余。F1上USART1挂的是APB2总线,通常APB2的时钟是72MHz。目标波特率假设是115200,那么:

USARTDIV = 72000000 / (16 × 115200) = 39.0625

BRR寄存器是个16位的字段,高16位的小数部分是4位。小数部分怎么处理?把0.0625乘以16,得到1,正好也就会是整数。所以BRR = 39 × 16 + 1 = 625,十六进制就是0x271。

写进BRR:

USART1->BRR = 0x271;

如果你想要9600波特率,USARTDIV = 72000000 / (16 × 9600) = 468.75,整数468,小数0.75×16=12,BRR = 468×16+12 = 7500,也就是0x1D4C。这个计算过程就是手册里表格的来历,你会算了以后再也不用查表。实际开发中如果波特率出现乱码,首先该查的就是APB2时 钟是不是真72MHz——很多人外部晶振没起振,系统时钟跑在内部RC上,算出来的BRR当然全错。

4.3 发送状态的轮询与等待

配置完BRR和CR1,发送一个字节就很简单:把数据丢进数据寄存器DR,然后等发送完成标志位。关键是要理解DR寄存器的“一写就触发发送”特性,以及TXE和TC这两个标志位的区别。

TXE(发送数据寄存器空)表示你写的字节已经从DR挪到了移位寄存器,DR可以接收下一个字节了。TC(发送完成)表示整个字节的移位发送过程结束,引脚上已经空闲了。单字节发送,等TC是更稳妥的做法;连续发送多字节,等TXE就够了。

发送一个字节的代码模型:

void uart_send_byte(uint8_t ch) { while (!(USART1->SR & USART_FLAG_TXE)); // 等待DR空 USART1->DR = ch; while (!(USART1->SR & USART_FLAG_TC)); // 等待发送完成 }

我实际用的时候,通常会删掉第二个while,因为后面的字节马上要写DR时,TXE本身会保证前一个字节已经移出DR。但如果你发送完最后一个字节之后立刻进入休眠或者切换引脚模式,一定要等TC,否则最后一个字符会卡在半道,永远发不出去。

这里还藏着一个经典坑:读SR寄存器会清除某些标志位。如果你先读了SR来判断是否发完,再去写DR,有时候莫名其妙的第一个字节丢掉了。正确做法是先等标志,再写数据,不要反过来。

5. 踩坑实录:寄存器开发常见的5个问题

5.1 时钟没开,寄存器写了等于白写

这个坑我在前面已经强调过,但它实在值得再立一个条目。症状是代码明明写了某个外设的配置,程序却完全没反应。排查思路也很固定:第一看电源,第二看时钟,第三看配置,别一上来就怀疑芯片坏了。

有一次我用STM32驱动I2C总线,SCL和SDA引脚电平纹丝不动,怎么检查GPIO配置都是对的。后来查I2C外设的对应时钟使能位才发现,I2C挂在APB1总线上,而我一直在APB2的寄存器里找I2C的使能位——方向错了。找到APB1ENR里的I2C位并置1之后,问题立刻消失。

给你的建议是:写任何外设驱动之前,先打开参考手册的总线时钟使能寄存器那一页,用荧光笔把你用的外设对应位圈出来。这是最笨也最靠谱的方法。

5.2 位操作顺序错:先清位再置位

第二个高频坑就是我在GPIO配置时反复强调的“读-改-写”顺序。很多初学者图省事,配置CRL时只写了置位操作:

GPIOA->CRL |= (0x3 << 20);

如果PA5这个引脚之前已经被配置成模拟输入(MODE=00,CNF=00),这条语句会把MODE字段改成11,但CNF字段还是00,结果变成通用推挽输出,凑合也能亮。如果之前被配置成复用功能(CNF=10),那你这条置位语句会把CNF保留成10,MODE改成11,合成一个“复用推挽输出50MHz”,点灯就没反应了。

正确姿势永远是两步走:

GPIOA->CRL &= ~(0xF << 20); GPIOA->CRL |= (0x3 << 20);

有人嫌两步写起来麻烦,就搞了个一次性组合的计算方法,把要写的值和清零掩码拼起来。但我觉得清晰的代码比炫技更重要,尤其定期隔几个月回看你自己的代码时,能一眼看懂才是真功夫。

5.3 读SR标志位的“副作用”

USART的SR寄存器,里面有一堆状态标志位。手册里写了,很多标志位是“读后清零”的,比如TC标志,你读取SR时如果TC是1,读取动作本身就会把它清零。这在单线程轮询模式下没有影响,因为下次发送时TC又会置1。但一旦你引入了中断,就可能遇到这种情况:中断服务程序里读了一次SR,把TC清了,回到主程序再等TC标志,等了一万年也等不到。

解决办法是不要在主程序和中断里同时读同一个SR标志位。要么都归中断管,要么都归主循环管,或者用变量缓存一下标志状态。这种“读一个寄存器竟然会改变硬件状态”的行为,对新手真的很反直觉,但理解了之后,你会对硬件寄存器的“副作用”产生敏感——这是嵌入式高手的必备素质。

5.4 volatile修饰符与编译器优化

再说一个看着像软件问题、其实是硬件问题的事。你定义了一个全局变量来表示某种状态:

uint8_t flag = 0; while (flag == 0) { // 等待中断里把flag置1 }

在开优化(-O2)的情况下,编译器可能认定flag永远不会变,把while循环优化成死循环,中断里改了flag也跳不出来。为什么?因为编译器默认这个变量的值只会在本代码里改变,它不知道还有中断这回事。

解决办法也很简单:声明的时候加上volatile:

volatile uint8_t flag = 0;

volatile告诉编译器“这个变量的值可能在外部被修改,请你每次都从内存重新读取,不要放到寄存器里缓存”。我自己的习惯是:凡是中断里会修改的变量,一律加volatile;凡是寄存器映射的指针(比如GPIOA->ODR),CMSIS头文件里已经加过volatile了,不用再操心。

5.5 读-改-写的原子性问题

前面BSRR我已经埋了个伏笔,这里展开说。假设你在主循环里用ODR控制一个LED闪烁,用ODR |= (1<<5)把它置高。这时候来了一个外部中断,中断里为了另一个功能把同一个GPIOA的另一个引脚拉低:

// 中断服务程序里 GPIOA->ODR &= ~(1 << 6);

主循环的ODR |= (1<<5)看起来是“先读ODR再写ODR”,但中间这段读和写不是原子操作。如果中断恰好在主循环读完之后、写之前触发,那么中断里修改的那一位,在主循环写回时会被覆盖掉。症状是极偶然地出现某个引脚电平状态不对,很难复现。

这就是硬件设计者们为什么要提供BSRR/BRR这样的“写1生效”寄存器。BSRR里写1的那一位是独立的,写0的位不影响其他位,从根上消除了读-改-写窗口。所以遇到这种同端口多引脚操作频繁的场景,强制自己使用BSRR,别嫌它麻烦。

6. 从手册到代码:高效查阅STM32寄存器的方法

6.1 参考手册怎么翻才能快

STM32的参考手册动辄上千页,没有人会从头读到尾,但大家都需要能够迅速定位到关键信息。我的方法是先建立“树状结构”:第一层是总线架构,第二层是外设基地址,第三层是外设内部寄存器列表。有了这个框架,查找任何寄存器都能三步到位。

第一步确定外设挂在哪条总线上。查手册里的“内存映射”那一节,找到外设名,看它落在哪个地址范围。第二步确定外设基地址。第三步打开外设对应的章节,里面通常都有一张寄存器图或者寄存器列表,按偏移地址排序,找到你需要的那个寄存器。

看单个寄存器的时候,也别光看位图。位图右边通常有一列“复位值”,这列信息经常被人忽略,但它恰恰很关键。复位值告诉你这个寄存器上电时是什么状态,如果硬件默认已经使能了某个功能,你在配置时就得先把相关位清掉,否则旧配置会影响你的新配置。

6.2 我的个人学习路径和工具习惯

如果你问我寄存器怎么学最快,我的答案不是刷手册,而是做“最小系统实验”。找一块开发板,选一个最简单的目标——点灯、按键扫描、串口回环——每次只操作一个外设,用寄存器把整条链路打通。做完这三个实验,GPIO、时钟、中断、串口这些基础模块就都覆盖了。

我调试的时候习惯开着多个窗口:参考手册里放着外设章节,代码编辑器里看着自己的寄存器操作,调试器里实时观察寄存器的值。调试器有一个特别好用的功能是外设寄存器视图,它会把GPIOA的所有寄存器列出来,我可以直接看CRL的当前值,确认自己写的配置是否生效。这种“写一行代码、观察一次寄存器变化”的节奏,才是学寄存器最快的方式。

还有一个习惯是“注释里写明依据”。我在自己写的寄存器代码里,每一处关键配置都会注明数据手册的章节号和页码。比如:

// 参考手册 RM0008 第9.3.2节:IOPBEN位于RCC_APB2ENR的bit3 RCC->APB2ENR |= (1 << 3);

几个月后代码出问题,你能顺着注释翻回手册,效率高得多。而且这个习惯会迫使你自己去读手册原文,而不只是抄网上的代码——抄的代码早晚会让你踩坑,理解了之后写出来的代码才是你自己的。

本来还想写按外部中断的寄存器操作,但篇幅已经够了,那个知识点可以作为你点灯和串口之后的第三个实验自己去啃。坦白说,寄存器开发没有那么玄妙,核心就是地址、位、时序加上一点点耐心。我碰到过不少同学一开始觉得难,但亲手把寄存器从数据手册“翻译”成跑起来的代码之后,再回头去读参考手册,那种“原来如此”的感觉,是整个嵌入式学习中最爽的瞬间。

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

Scratch画圆进阶:用坐标与弧度实现圆心半径可控的圆

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

作者头像 李华
网站建设 2026/10/14 1:19:46

Python入门练手:逢七拍腿游戏实现与避坑指南

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

作者头像 李华
网站建设 2026/10/14 1:19:33

ICME23 知识蒸馏复现 · SLDNet阶段性沉淀

ICME23 知识蒸馏复现 阶段性沉淀项目&#xff1a;ICME 2023《Collaborative Spatial-Temporal Distillation for Efficient Video Deraining》 数据&#xff1a;NTURain&#xff08;合成测试集 8 序列 真实雨 7 序列&#xff09; 阶段&#xff1a;链路打通并验证&#xff0c;…

作者头像 李华
网站建设 2026/10/14 1:19:28

Python爬虫实战:requests+BeautifulSoup采集豆瓣电影TOP250

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

作者头像 李华
网站建设 2026/10/14 1:19:14

OpenSSL 3.0迁移实战:提供者机制与废弃API避坑指南

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

作者头像 李华
网站建设 2026/10/14 1:19:07

新媒体增长全链路:从引流到裂变的完整实操流程

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

作者头像 李华