news 2026/10/29 13:12:15

GD32单MCU三相四桥臂逆变器:10路PWM与15路采样边界

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GD32单MCU三相四桥臂逆变器:10路PWM与15路采样边界

简介:《基于GD32F330的三相四桥臂逆变电源设计》面向电源工程师、逆变器开发者及国产MCU学习者,围绕GD32F330RBT6在三相四线UPS中的实现展开。内容涵盖系统结构、10路PWM与15路AD分配、推挽升压DC/DC单电压环控制(100kHz)、三相四桥臂R/S/T电压外环加电流内环与N相开环控制(19.2kHz),并涉及软件锁相、串口通信、Flash模拟EEPROM、电池管理、过欠压/过流/短路/过温保护及在线升级。资源包为1个PDF文件,约185KB,篇幅紧凑,便于对照硬件选型、算法移植和引脚规划。已有533人学习,可帮助读者梳理国产芯片方案的模块划分、保护逻辑及关键控制参数。

1. 一块 MCU 扛下 10 路 PWM 和 15 路采样:这套 gd32 三相逆变器方案的真实边界

做三相四线输出的电源,多数人第一反应是三桥臂加一对分裂电容,简单、成熟、少一个桥臂。可只要负载稍微不平衡,中点电位就开始飘,三相输出电压跟着歪,带单相满载时波形更是没法看。这套基于 gd32 平台的三相逆变器方案走的是另一条路:多一个桥臂专门去控中点,用一颗国产芯片 MCU 把推挽升压、四桥臂逆变、充电管理、保护逻辑和串口升级全兜住。它适合已经摸过 PWM 和 ADC、想完整走一遍数字电源闭环的中级硬件或嵌入式工程师,也适合手里正好有电池组和三相负载、需要一个可复现参考设计的团队。完全没碰过功率级的人先别急,这类板子上电即炸管,没有中间态可以试错。

2. 能量链路拆开看:推挽升压加四桥臂的分工与选型理由

2.1 从市电到电池再到母线:三段式能量流为什么要这么切

系统的入口是三相四线市电,先经过充电电路给电池组补能;市电掉电后,能量反过来由电池经 DC/DC 升压到高压母线,再由三相四桥臂逆变器逆变成三相交流输出。这条链路被切成"充电"和"放电"两段独立通路,不是为了画图好看,而是因为两段的工作点差异太大:充电侧要控电流、要判电池状态,电压等级低、动态慢;放电侧要在毫秒级把母线立起来,还要顶住负载突变。两条通路分开,控制目标互不打架,故障也能隔离——充电回路短路不该把逆变输出一起拉死。

母线必须独立升压这一步没什么可省的。电池组是低压直流,常见几十伏量级,要逆出三相四线的相电压和线电压,母线电压得显著高于线电压峰值,否则调制比直接顶到过调制区,波形削顶、谐波爆表。所以"电池 → 推挽升压 → 高压母线 → 四桥臂逆变"是这类中小功率储能逆变器的标准骨架,不是这一份设计独有的选择。

控制板的位置在中间:它同时管 DC/DC 和逆变两级,采样 15 路模拟量,输出 10 路 PWM。整个系统只有一颗 GD32F330RBT6,没有第二颗协处理器。这个约束决定了后面所有的软件结构——中断预算、采样时序、保护响应,都得在这一颗芯片里算清楚。

2.2 推挽拓扑为什么适合低压侧,以及 100kHz 开关频率的取舍

低压侧升压,可选的拓扑无非推挽、全桥、Boost 加隔离。这套设计选的是经典推挽升压:变压器原边中心抽头接电池正极,两个主开关管轮流导通,副边整流后经 LC 滤波得到母线。推挽最大的好处是驱动简单——两个管子的源极共地,不需要高边自举电路,也不需要隔离驱动,一路互补 PWM 加死区就能推。对成本敏感的板子,这一条就值回票价。

代价也很明确。推挽的变压器利用率只有单端的一半,同样功率下磁芯要大一号;两个主管的导通时间必须严格对称,否则直流分量累积会把磁芯推向饱和,这是血泪级的坑;变压器漏感在关断瞬间会和开关管结电容谐振,产生电压尖峰,母线上常见做法是加 RCD 吸收或者副边全桥整流后加钳位。这几条在原理图评审时要一条条对着看。

