自学内容网 自学内容网

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 定时器 来调节蜂鸣器的频率,使其发出不同的音调
#defineGPD0CON    *(volatile long*)0x114000A0    // GPD0CON 寄存器的地址,用于配置 GPD0_0 引脚为 PWM 输出模式
#defineTCFG0*(volatile long*)0x139D0000    // TCFG0 寄存器的地址,用于设置 PWM 定时器的预分频值
#defineTCFG1*(volatile long*)0x139D0004  // TCFG1 寄存器的地址,用于设置 PWM 定时器的分频值
#defineTCON*(volatile long*)0x139D0008  // TCON 寄存器的地址,用于控制定时器的启动、停止等操作
#defineTCNTB0*(volatile long*)0x139D000C  // TCNTB0 寄存器的地址,用于设置定时器的周期(PWM 周期)
#defineTCMPB0*(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:
breset                  @ 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: 
  ldrr0, =0x40008000       @ 设置异常向量表的起始地址为 0x40008000
  mcrp15, 0, r0, c12, c0, 0 @ 将 r0 的值写入 CP15 协处理器的 Vector Base Address Register (VBAR),以设定向量表的地址
  
  ldrr0, =stacktop          @ 获取栈顶地址,stacktop 是一个定义好的符号,r0 中保存栈顶指针
/******** svc 模式栈 ********/
movsp, r0               @ 将栈顶指针装载到 SP 中,切换到 svc 模式
subr0, #128*4            @ 为 irq 模式保留 512 字节的栈空间
/**** irq 模式栈 ***/
msrcpsr, #0xd2           @ 切换到 irq 模式
movsp, r0               @ 设置 irq 模式的栈指针
subr0, #128*4            @ 为 irq 模式保留 512 字节的栈空间
/*** fiq 模式栈 ***/
msr cpsr, #0xd1           @ 切换到 fiq 模式
movsp, r0               @ 设置 fiq 模式的栈指针
subr0, #0                @ 为 fiq 模式保留 0 字节的栈空间
/*** abort 模式栈 ***/
msrcpsr, #0xd7           @ 切换到 abort 模式
movsp, r0               @ 设置 abort 模式的栈指针
subr0, #0                @ 为 abort 模式保留 0 字节的栈空间
/*** undefined 模式栈 ***/
msrcpsr, #0xdb           @ 切换到 undefined 模式
movsp, r0               @ 设置 undefined 模式的栈指针
subr0, #0                @ 为 undefined 模式保留 0 字节的栈空间
   /*** sys 模式和 usr 模式栈 ***/
msrcpsr, #0x10           @ 切换到 sys/usermode 模式
movsp, r0               @ 设置 user 模式的栈指针,预留 1024 字节空间

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

.align4                      @ 保证对齐到 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烧录进入开发板即可

原文地址:https://blog.csdn.net/m0_51830537/article/details/142848187

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!