news 2026/10/29 13:11:41

48脚MCU的三相四桥臂逆变电源:十路PWM与十五路AD设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
48脚MCU的三相四桥臂逆变电源:十路PWM与十五路AD设计

简介:围绕GD32F330RBT6展开的三相四桥臂逆变电源设计文档,面向从事数字电源、逆变器开发及国产MCU选型的工程师与高年级学生,重点解决三相四线供电、掉电切换与数字化控制的一体化实现问题。内容从系统结构入手,梳理市电充电、电池、推挽升压DC/DC与三相四桥臂逆变器的协作关系,并给出10路PWM和15路AD采样的引脚分配,便于直接对照原理图与程序。控制部分说明DC/DC单电压环(100kHz)及R、S、T三相电压外环加电流内环、N相开环(19.2kHz)的策略,还涉及软件锁相、串口通讯、Flash模拟EEPROM、电池管理、过欠压/过流/短路/过温保护与串口在线升级等附加功能。包内为1个PDF文件,约185KB,篇幅精炼、结构清晰,适合作为方案论证与调试排错的参考。已有533人学习。

1. 十路 PWM 与十五路 AD 挤进一颗 48 脚芯片:这套三相四桥臂逆变电源的选型起点

三相四桥臂逆变电源的设计会卡在一个很实际的地方:拓扑画出来谁都看得懂,真正难的是控制资源怎么分。十路 PWM 要带死区和互锁,十五路模拟量要在一个 19.2kHz 的开关周期里采完并算完,还要跑市电锁相、充电管理、串口升级,粗算一遍就会发现引脚和算力都不富裕,不少方案最后被迫拆成 DSP 加一颗辅助 MCU。这套基于 GD32F330RBT6 的设计把这些全部收进一颗国产芯片:推挽 DC/DC 跑 100kHz 单电压环,四桥臂 R/S/T 三相各配一组电压外环加电流内环、N 相开环,控制频率与开关频率同为 19.2kHz,市电锁相用软件正余弦硬算。正在做电源方向毕设或课程设计、需要一套能上电也能答辩的三相逆变方案,这套结构可以整段拿来对照。

2. 功率链路分层:市电、电池、推挽升压与四桥臂的职责边界

2.1 四段链路各自管什么,顺序错一步就炸机

整套系统的功率流很清晰:三相四线市电进来,一路经充电电路给电池补能;市电掉的那一刻,电池接管,先由推挽 DC/DC 把电池电压升到高压母线,再由三相四桥臂逆变器把母线切成三相交流。四段链路串起来,任何一段的时序错位都会让后级直接炸机,所以控制板的第一职责其实不是算控制律,而是把四段的先后顺序管死。

市电段要同时干两件事:充电和检测。三相市电电压分别落在 PA6、PA7、PB0 三个 AD 口上,软件锁相在这里取信号,同时用幅值和相位突变判断掉电。充电那一侧要盯电池电压 PC0 和充电电流 PC1,做未接电池检测、容量估算、低压保护和过充保护,这几项功能看着零碎,实际是整套电源能不能长期挂在现场的关键。

电池段是整个系统的能量池。电池电压范围宽,直接逆变的母线电压不够,所以中间必须插一级升压。这一级的输入是低压大电流,输出是高压小电流,变比大、功率密度要求高,推挽拓扑在这个场景里是最省事的选择:原边两只管子交替导通,变压器双向磁化,磁芯利用率比单端高,也不需要额外的复位绕组。

推挽升压段把电池电压抬到母线,母线电压由 PB1 采样回来做闭环。这一级用 100kHz 开关频率,只做单电压环,目标是母线稳、响应快,不去管后级的瞬时功率波动,那部分由母线电容和后级双环吸收。升压级和逆变级都做数字控制,好处是参数在固件里改,坏处是所有保护逻辑、死区、时序都得自己写,没有模拟方案那种「搭好就差不多」的容错。