开关频率定在 100kHz,是一个体积和损耗的折中。频率往上走,变压器和滤波电感能缩小,但磁芯损耗按频率的 1.6 次方左右涨,开关损耗也跟着涨;频率往下走,变压器体积顶不住,动态响应也变慢。中小功率的推挽升压在 50kHz 到 150kHz 区间最常见,100kHz 落在一个比较舒服的位置:磁芯选常见的功率铁氧体就能压住温升,变压器也不用做太大。

注意:推挽两级主管如果共用一个定时器的两个通道,务必确认两路占空比之和严格小于 100%,留出的就是死区。软件里不要指望"我算出来刚好 50/50",一定要在寄存器层面钳位。

2.3 四桥臂而不是三桥臂加分裂电容:中点可控性换来的代价

三桥臂逆变器带三相三线负载没问题,一旦要出中线、要带单相负载,就必须处理中点。传统做法是母线用两个电容串联造一个虚拟中点,但电容的均压能力有限,负载不平衡时中点电位会跟着负载跑,输出三相电压立刻不对称。要压住这个偏移,电容得做得很大,体积和成本都上去了。

四桥臂的思路是:再加一个半桥,专门驱动中线。这个桥臂可以主动往中点注入或抽取电流,把中点电位摁在想要的位置。带来的好处很直接——不平衡负载下三相电压仍然对称,直流电压利用率也比分裂电容方案高,因为不用给电容均压留裕量。对带单相负载、带非线性负载的场景,这个优势是实打实的。

代价同样要说清楚。多一个桥臂就是多两个功率管、多一路驱动、多一路 PWM;N 相电感要单独设计,电感量、饱和电流都得算;控制上多一个自由度,N 相的调制波怎么给,需要额外的算法;EMI 上共模噪声路径也变了。更关键的是,四桥臂的 N 相要真正闭环,必须有 N 相电流采样——而这份设计采集的 15 路模拟量里,只有 R、S、T 三相电感电流,没有 N 相电流。这就决定了 N 相只能开环,靠三次谐波注入的零序分量去驱动。

一句话总结选型逻辑:如果你只带三相对称负载,三桥臂更划算;只要有不平衡负载或者单相负载,四桥臂的中点可控性值这个成本,但前提是你接受 N 相开环带来的边界。

2.4 一颗 RBT6 扛全部控制:外设资源是怎么被榨干的

GD32F330RBT6 是 64 脚封装,选它而不是更小的 48 脚型号,核心原因就是引脚数不够用。数一下需求:10 路 PWM 占 10 个脚,15 路模拟输入占 15 个脚,串口收发 2 个脚,SWD 调试 2 个脚,再加上继电器控制、风扇、指示灯、电池检测使能这些零碎 GPIO,48 脚根本排不开。RB 后缀的存储容量也留了余量,控制环、保护阈值、串口升级的 bootloader 都要占空间。

外设分配上,三相互补 PWM 交给 TIMER0——这是片上的高级定时器,CH0/CH1/CH2 三路都带互补输出和硬件死区,正好对应 R、S、T 三相的上下管。N 相单独用 TIMER15 的 CH0 加互补通道,它同样带死区和互补能力。DC/DC 推挽用 TIMER2 的 CH0/CH1,通用定时器足够,推挽不需要互补死区之外的复杂功能。

模拟侧是这套设计里比较紧的地方。15 路采样全部落在 ADC0 的通道上:PA0 到 PA7 对应 IN0 到 IN7,PB0、PB1 对应 IN8、IN9,PC0、PC1、PC2、PC4、PC5 对应 IN10 到 IN15,中间 PC3 空着。这意味着 15 路必须靠规则组序列扫描加 DMA 搬运来完成,没有第二组 ADC 帮你并行分担。后面第 3 章会详细算这笔时序账,先记住这个结论:采样时间是这套设计里第一个要抠的参数。

3. 10 路 PWM 与 15 路采样:引脚表怎么落成能跑的初始化代码

3.1 PWM 引脚分配表的读法:三组定时器各管一段

