1 项目介绍
使用矩阵键盘、蜂鸣器、流水灯和电位计设计音乐键盘,让不同按键发出不同音调,电位计用来控制声音大小,流水灯用来显示声音档位,OLED显示屏显示当前键值即发声模式。
2 设计思路
- GPIO实现矩阵键盘扫描读取键码值;
- S1~S14为14个琴键,按下S15切换为独立发声模式,按下S16切换为连续发生模式;
- 定时器输出PWM从而驱动无源蜂鸣器;
- 通过设置PWM的频率以及占空比来实现更改音乐键盘的不同琴音以及响度;
- ADC对模拟输入(电位器)进行采样,采样由定时器触发;
- OLED显示输出当前发声模式以及按下的键值;
3 软件流程图
4 实现的功能及图片展示
4.1开机上电(独立发声模式)
此时发声模式默认为独立发声(可按下S15切换为此),右下角从左数四枚LED灯均亮起,表示此时为音量满格状态.
按下S1~S14后,获取对应键值,为PWM载入对应频率,打开定时器计数,无源蜂鸣器持续响500ms后关闭计数。
4.2 连续发声模式
按下S16,模式切换为连续发声模式,OLED显示随之变化,定时器计数打开,蜂鸣器开始工作。此时按下琴键后将持续鸣响对应频率。
4.3 音量调节
通过旋转左侧电位计,可控制响度大小,共设有五个档位,通过右下角左侧四枚LED灯显示(0枚亮声音最小,四枚全亮声音最大)。
5 主要代码片段及说明
5.1 调节PWM频率(音调)及占空比(响度)
//设置PWM占空比,即响度
void Set_Pwm(float duty,uint8_t channel)
{
uint32_t Compare;
Compare = period - period * duty;//公式
if(channel == 1)
{
DL_TimerG_setCaptureCompareValue(PWM_0_INST,Compare,GPIO_PWM_0_C1_IDX);
}
}
//改变PWM频率,即音调
void Set_Freq(uint32_t freqency)
{
period = PWM_0_INST_CLK_FREQ / freqency;//周期计算公式
DL_TimerG_setLoadValue(PWM_0_INST,period);//改变系统周期
}
5.2 发声模式选择
void FLAG()
{
if(Mode_Flag == 0)
{
OLED_ShowString(0, 0, "MODE:INDIVIDUAL");
if(Count_Flag == 0)
{
DL_TimerG_stopCounter(PWM_0_INST);
}
else{
DL_TimerG_startCounter(PWM_0_INST);
}
}
else
{
OLED_ShowString(0, 0, "MODE:CONTINUITY");
DL_TimerG_startCounter(PWM_0_INST);
}
}
当Mode_Flag == 0 时,为独立发声模式,OLED显示屏输出"MODE:INDIVIDUAL";当检测到按键按下后,将Count_Flag 置一,从而打开定时器计数,并且无源蜂鸣器开始工作,在500ms后,停止定时器计数,蜂鸣器停止工作,即响声持续500ms后停止。
当Mode_Flag == 1 时,为连续发声模式,OLED显示屏输出"MODE:CONTINUITY";此时尽管Count_Flag 为0但仍然开启了定时器,所以蜂鸣器会持续工作直至不再是连续发声模式。
5.3 ADC采集
void getADC_value()
{
//ADC转换结束,则读取数据
if(adc_flag)
{
//读取ADC0的MEM0的数据
adc_value = DL_ADC12_getMemResult(ADC12_0_INST,DL_ADC12_MEM_IDX_0);
//对ADC的转换结果进行处理(点亮对应档位LED,设置DUTY值)
if (adc_value >= 0x000 && adc_value <= 0x1FF)
{
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);
DL_GPIO_setPins(LED_PORT, LED_LED_4_PIN);
DUTY = 0.01;
}
if (adc_value >= 0x1FF && adc_value <= 0x2FF)
{
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);
DL_GPIO_setPins(LED_PORT, LED_LED_4_PIN);
DUTY = 0.02;
}
if (adc_value > 0x2FF && adc_value <= 0x3ff)
{
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);
DL_GPIO_setPins(LED_PORT, LED_LED_4_PIN);
DUTY = 0.03;
}
if (adc_value > 0x3ff && adc_value <= 0x4FF)
{
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);
DL_GPIO_setPins(LED_PORT, LED_LED_4_PIN);
DUTY = 0.04;
}
if (adc_value > 0x4ff && adc_value <= 0xFFF)
{
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);
DL_GPIO_clearPins(LED_PORT, LED_LED_4_PIN);
DUTY = 0.05;
}
adc_flag = false;
//开启ADC转换,需要重复开启
DL_ADC12_startConversion(ADC12_0_INST);
//使能ADC,否则ADC只能转换一次
DL_ADC12_enableConversions(ADC12_0_INST);
}
}
值得提醒的点是务必记得手动重复开启,否则无法自动连续采集。
5.4 定时器的中断服务函数
void TIMER_0_INST_IRQHandler(void)
{
static unsigned long int Counter;
switch( DL_TimerG_getPendingInterrupt(TIMER_0_INST) )
{
case DL_TIMER_IIDX_ZERO:
if(Count_Flag == 1)
{
Counter++;
}
if(Counter >= 5)
{
Counter = 0;
Count_Flag = 0;
}
break;
default:
break;
}
}
void ADC12_0_INST_IRQHandler(void)
{
switch(DL_ADC12_getPendingInterrupt(ADC12_0_INST))
{
//MEM0装载数据
case DL_ADC12_IIDX_MEM0_RESULT_LOADED:
adc_flag = true;
break;
default:
break;
}
}
这里涉及到了两部分,第一部分为独立发声方面工作,控制确保无源蜂鸣器工作500ms后关闭;
第二部分为ADC采集方面工作,确保其能稳定运行。
5.5 主函数
int main(void)
{
SYSCFG_DL_init(); // Initialize the device
OLED_Init();
OLED_Clear();
//清除定时器中断标志
NVIC_ClearPendingIRQ(TIMER_0_INST_INT_IRQN);
//使能定时器中断
NVIC_EnableIRQ(TIMER_0_INST_INT_IRQN);
//使能ADC中断
NVIC_EnableIRQ(ADC12_0_INST_INT_IRQN);
//开启ADC转换,需要重复开启
DL_ADC12_startConversion(ADC12_0_INST);
while(1)
{
key_value = getKeyValue();
FLAG();
getADC_value();
Set_Pwm(DUTY,1);
if(key_value != 0)
{
OLED_ShowNum(2, 3, (unsigned int)key_value, 2, 16);
switch(key_value)
{
case 1: Set_Freq(M1); break;
case 2: Set_Freq(M2); break;
case 3: Set_Freq(M3); break;
case 4: Set_Freq(M4); break;
case 5: Set_Freq(M5); break;
case 6: Set_Freq(M6); break;
case 7: Set_Freq(M7); break;
case 8: Set_Freq(H1); break;
case 9: Set_Freq(H2); break;
case 10: Set_Freq(H3); break;
case 11: Set_Freq(H4); break;
case 12: Set_Freq(H5); break;
case 13: Set_Freq(H6); break;
case 14: Set_Freq(H7); break;
case 15: Mode_Flag = 0; break;
case 16: Mode_Flag = 1; break;
}
Count_Flag = 1;
}
}
}
较为基础,初设置各部件初始化外,主要负责实现了按键功能。
6 遇到的主要难题及解决方法
- 面对新上手的开发板以及底板众多引脚和外设以及全新的Sysconfig一度使得困难陡增,但沉心研究项目例程后才悟出通点。
- 采用中断实现矩阵键盘时会导致蜂鸣器的工作不稳定,PWM波形失调,最后重新编写矩阵键盘模块才得以正常。
- 调节占空比但无法使得响度大小发生差异,这是因为响度大小对于脉冲宽度是呈指数变化的,只有当占空比在10%以内时调节,人耳才能有较为明显的感知。
- ADC采样后的数据处理起初间距设置的等分,导致想调节档位得拧很多下电位计,这与开发目的和使用习惯相违背了。
7 未来的计划或建议
该项目已经成功实现了音乐键盘以及电位计调节音量的功能,并达到了预期指标。然而通过更换硬件,还有许多可以提升与扩展的地方:
- 板上的OLED屏幕分辨率较低,无法显示信号细节与更多信息。可以使用分辨率更高的屏幕,或将波形信息直接发送给上位机,由上位机进行显示。
- 更换性能更强大的主控芯片,通过引脚复用复刻出真正全键盘的电子琴。
- 对频率以及占空比的控制还可以更加精细,如使用旋转编码器去调节音量,在手感上仍有提升余地。