旧乡故客
发表于: 2018-7-16 22:17:27 | 显示全部楼层

本篇文章属于“如何制作基于EFM8的声音合成器”系列的第3部分。该系列包含以下内容:

●     第1部分:从方波到正弦波

●     第2部分:驱动扬声器

●     第3部分:通过USB播放旋律


所需的硬件/软件

●     SLSTK2000A EFM8评估板

●     Simplicity Studio集成开发环境

●     Scilab


项目概况

经过上一篇文章的介绍后,我们有了固件以及音频电路,它们可以一起产生能够驱动扬声器的经过滤波放大的音符频率正弦波。我们甚至发现这个相对简单的系统产生的音调令人惊讶地悦耳可及。但是,如果我们的声音合成器能做的只是无限循环的播放C大调音阶,它将会逐渐失去吸引力,那么我们需要一种方便的方式来打开和关闭音频,更重要的是,能够播放舒缓的旋律。


我们将通过在EFM8和Scilab之间建立虚拟COM端口(VCP)USB连接来实现此目的。此项目中使用的Scilab脚本提供了一个简单的命令行界面,用户可以通过该界面启动和停止音频播放,设置拍速,以及输入一个简单旋律的音符信息。


SCILAB

以下是使用的脚本:

USB_synth_Scilabcode_2.jpg


首先,Scilab配置并打开VCP。如果由于某种原因串行端口库无法与EFM8建立连接,Scilab控制台将声明COM端口无法打开然后“中止”,这是Scilab用于“停止脚本执行并返回正常操作”的用语“如果连接成功建立,脚本将进入无限的命令行输入循环,其中Scilab控制台窗口会提示用户输入要发送到EFM8的字符串。要发送命令,只需键入相应的字符,然后按Enter键。要打破循环并退出脚本,请按Enter键而不键入任何其他字符(请注意,在脚本终止之前,VCP连接已关闭且串行库已“卸载”)。


