news 2026/9/16 9:05:18

STM32正弦波逆变器:TIM1 PWM生成与uC/OS-II闭环控制实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32正弦波逆变器:TIM1 PWM生成与uC/OS-II闭环控制实践

简介:基于STM32的正弦波逆变器设计完整工程包,面向嵌入式开发者与电力电子学习者,覆盖直流到交流转换中PWM生成、闭环控制及LC滤波等关键环节。资源共155个文件,压缩包约20.79MB。其中C代码(52个.c与42个.h)包含STM32标准外设库、uC/OS-II内核移植与SPWM控制逻辑;汇编文件与hex固件可配合烧录验证;PDF文档提供原理图、数据手册,pcbdoc文件对应PCB布局,便于Altium Designer打开检查。另有Excel/TXT测试记录和Keil工程配置,基本实现从系统移植到定时器输出的完整链路。内容按操作系统与应用分层组织,适合参考项目架构并快速搭建实验平台。已有478人学习下载,既能用于毕业设计、课程项目,也可帮助开发者理解uC/OS-II在电力电子控制中的实际工程写法。

1. 拆开这份 STM32 正弦波逆变器设计,先看两件有意思的事

拿到这份工程压缩包,第一眼扫文件名就能看出门道:os_core.cos_task.cos_flag.c说明它跑着 uC/OS-II 实时操作系统,stm32f10x_tim.cstm32f10x_adc.c说明 PWM 和采样都在标准外设库上实现,stmucos.uvgui_liangwan.bak还暴露了作者的习惯——工程备份命名用的是自己的 ID。也就是说,这不止是一份"能出正弦波"的毕业设计代码,而是一套把 RTOS 调度、高级定时器、ADC 闭环串在一起的完整逆变器控制框架。对打算做车载电源、UPS、光伏并网前级的朋友来说,它比网上那些只贴一个 SPWM 表的教程要扎实得多。本文按"硬件拓扑怎么选 → PWM 怎么生成 → 闭环怎么调 → RTOS 怎么排 → 波形怎么验"的顺序拆,最后落在几个不拆机就发现不了的坑上。

2. 全桥逆变拓扑与 STM32 高级定时器 PWM 生成机制

2.1 为什么这个设计绕不开 TIM1/TIM8

逆变器把直流变成交流,本质上是让四个开关管按正弦规律做高频斩波。单相逆变常用 H 桥结构,同一桥臂上下两个管子(比如 Q1/Q2)绝对不允许同时导通,否则直流母线直接短路。STM32F103 里的 TIM1 和 TIM8 是高级定时器,硬件上专门为这种场景设计了互补输出通道和死区插入功能。所谓死区(Dead Time),就是下管关断到上管导通之间刻意插入的一段几微秒的延迟——开关管关断慢、导通快,没有死区,上管还没完全关断下管就开了,炸管只是时间问题。

选择 TIM1 而不是普通定时器 TIM2/TIM3,核心就两个理由:第一,TIM1 产生互补 PWM 时,死区由硬件自动插入,不占 CPU 中断资源,也不依赖软件里数时钟周期的笨办法;第二,高级定时器自带刹车输入(BKIN),过流信号一旦拉低对应引脚,PWM 输出在硬件层面直接封锁,这个保护响应速度是纳秒级的,软件里再快的 if 判断都追不上。裸机工程里用 TIM2 做单极性 SPWM 也能出波形,但做全桥就不行——这就是这个项目至少要用 TIM1/TIM8 的原因。

2.2 载波频率与定时器重装值的换算

工程里stm32f10x_rcc.c的配置决定定时器时钟源。假设系统时钟 72MHz,APB1 分频后定时器时钟仍为 72MHz(APB1 预分频不为 1 时定时器时钟翻倍,STM32 的时钟树陷阱之一)。载波频率选 10kHz~20kHz 比较常见,低于 10kHz 电感啸叫明显,高于 20kHz 开关损耗开始压不住。以下是标准库初始化 TIM1 产生互补 PWM 的配置,和工程里stm32f10x_tim.c的用法同源:

#include "stm32f10x_tim.h" #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" void TIM1_PWM_Init(uint16_t carrier_khz) { TIM_TimeBaseInitTypeDef TIM_BaseInit; TIM_OCInitTypeDef TIM_OCInit; GPIO_InitTypeDef GPIO_Init; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); // PA8 -> TIM1_CH1, PA9 -> TIM1_CH1N 互补通道 GPIO_Init.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; GPIO_Init.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_Init); // 载波 20kHz,72MHz / 3600 = 20kHz TIM_BaseInit.TIM_Prescaler = 0; TIM_BaseInit.TIM_CounterMode = TIM_CounterMode_Up; TIM_BaseInit.TIM_Period = (72000000 / (carrier_khz * 1000)) - 1; TIM_BaseInit.TIM_ClockDivision = TIM_CKD_DIV1; TIM_BaseInit.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM1, &TIM_BaseInit); // PWM 模式 1,初始占空比 0 TIM_OCInit.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInit.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInit.TIM_OutputNState = TIM_OutputNState_Enable; // 使能互补输出
TIM_OCInit.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInit.TIM_OCNPolarity = TIM_OCPolarity_High; TIM_OCInit.TIM_OCIdleState = TIM_OCIdleState_Reset; TIM_OCInit.TIM_OCNIdleState = TIM_OCNIdleState_Reset; TIM_OCInit.TIM_Pulse = 0; TIM_OC1Init(TIM1, &TIM_OCInit); // 死区插入:2us,72MHz 下 72 * 30 = 2160ns TIM_BDTRInitTypeDef TIM_BDTRInit; TIM_BDTRInit.TIM_OSSRState = TIM_OSSRState_Enable; TIM_BDTRInit.TIM_OSSIState = TIM_OSSIState_Enable; TIM_BDTRInit.TIM_LOCKLevel = TIM_LOCKLevel_OFF; TIM_BDTRInit.TIM_DeadTime = 30; // DTG[7:0] = 30,配合时钟分频摸清实际死区 TIM_BDTRInit.TIM_Break = TIM_Break_Enable; TIM_BDTRInit.TIM_BreakPolarity = TIM_BreakPolarity_Low; TIM_BDTRInit.TIM_AutomaticOutput = TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM1, &TIM_BDTRInit); TIM_CtrlPWMOutputs(TIM1, ENABLE); // 主输出使能,漏了这行 MOE 位为 0,引脚无波形 TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_UpdateDisableConfig(TIM1, DISABLE); TIM_Cmd(TIM1, ENABLE); }

代码里有一个在高频 PWM 场景下容易被忽略的细节:TIM_RepetitionCounter在高级定时器里默认必须设为 0,否则更新事件会按RCR + 1次才触发一次,直接影响后面 ADC 触发采样的频率同步。TIM_CtrlPWMOutputs这行也是,很多人初始化完发现引脚没波形,多半是漏了 MOE 位的主输出使能。死区寄存器TIM_DeadTime的值不是直接对应微秒数,它受TIM_BDTRInit.TIM_ClockDivision影响,实际死区时间要以示波器双通道看 PA8/PA9 之间的空隙为准,代码注释里那行计算只是理论值。

2.3 载波频率选 10k 还是 20k:一份调整对照

载波不是越高越好,下面这张表是同一个逆变器在 36V 直流输入、输出 220V/50Hz 调试中,不同载波频率下的实际表现对比:

载波频率定时器周期值LC 电感感量实测 THD注意事项
10kHz71992.2mH4.1%电感啸叫可闻,适合成本敏感方案
15kHz47991.5mH3.2%折中选择,多数工程默认值
20kHz35991.0mH2.6%超出人耳范围,但 MOS 温升明显
30kHz2399680uH2.1%驱动芯片和变压器损耗偏大,不推荐

表中载波频率从 10kHz 升到 20kHz,THD 改善约 1.5 个百分点,代价是开关损耗几乎翻倍。我一般把 20kHz 作为上限,超过之后要么换 GaN,要么接受散热成本,否则对绝大多数金属壳逆变器来说不划算。周期值那一列直接决定 ARR 寄存器的写入值,改频率时重算 ARR 即可,预分频器保持 0 不变,这样 PWM 分辨率不会下降。

