基于RP2350B实现音乐播放器
该项目使用了rp2350B核心板卡和综合技能训练板,实现了音乐播放器的设计,它的主要功能为:使用rp2350B通过pwm控制无源蜂鸣器播放音乐,并将歌曲名称显示在oled上,歌曲序号显示在数码管上。
标签
Arduino
RP2350B
播放器
lshy
更新2025-07-11
10

项目介绍

本项目基于树莓派的rp2350B芯片,实现了使用无源蜂鸣器播放歌曲的功能。同时也实现了数码管显示及led驱动功能。由于micropython没有rp2350B的可用固件,及csdk的环境下载困难,本项目使用的软件框架是广为人知的arduino。

硬件介绍

使用了搭载rp2350B的核心板。

核心板具备以下硬件:

  • 8颗单色LED,用于各种LED相关的编程练习,比如心跳灯、流水灯、呼吸灯灯
  • 2个三色LED,用于使用RGB三种颜色的编程练习,比如交通灯的控制,在这里2颗三色灯是通过一根串行信号线来控制,同小脚丫FPGA核心板通过6根IO来控制不同,主要是为了节省IO信号线的数量
  • 2个7段数码管,用于计数、显示数字信息,可以显示0-99之间的数字,在这里我们使用了两颗74HC595将3根控制信号线转换为7段数码管的驱动信号
  • 4个拨码开关,用于设置、切换一些状态,其状态是使用模拟信号的方式送到RP2350B的ADC进行判断
  • 4个轻触按键,用于控制信息的输入,其状态是使用模拟信号的方式送到RP2350B的ADC进行判断
  • 2M外挂flash

下图是核心板上的功能以及用到的管脚

方案框图

项目设计思路

本项目中,系统有三首歌:两只老虎、欢乐颂、小星星。可以通过3个按键去控制接下来将要播放那一首歌。按键按下后,液晶屏显示歌曲名称,数码管显示当前歌曲序号,并且在播放过程中led会点亮,当歌曲播放完毕,led灭。

软件流程图

总流程

按键采集

数码管显示

关键代码介绍

按键采集

本次项目中的按键不是常规意义上的只有高低两种状态的按键。在一个分压电阻网络上,通过按键实现不同的分压导通电路,这样adc采集到的电压就会有所不同。不同按键按下后,输出的电压存在一定的差值,可以通过这个差值对采集到的电压进行划分,对应到不同的键值。

float key_ad_get(void) {
  int adc_value = analogRead(A7);
  float voltage = adc_value * 3.3 / 4095.0;
  Serial.print("Voltage: ");
  Serial.println(voltage, 2);  // 保留两位小数
  return voltage;
}


uint8_t key_read(void) {
  float voltage = key_ad_get();
  if ((voltage > 1.4) && (voltage < 1.6)) {
    delay(20);
    voltage = key_ad_get();
    if ((voltage > 1.4) && (voltage < 1.6)) {
      return 0;
    }
  } else if ((voltage > 2.2) && (voltage < 2.4)) {
    delay(20);
    voltage = key_ad_get();
    if ((voltage > 2.2) && (voltage < 2.4)) {
      return 1;
    }
  } else if ((voltage > 2.5) && (voltage < 2.7)) {
    delay(20);
    voltage = key_ad_get();
    if ((voltage > 2.5) && (voltage < 2.7)) {
      return 2;
    }
  }
  return 255;
}

image.png

数码管显示

核心板上的数码管是共阴极的,通过两颗74HC595分别控制数码管显示的内容和具体哪个数码管亮。由于需要两个数码管显示不同的内容,动态刷新数码管就是必不可少的。为了避免和歌曲播放冲突,这里通过freertos接口创建了一个独立的刷新任务,用于实时刷新数码。外部可以通过segment_data_set接口控制数码管的显示内容。

#include <Arduino.h>
#include <FreeRTOS.h>
#include <task.h>



TaskHandle_t Task1;


volatile uint8_t segment_data = 0;


// 引脚定义
const int latchPin = 1;  // ST_CP
const int clockPin = 2;  // SH_CP
const int dataPin = 0;   // DS


// 共阴极数码管段码表(0-9)
byte segmentCodes[] = {
  0x3F,  // 0 (0b00111111)
  0x06,  // 1 (0b00000110)
  0x5B,  // 2
  0x4F,  // 3
  0x66,  // 4
  0x6D,  // 5
  0x7D,  // 6
  0x07,  // 7
  0x7F,  // 8
  0x6F   // 9
};


void segment_data_set(uint8_t data) {
  segment_data = data;
}




