用RP2350B制作的音乐播放器
该项目使用了RP2350B,实现了midi音乐播放的设计,它的主要功能为:使用RP2350B的pwm功能,使用蜂鸣器播放音乐。
标签
嵌入式系统
测试
显示
开发板
aramy
更新2025-07-11
299

一、硬件介绍:
2024年8月,树莓派PICO2 发布,这是树莓派的第二代微控制器板,基于 RP2350 设计的新型高性能、安全的微控制器。它有着更高的内核时钟速度、两倍的内存、更强大的 ARM 内核、新的安全功能和升级的接口功能,Pico 2 提供了显著的性能和功能提升,同时保留了与 Pico 系列早期成员的硬件和软件兼容性。这次电子森林推出的STEP_RP2350B核心板,使用了GPIO更多的RP2350B,拥有48个GPIO,并且提供了一个扩展板,非常利于学习PICO2的实验。

二、任务选择:

选择了任务4:【搭配综合技能训练板】利用PWM制作一个音乐播放器

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

简单分析一下任务。要实现音乐播放,这里使用RP2350的pwm输出,控制输出波形的频率,就可以控制蜂鸣器的音调,从而组合出音乐来。板子上有4颗按键,使用分压方式接到主控的。所以需要使用RP2350的ADC功能来读取按键,识别不同的按键。扩展板上集成了一块OLED屏幕,使用SPI通讯方式控制。

RP2350支持C,Arduino,micropython等方式开发。这里尝试使用vscode+platformio做开发,但是硬件支持找不到RP2350B的支持,只有RP2350A的支持,无法控制20以后的GPIO。最终选择使用Arduino进行开发。

三、环境搭建

参考着产品维库搭建Arduino环境,配置好Preferences窗口。

image.png

四、任务实现

方案框图

image.png

实现流程图:

image.png

1、OLED显示

RP2350内部拥有双核,所以可以直接使用多进程工作。这里将PWM、adc检测的任务放到一个内核上,将OLDE显示放到另外一个内核上。OLED屏幕驱动使用U8g2库,这个库可以很方便地使用中文支持。

// oled管脚
#define OLED_MOSI 44
#define OLED_CLK 45
#define OLED_DC 42
#define OLED_CS 41
#define OLED_RESET 43
U8G2_SSD1306_128X32_UNIVISION_1_4W_SW_SPI u8g2(U8G2_R0, OLED_CLK, OLED_MOSI, OLED_CS, OLED_DC, OLED_RESET);

OLED使用的是SSD1306的I驱动,走SPI接口,使用了5个GPIO,根据电路图找出对应的管脚信息。

void setup1()
{
  u8g2.begin();
  u8g2.enableUTF8Print(); // 启用UTF8支持
}


void loop1()
{
  u8g2.setFont(u8g2_font_wqy16_t_gb2312); // 设置中文字体
  u8g2.setFontDirection(0);
  u8g2.firstPage();
  do
  {
    // u8g2.setCursor(0, 10);
    // u8g2.print("汉字显示12345ABCder"); // 显示中文
    u8g2.setCursor(40, 25);
    // u8g2.print("中文,小学校");
    switch (keyval.key)
    {
    case 1: // 小星星
      u8g2.print(startsname);
      break;
    case 2: // 樱花
      u8g2.print(flowname);
      break;
    case 3: // 生日歌
      u8g2.print(birthdayname);
      break;
    }


  } while (u8g2.nextPage());
  // Serial.println(sizeof(pmusic));
  delay(1000);
}

2、按键读取。

image.png

从电路图上可以得知,四个按键通过与电阻的分压,实现了按键按下后DVout端的电压变化。在代码中设置一个定时器,每5ms中断一次。每次定时器中断,都会用AD检查DVout端的电压变化。当电压变化并累计一定次数(3次)后,就判定按键按下了,将对应的按键值赋予变量。

