一、项目简介
本项目基于RP2350B核心板与配套电子琴扩展板,设计并实现了一款功能完善的电子琴系统。该系统支持按键实时弹奏、和弦播放、谐波增强音色、旋律录制与回放,并可通过切换开关选择蜂鸣器或扬声器作为音频输出设备,具备良好的交互体验和音质表现。
二、硬件组成
- 核心控制器:RP2350B开发板,其主要特点包括:两个核心,最高运行频率均为150MHz;灵活的时钟系统;丰富的GPIO 引脚和多种接口,如SPI、I2C、UART 和PWM。RP2350B 还支持USB 1.1 主机和设备模式,并可作为大容量存储器进行程序拖放式下载。
- 电子琴扩展板 :包含13个钢琴键+2个上下键,1个拨码开关,1个音量调节滚轮,LPF电路,蜂鸣器和扬声器。
- 音频输出:扬声器可通过PWM+低通滤波实现音频输出,蜂鸣器则通过PWM+三极管输出。
- 按键模块:13个音符按键 + “上”/“下”两个音程扩展键 + 模式切换开关
三、电路介绍
核心板和电子琴扩展板均有主办方提供,这里介绍使用到的部分电路。
1. 扬声器输出:PWM + 低通滤波实现DAC功能
由于RP2350B芯片不具备原生DAC模块,本项目采用PWM结合低通滤波器(LPF)来模拟模拟音频输出。具体设计如下:
- 设定PWM频率为250kHz,以满足音频信号中的高频部分,确保波形不失真。
- 采样点数量为600个,可精细还原20kHz以内的音频信号。
- PWM的占空比根据目标音频波形进行实时调整,经过LPF平滑后生成对应的模拟电压,驱动扬声器发声。
2. 核心板R-2R按键:ADC检测
R-2R电路是基于电阻串并特性构建的很有趣的层级衰减电路,即2R和2R电阻并联得到1R电阻,基于与1R电阻串联得到2R电阻,又重复并连与串联,使得电压输出呈现1/2的衰减。此电路不仅可以用在按键检测上,也可以用于模拟电压输出上(DAC)。
对于该电路,通过核心板的ADC功能读取模拟电压值,可通过二分法判断各个按键状态。
3. 核心板LED驱动:引脚的三态控制
单片机开关具备多种输入输出模式,在输出高电平、输出低电平、高阻态三种状态之间进行切换,可完成对更多的LED控制。
四、软件流程和关键代码
1. PWM配置:pwm.c
// 设置 AUDIO_PIN 为输出模式
gpio_set_function(pwm_pin, GPIO_FUNC_PWM);
// 获取与 AUDIO_PIN 关联的 PWM 通道
uint slice_num = pwm_gpio_to_slice_num(pwm_pin);
// 设置 PWM 回调函数
pwm_clear_irq(slice_num);
pwm_set_irq_enabled(slice_num, true);
irq_set_exclusive_handler(PWM_IRQ_WRAP, on_pwm_wrap); // 全局时钟更新
irq_set_enabled(PWM_IRQ_WRAP, true);
// 配置 PWM 设置
pwm_config config = pwm_get_default_config();
// 设置 PWM 分频和占空比,其中频率计算为f=sys_clk/div/(wrap+1)。目前计算结果为 150 000 000/1/250 000=600 Hz
pwm_config_set_clkdiv(&config, 1.0f); // 时钟分频,调整 PWM 输出频率
pwm_config_set_wrap(&config, AUDIO_MAX_LEVEL); // 设置计数器最大值(PWM周期)
pwm_init(slice_num, &config, true); // 初始化 PWM 并启动
// 设置 PWM 占空比(0 到 wrap 的范围内)
pwm_set_gpio_level(pwm_pin, 0);
2. 音调与谐波合成:audio.c
- 基础音调:由基频freq正弦波构成,不同音高对应不同频率。
- 音色增强:为提升听感效果,在基频的基础上添加最多14阶谐波Amp[numharmonic],模拟实际乐器中丰富的频谱特性。
- 包络控制:音调随时间进行动态调制,加入快速上升与缓慢衰减的幅度包络attenuation,模拟自然的音量变化过程。
#define numharmonic 15
float Amp[numharmonic] = {1, 0.340, 0.102, 0.085, 0.070, 0.065, 0.028, 0.085, 0.011, 0.030, 0.010, 0.014, 0.012, 0.013, 0.004};
/**
* 具有谐波和衰减特性的正弦波叠加
*/
#define harmonic_sine_k 2 * _PI / 1000000.0f
float harmonic_sine_wave(uint64_t time_us, float freq, uint64_t time_start)
{
// 计算频率叠加
float res = 0;
for (int i = 0; i < 15; i++) {
res += _sin(harmonic_sine_k * freq * (i + 1) * (time_us % 10000000)) * Amp[i];
}
res = res * attenuation((time_us - time_start) / 1000);
return res;
}
3. 和弦功能:audio.c
支持多个按键同时按下,系统可将不同频率的音符及其对应谐波信号进行叠加,实现和弦演奏效果。
/**
* 继进行叠加多个按键
*/
float merged_harmonic_sine_wave(uint64_t time_us, float *freqs, uint64_t *times, uint8_t* states)
{
float res = 0;
for (uint8_t i = 0; i < 13; i++) {
if (states[i]) {
res += harmonic_sine_wave(time_us, freqs[i], times[i])*0.9; // 叠加多个频率,增加一个衰减防止失真
}
}
return res;
}
4. 2-2R按键控制:adckey.c
这里使用12位ADC读取引脚电压,判断前三个按键的状态:
#define KEYPAD_PIN 47
#define KEY_THRESHOLD_HIGH 4095
// 读取按键的状态
uint8_t read_keypad() {
// 读取 ADC 值(范围 0 到 4095)
uint16_t adc_value = adc_read(); // 12 位 ADC 读取值
if(adc_value<(KEY_THRESHOLD_HIGH/2-10)) {
return 1;
}else if((adc_value%2048)<(KEY_THRESHOLD_HIGH/4-10)) {
return 2;
}else if((adc_value%1024)<(KEY_THRESHOLD_HIGH/8-10)) {
return 3;
}
return 0; // 没有按键被按下
}
5. 音乐录制与回放:recorder.c
- 系统利用RP2350B内置Flash,分配4KB空间用于旋律数据的存储。
- 记录内容包括:按键音高、按压时间、按下/释放状态等。
- Flash写入前需执行擦除操作,符合Flash存储特性。
- 回放时系统按顺序解析数据并通过PWM输出音频信号,自动重现用户演奏的旋律。
// 1. 一个音符存储使用7字节
recorder_data[recorder_index] = mode;
recorder_data[recorder_index + 1] = (uint8_t)(tone >> 8); // 高字节
recorder_data[recorder_index + 2] = (uint8_t)(tone & 0xFF); // 低字节
recorder_data[recorder_index + 3] = (uint8_t)(time_us >> 24); // 时间高字节
recorder_data[recorder_index + 4] = (uint8_t)(time_us >> 16);
recorder_data[recorder_index + 5] = (uint8_t)(time_us >> 8);
recorder_data[recorder_index + 6] = (uint8_t)(time_us); // 时间低字节
// 2. 存储数据到flash
irq_set_enabled(PWM_IRQ_WRAP, false); // 停止PWM中断,防止写入错误
{
flash_erase(FLASH_TARGET_OFFSET, MAX_DATA_SIZE);
sleep_ms(5); // 添加延时,否则会死机
flash_write(FLASH_TARGET_OFFSET, recorder_data, MAX_DATA_SIZE);
sleep_ms(5);
}
irq_set_enabled(PWM_IRQ_WRAP, true);
// 3. 播放旋律按照时间顺序播放
stored_tone = (recorder_data[recorder_index + 1] << 8) | recorder_data[recorder_index + 2];
stored_time_us = ((uint64_t)recorder_data[recorder_index + 3] << 24) |
((uint64_t)recorder_data[recorder_index + 4] << 16) |
((uint64_t)recorder_data[recorder_index + 5] << 8) |
(recorder_data[recorder_index + 6]);
// 比较时间,检查是否达到了播放的时间点
if (stored_time_us <= elapsed_time)
{
// 如果当前时间 >= 存储的时间,播放数据
*mode = stored_mode;
*tone = stored_tone;
// 移动到下一条记录
recorder_index += 7;
return 2; // 返回播放数据
}
6. 音频设备切换功能:piano.c
支持两种播放方式,可通过物理拨码开关在蜂鸣器与扬声器间切换。系统根据拨码状态初始化对应的输出引脚并重启音频模块。
// 1. 读取mode引脚状态
mode_now = mode_state;
if (mode_state == 0) // 喇叭
audio_init(BEEP_PIN);
else // 蜂鸣器
audio_init(SPEAKER_PIN);
// 2. 读取到拨码开关后进行重启
void software_reset_simple()
{
watchdog_reboot(0, 0, 0); // 立即触发看门狗复位
}
五、功能展示
由于钢琴弹奏主要通过声音展示,请查看上方的视频。主要展示内容包括:
- 单个按键弹奏播放钢琴音色,上下按键切换频率范围。
- 单个按键长按播放随时间变化的强度。
- 多个按键弹奏和弦。
- 存储旋律与回放旋律。
- 切换蜂鸣器弹奏。
六、关键问题与解决方案
问题 | 解决策略 |
---|---|
PWM输出不够平滑,音色失真 | PWM频率至250kHz,占空比可设置0-599,使用阶梯值平滑控制 |
Flash存储无法重复写入 | 在每次写入前进行擦除操作 |
多音符叠加导致失真 | 控制最大谐波阶数并加入限幅机制 |
音色不佳 | 通过基频+谐波共15个频率按不同幅度叠加,并模拟钢琴按下时音量先增加后减少特性控制幅度 |
七、复刻教程
1. 工程创建
- 推荐在良好网络环境下使用VSCode+Raspberry PI Pico插件创建工程。
CMakeLists.txt
需提前声明所需模块(如pwm
、flash
、adc
等)。
2. 烧录流程
- 默认支持一键烧录。
- 若遇到单片机死机导致烧录失败,可:
- 断电
- 短接Boot引脚
- 上电进入Bootloader模式
- 再次烧录恢复
八、项目总结与心得体会
通过本项目,我们深入理解了数字音频合成、PWM模拟DAC、谐波叠加、Flash存储机制、嵌入式系统设计等关键技术。项目不仅提升了我们对嵌入式软硬件协同设计的能力,也激发了我们对音频数字处理、乐器仿真等方向的进一步探索兴趣。