考虑基于前面设计的程序实现一个蜂鸣器的驱动。
蜂鸣器部分的电路是:
蜂鸣器看起来可以被叫做beep或buzzer。这里我在程序上使用的名称是buzzer。这个Buzzer连在了PB4上面的。
环境 | 描述 |
---|---|
MCU | STM32F407VGT6 |
IDE | Keil mdk |
RTOS | Keil RTX5 |
串口 | 继承前面的设计没有改变。USART1,RX: PA9,TX:PA10,Baudrate 115200 |
蜂鸣器Buzzer | PB4(TIM3_CH1) |
一共有3个功能要做:
这个蜂鸣器必须给脉冲才能发出声音。可以考虑DAC或者PWM。我这里考虑PB4的AF2是TIM3_CH1,所以采用PWM输出比较好。
PWM是通过配置定时器实现的。Tim3属于General-purpose timers,所以参考手册的第18章来进行配置。
要让定时器实现PWM一共有3个外设需要配置:
外设名称 | 用途 |
---|---|
RCC | 使能GPIOB和TIM3的时钟 |
GPIOB | 相应的管脚配置。即配置PB4为AF2,且上拉。 |
TIM3 | 配置输出的PWM |
前面两个都比较熟悉。关于TIM3配置输出PWM,首先需要了解一下407产生PWM的原理。这个407产生PWM的原理描述可能和咱们电力电子、通讯上面说的PWM会有点不一样,但是理解了以后用起来都没有问题。弄清楚就行了。用大白话说就是,一共就2个PWM模式,PWM1和PWM2。对于PWM1来说,基础定时器开始工作,CNT周期性装载ARR的值。如果CNT的值比CCR1的大,就输出使能,否则就是输出禁止。至于使能和禁止对应的是高电平还是低电平,那就看CCER的配置位。PWM2也差不多,不一样的是,如果CNT的值比CCR1的大,就输出禁止,否则就是输出使能。
需要关注的外设寄存器有CR1,ARR,CCER,CCR1,CCMR1和PSC。看起来这些寄存器的数量有点多,其实做一下会发现挺简单的。
外设寄存器 | 功能描述以及在本工程中的用法 |
---|---|
CR1 | 如果另有安排就参考手册配置,否则保持默认值。就用一下那个CEN位。其它的感兴趣的自行看手册 |
ARR | 用于配置PWM基波的频率,或者说是计数 |
CCMR1 | 配置一下Channel1的工作模式为输出PWM。就是吧CCMR1_OCIM[2:0]配置成110(PWM1)或111(PWM2) |
CCER | 用于使能或禁止Tim3_Channel1。就是操作TIM3_CCER_CC1E位。 |
CCR1 | 配置占空比。如上文所述,CNT到这个值的时候要改变输出电平 |
PSC | 配置TIM3的计数器的工作频率。如果你想要200kHz,就配置成SystemCoreClock/200k就可以了。 |
在初始化的时候需要考虑上面所述的所有的寄存器。但是在启动起来以后需要开启关闭、调节音量和频率,则只需要关注这4个寄存器。
外设寄存器 | 功能和用法 |
---|---|
CR1 | 启动或关闭定时器,无脑01操作,用于修改工作频率 |
ARR | 用于修改频率,无脑写数字 |
CCER | 启动或关闭PWM输出。无脑01操作。用于启动或关闭蜂鸣器发声、设置工作频率和音量 |
CCR1 | 用于修改音量和工作频率,无脑写数字 |
完整的工程文件在我的资源《基于HX32F4开发板的Buzzer控制程序》中。和前面的一样,新建工程,STM32F407VG,勾上Core、Keil RTX5 Lib,勾上Startup,Compiler IO Stdin和Stdout。把前面的代码加进来,再创建buzzer.h和buzzer.s。
头文件的定义buzzer结构体类型,主要用于给C调用。
#ifndef _BUZZER_H_
#define _BUZZER_H_
#include "stdint.h"
typedef struct {
void (*init)(void);
void (*set_state)(uint32_t);
void (*set_vol)(uint32_t);
void (*set_freq)(uint32_t);
}Buzzer_Def;
extern Buzzer_Def buzzer;
#endif
接下来是在buzzer.s中实现buzzer单类。这个文件分段来说。
;------------------------------------------------------------
;
; Filename: buzzer.s
; Author: 超级喵窝窝
;
;------------------------------------------------------------
; Buzzer is connected to PB4
; The AF of PB4: AF2: Tim3_CH1
get registers.s
get bus_clocks.s
rGPIOB rn r12
rRCC rn r11
rTIM3 rn r10
rBuzzer rn r9
macro
load_rRCC_to_r11
ldr rRCC, =RCC_BaseAddr
mend
macro
load_rGPIOB_to_r12
ldr rGPIOB, =GPIOB_BaseAddr
mend
macro
load_rTIM3_to_r10
ldr rTIM3, =TIM3_BaseAddr
mend
CK_INT equ 100000
CK_PWM_HIGH equ CK_INT / 2000 - 1 ; 49(0x31), 2kHz
CK_PWM_NORMAL equ CK_INT / 1500 - 1 ; 65(0x41), 1.5kHz
CK_PWM_LOW equ CK_INT / 700 - 1 ; 141(0x8d), 700Hz
VOL_HIGH equ 64 ; 50% duty cycle, 64/128
VOL_NORMAL equ 32 ; 25% duty cycle, 32/128
VOL_LOW equ 3 ; 2.5% duty cycle, about 3/128
一共定义了3个频率值所对应的ARR的值,和对应的占空比值。当然,为了便于计算,50%定义成了64/128,其他的类推。
接下来做一个实体类,用于存放正在运行的buzzer的信息。这里只保存频率和音量。
area BUZZER_DATA, readwrite
align 4
freq equ 0
vol equ freq + 1
Buzzer_Entity dcb CK_PWM_NORMAL, VOL_HIGH
这个就是很简单做个类似结构体的存在。相当于C中下面的代码。
typedef struct{
unsigned char freq;
unsigned char vol;
} Buzzer_Entity_Def;
Buzzer_Entity_Def Buzzer_Entity;
可以用这样的C语言进行赋值。
Buzzer_Entity.freq = 1;
Buzzer_Entity.vol = 1
那么对于汇编来说,我可以用类似下面的基地址+偏移量来进行访问。
macro
load_rBuzzer_Entity_to_r9
ldr rBuzzer, =Buzzer_Entity
mend
...
load_rBuzzer_Entity_to_r9
ldr r0, [r9, #freq]
ldr r1, [r9, #vol]
补充定义一下这个宏,用于加载Buzzer_Entity的首地址到r9中。前面已经将r9重命名成了rBuzzer。
macro
load_rBuzzer_Entity_to_r9
ldr rBuzzer, =Buzzer_Entity
mend
正如前面说的,就是配置一下RCC、GPIO和TIM的有关寄存器,实现功能。那些有名称的立即数,例如GPIO_AFRL_4_POS等,都是自己查手册将数据录入register.s中的。
area BUZZER_CODE, code
;-----------------------------------------------------------------
; Function Name : init
; Description : Initialise the output pins, TIM3 to generate
; a PWM signal to drive the buzzer.
;-----------------------------------------------------------------
align 4
init proc
push callee_regs
load_rRCC_to_r11
load_rGPIOB_to_r12
load_rTIM3_to_r10
; Enable the clocks for GPIOB and TIM3
ldr r0, [rRCC, #RCC_AHB1ENR]
orr r0, rRCC, #RCC_AHB1ENR_GPIOBEN
str r0, [rRCC, #RCC_AHB1ENR]
ldr r0, [rRCC, #RCC_APB1ENR]
orr r0, rRCC, #RCC_APB1ENR_TIM3EN
str r0, [rRCC, #RCC_APB1ENR]
; Configure the pins
ldr r0, [rGPIOB, #GPIO_MODER]
mov r1, #GPIO_MODER_AF
bfc r0, #8, #2
bfi r0, r1, #8, #2
str r0, [rGPIOB, #GPIO_MODER]
ldr r0, [rGPIOB, #GPIO_AFRL]
mov r1, #GPIO_AF_2
bfc r0, #GPIO_AFRL_4_POS, #GPIO_AF_LEN
bfi r0, r1, #GPIO_AFRL_4_POS, #GPIO_AF_LEN
str r0, [rGPIOB, #GPIO_AFRL]
ldr r0, [rGPIOB, #GPIO_PUPDR]
mov r1, #GPIO_PUPDR_PU
bfc r0, #GPIO_PUPDR_4_POS, #GPIO_PUPDR_LEN
bfi r0, r1, #GPIO_PUPDR_4_POS, #GPIO_PUPDR_LEN
str r0, [rGPIOB, #GPIO_PUPDR]
; Configure the TIM3
; The RCC of TIM3 is connected to APB1 bus.
; The frequency of APB1 is FREQ_APB1
; Configure the Internal Clock (CK_INT) to 100kHz
CK_INT_DIV equ FREQ_APB1 / CK_INT - 1
ldr r0, =CK_INT_DIV
str r0, [rTIM3, #TIM_PSC]
; Set the frequency to CK_PWM_NORMAL with duty cycle VOL_NORMAL
mov r0, #CK_PWM_NORMAL
str r0, [rTIM3, #TIM_ARR]
;初始化的时候把CL_PWM_NORMAL的值装载到ARR中。目前这个程序还不会记忆历史设置
mov r0, #((CK_PWM_NORMAL * VOL_NORMAL ):shr: 7) - 1; 根据占空比值算出要装载的CCR1值
str r0, [rTIM3, #TIM_CCR1]
ldr r0, [rTIM3, #TIM_CCMR1]
bfc r0, #TIM_CCMR1_OC1M_POS, #TIM_CCMR1_OC1M_LEN
mov r1, #TIM_CCMR_OCM_PWM1
bfi r0, r1, #TIM_CCMR1_OC1M_POS, #TIM_CCMR1_OC1M_LEN
str r0, [rTIM3, #TIM_CCMR1]
ldr r0, [rTIM3, #TIM_CR1]
orr r0, #TIM_CR1_CEN
str r0, [rTIM3, #TIM_CR1]
pop callee_regs
bx lr
endp
关于这段程序要说的是两个汇编指令:bfc和bfi。这两个指令可以用于清零寄存器32位数字中的一段连续的位。也可以把寄存器中的数装入另一个寄存器中的一段连续的位。非常适合GPIO_MODER、TIM3_CCMR1_OCM1这样的位的处理。
这个函数用于开关buzzer。只要是0就是关,否则就是开。
;-----------------------------------------------------------------
; Function Name : set_state
; Description : Turn on/off the buzzer.
;-----------------------------------------------------------------
align 4
set_state proc
push callee_regs
load_rTIM3_to_r10
ldr r1, [rTIM3, #TIM_CCER]
cmp r0, #0
ite gt
orrgt r1, #TIM_CCER_CC1E
bicle r1, #TIM_CCER_CC1E
str r1, [rTIM3, #TIM_CCER]
pop callee_regs
bx lr
endp
这里我尝试使用{TRUE}和{FALSE}内置常量代替#0,但是汇编器都不认。所以只能使用#0。
设置音量就是别的不变,只是改改CCR1的值。当然改之前要先把PWM关了。注意关了PWM就可以了,可以不用去关定时器。
;-----------------------------------------------------------------
; Function Name : set_vol
; Description : Set the volumne of the buzzer.
;-----------------------------------------------------------------
align 4
set_vol proc
; r0 holds the value of vol. Do not overwrite.
; Just change the duty cycle according to r0.
; 0 is low, 1 is normal and 2 is high. Simple.
; Based on the current TIM->ARR value , calculate the CCR1 value.
push callee_regs
load_rTIM3_to_r10
load_rBuzzer_Entity_to_r9
ldr r1, [rTIM3, #TIM_CCER]
bic r1, #TIM_CCER_CC1E
str r1, [rTIM3, #TIM_CCER]
cmp r0, #2
movgt r0, #2
ldr r1, =Volunme_Table
ldrb r2, [r1, r0]
strb r2, [rBuzzer, #vol]
ldrb r1, [rBuzzer, #freq]
mul r1, r2, r1
lsr r1, #7 ; Divided by 128(0x80)
str r1, [rTIM3, #TIM_CCR1]
ldr r1, [rTIM3, #TIM_CCER]
orr r1, #TIM_CCER_CC1E
str r1, [rTIM3, #TIM_CCER]
pop callee_regs
bx lr
Volunme_Table
dcb VOL_LOW, VOL_NORMAL, VOL_HIGH
endp
这里注意这2句.
cmp r0, #2
movgt r0, #2
mov是可以在IT块外进行条件判定的。
再注意这2句:
ldr r1, =Volunme_Table
ldrb r2, [r1, r0]
ldr族的指令是可以进行偏移量加载的。利用这个特性可以避免使用TBB这种跳转赋值指令。
用于设置频率。这里注意,设置频率需要修改ARR的值,那么为了保证音量不变,则必须修改CCR1的值。其实就是折腾一下ARR和CCR1而已。当然,折腾他们前记得把PWM和定时器都关了。用到的手法和改音量一样,只不过就这次要改两个寄存器的值而已。
;-----------------------------------------------------------------
; Function Name : set_freq
; Description : Set the frequency of the buzzer.
;-----------------------------------------------------------------
align 4
set_freq proc
; r0 hold the value of required frequency level.
; Simple. 0 is the low , 1 is normal and 2 is high.
; Change the frequency of pwm.
; Re-calculate the value of TIM-ARR.
; Re-calculate the vlaue of TIM-CCR1
push callee_regs
load_rTIM3_to_r10
load_rBuzzer_Entity_to_r9
ldr r1, [rTIM3, #TIM_CCER]
bic r1, #TIM_CCER_CC1E
str r1, [rTIM3, #TIM_CCER]
ldr r1, [rTIM3, #TIM_CR1]
bic r1, #TIM_CR1_CEN
str r1, [rTIM3, #TIM_CR1]
cmp r0, #2
movgt r0, #2
ldr r1, =Frequency_Table
ldrb r2, [r1,r0]
strb r2, [rBuzzer, #freq]
str r2, [rTIM3, #TIM_ARR]
ldrb r1, [rBuzzer, #vol]
mul r1, r2, r1
lsr r1, #7 ;Devided by 128
str r1, [rTIM3, #TIM_CCR1]
ldr r1, [rTIM3, #TIM_CR1]
orr r1, #TIM_CR1_CEN
str r1, [rTIM3, #TIM_CR1]
ldr r1, [rTIM3, #TIM_CCER]
orr r1, #TIM_CCER_CC1E
str r1, [rTIM3, #TIM_CCER]
pop callee_regs
bx lr
Frequency_Table
dcb CK_PWM_LOW, CK_PWM_NORMAL, CK_PWM_HIGH
endp
所有该做的函数都做好了。整合封装一下就可以了。
align 4
buzzer
export buzzer
dcd init, set_state, set_vol, set_freq
end
做个测试用例线程看看这个buzzer单类是否可以正常工作。用new item_User Code Template_CMSIS_CMSIS-RTOS2 Thread做一个测试用例,再把代码改改就行了。如下所示。
#include "cmsis_os2.h" // CMSIS RTOS header file
/*----------------------------------------------------------------------------
* Thread 1 'Thread_Name': Sample thread
*---------------------------------------------------------------------------*/
#include "bsp.h"
#include "cmsis_os2.h"
#include "rtx_os.h"
#define STACK_SIZE (64/4)
static osRtxThread_t cb_mem;
static uint64_t tcc_stack[STACK_SIZE];
static osThreadId_t tid_test_case_c; // thread id
extern int Init_test_case_c (void);
void test_case_c (void *argument); // thread function
static osThreadAttr_t attrTCC = {
.name = "tTCC",
.attr_bits = osThreadDetached,
.cb_mem = &cb_mem,
.cb_size = osRtxThreadCbSize,
.priority = osPriorityNormal1,
.stack_mem = tcc_stack,
.stack_size = sizeof(tcc_stack),
};
int Init_test_case_c (void) {
tid_test_case_c = osThreadNew(test_case_c, (void *)0x00, &attrTCC);
if (tid_test_case_c == NULL) {
return(-1);
}
return(0);
}
__NO_RETURN void test_case_c (void *argument) {
buzzer.set_vol(0);
buzzer.set_freq((uint32_t)argument);
while (1) {
buzzer.set_state(1);
osDelay(10);
buzzer.set_state(0);
osDelay(500);
}
}
这里注意那个uint64_t其实是typedef unsigned __INT64 uint64_t;
这样定义的。其实是个无符号整型数字而已。
关于这个KEIL RTX5系统其实很有意思的。如果你不用那个浮点,说是最小只要64个字节的任务栈。否则至少200字节。这真的是个很小的系统哈哈。但是在使用了 “stdio.h” 里的函数后就必须把这个最小任务栈提高到256以上。我还没有找到相关的文档说明stdio里面的函数到底需要多少内存才能运行。
需要配置RCC、GPIO、TIM3实现PWM,就可以输出脉冲驱动蜂鸣器。
一个很小的系统,但是看起来运行着挺开心的。
其实文中也说了,这里再来一遍。完整的工程文件在我的资源《基于HX32F4开发板的Buzzer控制程序》中。
有关的开发文档在我的资源里《常用的开发文档-ARM有关的手册 》。
1、ARM Architecture Reference Manual Thumb-2 Supplement.pdf
2、DDI0403E_e_armv7m_arm.pdf
3、DUI0068ARM® Developer Suite_Assemble guide.pdf
4、DUI0473C_using_the_arm_assembler.pdf
5、DUI0474F_using_the_arm_linker.pdf
6、rm0090-stm32f405415-stm32f407417-stm32f427437-and-stm32f429439-advanced-armbased-32bit-mcus-stmicroelectronics.pdf
7、stm32f407vg.pdf