四桥臂逆变段是输出的最后一站。R、S、T 三相各自一组电压外环加电感电流内环,N 相开环,控制频率和开关频率都是 19.2kHz。第四桥臂不是冗余,它给中线电流提供了一条可控通路,带单相负载、带非线性负载时中点电位靠它拉住,这一点在后面 2.3 里展开。

2.2 推挽级的数字实现:两路独立 PWM 与软件互锁

推挽级最大的坑是两只管子同时导通,那相当于把电池直接短路,一颗管子都保不住。翻 GD32F330 的定时器手册会发现,PC6、PC7 挂的是 TIMER2_CH0 和 TIMER2_CH1,TIMER2 是通用定时器,没有互补输出通道,也就没有硬件死区。这意味着互锁只能在软件层做,占空比上限必须手动扣掉死区对应的计数值。

/* 推挽 DC/DC:TIMER2_CH0(PC6)/CH1(PC7) 两路独立 PWM,100kHz * TIMER2 为通用定时器,无互补通道,两管互锁完全靠软件保证 * 120MHz / (0+1) / (1199+1) = 100kHz */ #define DCDC_PERIOD 1199 #define DCDC_DEAD_CNT 36 /* 36 个计数 @120MHz ≈ 300ns 死区 */ void dcdc_pwm_init(void) { timer_parameter_struct tp; timer_oc_parameter_struct oc; rcu_periph_clock_enable(RCU_GPIOB); rcu_periph_clock_enable(RCU_TIMER2); gpio_af_set(GPIOB, GPIO_AF_1, GPIO_PIN_6 | GPIO_PIN_7); gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_6 | GPIO_PIN_7); gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_6 | GPIO_PIN_7); timer_deinit(TIMER2); timer_struct_para_init(&tp); tp.prescaler = 0; tp.alignedmode = TIMER_COUNTER_EDGE; tp.counterdirection = TIMER_COUNTER_UP; tp.period = DCDC_PERIOD; tp.clockdivision = TIMER_CKDIV_DIV1; tp.repetitioncounter = 0; timer_init(TIMER2, &tp); timer_channel_output_struct_para_init(&oc); oc.outputstate = TIMER_CCX_ENABLE; oc.ocpolarity = TIMER_OC_POLARITY_HIGH; oc.ocidlestate = TIMER_OC_IDLE_STATE_LOW; /* 停机态强制低,上电不直通 */ timer_channel_output_config(TIMER2, TIMER_CH_0, &oc); timer_channel_output_config(TIMER2, TIMER_CH_1, &oc); timer_channel_output_mode_config(TIMER2, TIMER_CH_0, TIMER_OC_MODE_PWM0); timer_channel_output_mode_config(TIMER2, TIMER_CH_1, TIMER_OC_MODE_PWM0); timer_channel_output_shadow_config(TIMER2, TIMER_CH_0, TIMER_OC_SHADOW_DISABLE); timer_channel_output_shadow_config(TIMER2, TIMER_CH_1, TIMER_OC_SHADOW_DISABLE); timer_auto_reload_shadow_enable(TIMER2); timer_counter_value_config(TIMER2, 0); timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_0, 0); timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_1, 0); timer_enable(TIMER2); }

这段代码里真正值得看的是ocidlestate和两个通道初始占空比都写 0。前者保证定时器被刹车或还没初始化完时,两个引脚都是低电平;后者保证上电第一拍不会因为寄存器默认值把两只管子同时打开。很多板子第一次上电就炸,追到原理图也看不出问题,最后发现就是这两行漏了。

推挽的驱动时序要求两个通道的占空比必须「成对」更新,中间不能出现只有一个通道变、另一个还停在上一个值的状态,否则某个周期里两管的导通时间会重叠。通常的做法是在定时器更新中断里统一写入:

