
基于天空星的多功能电子琴桌面小摆件
简介
这个项目是基于立创天空星GD32F407VET6的桌面小摆件,显示时间日期,支持双击、长短按、连击、记录音符长短、暂停播放、加速减速播放、动画和时间显示、蜂鸣器播放音乐、按键扫描消抖互不干扰
简介:这个项目是基于立创天空星GD32F407VET6的桌面小摆件,显示时间日期,支持双击、长短按、连击、记录音符长短、暂停播放、加速减速播放、动画和时间显示、蜂鸣器播放音乐、按键扫描消抖互不干扰开源协议
:GPL 3.0
描述
立创训练营·天空星GD32F407VET6开发板-青春版 的电子琴桌面小摆件
焊接水平一般般哈哈哈~~~
嘉立创的彩色丝印是真的好看,加上面板也是绝了~~~
1. 项目简介
这个项目是基于立创天空星GD32F407VET6的桌面便携式电子小摆件,显示时间日期,也可以是简单的掌上游戏机,无聊的适合玩玩简单解压的小游戏。因为最初设计包含了电子琴功能,加了很多按键,还可以作为电子琴使用。支持充电管理,不用一直插着线供电了,走到哪带到哪。还可以用来学习GD32的开发,这也是我设计这个小玩意的初衷,我参照嘉立创的实战派,给它取个名字,就叫蓝莓派1.0,这也是我第一次做这个,有很多不完善的地方,请大佬帮忙指正,谢谢!!
2. 外设使用
2.1. 1路ADC:采集锂电池电压显示电池电量
GD32F407VET6一共有16个外部ADC通道,我们将光敏电阻模块的AO引脚连接在PC1引脚上,查找数据手册的引脚定义可知,PC1的附加功能有ADC0的通道11,ADC1的通道11,ADC2的通道11。
2.2. 2个串口
2.2.1. 板载串口1调试使用
2.2.2. 扩展板串口2(蓝牙)
2.3. 7路IO输出
2.3.1. 一个无源蜂鸣器
2.3.2. 两个RGB灯6个IO
2.3.3. 1路IO输出开关机
开机POWER_ON,高电平有效
2.4. 1个SPI:W25Q64保存汉字库
2.5. 10个IO输入:
2.5.1. 8个按键输入检测
2.5.2. 锂电池充电CHRG,充电完成的STDBY
2.6. RTC:闹钟日历时间
GD32F4 RTC 闹钟与自动唤醒中断配置深入解析-物联沃-IOTWORD物联网
GD32实战14__RTC_gd32 rtc电路-CSDN博客
32.768KHz
CR2012
2.7. 1个IIC:OLED多级菜单
3. 蜂鸣器播放音乐基本原理
3.1. 音调
频率决定音调,占空比决定音量,这里只用到频率,占空比改变没用,保持50%就行。
PWM的周期为1ms,高电平时间为0.5ms,低电平时间为0.5ms,则频率就为1kHz,占空比就为百分之五十。
pwm的调节作用来源于对“占周期”的宽度控制,“占周期”变宽,输出的能量就会提高,通过阻容变换电路所得到的平均电压值也会上升,“占周期”变窄,输出的电压信号的电压平均值就会降低,通过阻容变换电路所得到的平均电压值也会下降
也就是,在一定的频率下,通过不同的占空比即可得到不同的输出模拟电压,由此,我们可以通过控制PWM输出频率控制蜂鸣器发出不同音调。
Tout
为计算所得产生中断周期,其中arr
为自动重装载值,为psc
预分频,为Tclk
对应时钟频率。
若要使音调对应定时器频率,以低音DO为例,将f=262,arr=1000-1
, 带入上述公式,即可求出的psc
值,修改不同的对应不同的音调。
由定时器周期计算公式我们可知,Tclk为定时器挂靠时钟总线频率,arr为设定定时器自动重装载值,f已知为不同音调频率,图中“dao”频率为262,通过计算得出psc赋给定时器以获得不同音调。
#define proport 168000 //Tclk/(arr+1)=168000000/(1000)
//根据Tout= ((arr+1)*(psc+1))/Tclk推出psc值就是本句define定义的值,Tout为音调频率131Hz的倒数,Tclk=168MHz
#define L1 ((proport/262)-1)//低调 do 的频率
#define L2 ((proport/296)-1)//低调 re 的频率
#define L3 ((proport/330)-1)//低调 mi 的频率
#define L4 ((proport/349)-1)//低调 fa 的频率
#define L5 ((proport/392)-1)//低调 sol 的频率
#define L6 ((proport/440)-1)//低调 la 的频率
#define L7 ((proport/494)-1)//低调 si 的频率
#define M1 ((proport/523)-1)//中调 do 的频率
#define M2 ((proport/587)-1)//中调 re 的频率
#define M3 ((proport/659)-1)//中调 mi 的频率
#define M4 ((proport/699)-1)//中调 fa 的频率
#define M5 ((proport/784)-1)//中调 sol的频率
#define M6 ((proport/880)-1)//中调 la 的频率
#define M7 ((proport/988)-1)//中调 si 的频率
#define H1 ((proport/1048)-1)//高调 do 的频率
#define H2 ((proport/1176)-1)//高调 re 的频率
#define H3 ((proport/1320)-1)//高调 mi 的频率
#define H4 ((proport/1480)-1)//高调 fa 的频率
#define H5 ((proport/1640)-1)//高调 sol的频率
#define H6 ((proport/1760)-1)//高调 la 的频率
#define H7 ((proport/1976)-1)//高调 si 的频率
#define Z0 0//
3.2. 音阶
点在上面为高音,没有点为中音,点在下面为低音
3.3. 音长
简谱对应音阶下无横线为一拍,有单横线为半拍,双横线为1/4拍。音阶数字后有点加半拍音长,有横线加一拍。
const uint16_t music4[]=
{
//我的心总是不安。
L6,25,L6,25,M3,25,M4,25,M3,25,M4,25,M3,50,L6,50,
//哦,我现在已病人膏肓。
Z0,50,L6,50,L5,25,L6,25,L6,25,L5,25,L6,50,L6,50,M1,100,
//哎哟!难道,真的因你而疯狂吗?
M4,100,M3,100,L6,50,L6,50,Z0,50,L6,50,L6,100,M3,25,M4,25,M3,25,M4,25,M3,50,L6,50,
//嗯,我本来不是这种人,因你变成奇怪的人。
Z0,50,L6,50,L5,25,L6,25,L6,25,L5,25,L6,50,L6,50,L6,100,Z0,50,L6,50,L5,25,L6,25,L6,25,L5,25,L6,50,L6,50,M1,100,
//第一次呀变成这样的我
H1,100,M7,50,M5,50,M5,50,M3,50,M3,50,M3,50,H1,100,M7,50,M5,50,M5,50,M3,50,M3,50,M5,150,
//不管我怎么去否认
Z0,50,M3,50,M3,50,M3,50,M5,50,M5,50,M5,50,M6,50,M7,50,
//只因你太美,baby
L6,25,L6,25,M3,50,M3,50,L6,100,Z0,100,Z0,50,L6,25,L6,25,L6,50,Z0,50,Z0,150,
//只因你太美,baby
Z0,50,L6,25,L6,25,M3,50,M3,50,L6,100,Z0,100,Z0,50,L6,25,L6,25,
//只因你是在是太美,baby,
L6,50,Z0,50,Z0,50,L6,25,L6,25,L6,50,L6,25,L6,25,M3,50,M3,50,L6,100,Z0,100,Z0,50,L6,25,L6,25,L6,50,Z0,50,
};
uint16_t length4 = sizeof(music4)/sizeof(music4[0]);
void music_play(const uint16_t* music_buf,uint16_t length)
{
clear_buzzer_buf();
OLED_DrawRectangle(122,8,3,56,2);//清除进度条
buzzer_record_mode =1;//音符记录模式
/*
如果这里不把音乐的音符数据music_buf通过buzzer_beep_tone,
存到待播放buzzer_beep_data[]里面去就会,
就需要用while等一个音符播放完,才能播放下一个音符,这样程序暂时会卡死在这个while里面,
无法相应其他的操作,比如刷新OLED的显示时间,但是如果用for把要播放的音符一次性都转移到待播放数组里面,
然后再放在定时器中断里面一直扫描就行,牺牲空间换取了时间,这里也体现了任务调度的重要性,任务一多还是要用RTOS会更好一点
*/
for(int i=0;(i<(length/2));i++)
{
buzzer_beep_data_write_pos++;//记录模式,记录每个按下的音符
if (buzzer_beep_data_write_pos >= MAX_BUZZER_DATA_SIZE)
buzzer_beep_data_write_pos = 0;
buzzer_beep_tone(music_buf[i*2],100,(music_buf[i*2+1]),0);//赋值
}
buzzer_record_play = 1;//记录完就播放
buzzer_record_mode = 2;//记录播放进入播放音乐模式
}
4. 按键扫描消抖实现长短按,双击,连击
4.1. 头文件宏定义
#define CONFIRM_TIME 10//按键消抖时间窗10ms
#define LONGPRESS_TIME 1000//长按时间窗1000ms
#define FAST_PRESS_TIME 10//长按触发执行10ms
#define DOUBLE_PRESS_TIME 130//连击间隔130ms
#define KEY1_SHORT_PRESS 0X01
#define KEY1_DOUBLE_PRESS 0x11
#define KEY1_FAST_PRESS 0X71
#define KEY1_LONG_PRESS 0X81
#define KEY2_SHORT_PRESS 0X02
#define KEY2_DOUBLE_PRESS 0x12
#define KEY2_FAST_PRESS 0X72
#define KEY2_LONG_PRESS 0X82
#define KEY3_SHORT_PRESS 0X03
#define KEY3_DOUBLE_PRESS 0x13
#define KEY3_FAST_PRESS 0X73
#define KEY3_LONG_PRESS 0X83
#define KEY4_SHORT_PRESS 0X04
#define KEY4_FAST_PRESS 0X74
#define KEY4_LONG_PRESS 0X84
#define KEY5_SHORT_PRESS 0X05
#define KEY5_FAST_PRESS 0X75
#define KEY5_LONG_PRESS 0X85
#define KEY_NUM_MAX (sizeof(g_gpioList) / sizeof(g_gpioList[0]))
4.2. 初始化
typedef struct
{
rcu_periph_enum rcu;
uint32_t port;
uint32_t pin;
} Key_GPIO_t;
/*按键初始化和扫描用到的结构体数组*/
static Key_GPIO_t g_gpioList[]=
{
{RCU_GPIOC,GPIOC,GPIO_PIN_12},//keyl
{RCU_GPIOC,GPIOC,GPIO_PIN_11},//key2
{RCU_GPIOC,GPIOC,GPIO_PIN_13},//key3
{RCU_GPIOE,GPIOE,GPIO_PIN_5},//key4
{RCU_GPIOC,GPIOC,GPIO_PIN_2}//key5
};
/**
* @function KeyDrvInit
* @brief 按键初始化
* @param none
* @return none
*/
void KeyDrvInit(void)
{
for(uint8_t i=0; i
4.3. 调用
/**
* @function GetKeyVal
* @brief 获取按键状态值
* @param none
* @return 按下返回按键值,没按下返回0
*/
uint8_t GetKeyVal(void)
{
uint8_t res=0;
for(uint8_t i=0; i=255)g_key_cout=0;
break;
case KEY4_FAST_PRESS:
if(++g_key_cout>=255)g_key_cout=0;
break;
case KEY5_SHORT_PRESS:
if(--g_key_cout<=0)g_key_cout=255;
break;
case KEY5_FAST_PRESS:
if(--g_key_cout<=0)g_key_cout=255;
break;
case KEY1_DOUBLE_PRESS:
bsp_led_off(LED2_R);
break;
case KEY2_DOUBLE_PRESS:
bsp_led_off(LED2_B);
break;
case KEY3_DOUBLE_PRESS:
bsp_led_off(LED2_G);
break;
default:
break;
}
}
4.4. 短按(非阻塞)
typedef struct
{
uint16_t key_count1;
uint8_t key_short_flag;
} Key_Info_t;
static Key_Info_t g_keyInfo[KEY_NUM_MAX];
/**
* @function KeyScan
* @brief 按键动态扫描
* @param keyIndex 按键索引值
* @return 返回按键键值
*/
static uint8_t KeyScan(uint8_t keyIndex)
{
uint8_t keyPress;
keyPress = gpio_input_bit_get(g_gpioList[keyIndex].port,g_gpioList[keyIndex].pin);
if (keyPress)
{
g_keyInfo[keyIndex].key_count1=0;
g_keyInfo[keyIndex].key_short_flag=0;
}
else if(!g_keyInfo[keyIndex].key_short_flag)
{
g_keyInfo[keyIndex].key_count1++;
if(g_keyInfo[keyIndex].key_count1 > CONFIRM_TIME)
{
g_keyInfo[keyIndex].key_short_flag=1;
g_keyInfo[keyIndex].key_count1=0;
return (keyIndex+1);
}
}
return 0;
}
4.5. 长短按
typef struct
{
uint32_t key_count;
uint8_t key_lock_flag;
uint8_t key_short_flag;
uint8_t key_long_flag;
} Key_Info_t;
static Key_Info_t g_keyInfo[KEY_NUM_MAX];
/**
* @function KeyScan
* @brief 按键动态扫描
* @param keyIndex 按键索引值
* @return 返回按键键值
*/
static uint8_t KeyScan(uint8_t keyIndex)
{
uint8_t keyPress;
keyPress = gpio_input_bit_get(g_gpioList[keyIndex].port,g_gpioList[keyIndex].pin);
if (keyPress)
{
g_keyInfo[keyIndex].key_count=0;
g_keyInfo[keyIndex].key_lock_flag=0;
g_keyInfo[keyIndex].key_long_flag=0;
if(g_keyInfo[keyIndex].key_short_flag)
{
g_keyInfo[keyIndex].key_short_flag=0;
return (keyIndex+1);
}
}
else if((!g_keyInfo[keyIndex].key_lock_flag)&&(g_keyInfo[keyIndex].key_long_flag==0))
{
g_keyInfo[keyIndex].key_count++;
if(g_keyInfo[keyIndex].key_count > CONFIRM_TIME)
{
g_keyInfo[keyIndex].key_short_flag=1;
}
if(g_keyInfo[keyIndex].key_count > LONGPRESS_TIME)
{
g_keyInfo[keyIndex].key_count=0;
g_keyInfo[keyIndex].key_short_flag=0;
g_keyInfo[keyIndex].key_long_flag=1;
return (keyIndex+0x81);
}
}
return 0;
}
4.6. 连发
typedef struct
{
uint16_t key_count1;
uint16_t key_count2;
uint8_t key_short_flag;
uint8_t key_long_flag;
} Key_Info_t;
static Key_Info_t g_keyInfo[KEY_NUM_MAX];
/**
* @function KeyScan
* @brief 按键动态扫描
* @param keyIndex 按键索引值
* @return 返回按键键值
*/
static uint8_t KeyScan(uint8_t keyIndex)
{
uint8_t keyPress;
keyPress = gpio_input_bit_get(g_gpioList[keyIndex].port,g_gpioList[keyIndex].pin);
if (keyPress)
{
g_keyInfo[keyIndex].key_count1=0;
g_keyInfo[keyIndex].key_short_flag=0;
g_keyInfo[keyIndex].key_long_flag=0;
}
else if(!g_keyInfo[keyIndex].key_short_flag)
{
g_keyInfo[keyIndex].key_count1++;
if(g_keyInfo[keyIndex].key_count1 > CONFIRM_TIME)
{
g_keyInfo[keyIndex].key_short_flag=1;
g_keyInfo[keyIndex].key_count1=0;
return (keyIndex+1);
}
}
else if(g_keyInfo[keyIndex].key_count1 < LONGPRESS_TIME)
{
g_keyInfo[keyIndex].key_count1++;
}
else if(!g_keyInfo[keyIndex].key_long_flag)
{
g_keyInfo[keyIndex].key_long_flag=1;
return (keyIndex+0x81);
}
else
{
g_keyInfo[keyIndex].key_count2++;
if(g_keyInfo[keyIndex].key_count2 > FAST_PRESS_TIME)
{
g_keyInfo[keyIndex].key_count2 =0;
return (keyIndex+0x71);
}
}
return 0;
}
4.7. 长短按+多击+连发
/**
* @function KeyScan
* @brief 按键动态扫描
* @param keyIndex 按键索引值
* @return 返回按键键值
*/
static uint8_t KeyScan(uint8_t keyIndex)
{
uint8_t keyPress;
keyPress = gpio_input_bit_get(g_gpioList[keyIndex].port,g_gpioList[keyIndex].pin);
if (keyPress)
{
g_keyInfo[keyIndex].key_count1=0;
g_keyInfo[keyIndex].key_short_flag=0;
g_keyInfo[keyIndex].key_long_flag=0;
if(g_keyInfo[keyIndex].key_times > 0)
{
g_keyInfo[keyIndex].key_count3++;
if(g_keyInfo[keyIndex].key_count3 > DOUBLE_PRESS_TIME)
{
if(g_keyInfo[keyIndex].key_times == 1)
{
g_keyInfo[keyIndex].key_times = 0;
return (keyIndex+1);
}
else if(g_keyInfo[keyIndex].key_times == 2)
{
g_keyInfo[keyIndex].key_times = 0;
return (keyIndex+0x11);
}
g_keyInfo[keyIndex].key_times = 0;
}
}
}
else if(!g_keyInfo[keyIndex].key_short_flag)
{
g_keyInfo[keyIndex].key_count1++;
if(g_keyInfo[keyIndex].key_count1 > CONFIRM_TIME)
{
g_keyInfo[keyIndex].key_short_flag=1;
g_keyInfo[keyIndex].key_count1=0;
g_keyInfo[keyIndex].key_times++;
g_keyInfo[keyIndex].key_count3 = 0;
}
}
else if(g_keyInfo[keyIndex].key_count1 < LONGPRESS_TIME)
{
g_keyInfo[keyIndex].key_count1++;
}
else if(!g_keyInfo[keyIndex].key_long_flag)
{
g_keyInfo[keyIndex].key_long_flag=1;
g_keyInfo[keyIndex].key_times = 0;
return (keyIndex+0x81);
}
else
{
g_keyInfo[keyIndex].key_count2++;
if(g_keyInfo[keyIndex].key_count2 > FAST_PRESS_TIME)
{
g_keyInfo[keyIndex].key_count2 =0;
return (keyIndex+0x71);
}
}
return 0;
}
5. 项目总结
这个项目大概花了两个月的时间,因为在边实习,边搞,所以都是挤时间做,硬件也遇到一些问题,主要是软件代码方面花的时间最多,这个项目,看起来简单,其实一点也不容易,就是因为容易,所以我才精益求精,支持双击、长短按、连击、记录音符长短、暂停播放、加速减速播放、动画和时间显示、蜂鸣器播放音乐、按键扫描消抖互不干扰,几乎没用到delay,全是动态扫描,没有移植操作系统,后续升级可能会移植,上操作系统还是要简单些,OLED用的硬件iic,还在很多地方用到了c++面向对象的编程思维,这是我第一次用这种编程思维,用了都说好。
这个项目真的是我认真做了很久的项目,一有空就埋在电脑前搞,也有过崩溃的时候,一个bug能困惑我好几天,吃饭睡觉都在想,因为没用操作系统,东西一多,逻辑就有点乱,bug层出不穷,但是解决之后发现,不过如此,哈哈哈。
那个写乐谱让蜂鸣器唱歌,和OLED显示动画给我搞得好累的,一个kunkun跳舞119帧,取模的时候快崩溃了哈哈哈,还好最后也像模像样的播放了。
6. 功能演示
具体功能演示见B站视频:
设计图

BOM


评论