void segment_display(void *pvParam) {
  while (1) {
    digitalWrite(latchPin, LOW);                                             // 准备数据输入
    shiftOut(dataPin, clockPin, MSBFIRST, 0x02);                             // 发送段码
    shiftOut(dataPin, clockPin, MSBFIRST, segmentCodes[segment_data / 10]);  // 发送段码
    digitalWrite(latchPin, HIGH);                                            // 锁存输出
    vTaskDelay(10 / portTICK_PERIOD_MS);
    digitalWrite(latchPin, LOW);                                             // 准备数据输入
    shiftOut(dataPin, clockPin, MSBFIRST, 0x01);                             // 发送段码
    shiftOut(dataPin, clockPin, MSBFIRST, segmentCodes[segment_data % 10]);  // 发送段码
    digitalWrite(latchPin, HIGH);                                            // 锁存输出
    vTaskDelay(10 / portTICK_PERIOD_MS);
  }
}



void segment_init(void) {
  pinMode(latchPin, OUTPUT);
  pinMode(clockPin, OUTPUT);
  pinMode(dataPin, OUTPUT);
  xTaskCreate(segment_display, "segment", 128, NULL, 1, &Task1);
}

歌曲播放

通过GPIO20输出不同频率的pwm控制无源蜂鸣器播放音调组成歌曲。这里使用了tone库,控制gpio的pwm输出。

#define BUZZER_PIN 20 


// C调音阶频率(单位:Hz)
const float c_noteFreq[] = {
  261.63,  // C4 (Do)
  293.66,  // D4 (Re)
  329.63,  // E4 (Mi)
  349.23,  // F4 (Fa)
  392.00,  // G4 (Sol)
  440.00,  // A4 (La)
  493.88 ,  // B4 (Si)
  220,//A3
  196,//G3
  523,//C5
  165,//E3
  587,//E3
};


// D调音阶频率定义(单位:Hz)[4,7](@ref)
const int d_noteFreq[] = {
  294,  // D4 (Re)
  330,  // E4 (Mi)
  349,  // F4 (Fa)
  392,  // G4 (Sol)
  440,  // A4 (La)
  494,  // B4 (Si)
  523   // C5 (Do)
};


// 《两只老虎》简谱映射(数字对应简谱1-7,0表示休止符)
int lzlh_ddz_melody[] = {
  1, 2, 3, 1, 1, 2, 3, 1,
  3, 4, 5, 3, 4, 5,
  5, 6, 5, 4, 3, 1,
  5, 6, 5, 4, 3, 1,
  2, 5, 1, 2, 5, 1
};


// 节拍(4=四分音符,500ms;2=二分音符,1000ms)
int lzlh_rhythm[] = {
  4, 4, 4, 4, 4, 4, 4, 4,
  4, 4, 2, 4, 4, 2,
  8, 8, 8, 8, 4, 4,
  8, 8, 8, 8, 4, 4,
  4, 4, 2, 4, 4, 2
};



// 《欢乐颂》简谱映射(数字对应简谱1-7,0为休止符)[6,8](@ref)
int hls_ddz_melody[] = {
  3,3,4,5,5,4,3,2,1,1,2,3,3,2,2,0,
  3,3,4,5,5,4,3,2,1,1,2,3,2,1,1,0,
};


// 节拍(四分音符=500ms,八分音符=250ms)[6,1](@ref)
float hls_rhythm[] = {
  1,1,1,1,1,1,1,1,1,1,1,1,1.5,0.5,2,1,
  1,1,1,1,1,1,1,1,1,1,1,1,1.5,0.5,2,1,
};



void buzzer_init() {
  pinMode(BUZZER_PIN, OUTPUT);
}


void buzzer_hls_play(){
for (int i = 0; i < sizeof(hls_ddz_melody)/sizeof(int); i++) {
    if (hls_ddz_melody[i] != 0) {
      int noteIndex = hls_ddz_melody[i] - 1;  // 简谱数字转数组索引
      tone(BUZZER_PIN, d_noteFreq[noteIndex]);
      delay(500 * hls_rhythm[i]);  // 基础节拍设为500ms/拍
      noTone(BUZZER_PIN);
      delay(50);  // 音符间隔防粘连[5](@ref)
    } else {
      delay(500 * hls_rhythm[i]);  // 处理休止符
    }
  }
}


void buzzer_lzlh_play() {
  for (int i = 0; i < sizeof(lzlh_ddz_melody)/sizeof(int); i++) {
    if (lzlh_ddz_melody[i] != 0) {
      tone(BUZZER_PIN, c_noteFreq[lzlh_ddz_melody[i] - 1]);  // 播放音符
      delay(1000 / lzlh_rhythm[i]);                    // 根据节拍延时
      noTone(BUZZER_PIN);                         // 停止发声
      delay(50);                                  // 音符间隔防粘连
    } else {
      delay(1000 / lzlh_rhythm[i]);                    // 休止符处理
    }
  }
}