/* 占空比上限扣掉死区余量,两通道在同一更新事件内成对写入 * 避免出现"A 已加宽、B 还没收窄"的中间态造成共导 */ #define DCDC_DUTY_MAX (DCDC_PERIOD - DCDC_DEAD_CNT) static uint16_t dcdc_duty = 0; void dcdc_set_duty(uint16_t duty) { if (duty > DCDC_DUTY_MAX) { duty = DCDC_DUTY_MAX; } dcdc_duty = duty; timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_0, dcdc_duty); /* 推挽两管交替,第二路相位上错开半个周期由软件在更新中断里处理 */ timer_channel_output_pulse_value_config(TIMER2, TIMER_CH_1, dcdc_duty); }

参数上还有两个细节值得提前定死:一是DCDC_DUTY_MAX不要贴着 100% 用,留 3% 左右给死区和变压器复位,实际取到 97% 就已经很激进了;二是软启动斜率,母线从 0 爬到额定值的过程通常给 200~500ms,太快会把电池侧的浪涌电流拉爆,太慢则市电掉电到逆变建立的那段时间撑不住后级负载。

2.3 第四桥臂:三次谐波注入与 N 相开环的取舍

三桥臂逆变器带不平衡负载时,中点靠两颗电容分压撑住,负载一偏,中点就跟着漂,三相电压立刻不对称。四桥臂多出来的那一相直接接到负载中线上,用一只桥臂主动把中点电压钳在母线中点,不平衡电流从中性桥臂走掉,三相输出电压基本不受影响。这是四桥臂相对三桥臂最核心的价值,也是这套方案在带单相负载、带整流性负载时还能稳住的原因。

控制策略上,R、S、T 三相各做独立的电压外环加电感电流内环,N 相开环。N 相开环并不代表不管它,它的占空比由三相调制波的零序分量直接合成:三相正弦先算出瞬时最大值和最小值,取两者中点作为注入量,再把这个零序量取反送给 N 桥臂。三相各自叠上同一个零序分量后,线电压不变,但相电压的峰值被压到母线电压以内,母线利用率从 0.866 提到接近 1.0,这就是三次谐波注入的实际收益。

这里有个容易混淆的点:三次谐波注入和第四桥臂是两件事。前者解决的是「母线电压不够高、正弦调制波削顶」的问题,后者解决的是「中点电位没人管」的问题。两个手段叠在一起用,才能既把母线用满,又在中点被负载拽偏时拉得住。如果只做注入不做第四桥臂,带不平衡负载时相电压仍然会一高一低;如果只做第四桥臂不做注入,母线电压得往上抬 15% 才够用,器件耐压和成本都得跟着涨。

3. 引脚与采样资源分配:十路 PWM 和十五路 AD 怎么排才不打结

3.1 十路 PWM 的定时器归属与死区预算

十路 PWM 的分组逻辑是「一组 DC/DC 加四组逆变」,DC/DC 那两路前面已经说过,剩下八路分给四个桥臂。R、S、T 三相各自上下管成对,挂在高阶定时器 TIMER0 的三个通道上,靠互补输出加硬件死区;N 相单独挂在 TIMER15 上。完整的引脚定义如下表,其中 N 相下管那一项在原始记录里写的是 PN8,这个口在 48 脚封装上并不存在,按 TIMER15_CH0_ON 的功能反查,实际应落在 PB 口相邻引脚上,画板前必须对着数据手册的复用功能表逐个核一遍,不能照抄。

引脚功能定时器通道说明
PC6DCDCPWM1TIMER2_CH0推挽原边 A 管,无互补
PC7DCDCPWM2TIMER2_CH1推挽原边 B 管,软件互锁
PA8R_PWM_HTIMER0_CH0R 相上管,互补主通道
PB13R_PWM_LTIMER0_CH0_ONR 相下管,互补通道
PA9S_PWM_HTIMER0_CH1S 相上管
PB14S_PWM_LTIMER0_CH1_ONS 相下管
PA10T_PWM_HTIMER0_CH2T 相上管
PB15T_PWM_LTIMER0_CH2_ONT 相下管
PB8N_PWM_HTIMER15_CH0N 相上管,独立定时器
待核N_PWM_LTIMER15_CH0_ONN 相下管,按 AF 表确认