// 在每次时间中断中,检查ADC的值
struct repeating_timer segtimer, pwmtimer; // 两个定时器 1个用来控制数码官,一个用来控制pwm播放
bool segdisp_timer_callback(struct repeating_timer *t)
{
  uint adcval;
  Tube.displayInt(keyval.key);
  // 将读取到的值缩小1000倍,拨玛开关全关闭情况下 大约为 59:无按键0    56:按键1   52:按键2  45:按键3   30:按键4
  adcval = analogRead(ADCBTN) / 1000; // 从47口读取电压
  // 消抖:当前读取的ADC值与当前按键值作比较,相同超过指定次数,就赣边keyval的值
  if (adcval >= 58)
  {
    // 无按键
    if (keyval.curkeyval == 0)
    {
      //keyval.val++;
    }
    else
    {
      keyval.curkeyval = 0;
      keyval.val = 0;
    }
  }
  else if (adcval >= 55) // 按键1
  {
    if (keyval.curkeyval == 1)
      keyval.val++;
    else
    {
      keyval.curkeyval = 1;
      keyval.val = 0;
    }
  }
  else if (adcval >= 51) // 按键2
  {
    if (keyval.curkeyval == 2)
      keyval.val++;
    else
    {
      keyval.curkeyval = 2;
      keyval.val = 0;
    }
  }
  else if (adcval >= 44) // 按键3
  {
    if (keyval.curkeyval == 3)
      keyval.val++;
    else
    {
      keyval.curkeyval = 3;
      keyval.val = 0;
    }
  }
  else if (adcval >= 29) // 按键4
  {
    if (keyval.curkeyval == 4)
      keyval.val++;
    else
    {
      keyval.curkeyval = 4;
      keyval.val = 0;
    }
  }
  if (keyval.val > 2)
  {
    keyval.key = keyval.curkeyval == 0 ? keyval.key : keyval.curkeyval;
    keyval.isChange = keyval.curkeyval == 0 ? false:true;
  }
  return true;
}

3、PWM输出。

这里使用了RP2040的PWM库,经测试在RP2350上正常运行。这里预先存放了3首曲子的简谱,将简谱中的音调对应到振动频率上去。使用一个定时器逐次读取预先存放的曲调的频率,依次输出给蜂鸣器。当曲子播放完成后,重新返回头部循环播放。PWM中高电平驱动蜂鸣器动作,测试了一下,占比在1%~10%的范围内,蜂鸣器的音量有比较明显的变化,超过10%后,音量变化不明显了,猜测是驱动饱和了,再增加高电平的占空比意义不大,并且会使得蜂鸣器发烫。所以PWM的占空比设置为一个小于10%的值。

uint musicoffset = 0;


bool pwmmusic_timer_callback(struct repeating_timer *t)
{
  float freq;
  switch (keyval.key)
  {
  case 1: //小星星
    if (musicoffset >= sizeof(starts)/4)
      musicoffset = 0;
    freq = MUSICFREQ[starts[musicoffset]];
    PWM_Instance->setPWM(BEEP, freq, starts[musicoffset] == nu ? 0 : PWMVAL);    
    break;
  case 2: //樱花
    if (musicoffset >= sizeof(flower)/4)
      musicoffset = 0;
    freq = MUSICFREQ[flower[musicoffset]];
    PWM_Instance->setPWM(BEEP, freq, flower[musicoffset] == nu ? 0 : PWMVAL);    
    break;
  case 3: //生日歌
    if (musicoffset >= sizeof(birthday)/4)
      musicoffset = 0;
    freq = MUSICFREQ[birthday[musicoffset]];
    PWM_Instance->setPWM(BEEP, freq, birthday[musicoffset] == nu ? 0 : PWMVAL);    
    break;
  }
  musicoffset++;
  return true;
}


void setup()
{
  Serial.begin(115200); //
  pinMode(LED, OUTPUT);
  analogReadResolution(16); // 调整ADC分辨率为16位
  PWM_Instance = new RP2040_PWM(BEEP, 1000, 3);
  delay(100);
  // PWM_Instance->disablePWM();
  add_repeating_timer_ms(20, segdisp_timer_callback, NULL, &segtimer);            // 每5ms调用一次
  add_repeating_timer_ms(MUSICTIME, pwmmusic_timer_callback, NULL, &pwmtimer); // 指定周期调用一次
}