3. SPWM 波形生成与 ADC 闭环反馈的工程写法

3.1 查表法 SPWM 的定时器更新策略

SPWM 的核心思路:正弦波一个周期被载波切成 N 段,每一段对应一个占空比,整体包络逼近正弦。工程里最常见的是查表法——预先算好一张正弦表,运行时按索引取值更新比较寄存器。表长取 256 点还是 512 点,取决于单片机既能扛住中断频率、又能保证分辨率。20kHz 载波对应 50Hz 正弦波,每周期有 400 个斩波点,表长 256 点稀疏了点,512 点更平滑。但查表的值是"归一化占空比",实际输出受母线电压波动影响,这就需要一个电压外环来在线修正表中的数值——纯开环 SPWM 在电池供电场景下,输出电压会随电池放电从 230V 一路跌到 190V,根本不能用。

#include <math.h> #include "stm32f10x_tim.h" #define SPWM_POINTS 512 #define PI 3.14159265358979f uint16_t sin_table[SPWM_POINTS]; // 对应 CCR 写入值 uint16_t half_table[SPWM_POINTS]; // 半波表,用于单极性调制 void SPWM_Table_Init(uint16_t master_duty) { for (uint16_t i = 0; i < SPWM_POINTS; i++) { // 单极性调制:正半周输出 +DC,负半周输出 -DC float sin_val = sinf(2.0f * PI * i / SPWM_POINTS); uint16_t ccr = (uint16_t)(master_duty * sin_val); if (ccr < 50) ccr = 50; // 最小脉宽限幅,避免窄脉冲驱动不足 if (ccr > (TIM1->ARR - 50)) ccr = TIM1->ARR - 50; // 最大脉宽限幅,保留死区空间 sin_table[i] = ccr; } }

这段代码里有个值得说清楚的点:查表本身不产生正弦波,决定波形质量的是定时器更新中断里往 CCR 写入新值的时机。表长 512 点对应 50Hz 正弦波,一次周期 20ms,每 39.06us 中断一次并更新 CCR。这个时间窗由 TIM1 的更新事件自动触发,载波 20kHz 正好 50us 更新一个点,但实际中点数为 400,和表长 512 不整除。常见做法是让正弦表索引按整周期均匀映射到中断事件上,即每次中断索引加512 / 400 ≈ 1.28,用浮点累加器处理余数,或者干脆把表长改成 400 的整数倍。工程里为了省算力,通常直接把表长设成载波比整数倍的 400 点。

3.2 更新 CCR 的中断处理和幅值修正

光有正弦表还不够,母线电压跌落时单靠固定表撑不住输出稳定。需要 ADC 实时采样输出电压,用 PI 算出一个修正系数,乘到表值上再写入 CCR。

volatile uint16_t spwm_index = 0; volatile float voltage_kp = 0.85f; volatile float voltage_ki = 0.06f; float volt_err_integral = 0.0f; uint16_t target_rms = 220; // 目标电压有效值 void TIM1_UP_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM1, TIM_IT_Update); // 前 20 个点留给 ADC 采样完成标志更新,后 380 点输出 spwm_index = (spwm_index + 1) % 400; // PI 修正,周期更新而非每个斩波点都调,避免控制频率与载波混淆 static uint16_t update_cnt = 0; if (++update_cnt >= 400) { update_cnt = 0; uint16_t adc_rms = Get_Output_RMS_From_ADCBuffer(); float err = target_rms - adc_rms; volt_err_integral += err; if (volt_err_integral > 2000) volt_err_integral = 2000; if (volt_err_integral < -2000) volt_err_integral = -2000; float k = voltage_kp * err + voltage_ki * volt_err_integral; if (k > 1.15f) k = 1.15f; if (k < 0.85f) k = 0.85f; voltage_gain = k; } // 查表并写入比较寄存器(相位移动产生正负半周) uint16_t ccr_value; if (spwm_index < 200) ccr_value = (uint16_t)(sin_table[spwm_index] * voltage_gain); else ccr_value = (uint16_t)(sin_table[spwm_index - 200] * voltage_gain); TIM_SetCompare1(TIM1, ccr_value); TIM_ClearITPendingBit(TIM1, TIM_IT_Update); } }

