【电力电子技术】正弦波逆变器无极调频调压原理、相位累加法\直接频率合成
【电力电子技术】正弦波逆变器无极调频调压原理、相位累加法\直接频率合成
1. 引言
在正弦波逆变器的调制方式中,最广为熟知的方法便是载波调制:用正弦波与三角波比较。
体现在代码上,便是使用正弦表数组,例如每周期查表400次,频率50HZ,每次PWM周期都更新占空比,对应开关频率20KHZ。
如果想在上述基础上修改频率,那么自然想到的就是修改PWM频率,而每周期点数不变,进而改变每周期输出的正弦频率。但有个问题,单片机修改频率时,Period值也会变,占空比的CCR值不变时,占空比便会发生变化,输出波形就会异常、失真。
解决措施就是PWM频率不变,每周期查表次数也不变,在此基础上,使用专门的另一个定时器来查表,改变定时器中断速度,进而改变查表速度,来控制频率:
// ----------- 用于改变输出频率的函数 -----------
void SPWM_SetOutputFrequency(uint32_t spwm_freq_hz)
{
// 1. 停止节拍定时器
HAL_TIM_Base_Stop_IT(&htim2);
if (spwm_freq_hz == 0) return;
// 2. 计算新的中断频率 (查表频率)
uint32_t update_freq = spwm_freq_hz * 100; // 100是每周期点数
// 3. 根据中断频率,计算TIM2的ARR和PSC
// 这里需要一个公式来动态计算,例如:
//htim2.Instance->PSC = ...
htim2.Instance->ARR = (HAL_RCC_GetPCLK1Freq() / htim2.Instance->PSC) / update_freq;
// 4. 启动节拍定时器
HAL_TIM_Base_Start_IT(&htim2);
}
// ----------- 通用定时器TIM2的中断服务函数 -----------
void TIM2_IRQHandler(void)
{
if(__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET)
{
__HAL_TIM_CLEAR_IT(&htim2, TIM_IT_UPDATE);
SPWM_SET(); // 在这里调用查表更新函数
}
}
// ----------- SPWM更新函数 (但注意幅值缩放) -----------
uint16_t spwm_num1 = 0;
uint16_t spwm_num2 = 50; //相位相差180度(单极性倍频调制)
void SPWM_SET()
{
spwm_num1++;
if(spwm_num1 >= 100){spwm_num1 = 0;} // 使用 >= 更安全
spwm_num2++;
if(spwm_num2 >= 100){spwm_num2 = 0;}
uint32_t hrtim_period = 3600;
// 进行幅值缩放,确保比较值 < 周期值
uint32_t cmp_val1 = (uint32_t)(spwm_Gain * (float)spwm_code[spwm_num1] / 3550.0f * hrtim_period);
uint32_t cmp_val2 = (uint32_t)(spwm_Gain * (float)spwm_code[spwm_num2] / 3550.0f * hrtim_period);
hhrtim1.Instance->sTimerxRegs[0].CMP1xR = cmp_val1;
hhrtim1.Instance->sTimerxRegs[1].CMP1xR = cmp_val2;
}
该方法便于理解,也很常用。
2. 本文介绍另一种无极变频方法:相位累加法
这种方法不需要额外的定时器,更高级,频率分辨率极高。
-
HRTIM载波频率固定 (例如20kHz),其中断(更新中断)频率也是固定的20kHz。
-
在中断中,不再是简单地 spwm_num++,而是维护一个32位的相位累加器 phase_acc。
-
每次中断都给 phase_acc 增加一个固定的 相位增量 phase_step。
-
正弦表的索引 spwm_num 取自phase_acc 的高位。spwm_num = phase_acc >> 24; (例如)
-
如何改变输出频率? 只需要改变 phase_step 的大小即可。phase_step 越大,相位累加越快,查表越快,输出频率越高。
-
输出频率 = (相位增量 * 载波频率) / 2^32 这个方法避免了动态修改定时器的麻烦,是数字信号处理中生成变频信号的标准做法。
相位累加器法,它在数字信号处理中也被称为 直接数字频率合成 (Direct Digital Synthesis, DDS)。
这个方法非常强大且高效,是绝大多数现代变频器、信号发生器等设备生成高质量可变频率波形的核心技术。
核心原理:从“位置”思维到“速度”思维的转变
我们先用一个时钟的例子来理解两种方法的区别:
方法一(原始方法变种):就像一个 “报时员”。有一个额外的定时器作为节拍器,这个节拍器每响一次(中断一次),报时员(SPWM_SET函数)就去看一下秒针应该走到哪个位置 (spwm_num++),然后更新输出。想让秒针走快点(提高频率),就让节拍器响得更频繁。这种方法的缺点是,每次改变频率,都要重新计算并设定节拍器的参数。
方法二(相位累加器法):更像一个 “高精度齿轮”。我们有一个固定的、极快的时钟滴答声(这就是HRTIM的20kHz更新中断,永不改变)。我们还有一个巨大的、有 2^32 (约43亿) 个刻度的精密齿轮,这个齿轮代表了从0到360度的完整相位。
“速度”由谁决定? 由一个叫 相位增量 (phase_inc) 的值决定。
如何工作? 每当听到一次时钟滴答,我们就把这个大齿轮往前拨动 phase_inc 这么多个小刻度。
phase_inc 大一点:每次拨动的角度大,齿轮转得快,输出频率就高。
phase_inc 小一点:每次拨动的角度小,齿轮转得慢,输出频率就低。
关键点:我们改变输出频率,不再是去修改任何定时器的频率,而仅仅是去修改一个内存中的变量 phase_inc 的值。
“相位”与“相位累加器”
相位 (Phase):在正弦波中,相位就是它在一个周期中所处的位置,通常用0到360度或0到2π弧度表示。
相位累加器 (phase_acc):就是一个非常大的无符号整数(通常是32位,uint32_t),我们用它的整个数值范围 [0, 2^32 - 1] 来精确地映射一个完整的360度相位。
0 对应 0 度。
2^31 (一半) 对应 180 度。
2^32 - 1 (最大值) 对应接近 360 度。
当这个32位整数溢出(从最大值 0xFFFFFFFF 再加1变回 0)时,正好完美对应了相位从360度回到0度的周期性循环。我们利用的就是这个天然的溢出特性!(不必手动归零)
详解“高低位是怎么回事?”——从相位到数组索引的映射
这是整个方法最巧妙的地方。现在我们有了一个代表精确相位的、高达43亿的数字 phase_acc,但我们的正弦表 spwm_code 可能只有100个点,甚至为了效率更高,我们通常会用256或512个点(强烈推荐使用2的N次方大小的表)。
问题: 如何将一个0到43亿的数字,映射到0到255的数组索引上?
答案就是:取这个32位数字的最高有效位 (Most Significant Bits, MSB)。
让我们以一个256点的正弦表为例,这会让解释变得极其简单和直观。
一个256点的数组,它的索引范围是0到255。要表示0-255,我们需要多少个二进制位?答案是8位 (2^8 = 256)。
现在想象你的32位相位累加器 phase_acc:
- 高8位 (b31 - b24):这8位组合起来,数值范围正好是0到255。当
phase_acc从0缓慢增长到0xFFFFFFFF时,这高8位也会从0平滑地增长到255。它们就像时钟的时针,虽然走得慢,但指明了当前大概在哪个小时。 - 低24位 (b23 - b0):这24位提供了极高的精度,代表了两个主要刻度之间的细微相位。它们就像时-钟的秒针和更精细的毫秒针。
映射操作:
我们只需要取出 phase_acc 的高8位,就可以直接把它当作256点正弦表的索引!在C语言中,这个操作就是右移位。
uint8_t index = (uint8_t)(phase_acc >> 24);
>> 24的意思是:将这个32位的数字向右移动24位。原来的b24移动到b0的位置,b25移动到b1的位置…b31移动到b7的位置。这样,原来的高8位现在就变成了这个数的低8位,我们把它取出来,就得到了0-255的索引。
这就是“高低位”的秘密:我们用整个32位来累加相位以获得高精度,但只用它的最高几位来查表,因为我们的波形表没有那么精细。
频率是如何计算的
这个方法的频率计算非常精确:
输出频率 (Fout) = (相位增量 (phase_inc) * 更新频率 (Fs)) / 2^32
- Fs (更新频率/载波频率):是固定不变的,就是你HRTIM更新中断的频率,比如 20,000 Hz。
- 2^32:是相位累加器的总刻度。
所以,如果我们想得到某个频率 Fout,我们只需要反过来计算 phase_inc 即可:
相位增量 (phase_inc) = (Fout * 2^32) / Fs
举例:
- 系统时钟72MHz, HRTIM周期3600 -> 载波频率
Fs= 20,000 Hz. 2^32约等于4.295 * 10^9
-
想输出 50 Hz:
phase_inc = (50 * 2^32) / 20000 = 10,737,418(取整) -
想输出 500 Hz:
phase_inc = (500 * 2^32) / 20000 = 107,374,182(取整)
你看,想要改变频率,只需要修改 phase_inc 这个变量的值。切换可以是瞬间的,无缝的,并且频率分辨率极高(可以输出50.001Hz这样的频率)。
例程
#include <math.h> // for sinf
#define CARRIER_FREQ 20000UL // 固定载波频率, Hz
#define SYS_CLOCK_FREQ 72000000UL
#define HRTIM_PERIOD (SYS_CLOCK_FREQ / CARRIER_FREQ)
// !!! 强烈建议使用2的N次方大小的表,这里以256为例 !!!
#define SPWM_TABLE_SIZE 256
uint16_t spwm_code[SPWM_TABLE_SIZE];
// 全局、易失的相位变量
volatile uint32_t g_phase_acc_A = 0;
volatile uint32_t g_phase_inc_A = 0; // 控制A相频率
volatile uint32_t g_phase_acc_B = 0;
volatile uint32_t g_phase_inc_B = 0; // 控制B相频率
float spwm_Gain = 0.5;
// ---- 1. 在初始化时调用一次,生成正弦表 ----
void GenerateSpwmTable(void)
{
for(int i = 0; i < SPWM_TABLE_SIZE; i++)
{
// 生成0-3599范围的值 (假设HRTIM周期是3600)
float angle = (float)i * 2.0f * M_PI / SPWM_TABLE_SIZE;
spwm_code[i] = (uint16_t)((1.0f + sinf(angle)) * (HRTIM_PERIOD / 2.0f));
}
}
// ---- 2. 用于改变输出频率的函数,可在主循环中随时调用 ----
void SPWM_SetOutputFrequency(float freq_hz_A, float freq_hz_B)
{
// 使用64位整数进行中间计算防止溢出
g_phase_inc_A = (uint32_t)(((uint64_t)freq_hz_A * 4294967296ULL) / CARRIER_FREQ);
g_phase_inc_B = (uint32_t)(((uint64_t)freq_hz_B * 4294967296ULL) / CARRIER_FREQ);
}
// ---- 3. HRTIM的更新中断服务函数 (以TimerA为例) ----
// 假设此中断以固定的 CARRIER_FREQ (20kHz) 频率触发
void HRTIM1_TIMA_UP_IRQHandler(void)
{
if(__HAL_HRTIM_GET_FLAG(&hhrtim1, HRTIM_FLAG_TIMA_REP))
{
__HAL_HRTIM_CLEAR_FLAG(&hhrtim1, HRTIM_FLAG_TIMA_REP);
// --- A相处理 ---
// 累加相位
g_phase_acc_A += g_phase_inc_A;
// 取高8位作为索引 (32 - 8 = 24)
uint8_t index_A = g_phase_acc_A >> 24;
// --- B相处理 (可以用同一个累加器加一个相位偏移) ---
// 假设B相滞后A相120度 (1/3周期)
uint32_t phase_offset_120_deg = 0xFFFFFFFF / 3;
uint32_t current_phase_B = g_phase_acc_A + phase_offset_120_deg; // 加上相位偏移
uint8_t index_B = current_phase_B >> 24;
// 更新比较值
uint32_t cmp_val_A = (uint32_t)(spwm_Gain * spwm_code[index_A]);
uint32_t cmp_val_B = (uint32_t)(spwm_Gain * spwm_code[index_B]);
hhrtim1.Instance->sTimerxRegs[0].CMP1xR = cmp_val_A; // Timer A
hhrtim1.Instance->sTimerxRegs[1].CMP1xR = cmp_val_B; // Timer B
}
}
这个方法初看可能有点抽象,但一旦理解了“用固定的节拍,通过改变步进大小来控制速度”的核心思想,就会发现它非常优雅和高效。
而无极调压原理很简单,上述代码演示的是按键开环,只需要调节spwm_Gain(0-1)即可,也就是调节衰减系数。
视频实机演示:50HZ-500HZ无极调频,步进10HZ
【电力电子技术】正弦波逆变器无极调频调压原理、相位累加法
- 本人电力电子新能源行业硬件工程师,软件也会点
- 我的B站:尚知物理 (全网同名)
- 个人网站:www.yyscience.cn