#define NOTE_C4 262  // Do
#define NOTE_D4 294  // Re
#define NOTE_E4 330  // Mi
#define NOTE_F4 349  // Fa
#define NOTE_G4 392  // Sol
#define NOTE_A4 440  // La
#define NOTE_B4 494  // Ti


int xxx_melody[] = {
  NOTE_C4, NOTE_C4, NOTE_G4, NOTE_G4, NOTE_A4, NOTE_A4, NOTE_G4,
  NOTE_F4, NOTE_F4, NOTE_E4, NOTE_E4, NOTE_D4, NOTE_D4, NOTE_C4
};


int xxx_noteDurations[] = {
  4, 4, 4, 4, 4, 4, 2,  // 前7个音符("1 1 5 5 6 6 5-")
  4, 4, 4, 4, 4, 4, 2   // 后7个音符("4 4 3 3 2 2 1-")
};



void buzzer_xxx_play() {
   for (int i = 0; i < 14; i++) {
    int duration = 1000 / xxx_noteDurations[i];  // 四分音符=250ms(120BPM)
    tone(BUZZER_PIN, xxx_melody[i], duration);
    delay(duration * 1.30);  // 增加30%间隔防止音符粘连[1](@ref)
    noTone(BUZZER_PIN);
  }
}

屏幕显示

使用PCtoLCD2002软件按照下方配置可以将字模输出。

lcd2002配置.png

const uint8_t lian[] PROGMEM = { 0x00, 0x00, 0xFF, 0xFE, 0x04, 0x40, 0x04, 0x40, 0x04, 0x40, 0x7F, 0xFC, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x4A, 0xA4, 0x4A, 0x94, 0x51, 0x14, 0x42, 0x04, 0x40, 0x04, 0x40, 0x14, 0x40, 0x08 };
const uint8_t zhi[] PROGMEM = { 0x00, 0x00, 0x1F, 0xF0, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F, 0xF0, 0x10, 0x10, 0x00, 0x00, 0x08, 0x20, 0x08, 0x10, 0x10, 0x08, 0x20, 0x04, 0x40, 0x04 }; /*"只",1*/
const uint8_t lao[] PROGMEM = { 0x02, 0x00, 0x02, 0x08, 0x3F, 0xD0, 0x02, 0x20, 0x02, 0x40, 0xFF, 0xFE, 0x01, 0x00, 0x02, 0x00, 0x0C, 0x10, 0x18, 0xE0, 0x2F, 0x00, 0x48, 0x08, 0x88, 0x08, 0x08, 0x08, 0x07, 0xF8, 0x00, 0x00 }; /*"老",2*/
const uint8_t hu[] PROGMEM = { 0x01, 0x00, 0x01, 0xF8, 0x01, 0x00, 0x3F, 0xFC, 0x21, 0x04, 0x21, 0x60, 0x2F, 0x88, 0x21, 0x08, 0x20, 0xF8, 0x20, 0x00, 0x23, 0xE0, 0x22, 0x20, 0x22, 0x20, 0x44, 0x24, 0x48, 0x24, 0x90, 0x1C };  /*"虎",3*/


const uint8_t huan[] PROGMEM = { 0x00, 0x80, 0x00, 0x80, 0xFC, 0x80, 0x04, 0xFC, 0x05, 0x04, 0x49, 0x08, 0x2A, 0x40, 0x14, 0x40, 0x10, 0x40, 0x28, 0xA0, 0x24, 0xA0, 0x45, 0x10, 0x81, 0x10, 0x02, 0x08, 0x04, 0x04, 0x08, 0x02 }; /*"欢",0*/
const uint8_t le[] PROGMEM = { 0x00, 0x20, 0x00, 0xF0, 0x1F, 0x00, 0x10, 0x00, 0x11, 0x00, 0x21, 0x00, 0x21, 0x00, 0x3F, 0xFC, 0x01, 0x00, 0x09, 0x20, 0x09, 0x10, 0x11, 0x08, 0x21, 0x04, 0x41, 0x04, 0x05, 0x00, 0x02, 0x00 };   /*"乐",1*/
const uint8_t song[] PROGMEM = { 0x00, 0x00, 0x28, 0xFE, 0x24, 0x20, 0x24, 0x40, 0x42, 0xFC, 0x52, 0x84, 0x90, 0x94, 0x10, 0x94, 0x20, 0x94, 0x20, 0x94, 0x48, 0x94, 0x44, 0xA4, 0xFC, 0x30, 0x44, 0x48, 0x00, 0x84, 0x03, 0x02 }; /*"颂",2*/


