基于EVM_MSPM0L1306开发套件实现简易PWM发生器
该项目使用了keil软件、sysconfig软件、C语言,实现了简易PWM发生器的设计,它的主要功能为:基于EVM_MSPM0L1306开发套件,使用MSPM0L1306的定时器,生成2路重复频率1Hz~1MHz的PWM信号,每一路的重复频率和占空比都可独立调节,将PWM的频率和占空比和输出引脚在OLED上显示,并将信息通过串口上传到上位机。。
标签
嵌入式系统
PWM
参加活动
参加活动/培训
MSPM0L1306
Travels
更新2024-07-18
北京邮电大学
274

EVM_MSPM0L1306开发套件实现简易PWM信号发生器

一、项目介绍

我做的项目是“简易信号发生器”,是EVM_MSPM0L1306开发板完成全额返活动中的任务2,具体任务要求是:image.png

该项目按要求使用EVM_MSPM0L1306开发板实现任务指定的功能要求。

在任务要求中,我要做到如下功能:

1. 使用定时器生成2路重复频率1Hz~1MHz的PWM信号,其重复频率和占空比能用按钮或拨码开关调节,而且调节精度要能达到表格的指标。

2. 使用32MHz的主时钟

3. 能在OLED上显示2路PWM分别输出的引脚及PWM参数(频率和占空比)。

4. 能够将PWM参数(频率、占空比、通道工作与否)按照1s左右的周期,通过串口发送到上位机。


二、设计思路

1.PWM生成和调整。

PWM生成的设计我采用了好几种办法,在经过不断地优化后,我选择了采用sysconfig中的TIMER-PWM来生成PWM波,并在程序内:通过DL_TimerG_setLoadValue函数来修改PWM波的频率,通过DL_TimerG_setCaptureCompareValue函数来修改PWM波的占空比。

 

2. OLED显示。

我采用的是例程中使用SPI控制OLED显示的方式,并且使用了例程中写好的库文件来显示字符、字符串、数字、文字等等。

 

3. 矩阵键盘控制。

我采用的是矩阵键盘的中断实现例程的方法,通过按键的中断来判断当前矩阵键盘按的是哪个按钮。然后,执行对应的控制功能。

 

4. 串口输出到上位机。

我采用的是UART上传的方法,在sysconfig中初始化UART传输,然后使用相关函数上传信息。

 

三、硬件框图

image.png


四、软件流程图

image.png


五、硬件介绍

使用了mspm0l1306芯片,外设使用4*4矩阵键盘实现功能控制,两个定时器控制输出PWM,7针OLED显示信息,串口输出信息到电脑。


六、实现功能

1. 实现了在PA8和PA16引脚上输出独立的PWM波,并且频率和占空比都可以通过按键在1Hz~10kHz之间调节,而且,可以通过按键直接控制频率调节成100kHz或1MHz。


2. 实现了将PWM波的引脚信息和PWM波的频率和占空比显示在OLED上,并且矩阵键盘的输入、控制等操作也会在OLED上展示出来,方便使用。


3. 实现了将PWM波的频率和占空比信息上传到OLED上,为了节约板载资源使用,减少对其他模块的影响,这里只在按键进行控制时才进行串口信息的上传(因为串口信息的修改也仅仅有可能发生在按键进行控制之后),但是并没有实现指标要求的1s上传一次。


七、图片展示

1.一路是输出频率为6666Hz,占空比为80%的PWM波,二路是输出频率为666Hz,占空比为30%(这里有点问题,测试仪器是借的老师的很贵只借了一小会,就没有再调整程序)的PWM波。图4.jpg


2.这个时候的板子OLED显示的清晰图。(本来是PA8输出1路,但是示波器显示不出波形就修改到了PA26才有波形,可能是复用引脚的原因,程序已改)图3.jpg


3.100kHz的输出。(可能是当时没细调,也可能是性能原因,导致这种情况下波形不太好看。但是从下面的测量值可以看出输出的PWM波在100kHz左右的)图6.jpg


4.1MHz的输出。(这里把示波器输出波形调好了些,从下面测量值看确实是1MHz。)图5.jpg


下面就主要是一些OLED上和串口上的图片展示。

5.信息显示顺序:时间、1路频率、1路占空比(0~100)、2路频率、2路占空比(0~100))

image.png


