1 项目需求
使用矩阵键盘、蜂鸣器、流水灯和电位计设计音乐键盘,让不同按键发出不同音调,电位计用来控制声音大小,流水灯用来显示声音档位。
2 完成的功能及达到的性能
2.1.蜂鸣器的音调与音量的控制
图2-1
如图所示为蜂鸣器模块的电路图。可以控制PA15的PWM波驱动三极管,从而使蜂鸣器工作。但是,在实际工作中,我发现这个PA15的PWM设计非常不好。可以见图2-2。
图2-2
在单片机中,只有TIMG4有PA15端口的设置。遗憾的是,使用TIMG4就意味着要同时使用Pin0端口,而恰好TIMG4中的几个引脚都有冲突,其中的PA10/PA14、PA17与矩阵键盘冲突,其中的PA20/PA24与DEBUG冲突。考虑再三后,我发现使用PA16更加适合,因为TIMG0中的引脚Pin24为ADC引脚,正好在本项目中不需要过多的ADC,允许占用。再者,PA16与蜂鸣器距离很近,就在同一个排针的上面,所以我最后使用了PA16作为蜂鸣器PWM的输出引脚。
如何控制PWM波实现不同音调与音量的声音呢。其中音调是比较好控制的,因为声音的频率实际上就是PWM波的频率。考虑到一些声音的频率过高不适用于蜂鸣器,毕竟他质量也就这样。我选择了C大调的前16个音阶。其可以见图2-3所示。
图2-3
对应在程序中就是我的图2-4所示的数组,我可以很方便的调用。
图2-4
那么如何改变PWM波的频率呢,那就需要控制PWM Period Count来实现了,具体如何实现的程序将在代码中讲解。但是一个Calculated Clock Frequency对应的最低频率是有限的,这就需要通过计算来确定预分频的大小,最后我选择了BUSCLK并对他进行了四分频。这样就能保证最低262HZ的频率。
至于声音的音量如何控制,可以通过控制PWM的占空比来控制声音的大小,但是必须知道的是,蜂鸣器是通过震动而不是电压的大小产生的声音,所以从本质上来说20%的占空比和80%占空比所发出的声音的音量是一样的。
在实际测试中,当占空比来到10%左右时,声音音量的变化程度就很小了。所以实际中我采用的PWM占空比是从0%到10%。
2.2矩阵键盘
图2-5是开发套件中的矩阵键盘原理图,他可以分成行控制引脚和列控制引脚。值得注意的是,开发套件中矩阵键盘的引脚在其他地方也使用到了,这存在着不同器件之间的干扰。比如说拨码开关的原理图2-6中,PA0,PA1,PA7,PA12是共用的,这四个引脚如果作为输出使用,必须要把拨码开关往下拨。
图2-5
图2-6
矩阵键盘有三种控制方式,第一种是将行引脚作为信号的输入,列引脚作为信号的输出;第二种是将列引脚作为信号的输出,行引脚作为信号的输入;还有第三种是行列都可以输入或者输出。本项目参考了开源资料,使用了行引脚输入列引脚输出,也就是行引脚输出信号,列引脚接受信号的控制方式,那么初始化的工作方式可以见图2-7,图2-8。分别为输出和输入工作方式。
图2-7 图2-8
2.3LED流水灯
图2-9是LED的原理图,由于我额外添加了OLED屏幕,所以LED的后四个引脚均被占用无法使用,所以我是用的是5档调节LED,即使用四个LED作为声音音量大小的显示。LED的初始化配置与矩阵键盘类似。
图2-9
2.4ADC采集
图2-10是电位计的原理图,其通过1k电阻进行分压,所以其理论最大测得电压为3V,通过ADC12模块得到的ADC值与实际电压值存在以下关系:电压=ADC采样值3.3/4095,而如果需要知道电位计的比值,就可以用到如下关系:比例=ADC采样值*1.1/4095。
图2-10
ADC的配置可以见图2-11所示,比较值得注意的是ADC转换触发源,可以有软件连续触发,软件触发,事件触发三种。这里我为了方便使用采用了软件连续触发的方式。
图2-11
其次是ADC的中断,ADC的中断也有很多设置的方式,可以有overflow,dma,memeory触发中断,这里我选择了memeory触发中断,就是读取到数据存到记忆寄存器后触发,这样可以在中断中直接读取数据。图2-12。
图2-12
3 软件程序
整个软件程序的流程如图3-1所示:
图3-1
3.1初始化程序
初始化程序包含了系统各个模块的初始化以及数据清零的功能。
SYSCFG_DL_init(); // Initialize the device
DL_TimerG_startCounter(PWM_0_INST);
OLED_Init();
OLED_Clear();
OLED_ShowString(0, 0, "Music Keyboard!");
OLED_ShowString(0, 1, "Note:");
OLED_ShowString(0, 3, "Volume:");
OLED_ShowString(15, 4, "%");
NVIC_EnableIRQ(ADC12_0_INST_INT_IRQN);
NVIC_EnableIRQ(TIMER_0_INST_INT_IRQN);
DL_ADC12_startConversion(ADC12_0_INST);
DL_TimerG_startCounter(TIMER_0_INST);
gCheckADC = true;
DL_TimerG_setCaptureCompareValue(PWM_0_INST, 0, GPIO_PWM_0_C0_IDX);
flag_playcommd = 0;
3.2矩阵键盘
采用的是先对行引脚赋值,得到输出端的数据。将四个行引脚都按照这种方式赋值最后得到数据相加就是矩阵键盘的编号。
int getKeyValue(void)
{
int h_arr[4] = {KEY_H1_PIN, KEY_H2_PIN, KEY_H3_PIN, KEY_H4_PIN};
int v_arr[4] = {KEY_V1_PIN, KEY_V2_PIN, KEY_V3_PIN, KEY_V4_PIN};
int i, j = 0;
int key_value = 0;
for (i = 0; i < 4; i++)
{
delay_cycles(100);
DL_GPIO_setPins(KEY_PORT, h_arr[i]);
DL_GPIO_clearPins(KEY_PORT, h_arr[(i + 1) % 4]);
DL_GPIO_clearPins(KEY_PORT, h_arr[(i + 2) % 4]);
DL_GPIO_clearPins(KEY_PORT, h_arr[(i + 3) % 4]);
delay_cycles(20000);
for (j = 0; j < 4; j++)
{
if (DL_GPIO_readPins(KEY_PORT, v_arr[j]) != 0)
{
key_value = j * 4 + i + 1;
}
delay_cycles(100);
}
delay_cycles(100);
}
DL_GPIO_clearPins(KEY_PORT,h_arr[0]||h_arr[1]||h_arr[2]||h_arr[3]);
return key_value; // 没有按下,返回0
}
3.3PWM控制
由于蜂鸣器的声音必须是一段时间的,所以PWM需要控制它的结束和开始,还有就是PWM的占空比和频率,用来控制音调和音量。
if(key_value != 0)
{
OLED_ShowNum(0, 2, (unsigned int)key_value, 2, 12);
if(flag_playcommd == 0)
flag_playcommd = 1;
}
if(flag_playcommd == 0)DL_TimerG_setCaptureCompareValue(PWM_0_INST, 0, GPIO_PWM_0_C0_IDX);
if(flag_playcommd == 1)
{
DL_TimerG_setLoadValue(PWM_0_INST,PWM_0_INST_CLK_FREQ/freq);
DL_TimerG_setCaptureCompareValue(PWM_0_INST, percent*PWM_0_INST_CLK_FREQ/freq/10, GPIO_PWM_0_C0_IDX);
flag_playcommd =2;
}
if(flag_playcommd == 3)
{
DL_TimerG_setCaptureCompareValue(PWM_0_INST, 0, GPIO_PWM_0_C0_IDX);
flag_playcommd = 0;
PWM_playnum = 0;
}
可以有如上步骤控制PWM:1.发出PWM请求,2.接受请求并锁存,3.启动PWM,4.定时器中延时,5.收到延时结束命令并解锁。
其次就是PWM的占空比的设置,它所需要的寄存器的值会随着PERIOD COUNT的变化而变化,所以我干脆直接用站控比来控制,在程序中直接复制粘贴下面两行代码就可以控制频率和占空比。
DL_TimerG_setLoadValue(PWM_0_INST,PWM_0_INST_CLK_FREQ/freq);
DL_TimerG_setCaptureCompareValue(PWM_0_INST, percent*PWM_0_INST_CLK_FREQ/freq/10, GPIO_PWM_0_C0_IDX);
3.4ADC控制
ADC由于采用的软件连续读取和MEM0寄存器中断,所以直接在ADC中断读取数据并计算就行,很方便。
void ADC12_0_INST_IRQHandler(void)
{
switch (DL_ADC12_getPendingInterrupt(ADC12_0_INST)) {
case DL_ADC12_IIDX_MEM0_RESULT_LOADED:
gAdcResult = DL_ADC12_getMemResult(ADC12_0_INST, DL_ADC12_MEM_IDX_0);
percent=(float)gAdcResult*1.1/(float)4095;
if(percent>0.8)led_maxid=4;
else if(percent>0.6)led_maxid=3;
else if(percent>0.4)led_maxid=2;
else if(percent>0.2)led_maxid=1;
else led_maxid=0;
break;
default:
break;
}
}
3.5定时中断控制
定时中断主要是流水灯的控制以及PWM的延时。PWM不能直接在主程序中调用delay_ms函数延时,因为这样程序一定会卡死跑飞。采用定时中断也相当于延时,但是效果和影响是最好的。其次就是为什么流水灯放在定时中断中,这是因为我把他放在主程序中直接卡死了。。。后来我查看程序发现矩阵键盘那一块的程序延时比较多。这个问题挺玄学的,但总之放在中断能用。当时如果要严格一点,还是最好使用freerots的实时操作系统更好一点。因为这样各个模块任务在宏观上就是同步的,也就不会存在相互影响的问题。
void TIMER_0_INST_IRQHandler(void)
{
switch (DL_TimerG_getPendingInterrupt(TIMER_0_INST)) {
case DL_TIMER_IIDX_ZERO:
if(flag_playcommd ==2)
{
PWM_playnum ++;
if(PWM_playnum>300)
{
flag_playcommd = 3;
}
}
if(led_maxid==0)
{
DL_GPIO_setPins(LED_PORT, LED_LED_0_PIN);
DL_GPIO_setPins(LED_PORT, LED_LED_1_PIN);
DL_GPIO_setPins(LED_PORT, LED_LED_2_PIN);
DL_GPIO_setPins(LED_PORT, LED_LED_3_PIN);
}
if(led_maxid==1)
{
DL_GPIO_clearPins(LED_PORT, LED_LED_0_PIN);
DL_GPIO_setPins(LED_PORT, LED_LED_1_PIN);
DL_GPIO_setPins(LED_PORT, LED_LED_2_PIN);
DL_GPIO_setPins(LED_PORT, LED_LED_3_PIN);
}
if(led_maxid==2)
{
DL_GPIO_clearPins(LED_PORT, LED_LED_0_PIN);
DL_GPIO_clearPins(LED_PORT, LED_LED_1_PIN);
DL_GPIO_setPins(LED_PORT, LED_LED_2_PIN);
DL_GPIO_setPins(LED_PORT, LED_LED_3_PIN);
}
if(led_maxid==3)
{
DL_GPIO_clearPins(LED_PORT, LED_LED_0_PIN);
DL_GPIO_clearPins(LED_PORT, LED_LED_1_PIN);
DL_GPIO_clearPins(LED_PORT, LED_LED_2_PIN);
DL_GPIO_setPins(LED_PORT, LED_LED_3_PIN);
}
if(led_maxid==4)
{
DL_GPIO_clearPins(LED_PORT, LED_LED_0_PIN);
DL_GPIO_clearPins(LED_PORT, LED_LED_1_PIN);
DL_GPIO_clearPins(LED_PORT, LED_LED_2_PIN);
DL_GPIO_clearPins(LED_PORT, LED_LED_3_PIN);
}
break;
default:
break;
}
}
4 实物展示
首先是整体的布局
OLED屏幕显示了声音的音调和音量信息:
不同的摁键对应不同的音调
电位计的变化会导致流水灯的变化,流水灯熄灭的阈值依次为20%,40%,60%,80%:
由于蜂鸣器选择了A16作为输出,所以需要通过杜邦线将两者连接:
5 未来的计划建议
MSPM0L1306作为一款性能优良的单片机,还是有很多的地方我没有发挥出来,我希望能在以后的项目中使用这款单片机,继续探索他的更多有趣的地方。其次就是要学习freertos等实时操作系统,并将使用实时操作系统写程序作为一种习惯。