功能设想:自定义简单波形音乐合成器。就是可以自定义输出乐器的音色(修改波形数据),可以输出任意音频频率,计算音阶。用于简单演奏。首先实现频率合成部分。
一、开发环境。
使用官方集成开发环境STM32CUBEIDE版本1.19.0。
二、系统设计。
- 硬件资源。

用到定时器TIM5(32位可重载计数器,方便频率计算);DAC1(基本覆盖音频应用,一般乐器频率范围在16.352Hz到15804.639Hz);DMA1直接数据传输通道(解放CPU,方便其他功能实现)。只需要片上硬件资源就可以实现基本功能。外部贮存器和屏幕暂时不用,以后可以扩展。
- 软件逻辑。
基本原理:TIM5根据设定计数不断触发DMA1循环传输缓冲区数据到DAC1,由DAC1转换输出模拟波形;DMA1配置为循环传输模式,保证波形连续输出。
其中缓冲区配置两个,每个缓冲区大小为64(即每个波形取样点数为64):一个缓冲区用于保存旧的波形数据,供DMA1循环传输数据到DAC1输出连续波形;另一个备用缓冲区用于新波形数据贮存,当波形数据有更新,就在完成一个周期(一个完整波形)时改变缓冲区,达到波形(音色)更改目的。
频率的改变则是通过改变TIM5重载计数器的值来实现的。也就是说改变DMA1的触发频率,使传输波形数据到DAC的速率改变,从而改变波形输出频率。这种情况下,不管频率高低,每个波形的数据量都是一定的,缓冲区大小就是每个波形的取样数,可以保证高频音阶的质量不会变差。
频率计算方法:利用外部25MHz晶体,通过PLL倍频,将CPU最高时钟频率配置为480MHz,定时器总线频率配置为240MHz。频率计算公式为:重载计数值=240000000/缓冲区大小/频率值。经计算,最高音阶误差在2个音分左右,频率越低误差越小。事实上最高一组音阶已经非常高,一般很少用到,听感上也很难分辨出两个音分的误差,所以整个系统作为音阶发生器是基本可行的。
三、实施步骤。
1、新建工程。
按下图所示操作即可。



2、时钟树配置。
工程新建成功自动进入MX配置界面。如下图所示:

第一步:配置晶体。
第③步是配置主时钟晶体,必须,如果板上没有32.768MHz晶体,则第④步可以省去。
然后向下翻到PTC项,按图示操作。

第二步、配置总线时钟:

向右滚动到如下图所示:保证To APB1 Timer Clocks(MHz)一项显示为240。

第三步、配置定时器TIM5。

第四步:配置DAC1。

第五步:配置DMA。


第六步:按Ctrl+S组合键保存配置并自动生成代码后进入main.c代码编辑界面。