6.修改对象(用星号标识):1路的占空比。目前输入值:1236(很明显不会输入成功)image.png


7.修改了1路的频率和2路的占空比,已经将2路的频率修改为了1MHz

image.png


八、主要代码片段及说明

1.main函数内,电源,GPIO,OLED,变量,矩阵键盘中断的初始化。

SYSCFG_DL_init();


OLED_Init();
OLED_Clear();

//变量创建与初始化
int ini_fre_1=1000; //在改变频率显示模式时记录普通模式下的频率值,方便恢复
int ini_fre_2=1000;
int input_value[8]={0,0,0,0,0,0,0,0}; //最大写入长度:7
int input_flag=-1; //用来计数当前input_value存储到哪一位
int select_x=56;
int select_y=4;
frequence_1=1000;
frequence_2=1000;
//表示PWM波的占空比
duty_cycle_1=80;
duty_cycle_2=40;
int input_value_sum=0;
int channel_now=1; //初始化为1路
int object_now=1; //初始化为控制频率"F"(用1表示)
int special_fre_change=0; //表示当前为普通显示方式(当切换到它时,或点击确认键时,都要切回到普通显示)
load_value_1=320000*(100-80)/1000;//比较值数,和低电平周期数相同(因为是倒数)
load_value_2=320000*(100-40)/1000;
compare_value_1=320000*(100-80)/1000;
compare_value_2=320000*(100-40)/1000;

//矩阵键盘中断使能
NVIC_EnableIRQ(MAT_KEY_INT_IRQN); // IO中断使能
DL_GPIO_clearPins(MAT_KEY_PORT, MAT_KEY_ROW0_PIN | MAT_KEY_ROW1_PIN | MAT_KEY_ROW2_PIN | MAT_KEY_ROW3_PIN); // 0000


2.main函数内判断当前按键。功能:修改选中对象:是1路/2路的频率/占空比。

if(key_num == 4)
{
channel_now=(channel_now == 1)? 2 : 1; //实现变量在1和2之间切换(1/2路)
Change_xy(channel_now,object_now,&select_x,&select_y);
ShowBasicInfo(input_value_sum,select_x,select_y);
ShowBasicInfo(input_value_sum,select_x,select_y);
}
else if (key_num == 12)
{
object_now=(object_now == 1)? 2 : 1; //实现变量在1和2之间切换(频率/占空比)
Change_xy(channel_now,object_now,&select_x,&select_y);
ShowBasicInfo(input_value_sum,select_x,select_y);
ShowBasicInfo(input_value_sum,select_x,select_y);
}


3.判断按键输入。功能:判断0~9数值的输入,存储到数组中,计算输入的总的值,并显示(方便之后对频率或占空比进行修改)。之所以调用了两次ShowBasicInfo函数是因为这样能解决OLED显示方向出错的bug,虽然不知道为什么。

else if ((key_num >= 1 && key_num <= 3) || (key_num >= 5 && key_num <= 8) || (key_num >= 9 && key_num <= 11))
{
//如果输入的是数字,则存入数组,计算总的数值,并显示。
if(input_flag+1 == 7) //如果超出写入位置
{
//不写入数组
}
else
{
input_flag++;
input_value[input_flag]=key2realnum[key_num]; //顺位写入键盘输入值
}

//计算总的值
sum_value1=0;
value2=0;
for(int i=0;i<=input_flag;i++)
{
value2=sum_value1;
sum_value1=value2*10+input_value[i];
}
input_value_sum=sum_value1;

ShowBasicInfo(input_value_sum,select_x,select_y);
ShowBasicInfo(input_value_sum,select_x,select_y);
}


4.判断输入按键。功能:切换频率是显示普通频率(1Hz~10kHz)或显示100kHz或显示1MHz。

