基于RP2350B制作音乐播放器
该项目使用了RP2350B核心板,实现了PWM音乐播放器的设计,它的主要功能为:利用PWM引脚控制蜂鸣器进行音乐播放,用OLED显示屏显示歌曲名字,并且可以使用按钮进行音乐切换和选择。
标签
嵌入式系统
显示
RP2350B
一颗星
更新2025-07-08
沈阳理工大学
258

1、项目介绍

很荣幸能够参加本次由硬禾推出的全新RP2350B核心板试用活动,这不仅是一次宝贵的学习机会,也为我深入了解并实践嵌入式系统开发提供了良好平台。在本项目中,我选择使用RP2350B核心板来完成一个PWM音乐播放器的制作,利用板载的PWM功能实现音符的频率控制,结合简单的控制逻辑播放旋律。通过该项目,我不仅加深了对RP2350B硬件资源的理解,也提升了动手实践与软硬件协同开发的能力。完成的主要内容如下:

  1. 通过PWM产生不同的音调,并驱动板上蜂鸣器将音调输出
  2. 能够播放三首不同的曲子,可以切换播放
  3. 曲子的切换使用核心板上的按键,具有按键消抖的功能
  4. 播放的曲子的名字在OLED屏幕上显示出来(汉字显示)

2、硬件介绍

本项目使用到了RP2350B核心板以及与其搭配的综合训练板。RP2350具有强大性能 - 双核Arm处理器 + 双核RISC-V处理器,以及可编程IO(PIO),该芯片不仅支持通用的外设总线(I2C、SPI以及UART)访问,还可以通过适当的配置让PIO访问高速外设,在很多场景下能够完成FPGA才能实现的功能。相对于RP2040和RP2350A只有30个IO,RP2350B拥有48个IO,这增加的18个IO就可以通过合理的配置,可以访问更多的外设。本项目使用到了RP2350B核心板上的四个按钮,用于进行歌曲的切换和选择,RP2350B核心板如下图所示:

RP2350B核心板


为了完成该项目,使用到的综合训练板上的硬件包括:

  1. 128*32分辨率的OLED单色显示屏,通过SPI总线进行访问,可以显示文本和图形化信息
  2. 蜂鸣器,使用PWM驱动

综合训练板如下图所示:

综合训练扩展板


3、设计思路

首先制作两个数据库,分别为字符库和音乐库。其中,字符库包含所要播放音乐的中文名字,用于显示到OLED显示屏上,音乐库包含要播放音乐的音符频率和时间,通过PWM控制蜂鸣器进行音乐的播放。RP2350B读取到文件内容后,通过使用PWM驱使蜂鸣器进行音乐的播放,使用SPI给显示屏传输信息,进行歌曲名字的显示,同时通过利用RP2350B核心板上带有的使用ADC通信的按键,完成歌曲的切换和选择,其设计方案框图如下。


设计方案框图


4、软件流程图

本项目通过引入状态机编程思想,实现了多首歌曲的自动循环播放与按键控制切换功能,充分发挥了嵌入式系统在逻辑控制方面的优势。状态机共设计了四种状态,分别对应歌曲1、歌曲2、歌曲3以及一个专门用于切换的“歌曲切换状态”。在默认情况下,也就是没有检测到按键输入时,系统会根据预设的逻辑在歌曲1、2、3这三个播放状态之间进行自动切换,从而实现歌曲的连续循环播放。当用户按下按键时,系统立即响应,跳转至“歌曲切换状态”,并根据按键输入判断切换逻辑,快速过渡到下一首歌曲的播放状态。这种设计不仅提高了播放器的交互性和响应速度,也增强了系统的可控性与用户体验。整个状态机的实现简洁高效,逻辑清晰,便于维护与扩展,其软件流程图如下。


程序流程框图


5、关键代码介绍

(1)使用PWM进行音乐播放,通过设置PWM的频率和占空比以及播放延时来完成音符的播放

void my_pwm_init(){
gpio_set_function(20, GPIO_FUNC_PWM);
uint slice_num = pwm_gpio_to_slice_num(20);
pwm_set_clkdiv(slice_num, 125.0f);  
}

void my_pwm_work(int frequency){
    uint wrap = ((uint)(1000000/frequency) -1);
    uint duty = wrap/2;
    uint slice_num = pwm_gpio_to_slice_num(20);
    // 设置wrap为124,则周期为125个时钟
    pwm_set_wrap(slice_num, wrap);
    // 设置占空比为50% -> level为62
    pwm_set_chan_level(slice_num, pwm_gpio_to_channel(20), duty);
    pwm_set_enabled(slice_num, true);
}

void play_note(Note note) {
    my_pwm_work(note.freq);  // 你写的函数
    pwm_set_enabled(pwm_gpio_to_slice_num(20), true);
    sleep_ms(note.duration_ms);    // 延时播放
    pwm_set_enabled(pwm_gpio_to_slice_num(20), false);
    sleep_ms(50); // 简单的停顿
}

(2)使用SPI通信协议驱动OLED显示屏。

void oled_cmd(uint8_t cmd) {
    gpio_put(OLED_DC, 0);
    gpio_put(OLED_CS, 0);
    spi_write_blocking(SPI_PORT, &cmd, 1);
    gpio_put(OLED_CS, 1);
}

void oled_data(const uint8_t *data, size_t len) {
    gpio_put(OLED_DC, 1);
    gpio_put(OLED_CS, 0);
    spi_write_blocking(SPI_PORT, data, len);
    gpio_put(OLED_CS, 1);
}

void oled_init() {
    gpio_put(OLED_RES, 0);
    sleep_ms(50);
    gpio_put(OLED_RES, 1);


    for (size_t i = 0; i < sizeof(oled_init_cmds); i++) {
        oled_cmd(oled_init_cmds[i]);
    }
}

