KEIL 5.38的ARM-CM3/4 ARM汇编设计学习笔记5——PWM输出控制蜂鸣器

一、介绍

考虑基于前面设计的程序实现一个蜂鸣器的驱动。
蜂鸣器部分的电路是:
在这里插入图片描述
蜂鸣器看起来可以被叫做beep或buzzer。这里我在程序上使用的名称是buzzer。这个Buzzer连在了PB4上面的。

1.1 运行环境介绍

环境描述
MCUSTM32F407VGT6
IDEKeil mdk
RTOSKeil RTX5
串口继承前面的设计没有改变。USART1,RX: PA9,TX:PA10,Baudrate 115200
蜂鸣器BuzzerPB4(TIM3_CH1)

1.2 功能需求

一共有3个功能要做:

  1. 启动或关闭蜂鸣器。
  2. 设置蜂鸣器的频率。一共就是3个档,高中低,各挡位的频率大小值自定。
  3. 设置蜂鸣器的音量。一共3个档,高中低,各挡位的音量值大小自定。

1.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。

2.1 buzzer.h

头文件的定义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

2.2 buzzer.s

接下来是在buzzer.s中实现buzzer单类。这个文件分段来说。

2.2.1 一些寄存器重命名、文内关键宏定义 和一些常量命名。

;------------------------------------------------------------
;		
;		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,其他的类推。

2.2.2 Buzzer_Entity

接下来做一个实体类,用于存放正在运行的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]
	

2.2.3 load_rBuzzer_Entity_to_r9汇编宏定义

补充定义一下这个宏,用于加载Buzzer_Entity的首地址到r9中。前面已经将r9重命名成了rBuzzer。

		macro
		load_rBuzzer_Entity_to_r9
		ldr 	rBuzzer, =Buzzer_Entity
		mend

2.2.4 void init(void)函数

正如前面说的,就是配置一下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这样的位的处理。

2.2.5 void set_state(uint32_t)函数

这个函数用于开关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。

2.2.6 void set_vol(uint32_t)函数

设置音量就是别的不变,只是改改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这种跳转赋值指令。

2.2.7 void set_freq(uint32_t)函数

用于设置频率。这里注意,设置频率需要修改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  

2.2.8 buzzer单类的封装

所有该做的函数都做好了。整合封装一下就可以了。

				align 4
buzzer			
				export 		buzzer
				dcd			init, set_state, set_vol, set_freq
				end

2.3 测试用例

做个测试用例线程看看这个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里面的函数到底需要多少内存才能运行。

三、体会

3.1 关于蜂鸣器

需要配置RCC、GPIO、TIM3实现PWM,就可以输出脉冲驱动蜂鸣器。

3.2 关于KEIL RTX5

一个很小的系统,但是看起来运行着挺开心的。

3.3 完整的工程

其实文中也说了,这里再来一遍。完整的工程文件在我的资源《基于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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值