else if (key_num == 15)
{
//频率切换为1~10kHz或者100kHz或者1MHz。顺序切换
special_fre_change++;
if(special_fre_change==3)
{
special_fre_change=0;
}
if (special_fre_change == 0)
{
if (channel_now == 1)
{
frequence_1=ini_fre_1;
load_value_1=32000000/frequence_1;
compare_value_1=320000*(100-duty_cycle_1)/frequence_1;
}
else
{
frequence_2=ini_fre_2;
load_value_2=32000000/frequence_2;
compare_value_2=320000*(100-duty_cycle_2)/frequence_2;
}
}
else if(special_fre_change == 1)
{
if (channel_now == 1)
{
ini_fre_1=frequence_1;
frequence_1=100000;
load_value_1=320;
compare_value_1=32*(100-duty_cycle_1)/10;
}
else
{
ini_fre_2=frequence_2;
frequence_2=100000;
load_value_2=320;
compare_value_2=32*(100-duty_cycle_2)/10;
}
}
else if(special_fre_change == 2)
{
if (channel_now == 1)
{
frequence_1=1000000;
load_value_1=32;
compare_value_1=32*(100-duty_cycle_1)/100;
}
else
{
frequence_2=1000000;
load_value_2=32;
compare_value_2=32*(100-duty_cycle_2)/100;
}
}
//统一修改频率和占空比
DL_TimerG_setCaptureCompareValue(PWM_0_INST,compare_value_1, DL_TIMERG_CAPTURE_COMPARE_0_INDEX);
DL_TimerG_setCaptureCompareValue(PWM_1_INST,compare_value_2, DL_TIMERG_CAPTURE_COMPARE_1_INDEX);
DL_TimerG_setLoadValue(PWM_0_INST,load_value_1-1);
DL_TimerG_setLoadValue(PWM_1_INST,load_value_2-1);
//显示
ShowBasicInfo(input_value_sum,select_x,select_y);
ShowBasicInfo(input_value_sum,select_x,select_y);
}


5.按键判断。功能:确认按键,将输入的数值修改到对应通道上的频率或占空比。并将输入值清空。

else if (key_num == 16)
{
//按下确认键,则将写入的频率或占空比传递给对应的路(修改变量),并将输入值清空。然后显示。
//修改频率/占空比
if (channel_now==1)
{
if (object_now==1 && input_value_sum>=1 && input_value_sum<=10000)
{
//根据当前处于特殊频率(100kHz和1MHz)还是普通频率,选择赋值
if (special_fre_change == 0)
{
frequence_1=input_value_sum;
load_value_1=32000000/frequence_1;
compare_value_1=320000*(100-duty_cycle_1)/frequence_1;
}
else
{
ini_fre_1=input_value_sum;
}
}
else if (object_now==2 && input_value_sum>=0 && input_value_sum<=100)
{
duty_cycle_1=input_value_sum;
compare_value_1=320000*(100-duty_cycle_1)/frequence_1;
}
}
else if (channel_now==2)
{
if (object_now==1 && input_value_sum>=1 && input_value_sum<=10000)
{
//根据当前处于特殊频率(100kHz和1MHz)还是普通频率,选择赋值
if (special_fre_change == 0)
{
frequence_2=input_value_sum;
load_value_2=32000000/frequence_2;
compare_value_2=320000*(100-duty_cycle_2)/frequence_2;
}
else
{
ini_fre_2=input_value_sum;
}
}
else if (object_now==2 && input_value_sum>=0 && input_value_sum<=100)
{
duty_cycle_2=input_value_sum;
compare_value_2=320000*(100-duty_cycle_2)/frequence_2;
}
}

//按确认键修改完频率或占空比后,统一对输出的PWM波进行修改
load_value_1=32000000/frequence_1;
load_value_2=32000000/frequence_2;
compare_value_1=320000*(100-duty_cycle_1)/frequence_1;
compare_value_2=320000*(100-duty_cycle_2)/frequence_2;
DL_TimerG_setCaptureCompareValue(PWM_0_INST,compare_value_1, DL_TIMERG_CAPTURE_COMPARE_0_INDEX);
DL_TimerG_setCaptureCompareValue(PWM_1_INST,compare_value_2, DL_TIMERG_CAPTURE_COMPARE_1_INDEX);
DL_TimerG_setLoadValue(PWM_0_INST,load_value_1-1);
DL_TimerG_setLoadValue(PWM_1_INST,load_value_2-1);

input_value_sum=0; //存储值清零
input_flag=-1;
ShowBasicInfo(input_value_sum,select_x,select_y);
ShowBasicInfo(input_value_sum,select_x,select_y);
}


6.main函数内,按键判断完后的处理。比如给前面OLED的显示提供一定的缓冲时间,然后就是连接需要上传的字符串,并将其通过串口发送到上位机。

delay_ms(1000);	//给OLED显示提供一定的迟缓时间。(必要)