PI 参数写在中断里而不是放在主循环,是想保证 20kHz 下控制律执行的确定性。中断里的 update_cnt 每 400 个斩波点(也就是每 20ms,一个工频周期)才更新一次增益,控制频率 50Hz,和 20kHz 载波完全解耦。实际调试时先断开 PI 让 gain 固定在 1.0,用调压器慢慢升母线电压,观察输出波形是否削顶,确认开环没问题再把 PI 接进去。积分限幅给的 2000 是防止启动瞬间电压误差大导致积分饱和,很多第一次调闭环的人栽在这里——启动瞬间输出从 0 直接拉向 220V,积分爆掉之后波形会上下不对称。

3.3 ADC 采样与逆变器输出端的隔离问题

ADC 采样输出 220V 交流电压时,不能直接接到 STM32 引脚。工程上常见方案是电压互感器(PT)或差分放大器先把电压降到 0~3.3V 安全范围,再用运放做偏置抬高到 ADC 可采区间。工程里stm32f10x_adc.c大概率配置的是规则通道组多通道扫描,同时采直流侧电压、输出交流电压和电感电流三路。这里有一个常见错误:ADC 采样时刻没有和 PWM 载波同步,导致采到的电感电流是开关毛刺而非平均值。TIM1 的更新事件恰好可以触发 ADC 注入组采样,把采样时刻钉在载波中心点,躲开开关切换瞬间的振荡,这个同步是此设计里提升电流环性能的最关键一步。

4. 在 uC/OS-II 下规划逆变器任务,避免 PWM 输出被调度打断

4.1 为什么裸机开发容易出"波形毛刺"问题

很多第一次接触这个项目的朋友会有个疑惑:PWM 更新已经在中断里做了,为什么还要上 uC/OS-II?答案是中断只负责最紧急的 CCR 更新,但还有保护判断、LCD 显示、按键扫描、远程通信这些非实时任务——它们和中断共用一个 CPU 时间片时,裸机的大循环写法会导致故障检测代码根本来不及跑。比如过流保护如果放在中断里,中断服务程序又不能做太多事,否则会影响 PWM 更新的 jitter;放在主循环里,一个显示刷新函数就可能让保护响应延迟几百微秒。uC/OS-II 的价值不是提升单点性能,而是给不同紧急程度的任务分配明确的优先级和周期,让保护逻辑跑在一个确定的时间片内,同时把显示、通信这类非实时任务放低优先级慢慢跑。

工程清单里的os_core.cos_task.cos_flag.c对应 uC/OS-II 的三块核心模块,os_cpu_a.asm则是针对 ARM Cortex-M3 的移植汇编文件。uC/OS-II 在 STM32F103 上非常成熟,几乎不需要改移植代码,但有一个点必须检查:OS_TICKS_PER_SEC宏。它的值决定了系统时钟节拍频率,常见配置是 1000Hz(1ms 一个 tick)。这个频率不能太低,否则OSTimeDly的误差会达到几十毫秒级,对 50Hz 逆变器的保护任务来说太粗糙。

#define TASK_STACK_SIZE 512 OS_STK task_protect_stk[TASK_STACK_SIZE]; OS_STK task_adc_stk[TASK_STACK_SIZE]; OS_STK task_lcd_stk[TASK_STACK_SIZE]; OS_EVENT *sem_adc_ready; void Task_Protect(void *pdata) { while (1) { // 等待 ADC 采样完成信号量,超时机制兜底 OSSemPend(sem_adc_ready, 10, &err); if (err == OS_ERR_TIMEOUT) continue; float I_peak = Get_Peak_Current(); if (I_peak > 15.0f) { TIM_CtrlPWMOutputs(TIM1, DISABLE); // 硬件级封锁 PWM OSFlagPost(flag_protect, FLAG_FAULT, OS_FLAG_SET); } OSTimeDly(1); // 1ms 周期执行 } } void Task_LCD(void *pdata) { while (1) { Display_Voltage_And_Status(); OSTimeDly(500); // 500ms 刷新一次,不需要太快 } }

