
基于立创天空星的便携式可充电多功能掌机
简介
基于立创天空星GD32F407VET6的多功能掌机,实现时间、日期、闹钟显示设置,无聊的时候玩玩简单解压的怀旧小游戏,放点bgm,玩玩电子琴,支持充电管理,主打便携,还有丝滑多级菜单的UI界面
简介:基于立创天空星GD32F407VET6的多功能掌机,实现时间、日期、闹钟显示设置,无聊的时候玩玩简单解压的怀旧小游戏,放点bgm,玩玩电子琴,支持充电管理,主打便携,还有丝滑多级菜单的UI界面开源协议
:GPL 3.0
(未经作者授权,禁止转载)描述
视频链接:
项目简介
这个项目是基于立创天空星GD32F407VET6的多功能掌机,用开发板自带的RTC外设实现时间、日期、闹钟显示设置,无聊的时候玩玩简单解压的怀旧小游戏,放点bgm。因为最初设计包含了电子琴功能,加了很多按键,所以电子琴的功能我还是保留了。支持充电管理,不用一直插着线供电了,走到哪带到哪,主打一个编写。还可以用来学习GD32的开发,这也是我设计这个小玩意的初衷。
我参照嘉立创的实战派,给它取个名字,就叫小鸡派1.0hhhh
这也是我第一次做这个,有很多不完善的地方,请大佬帮忙指正,谢谢!!
项目功能
- 支持充电管理包括查看电池电量,充电小动画等;
- 支持时间、闹钟、日期的显示和设置;
- 还有电子琴、《贪吃蛇》、《小恐龙快跑》等小游戏;
- 甚至还支持“1bit音乐”播放,后台播放,丝毫不影响走时等其他功能;
- 多级菜单的UI设计,丝滑显示;
- 动态按键扫描,长短按、连击、双击的快捷键控制;
- 具体功能见B站视频演示。
部分功能演示动图:
示例图1--多级菜单:
示例图2--播放音乐界面:
示例图3--晚上灯光模式:
示例图4--充电动画:
示例图5--游戏模式:
图片尺寸问题,详情请看B站视频,谢谢,不看后悔嘻嘻嘻!!!!
项目参数
- 本项目采用PW5100升压芯片,把锂电池3.7V的电压升压
- 本项目采用AP2112K-3.3TRG1稳压芯片,把5V电压稳压3.3V
- 本项目采用TP4056锂电池充电管理芯片,管理锂电池充电
- 本项目采用1.3寸OLED来显示UI界面
原理解析(硬件说明)
-
5V升压电路
采用PW5100,把锂电池的电压升压到5V,这里还用了一个PMOS来隔离,当USB插入,外部输入有5V时,PMOS截止,关闭锂电池的升压,达到省电和保护锂电池的效果,D3二极管用来防止电压短路。
示例图1--5V升压电路:
-
隔离式3.3V稳压电路
AP2112K-3.3TRG1稳压芯片,把5V电压稳压3.3V,这里也用了一个PMOS来隔离,当USB插入,外部输入有5V时,PMOS截止,用外部输入的5V来稳压。
示例图2--隔离式3.3V稳压电路:
-
锂电池电压采样电路
锂电池在充满电的时候,是4.2V;在用完电的时候,不是0V,而是2.7V左右,每个厂家制作的锂电池,略有差异。鉴于锂电池材料的局限性,电压超过4.2V,会发生危险,比如燃烧;电压低于2.7V左右,会造成无法再次充电,总之…锂电池电压过高和过低,都会造成永久损坏。所以,我们的产品在使用锂电池的时候,需要时刻监测锂电池电压。
锂电池电压范围:2.7 ~ 4.2V,图中电阻分压比0.6,参考电压3.3V
则锂电池电压:3.3/4096*BAT_ADC(分压之后读出的电压值)
示例图3--锂电池电压采样电路:
-
锂电池充电管理电路
采用TP4056实现锂电的充电管理,通过配置R11电阻值大小,来设置充电电流,I=1200/R11,充电时,CHRG输出低电平,红色LED3灯点亮,当电池充满电,STDBY输出低电平,绿色LED4灯点亮,
示例图4--锂电池充电管理电路:
-
电源功能隔离电路
采用一个PMOS和一个NPN三极管,还有一个BAT54C来组合实现按键长按开机,再按关机,当有TYPEC插入输入电压的时候,此功能无效。
示例图4--电源功能隔离电路:
-
蜂鸣器播放音乐
频率决定音调,占空比决定音量,这里只用到频率,占空比改变没用,保持50%就行
pwm的调节作用来源于对“占周期”的宽度控制,“占周期”变宽,输出的能量就会提高,通过阻容变换电路所得到的平均电压值也会上升,“占周期”变窄,输出的电压信号的电压平均值就会降低,通过阻容变换电路所得到的平均电压值也会下降。
在一定的频率下,通过不同的占空比即可得到不同的输出模拟电压,由此,我们可以通过控制PWM输出频率控制蜂鸣器发出不同音调。
软件代码
- 动态扫描按键和按键消抖
if (keyPress)
{
if(buzzer_beep_data_write_pos!=0)
{
if(gap_flag == 0)//开始间隔记录
{
gap_flag =2;
temp = get_system_tick();
}
else if(gap_flag == 1)//停止间隔记录,并计算出两个音符之间的间隔
{
gap_flag =2;
notes_gap_length = get_system_tick();
notes_gap_length = notes_gap_length - temp;
if(notes_gap_length >=2500)
notes_gap_length=0;
temp =0;
}
}
if(g_keyInfo[keyIndex].key_short_flag==1 && keyIndex != 7 && keyIndex != 8 && buzzer_record_play == 0)
{
buzzer_beep_tone(key_notes[keyIndex],100,notes_length,notes_gap_length);//赋值
notes_length =0;
lock_flag =0;//松开按键解锁
immediate_buzzer_end_time = 0;
buzzer_beep_port(0, 0);
_is_buzzer_beeping = 0;
notes_gap_length = 0;
gap_flag =0;//开始间隔记录
}
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+0x61);
}
g_keyInfo[keyIndex].key_times = 0;
}
}
}
else
{
if(keyIndex != 7 && keyIndex != 8 && buzzer_record_play == 0)//第八个按键是设置按键
notes_length++;
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;
if(lock_flag == 0 && keyIndex != 7 && keyIndex != 8
&& buzzer_record_play == 0 && work_mode == ORGAN_MODE)//只有暂停了记录的音乐播放才能去打断播放
{
lock_flag = 1;//自锁,防止多次触发,松开按键解锁
if(buzzer_record_mode != 1)
{
buzzer_beep_data_write_pos =0;
}
else
{
buzzer_beep_data_write_pos++;//记录模式,记录每个按下的音符
if (buzzer_beep_data_write_pos >= MAX_BUZZER_DATA_SIZE)
buzzer_beep_data_write_pos = 0;
}
buzzer_beep_data[buzzer_beep_data_write_pos].tone_freq = key_notes[keyIndex];
buzzer_beep_data[buzzer_beep_data_write_pos].volume = 100;
_is_buzzer_beeping=0;
buzzer_beep_immediate(5000);//按下立马响,不管记不记录,5000是因为要支持长按
gap_flag =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;
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;
- 蜂鸣器动态扫描
static uint32_t buzzer_beep_on_time = 0;
static uint32_t buzzer_beep_off_time = 0;
static uint32_t current_tick = 0;
static uint8_t inter_beeping_flag = 0;
current_tick = get_system_tick();
if (immediate_buzzer_end_time != 0)
{
// 处理立即鸣叫的变量,先判断是不是需要现在立马鸣叫
if (immediate_buzzer_end_time >= current_tick)
{
// 没在鸣叫就开始鸣叫
if (_is_buzzer_beeping == 0)
{
buzzer_beep_port(
buzzer_beep_data[buzzer_beep_data_write_pos].tone_freq,
buzzer_beep_data[buzzer_beep_data_write_pos].volume);
}
_is_buzzer_beeping = 1;
}
else
{
immediate_buzzer_end_time = 0;
buzzer_beep_port(0, 0);
_is_buzzer_beeping = 0;
}
}
// 如果现在不需要立即鸣叫
if (immediate_buzzer_end_time < current_tick)
{
// 播放记录的音符
if (buzzer_record_play == 0)
{
// 停止鸣叫
buzzer_beep_port(0, 0);
return;
}
// 如果没在鸣叫
if (inter_beeping_flag == 0)
{
if(buzzer_record_mode == 2)//播放音乐模式
{
buzzer_beep_on_time =
((buzzer_beep_data[buzzer_beep_data_read_pos].on_time)*multiplier_num[multiplier_pos])
+ current_tick;
}
else
{
buzzer_beep_on_time =
((buzzer_beep_data[buzzer_beep_data_read_pos].on_time))
+ current_tick;
}
buzzer_beep_off_time =
buzzer_beep_data[buzzer_beep_data_read_pos].off_time
+ buzzer_beep_on_time;
inter_beeping_flag = 1;
}
if (current_tick < buzzer_beep_on_time)
{
// TODO:可以在这里加入静音功能
if (_is_buzzer_beeping == 0)
{
buzzer_beep_port(
buzzer_beep_data[buzzer_beep_data_read_pos].tone_freq,
buzzer_beep_data[buzzer_beep_data_read_pos].volume);
}
_is_buzzer_beeping = 1;
}
else
{
// 停止鸣叫
buzzer_beep_port(0, 0);
_is_buzzer_beeping = 0;
}
if (current_tick <= buzzer_beep_off_time)
{
return;
}
inter_beeping_flag = 0;
buzzer_beep_data_read_pos++;
if (buzzer_beep_data_read_pos > buzzer_beep_data_write_pos)
{
buzzer_beep_data_read_pos = 0;
buzzer_record_play = 0;//播放完成
}
}
- 蜂鸣器播放音乐
void music_play(const uint16_t* music_buf,uint16_t length)
{
clear_buzzer_buf();
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],50,(music_buf[i*2+1]),0);//赋值
}
buzzer_record_play = 1;//记录完就播放
buzzer_record_mode = 2;//记录播放进入播放音乐模式
}
uint8_t music_play_pos=0;//选曲变量
uint8_t music_switch_flag =0;//切歌标志位 1-切歌
void music_option_play(void)
{
uint8_t Angle = 233;
music_data[music_play_pos].func(music_data[music_play_pos].music_buf,music_data[music_play_pos].music_length);//第一次进入自动放歌
work_mode = MUSIC_PLAY;
while(1)
{
if(alarm_beep_flag != 1)//闹钟没响
{
if(1 == music_switch_flag)//切歌
{
music_switch_flag =0;
music_data[music_play_pos].func(music_data[music_play_pos].music_buf,music_data[music_play_pos].music_length);
OLED_Clear();
}
if(1==oled_bmp_buf[0].oled_show_bmp_flag && buzzer_record_play == 1)
{
oled_bmp_buf[0].oled_show_bmp_flag =0;
OLED_Clear();
OLED_ShowImage(50, 10, 34, 36, gImage_JAY1);
OLED_Rotation_Block(50 + 18, 28, 18, Angle * 360 / 256);
Angle += Menu_RollEvent() * 8;
Angle += 2;
}
OLED_ShowString(2, 40, music_name[music_play_pos], OLED_6X8);//显示音乐名字
// OLED_ShowString(2, 40, "PLAYING!!!", OLED_8X16);
OLED_ShowNum(110,47,multiplier_pos,1,6);//显示播放倍速
OLED_ShowNum(108,55,NOTES_NUM,3,6);//显示音符数量
OLED_DrawRectangle(0,62,100,3,0);//显示进度条的框
if(NOTES_NUM !=0)
{
OLED_DrawRectangle(0,62,buzzer_beep_data_read_pos*(100.0/buzzer_beep_data_write_pos),3,1);
}
if(Key_Back_Get()) //双击快捷退出
{
work_mode = MENU_MODE;
return;
}
}
else if(alarm_beep_flag == 1)
{
OLED_Clear();
OLED_ShowString(40,15, "ALARM!!", OLED_8X16);
OLED_ShowString(27,35, "KEY4 & KEY5!!", OLED_6X8);
OLED_DrawRectangle(25,5,80,50, OLED_UNFILLED);
}
OLED_Update();
}
}
- 多级菜单
void Menu_RunMainMenu(struct Option_Class *Option_List,int8_t option_maxnum)
{
int8_t i;
int8_t Roll_Event = 0; // 记录菜单滚动事件
int8_t Show_i = 0; // 显示起始下标
int8_t Show_i_previous = 4; // 显示起始下标的前一个状态(用于比较)
int8_t Show_offset; // 显示Y轴的偏移
int8_t Cat_i = 1; // 选中下标默认为1,(因为Option_List[0]为"<<<")
int8_t Cur_i = 0; // 光标下标默认为0
/*光标下标限制等于窗口高度减去上下页边距再除以行高,就是窗口最多可以显示几个光标*/
int8_t Cur_i_Ceiling = 1; //主菜单只显示一个光标,显示了就下一个
int8_t Option_Max_Num =option_maxnum-1;
/**********************************************************/
while (1)
{
if(alarm_beep_flag != 1)//闹钟没响
{
if (Key_Enter_Get())
{
/*如果功能不为空则执行功能,否则返回*/
if (Option_List[Cat_i].func)
{
Option_List[Cat_i].func();
work_mode = MENU_MODE;
}
else
{
tips_show_cnt_flag =1;
}
}
/*根据按键事件更改选中下标和光标下标*/
Roll_Event = Menu_RollEvent();
if (Roll_Event)//“-1”也满足条件
{
/*更新下标*/
Cur_i += Roll_Event;
Cat_i += Roll_Event;
/*限制选中下标*/
if (Cat_i >= Option_Max_Num)
{
Cat_i = Option_Max_Num;
}
if (Cat_i < 0)
{
Cat_i = 0;
}
/*限制光标下标*/
if (Cur_i >= Cur_i_Ceiling)
{
Cur_i = Cur_i_Ceiling - 1;
}
if (Cur_i <= 0)
{
Cur_i = 0;
}
}
/**********************************************************/
OLED_ClearArea(0,8,132,56);//不清第一行
OLED_ClearArea(0,0,110,8);//不清除电池标
/*计算显示起始下标*/
Show_i = Cat_i;
if (1) // 增加显示偏移量实现平滑移动
{
if (Show_i - Show_i_previous) // 如果下标有偏移
{
Show_offset = (Show_i - Show_i_previous) * Menu_Global.Font_Width; // 计算显示偏移量
Show_i_previous = Show_i;
}
if (Show_offset)
{
Show_offset /= Menu_Global.Slide_ActSpeed; // 显示偏移量逐渐归零
}
}
if((Show_i+1)<=Option_Max_Num)//加一个限制,防止溢出
{
/*右边菜单项*/
OLED_ShowImage(
/*显示从窗口X起点,最左边菜单项*/
100,
/*显示从窗口Y起点,垂直居中*/
(Menu_Global.Window_H - 8)/2+2,
/*显示宽度范围*/
20,
/*显示高度就是行高(或字高)*/
20,
/*要显示的图片*/
gImageMenu20[Show_i+1]);
}
if((Show_i-1)>=0)
{
OLED_ShowImage(
/*显示从窗口X起点*/
10,
/*显示从窗口Y起点,垂直居中*/
(Menu_Global.Window_H - 8)/2+2,
/*显示宽度范围*/
20,
/*显示高度就是行高(或字高)*/
20,
/*要显示的图片*/
gImageMenu20[Show_i-1]);
}
/*中间放大的菜单选项*/
OLED_ShowImage(
/*显示从窗口X起点, 加上左右页边距*/
10 + Menu_Global.Window_X + 40 + Show_offset,
/*显示从窗口Y起点,垂直居中*/
(Menu_Global.Window_H-8)/2-10,
/*显示宽度范围*/
32,
/*显示高度就是行高(或字高)*/
32,
/*要显示的图片*/
gImageMenu32[Show_i]);
if(Menu_Global.CursorStyle == mouse2)
{
Menu_ShowCursor( Menu_Global.Window_X + 19 +Show_offset, //显示从窗口X起点, 加上左右页边距
(Menu_Global.Window_H - Menu_Global.Font_Height)/2-30, //显示从窗口Y起点,垂直居中
42,32,
Menu_Global.CursorStyle, // 光标状态
Menu_Global.Cursor_ActSpeed); // 光标速度
}
else if(Menu_Global.CursorStyle == frame2)
{
Menu_ShowCursor(8 + Menu_Global.Window_X + 40 + Show_offset, //显示从窗口X起点, 加上左右页边距
(Menu_Global.Window_H - Menu_Global.Font_Height)/2-9, //显示从窗口Y起点,垂直居中
34,34,
Menu_Global.CursorStyle, // 光标状态
Menu_Global.Cursor_ActSpeed); // 光标速度
}
else
{
// 调用显示光标函数
Menu_ShowCursor( Menu_Global.Window_X + 45 + Show_offset, //显示从窗口X起点, 加上左右页边距
(Menu_Global.Window_H - Menu_Global.Font_Height)/2-8, //显示从窗口Y起点,垂直居中
42,35,
Menu_Global.CursorStyle, // 光标状态
Menu_Global.Cursor_ActSpeed); // 光标速度
}
OLED_DrawRectangle(Menu_Global.Window_X, Menu_Global.Window_Y, Menu_Global.Window_W, Menu_Global.Window_H, 0); // 显示窗口边框,根据个人喜好选择
/**********************************************************/
/*调试信息*/
// OLED_ShowNum(85, 55, Cat_i, 1, OLED_6X8);
for(uint8_t j=0;j<(Option_Max_Num+1);j++)
{
if(Cat_i == j)
{
OLED_DrawRectangle(48+j*7,58,1,3,OLED_FILLED);
}
OLED_DrawRectangle(48+j*7,61,1,2,OLED_UNFILLED);
}
/**********************************************************/
battery_show();
OLED_Printf(0,0,6,"%02d:%02d:%02d",BCD_TO_DEC(rtc_get_time.hour),
BCD_TO_DEC(rtc_get_time.minute),
BCD_TO_DEC(rtc_get_time.second));//调用字体模式结构体显示“19:06:42”
if(_is_buzzer_beeping == 0){
OLED_ShowImage(82,0,12,8,gImage_mute);
}
else{
OLED_ShowImage(82,0,12,8,gImage_beep);
}
// if(tips_show_cnt_flag == 1) //弹窗
// {
// OLED_ClearArea(25,5,80,50);
// OLED_ShowString(40,15, "EMPTY!!", OLED_8X16);
// OLED_DrawRectangle(25,5,80,50, OLED_UNFILLED);
// }
// else if(tips_show_cnt_flag == 0)
// {
// tips_show_cnt_flag =2;
// OLED_ClearArea(25,5,80,50);
// }
}
else if(alarm_beep_flag == 1)
{
OLED_Clear();
OLED_ShowString(40,15, "ALARM!!", OLED_8X16);
OLED_ShowString(27,35, "KEY4 & KEY5!!", OLED_6X8);
OLED_DrawRectangle(25,5,80,50, OLED_UNFILLED);
}
OLED_Update();
}
}
注意事项
- 接锂电池的时候别接反了
- 锂电池不可以过充过放
- 注意防水处理
组装流程
设计图

BOM


评论