// //可以在按键检测完上传一次串口,减少资源使用。
int2char(frequence_1,fre_1_char);
int2char(frequence_2,fre_2_char);
int2char(duty_cycle_1,D_C_1_char);
int2char(duty_cycle_2,D_C_2_char);

//尝试连接字符串,然后统一发送
strcat(tx_str,fre_1_char);
strcat(tx_str," ");
strcat(tx_str,D_C_1_char);
strcat(tx_str," ");
strcat(tx_str,fre_2_char);
strcat(tx_str," ");
strcat(tx_str,D_C_2_char);
strcat(tx_str," ");

for (uint8_t i=0;i<20;i++)
{
while(DL_UART_isBusy(UART_0_INST));
DL_UART_Main_transmitData(UART_0_INST,tx_str[i]);
}
strcpy(tx_str,""); //重新将字符串置空
//按键值置0
key_num=0;


7.矩阵键盘的中断函数。(这部分是例程里给的,所以我放在最后展示并特此说明)

void GROUP1_IRQHandler(void)
{
int col_pin = 0;

int col = -1;
int row = -1;
switch (DL_GPIO_getPendingInterrupt(MAT_KEY_PORT)) // 获取col
{
case MAT_KEY_COL0_IIDX:
col = 0;
col_pin = MAT_KEY_COL0_PIN;
break;
case MAT_KEY_COL1_IIDX:
col = 1;
col_pin = MAT_KEY_COL1_PIN;
break;
case MAT_KEY_COL2_IIDX:
col = 2;
col_pin = MAT_KEY_COL2_PIN;
break;
case MAT_KEY_COL3_IIDX:
col = 3;
col_pin = MAT_KEY_COL3_PIN;
break;
default:
break;
}
if (col != -1)
{
delay_ms(10); // 按键消抖
if (!(DL_GPIO_readPins(MAT_KEY_PORT, col_pin))) // 按下对应列
{
DL_GPIO_setPins(MAT_KEY_PORT, MAT_KEY_ROW2_PIN | MAT_KEY_ROW3_PIN); // 0000->0011
delay_ms(10);
if (!(DL_GPIO_readPins(MAT_KEY_PORT, col_pin))) // row为0或1
{
DL_GPIO_setPins(MAT_KEY_PORT, MAT_KEY_ROW1_PIN); // 0011->0111
delay_ms(10);
if (!(DL_GPIO_readPins(MAT_KEY_PORT, col_pin))) // row为0
{
row = 0;
}
else // row为1
{
row = 1;
}
}
else // row为2或3
{
DL_GPIO_clearPins(MAT_KEY_PORT, MAT_KEY_ROW2_PIN); // 0011->1101
DL_GPIO_setPins(MAT_KEY_PORT, MAT_KEY_ROW0_PIN | MAT_KEY_ROW1_PIN);
delay_ms(10);
if (!(DL_GPIO_readPins(MAT_KEY_PORT, col_pin))) // row为2
{
row = 2;
}
else // row为3
{
row = 3;
}
}

DL_GPIO_clearPins(MAT_KEY_PORT, MAT_KEY_ROW0_PIN | MAT_KEY_ROW1_PIN | MAT_KEY_ROW2_PIN | MAT_KEY_ROW3_PIN); // 0000
delay_ms(10); // 防止重置行输出后再次进入中断
key_num = col * 4 + row + 1;
DL_GPIO_clearInterruptStatus(MAT_KEY_PORT, MAT_KEY_COL0_PIN | MAT_KEY_COL1_PIN | MAT_KEY_COL2_PIN | MAT_KEY_COL3_PIN); // 清除按键抖动或者二分扫描时产生的中断请求
}
}
}


8.整型(int)转字符串(char[])的函数。这个函数在将整型数据上传串口时会用到。

void int2char(int num,char str[])
{
int i = 0;

// 处理0的特殊情况
if (num == 0) {
str[i++] = '0';
str[i] = '\0';
return;
}

// 逆序构建字符串
while (num > 0) {
int rem = num % 10;
str[i++] = rem + '0'; // 将数字转换为字符
num = num / 10;
}

// 反转字符串
str[i] = '\0'; // 添加字符串结束符
int start = 0;
int j = i - 1;
while (start < j) {
char temp = str[start];
str[start] = str[j];
str[j] = temp;
start++;
j--;
}
}


9.OLED的展示的函数。

