项目介绍
这是硬禾学堂2025年寒假在家一起练活动平台3的自命题,使用的是STM32G031板卡,利用板上的PWM+LPF模块实现生成正弦波等常用信号的简易信号发生器的功能。
实现的功能
能够产生频率为1-100kHz、幅度为0.3-3V的正弦波、三角波以及方波等常用信号,频率每次可改变1kHz,幅度每次可改变0.3V,并且能够在板卡上的OLED显示屏上显示参数信息,方便用户进行参数调整。
成品展示
硬件介绍与流程图
主要硬件是板上的PWM+LPF模块,以及OLED显示屏、旋转编码器以及按键的使用。
项目流程图如下:
设计思路
由于板卡上缺少DAC模块,因此采用SPWM的方法通过板卡上的PWM+LPF+DMA来实现简易DDS信号发生器,受到LPF的限制,PWM的频率不能太低,STM32G031板卡的主频为64MHz,设置时钟频率为64MHz,PWM的预分频系数为0,自动重装载值为63,因此PWM的频率为1MHz。波形的生成,采用的是读表法,预先存储三种波形的数据数组,数组的长度为1000个,数组的数据是一个完整的波形数据,采用DMA循环模式进行数组的数据搬运并将其设置为PWM的比较值,改变PWM的占空比来改变输出电压的高低,从而生成各种波形,DMA的频率为1MHz,即每1微秒搬运一个数据,如果一个接一个地搬运数组的数据,搬运完1000个数据所需时间为1毫秒,因此生成的信号的频率为1kHz,如果隔着一个数据搬运,那么生成信号的频率为2kHz,通过改变读取数据步进值的大小来实现生成信号频率的改变,幅度的改变是通过按挡位(一共10个挡位)进行缩放实现的(比如10挡就是原数据,6挡就是原数据乘以6再除以10,但这样的做法其实是不精确的,尤其是在信号频率比较高的情况下更为明显)。
按键以及旋转编码器按下操作的检测是通过中断的方式来实现的,旋转编码器的左转和右转检测,则是将旋转编码器的A端(即原理图上的key3)设置为上升沿中断检测,B端(即原理图上的key5)设置为GPIO输入模式,然后在中断回调函数里检测A端是否为高电平,同时检测B端的电平高低情况,如果同为高电平,则是左转;如果一高一低,则是右转。
对于OLED显示屏,设置好SPI协议通信方式以及移植并适当修改OLED功能代码文件以后,便可以正常使用。
主要代码
根据参数调整DMA搬运的数组的数据:
void data_change(void)
{
uint16_t i = 0;
j = 0;
switch (mode) {
case 1:
for (i = 0; j < len; i = i + fre) {
if (i < len) {
buff_value[j] = square_value[i] * fv / 10;
j = j + 1;
}
if (i >= len) {
i = i - len;
buff_value[j] = square_value[i] * fv / 10;
j = j + 1;
}
}
break;
case 2:
for (i = 0; j < len; i = i + fre) {
if (i < len) {
buff_value[j] = sine_value[i] * fv / 10;
j = j + 1;
}
if (i >= len) {
i = i - len;
buff_value[j] = sine_value[i] * fv / 10;
j = j + 1;
}
}
break;
case 3:
for (i = 0; j < len; i = i + fre) {
if (i < len) {
if (triangle_value[i] == 30)
buff_value[j] = triangle_value[i];
else if (triangle_value[i] > 30)
buff_value[j] = triangle_value[i]
- (triangle_value[i] - 30) * (10 - fv) / 10;
else if (triangle_value[i] < 30)
buff_value[j] = triangle_value[i]
+ (30 - triangle_value[i]) * (10 - fv) / 10;
j = j + 1;
}
if (i >= len) {
i = i - len;
if (triangle_value[i] == 30)
buff_value[j] = triangle_value[i];
else if (triangle_value[i] > 30)
buff_value[j] = triangle_value[i]
- (triangle_value[i] - 30) * (10 - fv) / 10;
else if (triangle_value[i] < 30)
buff_value[j] = triangle_value[i]
+ (30 - triangle_value[i]) * (10 - fv) / 10;
j = j + 1;
}
}
break;
}
}
旋转编码器左转和右转检测:
void HAL_GPIO_EXTI_Rising_Callback(uint16_t GPIO_Pin)
{
switch(GPIO_Pin)
{
case key3_Pin:
HAL_Delay(1);
if (HAL_GPIO_ReadPin(key3_GPIO_Port, key3_Pin)
&& HAL_GPIO_ReadPin(key5_GPIO_Port, key5_Pin)) { //旋转编码器左转
HAL_TIM_PWM_Stop_DMA(&htim3, TIM_CHANNEL_3);
switch (pat) {
case 0:
fv = fv - 1;
if (fv == 0)
fv = 10;
break;
case 1:
fre = fre - 1;
if (fre == 0)
fre = 100;
break;
case 2:
mode = mode - 1;
if (mode == 0)
mode = 3;
break;
}
OLED_Clear();
switch (mode) {
case 1:
OLED_ShowString(10, 20, (uint8_t*)"Square ", 8, 1);
break;
case 2:
OLED_ShowString(10, 20, (uint8_t*)"Sine ", 8, 1);
break;
case 3:
OLED_ShowString(10, 20, (uint8_t*)"Triangular", 8, 1);
break;
}
switch (pat) {
case 0:
OLED_ShowString(10, 50, (uint8_t*)"State: Amplitude", 8, 1);
break;
case 1:
OLED_ShowString(10, 50, (uint8_t*)"State: Frequency", 8, 1);
break;
case 2:
OLED_ShowString(10, 50, (uint8_t*)"State: Wave Form", 8, 1);
break;
}
sprintf( Vpp, "%d", fv * 300);
OLED_ShowString(42, 30, (uint8_t*)Vpp, 8, 1);
sprintf( FV, "%d", fre);
OLED_ShowString(30, 40, (uint8_t*)FV, 8, 1);
OLED_Showinterface();
OLED_Refresh();
data_change();
HAL_TIM_PWM_Start_DMA(&htim3, TIM_CHANNEL_3, (uint32_t*) buff_value,
len);
}
else if (HAL_GPIO_ReadPin(key3_GPIO_Port, key3_Pin)
&& !HAL_GPIO_ReadPin(key5_GPIO_Port, key5_Pin)) { //旋转编码器右转
HAL_TIM_PWM_Stop_DMA(&htim3, TIM_CHANNEL_3);
switch (pat) {
case 0:
fv = fv + 1;
if (fv == 11)
fv = 1;
break;
case 1:
fre = fre + 1;
if (fre == 101)
fre = 1;
break;
case 2:
mode = mode + 1;
if (mode == 4)
mode = 1;
break;
}
OLED_Clear();
switch (mode) {
case 1:
OLED_ShowString(10, 20, (uint8_t*)"Square ", 8, 1);
break;
case 2:
OLED_ShowString(10, 20, (uint8_t*)"Sine ", 8, 1);
break;
case 3:
OLED_ShowString(10, 20, (uint8_t*)"Triangular", 8, 1);
break;
}
switch (pat) {
case 0:
OLED_ShowString(10, 50, (uint8_t*) "State: Amplitude", 8, 1);
break;
case 1:
OLED_ShowString(10, 50, (uint8_t*) "State: Frequency", 8, 1);
break;
case 2:
OLED_ShowString(10, 50, (uint8_t*) "State: Wave Form", 8, 1);
break;
}
sprintf( Vpp, "%d", fv * 300);
OLED_ShowString(42, 30, (uint8_t*)Vpp, 8, 1);
sprintf( FV, "%d", fre);
OLED_ShowString(30, 40, (uint8_t*)FV, 8, 1);
OLED_Showinterface();
OLED_Refresh();
data_change();
HAL_TIM_PWM_Start_DMA(&htim3, TIM_CHANNEL_3, (uint32_t*) buff_value,
len);
}
break;
default:
break;
}
}
遇到的困难
首先遇到的困难便是STM32CubeIDE软件的使用,之前也没有接触过类似的软件,所以刚开始使用起来还是比较困难的。其次由于是第一次接触这种项目,加上很多课程没学,目前自己所掌握的知识很少(能真正用得上的可能就只有C语言了),所以好多知识需要自己去现学以及上网查资料。项目的整体思路以及代码的编写参考了一些网上现有的例程,自己也是受益匪浅。
OLED显示屏的使用比较顺利,反复观看了几次配置视频后能初步使用OLED模块,但对OLED模块的具体原理不是很清楚。
项目进行过程中一个困惑我很久的问题是按键的外部中断检测无法正常使用,自己反复查看硬件配置以及代码编写后也没有发现问题。上网也查了不少资料,最后在一篇CSDN的博客中找到了问题所在:因为在外部中断里面使用了delay函数,因此在进行配置时,需要将系统滴答定时器优先级设置比外部中断优先级高,这样在外部中断里面使用delay函数才不会卡死。按照博客所说的内容重新进行配置,最终成功解决了相关问题。
心得体会
这次寒假在家一起练活动对我来说也算是一个不小的挑战,好多知识都需要自己现学,项目的思路设计也是上网查找了不少资料以及参考了一些现有的例程,最后拼拼凑凑完成了这个项目。虽然这个项目所实现的简易信号发生器功能并不是很完善,精准度也有待提高(尤其是高频情况下),但是对自己来说已经收获很多了。通过本次项目,我大致了解了STM32单片机以及STM32CubeIDE软件的使用,也学到了一些有关STM32以及如何进行信号生成的知识,同时提升了自己的信息获取能力,在网上学到了不少有用的知识。总的来说,这次活动让我受益匪浅。