先把表里的映射关系理清楚。DC/DC 推挽占两路,PC6 走 TIMER2_CH0,PC7 走 TIMER2_CH1,这两路是推挽的两个主管,互补导通,各占略小于 50% 的占空比。表里两路都标成 DCDCPWM1,按通道号理解就是推挽的两只管子,画原理图时别被这处笔误带偏。

逆变侧占八路,分四组:

桥臂上管引脚复用功能下管引脚复用功能
R 相PA8TIMER0_CH0PB13TIMER0_CH0_ON
S 相PA9TIMER0_CH1PB14TIMER0_CH1_ON
T 相PA10TIMER0_CH2PB15TIMER0_CH2_ON
N 相PB8TIMER15_CH0PB8 对应互补TIMER15_CH0_ON

R、S、T 三相全部挂在 TIMER0 上,共用同一个计数器,这一点非常关键:三相同步调制、死区一致、载波相位严格对齐,不会出现某一相载波错位导致的环流。N 相单独用 TIMER15,代价是它的载波和前三相不同源,如果两颗定时器同时启动有偏差,N 相桥臂的开关时刻会和三相错开一点,共模噪声会略差。常见做法是在初始化时用同一个触发源同时启动两颗定时器,把偏差压到一个时钟周期以内。

表里 N 相下管写作 PN8,这是原文档的一处笔误,按 PB8 理解即可,PB8 在 GD32F330 上确实可以复用成 TIMER15_CH0_ON。这类笔误在抄板子的时候最要命,画 PCB 之前一定对着数据手册的复用功能表逐脚核一遍。

3.2 中心对齐与硬件死区:为什么死区不能靠软件插

三相逆变用中心对齐模式比边沿对齐更合适。中心对齐时计数器先加到峰值再减回零,一个开关周期内比较匹配两次,输出波形关于周期中心对称,低次谐波明显更干净,ADC 也正好可以在计数器峰值那一刻触发采样,避开开关动作的噪声窗口。

中心对齐下的频率公式是 f = TIMER_CK / (2 × (ARR + 1)),比边沿对齐多一个 2 倍。以 19.2kHz 为目标,定时器时钟按实际系统填,ARR 要按这个公式倒算,不能拿边沿对齐的算法直接套。

/* 三相互补 PWM 初始化:TIMER0 中心对齐 + 硬件死区 */ #define INV_PWM_HZ 19200U /* 逆变开关频率 19.2kHz */ #define TIMER0_CK 120000000U /* 按实际定时器时钟修改 */ #define INV_ARR ((TIMER0_CK / (2U * INV_PWM_HZ)) - 1U) /* 3124 */ #define INV_DEADTIME 120U /* 死区计数值,单位 tDTS,见下文换算 */ static void inv_pwm_init(void) { timer_parameter_struct tp; timer_oc_parameter_struct oc; rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_GPIOB); rcu_periph_clock_enable(RCU_AF); rcu_periph_clock_enable(RCU_TIMER0); /* 上管:复用推挽输出,高速档 */ gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10); /* 下管:互补通道,同样复用推挽 */ gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15); timer_deinit(TIMER0); tp.prescaler = 0; /* 不分频,死区分辨率最高 */ tp.period = INV_ARR; tp.clockdivision = TIMER_CKDIV_DIV1; tp.counterdirection = TIMER_COUNTER_UP; tp.alignedmode = TIMER_COUNTER_CENTER_BOTH; /* 中心对齐,上下都产生事件 */ tp.repetitioncounter = 0; timer_init(TIMER0, &tp); /* CH0/CH1/CH2 全部配成互补 PWM 输出 */ timer_channel_output_struct_para_init(&oc); oc.outputstate = TIMER_CCX_ENABLE; oc.outputnstate = TIMER_CCXN_ENABLE; /* 互补通道必须打开 */ oc.ocpolarity = TIMER_OC_POLARITY_HIGH; oc.ocnpolarity = TIMER_OCN_POLARITY_HIGH; oc.ocidlestate = TIMER_OC_IDLE_STATE_LOW; oc.ocnidlestate = TIMER_OCN_IDLE_STATE_LOW; timer_channel_output_config(TIMER0, TIMER_CH_0, &oc); timer_channel_output_config(TIMER0, TIMER_CH_1, &oc); timer_channel_output_config(TIMER0, TIMER_CH_2, &oc); /* 死区:寄存器只接受 8 位编码,换算关系见下面的说明 */ timer_channel_output_deadtime_config(TIMER0, INV_DEADTIME); timer_channel_output_mode_config(TIMER0, TIMER_CH_0, TIMER_OC_MODE_PWM0); timer_channel_output_mode_config(TIMER0, TIMER_CH_1, TIMER_OC_MODE_PWM0); timer_channel_output_mode_config(TIMER0, TIMER_CH_2, TIMER_OC_MODE_PWM0); timer_primary_output_config(TIMER0, ENABLE); /* 高级定时器主输出必须使能 */ timer_auto_reload_shadow_enable(TIMER0); /* ARR 写入进影子寄存器,防撕裂 */ timer_enable(TIMER0); }