死区时间不是拍脑袋定的,它由三样东西决定:功率管的关断拖尾时间、驱动光耦或隔离器的传输延迟差、以及母线电压下的 dv/dt 引起的米勒误导通。600ns 在 20kHz 以内、母线几百伏的场合是比较稳妥的取值,折算到 120MHz 主频就是 72 个计数。TIMER0 的死区寄存器直接吃这个值,不需要软件补。

/* TIMER0 高级定时器:CH0/1/2 加三个互补通道,共六路驱动三相上下管 * 中心对齐 19.2kHz:120MHz / (2 * (3124 + 1)) = 19200Hz * 死区 72 个计数 @120MHz ≈ 600ns */ #define INV_PERIOD 3124 #define INV_DEAD 72 void inv_pwm_init(void) { timer_parameter_struct tp; timer_oc_parameter_struct oc; timer_break_parameter_struct brk; rcu_periph_clock_enable(RCU_GPIOA); rcu_periph_clock_enable(RCU_GPIOB); rcu_periph_clock_enable(RCU_TIMER0); gpio_af_set(GPIOA, GPIO_AF_2, GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10); gpio_mode_set(GPIOA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10); gpio_output_options_set(GPIOA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10); gpio_af_set(GPIOB, GPIO_AF_2, GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15); gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15); gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15); timer_deinit(TIMER0); timer_struct_para_init(&tp); tp.prescaler = 0; tp.alignedmode = TIMER_COUNTER_CENTER_UP; /* 中心对齐,谐波分布更干净 */ tp.counterdirection = TIMER_COUNTER_UP; tp.period = INV_PERIOD; tp.clockdivision = TIMER_CKDIV_DIV1; tp.repetitioncounter = 0; timer_init(TIMER0, &tp); timer_channel_output_struct_para_init(&oc); oc.outputstate = TIMER_CCX_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); 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_break_struct_para_init(&brk); brk.runoffstate = TIMER_ROS_STATE_DISABLE; brk.ideloffstate = TIMER_IOS_STATE_ENABLE; /* 空闲态六路全低 */ brk.deadtime = INV_DEAD; brk.breakpolarity = TIMER_BREAK_POLARITY_LOW; brk.outputautostate = TIMER_OUTAUTO_ENABLE; brk.protectmode = TIMER_CCHP_PROT_OFF; timer_break_config(TIMER0, &brk); timer_break_enable(TIMER0); timer_primary_output_config(TIMER0, ENABLE); timer_auto_reload_shadow_enable(TIMER0); timer_enable(TIMER0); }

N 相桥臂挂 TIMER15,它的占空比由零序分量算出后单独写入,这里不再重复贴代码,逻辑和推挽类似,只是频率必须和 TIMER0 严格一致,否则四个桥臂的开关时刻对不齐,中点会周期性抖动。实际做法是把 TIMER15 的周期值和 TIMER0 设为同一组参数,并在 TIMER0 的更新中断里同步触发 N 相占空比更新。

3.2 十五路 AD 的序列索引与采样时间分配

十五路模拟量在一颗只有 16KB SRAM 的芯片里必须一次性搬完,靠的是 ADC 规则组序列加 DMA 循环搬运,中途不能让 CPU 介入,否则采样间隔一抖,内环里的电流采样就不准了。序列顺序就是 DMA 缓冲里的数组下标,改顺序等于改代码,所以顺序一旦定下就别乱动。

序列引脚物理量采样时间建议
0PA0R 相输出电压55.5 周期
1PA1S 相输出电压55.5 周期
2PA2T 相输出电压55.5 周期
3PA3R 相电感电流13.5 周期
4PA4S 相电感电流13.5 周期
5PA5T 相电感电流13.5 周期
6PA6R 相市电电压55.5 周期
7PA7S 相市电电压55.5 周期
8PB0T 相市电电压55.5 周期
9PB1母线电压55.5 周期
10PC0电池电压55.5 周期
11PC1充电电流13.5 周期
12PC2散热器温度239.5 周期
13PC4参数 1(预留标定)239.5 周期
14PC5参数 2(预留标定)239.5 周期

