项目介绍:
我实现的是该项目的任务二,要求基于STM32设计一款DC-100KHz的DDS任意波形发生器,由于是初次接触这种项目对于整体构思参考了网上的一些已发表的项目,并且在代码编写上也求教了一些一起做项目的同学,受到诸多启发。
实现的功能及图片展示:
基于STM32G031的简易信号发生器所实现的功能有:
1.能产生正弦波和可调占空比(10% 90%,步进10%)的三角波和方波。
2.输出频率可在DC到100kHz之间调整,步进值(全频带可保证的分辨率)10Hz.e
3.输出无失真波形的峰峰值理论范围为OV— 2.8V(受限于硬件电路性能),理论上,输出峰峰值的可调精度最高为1.5625%(受限于系统主频),实际设计可调精度约1.7%。
4.有易于操作的用户界面,可通过旋钮与按键,在上述可调范围内调整输出的波形、占空比(仅限方波和三角波)、峰峰值、频率。
部分图片展示:
ch1接输出(蓝色波形),ch2接地(黄色波形),受器件精确度和环境因素影响,测量结果稍有偏差但在可接受范围内。
正弦波:设置为占空比50%,峰峰值2.5V,频率5KHZ
方波:设置参数为占空比70%,峰峰值2.70V,频率6KHZ
三角波:设置参数为占空比50%,峰峰值2.75V,频率9kHZ
整体架构(含代码分析思路)
1.SPWM法输出任意波形
由于STM32G031G8的外设资源中并没有板载的DAC,因此只能依据SPWM法的原理,运用PWM波+低通+跟随器的方式,通过调整PWM波占空比来近似改变Aout的输出电平,从而实现各种波形的输出。
1.SPWM法原理分析
本项目所用开发板上的硬件电路为二阶RC低通,其中R = 1kHz,C = 1nF。若按一阶RC电路的方法近似估计其时间常数,则有τ ≈ RC = 1us。根据RC充放电电路的电压随时间t的指数关系,若PWM周期为T,充电时间为t1,则占空比为D = t1 / T;设某个PWM周期开始时,电容电压为V0,PWM高电平为Vm,此周期结束后电容电压为V1,则:
V1 = [ V0 + (Vm - V0) * (1 - exp(-t / τ))] * exp(-(T - t1) / τ) (1)
将上式展开整理,得:
V1 = Vm * exp (-T * (1 - D) / τ) - (Vm - V0) * exp(-T / τ)
= V0 * exp(-T / τ) + Vm * exp(-T / τ) * (-1 + exp((T * D) / τ)) (2)
由于-1 + exp(x)在0附近的一阶展开为x,上式在满足 T * D << τ时近似为:
V1 = V0 * exp(-T / τ) + Vm * exp(-T / τ) * T * D / τ
= exp(-T / τ) * ( V0 + Vm * (T / τ) * D) (3)
则我们可以近似认为,本周期内电压增量与D近似成线性关系。以此类推,将上述关系式进行递推,设从V0起算的i个周期后,电容电压为Vi,第i个周期占空比为Di,则有:
V2 = exp(-2 * T / τ) * (V0 + Vm * (T / τ) * D1) + exp(-T / τ) * Vm * (T / τ) * D2
V3 = exp(-3 * T / τ) * (V0 + Vm * (T / τ) * D1) + exp(-2 * T / τ) * Vm * (T / τ) * D2
+ exp(-T / τ) * Vm * (T / τ) * D3
… … … … … …
可以看出,在Vi的表达式中,相对于最后一个指数项而言,可以认为前面的高次指数项足够小,因此可以近似忽略,于是近似有
Vi ≈ exp(-T / τ) * Vm * (T / τ) * Di (4)
从而第i个周期后的电平近似由第i个周期的占空比Di决定。
2.本项目的PWM输出设置与局限性
本项目所用芯片的主频最高为64MHz,结合1.中的分析,应使PWM的周期大约在1us以下,并且理论上越小越好。但另一方面,理论分辨率取决于定时器重装载值ARR,而
F_sysclk / F_pwm = ARR (5)
因此这两者是一对矛盾。
在考虑硬件电路参数的情况下兼顾理论分辨率,最终选择PWM频率为1MHz,则其理论分辨率为log2 ( 64) = 6 bit。
相比于使用DAC产生波形的方法,SPWM法虽然节省了外设资源,但整体而言,输出纹波较重,其信噪比其实远达不到标准的6位DAC的信噪比。因为本项目中,PWM周期T相对于等效时间常数τ而言,并不完全满足远小于的条件,纹波噪声也可以看作由此而导致的指数函数一阶近似的误差。
2.程序设计
1.整体概述
本程序的核心架构如下图所示。
主要思路是用定时器触发DMA通道传输内存数组到TIM3的CCR3(PWM输出通道的捕获/比较寄存器)。由于定时器的溢出频率是可控的,因此“DAC”的采样率,或样点数,是可控的。
2.频率精度
本程序的设计能在固定的RAM资源消耗下最大限度保证频率精度。PWM溢出频率为F_pwm = 1MHz。当要求输出频率为F_out≤100kHz时,理论上一周期内样点数N应为
N = F_pwm / F_out (6)
①当N恰为整数时,若F_out较大,则样点数相对较少(如10kHz时为100样点/周期),可以直接用数组来实现。然而当F_out较小,如10Hz时,理论样点数为100k样点/周期,而事实上,100样点/周期的精度已经足以保证保真度了。直接开出100k大小的数组不仅是对资源的极大浪费,而且对降低失真度没有贡献。
但PWM的溢出频率是不能降低的,也就是说TIM3的溢出频率不能改变。因此,当输出频率较小时,用TIM2作为TIM3的从定时器,对TIM3_TRGO进行分频,同时令TIM2_TRGO为DMA触发源。令TIM2_PSC=0,则当TIM2_ARR = M时,TIM3每溢出M次,TIM2才会产生一次DMA请求,否则TIM3_CCR3保持不变。这样,在输出10Hz时,只要令
TIM2_ARR = 1000,则可以在只有100样点的情况下正常输出波形。
②当N不为整数时,不能对N进行简单的取整。
假设F_out = 38kHz,则N = 26.31。若对N直接取整,令N = 26,则实际输出频率为38.46kHz。但按照这种规则,N在(25.50 , 26.49)这个范围内都应被取整成26,也就是说设置频率在(37.75kHz , 39.21kHz)范围内的输出均会变成38.46kHz,其分辨率连1kHz都达不到,这是不能接受的。
N的含义是“每一个输出周期内1MHz的周期个数”。若将N小数点右移两位,则100*N的含义变为“每100个输出周期内1MHz的周期个数”。也就是说,若取样范围扩大到100个周期,输出频率的精度将大幅提高。此时输出频率为38.008kHz,控制在
(38.002kHz , 38.016kHz)内,分辨率已经接近于10Hz。若输出频率较小,则仿照①中做法,对TIM3_TRGO进行分频。
最终程序设计的样点数组大小控制为4000。也就是100周期内最大4000个样点。
3.频率分辨率
考虑最坏的情况,若设置F_out = 99999Hz,则N = 10.0001,也就是说,要想在全频段内精确到1Hz,在1MHz的采样率下,至少要开一个几万大小的数组,这并不现实。若F_out = 99990Hz,则N = 10.001。以4000大小的数组为计,平均每个周期只能有约4个样点。但因为(6)式的反比例函数特性,当F_out变小,采样同样算法的分辨率将得到提高。因此平均而言,认为本程序在全频段内能保证的分辨率就是10Hz。
附上部分程序代码:(已注释)
void SPWM_Set(uint32_t frequency, float amplitude, wave_type wave, float duty_cycle)
{
uint32_t factor;
uint16_t arr;
uint16_t i;
float factor_f;
if(frequency > 100000 || amplitude > 3.3f)
return;
//wave generation module stop
{
TIM3->CR1 &= ~0X1;
TIM2->CR1 &= ~0X1;
DMA1_Channel1->CCR &= ~0X1;
TIM2->DIER &= ~(0X1 << 8);
TIM3->DIER &= ~(0X1 << 8);
}
//要先排除0的情况
if(frequency == 0)
{
arr = 0;
N = 0;
TIM3->CCR3 = (uint16_t)(64.0f * amplitude / 3.3f) - 1;
TIM3->CNT = 0;
TIM3->CR1 |= 0X1;
return;
}
//由于1M = 2^6 * 5^6,因此能够整除1M的数,必然含有且只含有质因数2、5
if(SPWM_MSPS % frequency == 0)
{
factor = SPWM_MSPS / frequency;
//factor = N * ARR
//factor 至少含有质因数2、5中的一个,且不可能包含其他的质因数
//说明factor只含有质因数5,即N * ARR <= 5^6,而且根据100kHz的限制,factor>10,因此factor此时不小于25
if(factor % 2 == 1)
{
N = 25;
//此时即使最坏的情况,factor=15625,ARR的值是1M / 25 = 625,也足以胜任
arr = factor / N;
while(arr > 1 && N <= 50)
{
N *= 5;
arr /= 5;
}//点数越多越好
}
else if(factor % 5 != 0)
{
//2^n是不可能被5整除的
//此时说明factor只含有质因数2,factor最大为64,不用使用ARR
N = factor;
arr = 0;
}
else
{
//该情况下factor至少含有一个2和一个5,则N应该从10开始计算
//最坏的情况下,factor = 1000000,在去掉一个10的情况下,ARR最大为100000,16位不能表示。
//但只要任意除一个2或5,则ARR最大为50000,16位已经可以表示。
//而点数自然越多越好
N = 10;
factor /= 10;
//先穷尽质因数5
while((factor % 5 == 0)&&(N <= 50))
{
N *= 5;
factor /= 5;
}
//再穷尽质因数2
while((factor % 2 == 0)&&(N <= 128))
{
N *= 2;
factor /= 2;
}
arr = factor;
}
if(wave == SINE)
{
for(i = 0 ; i < N ; i ++)
{
out_buf[i] = (uint8_t)((32.0f * sinf(2.0f * Pi * i / (N * 1.0f)) + 32.0f) * (63.0f/64.0f) * (amplitude / 3.3f));
}
}
else if(wave == TRIANGLE)
{
if(duty_cycle >= 1.0f && duty_cycle <= 0.0f)return;
for(i = 0; i < N ; i ++)
{
out_buf[i] = (uint8_t)(64.0f * trf(duty_cycle , 1.0f * i / (N * 1.0f)) * (63.0f/64.0f) * (amplitude / 3.3f));
}
}
else if(wave == SQUARE)
{
if(duty_cycle >= 1.0f && duty_cycle <= 0.0f)return;
for(i = 0 ; i < N ; i ++)
{
out_buf[i] = (uint8_t)(64.0f * sqf(duty_cycle , 1.0f * i / (N * 1.0f)) * (63.0f/64.0f) * (amplitude / 3.3f));
}
}
}
4.控制界面
由于本开发板上的按键、旋钮等资源相对较少,因此控制界面的设计思路是:通过左右按键和旋钮的按键来改变旋钮调整的对象,从而将多个调整功能集中在一个旋钮上。
GUI的构思如下:
如上图所示:
①按下左键进入第一层,按下右键进入第二层,按下旋钮进入第三层。
②旋钮只能在当前所在层的所在列内切换,用左旋或右旋表示增减、改变。
③若绿灯亮,说明当前输出波形与界面设置一致;当绿灯灭,说明在输出波形在上一次更新之后,又再次改动了界面,即输出波形和界面设置可能不一致。
④按住右键的情况下,点击左键,即可更新输出,此时可见绿灯再次亮起。在绿灯亮起的情况下只要转动旋钮,绿灯就会熄灭。
操作流程图如下: