ARM编程--->控制PWM波产生音乐

定时器与PWM波

  1. 晶振的作用
  • 晶振(Crystal Oscillator)用于提供一个稳定的时钟信号,帮助处理器和其他硬件设备衡量时间流逝。晶振的频率决定了时钟信号的速率。例如,8 MHz 的晶振每秒产生 8,000,000 次振荡。这些振荡信号用于驱动处理器的内部时钟和定时器模块。
  1. 定时器的工作原理
  • 定时器计数:定时器会通过每次时钟信号来递增或递减计数器(TCNT)。当计数器达到设定的周期(TCNTB),就会发出一个中断信号。定时器的工作周期取决于分频器的值以及晶振的频率。
  • 分频器:分频器用于将时钟信号的频率进行缩减,比如从 64 MHz 缩减到 1 KHz,最终生成一个 1 ms 的时钟周期,用于定时器计数。
  • 定时周期:定时器会从 0 计数到设定的周期值,然后触发中断。当定时器达到设定的周期值时,计数器重置并开始新一轮计数。
  1. 定时器与 PWM 结合
  • PWM(Pulse Width Modulation) 是一种常用于调节设备(如灯光、马达)输出强度的技术,利用定时器生成脉冲波,控制输出信号的占空比。
  • PWM 的基本原理:通过改变脉冲的占空比,可以调节输出信号的强弱。占空比是指信号为高电平的时间与总周期的比值。
  • PWM 的工作过程:
    • 设定两个值:周期(TCNTB)和比较值(TCMPB)。
    • 当定时器计数器的值达到周期值时,计数器重置并开始新一轮计数;当计数器值达到比较值时,输出电平改变。
    • 通过修改比较值 TCMPB 与周期值 TCNTB 之间的关系,可以改变 PWM 波形的占空比。
控制蜂鸣器产生音乐流程
  1. 初始化GPIO为PWM输出

    GPD0CON &= ~0xF;    // 清除 GPD0_0 的配置位
    GPD0CON |= 1<<1;    // 将 GPD0_0 配置为 PWM 输出(TOUT_0)
    
    • GPD0CON:用于控制 GPD0_0 管脚的功能,通过设置 0x2,将其配置为 PWM 输出模式。
  2. PWM定时器配置

    TCFG0 &= ~0xFF;     // 清除 TCFG0 的前8位
    TCFG0 |= 124;       // 设置预分频器为 124
    TCFG1 &= ~0xF;      // 清除 TCFG1 的前4位
    TCFG1 |= 3;         // 设置分频器为 1/8
    TCNTB0 = 100;       // 设置定时器周期寄存器的初始值为 100
    TCMPB0 = 30;        // 设置比较寄存器的值为 30
    
    • TCFG0 和 TCFG1:分别用于设置预分频和分频器。预分频器设置为 124,分频器设置为 1/8。
    • TCNTB0 和 TCMPB0:这两个寄存器用于设定 PWM 波形的周期和占空比。周期寄存器(TCNTB0)决定 PWM 信号的周期,而比较寄存器(TCMPB0)决定高电平时间的长短,从而控制占空比。
  3. 控制 PWM 定时器

    • 定时器初始化
    TCON |= 1<<3;       // 设置定时器为自动重载模式
    TCON &= ~(1<<2);    // 设置不翻转电平
    TCON |= 1<<1;       // 更新 TCNTB 和 TCMPB 到 TCNT 和 TCMP
    TCON &= ~(1<<1);    // 清除更新位
    
    
    • 定时器的启动与停止
    TCON |= 1<<0;       // 启动定时器
    TCON &= ~(1<<0);    // 停止定时器
    
    • TCON:用于控制定时器的状态(启动、停止、更新、翻转等)。当设置 TCON 的第 1 位时,定时器会更新 TCNT 和 TCMP 的值,并且立即清除此位以确保正确更新。
  4. 蜂鸣器频率控制

    char yf[] = {0,191,170,152,143,128,114,101};  // 不同频率对应的 PWM 周期值
    
    • yf 数组:存储了蜂鸣器不同音调对应的 PWM 周期值。程序通过循环修改定时器的周期值和占空比,产生不同频率的 PWM 信号,控制蜂鸣器发出不同频率的声音。
  5. 延时函数

    void mdelay(int msec)
    {
        while(msec--){
            int cnt = 0x1fff/3;
            while(cnt--);
        }
    }
    
    • mdelay():用于产生延时,通过空循环实现。在不同频率的声音之间加入延时,以便让声音可以持续一段时间。
  6. 主函数逻辑

    int main()
    {
        timer_init();   // 初始化定时器
    
        while(1){
            int i;
            timer_start();  // 启动定时器
            for(i = 1; i <= 7; i++){
                TCNTB0 = yf[i];        // 修改 PWM 周期
                TCMPB0 = yf[i] / 2;    // 修改 PWM 占空比
                mdelay(500);           // 等待 500ms,保持蜂鸣器声音
            }
            timer_stop();   // 停止定时器
            mdelay(2 * 1000);  // 停止后等待 2 秒
        }
    
        return 0;
    }
    
    • 调用 timer_init() 初始化定时器。
    • 进入主循环,通过循环设置不同的 PWM 周期和占空比(从 yf 数组中取值)。
    • 使用 timer_start() 启动定时器,蜂鸣器开始发声。
    • 改变 PWM 的周期值和占空比,产生不同的音调。
    • 延时 500 毫秒后,停止定时器并延时 2 秒,再重新开始新的音调。
  7. 整体代码如下

//控制 PWM 定时器 来调节蜂鸣器的频率,使其发出不同的音调
#define	GPD0CON    	*(volatile long*)0x114000A0    // GPD0CON 寄存器的地址,用于配置 GPD0_0 引脚为 PWM 输出模式
#define	TCFG0		*(volatile long*)0x139D0000    // TCFG0 寄存器的地址,用于设置 PWM 定时器的预分频值
#define	TCFG1		*(volatile long*)0x139D0004	  // TCFG1 寄存器的地址,用于设置 PWM 定时器的分频值
#define	TCON		*(volatile long*)0x139D0008	  // TCON 寄存器的地址,用于控制定时器的启动、停止等操作
#define	TCNTB0		*(volatile long*)0x139D000C	  // TCNTB0 寄存器的地址,用于设置定时器的周期(PWM 周期)
#define	TCMPB0		*(volatile long*)0x139D0010	  // TCMPB0 寄存器的地址,用于设置 PWM 占空比

void do_irq(void)
{
	// 中断服务函数,在该代码中并没有实际使用,可以用来处理定时器的中断事件。
}

void timer_init(void)
{
	GPD0CON &= ~0xF;                // 清除 GPD0_0 的配置位,确保只修改低 4 位
	GPD0CON |= 1<<1;                // 将 GPD0_0 引脚配置为 TOUT_0(PWM 输出模式)

	TCFG0 &= ~0xFF;                 // 清除 TCFG0 的前 8 位,确保只修改预分频值
	TCFG0 |= 124;                   // 设置预分频器值为 124,降低定时器输入时钟频率

	TCFG1 &= ~0xF;                  // 清除 TCFG1 的前 4 位,确保只修改分频值
	TCFG1 |= 3;                     // 设置分频器为 1/8

	TCNTB0 = 100;                   // 设置定时器周期寄存器 TCNTB0 的值为 100,这定义了 PWM 信号的总周期
	TCMPB0 = 30;                    // 设置比较寄存器 TCMPB0 的值为 30,定义占空比,即高电平时间为总周期的 30%

	TCON |= 1<<3;                   // 设置 TCON 的第 3 位,启用自动重载模式(PWM 波形在到达周期后自动重载)
	TCON &= ~(1<<2);                // 清除 TCON 的第 2 位,设置为不翻转电平

	TCON |= 1<<1;                   // 设置 TCON 的第 1 位,将 TCNTB 和 TCMPB 的值更新到 TCNT 和 TCMP
	TCON &= ~(1<<1);                // 清除 TCON 的第 1 位,确保更新操作完成后立即清零,以防止重复更新
}

void timer_start(void)
{
	TCON |= 1<<0;                   // 设置 TCON 的第 0 位,启动定时器
}

void timer_stop(void)
{
	TCON &=  ~(1<<0);               // 清除 TCON 的第 0 位,停止定时器
}

char yf[] = {0,191,170,152,143,128,114,101};  // 预定义的周期值数组,控制蜂鸣器发出不同频率的声音。每个值代表不同的 PWM 周期

void mdelay(int msec)
{
	while(msec--){                  // 循环 msec 次,产生延时
		int cnt = 0x1fff/3;          // 内部计数器,控制更短时间的延时
		while(cnt--);                // 空循环实现短时间的延时
	}
}

int main()
{
	timer_init();                   // 调用定时器初始化函数,设置 PWM 输出

	while(1){                       // 主循环,持续输出不同频率的声音
		int i;
		timer_start();               // 启动定时器,开始输出 PWM 信号

		for(i = 1; i <= 7; i++){     // 循环遍历 yf 数组中的频率值,从索引 1 到 7
			TCNTB0 = yf[i];          // 修改定时器的周期寄存器 TCNTB0,设置不同的 PWM 周期
			TCMPB0 = yf[i] / 2;      // 修改比较寄存器 TCMPB0,设置占空比为周期的一半
			mdelay(500);             // 等待 500 毫秒,保持当前频率的声音
		}

		timer_stop();                // 停止定时器,停止 PWM 信号输出
		mdelay(2 * 1000);            // 等待 2 秒,然后再次开始新的声音周期
	}

	return 0;
}

补充文件
  1. start.S
.global  delay1s          @ 定义全局符号 delay1s,表示延迟函数的入口点
.text                     @ 表示代码段的开始
.global _start            @ 定义全局符号 _start,程序的入口点

_start:
		b		reset                  @ 0x00处的指令,直接跳转到 reset,程序复位时从 reset 开始
		nop                          @ 0x04 处的空操作,占位符,用于延迟
		nop                          @ 0x08 处的空操作
		nop                          @ 0x0C 处的空操作
		nop                          @ 0x10 处的空操作
		nop                          @ 0x14 处的空操作
		ldr 	pc,_irq               @ 0x18 处加载_irq的地址到程序计数器 PC,用于处理中断
		nop                          @ 0x1C 处的空操作

_irq:					
	.word  irq_handler       @ 定义 _irq 的地址,即中断发生时,跳转到 irq_handler 处理
	@.word 0x12345678        @ 可选的字数据,用于测试或调试

irq_handler:		        @ 中断处理程序的入口
    sub  lr, lr, #4         @ 调整 LR 寄存器,LR 是中断返回地址,需要减去 4 才能指向中断发生前的指令
    stmfd  sp!, {r0-r12, lr} @ 将通用寄存器 r0-r12 和链接寄存器 lr 压入栈中,保存现场
    bl   do_irq             @ 调用 C 函数 do_irq 进行中断处理
    ldmfd  sp!, {r0-r12, pc}^ @ 从栈中恢复 r0-r12 和 PC 寄存器的值,恢复现场,并返回到中断发生前的位置

reset: 
	  ldr	r0, =0x40008000       @ 设置异常向量表的起始地址为 0x40008000
	  mcr	p15, 0, r0, c12, c0, 0 @ 将 r0 的值写入 CP15 协处理器的 Vector Base Address Register (VBAR),以设定向量表的地址
	  
	  ldr		r0, =stacktop          @ 获取栈顶地址,stacktop 是一个定义好的符号,r0 中保存栈顶指针
	/******** svc 模式栈 ********/
		mov		sp, r0               @ 将栈顶指针装载到 SP 中,切换到 svc 模式
		sub		r0, #128*4            @ 为 irq 模式保留 512 字节的栈空间
	/**** irq 模式栈 ***/
		msr		cpsr, #0xd2           @ 切换到 irq 模式
		mov		sp, r0               @ 设置 irq 模式的栈指针
		sub		r0, #128*4            @ 为 irq 模式保留 512 字节的栈空间
	/*** fiq 模式栈 ***/
		msr 	cpsr, #0xd1           @ 切换到 fiq 模式
		mov		sp, r0               @ 设置 fiq 模式的栈指针
		sub		r0, #0                @ 为 fiq 模式保留 0 字节的栈空间
	/*** abort 模式栈 ***/
		msr		cpsr, #0xd7           @ 切换到 abort 模式
		mov		sp, r0               @ 设置 abort 模式的栈指针
		sub		r0, #0                @ 为 abort 模式保留 0 字节的栈空间
	/*** undefined 模式栈 ***/
		msr		cpsr, #0xdb           @ 切换到 undefined 模式
		mov		sp, r0               @ 设置 undefined 模式的栈指针
		sub		r0, #0                @ 为 undefined 模式保留 0 字节的栈空间
   /*** sys 模式和 usr 模式栈 ***/
		msr		cpsr, #0x10           @ 切换到 sys/usermode 模式
		mov		sp, r0               @ 设置 user 模式的栈指针,预留 1024 字节空间

		b		main                 @ 跳转到 main 函数,开始执行主程序

	.align	4                      @ 保证对齐到 4 字节边界,优化存储器访问

.data

stack:	
  .space  4*512               @ 为所有模式分配 512 字节栈空间
stacktop:

.end                           @ 程序结束

  1. map.lds
  2. Makefile
all:
	arm-none-linux-gnueabi-gcc -fno-builtin -nostdinc -c -o start.o start.S
	arm-none-linux-gnueabi-gcc -fno-builtin -nostdinc -c -o main.o main.c
	arm-none-linux-gnueabi-ld start.o main.o -Tmap.lds -o main.elf
	arm-none-linux-gnueabi-objcopy -O binary  main.elf main.bin
	arm-none-linux-gnueabi-objdump -D main.elf > main.dis
clean:
	rm -rf *.bak *.o  *.elf  *.bin  *.dis
利用Makefile产生的bin烧录进入开发板即可
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值