采样时间的分配逻辑是:分压后的电压信号源阻抗高,用长采样;电流信号经过运放缓冲,源阻抗低,用短采样抢时间;温度是热敏电阻分压,变化缓慢,给最长的采样时间,误差大一点无所谓。整套序列在 19.2kHz 的一个周期(约 52us)内跑完,ADC 时钟按 60MHz 算,总转换时间要留出至少 20% 的余量,不能排满。

/* ADC0 规则组 15 通道 + DMA0 循环搬运 * 数组下标即序列号,顺序与上表严格对应,改顺序必须同步改结构体映射 */ #define ADC_CH_NUM 15 uint16_t adc_raw[ADC_CH_NUM]; /* DMA 目标缓冲,每个周期自动刷新 */ static const uint8_t seq_ch[ADC_CH_NUM] = { ADC_CHANNEL_0, ADC_CHANNEL_1, ADC_CHANNEL_2, /* 三相输出电压 */ ADC_CHANNEL_3, ADC_CHANNEL_4, ADC_CHANNEL_5, /* 三相电感电流 */ ADC_CHANNEL_6, ADC_CHANNEL_7, ADC_CHANNEL_8, /* 三相市电电压 */ ADC_CHANNEL_9, /* 母线电压 */ ADC_CHANNEL_10, ADC_CHANNEL_11, /* 电池电压、充电电流 */ ADC_CHANNEL_12, ADC_CHANNEL_14, ADC_CHANNEL_15 /* 温度、两路预留参数 */ }; void adc_dma_init(void) { dma_parameter_struct dp; uint8_t i; 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_mode_set(GPIOA, GPIO_MODE_ANALOG, GPIO_PUPD_NONE, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7); adc_deinit(); adc_mode_config(ADC_MODE_FREE); adc_special_function_config(ADC0, ADC_SCAN_MODE, ENABLE); adc_special_function_config(ADC0, ADC_CONTINUOUS_MODE, DISABLE); adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT); adc_channel_length_config(ADC0, ADC_REGULAR_CHANNEL, ADC_CH_NUM); for (i = 0; i < ADC_CH_NUM; i++) { /* 电压类给长采样,电流类给短采样,温度类给最长采样 */ uint8_t st = (i >= 3 && i <= 5) ? ADC_SAMPLETIME_13POINT5 : (i >= 12) ? ADC_SAMPLETIME_239POINT5 : ADC_SAMPLETIME_55POINT5; adc_regular_channel_config(ADC0, i, seq_ch[i], st); } adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE); adc_dma_mode_enable(ADC0); 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_CH_NUM; dp.priority = DMA_PRIORITY_HIGH; dma_init(DMA0, DMA_CH0, &dp); dma_circulation_enable(DMA0, DMA_CH0); dma_channel_enable(DMA0, DMA_CH0); adc_enable(ADC0); delay_1ms(1); adc_calibration_enable(ADC0); }

这段里最容易被忽略的是adc_calibration_enable。GD32 的 ADC 上电后必须做一次校准,不校准的话零点偏移能到十几个 LSB,三相电压的一致性直接崩掉。校准要等 ADC 稳定,手册里给的等待时间是 1ms 量级,少等这一下,后面所有标定都是白做。

3.3 采样时刻对齐:为什么把触发点钉在计数器中点

采样时刻选错,再好的控制律也救不回来。19.2kHz 中心对齐模式下,计数器从 0 数到 period 再数回来,三角载波的顶点和底点对应开关管动作时刻,那里电流正在换向,di/dt 最大,采到的值全是开关噪声。真正干净的时刻是计数器等于 period 的那一瞬间,也就是三角波的中点,此时三相下管全开,电感电流纹波正好围绕平均值对称分布,采到的就是「真实值」。

标准做法是找一个不驱动引脚的定时器通道专门产生 ADC 触发信号,把它配置成只在计数到中点时翻转一次,输出不引到外部,纯做内部事件源。