这段代码里有几个容易忽略的点。oc.outputnstate必须显式设成TIMER_CCXN_ENABLE,否则互补通道根本没输出,示波器上只能看到上管波形,会误判成下管驱动坏了。timer_primary_output_config是所有高级定时器输出的总闸,不打开的话三路 PWM 全部挂死——这个坑我见过太多次,新手排查半天以为定时器没跑起来。timer_auto_reload_shadow_enable打开后 ARR 写入先进影子寄存器,在下一个更新事件才生效,避免运行中改周期把波形撕开。

死区值这块要单独讲。死区寄存器是 8 位,不是一个线性的计数值,而是分段的:编码小于 128 时,步进等于一个 tDTS;128 到 191 之间,步进是两个 tDTS;192 到 223 之间步进八个 tDTS;224 到 255 之间步进十六个 tDTS。tDTS 就是定时器的计数时钟周期。假设定时器时钟 120MHz,tDTS 约 8.33ns,那么低段的死区分辨率就是 8.33ns,最大只能做到 127 × 8.33ns 约 1.06us。如果功率管是 IGBT,需要 2us 以上的死区,低段就不够用,必须切到高步进段,代价是分辨率变成一百多纳秒一档,没法精细调。

实际配置时先定目标死区时间,反推编码值,再核对落到了哪一段、实际误差多少,最后一定要用示波器双通道看上下管驱动波形的重叠区,别只信计算。

3.3 15 路 AD 全在 ADC0:序列扫描加 DMA 的时序账

15 路模拟量全部挂在 ADC0 上,这是这套设计的硬约束。ADC0 的规则组最多支持 16 个转换序列,配成 15 个通道按固定顺序扫描,转换结果由 DMA 循环搬运到内存数组,CPU 完全不参与搬运。这是标准做法,关键在于时序能不能算得过来。

ADC 的转换时间由采样时间和固定转换时间两部分组成,12 位精度下固定部分约 12.5 个 ADC 周期。采样时间可配,常见档位包括 13.5、28.5、41.5、55.5 个周期等。假设 ADC 时钟取 14MHz:

  • 采样时间 13.5 周期:单通道 (13.5 + 12.5) / 14MHz ≈ 1.86us,15 路约 27.9us
  • 采样时间 55.5 周期:单通道 (55.5 + 12.5) / 14MHz ≈ 4.86us,15 路约 72.9us

19.2kHz 的开关周期只有 52us 左右。用 55.5 周期采样,15 路扫完要 73us,比一个控制周期还长,第一路和最后一路的数据根本不在同一个时间切片里,控制环拿到的量测是"拼接"的,环路必然抖。这是这套设计里最容易被忽略、又最直接导致调试失败的坑。

工程上的处理办法有三条,通常组合使用。第一,把采样时间压到 13.5 周期,15 路控制在 28us 以内,留出 20 多微秒给算法。这对采样保持电路的驱动能力有要求,源阻抗要低,前级运放输出阻抗和 ADC 采样电容的建立时间要算过。第二,把慢变量从快速序列里摘出去——散热器温度、参数 1、参数 2 这三路的变化时间常数是秒级的,完全没必要每个开关周期采一次,可以用软件分频,比如 100 个周期采一次,或者干脆放到后台主循环里用单次转换做。第三,如果还是紧张,把部分通道分给 ADC1 并行采,代价是多一套 DMA 和同步逻辑。

