项目介绍
本项目基于树莓派的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;
}
数码管显示
核心板上的数码管是共阴极的,通过两颗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软件按照下方配置可以将字模输出。
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。
实物演示
心得体会
本次活动让我初步了解和学习了rp2350B这颗单片机,尤其是其同时包含arm和riscv两个架构,让我耳目一新。在完成项目的过程中,选择合适的软件框架花费了大量的时间。一开始想尝试micropython,但是只找到了rp2350a的可用固件。之后发现了circuit python可用,但是它的资料相对较少,我对其也不是很熟悉(后续学习看看),就放弃了使用python框架。在vscode上安装官方的插件后,例程老是下载失败,使用csdk的方案也就放弃了。最后arduino的环境顺利的安装好,也就选择了arduino。
本次活动中,硬件中的led引脚复用和按键的实现方式让我学习到不少。