news 2026/9/24 8:31:47

STM32定时器Encoder模式驱动EC11:从软件轮询到硬件正交解码的工程实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32定时器Encoder模式驱动EC11:从软件轮询到硬件正交解码的工程实战

早几年我刚开始碰STM32的时候,第一个让我纠结的外设就是EC11旋转编码器。这个东西在音量旋钮、数控电源、示波器菜单上遍地都是,但真到自己写驱动的时候,一直轮询GPIO电平、自己用状态机去解正交信号,代码又长又容易漏判,一转快了数值就乱跳。后来换成了定时器的Encoder模式,我才发现这东西本来就可以用硬件去解,根本不需要软件死磕。这篇文章就把我实际工程里怎么用STM32定时器Encoder模式驱动EC11、代码怎么从几十行缩到几行的完整过程写出来。不管是刚学STM32的新手,还是正在做毕业设计、做仪表项目的老哥,这套思路都能直接用。

1. 为什么放着轮询不用,非要折腾Encoder模式

1.1 轮询方案到底坑在哪

先说轮询。EC11编码器本质是两路正交方波信号,A相和B相,相差90度。很多人第一反应就是“那我直接读电平不就行了”,于是一开始我也这么干过:在主循环里不断读GPIO,然后根据A、B两相电平组合判断正反转。

这种做法理论上没错,但实际用起来全是坑。第一个问题是响应速度取决于主循环频率。如果你的主循环里还有屏幕刷新、按键扫描、PID运算,编码器转快了信号根本读不过来,一转快就丢步。第二个问题是代码量一点不小。你要维护上一次的状态,两个引脚的电平状态组合有4种,还要根据状态转移表判断是正转还是反转,一套状态机写下来少说五六十行,还要处理机械抖动、消抖延时,看着头大。

还有一个很多人没注意的问题:轮询方式下,编码器转动的瞬间CPU必须立刻响应,否则就丢脉冲。但如果此时CPU正在处理一个耗时操作,比如刷一块大屏、做浮点运算、写Flash,那编码器的脉冲就白白丢了。实时性完全没保障。

1.2 硬件正交解码才是正路

STM32的定时器Encoder模式,本质上就是硬件正交解码器。A相接定时器的CH1输入,B相接CH2输入,定时器会在硬件层面根据两路信号的相位关系自动加减计数。正转的时候计数器往上加,反转的时候计数器往下减,全程不需要CPU参与。

你想想这意味着什么:编码器转得再快,只要信号频率不超过定时器时钟频率(一般72MHz,编码器信号也就几十kHz),它一个脉冲都不会丢。CPU只需要在需要的时候去读一下CNT寄存器,看一眼转了多少、方向是什么。这就是硬件解耦带来的最大好处。

而且这个模式在STM32的中端型号里基本都有,F1、F4、F0这些系列都支持,不属于什么冷门功能。唯一要注意的是不是所有定时器都支持Encoder模式,一般是通用定时器TIM2、TIM3、TIM4、TIM5,高级定时器TIM1、TIM8也支持。实际项目里最常用的是TIM4,因为它的CH1、CH2在PB6、PB7,好走线,也不和串口、JTAG冲突。

1.3 什么时候值得用Encoder模式

用Encoder模式并不复杂,但要看你项目实际情况:

  • 只用一个编码器调节音量、切换菜单,这种简单交互其实用外部中断也行,Encoder模式属于杀鸡用牛刀,但也不算浪费。
  • 需要精确记录位移量(比如控制云台角度、电机位置),这种一定要用Encoder模式,硬件计数不丢步,精度有保障。
  • 系统主循环本来就很忙,编码器仅作为人机交互输入,这时候Encoder模式能让你的代码变得非常清爽,主循环里随便什么时候读一下计数都能拿到正确值。

我自己现在只要用到EC11,无脑上Encoder模式。配置一次之后,后面复制粘贴改个引脚就能用,省心程度跟轮询完全不是一个级别。

2. EC11编码器信号机制与定时器引脚分配

2.1 一个刻度到底发几个脉冲