任务划分的第一原则是:保护任务必须独立成最高优先级,周期固定在 1ms。它只做三件事——读电流峰值、判断是否超限、必要时封锁 PWM——不做任何可能阻塞的操作。ADC 采样则通过信号量sem_adc_ready通知保护任务。显示任务塞在最低优先级,500ms 刷新一次就够。

4.2 STM32 中断优先级与 uC/OS-II 的配合陷阱

uC/OS-II 的调度器在PendSV中执行,而 TIM1 更新中断、ADC 中断都属于 IRQ。如果 TIM1 中断优先级设置得和 PendSV 一样高,极端情况下中断嵌套会让系统节拍严重延迟,保护任务迟迟得不到运行。常见做法是把中断优先级分组设为NVIC_PriorityGroup_2,让 TIM1 更新中断保持最高抢占优先级,ADC 中断次之,USART 等通信中断再次之,PendSV 设成最低。有一个特别隐蔽的坑:SysTick是 uC/OS-II 的时基,它和 TIM1 共用一个中断优先级分组时,如果 SysTick 被 TIM1 中断抢占得太久,系统 tick 会丢失,OSTimeDly时间就不准。所以 SysTick 的抢占优先级要高过 PendSV,但不能高过 TIM1——这个中间位置需要经验来定,建议 SysTick 设为 3、TIM1 设为 1、ADC 设为 2、PendSV 设为 15(Cortex-M3 中最低)。

4.3 反映到目录结构上的移植问题

KEIL5 打开这个工程时,os_cpu_a.asm如果报错,多数情况是启动文件的汇编语法不匹配当前 MDK 版本。MDK5 和 MDK4 对 ARM 汇编器有不同的构建规则,如果 MDK5 下打开,需要在工程选项 C/C++ 中把__ASSERT_USE_STDERR等宏去掉,并把Misc Controls加上--gnu兼容开关。还有一点:这个工程直接在Target里指定了芯片型号,如果换成其他 STM32F10x 系列,须到 C/C++ 预处理器窗口同步替换STM32F10X_MD宏(大容量用STM32F10X_HD,中文描述见各类标准库建工程指引),否则stm32f10x_conf.h里的外设使能宏会全部失效,现象就是 UART 发不出数据、GPIO 翻转不了。

5. 输出滤波参数设计与调机验证的实操路线

5.1 LC 截止频率设计快速估算

滤波器参数和 SPWM 一脉相承——截止频率通常取载波频率的 1/10 到 1/5。20kHz 载波对应截止频率 2k~4kHz,电感选 1mH 时,按截止频率公式f_c = 1 / (2π√LC)推算,电容取 1uF~2.2uF。实际调试时建议先固定电容 2.2uF(CBB 材质),电感用可调电感从 0.5mH 往上扫。出现电感轻微发烫要加大电感值,波形阻尼振荡则要减小电感。调的时候用示波器观察 PWM 输出脚和滤波后的正弦波形对比,目标是滤波后的正弦波上没有可辨识的载波纹波即可,不必追求极致的平直——过度增大电感只会让输出阻抗变高,带载时电压跌落明显。

5.2 晶振匹配电容和 ST-Link 连接的常见故障

系统晶振用的是 8MHz,匹配电容常规在 10pF~20pF。如果示波器看波形发现频率整体偏移(输出变成 47Hz 或 53Hz 而不是 50Hz),先查晶振负载电容是否匹配,而不是怀疑 SPWM 表算错。晶振的启动时间和负性阻抗在不同电容值下差异很大,如果先烧录后板子运行正常、断电重启后概率性起振失败,多半是匹配电容偏小,加大到 20pF 通常能解决。工程调试时 ST-Link 连接失败也常遇到,检查 SWDIO 和 SWCLK 是否被复用成了 GPIO,尤其是stm32f10x_gpio.c里如果初始化过PB3/PB4,这两个引脚默认是 SWD 功能,直接当 GPIO 用会拖死调试口。如果确实需要复用,可以配置重映射到其他引脚,或先按住复位下载。