- 其中的资源下载站,汇集了包括电力电子技术在内的众多学习资料,方便大家查找下载。

- 之后可能会更新这个视频带讲解
更新:
为了更通俗的描述相位累加法,我们抛弃所有复杂的术语,用一个最直观的游乐园 merry-go-round(旋转木马) 的比喻来解释。
想象一下,我们的最终目标是让一个小人(PWM输出值)沿着一个圆形路径(正弦波)平滑地运动。我们想随心所欲地控制他跑得快还是慢(输出频率高还是低)。
第一步:”搭建我们的“游乐场”
-
一个超大的旋转木马:
这个旋转木马不是普通的360度,而是被我们划分成了 43亿个 (准确地说是 2^32 = 4,294,967,296)
极其微小的“位置点”。这个木马本身,就代表了 一个完整的0°到360°的正弦波周期。在代码里,这个旋转木马就是 uint32_t phase_acc;
(相位累加器)。它是一个32位的无符号整数,它的数值范围正好是0到大约43亿。 -
一个节奏固定的鼓手: 游乐场里有个鼓手,他以 永不改变
的速度在敲鼓,比如每秒敲2万下(20kHz)。每一次鼓声,都是我们命令小人“动一下”的信号。在代码里,这个鼓手就是 HRTIM 的更新中断。我们把它设置为一个固定的频率(比如20kHz),然后就再也不去动它了。
-
一本风景画册:
我们给这个巨大的旋转木马拍了一套风景照,但为了省事,我们没有拍43亿张,只在256个有代表性的位置拍了照片。这256张照片按顺序放在一本画册里,完整地展示了木马一圈的风景(一个完整的正弦波形状)。
在代码里,这本画册就是我们的正弦表 uint16_t spwm_code[256];。
第二步:如何让小人“动起来”?
现在,我们的目标是在每一次鼓声响起时,更新一下小人的位置(即PWM占空比)。
错误的方法:位置 = 位置 + 1。这样小人的速度就固定了,没法改变。
天才的方法(DDS):我们引入一个新概念——“步长”。
“步长” (phase_inc): 这不是小人的最终位置,而是每一次鼓响后,小人应该向前挪动多少个微小的位置点。
整个游戏的核心规则变得极其简单:
每一次鼓声响起,就执行: 当前位置 = 上一次的位置 + 步长
phase_acc = phase_acc + phase_inc;
现在,神奇的事情发生了:
如果我们想让小人跑得慢 (输出50Hz低频波):
我们就把 “步长”(phase_inc) 设得很小,比如设为 10,000。
每次鼓响,小人只往前挪一点点。他需要挪动很多次才能跑完一整圈(43亿个位置点),所以他转得很慢。
如果我们想让小人跑得飞快 (输出500Hz高频波):
我们就把 “步长”(phase_inc) 设得很大,比如设为 100,000。
每次鼓响,小人都往前跨一大步。他很快就能跑完一整圈,所以他转得飞快。
关键点:我们是通过改变一个简单的变量 phase_inc(步长),就控制了小人的速度(频率)。鼓手的节奏(HRTIM中断频率)自始至终都没有变过!
第三步:最难理解,但最巧妙的一点——如何从“位置”找到“照片”?
现在我们遇到了一个问题:
小人的“当前位置” phase_acc 是一个0到43亿之间的大数字。但我们的“风景画册” spwm_code 里只有256张照片,编号是0到255。
如何把一个43亿范围的数字,对应到一个256范围的编号上呢?
答案是:“只看大概,忽略细节”,也就是 取高位。
想象一下,43亿个位置点分布在一个32位的二进制数上:
|b31 b30 b29 b28 b27 b26 b25 b24| |b23 b22 … b1 b0|
<---- 最高8位 ----> ------------------------- <---- 低24位 ---->
最高8位 (上图左侧): 它们组合起来的数值范围正好是 0 到 255。这8位可以告诉我们,小人目前大概在旋转木马的哪一个“区域”。这256个“区域”正好对应我们画册里的256张照片。
低24位 (上图右侧): 它们提供了超高的精度,描述了小人在那个“区域”内部的具体位置。但这对我们选照片来说不重要!我们只需要知道他在哪个大区域就行了。
所以,我们只需要一个简单的操作:把 phase_acc 这个32位数,向右移动24位 (phase_acc >> 24)。
这个操作就像是把低24位的“细节”扔掉,只保留了最高8位的“大概区域编号”。这个编号,就是我们画册 spwm_code 的索引!
画册索引 = 当前位置 >> 24
index = phase_acc >> 24;
- 流程总结与代码演练
现在我们把整个流程串起来,看看到底发生了什么。
假设鼓手每秒敲4下(4Hz中断),我们的画册有4张照片。相位累加器是8位(0-255)。
-
目标:慢速转圈(1秒1圈 = 1Hz) 要用4步走完256个位置点,所以每一步的 步长 phase_inc = 256 / 4 = 64。

你看,它完美地循环了,而且4次鼓声正好循环一圈。 -
目标:快速转圈(1秒2圈 = 2Hz) 要用2步走完256个位置点,所以每一步的 步长 phase_inc = 256 / 2 =
128。
同样是每秒4下鼓声,只因为我们把 步长 从64改成了128,小人的速度就快了一倍!
结论
DDS/相位累加器方法的精髓:
- 固定一个高速时钟(中断):作为执行动作的“节拍”。
- 用一个大整数(相位累加器)来记录精确的“位置”。
- 每一次节拍,都让“位置”前进一个“步长” (phase_acc += phase_inc)。这个整数的自动溢出特性完美地实现了循环。
- 通过改变“步长”的大小,可以任意、精确地控制转圈的“速度”(频率)。
- 用“位置”的高几位作为“大概位置”,去小容量的波形表中查找当前的输出值 (index = phase_acc >> N)。
- 这个方法就像是数字世界的变速齿轮,简单、高效、精准。希望这次的旋转木马比喻能帮助您彻底理解它的工作原理。
更多推荐



所有评论(0)