/* 用 TIMER0_CH3 做 ADC 触发源,不占用驱动引脚,也不改动三相占空比 * 中心对齐模式下计数值到 period 即为三角波中点,此刻触发采样 */ void adc_trigger_from_timer0(void) { timer_oc_parameter_struct oc; timer_channel_output_struct_para_init(&oc); oc.outputstate = TIMER_CCX_DISABLE; /* 只产生内部事件,不驱动引脚 */ oc.ocpolarity = TIMER_OC_POLARITY_HIGH; timer_channel_output_config(TIMER0, TIMER_CH_3, &oc); timer_channel_output_pulse_value_config(TIMER0, TIMER_CH_3, INV_PERIOD / 2); timer_channel_output_mode_config(TIMER0, TIMER_CH_3, TIMER_OC_MODE_PWM0); adc_external_trigger_source_config(ADC0, ADC_REGULAR_CHANNEL, ADC0_1_2_EXTTRIG_REGULAR_T0_CH3); adc_external_trigger_config(ADC0, ADC_REGULAR_CHANNEL, ENABLE); }

触发点钉死之后,还要在软件里留一次「采样完成」的确认。通常做法是 DMA 传输完成中断里置一个标志位,控制环只在标志位置位后才读adc_raw[],读之前先把标志清掉。如果控制环执行时间超过一个开关周期,标志位会被覆盖,表现出来就是控制环偶尔用了两周前的数据,波形上看起来像是随机抖动,实际是时序没对齐。

4. 控制环设计:100kHz 单电压环与 19.2kHz 双环的配合

4.1 推挽单电压环:PI 离散化与占空比上限

推挽这一级的控制目标很单一:母线电压稳住。反馈量是 PB1 采回来的母线电压,指令是固件里设定的母线目标值,误差送进 PI,输出直接就是占空比。100kHz 意味着控制周期只有 10us,PI 必须在中断里跑完,浮点运算在这个时间尺度上是够的,但不能在里面做除法或者查表,那些操作放到初始化阶段算好。

typedef struct { float kp; float ki; float integral; float out_max; float out_min; } pi_ctrl_t; /* 位置式 PI,带积分限幅和饱和反算 * 输出打饱和时把这一拍的积分量退回去,避免退饱和过程拖出大延迟 */ float pi_calc(pi_ctrl_t *p, float ref, float fb, float dt) { float err = ref - fb; float out; float delta = p->ki * err * dt; p->integral += delta; if (p->integral > p->out_max) p->integral = p->out_max; if (p->integral < p->out_min) p->integral = p->out_min; out = p->kp * err + p->integral; if (out > p->out_max) { out = p->out_max; p->integral -= delta; } if (out < p->out_min) { out = p->out_min; p->integral -= delta; } return out; }

参数上,推挽级的 PI 带宽一般压在开关频率的 1/10 以内,也就是 10kHz 以下,再高就会和输出滤波器的谐振点打架。比例项决定母线跌落的恢复速度,积分项决定稳态误差,但积分不能给太大,否则市电掉电切换的那一瞬间母线会先冲高再回落,后级过压保护容易误动作。实际调的时候先把 ki 置零,把 kp 加到阶跃响应刚好不振荡,再把 ki 慢慢加回去,这是最省时间的顺序。

还有一层保护要做在 PI 外面:占空比输出必须再过一道上限,DCDC_DUTY_MAX就是前面算的 97%。PI 算出来的值超了就直接钳住,同时把积分退回去,这样即使母线采样断线、反馈变成 0,占空比也不会一路顶到 100% 把变压器打饱和。

4.2 四桥臂双环:电压外环给指令、电感电流内环给响应

三相四桥臂的控制结构是三个独立的双环,每一相都是「电压外环算电流指令,电流内环算占空比」。外环的反馈是 PA0~PA2 采到的相电压,内环的反馈是 PA3~PA5 采到的电感电流。外环带宽低,负责稳态精度;内环带宽高,负责动态响应和限流。

