1、项目介绍
很荣幸能够参加本次由硬禾推出的全新RP2350B核心板试用活动,这不仅是一次宝贵的学习机会,也为我深入了解并实践嵌入式系统开发提供了良好平台。在本项目中,我选择使用RP2350B核心板来完成一个PWM音乐播放器的制作,利用板载的PWM功能实现音符的频率控制,结合简单的控制逻辑播放旋律。通过该项目,我不仅加深了对RP2350B硬件资源的理解,也提升了动手实践与软硬件协同开发的能力。完成的主要内容如下:
- 通过PWM产生不同的音调,并驱动板上蜂鸣器将音调输出
- 能够播放三首不同的曲子,可以切换播放
- 曲子的切换使用核心板上的按键,具有按键消抖的功能
- 播放的曲子的名字在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核心板
为了完成该项目,使用到的综合训练板上的硬件包括:
- 128*32分辨率的OLED单色显示屏,通过SPI总线进行访问,可以显示文本和图形化信息。
- 蜂鸣器,使用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显示等内容让我对软硬件结合有了更深刻的理解。这次经历极大地增强了我的动手能力和解决问题的信心,同时希望以后可以参加更多这样有意义的活动。