基于RP2350B实现利用PWM制作一个音乐播放器
按键分析
我们通过原理图可以知道,其中的多个按键和拨码开关,实现的是ADC采样来实现电压变化,根据ADC采样。
因为我们要实现一个按键来控制,这里我们采集数据发现,K4按下的数据,会导致我们的ADC采样的电压为1.5V,1.6—1.5为我们K4按键按下。默认电压大于3V。这里我们开始做按键的判断分析。
任务要求分析
- 通过PWM产生不同的音调,并驱动板上蜂鸣器将音调输出
任务实现解析: - 频率与音调的对应关系
- 声音的音调由声波的振动频率决定,板子搭载了无源蜂鸣器,通过PWM控制我们高低电平的时间,就可以在一定时间内响起不同的音调,来实现我们的音乐的实现。
- 能够播放三首不同的曲子,可以切换播放
- 曲子的切换使用核心板上的按键,需要有按键消抖的功能
任务实现解析:
通过板载ADC按键实现按键消抖,通过按键的次数切换我们播放的歌曲。 - 播放的曲子的名字在OLED屏幕上显示出来(汉字显示)
任务实现解析:
我们的开发板屏幕不是硬件SPI,我们需要通过软件SPI来实现,通过取模来实现我们的中文汉字的读取显示到我们屏幕上。
方案框图
软件流程图
软件环境搭建
感谢UP主“就是希曼”【用C/C++学习树莓派RP2350(PICO2)——01—配置开发环境】 https://www.bilibili.com/video/BV1CDdZYJEXP/?share_source=copy_web&vd_source=7ee7054929c2530c4523e3f3f8bd8b54
详细介绍了板卡C/C++开发环境,其中遇到许多问题都能详细解决,我们板卡是B系类,其中引脚会多一些,在其他的环境只支持引脚较少,所以我这次选择了C环境开发,来实现我们的功能,在根据这个UP主的教程,我们成功将环境搭建起来,接下来是代码实现。
代码实现
ADC按键的实现
这里我们选择官方的示例,需要将引脚切换为47,因为我们在板卡检测引脚为47需要对应,之后我们需要观察串口引脚的数据,这里直接用万用表测量的数据不是很准确,这里使用串口读出的数据作为我们的按键电压的数值,我选择k4这里的范围在1.5V-1.6V左右,作为我的判断值。
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/gpio.h"
#include "hardware/adc.h"
int main() {
stdio_init_all();
printf("ADC Example, measuring GPIO26\n");
adc_init();
// Make sure GPIO is high-impedance, no pullups etc
adc_gpio_init(47);
// Select ADC input 0 (GPIO26)
adc_select_input(0);
while (1) {
// 12-bit conversion, assume max value == ADC_VREF == 3.3 V
const float conversion_factor = 3.3f / (1 << 12);
uint16_t result = adc_read();
printf("Raw value: 0x%03x, voltage: %f V\n", result, result * conversion_factor);
sleep_ms(500);
}
}
按键检测代码实现
bool read_key_state() {
adc_select_input(7); // 选择ADC输入通道7对应GPIO26
uint16_t raw_value = adc_read();
float voltage = raw_value * conversion_factor;
return voltage > KEY_PRESS_VOLTAGE_MIN && voltage < KEY_PRESS_VOLTAGE_MAX;
}
//主函数进行按键状态切换进行消抖
bool current_key_state = read_key_state();
// 如果按键状态变化
if (current_key_state != last_key_state)
OLED显示实现
oled这里选择的是软件SPI来驱动这里需要我们准备好我们相关驱动代码。
这里根据我们开发板对应的引脚设置我们的宏定义
// OLED 相关定义
#define OLED_MOSI_PIN 44
#define OLED_SCK_PIN 45
#define OLED_RESET_PIN 43
#define OLED_DC_PIN 42
#define OLED_CS_PIN 41
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
相关初始化代码,和基本写入和读取的相关函数
// OLED 缓冲区
uint8_t oled_buffer[OLED_WIDTH * OLED_HEIGHT / 8];
// OLED 初始化函数
void oled_init() {
gpio_init(OLED_RESET_PIN);
gpio_set_dir(OLED_RESET_PIN, GPIO_OUT);
gpio_put(OLED_RESET_PIN, 0);
sleep_ms(100);
gpio_put(OLED_RESET_PIN, 1);
sleep_ms(100);
gpio_init(OLED_DC_PIN);
gpio_set_dir(OLED_DC_PIN, GPIO_OUT);
gpio_init(OLED_CS_PIN);
gpio_set_dir(OLED_CS_PIN, GPIO_OUT);
gpio_put(OLED_CS_PIN, 1);
gpio_init(OLED_MOSI_PIN);
gpio_set_dir(OLED_MOSI_PIN, GPIO_OUT);
gpio_init(OLED_SCK_PIN);
gpio_set_dir(OLED_SCK_PIN, GPIO_OUT);
// 初始化OLED显示
oled_write_command(0xAE); // Display OFF
oled_write_command(0xD5); // Set Display Clock Divide Ratio / Oscillator Frequency
oled_write_command(0x80);
oled_write_command(0xA8); // Set Multiplex Ratio
oled_write_command(0x1F);
oled_write_command(0xD3); // Set Display Offset
oled_write_command(0x00);
oled_write_command(0x40); // Set Start Line (0 offset)
oled_write_command(0x8D); // Charge Pump Setting
oled_write_command(0x14);
oled_write_command(0x20); // Memory Addressing Mode
oled_write_command(0x00);
oled_write_command(0xA1); // Set Segment Re-map
oled_write_command(0xC8); // Com Output Scan Direction
oled_write_command(0xDA); // Set COM Pins Hardware Configuration
oled_write_command(0x02);
oled_write_command(0x81); // Set Contrast Control
oled_write_command(0xCF);
oled_write_command(0xD9); // Set Pre-charge Period
oled_write_command(0xF1);
oled_write_command(0xDB); // Set VCOMH Deselect Level
oled_write_command(0x40);
oled_write_command(0xA4); // Entire Display On/Off
oled_write_command(0xA6); // Normal Display
oled_write_command(0xAF); // Display ON
}
// 软件SPI 写字节函数
void oled_write_byte(uint8_t byte, bool is_command) {
gpio_put(OLED_CS_PIN, 0);
gpio_put(OLED_DC_PIN, !is_command);
for (int i = 0; i < 8; i++) {
gpio_put(OLED_MOSI_PIN, (byte >> (7 - i)) & 1);
gpio_put(OLED_SCK_PIN, 1);
gpio_put(OLED_SCK_PIN, 0);
}
gpio_put(OLED_CS_PIN, 1);
}
// OLED 写命令函数
void oled_write_command(uint8_t command) {
oled_write_byte(command, true);
}
// OLED 写数据函数
void oled_write_data(uint8_t data) {
oled_write_byte(data, false);
}
// OLED 清屏函数
void oled_clear_screen() {
memset(oled_buffer, 0x00, sizeof(oled_buffer));
for (int page = 0; page < 8; page++) {
oled_write_command(0xB0 + page); // 设置页地址
oled_write_command(0x00); // 设置列地址低4位
oled_write_command(0x10); // 设置列地址高4位
for (int col = 0; col < 128; col++) {
oled_write_data(oled_buffer[page * 128 + col]);
}
}
}
// 更新OLED屏幕
void oled_update_screen() {
for (int page = 0; page < 8; page++) {
oled_write_command(0xB0 + page); // 设置页地址
oled_write_command(0x00); // 设置列地址低4位
oled_write_command(0x10); // 设置列地址高4位
for (int col = 0; col < 128; col++) {
oled_write_data(oled_buffer[page * 128 + col]);
}
}
}
void oled_display_String(int ch, int x, int y)
{
// 写入16x16汉字数据
for (int page = 0; page < 2; page++) { // 16像素高度需要2页
for (int col = 0; col < 16; col++) { // 16像素宽度
// 计算要写入的页和列
int buffer_page = y/8 + page;
int buffer_col = x + col;
if (buffer_page < OLED_HEIGHT/8 && buffer_col < OLED_WIDTH) {
// 获取字模数据
uint8_t data = font_16x16_chinese[ch][page * 16 + col];
// 写入显存
oled_buffer[buffer_page * OLED_WIDTH + buffer_col] = data;
}
}
}
}
这里要显示汉字,需要我们进行取模,这里我们的字模选择16*16
我选择简单网上的字模生成
基本规格每个字大小: 16x16,点阵大小:64x16,字体:宋体
这里大家可以根据自己想要的文字进行生成,可以在我的代码中查看,这里就不添加出来。
蜂鸣器驱动
我们要实现就是将我们的音调通过PWM来实现,蜂鸣器连接的引脚在20上,我们初始化相关引脚
gpio_set_function(BUZZER_PIN, GPIO_FUNC_PWM);
通过配置PWM的相关时间参数,实现我们不同音调的变化
void play_note(float frequency, uint duration_ms) {
uint slice_num = pwm_gpio_to_slice_num(BUZZER_PIN);
uint channel = pwm_gpio_to_channel(BUZZER_PIN);
// 计算PWM参数
float sys_clk = clock_get_hz(clk_sys);
float div = 256.0; // 分频系数
uint wrap = sys_clk / (div * frequency);
// 配置PWM
pwm_config config = pwm_get_default_config();
pwm_config_set_clkdiv(&config, div);
pwm_config_set_wrap(&config, wrap);
pwm_init(slice_num, &config, true);
// 设置50%占空比
pwm_set_chan_level(slice_num, channel, wrap / 2);
sleep_ms(duration_ms);
// 停止输出(静音)
pwm_set_enabled(slice_num, false);
}
我们最后根据我们的音调和相关的持续时间就可以实现我们的音乐播放。
// 小星星音符频率(Hz)与持续时间(ms)对照表
uint16_t star_notes[] = {
523, 523, 784, 784, 880, 880, 784, // 哆哆嗦嗦啦啦嗦
698, 698, 659, 659, 587, 587, 523 // 发发咪咪来来哆
};
uint16_t star_durations[] = {
400, 400, 400, 400, 400, 400, 800, // 每音400ms,结尾音800ms
400, 400, 400, 400, 400, 400, 800
};
// 欢乐颂音符频率(Hz)与持续时间(ms)对照表
uint16_t ode_notes[] = {
392, 392, 440, 523, 523, 440, 440, 392, // 嗦嗦啦哆哆啦啦嗦
349, 349, 392, 440, 392 // 发嗦啦嗦
};
uint16_t ode_durations[] = {
300, 300, 300, 300, 300, 300, 300, 500,
300, 300, 300, 300, 500
};
// 生日快乐音符频率(Hz)与持续时间(ms)对照表
uint16_t happy_notes[] = {
262, 262, 294, 262, 349, 330, 262, 262, // 嗦嗦啦哆哆啦啦嗦
294, 262, 392, 349 // 发嗦啦嗦
};
uint16_t happy_durations[] = {
300, 300, 600, 600, 600, 1200, 300, 300,
600, 600, 600, 1200
};
效果展示
上电显示
播放歌曲
具体的音乐可以查看视频来得知,所有以上功能都成功实现。
总结收获
感谢硬禾学堂让我成功上手到RP2350B系类的板卡,其中根据板卡了解到树莓派的PICO,也第一次接触到PICO的C的环境开发,之前的大多开发环境在micropython中来完成RP2040,这次因为板载引脚的数量多,让我尝试搭建C的环境开发的尝试,也了解到C环境的相关开发的经验,后续也能在PICO上完成相关的环境开发,也渐渐熟悉相关板卡平台。