内环之所以必要,是因为电感电流是唯一能快速反映负载突变的量。纯电压环要等输出电压掉下来才动作,中间隔着 LC 滤波器的延迟,带整流性负载时电压波形会塌出一块。加上电流内环后,负载一跳,电感电流立刻变,内环在同一个开关周期内就能把占空比调过来,输出电压的跌落被压得很小。

内环的输出还要过一道限幅,这道限幅就是过流保护的软阈值。电流指令被钳在最大允许值上,即使外环因为某种原因算出一个荒唐的指令,实际电流也不会超过硬件能承受的范围。硬件比较器做的那道过流保护是最后一道闸,软限幅是常规手段,两道都要有。

N 相的处理和三相不一样。它不参与双环,占空比直接由三相调制波的零序分量合成:

/* 零序注入:取三相瞬时值的最大最小中点取反,送给 N 桥臂 * 三相各自叠加同一个零序量后线电压不变,相电压峰值被压到母线以内 * 同时这个量也决定了 N 桥臂的输出,用来把中点电位钳住 */ static inline float zero_seq_calc(float ur, float us, float ut) { float umax = (ur > us) ? ((ur > ut) ? ur : ut) : ((us > ut) ? us : ut); float umin = (ur < us) ? ((ur < ut) ? ur : ut) : ((us < ut) ? us : ut); return -(umax + umin) * 0.5f; } void inv_modulate(float ur, float us, float ut, float udc) { float uz = zero_seq_calc(ur, us, ut); float half = udc <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:10:09

MLN跨平台开发入门:5分钟用JavaScript构建原生UI应用

1. 项目概述&#xff1a;这不是又一个“Hello World”&#xff0c;而是跨平台开发的轻量级破局点 “5分钟上手MLN&#xff1a;从安装到运行第一个跨平台应用的完整教程”——这个标题里藏着三个关键信号&#xff1a; 时间承诺&#xff08;5分钟&#xff09; 、 技术对象&am…

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

YOLO26算法在草莓智能采摘系统中的应用与优化

1. 项目背景与核心价值 在现代化农业种植领域&#xff0c;草莓作为高经济价值作物&#xff0c;其生长状态监测和采摘环节长期面临人工成本高、效率低的痛点。传统人工巡检方式存在三个显著缺陷&#xff1a;一是无法实现24小时不间断监测&#xff0c;二是采摘时机判断依赖经验导…

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

大学计算机基础知识点整理PDF:从备考到重建的系统复习法

简介&#xff1a;《大学计算机基础》知识点整理PDF&#xff0c;是一份面向高校学生与计算机初学者的复习资料。内容围绕课程常见考点与易混淆概念展开&#xff0c;覆盖计算机硬件组成、存储器分类、进制转换、软件分类、网络通信与OSI模型、IP地址及域名等核心模块&#xff0c;…

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

嵌入式Mplay播放器移植:交叉编译、音视频输出与性能调优

嵌入式系统播放器 Mplay 移植这件事&#xff0c;说难不难&#xff0c;说简单也真不简单。我第一次把 Mplay 搬到一块 Cortex-A7 级别的板子上&#xff0c;前后折腾了差不多两周&#xff0c;其中真正写代码的时间不到两天&#xff0c;剩下的全在跟工具链、解码库和音频接口较劲。…

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

KindEditor优化:解决Word粘贴图片自动上传问题

1. 项目背景与需求分析KindEditor作为一款经典的富文本编辑器&#xff0c;在企业内容管理系统(CMS)中应用广泛。但在实际使用中&#xff0c;用户从WORD文档复制内容到编辑器时&#xff0c;经常会遇到图片无法自动上传的问题——图片仅以base64编码形式嵌入HTML&#xff0c;导致…

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

软著申请材料一致性、源程序排版与补正排查

很多人对软件著作权申请的第一印象是"填个表、传个代码就完事了"&#xff0c;真正动手才发现&#xff0c;卡人的从来不是技术含量&#xff0c;而是材料的机械性规范&#xff1a;页眉少个版本号、源程序页数凑不够、说明书的截图里露出别的产品的标识、申请表里的开发…

作者头像 李华