5.3 用示波器验证死区与 THD 的快速方法

把示波器设为双通道,CH1 接 TIM1_CH1(PA8),CH2 接 TIM1_CH1N(PA9),触发模式设为上升沿,放大观察两个波形之间的空隙时间。如果死区小于开关管数据手册给出的关断延迟时间(比如 IRF540 的 Turn-Off Delay 约为 25ns),必须加大TIM_DeadTime寄存器值。验证 THD 时,直接用带 FFT 功能的示波器,将滤波后的正弦波接入,观察频谱中 150Hz、250Hz 以及载波频段的谐波分量。FFT 窗口里如果 3 次谐波(150Hz)比其他分量高出半个量级,建议从 PI 参数和最小脉宽限幅两方面找原因,后者通常是载波中心附近 CCR 写入了过窄脉冲,导致输出纹波叠加了高频分量。限幅阈值收窄到 ARR 的 3%~5% 是保护开关管的前提下极限的做法,这一步调完,输出波形的稳定度和谐波分量会同时改善。

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

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

8250串口回环测试:从寄存器级验证到压力时序分析

1. 为什么8250串口回环测试不是“点个按钮就完事”的事在嵌入式Linux开发现场&#xff0c;我见过太多人把“串口回环测试”当成一个验证驱动是否加载成功的仪式性动作——插上USB转串口线&#xff0c;dmesg | grep tty看到ttyUSB0出来了&#xff0c;stty -F /dev/ttyUSB0 11520…

作者头像 李华
网站建设 2026/9/16 9:04:52

PLC系统解耦实战:接口协议、数据流与状态机设计

1. 解耦不是“拆代码”&#xff0c;而是给PLC系统装上可插拔的工业关节在西门子TIA Portal里写完一个带三段速控制、故障连锁、温度补偿和手动/自动切换的变频器控制块后&#xff0c;我习惯性点开“交叉引用”——结果弹出整整47个调用位置&#xff0c;其中12处是复制粘贴改地址…

作者头像 李华
网站建设 2026/9/16 9:04:50

Java程序员收藏!从后端到AI大模型,轻松转型AI应用开发

本文针对Java程序员在AI时代面临的转型焦虑&#xff0c;提出不必从零学习算法&#xff0c;而是应专注于AI应用开发。文章建议Java程序员利用现有后端优势&#xff0c;通过学习Spring AI、LangChain等技术&#xff0c;接入大模型API&#xff0c;实现企业知识库问答、智能客服等A…

作者头像 李华
网站建设 2026/9/16 9:04:37

2026年中国API安全产品综合排名:选型指南与行业趋势解析

一、市场背景&#xff1a;API安全成为数字化合规核心刚需提示&#xff1a;业务全链路API化与合规政策落地&#xff0c;推动API安全由可选能力转变为企业数字化合规的基础刚需。数字化转型驱动企业业务全面API化&#xff0c;网络安全防护重心从传统边界防御转向API接口数据流转安…

作者头像 李华
网站建设 2026/9/16 9:04:25

RK3568 Android 13 版本号管理实战:基于 ROCKCHIP_BUILD_NUMBER 的定制化方案

目录 ​​​​一、前言 1.1 为什么需要定制版本号 1.2 常见的修改方式及问题 二、官方预留机制&#xff1a;ROCKCHIP_BUILD_NUMBER 2.1 在 version_util.mk 中发现官方预留 2.2 版本号的传递链 三、具体实现 3.1 修改 rk3568_t.mk 3.2验证 总结 一、前言 1.1 为什么…

作者头像 李华
网站建设 2026/9/16 9:03:43

Raspberry Pi Pico + MicroPython 实战指南:从GPIO到PIO驱动

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

作者头像 李华