用STM32H743XIH6制作DDS频率合成器

部署运行你感兴趣的模型镜像

功能设想:自定义简单波形音乐合成器。就是可以自定义输出乐器的音色(修改波形数据),可以输出任意音频频率,计算音阶。用于简单演奏。首先实现频率合成部分。

一、开发环境。

    使用官方集成开发环境STM32CUBEIDE版本1.19.0。

二、系统设计。

  1. 硬件资源。

    用到定时器TIM5(32位可重载计数器,方便频率计算);DAC1(基本覆盖音频应用,一般乐器频率范围在16.352Hz到15804.639Hz);DMA1直接数据传输通道(解放CPU,方便其他功能实现)。只需要片上硬件资源就可以实现基本功能。外部贮存器和屏幕暂时不用,以后可以扩展。

  1. 软件逻辑。

    基本原理: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占用极低,功能扩展空间很大。

您可能感兴趣的与本文相关的镜像

ACE-Step

ACE-Step

音乐合成
ACE-Step

ACE-Step是由中国团队阶跃星辰(StepFun)与ACE Studio联手打造的开源音乐生成模型。 它拥有3.5B参数量,支持快速高质量生成、强可控性和易于拓展的特点。 最厉害的是,它可以生成多种语言的歌曲,包括但不限于中文、英文、日文等19种语言

### STM32H743XIH6 UART配置和使用教程 #### 配置UART引脚 对于STM32H743XIH6微控制器,在设置USART1作为日志输出端口时,默认情况下其TX和RX引脚可能并非PA9和PA10。因此,需要手动指定这些引脚以便正确初始化外设通信功能[^1]。 ```c // 定义GPIO结构体并初始化USART1的Tx/Rx引脚为PA9/PA10 MX_GPIO_Init(); static void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 115200; // 设置波特率 huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } } ``` #### 初始化函数调用 上述代码片段展示了如何通过修改`MX_USART1_UART_Init()`函数中的参数来适配特定硬件平台的需求。这里选择了常见的115200bps波特率,并启用了全双工模式下的发送接收能力。 #### 使用FreeRTOS管理任务 当构建基于FreeRTOS的操作系统环境时,可以创建专门的任务用于处理串行数据收发操作: ```c void StartDefaultTask(void const * argument) { /* Infinite loop */ for(;;) { char log_message[] = "System is running.\r\n"; HAL_UART_Transmit(&huart1, (uint8_t*)log_message, strlen(log_message), HAL_MAX_DELAY); osDelay(1000); // 延迟一秒再重复执行下一次循环 } } int main(void) { ... osThreadDef(default_task, StartDefaultTask, osPriorityNormal, 0, configMINIMAL_STACK_SIZE); task_handle = osThreadCreate(osThread(default_task), NULL); ... } ``` 这段程序定义了一个简单的后台线程,它每隔一秒钟向连接到USART1的日志接口发送一条消息字符串表示当前状态正常运行中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悟渔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值