EFM8固件识别四种类型的命令:播放音频(“P”),停止音频(“S”),设置拍速(“T ###”),以及创建旋律(下面讨论的命令格式)。播放和停止音频命令打开或关闭声音信号和滤波器时钟方波,而不会干扰固件的正常操作。 set tempo命令用于控制播放速度;速度以每分钟节拍数(BPM)输入。固件当前将速度限制在60到180 BPM之间,因此EFM8将高于或低于此范围的数字分别更改为180或60。


创建旋律的方法可能看起来有点神秘,但它很容易实现,如果你在输入字符时看一段音乐,它实际上可以非常快速和直观。旋律作为一系列字符对输入,包括字母(表示音符)和数字(表示音符播放的时间长度)。固件目前支持整个音符,半音符,四分音符和八分音符。

musical_notes.jpg


输入旋律时,整个音符使用“1”,半音符使用“2”,四分音符使用“4”,第八个音符使用“8”。对于笔记本身,您使用相应的字母。命令行界面支持两个八度音阶,以C开头,以B结尾(没有锐利或平面)。下八度音符中的音符用小写字母表示,高八度音符中的音符用大写字母表示。因此,如果您想播放界面支持的所有音符,按升序排列并且每个音符都是四分音符,您可以输入“c4d4e4f4g4a4b4C4D4E4F4G4A4B4”。注意:当前固件支持的音符仅在第二个八度音程中高达E;演示旋律不需要更高的音符,这些音符将通过RC低通滤波器逐渐衰减,如前一篇文章所述。此外,在某些时候,滤波器时钟信号将变得不可靠,因为PCA中断将频繁发生,以至于EFM8的处理器将无法一致地更新捕获/比较寄存器。您当然可以扩展固件以包含这些更高的音符,但您需要密切关注滤波器时钟方波,并且您可能需要修改RC低通滤波器以获得更高的截止频率。


有关命令行界面的其他详细信息:

●    对于速度命令,速度值的ASCII表示将转换为单字节数,以便EFM8不需要执行此转换。其他命令直接传递给EFM8。

●    发送旋律命令时会自动启用播放。

●    所有命令都由EFM8回显,如命令行示例中所示。

●    您可以使用“r”后跟“1”,“2”,“4”或“8”指定休息(即没有播放音符的时段);该数字指定剩余的持续时间为整个,一半,四分之一或八分音符的持续时间。

●    您可以随时设置速度,但在您输入新的旋律(或重新输入当前的旋律)之前,播放速度不会改变。默认速度为120 BPM。

●    在命令提示符下按向上箭头键可以循环显示先前输入的字符串;当您发送长旋律命令并且只想更改一个音符或者您需要重新输入上一个旋律时,这非常有用。

●    为简单起见,所有命令目前仅限于一个64字节的USB数据包。每个音符需要两个字节,因此最大旋律长度为32个音符。


下面是一个显示正确使用命令行界面的示例。脚本被执行,并且EFM8被编程为以所有四分音符播放升序C大调音阶,然后是具有所有八分音符的降序C大调音阶。接下来,速度降低到60 BPM,然后增加到180 BPM。最后,在脚本终止之前禁用播放然后再次启用。您会注意到速度命令的回显字符看起来不正确,但实际上它们是 - Scilab显示与EFM8接收的单字节速度编号对应的ASCII字符。

command-line-example.jpg


USB配置

在这里,我们将简要回顾将VCP通信合并到我们的固件中所涉及的所有步骤:


1.   将“VCPXpress.h”,“VCPXpress.lib”,“descriptor.c”和“descriptor.h”复制到项目目录中:

USB_step_1.jpg

2.   在项目属性中添加适当的包含路径:

USB_step_2.jpg


3.   在项目属性中添加VCPXpress库:

USB_step_3.jpg


4.   插入包含用于与VCPXpress库交互并处理USB数据包的代码的源文件:

USB_step_4.jpg


5.    为新函数原型,全局变量和预处理器定义插入代码(构建错误将帮助您找到可能遗漏的任何内容),并在任何需要它们的源文件中包含“VCPXpress.h”和“descriptor.h” :

USB_step_5.jpg

6.    在调用硬件初始化函数后,添加对USB_Init()和API_Callback_Enable()的调用:

USB_step_6.jpg

7.    要禁止链接器警告,请将“OVERLAY(?PR?_USB_WRITEFIFO?EFM8_USBDEP!*,?PR?_USBD_READ?EFM8_USBD!*,?PR?_USBD_WRITE?EFM8_USBD!*,?PR?_VCPXCORE_WRITE?VCPXPRESS!*)”复制到“链接器设置中的“附加标志”:

USB_step_7.jpg


中断优先级

EFM8外设/中断配置的唯一变化是一个微妙但重要的变化:PCA中断已设置为高优先级。

PCA_int_priority.jpg


最初,滤波器时钟方波输出严重不可靠,可能是因为与VCPXpress库相关的高优先级中断正在抢占PCA中断。对优先级设置的这种修改可确保及时处理PCA中断。


函数细节

固件通过将命令字符串转换为一系列音符和相应的Timer3计数序列来生成旋律,每个序列存储在一个单独的数组中:

  1. void StoreNewMelody()
  2. {
  3.         unsigned char x,y;

  4.         y = 0;

  5.         for(x = 0; x < USBBytesReceived; x += 2)
  6.         {
  7.                 NotesSequence[y] = USBRxPacket[x];

  8.                 switch(USBRxPacket[x+1])
  9.                 {
  10.                         case '1':
  11.                                 Timer3Counts_Sequence[y] = WholeNoteCounts;
  12.                                 break;
  13.                         case '2':
  14.                                 Timer3Counts_Sequence[y] = HalfNoteCounts;
  15.                                 break;
  16.                         case '4':
  17.                                 Timer3Counts_Sequence[y] = QuarterNoteCounts;
  18.                                 break;
  19.                         case '8':
  20.                                 Timer3Counts_Sequence[y] = EighthNoteCounts;
  21.                                 break;
  22.                 }

  23.                 y++;
  24.         }

  25.         NumberofNotes = y;

  26.         CurrentNoteIndex = NumberofNotes - 1;        //start playback at the beginning of the melody

  27.         PLAYorSTOP = PLAY;
  28. }
复制代码

当收到设定的速度命令时,计算用于整个、一半、四分之一或八分音符的Timer3计数数:

  1. void SetTempo(unsigned char TempoBPM)
  2. {
  3.         if(TempoBPM > 180)
  4.                 TempoBPM = 180;

  5.         else if(TempoBPM < 60)
  6.                 TempoBPM = 60;

  7.         QuarterNoteCounts = ((float)60/TempoBPM) * 10000;

  8.         WholeNoteCounts = QuarterNoteCounts*4;
  9.         HalfNoteCounts = QuarterNoteCounts*2;
  10.         EighthNoteCounts = QuarterNoteCounts/2;
  11. }
复制代码

这些计算假设四分音符的持续时间是一拍,因此整个音符是4拍,半音2拍,第八音半拍。 因此,在速度为60 BPM的情况下,音符将具有以下持续时间。

musical_notes_duration.jpg


在主while循环中,程序循环音符数组中的每个元素并相应地设置声音信号和滤波器时钟增量,然后它使用Timer3计数数组中的相应值来实现适当的延迟。

  1. while(1)
  2. {
  3.         for(CurrentNoteIndex = 0; CurrentNoteIndex < NumberofNotes; CurrentNoteIndex++)
  4.         {
  5.                 REPEATED_NOTE = FALSE;
  6.                 if(CurrentNoteIndex < (NumberofNotes - 1))
  7.                 {
  8.                         if(NotesSequence[CurrentNoteIndex] == NotesSequence[CurrentNoteIndex + 1])
  9.                                 REPEATED_NOTE = TRUE;
  10.                 }

  11.                 switch(NotesSequence[CurrentNoteIndex])
  12.                 {
  13.                         case 'c':
  14.                                 Current_SoundSignal_Increment = SOUND_C5_INCREMENT;
  15.                                 Current_FilterClock_Increment = FILTCLK_C5_INCREMENT;
  16.                                 break;

  17.                         case 'd':
  18.                                 Current_SoundSignal_Increment = SOUND_D5_INCREMENT;
  19.                                 Current_FilterClock_Increment = FILTCLK_D5_INCREMENT;
  20.                                 break;

  21.                         case 'e':
  22.                                 Current_SoundSignal_Increment = SOUND_E5_INCREMENT;
  23.                                 Current_FilterClock_Increment = FILTCLK_E5_INCREMENT;
  24.                                 break;

  25.                         case 'f':
  26.                                 Current_SoundSignal_Increment = SOUND_F5_INCREMENT;
  27.                                 Current_FilterClock_Increment = FILTCLK_F5_INCREMENT;
  28.                                 break;

  29.                         case 'g':
  30.                                 Current_SoundSignal_Increment = SOUND_G5_INCREMENT;
  31.                                 Current_FilterClock_Increment = FILTCLK_G5_INCREMENT;
  32.                                 break;

  33.                         case 'a':
  34.                                 Current_SoundSignal_Increment = SOUND_A5_INCREMENT;
  35.                                 Current_FilterClock_Increment = FILTCLK_A5_INCREMENT;
  36.                                 break;

  37.                         case 'b':
  38.                                 Current_SoundSignal_Increment = SOUND_B5_INCREMENT;
  39.                                 Current_FilterClock_Increment = FILTCLK_B5_INCREMENT;
  40.                                 break;

  41.                         case 'C':
  42.                                 Current_SoundSignal_Increment = SOUND_C6_INCREMENT;
  43.                                 Current_FilterClock_Increment = FILTCLK_C6_INCREMENT;
  44.                                 break;

  45.                         case 'D':
  46.                                 Current_SoundSignal_Increment = SOUND_D6_INCREMENT;
  47.                                 Current_FilterClock_Increment = FILTCLK_D6_INCREMENT;
  48.                                 break;

  49.                         case 'E':
  50.                                 Current_SoundSignal_Increment = SOUND_E6_INCREMENT;
  51.                                 Current_FilterClock_Increment = FILTCLK_E6_INCREMENT;
  52.                                 break;

  53.                         case 'r':
  54.                                 SFRPAGE = PCA0_PAGE;
  55.                                 PCA0CN0_CR = STOP;
  56.                                 break;
  57.                 }

  58.                 //delay for the length of time corresponding to the current note
  59.                 SFRPAGE = TIMER3_PAGE; TMR3 = 0; while(TMR3 < Timer3Counts_Sequence[CurrentNoteIndex]);
复制代码

这种方法的一个问题是,相同音高的两个较短音符听起来像一个较长的音符。 为了解决这个缺陷,代码检查下一个音符是否与当前音符相同,如果是,则当前音符后面是一个短暂的延迟,在此期间声音信号和滤波器时钟方波被禁用:

  1. REPEATED_NOTE = FALSE;
  2. if(CurrentNoteIndex < (NumberofNotes - 1))
  3. {
  4.         if(NotesSequence[CurrentNoteIndex] == NotesSequence[CurrentNoteIndex + 1])
  5.                 REPEATED_NOTE = TRUE;
  6. }

  7. //-----------------------------------------------------
  8. //then after the note has been played...
  9. //-----------------------------------------------------

  10. if(REPEATED_NOTE == TRUE)
  11. {
  12.         SFRPAGE = PCA0_PAGE;
  13.         PCA0CN0_CR = STOP;
  14.         SFRPAGE = TIMER3_PAGE; TMR3 = 0; while(TMR3 < 200);
  15. }

  16. SFRPAGE = PCA0_PAGE;
  17. PCA0CN0_CR = PLAYorSTOP;
复制代码

这种技术让我们真正受益于“咔嗒”声音,这些声音通常是音频电路的一个麻烦方面:重新启用方波时产生的快速点击或弹出提供了重复音符之间的额外清晰度。但是,我们不需要太多的咔嗒声,因此电路中的隔直电容已经从2μF降低到0.1μF,以缓和这些伪影,这部分是由于相关的瞬态电流带充电滤波电容器。

LTC1063_schem_newcap.jpg


如果您觉得该项目合适,那么试试这个项目吧!

跳转到指定楼层
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

主题 29 | 回复: 32



手机版|

GMT+8, 2024-4-20 12:20 , Processed in 0.141878 second(s), 8 queries , Gzip On, MemCache On. Powered by Discuz! X3.5

YiBoard一板网 © 2015-2022 地址:河北省石家庄市长安区高营大街 ( 冀ICP备18020117号 )

快速回复 返回顶部 返回列表