void loop()
{
  switch (keyval.key)
  {
  case 1: // 小星星
    if (keyval.isChange)
    {
      keyval.isChange = false;
      musicoffset = 0;
      Serial.println(sizeof(starts));
      // add_repeating_timer_ms(startsspeed, pwmmusic_timer_callback, NULL, &pwmtimer); //指定周期调用一次
    }
    break;
  case 2: // 樱花
    if (keyval.isChange)
    {
      keyval.isChange = false;
      musicoffset = 0;
      Serial.println(sizeof(flower));
      // add_repeating_timer_ms(flowerspeed, pwmmusic_timer_callback, NULL, &pwmtimer); //指定周期调用一次
    }
    break;
  case 3: // 生日歌
    if (keyval.isChange)
    {
      keyval.isChange = false;
      musicoffset = 0;
      Serial.println(sizeof(birthday));
      // add_repeating_timer_ms(birthdayspeed, pwmmusic_timer_callback, NULL, &pwmtimer); //指定周期调用一次
    }
    break;
  }
}

4、数码管显示。

STEP_RP2350B核心板上还有个很漂亮的数码管,这里已将其驱动起来,用来显示当前的按键选择。

image.png

从电路图可以看出,数码管使用两颗74HC595级联。可以使用3个GPIO,用串行方式来驱动数码管的显示。这里写了个Tube595的驱动。显示时,数码管点亮一小会,利用肉眼错觉来显示具体的数字。

#ifndef TUBE595_cpp
#define TUBE595_cpp
#include "Tube595.h"
unsigned char Tube595::LED_MODEL[17] = {
  // 0    1     2   3    4    5    6    7    8     9   A    B    C    D    E    F    -
  0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E, 0x79, 0x71, 0x40
};
unsigned char Tube595::LedData[8] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
Tube595::Tube595(int dataPin, int sclkPin, int rclkPin) {
  DIO = dataPin;
  SCLK = sclkPin;
  RCLK = rclkPin;
  pinMode(SCLK, OUTPUT);
  pinMode(RCLK, OUTPUT);
  pinMode(DIO, OUTPUT);
  digitalWrite(DIO, LOW);
  digitalWrite(SCLK, LOW);
  digitalWrite(RCLK, LOW);
}
// display a int number
// 整形显示,point  0 第0位的小数点 1 第1位的小数点 2 两个一起亮
void Tube595::displayInt(int num, int point) {
  char string[9] = { 0 };
  sprintf(string, "%8d", num);
  for (int i = 7; i >= 0; i--) {
    switch (string[i]) {
      case ' ':
        break;
      case '-':
        LedData[7 - i] = LED_MODEL[16];
        break;
      default:
        LedData[7 - i] = LED_MODEL[string[i] - '0'];
        // LedData[7-i] = LED_MODEL[1];
        break;
    }
  }
  if (point == 1) setPoint(0);
  else if (point == 2) setPoint(1);
  else if (point == 3) {
    setPoint(0);
    setPoint(1);
  }
  update();
}
void Tube595::setNoPoint(int place) {
  LedData[place] &= 0x80;
}
void Tube595::setPoint(int place) {
  LedData[place] |= 0x80;
}
void Tube595::closeDisplay() {
  for (int i = 0; i < 8; i++)
    LedData[i] = 0x7F;


  update();
}
void Tube595::update() {
  // 仅有两个数码管
  for (int i = 0; i < 2; i++) {
    shiftOut(DIO, SCLK, MSBFIRST, 1 << i);
    shiftOut(DIO, SCLK, MSBFIRST, LedData[i]);
    digitalWrite(RCLK, LOW);
    digitalWrite(RCLK, HIGH);
    delayMicroseconds(2000);  //数据锁存  显示2ms
  }
  shiftOut(DIO, SCLK, MSBFIRST, 0);
  digitalWrite(RCLK, LOW);
  digitalWrite(RCLK, HIGH);
}
#endif

五、作品展示

使用简谱写了三首常见的音乐。通过按键切换不同的曲子。

image.png

image.png

image.png


六、心得体会

感谢硬禾学堂组织的RP2350B核心板活动。能够学习到树莓派RP2350B这款强大的芯片,功能强大、管脚众多,学习了使用ADC来识别多个按键,成就满满!

附件下载
music_play.zip
团队介绍
单片机业余爱好者,瞎捣鼓小能手。
团队成员
aramy
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号