Keil MDK STM32系列(八) STM32F4基于HAL的PWM和定时器输出音频

2023-02-18,,,,

Keil MDK STM32系列

Keil MDK STM32系列(一) 基于标准外设库SPL的STM32F103开发
Keil MDK STM32系列(二) 基于标准外设库SPL的STM32F401开发
Keil MDK STM32系列(三) 基于标准外设库SPL的STM32F407开发
Keil MDK STM32系列(四) 基于抽象外设库HAL的STM32F401开发
Keil MDK STM32系列(五) 使用STM32CubeMX创建项目基础结构
Keil MDK STM32系列(六) 基于HAL的ADC模数转换
Keil MDK STM32系列(七) 基于HAL的PWM和定时器
Keil MDK STM32系列(八) 基于HAL的PWM和定时器输出音频
Keil MDK STM32系列(九) 基于HAL和FatFs的FAT格式SD卡TF卡读写

方式1: 通过PWM和TIM输出音频

机制

音频使用一个预生成的的8bit无符号数组, 采样率为8KHz
输出包含两部分, 一部分是TIM2产生连续的PWM, PWM分辨率设置为256, 正好对应8bit PCM采样
输出的第二部分是TIM3产生的定时中断, 中断的频率正好是8KHz, 每次中断都修改一次PWM的占空比
通过调节PWM频率可以调节输出音质, PWM频率越高音质越好(谐振频率越远离音频)
通过调节PWM分辨率可以调节音量, PWM分辨率越高, 音量越低

配置STM32CubeMX

选择芯片STM32F401CCU6, 创建新项目

系统时钟

System Core -> SYS-> Debug: Serial Wire
System Core -> RCC-> High Speed Clock (HSE): Crystal/Ceramic Resonator 启用外接高速晶振
Clock Configuration: (配置为最高84MHz)选择外部晶振, 连接HSE和PLLCLK, 在HCLK上输入84回车, 软件会自动调节各节点倍数

PWM(使用TIM2)

Timers -> TIM2
Clock Source: Internel Clock, 使用系统的时钟源
Channel1: PWM Generation CH1
Counter Settings PWM频率 = 84MHz / (Perscaler + 1) / (Counter Period + 1)
Perscaler: 0
Counter Mode: Up
Counter Period: 255
Internal Clock Division(CKD): No Division
auto-reload preload: Enable
Trigger Output
Master/Slave Mode (MSM bit): Disable
Trigger Event Selection: Reset (UG bit from TIMx_EGR)
PWM Generation Channel 1
Mode: PWM mode 1
Pulse: 0
Output compare perload: Enable
Fast Mode: Disable
CH Polarity: High

8KHz定时中断(使用TM3)

Timers -> TIM3
勾选 Internal Clock
Counter Settings
Prescaler: 0
Counter Mode: Up
Counter Period: 10499 # 10500 = 84MHz / 8KHz
Internal Clock Division (CKD): No division
auto-reload preload: Disable
Trigger Output (TRGO) Parameters
Master/Slave Mode (MSM bit): Disable
Trigger Envent Selection: Reset
NVIC Settings
TIM3 global interrupt: Enable

代码修改

通过STM32CubeMX生成代码后, 需要对main.c添加代码

/* USER CODE BEGIN PV */
uint8_t pwm_buf[] = {125, 125, ..., 126, 125}; // 这里是一个长数组, 可以自己通过工具生成
uint8_t *start = pwm_buf, *end = pwm_buf, *lb = pwm_buf, *rb = (pwm_buf + 27451); // 27451是数组长度
/* USER CODE END PV */

main函数

int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM2_Init();
MX_TIM3_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_1);
HAL_TIM_Base_Start_IT(&htim3);
/* USER CODE END 2 */ while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
}

添加定时器中断处理函数

/* USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM3)
{
__HAL_TIM_SetCompare(&htim2, TIM_CHANNEL_1, *start++);
if (start == rb) {
start = lb;
}
}
}
/* USER CODE END 4 */

输出效果演示

