• STM32实战总结:HAL之PWM蜂鸣器


    PWM蜂鸣器基础知识参考:

    51单片机外设篇:蜂鸣器_路溪非溪的博客-CSDN博客

    通用定时器

    比基础定时器多了一些功能。

    以上,

    蓝色部分就是基础定时器,不过在TRGO部分,多了至其他定时器和ADC的功能,计数器多了向下计数和中央对齐模式;

    红色部分是相比基础定时器多出来的三个时钟源;

    绿色部分是输入捕获功能;

    黄色部分是输出比较功能;

    注意:

    图上可以看到,TIMx的四个通道,在输入捕获中被使用;在输出捕获中也被使用,这不就冲突了吗?

    其实,是根据配置来复用的。

    当使用输入捕获时,就给输入捕获用,当使用输出比较时,就给输出比较用。

    另外,捕获/比较寄存器也是输入捕获和输出比较共用的。

    时钟源

    计数器时钟可由下列时钟源提供:

    ● 内部时钟(CK_INT)

    ● 外部时钟模式1:外部输入脚(TIx),注意只来自CH1和CH2。

    ● 外部时钟模式2:外部触发输入(ETR)

    ● 内部触发输入(ITRx):使用一个定时器作为另一个定时器的预分频器,如可以配置一个定时器Timer1而作为另一个定时器Timer2的预分频器。

    注:时钟通常是一个周期记一个数,但是TI1F_ED一个周期会计数两次,上升沿和下降沿各计数一次。

    输出比较

     一直以为输出比较和输出PWM波是同一个意思,但是根据MX中的配置选项来看并不一样。

    首先,理解下波形的相位。波形的幅度,频率,周期都比较好理解。

    那相位是什么概念?简单理解就是波形上的某一点在重复周期中所处的位置。

    相位与波形之间的时间形成对比,以 0 到 360 度计算。

    当两个波形在同一时间开始,就称它们相位符合相位对齐。如果一个波形稍微迟于另一个波形,则称它们偏离相位

    然后,参考这篇文章:

    STM32输出比较模式和PWM模式 比较_嵌入式@hxydj的博客-CSDN博客

    输出PWM波

    输出PWM原理

    总结就是:

    ARR决定周期;CCR决定占空比;

    根据设置,有不同的模式:

    哪种模式并不重要,重要的是计数值和比较值的设置。

    输入捕获

    什么是输入捕获(捕捉)

    输入捕获可以对输入的信号的上升沿,下降沿或者双边沿进行捕获,通常用于测量输入信号的脉宽、测量 PWM 输入信号的频率及占空比。

    输入捕获模式可以用来测量脉冲宽度或者测量频率。STM32的定时器,除了TIM6和TIM7,其他定时器都有输入捕获功能。STM32的输入捕获,简单的说就是通过检测TIMx_CHx上的边沿信号,在边沿信号发生跳变(比如上升沿/下降沿)的时候,将当前定时器的值(TIMx_CNT)存放到对应的通道的捕获/比较寄存(TIMx_CCRx)里面,完成一次捕获。同时还可以配置捕获时是否触发中断/DMA 等。

    例如:我们用到TIM5_CH1来捕获高电平脉宽,也就是要先设置输入捕获为上升沿检测,记录发生上升沿的时候TIM5_CNT的值。然后配置捕获信号为下降沿捕获,当下降沿到来时,发生捕获,并记录此时的TIM5_CNT值。这样,前后两次TIM5_CNT之差,就是高电平的脉宽,同时TIM5的计数频率我们是知道的,从而可以计算出高电平脉宽的准确时间。

    因为我们要捕获的是高电平信号的脉宽,所以,第一次捕获是上升沿,第二次捕获时下降沿,必须在捕获上升沿之后,设置捕获边沿为下降沿,同时,如果脉宽比较长,那么定时器就会溢出,对溢出必须做处理,否则结果就不准了。这两件事,我们都在中断里面做,所以必须开启捕获中断和更新中断。

    根据上图可知,计数值为,N*(ARR + 1) + CCRx2,这里不是乘以2,是埃克斯2。

    我们之后计数器的频率和周期,所以就能算出高电平的时间。

    溢出是计数器溢出,然后会自动重装载,再次循环计数。别理解错了。

    脉冲计数

    在四种时钟来源中,常用的是内部时钟,内部触发时钟常用与定时器的级联。

    另外还有外部时钟模式1和外部时钟模式2,即从CH1/CH2,或者ETR复用管脚输入时钟。

    而脉冲计数,就是针对这两个时钟来使用的。

    以下以外部时钟模式1举例说明。

    其中,TI1FP1和TI1FP2是单边沿触发,每周期计一个数;TI1F_ED是双边沿触发,每周期会计两个数,实际中根据需要选择即可。

     定时器级联

    定时器的主从模式

    当定时器的工作受到外来触发信号的影响或控制时,它就是工作在从模式,其中从模式可以有多种;

    如果某定时器能产生触发输出并作为其它定时器的触发输入信号时,此时该定时器就是工作在主模式;

    如果某定时器的工作既受外来触发信号的影响或控制,同时又能输出触发信号影响或控制别的从定时器,它就是处于主从双角色模式。

    关于从模式,可以仔细阅读这篇文章:《STM32定时器的信号触发与主从模式》

    大意就是,如果时钟不是RCC的内部时钟,就属于工作在从模式下。

    TIMx定时器能够在多种模式下和一个外部的触发同步。

    整体上讲,STM32通用或高级定时器的主要从模式有如下几种:[SMS@TIMx_SMCR]

    1、复位模式 【Reset mode】

    2、触发模式 【Trigger mode】

    3、门控模式 【Gate mode】

    4、外部时钟模式1 【External clock mode 1】

    其实从模式就是的设置就是关于定时器级联以及外部触发时钟的选择。

    如果不用级联,并且不适用外部触发时钟,可以不管。

    单脉冲模式

    首先,什么是脉冲信号?

    在电子技术中,脉冲信号是一个按一定电压幅度,一定时间间隔连续发出的脉冲信号。脉冲信号之间的时间间隔称为周期;而将在单位时间,如1秒内所产生的脉冲个数称为频率。

    什么是单脉冲?

    单脉冲就是只有一个脉冲电压的信号,即电压在短时间内迅速上升,一段时间后又快速下降,就形成一个脉冲,如果该脉冲只有一个的话,就是单脉冲。

    关于STM32的单脉冲模式,具体参考这篇文章:

    STM32定时器单脉冲输出模式_stm32单脉冲输出_ICer_Wx的博客-CSDN博客

    定时器的异或功能

    TIMx_CR2寄存器中的TI1S位,允许通道1的输入滤波器连接到一个异或门的输出端,异或门的 3个输入端为TIMx_CH1TIMx_CH2TIMx_CH3

    异或输出能够被用于所有定时器的输入功能,如触发或输入捕获。此特性可用于连接霍尔传感器,基本用不到,可忽略。

    查看原理图

    根据原理图可知,只要在PA8端口,即TIM2和输入某种频率的PWM波即可发出声响。

    用51的老办法,手动构造一个方波信号,也能驱动蜂鸣器

    那么,在32中,是怎么实现的呢?

    PWM实现

    在32中,可以通过通用定时器或者高级定时器来实现PWM,在这里的硬件电路上选择的是高级定时器TIM1(拥有通用寄存器的所有功能)来实现的。

    此功能无需产生中断,只需要控制定时器不断输出一个PWM波形即可。

    配置MX

    虽然硬件是用高级定时器来实现的,但是因为本文主要是想讲通用定时器,所以我们这里先看下通用定时器的MX配置情况

    基本使用中,从模式不用管,混合通道不用管,异或功能不用管, 单脉冲不用管。

    关键配置时钟源,然后通道配置。

    直接捕获

    间接捕获

    触发捕获

    输出比较但无输出

    输出比较(且输出)

    PWM生成但无输出

    PWM生成(且输出)

    再看对应的参数设置:

    在输出PWM波的场景中,配置可分为时基配置、TRGO和PWM生成。

    时基配置从上到下依次为:

    ◆预分频值;

    ◆计数模式:

    ◆计数值;

    ◆时钟分频因子:这个是在输入捕获时使用的,是决定数字滤波器(ETRTIx)采样频率的参数;

    ◆自动预装载使能,根据相应寄存器中的自动装载预装载使能位的设置,预装载寄存器的内容被立即或在每次的更新事件UEV时传送到影子寄存器

    TRGO参数暂时不管;

    PWMCH1配置从上到下依次为:

    ◆PWM模式选择:模式1和模式2有什么区别?

    模式1和模式2决定了比较结果的高电平或者低电平:

    如果模式1是高低高低高低,那么同样的设置时模式2就是低高地高地高。

    ◆脉冲数,即比较寄存器里要写入的比较数;

    ◆输出比较寄存器预装载使能,根据相应寄存器中的自动装载预装载使能位的设置,预装载寄存器的内容被立即或在每次的更新事件UEV时传送到影子寄存器

    ◆快速模式:先不用管

    ◆通道极性:这个也是控制输出波形的极性的。表明当计数值小于比较值时是什么波形?

    CH Polarity即通道输出极性。假设我们用TIM3向上计数模式,按照前面PWM模式选择的原理,CH Polarity的极性加上IO逻辑, 才是最终PWM的波形。举个栗子, PWM模式1时,如果CH Polarity为LOW(0),那么当向上计数到CCRx之前本该是1(高电平),但实际上还要加上Ch的输出极性, 所以那一段实际上是输出0(低电平)。 细细品一下脑子就通了。

    接下来,根据高级定时器的硬件连接,来配置MX,驱动蜂鸣器。

    配置时钟:

    高级定时器的时钟来自于APB2

    接着配置相关参数:

    有个小插曲,这里配置CH1时,以为只有以下几个选项:

    还傻傻地以为怎么没有PWM波输出选项。

    就是没看到右侧的下滑条。服了!!!!!

    具体参数设置:

    PWM波的必要说明,请参照上方通用定时器的MX内容。

    至此,完成高级定时器1的MX配置。

    比较简单,就是频率的设置,以及占空比的设置。

    如果我想设置1KHz,占空比50%的PWM波,应当如何设置。

    频率1KHz,也就是周期1ms

    PCLK2为72MHz,71分频后为1MHz,每个计数为1us,想要1ms,就得计数1000个,即填入999。占空比50%,就得填入499的比较值。

    代码实现

    实现功能:通过按键KEY1实现蜂鸣器的开启和关闭,通过基本定时器TIM6改变蜂鸣器的频率。

    PWM波相关函数,在文件stm32f1xx_hal_tim.h中:

    1. /** @addtogroup TIM_Exported_Functions_Group3 TIM PWM functions
    2. * @brief TIM PWM functions
    3. * @{
    4. */
    5. /* Timer PWM functions ********************************************************/
    6. HAL_StatusTypeDef HAL_TIM_PWM_Init(TIM_HandleTypeDef *htim);
    7. HAL_StatusTypeDef HAL_TIM_PWM_DeInit(TIM_HandleTypeDef *htim);
    8. void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim);
    9. void HAL_TIM_PWM_MspDeInit(TIM_HandleTypeDef *htim);
    10. /* Blocking mode: Polling */
    11. HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
    12. HAL_StatusTypeDef HAL_TIM_PWM_Stop(TIM_HandleTypeDef *htim, uint32_t Channel);
    13. /* Non-Blocking mode: Interrupt */
    14. HAL_StatusTypeDef HAL_TIM_PWM_Start_IT(TIM_HandleTypeDef *htim, uint32_t Channel);
    15. HAL_StatusTypeDef HAL_TIM_PWM_Stop_IT(TIM_HandleTypeDef *htim, uint32_t Channel);
    16. /* Non-Blocking mode: DMA */
    17. HAL_StatusTypeDef HAL_TIM_PWM_Start_DMA(TIM_HandleTypeDef *htim, uint32_t Channel, uint32_t *pData, uint16_t Length);
    18. HAL_StatusTypeDef HAL_TIM_PWM_Stop_DMA(TIM_HandleTypeDef *htim, uint32_t Channel);

    这里,我们重点关注这个函数:

    HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel);

    创建文件,buzzer.c和buzzer.h

    buzzer.h

    1. #ifndef _BUZZER_H_
    2. #define _BUZZER_H_
    3. typedef enum
    4. {
    5. CLOSE,OPEN
    6. } buzzer_state;
    7. typedef struct
    8. {
    9. buzzer_state buzzerState;
    10. void (*buzzerOpen)(void);
    11. void (*buzzerClose)(void);
    12. } buzzer_handler;
    13. extern buzzer_state buzzerState;
    14. extern buzzer_handler buzzerHandler;
    15. #endif

    buzzer.c

    1. #include "myapplication.h"
    2. static void BuzzerOpen(void);
    3. static void BuzzerClose(void);
    4. buzzer_handler buzzerHandler =
    5. {
    6. CLOSE,
    7. BuzzerOpen,
    8. BuzzerClose
    9. };
    10. static void BuzzerOpen(void)
    11. {
    12. HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);//这里的通道别搞错了,我一开始写成了HAL_TIM_ACTIVE_CHANNEL_1
    13. }
    14. static void BuzzerClose(void)
    15. {
    16. HAL_TIM_PWM_Stop(&htim1, TIM_CHANNEL_1);
    17. }

    再新增key.c和key.h

    key.h

    1. #ifndef _KEY_H_
    2. #define _KEY_H_
    3. typedef struct
    4. {
    5. void (*key1BuzzerSORP)(void);
    6. } key_handler;
    7. extern key_handler keyHandler;
    8. #endif

    key.c

    1. #include "myapplication.h"
    2. static void Key1BuzzerSORP(void);
    3. key_handler keyHandler =
    4. {
    5. Key1BuzzerSORP
    6. };
    7. static void Key1BuzzerSORP(void)
    8. {
    9. if(buzzerHandler.buzzerState == CLOSE)
    10. {
    11. buzzerHandler.buzzerOpen();
    12. buzzerHandler.buzzerState = OPEN;
    13. }
    14. else
    15. {
    16. buzzerHandler.buzzerClose();
    17. buzzerHandler.buzzerState = CLOSE;
    18. }
    19. }

    之后,在按键1的外部中断函数中调用按键1处理函数

    1. //重写外部中断函数
    2. void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
    3. {
    4. switch(GPIO_Pin)
    5. {
    6. case KEY0_Pin :
    7. keyHandler.key1BuzzerSORP();
    8. led_operater_middle.ledMiddle(LED1, LedExtinguish);
    9. break;
    10. case KEY1_Pin :
    11. printf("second key is running.\n\r");
    12. led_operater_middle.ledMiddle(LED2, LedExtinguish);
    13. break;
    14. case KEY2_Pin :
    15. printf("third key is running.\n\r");
    16. led_operater_middle.ledMiddle(LED3, LedExtinguish);
    17. break;
    18. case KEY3_Pin :
    19. printf("forth key is running.\n\r");
    20. relayObj.relayOpen();
    21. break;
    22. default:
    23. printf("key fault!please click right key.\n\r");
    24. }
    25. }

    到了这里,就完成了按键一控制蜂鸣器开启或者关闭的功能。

    接下来,需要实现改变蜂鸣器频率的功能。

    蜂鸣器频率的改变取决于装载值和比较值。

    当前的装载值是999,比较值是498,也就是说,周期是1ms,占空比50%。

    为了改变这两个值,就需要去改变相应的寄存器的值。

    当前设置,一旦开启,每1秒改变一次频率。让频率往更大的方向去变化(越来越尖锐),频率越大,周期越短,装载值也就越小。

    那么,就每隔1秒让装载值减少100,直到减少到499,就再循环回去从999开始。

    相对应的,比较值都是一半,保证占空比都是50%。

    那么,怎么去改变这两个寄存器的值呢?

    通过寄存器的结构体指针去访问对应的寄存器。

    TIM1 -> ARR来改变装载值;

    TIM1 -> CCR1来改变通道1的比较值。

    TIM1是个结构体指针的宏定义。

    那么,我们就再TIM6的回调函数中实现这个功能。

    1. //重写TIM6中断调用函数
    2. void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
    3. {
    4. if((htim->Instance) == (htim6.Instance) & buzzerHandler.buzzerState == OPEN)
    5. {
    6. if(++circleCount == TIME_COUNT_1S)
    7. {
    8. //改变频率
    9. TIM1->ARR -= 100;
    10. if(TIM1->ARR < 100)
    11. {
    12. TIM1->ARR = 999;
    13. }
    14. TIM1->CCR1 = (TIM1->ARR - 1)/2;
    15. printf("current arr is %d\n\r", TIM1->ARR);
    16. circleCount = 0;
    17. }
    18. }
    19. }

    注意:别忘了开启定时器6。

    尝试发出do、re、mi、fa、sol、la、si

    以国际标准音A-la-440HZ为准:

    do的频率为261.6HZ,相应的计数值为3822

    re的频率为293.6HZ,相应的计数值为3406

    mi的频率为329.6HZ,相应的计数值为3034

    fa的频率为349.2HZ,相应的计数值为2864

    sol的频率为392HZ,相应的计数值为2551

    la的频率为440HZ,相应的计数值为2272

    si的频率为493.8HZ,相应的计数值为2025

    1. //重写TIM6中断调用函数
    2. void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
    3. {
    4. if((htim->Instance) == (htim6.Instance) & buzzerHandler.buzzerState == OPEN)
    5. {
    6. uint16_t fluency[] = {3822, 3406, 3034, 2864, 2552, 2272, 2026};
    7. static uint8_t fluencyCount = 0;
    8. if(++circleCount == TIME_COUNT_1S)
    9. {
    10. //改变频率
    11. TIM1->ARR = fluency[fluencyCount];
    12. if(fluencyCount++ == 6)
    13. {
    14. fluencyCount = 0;
    15. }
    16. TIM1->CCR1 = TIM1->ARR / 2;
    17. printf("current arr is %d\n\r", TIM1->ARR);
    18. circleCount = 0;
    19. }
    20. }
    21. }

    补充 

    什么是脉宽?

    脉宽(Pulse-Width)是脉冲宽度的缩写,脉冲宽度就是高电平持续的时间。

    脉宽由信号的周期和占空比确定,其计算公式是脉宽W=T×P(T:周期,P:占空比)

    关于PWM波可参考:

    详解PWM原理、频率与占空比_嵌入式资讯精选的博客-CSDN博客

  • 相关阅读:
    全球领先飞瞳引擎™云服务全球两千+企业用户,集装箱识别集装箱箱况残损检测,正常箱号识别率99.98%以上,箱信息识别及铅封识别免费
    操作系统-内存管理、进程线程
    物联网-物联前端安全加密技术简介
    2025快手校招面试真题汇总及其解答(二)
    docker-redis
    【学习笔记】云原生初步
    CSS3 —— CSS3 基础(边框、渐变、文本效果)
    智能优化与机器学习结合算法实现数据预测matlab代码清单
    【笔记整理】软考-软件设计师
    Linux性能优化实战内存篇(五)
  • 原文地址:https://blog.csdn.net/qq_28576837/article/details/126807743