const uint8_t xiao[] PROGMEM = { 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x11, 0x10, 0x11, 0x08, 0x11, 0x04, 0x21, 0x04, 0x21, 0x02, 0x41, 0x02, 0x81, 0x02, 0x01, 0x00, 0x01, 0x00, 0x05, 0x00, 0x02, 0x00 }; /*"小",0*/
const uint8_t xing[] PROGMEM = { 0x00, 0x00, 0x1F, 0xF0, 0x10, 0x10, 0x1F, 0xF0, 0x10, 0x10, 0x1F, 0xF0, 0x01, 0x00, 0x11, 0x00, 0x1F, 0xF8, 0x21, 0x00, 0x41, 0x00, 0x1F, 0xF0, 0x01, 0x00, 0x01, 0x00, 0x7F, 0xFC, 0x00, 0x00 }; /*"星",1*/
   


// 自定义软件SPI引脚定义
#define OLED_MOSI 44   // 数据线
#define OLED_CLK 45    // 时钟线
#define OLED_DC 42     // 数据/命令切换
#define OLED_CS 41     // 片选
#define OLED_RESET 43  // 复位


Adafruit_SSD1306 display(128, 32, OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);


void setup() {
  Serial.begin(9600);
  if (!display.begin(SSD1306_SWITCHCAPVCC)) {
    Serial.println("SSD1306初始化失败!");
    while (1)
      ;
  }
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);
  display.display();
  buzzer_init();
  analogReadResolution(12);  // 设置ADC为12位模式
  led_init();
  segment_init();
}

void loop() {
  uint8_t key = 0xff;
  key = key_read();
  if (key != 0xff) {
    segment_data_set(key);
  }


  if (key == 0) {
    led_power_switch(true);
    display.clearDisplay();
    display.drawBitmap(0, 0, lian, 16, 16, SSD1306_WHITE);  // X,Y,字模,宽,高,颜色
    display.drawBitmap(16, 0, zhi, 16, 16, SSD1306_WHITE);  // X,Y,字模,宽,高,颜色
    display.drawBitmap(32, 0, lao, 16, 16, SSD1306_WHITE);  // X,Y,字模,宽,高,颜色
    display.drawBitmap(48, 0, hu, 16, 16, SSD1306_WHITE);   // X,Y,字模,宽,高,颜色
    display.display();
    buzzer_lzlh_play();
    led_power_switch(false);
  } else if (key == 1) {
    led_power_switch(true);
    display.clearDisplay();
    display.drawBitmap(0, 0, huan, 16, 16, SSD1306_WHITE);   // X,Y,字模,宽,高,颜色
    display.drawBitmap(16, 0, le, 16, 16, SSD1306_WHITE);    // X,Y,字模,宽,高,颜色
    display.drawBitmap(32, 0, song, 16, 16, SSD1306_WHITE);  // X,Y,字模,宽,高,颜色
    display.display();
    buzzer_hls_play();
    led_power_switch(false);
  } else if (key == 2) {
    led_power_switch(true);
    display.clearDisplay();
    display.drawBitmap(0, 0, xiao, 16, 16, SSD1306_WHITE);   // X,Y,字模,宽,高,颜色
    display.drawBitmap(16, 0, xing, 16, 16, SSD1306_WHITE);  // X,Y,字模,宽,高,颜色
    display.drawBitmap(32, 0, xing, 16, 16, SSD1306_WHITE);  // X,Y,字模,宽,高,颜色
    display.display();
    buzzer_xxx_play();
    led_power_switch(false);
  }
}

由于电路上屏幕的spi并没有接到rp2350b的硬件spi上,所以这里使用的软件spi。

实物演示

99bdb4ad50919aaab8b2f6782680163.jpg

22f2f36aac0fb69ee38f07035c55406.jpg

fddf1920e016ff2fa74b22ec6419ceb.jpg

心得体会

本次活动让我初步了解和学习了rp2350B这颗单片机,尤其是其同时包含arm和riscv两个架构,让我耳目一新。在完成项目的过程中,选择合适的软件框架花费了大量的时间。一开始想尝试micropython,但是只找到了rp2350a的可用固件。之后发现了circuit python可用,但是它的资料相对较少,我对其也不是很熟悉(后续学习看看),就放弃了使用python框架。在vscode上安装官方的插件后,例程老是下载失败,使用csdk的方案也就放弃了。最后arduino的环境顺利的安装好,也就选择了arduino。

本次活动中,硬件中的led引脚复用和按键的实现方式让我学习到不少。


团队介绍
连续两年半的独立练习生lshy
团队成员
lshy
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号