https://www.bilibili.com/video/BV1pb4y1177L

方式2: 通过PWM+DMA

通过配置成DMA的方式, 可以省掉一个定时器, 并且不需要主进程介入而直接将数组赋值给PWM.

这里有个需要注意的地方, STM32F401的各个TIMx计数器位宽不同, TIM2,TIM5是32bit, 其它的都是16bit, 而STM32F103的TIMx全是16bit位宽的. 之前在这个问题上困惑了很长时间, 后来费了不少工夫测试, 加上对比其它项目代码的配置才找到原因.

在设置DMA时, DMA_HandleTypeDef.Init.PeriphDataAlignment要与TIMx的计数器位宽一致, 如果没设置成一致会导致PWM输出错误.

而MemDataAlignment要与数组的数据类型一致, 实际上也要设置成对应的位宽.

根据ST的手册如果勾选了FIFO, 可以设置为其它位宽, 系统会自动补位, 但是实际测试并不能, 无论如何调整FIFOThreshold, MemBurst, 音频的前半部分都是错误的, 只能播放后半部分. 原因待查.

配置STM32CubeMX

选择芯片STM32F401CCU6, 创建新项目

系统时钟

System Core -> SYS-> Debug: Serial Wire
System Core -> RCC-> High Speed Clock (HSE): Crystal/Ceramic Resonator 启用外接高速晶振
Clock Configuration: (配置为最高84MHz)选择外部晶振, 连接HSE和PLLCLK, 在HCLK上输入84回车, 软件会自动调节各节点倍数

PWM(使用TIM3)

Timers -> TIM3
Internel Clock: 勾选, 使用系统的时钟源
Channel1: PWM Generation CH1
Counter Settings PWM频率 = 84MHz / (Perscaler + 1) / (Counter Period + 1)
Perscaler: 40
Counter Mode: Up
Counter Period: 255
Internal Clock Division(CKD): No Division
auto-reload preload: Enable
Trigger Output
Master/Slave Mode (MSM bit): Disable
Trigger Event Selection: Reset (UG bit from TIMx_EGR)
PWM Generation Channel 1
Mode: PWM mode 1
Pulse: 0
Output compare perload: Enable
Fast Mode: Disable
CH Polarity: High

DMA Settings: Add

DMA Request: TIM3_CH1/Trig
Stream: DMA1 Stream4
Direction: Memory To Peripheral
Priority: High
Mode: Circular
Increment Address: Peripheral[不勾选], Memory[勾选]
Use Fifo: 不勾选
Data Width: Peripheral[Half Word], Memory[Half Word]

代码修改

只需要在main.c中添加变量和启动方法

/* USER CODE BEGIN PV */
uint16_t pwm_buffer[] = {125, 125, 128, ...};
/* USER CODE END PV */ //... MX_TIM3_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_PWM_Start_DMA(&htim3, TIM_CHANNEL_1, (uint32_t *)pwm_buffer, 27452);
/* USER CODE END 2 */

在PA6上就能观察到PWM, 接上喇叭能听到输出. 这种方式因为基频8KHz就在人耳的听觉范围内, 会有持续的明显的高频声, 通过增加RC低通滤波能改善但是无法消除, 最好的方式还是将基频提升到20KHz以上, 这样基本上就不会被人耳感知了.

参考

详细说明了STM32的DMA工作方式 https://vivonomicon.com/2019/07/05/bare-metal-stm32-programming-part-9-dma-megamix/
DMA+PWM的位宽讨论 https://community.st.com/s/question/0D50X0000C6bAMdSQM/hal-timers-dma-method-enforces-4bytes-alignment-why-
另一个位宽相关的讨论 https://community.st.com/s/question/0D50X0000B45uUx/generation-of-pwm-wave-with-dma

Keil MDK STM32系列(八) STM32F4基于HAL的PWM和定时器输出音频的相关教程结束。

《Keil MDK STM32系列(八) STM32F4基于HAL的PWM和定时器输出音频.doc》

下载本文的Word格式文档,以方便收藏与打印。