很多人搜“EC11转一下发几个脉冲”,答案其实很直接:EC11一转一个刻度格,A、B两相各输出一个完整的方波周期,所以一个刻度对应一个脉冲。一圈一般是20个刻度,也就是20个脉冲。当然也有15、24这种规格的,但市面上最常见的EC11就是20脉冲一圈。

这里要强调一个大家经常混淆的点:一个脉冲不等于计数器只加1。如果你在Encoder模式里配置成“1倍频”(只在A相上升沿计数),那一个刻度计数值加1。如果你配置成“2倍频”(A相上升沿和下降沿都计数),那一个刻度加2。如果配置成“4倍频”(A、B两相的所有边沿都计数),那一个刻度加4。

实际项目里,如果你只是用来调参数,用1倍频就够了,一个刻度对应1,逻辑清晰,也好调试。如果你想要更高的分辨率,比如后面要接电机做位置环,那用4倍频能拿到4倍的反馈精度。注意EC11本身是机械触点输出,信号质量一般,太高倍频反而容易把抖动也算进去,所以人机交互场景1倍频最稳。

2.2 EC11硬件电路没那么简单

EC11编码器除了A、B两相信号外,还有一个按压开关引脚,按下时输出低电平。所以硬件上要接的东西有:A相、B相、按键引脚,加上VCC和GND,一共5个引脚。

关键的电气问题是:EC11内部是机械触点,不是推挽输出,低电平能拉稳,高电平必须靠外部上拉。STM32的GPIO可以配置内部上拉,所以理论上不接外部上拉也能工作。但实际经验是,内部上拉的阻值一般在30~50kΩ左右,对机械触点来说偏大,抗干扰能力不够强。我一般都直接在PCB上放两个10kΩ上拉电阻接到3.3V,A、B两相各一个。这样信号边沿陡、抗干扰强,实测明显比只靠内部上拉要稳。

还有一个高频踩坑点:机械触点导通瞬间会有抖动,时间大概在几毫秒到十几毫秒不等,如果不处理,Encoder模式会把抖动当成转动脉冲。软件上可以在读取到计数变化后加一个几毫秒的延时或者过滤,硬件上最简单的办法是在A、B两相到地之间各并联一个0.1uF的电容,把高频抖动滤掉。这两种方式可以结合,效果最好。

2.3 定时器引脚怎么选

以STM32F103为例,TIM4的CH1在PB6,CH2在PB7,这是最常用的编码器组合,几乎每块开发板上都有引出。TIM3的CH1在PA6,CH2在PA7,也可以,但PA6、PA7有时候会被SPI1或者ADC复用,要看你板子上的资源占用情况。

引脚选好后,要把它们配置成输入模式。很多人这里会犯一个错:以为接的是定时器通道,就要配置成复用推挽输出。完全不是这么回事,Encoder模式下这两个引脚是输入,要配置成浮空输入或者上拉输入,我一般选上拉输入,省掉外部上拉,当然外部上拉方案把内部上拉关了也行。

如果你用的是带引脚重映射的型号,比如STM32F103C8T6,要注意部分定时器通道默认引脚和重映射引脚的配置区别。默认情况下用PB6/PB7就不用开AFIO重映射,直接配GPIO就行。如果要用别的引脚,一定要在CubeMX里看清楚是否勾选了Remap,否则定时器输入根本引不到你接的那个引脚上。

3. CubeMX配置与工程搭建完整流程

3.1 用STM32CubeMX初始化定时器

现在做STM32开发,我基本都直接用CubeMX生成初始化代码,省事,也不容易漏配置。下面以STM32F103C8T6 + TIM4为例,把Encoder模式配置流程走一遍。

新建工程后,先把系统时钟配好,我习惯用外部晶振,配到72MHz主频。然后在左侧Categories里找到Timers,点开TIM4,把Slave Mode选成Encoder Mode,把Combined Channels选成Encoder Mode TI1 and TI2,这样就把TIM4的CH1和CH2配置成编码器输入了。

接着配置Counter Period,也就是自动重装载值ARR。这个值决定了计数范围,F1的定时器是16位的,最高65535。Encoder模式一般直接把ARR设成65535,让计数器在这个范围内自由加减,溢出后自动回绕,后面代码里做差值就能自然处理回绕问题。

