这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 电子DIY+RP2350B的音乐播放器

共1条 1/1 1 跳转至

电子DIY+RP2350B的音乐播放器

高工
2026-03-19 22:09:07     打赏

一、硬件配置与任务需求匹配

核心板选型

STEP_RP2350B核心板:基于RP2350B芯片,提供48个GPIO(相比RP2350A的30个GPIO扩展性更强),满足按键、OLED屏幕、蜂鸣器等多外设需求。

扩展板功能:

OLED屏幕:128×32分辨率,SPI接口,用于显示曲目名称(支持汉字)。

按键输入:4颗按键通过分压电阻接入ADC引脚,通过电压值区分按键状态(需消抖处理)。

蜂鸣器:通过PWM输出控制音调,生成音乐旋律。

关键外设连接

OLED屏幕:

SPI接口需手动映射GPIO(如SCK→GP45、MOSI→GP44),因硬件SPI引脚与扩展板不匹配,需使用软件SPI模拟。

使用U8g2库驱动屏幕,支持中文显示(需加载字库文件,如wqy16.ttf)。

按键输入:

通过ADC读取分压值(如按键1→56、按键2→52等),结合定时器中断实现消抖(连续3次采样一致才确认按键状态)。

PWM输出:

蜂鸣器连接至任意PWM引脚(如GP0),通过调整频率(如262Hz对应C4音调)和占空比(5%~10%)控制音高和音量。


二、开发环境搭建与问题解决

Arduino IDE配置

安装支持库:

RP2350B核心:通过Arduino Board Manager添加URL(如https://github.com/earlephilhower/arduino-pico/releases/download/global/package_rp2040_index.json),选择Raspberry Pi Pico/RP2040板卡(需确认兼容性)。

U8g2库:通过库管理器安装,用于OLED汉字显示。

引脚定义修正:

若PlatformIO仅支持RP2350A,需手动修改pins_arduino.h文件,将NUM_DIGITAL_PINS定义为48,并补充GPIO映射表。

或通过编译标志-D PICO_RP2350B=1临时扩展引脚访问(需验证稳定性)。

替代方案:MicroPython开发

环境搭建:

使用Thonny IDE,选择MicroPython解释器(Raspberry Pi Pico)。

拖放UF2固件至Pico 2(按住BOOTSEL键上电)。


三、环境搭建

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

dc326c39-4ba6-46fd-940f-dcd7c795d8bc.png

选择开发板

8dd8f698-f989-4c24-9d25-be54cbb1bbed.png

任务实现

方案框图:

95c7d5bc-a208-478f-a18d-ef8be69268ef.png

实现流程图:

38dace83-d073-4edb-869f-573796facc06.png

RP2350B的双核(Core 0和Core 1)可独立运行任务,通过硬件邮箱(Mailbox)或共享内存通信。本方案将高实时性任务(PWM生成、ADC检测)分配给Core 0,低实时性任务(OLED显示)分配给Core 1,避免屏幕刷新干扰音乐播放的稳定性。

代码

// 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);
}

按键读取。

216066df-c54f-4117-91a3-37fc8620b069.png

从电路图可知,四个按键经电阻分压使DVout端电压随按键按下而变化。代码中启用定时器,以5ms为周期触发中断,每次中断时通过ADC读取DVout电压值。为消除机械按键抖动,当检测到电压变化时启动计数器,仅当连续3次中断(15ms)内电压保持稳定,才判定按键有效,并将对应键值赋给变量。此方法兼顾了实时性与抗干扰性,适用于嵌入式系统低功耗场景。

// 在每次时间中断中,检查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;
}
核心功能实现PWM库适配采用RP2040官方hardware_pwm库,经实测在RP2350上稳定运行,支持动态频率与占空比调节。曲谱存储与频率映射将3首曲目的简谱转换为频率数组(如C4=262Hz,D4=294Hz),存储于程序Flash中:
const uint16_t song_freq[] = {262, 294, 330, 349}; // 示例:《小星星》片段
定时器驱动播放通过硬件定时器(如1ms周期中断)逐次读取频率数组,动态更新PWM输出:
void timer_isr() {
   static uint8_t note_idx = 0;
   if (note_idx >= SONG_LENGTH) note_idx = 0; // 循环播放
   pwm_set_freq_duty(song_freq[note_idx++], 8); // 更新频率与占空比
}

 PWM参数优化占空比选择测试范围:1%~10%占空比下,蜂鸣器音量随占空比线性增强;饱和阈值:超过10%后音量提升不明显,且蜂鸣器发热严重;最终设定:固定占空比为8%,平衡音量与功耗。频率精度控制PWM时钟源配置为125MHz,通过分频器实现1Hz级频率调节(如262Hz需分频系数=125M/262≈477096);支持无源蜂鸣器音高准确还原。
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;
  }
}

数码管显示。

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

12fc55c7-e61f-4c77-8c4a-ef24ea0f7222.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

作品展示

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

63abca3d-a6ef-4b01-9b91-2d52e784d012.png

b9f27e81-0307-4533-b2f4-2c8ccd5e4695.png

12473175-e9ff-48e7-b1f8-647ed2d0aa88.png



共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]