简介:围绕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 口相邻引脚上,画板前必须对着数据手册的复用功能表逐个核一遍,不能照抄。
| 引脚 | 功能 | 定时器通道 | 说明 |
|---|---|---|---|
| PC6 | DCDCPWM1 | TIMER2_CH0 | 推挽原边 A 管,无互补 |
| PC7 | DCDCPWM2 | TIMER2_CH1 | 推挽原边 B 管,软件互锁 |
| PA8 | R_PWM_H | TIMER0_CH0 | R 相上管,互补主通道 |
| PB13 | R_PWM_L | TIMER0_CH0_ON | R 相下管,互补通道 |
| PA9 | S_PWM_H | TIMER0_CH1 | S 相上管 |
| PB14 | S_PWM_L | TIMER0_CH1_ON | S 相下管 |
| PA10 | T_PWM_H | TIMER0_CH2 | T 相上管 |
| PB15 | T_PWM_L | TIMER0_CH2_ON | T 相下管 |
| PB8 | N_PWM_H | TIMER15_CH0 | N 相上管,独立定时器 |
| 待核 | N_PWM_L | TIMER15_CH0_ON | N 相下管,按 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 缓冲里的数组下标,改顺序等于改代码,所以顺序一旦定下就别乱动。
| 序列 | 引脚 | 物理量 | 采样时间建议 |
|---|---|---|---|
| 0 | PA0 | R 相输出电压 | 55.5 周期 |
| 1 | PA1 | S 相输出电压 | 55.5 周期 |
| 2 | PA2 | T 相输出电压 | 55.5 周期 |
| 3 | PA3 | R 相电感电流 | 13.5 周期 |
| 4 | PA4 | S 相电感电流 | 13.5 周期 |
| 5 | PA5 | T 相电感电流 | 13.5 周期 |
| 6 | PA6 | R 相市电电压 | 55.5 周期 |
| 7 | PA7 | S 相市电电压 | 55.5 周期 |
| 8 | PB0 | T 相市电电压 | 55.5 周期 |
| 9 | PB1 | 母线电压 | 55.5 周期 |
| 10 | PC0 | 电池电压 | 55.5 周期 |
| 11 | PC1 | 充电电流 | 13.5 周期 |
| 12 | PC2 | 散热器温度 | 239.5 周期 |
| 13 | PC4 | 参数 1(预留标定) | 239.5 周期 |
| 14 | PC5 | 参数 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>