在Parameter Settings里还有个Encoder Mode下拉框,里面有几个选项:TI1、TI2、TI1 and TI2,它们对应的就是前面说的1倍频、2倍频、4倍频。我做人机交互的旋钮用的是TI1 and TI2,这么选有点意思,TI1 and TI2其实是可以任意倍频的,但在F1的CubeMX里直接选TI1 and TI2就是4倍频。这里我建议选TI1,一个刻度计数值加1,最好用。如果你想要2倍频,选TI1和TI2中的a也行,大家自己试的时候可以留意一下。

接下来配置GPIO。在Pinout视图里,TIM4_CH1对应的PB6和TIM4_CH2对应的PB7会自动变成绿色。点开这两个引脚,把GPIO mode选成Input mode,Pull-up/Pull-down选Pull-up,速度随便选就行,输入模式速度不影响。

如果要用定时器更新中断来做溢出计数(后面会详细讲),还需要在NVIC Settings里勾选TIM4 global interrupt的Enabled。

这样初始化代码就生成好了,CubeMX已经把TIM4的底层初始化、GPIO初始化、NVIC配置全部搞定,你只需要在main函数里启动定时器就行。

3.2 关键参数背后的原理:为什么ARR要设65535

很多新手不理解ARR为什么非要设成65535。其实原因很简单:Encoder模式下,计数器是在一个范围内来回加减的,ARR设得越大,计数器能表示的数值范围就越大,越不容易溢出出问题。

如果把ARR设成100,那计数器加到100后会自动清零,如果你读到的计数值是10,但你不知道之前已经清过多少次零,那你就无法知道编码器总共转了多少。而ARR设成65535后,计数器在0~65535之间自由跑,你只需要在代码里定期读取并做差值,就能算出增量,即使发生回绕,差值计算也能自动修正。

这也引出一个关键点:如果你需要记录编码器的绝对位置,而不是增量,那ARR值就不应该设成65535,而应该根据你的实际范围设。比如你要调一个0~100的参数,可以把ARR设成100,计数器会卡在0~100之间,但这反而限制了溢出处理,所以我个人还是建议ARR设最大,然后用增量累加的方式去处理,灵活度最高。

3.3 初始化之后还必须做的两件事

CubeMX生成代码后,main函数里不会自动启动TIM4,你需要手动添加:

HAL_TIM_Encoder_Start(&htim4, TIM_CHANNEL_ALL);

这行代码的作用是启动编码器接口,让定时器开始采集A、B两相信号。忘了这行,编码器数据永远是0,很多新手卡在这里半天找不到问题。

另外,为了确保读取到的计数值是从0开始的,启动之前最好清一下计数器:

__HAL_TIM_SET_COUNTER(&htim4, 0);

放在启动之前,把CNT寄存器清零。这样整个工程的初始状态是确定的,后面读到的第一个差值就是真实的转动量,不会带一个随机偏置。

4. 核心代码实现:代码量减半的关键

4.1 三个顶层API搞定全部功能

代码量减半的核心,就是把“读取编码器”这个操作抽象成三个简单的函数。第一个是读取当前计数值,第二个是读取增量并清零,第三个是判断旋转方向。这三个函数加起来不超过20行,却能覆盖绝大部分应用场景。

先看最简单的读取函数:

int16_t Encoder_GetCount(void) { return (int16_t)__HAL_TIM_GET_COUNTER(&htim4); }

这里用(int16_t)强制转换,是为了把计数器的无符号数变成有符号数,这样当计数值超过32767时,读取出来就是一个负数,方便后面做加减判断。因为EC11一个刻度对应1个计数,一次操作顶多转几十个刻度,完全不会超过int16_t的范围,所以这样处理非常安全。

再看增量读取。假设每次读取之间,编码器转了N个刻度,正转是正N,反转是负N:

int16_t Encoder_GetDelta(void) { static int16_t s_last_count = 0; int16_t current_count = (int16_t)__HAL_TIM_GET_COUNTER(&htim4); int16_t delta = current_count - s_last_count; s_last_count = current_count; return delta; }