/* 15 路规则组序列 + DMA 循环搬运 */ #define ADC_SEQ_LEN 15U uint16_t adc_raw[ADC_SEQ_LEN]; /* DMA 目标缓冲区,顺序与序列一致 */ static void adc_dma_init(void) { dma_parameter_struct dp; rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_GPIOB); rcu_periph_clock_enable(RCU_GPIOC); rcu_periph_clock_enable(RCU_ADC0); rcu_periph_clock_enable(RCU_DMA0); /* 全部模拟输入脚配成模拟模式 */ gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7); gpio_init(GPIOB, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0 | GPIO_PIN_1); gpio_init(GPIOC, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_4 | GPIO_PIN_5); adc_deinit(ADC0); /* ADCCLK = PCLK2 / 4,按实际总线频率算,不要超过手册上限 */ rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV4); adc_mode_config(ADC_MODE_FREE); /* 序列长度 15,逐个挂通道,采样时间统一 13.5 周期 */ adc_channel_length_config(ADC0, ADC_ROUTINE_CHANNEL, ADC_SEQ_LEN); adc_routine_channel_config(ADC0, 0, ADC_CHANNEL_0, ADC_SAMPLETIME_13POINT5); /* PA0 R 相电压 */ adc_routine_channel_config(ADC0, 1, ADC_CHANNEL_1, ADC_SAMPLETIME_13POINT5); /* PA1 S 相电压 */ adc_routine_channel_config(ADC0, 2, ADC_CHANNEL_2, ADC_SAMPLETIME_13POINT5); /* PA2 T 相电压 */ adc_routine_channel_config(ADC0, 3, ADC_CHANNEL_3, ADC_SAMPLETIME_13POINT5); /* PA3 R 相电流 */ adc_routine_channel_config(ADC0, 4, ADC_CHANNEL_4, ADC_SAMPLETIME_13POINT5); /* PA4 S 相电流 */ adc_routine_channel_config(ADC0, 5, ADC_CHANNEL_5, ADC_SAMPLETIME_13POINT5); /* PA5 T 相电流 */ adc_routine_channel_config(ADC0, 6, ADC_CHANNEL_6, ADC_SAMPLETIME_13POINT5); /* PA6 R 相市电 */ adc_routine_channel_config(ADC0, 7, ADC_CHANNEL_7, ADC_SAMPLETIME_13POINT5); /* PA7 S 相市电 */ adc_routine_channel_config(ADC0, 8, ADC_CHANNEL_8, ADC_SAMPLETIME_13POINT5); /* PB0 T 相市电 */ adc_routine_channel_config(ADC0, 9, ADC_CHANNEL_9, ADC_SAMPLETIME_13POINT5); /* PB1 母线电压 */ adc_routine_channel_config(ADC0, 10, ADC_CHANNEL_10, ADC_SAMPLETIME_13POINT5); /* PC0 电池电压 */ adc_routine_channel_config(ADC0, 11, ADC_CHANNEL_11, ADC_SAMPLETIME_13POINT5); /* PC1 充电电流 */ adc_routine_channel_config(ADC0, 12, ADC_CHANNEL_12, ADC_SAMPLETIME_13POINT5); /* PC2 散热器温度 */ adc_routine_channel_config(ADC0, 13, ADC_CHANNEL_14, ADC_SAMPLETIME_13POINT5); /* PC4 参数 1 */ adc_routine_channel_config(ADC0, 14, ADC_CHANNEL_15, ADC_SAMPLETIME_13POINT5); /* PC5 参数 2 */ dma_deinit(DMA0, DMA_CH0); dma_struct_para_init(&dp); dp.direction = DMA_PERIPHERAL_TO_MEMORY; dp.memory_addr = (uint32_t)adc_raw; dp.memory_inc = DMA_MEMORY_INCREASE_ENABLE; dp.periph_addr = (uint32_t)&ADC_RDATA(ADC0); dp.periph_inc = DMA_PERIPH_INCREASE_DISABLE; dp.memory_width = DMA_MEMORY_WIDTH_16BIT; dp.periph_width = DMA_PERIPH_WIDTH_16BIT; dp.number = ADC_SEQ_LEN; dp.priority = DMA_PRIORITY_HIGH; dma_init(DMA0, DMA_CH0, &dp); dma_circulation_enable(DMA0, DMA_CH0); dma_channel_enable(DMA0, DMA_CH0); adc_dma_mode_enable(ADC0); /* ADC 转换完成直接触发 DMA 请求 */ adc_external_trigger_config(ADC0, ADC_ROUTINE_CHANNEL, EXTERNAL_TRIGGER_ENABLE); adc_external_trigger_source_config(ADC0, ADC_ROUTINE_CHANNEL, ADC0_1_EXTTRIG_ROUTINE_T0_TRGO); /* 由 TIMER0 触发 */ adc_enable(ADC0); delay_1ms(1); /* 上电稳定,手册要求 */ adc_calibration_enable(ADC0); /* 校准必须做,不做零点漂移 */ adc_software_trigger_enable(ADC0, ADC_ROUTINE_CHANNEL); }

