构建一款裸金属STM32音频合成器,从零开始将物理按键按下转化为动态实时的音频
本项目展示了我们在固件编程方面的初步工作。我们开发该项目,旨在通过实践操作更好地理解课程中的概念和应用。我们的项目名为“电子音调发生器”,通过一个小扬声器产生不同的声音。按下不同的按钮或组合按键,系统会生成不同频率,从而发出各种音调。
本项目的主要目标是帮助我们理解频率生成、声音产生以及数字电子学的基本原理。它展示了电子设备如何接收用户输入,通过微控制器(MCU)进行处理,并生成相应的输出。通过该项目,我们能够掌握固件与硬件在嵌入式系统中协同工作的实际知识。
工作原理
概念
在这个项目中,我们正在构建一个系统,它能从零开始生成音频,通过快速地将单个引脚切换为高电平和低电平,来推动并拉动扬声器锥体(从而产生方波)。
由于我们是在裸机上编程(没有操作系统),我们的微控制器会持续运行一个循环,执行以下两项任务:
轮询:它会持续检查你是否在按任何按钮。
位敲击:当我们按下播放键时,会通过精确的微秒级延迟来开关扬声器引脚。这意味着我们无需使用任何DAC硬件,就能手动操控比特以生成音频。
当我们按下上下按钮时,只是在那微秒延迟的数学计算上进行更改。延迟时间越短,引脚切换速度越快(产生更高的音调);延迟时间越长,切换速度越慢(产生更低的音调)。
软件与硬件架构
在软件方面,项目中使用了以下功能。
update_buttons() 函数是本项目的主要核心。它会不断轮询按钮的输入,并在特定按钮被按下时执行相应操作。该函数会在一个无限循环中每次被调用。
以下是如何在后台实现 update_buttons() 的方式:
这个过程在我们的 main() 函数的 while(1) 循环中被无限循环调用。只有当按钮被按下时才会执行相关任务。如果未按下任何按钮,它会跳过上述任务,然后再次检查按钮。这种方法通过轮询按钮来检测用户输入。
在项目硬件方面,我们使用了以下电路图:
SW1、SW2 和 SW3 分别映射到 RT Spark 1 板的内部按钮。扬声器连接至 AUDIO_OUT 和 GND。对于按钮,其配置为低电平有效(ACTIVE LOW)按钮。上述电路图未显示上拉电阻,因为 STM32F407XX 微控制器已在代码配置中自动处理此功能。然而,上拉电阻仍然很重要,用于防止电压漂移。
硬件设置
本项目中的硬件部分包括制作一个8Ω 0.5W的扬声器模块,该模块内含一个330Ω电阻以限制电流,以及一个1μF、耐压16V的电容,用于阻断直流电流进入扬声器。
以下是本项目硬件所需组件:
此外,我之后会使用实心芯线来焊接所有元件。
下图展示了我们必须遵循的精确电路图。
电阻和电容必须连接在8Ω 0.5W扬声器的信号线上,然后扬声器的另一端必须连接到GND。
下图展示了我根据上方的电路图进行元件焊接的过程。
我们在焊接时确实犯了一个错误,把红色和黑色的实心导线搞反了。我们用黑色导线连接了扬声器的信号端,而用红色实心导线连接了扬声器的接地端。直到完全焊接好电路后才意识到这一点。不过这没关系,因为导线的颜色不会影响电流的流动,只要之后在RT Spark 1开发板上正确地将对应的导线接到正确的引脚上,设备应该仍然能正常工作。
软件设置
MCU 配置设置
要开始使用该软件,请打开 STM32CubeMX 程序,你应该会看到如下所示的界面。
•转到“文件”>“新建项目”以创建新项目
•之后,应该会弹出一个窗口,这就是项目创建界面。
•我们需要使用 RT Spark 1 的微控制器型号,即 STM32F407ZGT6。用该型号在 STM32CubeMX 软件中搜索并选择对应的 MCU。
•选定确切的MCU零件号后,点击“新建项目”窗口右上角的“开始项目”按钮。
•然后你应该会看到下面的屏幕,这表示STM32CubeMX项目已成功创建。
GPIO 配置
我们将首先配置本项目中需要使用的GPIO。
以下是我们本项目将使用的GPIO引脚列表:
•PA3 - 输出模式,用于驱动8Ω 0.5W扬声器的引脚,输出我们生成的音频信号。
•PC0 - 外部中断输入,带上拉电阻模式,该引脚物理连接在RT Spark 1的内置BUTTON_LEFT上,用于触发音调发生器函数的频率增加。
•PC1 - 外部中断输入,带上拉电阻模式,该引脚与RT Spark 1上的BUTTON_DOWN引脚物理连接,用于触发播放/暂停功能。
•PC4 - 外部中断输入,带上拉电阻模式,该引脚物理连接在RT Spark 1的内置BUTTON_RIGHT上,用于触发对音调发生器功能的频率降低。
首先将 PC0 配置为输入引脚。点击引脚 PC0,或在屏幕底部输入内容进行搜索。
从将显示的上下文菜单中选择 GPIO_Input。这会将 PC0 配置为读取 RT Spark 1 板上第一个按钮的按下信号,该按钮会触发音调发生器的“增加频率”功能。
•接下来,找到 PC1 引脚(通常在引脚图中位于 PC0 引脚旁边),然后从弹出的上下文菜单中选择 GPIO_Input。
•此设置将RT Spark 1板上的第二个按钮配置为输入,用于触发音调发生器的播放/暂停功能。
•接下来,在引脚图中找到 PC4,然后点击并选择它,将其设置为 GPIO_Input。
•此设置将RT Spark 1板上的第三个按钮配置为输入,用于触发音调发生器的降低频率功能。
•接下来,找到引脚 PA3,并从弹出的上下文菜单中选择 GPIO_Output。
•此设置将 PA3 作为音调发生器的音频输出。
现在,从系统核心 > GPIO 进入 GPIO 配置面板。以下配置将用于本项目中使用的所有 GPIO。
下方图像显示了MCU中每个引脚所使用的详细GPIO配置:
现在,切换到项目管理器选项卡。在文本框中输入所需的项目名称以及项目位置。
之后,点击工具链/IDE设置,并从下拉菜单中选择STM32CubeIDE。这可确保日后使用STM32CubeIDE程序对我们的RT Spark 1板进行编程时的兼容性。
接下来,点击窗口右上角的“生成项目”按钮。这将生成 STM32F407XX 运行所需的所有样板代码,包括 MCU 的所有 HAL、CMSIS 和底层驱动程序。
现在,我们将进入程序代码。
程序代码设置
首先,我们打开 STM32CubeIDE,然后导入之前由 STM32CubeMX 生成的项目。为此,在 STM32CubeIDE 中,进入“文件”>“从文件系统打开项目”……
点击后,将弹出一个新窗口,我们可以在其中选择之前使用 STM32CubeMX 生成的项目目录。
请确保导入的项目文件夹是STM32CubeMX生成所有项目文件的根目录。同时,请勾选上方红色方框内表格中项目名称旁边的复选框。完成上述操作后,点击“完成”按钮以结束项目导入。
创建主程序
我们将首先打开位于 Core/Src/main.c 中的 boilerplate 主代码文件 main.c。
通过查看标题来确认代码是否已正确生成且不为空。目前生成的模板看起来良好。
现在,根据我们的项目需求,我们将首先为项目的延时函数创建 delay.c 和 delay.h 源文件。
首先在“核心”下的“Inc”文件夹上右键单击,然后选择“新建”>“头文件”。
在弹出的窗口中,将头文件命名为“delay.h”。这将成为我们延迟函数代码的头文件。点击“完成”即可。
现在,使用下面的代码来编写“delay.h”,并将其放入我们刚刚创建的 delay.h 头文件中。
现在,再次右键点击“Core”文件夹下的“Src”文件夹,然后选择“新建 > 源文件”。
在将弹出的窗口中,将“delay.c”作为文件名输入。
之后,点击“完成”以继续创建文件。
打开新创建的 delay.c 文件,并使用下面所示的代码:
之后,返回 main.c 文件,并导入我们刚刚创建的 delay.h 头文件。
请确保将代码放在 main.c 文件的 BEGIN Includes 和 END Includes 之间。如果将代码放在这些注释之外,当在 STM32CubeMX 中重新生成硬件配置时,代码将会丢失。
之后,为项目创建所需的变量。首先将以下代码放入 USER CODE BEGIN 0 中。
对于 play_tone() 函数,请在 USER CODE BEGIN 4 内使用以下代码。
在 USER CODE BEGIN 4 内部的 play_tone() 函数下方,插入 update_buttons() 函数的代码。
现在,我们将在用户代码开始部分(USER CODE BEGIN PFP)内定义这些函数的函数原型。
之后,我们需要开始轮询按钮并检查用户输入。为此,需要在主循环的 while(1) 中调用 update_buttons() 函数。请将以下代码放在 USER CODE BEGIN 3 内部。
然后,我们的 delay_us() 函数在之后还无法使用。我们需要在 ARM 微控制器中初始化 DWT 计数器或数据断点周期计数器。为此,我们之前创建的 delay.c 文件包含一个名为 DWT_Init() 的函数,用于启用此功能。通过将以下代码放入 USER CODE BEGIN 2 中来启用该功能。
现在我们的代码已经完成,可以开始构建项目了。首先点击IDE工具栏上的锤子图标,然后点击锤子图标旁边的向下箭头,选择“调试”。
之后,再次点击锤子图标,应该会显示一个控制台窗口。查找蓝色的“Build Finished”文本,这表示代码已成功编译。
之后,我们将将其烧录到RT Spark 1板上。
为此,我们首先需要将项目配置为可运行的程序。操作方法是:在项目资源管理器窗口中右键单击项目名称,然后选择“作为…运行”>“1 个 STM32 C/C++ 应用程序”。
之后,应该会弹出一个“启动配置属性”窗口。
只需点击“确定”按钮,无需更改任何内容。
现在,要将代码刷入 RT Spark 1 开发板,请点击工具栏上的绿色播放图标。然后点击其旁边的向下箭头图标,以显示可刷入该板的所有可用项目。选择正确的项目名称,以确保代码能在板上正确运行。
选择项目后,应再次显示控制台窗口。它会显示有关板卡编程的提示信息,应显示“下载验证成功”。
完成后,我们的电路板就可进行测试了!
测试
为了测试我们的代码和系统,现在可以将硬件连接到RT Spark 1开发板。
如上图所示,我们直接通过RT Spark 1板的GPIO接口连接了硬件。这个电路看起来比较简单,因此我们认为将其焊接在PCB上可能对这个项目来说有些过度。
连接后,我们用其中的代码为电路板通电。
如您所见,音频设备正在播放音调,但音量非常小且微弱。因此,我们将把它连接到一个放大器上。我们家里唯一可用的放大器是700W立体声功率放大器。通过跳线上的鳄鱼夹将它连接至改装后的AUX IN线路。我们将AUX IN的GND接到RT Spark 1的GND,AUX IN的R(右声道)接到项目中的AUDIO_OUT。
黄色的Aligtor夹子为GND,黑色为右侧通道,绿色为左侧通道。
连接后,我们可以调节放大器的音量,以听到产生的音调。
按下电路板上的BTN_DOWN键会发出声音,松开按键后,声音应暂停。然后在按住电路板的BTN_DOWN键的同时,按下BTN_LEFT或BTN_RIGHT键,可使生成的声音频率增加或减小。
如您所见,它按我们的预期运行了。现在我们可以开始分析和测试了!
分析与解读
我们将使用示波器来分析项目中音调的生成情况。
首先,将CH1和CH2探头连接到示波器的输入端口。然后,CH1探头夹应连接并分析播放/暂停按钮,而CH2探头夹则应连接并分析AUDIO_OUT引脚,以检测生成的音频信号。
然而,由于我们正在监测播放/暂停按钮,因此需要在RT Spark 1开发板上驱动一个外部引脚,因为内置按钮所连接的引脚(在此情况下为PC1)无法直接连接探针。
为了监测按钮按下,我们需要一个调试引脚。对于这个项目,我们来配置一个。
我们先回到 STM32CubeMX。为了用示波器测试按钮按下,我们将使用 PB14。
在 STM32CubeMX 引脚图中,找到 PB14 并将其设置为 GPIO 输出模式。
将设置为输出引脚后,转到系统核心 > GPIO,并按以下方式配置 PB14:
配置完成后,请仔细检查所有GPIO设置,确保第一次就能正常工作。
现在,点击 STM32CubeMX 右上角的“生成代码”按钮。等待文件在 STM32CubeIDE 中重新加载。
之后,我们将稍作修改代码,以添加对示波器的支持。
首先,通过添加以下代码来修改 USER CODE BEGIN 0
然后修改 USER CODE BEGIN 4,以向示波器的测试/调试引脚发出信号,以便查看:
现在,我们准备好测试信号了!
再次将CH1和CH2探头连接到示波器的输入端口。然后,CH1探头夹应连接并分析播放/暂停按钮,而CH2探头夹应连接并分析AUDIO_OUT引脚,以检测生成的音频信号。
可通过MENU(垂直)按钮将示波器设置为单次模式,以捕获信号一次。将CH1设为触发源,选择上升沿作为斜率。若要查看连续的信号流,请将其设置为自动模式。
您可能需要调整“伏特/档位”和“时间”旋钮,以便更清晰地查看信号。您还可以通过垂直位置旋钮将两个通道重叠放置。此外,请再次检查每个探头上的x1和x10物理开关设置。如果两者设置不同,您可能会发现其中一个通道的信号灵敏度远高于另一个通道。因此,下图中CH1显示为2.00V,而CH2显示为200mV,因为我们尚未进行校验。但这些读数不会造成影响,仅是灵敏度的不同。
现在,我们开始第一次读数。将示波器设置为单模式后,按下RUN/STOP按钮以清除屏幕上的所有内容。根据示波器的品牌和型号不同,暂停时RUN/STOP按钮应显示红色,运行时显示绿色。按钮应为绿色,以便捕捉信号。
现在,我们按下了播放按钮,它捕捉到了这些信号。
CH1 是按键按下,CH2 是产生的音调。
现在,我们将测量一些内容。这些是我们项目中要识别的事项:
第一个是Tisr,其中Tbtn表示按钮脉冲变为高电平的精确时间,Taudio表示第一个高电平音调由AUDIO_OUT生成并输出的精确时间。
我们了解到,该按钮使用了轮询机制,即通过无限循环来检查按钮是否被按下。我们这里称之为Tisr,因为更符合标准化的命名方式。这意味着在识别到按钮按下后生成音频所需的时间。
如上图所示,有两个黄色的垂直线,它们就是我们的光标。光标1位于CH1的上升沿,光标2位于CH2的上升沿。因此读数显示Ta = 800.0 ns,这将成为我们的Tbtn;Tb = -5.600 μs,这将成为我们的Taudio。将它们代入公式后,结果为
因此,我们的Tisr相当于6400.0纳秒或6.400微秒,这正是上述示波器中ΔV所显示的值。这意味着从按下播放按钮开始,MCU处理并生成音调所需的时间为6400.0纳秒,即6.400微秒。
接下来我们找到以下内容:
Vpp 是我们的电压峰峰值,即总电压等于信号中最高电压减去最低电压。由于示波器会在屏幕的特定位置显示信号,因此除非图形位于屏幕最下方,否则较低电压不会恰好为 0V。因此,我们需要计算 Vpp 来验证信号的电压值。
为此,我们首先在光标菜单中启用伏特模式。然后调整光标位置,使光标1位于信号上方,光标2位于信号下方,如下图所示。
如您所见,我们正在测量CH2的Vpp,该值即为AUDIO_OUT。光标1位于信号上方的水平线上,用于测量信号高电平时达到的最高电压;光标2位于信号下方的水平线上,用于测量信号低电平时达到的最低电压。
Va 将是我们信号中的最高电压,Vb 则为最低电压。请注意电压值为负数,这是因为我们自由调整了图中信号的垂直位置,这也会导致电压读数的变化。
为了求解 Vpp,我们将数值代入公式,得到:
因此,我们已成功验证,我们的AUDIO_OUT输出的信号RT Spark 1确实大约产生3.6V的电压峰值。这意味着我们的代码运行正常,并且能够生成正确的电压。
我们之前遇到的问题是由于扬声器模块需要更大的电流和电压才能发出更响的声音,而不是我们的代码所致。我们甚至在音频线上加了一个电阻,进一步降低了通过的电流和电压。因此,我们将其连接到放大器上,以便听到更响亮的声音。
最后,我们将检查当调节生成的音调频率上升或下降时,CH2 的宽度会发生怎样的变化。
脉冲宽度较长时,输出的频率较低,与视频中听到的声音一致;当示波器显示脉冲宽度较短时,输出的频率较高,与视频中听到的声音一致。
这意味着脉冲宽度是代码用来改变音频音调音高所控制的参数。脉冲宽度由代码中的时间来控制,而该时间则是AUDIO_OUT引脚每次切换之间的延迟时间。
项目结论
本电子音调发生器项目成功展示了嵌入式系统中固件硬件集成的核心原理。通过配置MCU的GPIO引脚,并利用精确的周期计数(DWT)进行定时,我们所构建的系统能够有效处理物理按钮输入,从而生成并调制音频频率。
主要发现
频率与脉冲宽度:示波器分析表明,声音音高与脉冲宽度成反比。通过修改代码中的时间延迟,成功改变了引脚切换速度,从而直接调控了输出频率。
系统响应性:从按键按下到首次信号生成的时间响应为6.4μs,表现出极高的效率,充分证明了轮询和延迟功能的可靠性。
信号验证与硬件限制:MCU 准确输出了约 3.6V 的预期峰峰值电压 Vpp。初始的低音量被正确识别为由限流电阻和 8Ω 音箱的功耗引起的硬件限制,而非软件缺陷。通过外部放大器对准确的 3.6V 信号进行路由,成功解决了这一问题。
最终,该项目为数字时间处理在固件中的实现直接转化为物理模拟输出提供了实际且可操作的验证。
本文编译自hackster.io





