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 四桥臂而不是三桥臂加分裂电容中点可控性换来的代价三桥臂逆变器带三相三线负载没问题一旦要出中线、要带单相负载就必须处理中点。传统做法是母线用两个电容串联造一个虚拟中点但电容的均压能力有限负载不平衡时中点电位会跟着负载跑输出三相电压立刻不对称。要压住这个偏移电容得做得很大体积和成本都上去了。四桥臂的思路是再加一个半桥专门驱动中线。这个桥臂可以主动往中点注入或抽取电流把中点电位摁在想要的位置。带来的好处很直接——不平衡负载下三相电压仍然对称直流电压利用率也比分裂电容方案高因为不用给电容均压留裕量。对带单相负载、带非线性负载的场景这个优势是实打实的。代价同样要说清楚。多一个桥臂就是多两个功率管、多一路驱动、多一路 PWMN 相电感要单独设计电感量、饱和电流都得算控制上多一个自由度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 个脚再加上继电器控制、风扇、指示灯、电池检测使能这些零碎 GPIO48 脚根本排不开。RB 后缀的存储容量也留了余量控制环、保护阈值、串口升级的 bootloader 都要占空间。外设分配上三相互补 PWM 交给 TIMER0——这是片上的高级定时器CH0/CH1/CH2 三路都带互补输出和硬件死区正好对应 R、S、T 三相的上下管。N 相单独用 TIMER15 的 CH0 加互补通道它同样带死区和互补能力。DC/DC 推挽用 TIMER2 的 CH0/CH1通用定时器足够推挽不需要互补死区之外的复杂功能。模拟侧是这套设计里比较紧的地方。15 路采样全部落在 ADC0 的通道上PA0 到 PA7 对应 IN0 到 IN7PB0、PB1 对应 IN8、IN9PC0、PC1、PC2、PC4、PC5 对应 IN10 到 IN15中间 PC3 空着。这意味着 15 路必须靠规则组序列扫描加 DMA 搬运来完成没有第二组 ADC 帮你并行分担。后面第 3 章会详细算这笔时序账先记住这个结论采样时间是这套设计里第一个要抠的参数。3. 10 路 PWM 与 15 路采样引脚表怎么落成能跑的初始化代码3.1 PWM 引脚分配表的读法三组定时器各管一段先把表里的映射关系理清楚。DC/DC 推挽占两路PC6 走 TIMER2_CH0PC7 走 TIMER2_CH1这两路是推挽的两个主管互补导通各占略小于 50% 的占空比。表里两路都标成 DCDCPWM1按通道号理解就是推挽的两只管子画原理图时别被这处笔误带偏。逆变侧占八路分四组桥臂上管引脚复用功能下管引脚复用功能R 相PA8TIMER0_CH0PB13TIMER0_CH0_ONS 相PA9TIMER0_CH1PB14TIMER0_CH1_ONT 相PA10TIMER0_CH2PB15TIMER0_CH2_ONN 相PB8TIMER15_CH0PB8 对应互补TIMER15_CH0_ONR、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 时步进等于一个 tDTS128 到 191 之间步进是两个 tDTS192 到 223 之间步进八个 tDTS224 到 255 之间步进十六个 tDTS。tDTS 就是定时器的计数时钟周期。假设定时器时钟 120MHztDTS 约 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.86us15 路约 27.9us采样时间 55.5 周期单通道 (55.5 12.5) / 14MHz ≈ 4.86us15 路约 72.9us19.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 hrefhttps://download.csdn.net/download/weixin_43734531/84992398 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p