3、代码编写。
(1)、在注释区/* USER CODE BEGIN PV */和/* USER CODE END PV */之间添加如下代码。
#define BUFFER_SIZE 64
uint16_t dataBufferIndex=0;
uint16_t dataBuffer[3][BUFFER_SIZE] = {0};
#define PI 3.14159265358979323846
#define DAC_MAX_VALUE 4095 // 12位DAC最大值
float PitchNotation[12]={8372.224,8870.062,9397.502,9956.306,10548.337,11175.573,11840.106,12544.155,13290.068,14080.335,14917.594,15804.639};//最高音阶组频率值。
uint16_t PitchNotationIndex=0;//索引每一组音阶内的频率。
uint16_t Octave=9;//理论可以0-9共10个八度!
uint16_t BufferUpdatelock=0;//缓冲区数据更新状态:0--没有更新;1--更新完成;2--(或大于1的其他数)正在更新缓冲区数据。
(2)、在/* USER CODE BEGIN 0 */和/* USER CODE END 0 */之间添加如下代码:
于用申明自定义函数。
void generate_sine_wave(uint16_t* buffer, float amplitude);
void generate_sawtooth_wave(uint16_t* buffer, float amplitude, float offset);
void generate_triangle_wave(uint16_t* buffer, float amplitude, float offset);
void SetMusicIndex(uint16_t MusicIndex);//缓冲区数据更改波形为正弦波。
void SwitchBufferPolling(void);//检查是否有缓冲区数据更新,如果有则更改DMA传输缓冲区为新数据的缓冲区。
void setAmplitude(float amplitude);
void setFrequency(float InFrequency);
(3)、在/* USER CODE BEGIN WHILE */下面(主循环之前)添加以下代码:
SetMusicIndex(0);//选择音色,会自动启动DMA传输。
该语句初始化缓冲区数据为一个正弦波形。
(4)、在/* USER CODE BEGIN 3 */下面(主循环内)添加代码验证音阶生成。
/* USER CODE BEGIN WHILE */
SetMusicIndex(0);//选择音色,会自动启动DMA传输。
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
PitchNotationIndex++;
if(PitchNotationIndex>11){
PitchNotationIndex=0;
if(Octave>0){
Octave--;
}else{
Octave=9;
HAL_Delay(3000);
HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_13);
}
}
setFrequency(PitchNotation[PitchNotationIndex]/pow(2,Octave));//设置音频频率。
HAL_Delay(500);
}
/* USER CODE END 3 */
(5)、在/* USER CODE BEGIN 4 */和/* USER CODE END 4 */之间添加自定义函数:
这些代码负责实现波形数据更改,频率更改等功能。
/* USER CODE BEGIN 4 */
void SetMusicIndex(uint16_t MusicIndex){
if(BufferUpdatelock>0){return;}//等待别的进程完成缓冲区数据更新工作。
BufferUpdatelock=2;//锁定缓冲区,防止别的进程修改。
dataBufferIndex=(dataBufferIndex+1)%2;//切换备用缓冲区(当前数据缓冲区正在被DMA传输使用!)。
switch(MusicIndex){
case 0:
generate_sine_wave(dataBuffer[dataBufferIndex],1);
break;
case 1:
generate_sawtooth_wave(dataBuffer[dataBufferIndex],1,0);
break;
case 2:
generate_triangle_wave(dataBuffer[dataBufferIndex],1,0);
break;
default:
break;
}
for(uint16_t Index=0;Index<BUFFER_SIZE;Index++){dataBuffer[3][Index]=dataBuffer[dataBufferIndex][Index];}//波形数据保存到第三个缓冲区,方便幅度计算用。
BufferUpdatelock=1;//数据修改完成。等待DMA传输。
SwitchBufferPolling();
}
void setAmplitude(float amplitude){
amplitude = (amplitude > 1.0f) ? 1.0f : (amplitude < 0.0f) ? 0.0f : amplitude;//参数校验
if(BufferUpdatelock>0){return;}//等待别的进程完成缓冲区数据更新工作。
BufferUpdatelock=2;//锁定缓冲区,防止别的进程修改。
dataBufferIndex=(dataBufferIndex+1)%2;//切换备用缓冲区(当前数据缓冲区正在被DMA传输使用!)。
for(uint16_t Index=0;Index<BUFFER_SIZE;Index++){dataBuffer[dataBufferIndex][Index]=(uint16_t)((float)(dataBuffer[3][Index])*amplitude);}
BufferUpdatelock=1;//数据修改完成。等待DMA传输。
SwitchBufferPolling();
}
void generate_sine_wave(uint16_t* buffer, float amplitude) {
//生成正弦波
amplitude = (amplitude > 1.0f) ? 1.0f : (amplitude < 0.0f) ? 0.0f : amplitude;//参数校验
for (uint16_t i = 0; i < BUFFER_SIZE; i++) {
float radian = 2 * PI * i / BUFFER_SIZE;
float sine_value = sinf(radian);
// 转换为DAC值 (0-4095)
buffer[i] = (uint16_t)((sine_value * amplitude + 1.0f) * (DAC_MAX_VALUE / 2));
}
}
/**
* @brief 填充锯齿波数据到缓冲区
* @param buffer: 目标缓冲区指针
* @param amplitude: 幅值(0-4095对应0-3.3V)
* @param offset: 直流偏置
*/
void generate_sawtooth_wave(uint16_t* buffer, float amplitude, float offset) {
amplitude = (amplitude > 1.0f) ? 1.0f : (amplitude < 0.0f) ? 0.0f : amplitude;//参数校验
offset = (offset > 1.0f) ? 1.0f : (offset < 0.0f) ? 0.0f : offset;//参数校验
for(uint32_t i = 0; i < BUFFER_SIZE; i++) {
buffer[i] = (uint16_t)((i * amplitude*4096 / BUFFER_SIZE) + offset*4096);
}
}
/**
* @brief 填充三角波数据到缓冲区
* @param buffer: 目标缓冲区指针
* @param amplitude: 幅值(0-4095对应0-3.3V)
* @param offset: 直流偏置
*/
void generate_triangle_wave(uint16_t* buffer, float amplitude, float offset) {
amplitude = (amplitude > 1.0f) ? 1.0f : (amplitude < 0.0f) ? 0.0f : amplitude;//参数校验
offset = (offset > 1.0f) ? 1.0f : (offset < 0.0f) ? 0.0f : offset;//参数校验
uint32_t half_size = BUFFER_SIZE / 2;
// 上升沿
for(uint32_t i = 0; i < half_size; i++) {
buffer[i] = (uint16_t)((i * amplitude*4096 * 2 / BUFFER_SIZE) + offset*4096);
}
// 下降沿
for(uint32_t i = half_size; i < BUFFER_SIZE; i++) {
buffer[i] = (uint16_t)((amplitude*4096 * 2 - i * amplitude*4096 * 2 / BUFFER_SIZE) + offset*4096);
}
}
void SwitchBufferPolling(void)
{
//缓冲区切换。
uint32_t timeOutNum=0;
if(BufferUpdatelock!=1){return;}//数据无更新为0;数据更新完成为1;数据更新中数字大于1。
//正确操作顺序应严格遵循: ① 等待DMA计数器到达缓冲区末尾 → ② 停止DMA → ③ 更新缓冲区 → ④ 重启DMA。(在使用备用缓冲区情况下, ③ 更新缓冲区这步可以提前完成,直接切换缓冲区速度更快。)
//通过检查计数值是否变化判断传输是否正在进行。如果连续三次读取到计数值相同,表示传输没有开始。
while(__HAL_DMA_GET_COUNTER(&hdma_dac1_ch1) != BUFFER_SIZE&&timeOutNum<1000000){timeOutNum++;} //之前程序一定要保证DMA一直在传输,如果之前DMA没有启动传输,可能大约会等待不到1秒左右时间。
HAL_TIM_Base_Stop(&htim5);
while(htim5.Instance->CR1 & TIM_CR1_CEN); // 等待定时器完全停止
HAL_DAC_Stop_DMA(&hdac1, DAC_CHANNEL_1);__NOP();__NOP();__NOP();//加三个空操作是为了延时待MDA传输完全停止。然后再重新配置。未停止当前DMA传输直接重启新传输,可能引发DMA控制器状态机混乱。
HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1, (uint32_t*)dataBuffer[dataBufferIndex], BUFFER_SIZE, DAC_ALIGN_12B_R);
HAL_TIM_Base_Start(&htim5);
BufferUpdatelock=0;//表示缓冲区数据更改已经完成。
}
void setFrequency(float InFrequency){
//设置震荡频率。InFrequency:标准频率(最高八度频率组);InOctave:等分倍率(分频得到低若干个八度的音准)。
//uint32_t APB1_TimerClocks=240000000;
uint32_t TemOverload=0;//uint32_t timeOutNum=0;
TemOverload=(uint32_t)(240000000/BUFFER_SIZE/InFrequency-1);
//while(__HAL_DMA_GET_COUNTER(&hdma_dac1_ch1) != BUFFER_SIZE&&timeOutNum<10000000){timeOutNum++;} //之前程序一定要保证DMA一直在传输,如果之前DMA没有启动传输,可能大约会等待不到1秒左右时间。
//__HAL_TIM_DISABLE_IT(&htim5, TIM_IT_UPDATE);
HAL_TIM_Base_Stop(&htim5);
htim5.Init.Period =TemOverload;
htim5.Init.Prescaler =0;
HAL_TIM_Base_Init(&htim5);
HAL_TIM_Base_Start(&htim5);
}
/* USER CODE END 4 */
至此代码编写完成,可以编译测试了。
告别提醒:如果你的代码中有MPU_Config();这一句,请将它注释掉,这个是内存保护功能,如果不注释掉,你必须设置内存保护相关项,本工程不用内存保护,直接注释掉就行了,如果不注释掉这句,可能程序无法运行,因为缓冲区读写等可能被禁止。
4、编译运行。
(1)、调试器设置。
确保调试器驱动已经安装。我使用的是Risym ST-LINK V2,配置如下图所示:




(2)、结束调试,全速运行,用示波器观察DAC1输出波形。(本工程使用DAC1默认输出引脚PA4):

如果接入滤波及音频放大电路,你就可以听到音阶由低到高不断变化的音频信号了。
四、总结。
通过硬件配置和代码配合使用TIM5定时器不断触发DAM1循环传输缓冲区数据到DAC1,从而生成模拟音频信号(DDS),辅以频率计算过程,产生音阶。如果继续完善电路和代码,可以做一个简单的合成器,可扩展音色选择、弯音调节、音量调节以及包络功能等。因为是DMA1循环传输模式,CPU占用极低,功能扩展空间很大。
2699

被折叠的 条评论
为什么被折叠?