void ShowBasicInfo(int show_num1,int select_x,int select_y)		//OLED显示基础信息
{
OLED_Clear();
OLED_WR_Byte(0xA1, OLED_CMD); //--Set SEG/Column Mapping 0xa0左右反置 0xa1正常
OLED_WR_Byte(0xC8, OLED_CMD);

OLED_ShowNum(0,0,1,1,14);
OLED_ShowString(0,2,"PA26");
OLED_ShowNum(0,4,frequence_1,7,14);
OLED_ShowNum(0,6,duty_cycle_1,3,14);
OLED_ShowChar(36,6,'%',16);

OLED_ShowNum(60,0,2,1,14);
OLED_ShowString(60,2,"PA16");
OLED_ShowNum(60,4,frequence_2,7,14);
OLED_ShowNum(60,6,duty_cycle_2,3,14);
OLED_ShowChar(96,6,'%',16);
//
// //额外展示的选中修改信息
OLED_ShowChar(select_x,select_y,'*',8); //显示当前选中通道
OLED_ShowNum(75,0,show_num1,7,14);
}


九、遇到的主要难题及解决办法

主要难题是:使用多个定时器时会出现中断资源冲突问题。

最开始我使用两个定时器通过中断修改GPIO电平来输出PWM波,然后又设置了一个定时器来控制OLED每产生1000次中断(1s)刷新一次,又设置了一个定时器来控制串口每产生1000次中断(1s)发送一次信息到上位机。


然而我很快发现,有的时候OLED突然就不显示,又或者有的时候写在主程序while中进行判断的按键突然就没有反应了,有的时候串口不发送信息了,或者PWM波输出异常了。通过多次实验测试我才发现,中断资源是会互相抢占影响的,这大大影响了各个模块的正常使用,这也诱发我思考将这些模块减少中断实现。


于是我尝试将OLED显示和串口上传放到主程序中,每隔0.5s(因为按键判断也占一定的时间)执行一次,然而,按键的执行仍然不灵敏,而且PWM波的输出仍然会受到OLED显示和串口上传的影响。我突然意识到这些资源都是有限的,该怎么才能使用最少的资源实现最好的效果呢?


经过我的多次实验(实际上是好几天的折磨),才尽量将每个模块使用的资源降到我尽力能降到的最低。

1. 优化PWM波的输出方式。改变之前每隔一个占空比调节精度产生一个中断的方式,先是优化成每次改变信号输出电平产生一个中断,到再优化成直接使用TIMER-PWM来输出PWM波(因为之前一直以为无法修改其频率遂在前期抛弃使用)。

2. 优化OLED显示时机。因为OLED显示内容的变化总是发生在按键控制之后,于是我设定每次按键按过后,执行过相关操作后再让OLED显示,并易于其1s的缓冲显示时间,同时降低键盘误触风险。缺点是按键盘可能每次需要至少隔1s时间。

3. 优化串口上传时机。因为尝试过每隔1s上传串口信息会影响我按键的判断,所以我讲串口上传的时间也修改在了按键按过之后。于是,每次串口上传时间差也大于1s。但其实我觉得这影响不大,因为我的每次频率和占空比的修改都是在按键按过之后才会发生,因此不会有漏显示信息的风险,反而,信息的输出具有冗余。


感觉最大的问题就是设计和优化问题,其他倒还是小问题。


十、未来的计划或建议

未来我可能不会用这块板子,因为它的性能要求实在太苛刻了,需要大量时间的调优和重新设计。


我的建议是,如果是想学习一下怎么一次次优化(zhe mo zi ji)实现方式,可以尝试一下这块板子。不然,我不建议新手碰这块板子,它绝对不是一块适合单片机入门学习的板子,用stm32,或者用小脚丫这类fpga这样的会好很多。


而且,它的资料和例程也比较少,如果你要说SDK例程挺多,但是我觉得看SDK的例程是大佬(我不是大佬,我看不懂就是了)会干的事,绝对不适合新手。这是我作为一个菜鸟痛苦挣扎这么多天的经验和建议。


然后,可以加一下这个板子的官方交流群,群友很友好,提问题也会帮忙,特此感谢友好的大家。

附件下载
简易PWM发生器工程.zip
类似于例程形式的整个工程文件,打成了压缩包
团队介绍
钱鹏宇
团队成员
Travels
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号