这个函数的精髓在于静态变量s_last_count记录上一次的计数值,每次调用都算差值。差值就是这段时间内编码器转了多少格,正负号就是方向。不管计数器怎么回绕,只要两次读取间隔内转动的刻度不超过32767,这个差值永远是对的。这比判断正反转然后手动累加要简洁太多。

最后一个方向判断函数:

int8_t Encoder_GetDirection(void) { int16_t delta = Encoder_GetDelta(); if (delta > 0) return 1; if (delta < 0) return -1; return 0; }

返回1表示正转,-1表示反转,0表示没转。实际项目里我一般直接用增量值做乘法,很少单独调这个函数,但它可以帮助刚接触的人理解方向是怎么判断出来的。

4.2 在主循环里怎么优雅地使用

有了这三个函数,主循环里的逻辑就非常清爽了。比如你要实现一个“旋转旋钮调节音量,按下旋钮确认”的功能:

while (1) { int16_t delta = Encoder_GetDelta(); if (delta != 0) { volume += delta; if (volume < 0) volume = 0; if (volume > 100) volume = 100; Volume_Set(volume); // 更新音量输出 Display_ShowVolume(volume); // 刷新屏幕 } if (Key_Scan() == KEY_PRESSED) { Settings_Save(volume); // 保存设置 } HAL_Delay(5); }

看到了吧,主循环里的编码器部分就这么几行。你不需要关心A、B两相的状态,不需要判断正反转,不需要写状态转移表,只需要读一个增量,然后按自己的业务逻辑处理就行。

我做的一个数控电源项目里,用同样的代码只改了变量名和范围限制,就实现了电压调节、电流调节、菜单切换三个功能,整体代码量比之前轮询方案少了一半不止。

4.3 按键检测也一起解决了

EC11内置的按键虽然和旋转信号是独立的,但项目里经常要一起处理。这里给一个简单的按键扫描函数,放在同一个文件里:

uint8_t Key_Scan(void) { static uint8_t s_key_state = 0; static uint32_t s_last_debounce_time = 0; uint32_t now = HAL_GetTick(); if (s_key_state == 0 && HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET) { s_last_debounce_time = now; s_key_state = 1; return 0; } if (s_key_state == 1) { if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET) { if (now - s_last_debounce_time > 20) // 消抖20ms { s_key_state = 2; return 1; // 检测到按下事件 } } else { s_key_state = 0; } } return 0; }

注意看,这个按键扫描用的是“按下后持续20ms才算有效”的消抖方式,不需要阻塞延时,非常适合在主循环里用。按下事件只需要执行一次,后续长按的话可以继续判断s_key_state的状态来扩展长按逻辑。

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

5.1 数值乱跳、一下加好几格

这是EC11调试里最常见的问题,我一共遇到过两次,一次是没接上拉,一次是机械抖动没处理。

如果你用的是内部上拉,先检查一下GPIO有没有配置成Pull-up。有些人CubeMX里默认没选,用的是浮空输入,那编码器高电平就是悬空的,信号一抖动数值就乱飞。换成上拉输入或者外接10kΩ上拉,问题基本能解决。

如果上拉没问题,就是抖动导致的。EC11内部是两个金属弹片在接触,转动瞬间会反弹,产生一连串的脉冲。软件上可以过滤:检测到计数变化后,在短时间内忽略后续变化。更简单有效的办法是硬件上在A、B两相对地各并一个0.1uF电容,实测能干掉绝大多数抖动。

还有一个容易被忽略的问题:你的主循环读取频率太慢,导致一次读取拿到了好几个刻度的差值。这不是故障,只是你处理不及时。如果你的程序要处理耗时的操作,建议把编码器读取放到定时器中断里,保证每次中断间隔都能及时采集增量。

5.2 只能加不能减(或者反了)

这个问题的原因很简单:A、B两相接反了,或者方向判断逻辑反了。解决办法也最直接:把A、B两相的连接互换一下,问题就解决了。

如果你不想动硬件,在CubeMX里也可以把TIM4_CH1和TIM4_CH2的输入极性配置反过来,或者在代码里把增量取反,都是三秒钟的事。但我建议养成好习惯:接线按固定的颜色规范来,比如EC11的A相接黄色线,B相接绿色线,别用了一两次又忘了哪个是哪个。

5.3 计数值卡在0或者65535附近不动

如果你发现编码器转到某个位置后,数值就不再变化了,多半是计数器碰到了0或者ARR值,而ARR设得太小了。

解决方法是把ARR设成最大值65535,然后按照前面说的差值法读取。这样计数器就是0到65535之间循环,不会出现“卡边界”的现象。只要两次读取间隔内的转动量不超过32767,差值法永远能算出正确的增量。

如果用了溢出中断来做计数扩展,还要检查溢出中断标志有没有正确处理。比如在中断回调里加了累计变量,但忘了清除更新标志,会导致中断反复进入,计数器看起来像在乱跳。

5.4 EC11转一下发几个脉冲的调试验证方法

这里分享一个判断编码器实际规格的小技巧。在调试时,用一个简单的测试程序:不断读取计数器值并打印,然后手动转一圈,观察计数值变化。如果一圈变化了20,说明你的EC11是20脉冲一圈,当前是1倍频;如果一圈变化了40,说明是20脉冲一圈但用了2倍频;如果一圈变化了80,那就是4倍频。

这个测试方法也适用于验证你的配置是否正确。比如你期望一个刻度一格的调节手感,但转一圈变化了80,那就说明你选了4倍频,把Encoder Mode改回TI1(1倍频)就可以了。

5.5 编码器模式与PWM输出冲突

热词里有人搜“STM32F103定时器PWM输出模式”和“STM32定时器中心对齐1和2有什么区别”,这里顺带提一个关键点:同一个定时器,Encoder模式和PWM输出模式不能同时使用。如果你用TIM4做编码器,就不能再用TIM4的某个通道做PWM输出了,因为定时器的比较寄存器、捕获寄存器已经被编码器的输入捕获占用了。

如果你既需要编码器输入,又需要PWM输出,有两个方案:一是用不同的定时器,比如TIM4做编码器,TIM2做PWM;二是检查该定时器是否还有未被编码器占用的通道可用,但一般Encoder模式会把两个输入通道都用上,所以最好还是换一个定时器。

中心对齐模式这里也顺带说一句:它主要用于PWM输出,和编码器模式没有直接关系,但如果你搜索时看到了这两个模式,不要混淆,Encoder模式下不需要关心中心对齐、边沿对齐这些PWM概念。

6. 从代码到工程:一些更深入的实战建议

6.1 定时器溢出中断能做更多事

前面演示的差值法,其实已经不需要溢出中断了,因为int16_t的差值计算天然容忍回绕。但如果你想做更复杂的事情,比如记录编码器总共转了多少圈,那溢出中断就有用了。

实现方式很简单:在定时器更新中断回调里,判断计数方向是正向溢出还是反向溢出,然后对圈数变量加1或减1:

extern int32_t g_total_count; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM4) { // 判断是向上溢出还是向下溢出 if (__HAL_TIM_IS_TIM_COUNTING_DOWN(&htim4)) { g_total_count -= 65536; } else { g_total_count += 65536; } } }

有了这个圈数扩展,你就能精确知道编码器累计转动的总步数,跟电机关联的场景会用到。

注意一点:STM32F103的定时器更新中断只告诉你计数器回绕了,但不会告诉你方向,所以要用__HAL_TIM_IS_TIM_COUNTING_DOWN去读方向寄存器。这个宏在HAL库里是有的,如果你用的标准库,可以对应读一下CR1的DIR位。

6.2 长按、双击这样的复杂按键逻辑怎么做

EC11的按键虽然简单,但如果你要做长按、双击,代码就得稍微设计一下。核心思路是用一个定时器(比如SysTick或者TIM2)提供时间基准,按键扫描时记录按下和释放的时间戳,然后根据时间差判断是短按、长按还是双击。

这里给一个简单的状态机框架:

typedef enum { KEY_IDLE, KEY_DOWN, KEY_UP_WAIT, KEY_LONG_PRESSED } key_state_t; key_state_t key_state = KEY_IDLE; uint32_t down_time = 0; void Key_Task(void) { uint8_t key_read = HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin); uint32_t now = HAL_GetTick(); switch (key_state) { case KEY_IDLE: if (key_read == GPIO_PIN_RESET) { down_time = now; key_state = KEY_DOWN; } break; case KEY_DOWN: if (key_read == GPIO_PIN_RESET) { if (now - down_time > 1000) { key_state = KEY_LONG_PRESSED; Key_ActionLongPress(); } } else { key_state = KEY_UP_WAIT; } break; case KEY_UP_WAIT: if (key_read == GPIO_PIN_SET) { if (now - down_time < 1000) { Key_ActionShortPress(); } key_state = KEY_IDLE; } break; default: key_state = KEY_IDLE; break; } }

注意我对长按的定义是按下超过1000ms就算,如果你要做双击,还需要在“短按抬起后的一段时间内再检测一次按下”,这个留给大家自己扩展。

6.3 低功耗场景怎么处理编码器

如果你做的是电池供电的便携设备,就要考虑进入低功耗模式时编码器怎么处理。

最简单的方案是:进入Stop模式前,把编码器所在定时器关闭,把A、B、KEY引脚配置成外部中断输入,让编码器转动时通过外部中断唤醒芯片。但要注意,唤醒后你必须重新初始化编码器接口,并且清理一下计数器,因为你在休眠期间可能会错过一些脉冲。

这个方案有个小坑:EC11的机械抖动可能会在休眠期间反复触发外部中断,导致频繁唤醒。解决办法是在外部中断回调里加一个消抖判断,确认电平稳定后再唤醒。实测下来,一档一档地转是没问题的,但如果你快速连转好几档,中间可能有脉冲丢失,需要接受这个精度损失。

6.4 这个编码器方案还能怎么扩展

聊完了EC11,其实这个思路可以扩展到很多地方。比如你用带ABZ输出的增量编码器(常见于电机),一样是接定时器的CH1、CH2,Encoder模式直接就能解出位置和方向,只不过要注意编码器输出的电平标准,有些是5V输出,需要做电平转换。

再比如你用的是带霍尔传感器的无刷电机,也可以利用定时器的Encoder模式来获取电角度信息,虽然一般有专用的电机控制外设,但做简单的转速测量足够了。

还有一个很实用的扩展:用Encoder模式配合Pulse计数,可以测频率。这时候并不需要两路正交信号,只要把一路信号接到CH1,另一路接地,配置成Encoder模式后计数器就能根据输入脉冲数计数,配合时间基准就能算频率。热词里有人搜“STM32定时器捕获测频率”,其实用Encoder模式反而更省事。

总之,定时器Encoder模式就是给你一个硬件计数的通道,具体怎么用完全看你的想象力。我用这个东西做过音量旋钮、数控电源的电压调节、示波器菜单选择,还有一个小型云台的角度反馈,都是一套代码换个参数就能用。

6.5 关于调试工具的一个小建议

调试编码器的时候,强烈建议用逻辑分析仪抓一下A、B两相的波形,而不是只用调试器看变量。逻辑分析仪能直接看出信号有没有抖动、相位关系对不对、转动频率是否过高。我遇到过很多次“代码没问题但编码器乱跳”的情况,最后都是靠逻辑分析仪发现是硬件接触不良或者上拉电阻虚焊。

如果没有逻辑分析仪,也可以用示波器看波形。关键看两点:一是高电平是否接近3.3V,如果只有2V左右说明上拉阻值可能太大;二是A、B两相的相位差是否保持在90度左右,偏离太远说明EC11的机械结构有问题。

用调试器看变量当然也能发现问题,但只能看到结果,看不到信号质量,排查起来效率低很多。真心建议做嵌入式的朋友备一个几十块钱的逻辑分析仪,关键时候能省好几个小时。

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

RV1126B MIPI-CSI图像采集失败的三大隐性断点与实操修复

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

作者头像 李华
网站建设 2026/9/24 7:52:34

树莓派串口全解析:UART、SPI、I2C与GPIO配置实战指南

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

作者头像 李华
网站建设 2026/9/24 7:36:48

STM32F407 USB Host直连4G模块:从硬件设计到AT指令状态机实战

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

作者头像 李华
网站建设 2026/9/24 7:35:51

ThinkPad UltraNav驱动v31.21.43.4安装与冲突排查:解决外接鼠标失灵

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

作者头像 李华