代码里有两处参数值得多说。RCU_CKADC_CKAPB2_DIV4是 ADC 时钟分频,必须保证分频后的 ADC 时钟在手册允许的上限之内,超了读数会整体偏移甚至乱跳。ADC0_1_EXTTRIG_ROUTINE_T0_TRGO表示用 TIMER0 的 TRGO 事件触发序列转换,这样采样时刻就由 PWM 定时器硬件决定,不依赖软件定时器,抖动小一个数量级。

缓冲区的顺序就是序列配置的顺序,下标 0 到 14 与表中的采样通道一一对应。控制环里按语义取用,别用裸下标满天飞,否则以后调整序列顺序会全线崩。

3.4 采样时刻与开关时刻对齐:把噪声窗口躲开

采样什么时候触发,比采样多快更影响精度。三相桥臂在开关瞬间会产生大幅度的 dv/dt 和 di/dt,如果采样落在开关动作之后的一两个微秒里,运放输出还没建立,采样值就是错的,反映到控制环里就是每周期一次的尖刺。

中心对齐模式下,计数器走到峰值那一刻,四组桥臂的开关状态刚好处于一个周期的中间位置,距离上下一次开关动作都最远,是最干净的采样窗口。把 TIMER0 的 TRGO 配成峰值触发,ADC 序列就在这一刻启动。15 路扫完约 28us,整个窗口都在开关动作的间隙里,不会撞上。

还要注意 ADC 序列的扫描顺序。把响应要求高的通道排前面——三相电压、三相电流放在最前面,母线和电池电压居中,温度和参数放最后。这样即使序列被拉长,关键量测的采样时刻也是最早的、最一致的。

4. 100kHz 单电压环与 19.2kHz 双环:控制环怎么落到中断里

4.1 DC/DC 单电压环:为什么一个环就够,以及中断该怎么分频

推挽升压这一级的目标非常单一:把电池电压升到母线电压并且稳住。它不负责输出波形质量,不负责三相平衡,只要母线电压稳、纹波小、动态跟得上,任务就完成了。所以单电压环足够了——采母线电压,和给定比较,PI 输出直接就是推挽的占空比。加电流环在这个场景里意义不大,除非你要做输入限流或者电池侧恒功率保护。

麻烦在中断预算上。开关频率 100kHz,周期只有 10us。如果每个开关周期都进一次中断跑 PI,在百兆级别的 MCU 上,一次中断进出加浮点运算很容易吃掉几百个周期,10us 里留给算法的时间所剩无几,跑满占空比之后 CPU 别的事情都干不了。

我的做法是让 PWM 继续跑 100kHz,但控制环降频执行。推挽的 LC 滤波器截止频率通常在几 kHz,控制环带宽只会更低,10kHz 的执行频率完全够用,动态几乎不受影响。实现上用 TIMER2 的更新中断,在中断里做一个计数器,每 10 次更新事件跑一次 PI,其余 9 次直接返回。这样 CPU 占用降一个数量级,母线纹波看不出区别。

