发作品签到
专业版

基于立创天空星的便携式可充电多功能掌机

工程标签

506
0
0
2

简介

基于立创天空星GD32F407VET6的多功能掌机,实现时间、日期、闹钟显示设置,无聊的时候玩玩简单解压的怀旧小游戏,放点bgm,玩玩电子琴,支持充电管理,主打便携,还有丝滑多级菜单的UI界面

简介:基于立创天空星GD32F407VET6的多功能掌机,实现时间、日期、闹钟显示设置,无聊的时候玩玩简单解压的怀旧小游戏,放点bgm,玩玩电子琴,支持充电管理,主打便携,还有丝滑多级菜单的UI界面
基于立创开发板电子设计征集令
复刻成本:99

开源协议

GPL 3.0

(未经作者授权,禁止转载)
创建时间:2024-09-18 23:41:11更新时间:2024-10-28 01:33:13

描述

视频链接:

B站视频--功能演示及介绍

image.png
image.png

项目简介

这个项目是基于立创天空星GD32F407VET6的多功能掌机,用开发板自带的RTC外设实现时间、日期、闹钟显示设置,无聊的时候玩玩简单解压的怀旧小游戏,放点bgm。因为最初设计包含了电子琴功能,加了很多按键,所以电子琴的功能我还是保留了。支持充电管理,不用一直插着线供电了,走到哪带到哪,主打一个编写。还可以用来学习GD32的开发,这也是我设计这个小玩意的初衷。
我参照嘉立创的实战派,给它取个名字,就叫小鸡派1.0hhhh
这也是我第一次做这个,有很多不完善的地方,请大佬帮忙指正,谢谢!!

项目功能

  • 支持充电管理包括查看电池电量,充电小动画等;
  • 支持时间、闹钟、日期的显示和设置;
  • 还有电子琴、《贪吃蛇》、《小恐龙快跑》等小游戏;
  • 甚至还支持“1bit音乐”播放,后台播放,丝毫不影响走时等其他功能;
  • 多级菜单的UI设计,丝滑显示;
  • 动态按键扫描,长短按、连击、双击的快捷键控制;
  • 具体功能见B站视频演示。

部分功能演示动图:

示例图1--多级菜单:
c1319fdedd586afa592323bd17baf130.gif
示例图2--播放音乐界面:
b5dcdca7de3e21520b02acb69aa082e3.gif

示例图3--晚上灯光模式:
65985a5480568e492f3e7f08cc3a13d5.gif

示例图4--充电动画:
45a627b5a13e7af516cc3ca6e58b7040.gif

示例图5--游戏模式:
1d6ad1a6cd4adc8d31bd0c970ada2999.gif
299c7e6a1bae509408608426600db894.gif

图片尺寸问题,详情请看B站视频,谢谢,不看后悔嘻嘻嘻!!!!

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升压电路:

image.png

  • 隔离式3.3V稳压电路

AP2112K-3.3TRG1稳压芯片,把5V电压稳压3.3V,这里也用了一个PMOS来隔离,当USB插入,外部输入有5V时,PMOS截止,用外部输入的5V来稳压。
示例图2--隔离式3.3V稳压电路:

image.png

  • 锂电池电压采样电路

锂电池在充满电的时候,是4.2V;在用完电的时候,不是0V,而是2.7V左右,每个厂家制作的锂电池,略有差异。鉴于锂电池材料的局限性,电压超过4.2V,会发生危险,比如燃烧;电压低于2.7V左右,会造成无法再次充电,总之…锂电池电压过高和过低,都会造成永久损坏。所以,我们的产品在使用锂电池的时候,需要时刻监测锂电池电压。
锂电池电压范围:2.7 ~ 4.2V,图中电阻分压比0.6,参考电压3.3V
则锂电池电压:3.3/4096*BAT_ADC(分压之后读出的电压值)
示例图3--锂电池电压采样电路:

image.png

  • 锂电池充电管理电路

采用TP4056实现锂电的充电管理,通过配置R11电阻值大小,来设置充电电流,I=1200/R11,充电时,CHRG输出低电平,红色LED3灯点亮,当电池充满电,STDBY输出低电平,绿色LED4灯点亮,
示例图4--锂电池充电管理电路:

image.png

  • 电源功能隔离电路

采用一个PMOS和一个NPN三极管,还有一个BAT54C来组合实现按键长按开机,再按关机,当有TYPEC插入输入电压的时候,此功能无效。
示例图4--电源功能隔离电路:
image.png

  • 蜂鸣器播放音乐

频率决定音调,占空比决定音量,这里只用到频率,占空比改变没用,保持50%就行
image.png

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();
	}
}

注意事项

  • 接锂电池的时候别接反了
  • 锂电池不可以过充过放
  • 注意防水处理

组装流程

image.png
image.png
image.png
image.png

设计图

未生成预览图,请在编辑器重新保存一次

BOM

暂无BOM

附件

序号文件名称下载次数
1
多级菜单.mp4
0
2
小游戏.mp4
0
3
音乐播放.mp4
0
克隆工程
添加到专辑
0
0
分享
侵权投诉

工程成员

评论

全部评论(1)
按时间排序|按热度排序
粉丝0|获赞0
相关工程
暂无相关工程

底部导航