void oled_set_cursor(uint8_t page, uint8_t column) {
    oled_cmd(0xB0 | page);                     // Page address
    oled_cmd(0x00 | (column & 0x0F));          // Lower column
    oled_cmd(0x10 | ((column >> 4) & 0x0F));   // Higher column
}
void oled_clear() {
    for (int page = 0; page < 4; page++) {  // 128x32 => 4页,每页8像素高
        oled_set_cursor(page, 0);
        for (int col = 0; col < 128; col++) {
            uint8_t zero = 0x00;
            oled_data(&zero, 1);
        }
    }
}

void display_word(int colum,const uint8_t data[32]){
    uint8_t up[16];
    uint8_t down[16];
    int temp=0;
    for(int i = 0; i < 16; i++){
       temp=2*i;
       up[i] = data[temp];
       temp=2*i+1;
       down[i] = data[temp];
    }
    temp = colum*16;
    oled_set_cursor(1,temp);
    oled_data(up, 16);
    oled_set_cursor(2,temp);
    oled_data(down, 16);
}

void display_sentence(int data[]){
    int index=0;
    int i = 0;
    while (data[i] != -1) {  // 用 -1 作为终止标志
        int index = data[i];
        display_word(i, font_13[index]);
        i++;
    }
}

(3)使用按钮进行状态选择

Song_State choose_song(float voltage,Song_State current){
    if (voltage > 3.0f) {      
        return current;
        }
        else if (voltage > 2.8f) {
            return current;           
        }
        else if (voltage > 2.5f) {
            return Song_three;           
        } else if (voltage > 2.0f) {       
           return Song_two;           
        } else if (voltage > 1.5f) {
            return Song_one;   
        } else {         
          return current;          
        }  
}

(4)主程序调用函数

int main() {
    stdio_init_all();
    my_pwm_init();
    adc_init();
    adc_gpio_init(47);
    adc_select_input(7);
   
    // SPI 初始化
    spi_init(SPI_PORT, 1 * 1000 * 1000);  // 1MHz
    gpio_set_function(PIN_SCK, GPIO_FUNC_SPI);
    gpio_set_function(PIN_MOSI, GPIO_FUNC_SPI);
    // 控制引脚初始化
    gpio_init(OLED_CS);
    gpio_init(OLED_DC);
    gpio_init(OLED_RES);
    gpio_set_dir(OLED_CS, GPIO_OUT);
    gpio_set_dir(OLED_DC, GPIO_OUT);
    gpio_set_dir(OLED_RES, GPIO_OUT);
    gpio_put(OLED_CS, 1);
    // 初始化 OLED
    oled_init();
    oled_clear();
    //显示句子
    int array_1[6]={0,1,2,2,3,-1};
    int array_2[8]={0,4,5,6,7,8,3,-1};
    int array_3[7]={0,9,10,11,12,3,-1};
Song_State STATE;
Song_State temp;
uint16_t result=0;
float voltage =0;
result = adc_read();
voltage = result * 3.3f / 4095;
while(true){
    oled_clear();
    switch (STATE)
    {
    case 0:
        display_sentence(array_1);
            for (int i = 0; i < sizeof(twinkle_twinkle)/sizeof(Note); i++) {
        result = adc_read();
        voltage = result * 3.3f / 4095;
        if (voltage < 3.0f) {
            break;
        }
        play_note(twinkle_twinkle[i]);}
            STATE=1;


        break;
    case 1:
        display_sentence(array_2);
                for (int i = 0; i < sizeof(happy_birthday)/sizeof(Note); i++) {
        result = adc_read();
        voltage = result * 3.3f / 4095;
        if (voltage < 3.0f) {
            break;
        }
        play_note(happy_birthday[i]);}
        STATE=2;
       
        break;
    case 2:
        display_sentence(array_3);
                for (int i = 0; i < sizeof(liangzhi_laohu)/sizeof(Note); i++) {
        result = adc_read();
        voltage = result * 3.3f / 4095;
        if (voltage < 3.0f) {
            break;
        }
        play_note(liangzhi_laohu[i]);}
        STATE=0;
        break;
   
    default:
        result = adc_read();
        if (voltage < 3.0f) {
            break;
        }
        voltage = result * 3.3f / 4095;
        break;
    }
    temp=STATE;
    STATE = choose_song(voltage,temp);
    sleep_ms(1000);
}
}

6、实物功能展示图及说明

该项目完成后的效果是,在没有按键按下时,循环播放《小星星》《生日快乐歌》《两只老虎》这三首歌,并将歌曲名字显示出来。一共有四个按钮,最左边的按钮是切换到下一首歌,剩下三个分别为这三首歌的单独选择按键。实物效果如下图。

播放生日快乐歌

播放两只老虎

播放小星星


7、项目中遇到的难题和解决方法

在实现歌曲名称显示功能时,为了提升可读性,我尝试将字体放大,采用了16×16像素的点阵字体。然而,这种字体超出了屏幕默认的8位数据传输范围,导致初始显示时屏幕出现乱码。经过分析和调试,最终通过将每个字符拆分为上下两个8×16像素的部分,分两次传输到屏幕上,成功解决了乱码问题,保证了大字体的正确显示效果。


8、对本次活动的心得体会

很高兴能够参加硬禾学堂组织的本次活动,通过参加本次项目,我不仅深入学习了RP2350B核心板的使用方法,也提升了自己在嵌入式系统方面的实践能力。项目中涉及的状态机控制、PWM输出以及OLED显示等内容让我对软硬件结合有了更深刻的理解。这次经历极大地增强了我的动手能力和解决问题的信心,同时希望以后可以参加更多这样有意义的活动。




附件下载
spi_master.rar
可执行程序
团队介绍
一颗星
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号