/* 推挽升压:10kHz 执行的控制环,运行在 TIMER2 更新中断里 */ #define DCDC_DIV 10U /* 100kHz / 10 = 10kHz 控制频率 */ #define VBUS_TARGET 0.0f /* 母线电压给定,按实际标定量纲填 */ #define DUTY_MAX 0.470f /* 单管占空比上限,两管之和必须 < 1 */ static volatile uint8_t dcdc_cnt = 0; static pi_t vbus_pi; /* 电压环 PI 结构体,见 4.2 */ void TIMER2_IRQHandler(void) { if (timer_interrupt_flag_get(TIMER2, TIMER_INT_FLAG_UP) == RESET) { return; } timer_interrupt_flag_clear(TIMER2, TIMER_INT_FLAG_UP); if (++dcdc_cnt < DCDC_DIV) { return; /* 其余 9 次直接退出,省 CPU */ } dcdc_cnt = 0; float vbus = adc_to_volt(adc_raw[IDX_VBUS]); /* 母线电压,含滤波 */ /* 前馈项:给定越大,基础占空比越大,减轻 PI 负担 */ float ff = vbus_ff_table(vbus); float duty = pi_calc(&vbus_pi, VBUS_TARGET - vbus, ff); /* 占空比硬钳位,任何情况下不允许两管直通 */ if (duty > DUTY_MAX) { duty = DUTY_MAX; } if (duty < 0.0f) { duty = 0.0f; } /* 只写比较寄存器,影子寄存器保证在下一个更新事件生效 */ timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_0, (uint32_t)(duty * DCDC <p> <a href="https://download.csdn.net/download/weixin_43734531/84992398" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 13:12:01

2026数据分析Agent技术演进与金融领域实践

1. 2026数据分析Agent技术演进全景图 数据分析Agent正在经历从"工具链集成"向"认知智能体"的跃迁。2026年的技术栈将呈现三大特征&#xff1a;LLM&#xff08;大语言模型&#xff09;成为核心推理引擎、多模态数据处理能力标准化、自主决策闭环形成。我跟踪…

作者头像 李华
网站建设 2026/10/29 13:09:38

企业腐败审计中的数据治理与智能识别技术应用

1. 企业腐败审计与数据治理的现状与挑战腐败问题一直是企业治理中的顽疾。根据ACFE发布的《2022年全球舞弊调查报告》&#xff0c;企业因舞弊造成的损失中位数达到15万美元&#xff0c;而超过20%的案件损失超过100万美元。在数字化转型背景下&#xff0c;腐败行为也呈现出新的特…

作者头像 李华
网站建设 2026/10/29 13:09:38

营销技能怎么学?拆解洞察、表达、落地、量化四大能力块

做了一个多行业的营销岗位&#xff0c;我见过太多人问同一个问题&#xff1a;营销技能到底怎么学&#xff1f;有人列了一长串工具清单&#xff0c;有人背了一堆模型&#xff0c;还有人干脆把网上搜到的"marketing skills"关键词当成索引&#xff0c;越看越焦虑。其实…

作者头像 李华
网站建设 2026/10/29 13:09:11

蓝莓成熟度检测数据集构建与YOLO优化实践

1. 项目背景与数据集价值在农业智能化浪潮中&#xff0c;蓝莓成熟度检测正成为精准农业的关键技术痛点。传统人工分拣方式存在效率低&#xff08;每小时仅能处理约200-300颗&#xff09;、误判率高&#xff08;成熟度误判率达15%-20%&#xff09;等问题。我们团队开发的这个包含…

作者头像 李华
网站建设 2026/10/29 13:08:44

大学物理实验报告全集:模板、数据处理与批量排版标准化

改实验报告这件事&#xff0c;我干了好几年。每到学期中段&#xff0c;桌上就会堆起一摞学生交上来的“大学物理实验报告全集”&#xff0c;有的装订得整整齐齐&#xff0c;有的十几页订书钉一钉就交上来了&#xff0c;翻开一看&#xff0c;数据表格空着两格&#xff0c;有效数…

作者头像 李华
网站建设 2026/10/29 13:08:23

SCARA机器人运动学与轨迹规划:MATLAB仿真从建模到可视化

简介&#xff1a;这份资源是JUB机器人与智能系统导论课程的MATLAB作业项目代码包&#xff0c;面向学习机器人建模与运动仿真的初学者及课程学生&#xff0c;用于解决SCARA机械臂与飞船对象建模、关节动画演示等实践问题。压缩包共19个文件&#xff0c;约129KB&#xff0c;以15个…

作者头像 李华