项目总结报告
2023寒假一起练平台(1)- 基于STEP Pico的嵌入式系统学习平台
本次项目有多个项目可以选择,由于单个项目较为容易,于是便产生了将所有内容合并到一起(超多功能,一次满足!)
项目1 - 制作一个反应测试器
项目2 - 制作一个交通灯控制器
项目3 - 制作一个音乐播放器
项目4 - 制作一个电压表
项目5 - 制作一个水平仪
项目6 - 制作一个节日彩灯
项目7 - 制作一个定时报警的时钟
此外,还增加了gif播放功能
由于希望所有功能都集成在一个固件上,所以进行菜单的选择也是必不可少的
一、项目介绍
本次项目是基于硬禾学堂推出的基于树莓派Pico的嵌入式系统学习平台,专为嵌入式系统学习而设计,其可以通过C/C++以及MicroPython编程来学习嵌入式系统的工作原理和应用。
其主控采用Raspberry PI的RP4020微控制器芯片,具有灵活的数字接口,该芯片搭载了ARM Cortex M0+双核处理器,高达133MHz的运行频率,内置了256KB SRAM以及2MB闪存,多达26个多功能的GPIO引脚。在软件上,可使用官方提供的C/C++SDK,当然也可以使用MicroPython和Arduino进行开发,配有完善的开发资料教程,方面用户快速入门并嵌入进产品中。
板卡硬件:
- 2个按键输入
- 4个单色LED
- 12个WS2812B RGB三色灯
- 1个姿态传感器
- 1个128*64 OLED显示屏
- 1个蜂鸣器
- 1个可调电位计(用于电压表)
- 1路音频信号输入(用于示波器)
- 8位R-2R电阻网络构成的DAC(用于DDS信号发生器)
可以看出,其功能是非常丰富的,而且外形精致,可用来实现各种功能。
使用这一个基于树莓派Pico的嵌入式系统学习平台,再编写相应的代码,便可完成此次任务!
二、开发环境准备
1.Platform IO with Visual Studio Code,用于编写代码及下载程序。
2.ComAssistant,用于串口调试
三、软件设计
本次项目使用Arduino框架开发,Arduino上手容易,简单快捷。
综合项目的要求,首先确定各个项目的具体实现需求:
0 - 菜单及按键功能
通过板上的两个按键进行选择,左键为向上,右键为向下,两个按键同时按下为确定
1 - 反应测试器
随机点亮板上的一个LED,按下板上的一个按键,重复多次取平均值,在显示屏上显示出从灯亮到按键之间的时间。
2 - 交通灯控制器
利用板上LED灯模拟显示马路上的交通灯的工作状态切换
3 - 音乐播放器
利用板上的蜂鸣器,播放音乐
4 -电压表
利用板上的电位计调节电压从0-3.3V之间变化,,以图形的方式在OLED显示屏上显示电压值及变化的波形
5 - 水平仪
利用板上的姿态传感器,在OLED屏上通过小球滚动的方式显示板子的倾斜度,板子水平的时候小球处于OLED显示器的中心位置
6 - 节日彩灯
利用板上的12个彩色LED灯,用按键来控制,实现不同的显示效果
7 - 定时报警的时钟
制作一个时钟,可以通过板上的OLED屏来指示时间,并通过蜂鸣器在设定的时间发声,当前时间及闹钟均可设置
8 - GIF播放功能
储存一段GIF图进行播放显示
下面逐个进行说明
0 - 菜单及按键功能,这个是整个项目的基础所在,只有完成好了这个内容,才能进行各项功能的开发。
按键功能使用扫描的方式,在loop循环中不断调用该函数进行按键的判断,并同时更新上一次按键的状态,以此来进行按键状态的切换,响应不同的操作。按键由于只有两个,但是基础的菜单就需要三个(上一个,下一个,确定),于是,在两个按键的基础上,使用判断两个按键同时按下作为确定键。
//按键扫描
void key_scan()
{
key[2].val = 1; //确定键手动置位
for (uint8_t i = 0; i < (sizeof(key) / sizeof(KEY)) - 1; ++i) //扫描0和1 两次
{
key[i].val = get_key_val(i); //获取键值
}
while (1)
{
if ((key[0].val == 1) && (key[1].val == 1) && (key[2].val == 1)) //两个按键都没按下,直接退出
{
break;
}
else if (key[0].val == 0) //左键已经被按下,扫描一次右键
{
Serial.print("KEYL ");
key[1].val = get_key_val(1);
}
else if (key[1].val == 0) //右键已经被按下,扫描一次左键
{
Serial.print("KEYR ");
key[0].val = get_key_val(0);
}
if ((key[0].val == 0) && (key[1].val == 0)) //左右键都被按下
{
key[0].val = 1;
key[1].val = 1;
key[2].val = 0;
}
if ((get_key_val(0)) && (get_key_val(1)))
{
break;
}
}
for (uint8_t i = 0; i < (sizeof(key) / sizeof(KEY)); ++i)
{
if (key[i].last_val != key[i].val) //发生改变
{
key[i].last_val = key[i].val; //更新状态
if (key[i].val == LOW)
{
key_msg.id = i;
key_msg.pressed = true;
}
}
}
}
菜单功能使用一个总的UI函数,在loop循环中不断调用该函数进行刷新,并设置一个页面的状态变量,当前页面为几,就调用几的函数进行处理,并绘制屏幕缓冲区,绘制完成后统一再将缓冲区的数据发送到屏幕上,以此达到oled屏的动态显示效果。
void ui_proc() //总的UI进程
{
switch (ui_state)
{
case S_NONE:
if ((ui_index != M_Voltmeter) && (ui_index != M_Gradienter) && (ui_index != M_RYGControl))
u8g2.clearBuffer(); //不清除显示
switch (ui_index)
{
case M_MainLOGO: //主LOGO界面
M_MainLOGO_proc();
break;
case M_MainMenu: //主菜单界面
M_MainMenu_proc();
break;
case M_ReactionTest: //反应测试界面
M_ReactionTest_proc();
break;
case M_RYGControl: //交通灯界面
M_RYGControl_proc();
break;
case M_MusicMenu: //音乐选择界面
M_MusicMenu_proc();
break;
case M_MusicPlay: //音乐播放界面
M_MusicPlay_proc();
break;
case M_Voltmeter: //电压表界面
M_Voltmeter_proc();
break;
case M_Gradienter: //水平仪界面
M_Gradienter_proc();
break;
case M_RGBlights: //节日彩灯界面
M_RGBlights_proc();
break;
case M_TimeClock: //定时时钟界面
M_TimeClock_proc();
break;
case M_TimeClock_Menu: //定时时钟界面
M_TimeClock_Menu_proc();
break;
case M_TimeClock_EDIT: //定时时钟界面
M_TimeClock_EDIT_proc();
break;
case M_VIDEO: //视频播放界面
M_VIDEO_proc();
break;
case M_ABOUT:
M_About_proc(); //关于本机界面
break;
default:
break;
}
break;
case S_DISAPPEAR:
disappear();
break;
default:
break;
}
u8g2.sendBuffer(); //绘制好了就发送显示一次
}
1 - 反应测试器
该功能在每次板子上电时,使用程序生成一个随机数种子,在该功能中,每次产生一个随机时间、随机方向、随机LED灯编号,在随机数的时间后点亮随机方向上的随机编号的LED灯,并开始计时直至用户按下对应侧按键,将本次时间显示在OLED屏幕上,重复该过程10次并计算平均值,再显示出最终的反应时间
randomSeed(27); //随机数种子
void fun1() //反应测试
{
reactiontimeSum = 0; //重置累计时间
u8g2.setFont(u8g2_font_wqy14_t_gb2312a); //设置中文字体
u8g2.setCursor(0, 15);
u8g2.print("说明: 当灯光亮起后"); //设置光标,打印菜单项目
u8g2.setCursor(0, 31);
u8g2.print("请按下对应侧的按键"); //设置光标,打印菜单项目请按下对应侧按键
u8g2.setCursor(0, 47);
u8g2.print("重复十次测试得终值"); //设置光标,打印菜单项目请按下对应侧按键
u8g2.setCursor(12, 63);
u8g2.print("-按任意键启动-"); //设置光标,打印菜单项目请按下对应侧按键
u8g2.sendBuffer();
while (KEYState) //等待按键启动
{
KEYState = (Key_ReadL && Key_ReadR);
}
KEYState = HIGH; //手动拉高
for (int Whilei = 1; Whilei <= WhileNum; Whilei++) //循环10次
{ // For each pixel...
pixels.clear(); // 清除显示
u8g2.clearBuffer();
u8g2.drawStr(20, 20, "test"); // write something to the internal memory
u8g2.setCursor(60, 20);
u8g2.print(Whilei);
u8g2.drawStr(80, 20, "begin!"); // write something to the internal memory
u8g2.sendBuffer();
rand1 = random(2);//随机方向
randtime = random(1000, 5000); //随机时间
randColorR = random(10, 40); //随机颜色
randColorG = random(10, 40); //随机颜色
randColorB = random(10, 40); //随机颜色
if (rand1 == 0) //右边
{
rand1 = random(1, 4);
}
else //rand1 == 1//左边
{
rand1 = random(7, 10);
}
delay(randtime); //随机延迟时间点亮led
pixels.setPixelColor(rand1, pixels.Color(randColorR, randColorG, randColorB));
pixels.show(); //将更新后的像素颜色发送到硬件
oldtime = millis(); //亮灯马上开始计时
if (rand1 < 5) //右边
{
while (KEYState)
{
KEYState = Key_ReadR;
}
}
else //rand1 == 1//左边
{
while (KEYState)
{
KEYState = Key_ReadL;
}
}
newtime = millis(); //按下按键的时间
Serial.println(rand1);
KEYState = HIGH; //手动拉高
pixels.clear(); // 清除显示
pixels.show(); // 灭灯
reactiontime = newtime - oldtime;
reactiontimeSum += reactiontime; //时间累加
u8g2.clearBuffer(); // clear the internal memory
u8g2.drawStr(20, 20, "test"); // write something to the internal memory
u8g2.setCursor(60, 20);
u8g2.print(Whilei);
u8g2.drawStr(80, 20, "over!"); // write something to the internal memory
u8g2.drawStr(0, 40, "This reaction time is"); // write something to the internal memory
u8g2.setCursor(64, 60);
u8g2.print(reactiontime);
u8g2.drawStr(95, 60, "ms!"); // write something to the internal memory
u8g2.sendBuffer();
delay(3000);
}
pixels.fill(pixels.Color(0, 20, 0), 0, 12);
pixels.show(); //填满绿色
delay(1000);
pixels.fill(pixels.Color(0, 0, 0), 0, 12);
pixels.show(); //填满黑色
delay(1000);
u8g2.clearBuffer(); // clear the internal memory
u8g2.drawStr(5, 20, "All test over!"); // write something to the internal memory
u8g2.drawStr(5, 40, "Your reaction time is"); // write something to the internal memory
u8g2.setCursor(70, 60);
u8g2.print(reactiontimeSum / WhileNum);
u8g2.drawStr(100, 60, "ms!"); // write something to the internal memory
u8g2.sendBuffer();
delay(8000);
}
2 - 交通灯控制器
利用板上LED灯模拟显示马路上的交通灯的工作状态切换,4个路口轮流绿灯黄灯红灯,使用状态机的思想,循环读取当前时间,并检测是否需要跳转到下一个状态,如此循环往复,实现交通灯功能。
void M_RYGControl_proc(void) //3交通灯的界面处理
{
if (key_msg.pressed)
{
key_msg.pressed = false;
ui_state = S_DISAPPEAR;
ui_index = M_MainMenu;
Traffic_state = 0;
Traffic_is_drawed = false;
pixels.clear(); // Set all pixel colors to 'off'
pixels.show(); // Send the updated pixel colors to the hardware.
}
if ((millis()) > lastTime) //控制更新时间
{
Serial.print(Traffic_state);
switch (Traffic_state)
{
case 0: //首次进入
Traffic_state++; //变1
lastTime = millis() + Light_Time_G; //加上绿灯时间
pixels.setPixelColor(2, pixels.Color(0, 20, 0));
pixels.setPixelColor(5, pixels.Color(20, 0, 0));
pixels.setPixelColor(8, pixels.Color(20, 0, 0));
pixels.setPixelColor(11, pixels.Color(20, 0, 0));
break;
case 1: //右绿的时候,进来变黄
Traffic_state++; //变2
lastTime += Light_Time_Y; //加上黄时间
pixels.setPixelColor(2, pixels.Color(40, 40, 0));
break;
case 2: //上黄的时候,进来变上红,右变绿
Traffic_state++; //变3
lastTime += Light_Time_G; //加右绿时间
pixels.setPixelColor(2, pixels.Color(20, 0, 0));
pixels.setPixelColor(5, pixels.Color(0, 20, 0));
break;
case 3:
Traffic_state++; //变4
lastTime += Light_Time_Y; //加右黄时间
pixels.setPixelColor(5, pixels.Color(40, 40, 0));
break;
case 4:
Traffic_state++; //变5
lastTime += Light_Time_G; //加下绿时间
pixels.setPixelColor(5, pixels.Color(20, 0, 0));
pixels.setPixelColor(8, pixels.Color(0, 20, 0));
/* code */
break;
case 5:
Traffic_state++; //变6
lastTime += Light_Time_Y; //加下黄时间
pixels.setPixelColor(8, pixels.Color(40, 40, 0));
break;
case 6:
Traffic_state++; //变7
lastTime += Light_Time_G; //加左绿时间
pixels.setPixelColor(8, pixels.Color(20, 0, 0));
pixels.setPixelColor(11, pixels.Color(0, 20, 0));
/* code */
break;
case 7:
Traffic_state++; //变8
lastTime += Light_Time_Y; //加左黄时间
pixels.setPixelColor(11, pixels.Color(40, 40, 0));
break;
case 8:
Traffic_state = 1; //变1
lastTime += Light_Time_G; //加上绿时间
pixels.setPixelColor(11, pixels.Color(20, 0, 0));
pixels.setPixelColor(2, pixels.Color(0, 20, 0));
/* code */
break;
default:
break;
}
pixels.show();
RYGControl_ui_show();
}
}
3 - 音乐播放器
在网络上找到一款输入简谱转C代码软件MusicEncode,使用该软件将简谱输入,即可生成下相应的数组代码,再对该数组进行解析,最后通过PWM波驱动蜂鸣器播放出来。
void Music_Play(const unsigned char *Sound, unsigned char Signature, unsigned Octachord, unsigned int Speed)
{
unsigned int NewFreTab[12]; //新的频率表
unsigned char i, j;
unsigned int Point, LDiv;
unsigned char Tone, Length, SL, SH, SM, SLen, XG, FD;
unsigned int CurrentFre = 0; //蜂鸣器的频率
unsigned int SoundLength = 0; //乐谱长度
unsigned int LDiv0 = 0; //一分音符的时间
unsigned int LDiv4 = 0; //四分音符的时间
unsigned int Sound_Time = 0; //蜂鸣器持续的时间
unsigned int Sound_Space = 0; //普通音符的停止间隔
Point = 0; //播放进程,初始化为0
// SoundLength = sizeof(Sound) / sizeof(Sound[0]); //获取数组长度
while(Sound[SoundLength] != 0x00) //计算歌曲长度
{
SoundLength+=2;
}
// Serial.println(SoundLength);
for (i = 0; i < 12; i++) // 根据调号及升降八度来生成新的频率表
{
j = i + Signature;
if (j > 11)
{
j = j - 12;
NewFreTab[i] = FreTab[j] * 2; //翻倍
}
else
NewFreTab[i] = FreTab[j];
if (Octachord == 1) //加8度
NewFreTab[i] >>= 2;
else if (Octachord == 3) //减8度
NewFreTab[i] <<= 2;
}
// SoundLength = 0;
// while(Sound[SoundLength] != 0x00) //计算歌曲长度
// {
// SoundLength+=2;
// }
Tone = Sound[Point];
Length = Sound[Point + 1]; // 读出第一个音符和它时时值
LDiv0 = 60000 / Speed *4; // 算出1分音符的长度(ms) //1分钟60*1000=60000ms,除以演奏速度再乘4,即为1分音符的时值
LDiv4 = LDiv0 / 4; // 算出4分音符的长度
Sound_Space = LDiv4 - LDiv4 * SOUND_SPACE; // 普通音最长间隔,为1/5
// Sound_Time = LDiv4 * SOUND_SPACE; // 普通音最长间隔标准,就是音符演奏的时间
while (Point < SoundLength)
{
Serial.print(Point);
SL = Tone % 10; //计算出音符
SM = Tone / 10 % 10; //计算出高低音
SH = Tone / 100; //计算出是否升半
CurrentFre = NewFreTab[SignTab[SL - 1] + SH]; //查出对应音符的频率
if (SL != 0) //首先SL不等于0才进行下面所有步骤
{
if (SM == 1)
CurrentFre >>= 2; //除以2低音
if (SM == 3)
CurrentFre <<= 2; //乘以2高音
SLen = LengthTab[Length % 10]; //算出是几分音符
XG = Length / 10 % 10; //算出音符类型(0普通1连音2顿音)//效果
FD = Length / 100;
LDiv = LDiv0 / SLen; //算出连音音符演奏的长度(多少个ms)
if (FD == 1) //浮点音符加半拍
LDiv = LDiv + LDiv / 2;
switch (XG) //计算音符长度
{
case 0: //普通音符
if (SLen <= 4) //1分,2分,4分音符
{
Sound_Space = LDiv4 - LDiv4 * SOUND_SPACE;
Sound_Time = LDiv - Sound_Space;
}
else //其他
Sound_Space = LDiv4 - LDiv4 * SOUND_SPACE;
Sound_Time = LDiv * SOUND_SPACE;
break;
case 1: //连音,就不间隔了
Sound_Time = LDiv;
Sound_Space = 0;
break;
case 2: //算出顿音的演奏长度
Sound_Time = LDiv / 2;
Sound_Space = Sound_Time ;
break;
default:
break;
}
tone(Buzzer, CurrentFre, Sound_Time); //播放音乐
music_last_time = millis();
while ((millis()-music_last_time) < (Sound_Time + Sound_Space)) //控制速度
{
//播放中
}
}
else if (SL == 0)
{
// Sound_Time = 0; //演奏时间为0
break;
}
Point += 2;
Tone = Sound[Point]; //读出下一个音符和时值
Length = Sound[Point + 1];
}
}
4 -电压表
每次进入该模式,先将不需要刷新的背景框架绘制好,每次更新时,直接使用ADC将电压相对数值读出,为确保数值稳定,ADC每次读取10次取平均值,再将该数值进行换算到坐标,每次更新坐标时,先将该坐标处和右侧2列清除显示,再绘制,以此达到动态刷新显示的效果
void Voltmeter_ui_show() //电压表界面
{
for (int i = 0; i < 10; i++)
{
analogInValue += analogRead(AnalogInPin);
}
analogInValue = analogInValue / 10;
vol_y = map(analogInValue, 0, 4095, 57, 24);
if (!frame_is_drawed) //进入模式后,框架只画一遍
{
u8g2.clearBuffer();
Voltmeter_draw_frame();
vol_y_last = map(analogInValue, 0, 4095, 57, 24);
frame_is_drawed = true;
}
u8g2.drawBox(70, 0, 36, 14);
u8g2.drawVLine(chart_x + 11, 59, 3); //删除下面一行标
if (chart_x == 100) //重新开始
{
chart_x = 0;
u8g2.drawVLine(chart_x + 11, 24, 34); //都变白色
u8g2.drawVLine(112, 59, 3); //底下的标清掉最后一列
}
u8g2.drawVLine(chart_x + 12, 24, 34); //都变为白色
u8g2.drawVLine(chart_x + 13, 24, 34);
u8g2.drawVLine(chart_x + 14, 24, 34);
u8g2.setDrawColor(0); //绘制黑色
u8g2.drawLine(chart_x + 11, vol_y_last, chart_x + 12, vol_y);
u8g2.drawVLine(chart_x + 13, 59, 3); //底下的标黑色
//异或绘制模式
u8g2.setDrawColor(2); //绘制异或
vol_y_last = vol_y;
chart_x += 1;
u8g2.drawBox(70, 0, 36, 14);
u8g2.setDrawColor(1);
u8g2.setCursor(70, 12);
u8g2.print(((float)(analogInValue)) * 3.3 / 4095, 3); //显示电压数值
analogInValue = 0;
}
5 - 水平仪
利用板上的姿态传感器,该传感器为加速度传感器,可获取三轴的加速度值。每次更新时,直接将加速度值读出,为确保数值稳定,每次读取20次取平均值,再将该数值进行换算到OLED屏幕相应的坐标,通过小球滚动的方式显示板子的倾斜度,板子水平的时候小球处于OLED显示器的中心位置。
void Gradienter_ui_show() //水平仪界面显示
{
uint16_t Gradienter_tick = 0;
//循环20次
for (Gradienter_tick = 0; Gradienter_tick < 20; Gradienter_tick++)
{
accelemeter.getXYZ(&Acc_Result_x, &Acc_Result_y, &Acc_Result_z);
Acc_Add_y += Acc_Result_x;
Acc_Add_x += Acc_Result_y;
delay(1);
}
Acc_Add_x = Acc_Add_x / 20;
Acc_Add_y = Acc_Add_y / 20;
u8g2.clearBuffer();
u8g2.setDrawColor(1);
u8g2.drawCircle(64, 32, 1, U8G2_DRAW_ALL); //画外圈的圆
u8g2.drawCircle(64, 32, 30, U8G2_DRAW_ALL); //画外圈的圆
u8g2.drawCircle(64, 32, 15, U8G2_DRAW_ALL); //画内圈的圆
DrawX = map(Acc_Add_x, -31, 32, -70, 70); //-50-50可以扩大
DrawY = map(Acc_Add_y, -31, 32, -70, 70);
DrawX = constrain(DrawX, -30, 30);
DrawY = constrain(DrawY, -30, 30);
u8g2.drawDisc(DrawX + 64, DrawY + 32, 3, U8G2_DRAW_ALL);
u8g2.drawStr(0, 20, "x:"); // write something to the internal memory
u8g2.setCursor(17, 20);
u8g2.print(Acc_Result_x);
u8g2.drawStr(0, 40, "y:"); // write something to the internal memory
u8g2.setCursor(17, 40);
u8g2.print(Acc_Result_y);
u8g2.drawStr(0, 60, "z:"); // write something to the internal memory
u8g2.setCursor(17, 60);
u8g2.print(Acc_Result_z);
Acc_Add_x = 0;
Acc_Add_y = 0;
}
6 - 节日彩灯
使用菜单选择功能,选择不同的彩灯效果,编写彩灯控制程序,驱动WS2812进行显示
void RGBlights_ui_show() //彩灯菜单选择
{
move_bar(&pixel_line_y, &pixel_line_y_trg); //移动进度条
move(&pixel_y, &pixel_y_trg); //移动菜单
move(&pixel_box_y, &pixel_box_y_trg); //移动菜单的框
move_width(&pixel_box_width, &pixel_box_width_trg, pixel_select, key_msg.id); //选项的宽度移动函数
u8g2.drawVLine(126, 0, pixel_total_line_length); //画线
u8g2.drawPixel(125, 0); //画点
u8g2.drawPixel(127, 0);
u8g2.setFont(u8g2_font_wqy14_t_gb2312a); //设置中文字体
for (uint8_t i = 0; i < pixel_num; ++i)
{
u8g2.setCursor(pixel_x, 16 * i + pixel_y + 13);
u8g2.print(pixel[i].select); //设置光标,打印菜单项目
u8g2.drawPixel(125, pixel_single_line_length * (i + 1));
u8g2.drawPixel(127, pixel_single_line_length * (i + 1));
}
u8g2.drawVLine(125, pixel_line_y, pixel_single_line_length - 1);
u8g2.drawVLine(127, pixel_line_y, pixel_single_line_length - 1);
u8g2.setDrawColor(2);
u8g2.drawRBox(0, pixel_box_y, pixel_box_width, 16, 1);
u8g2.setDrawColor(1);
if (pixel_state)
{
switch (pixel_index)
{
case Pixel_1:
colorWipe(pixels.Color(255, 0, 0), 50); // Red
colorWipe(pixels.Color(0, 255, 0), 50); // Green
colorWipe(pixels.Color(0, 0, 255), 50); // Blue
colorWipe(pixels.Color(0, 0, 0, 255), 50); // True white (not RGB white)
pixel_state = false; //执行完就关闭
break;
case Pixel_2:
whiteOverRainbow(75, 5);
pixel_state = false; //执行完就关闭
break;
case Pixel_3:
pulseWhite(5);
pixel_state = false; //执行完就关闭
break;
case Pixel_4:
rainbowFade2White(3, 3, 1);
pixel_state = false; //执行完就关闭
break;
case Pixel_5:
theaterChase(pixels.Color(10, 0, 0), 500);
pixel_state = false; //执行完就关闭
break;
case Pixel_6:
rainbow(500);
pixel_state = false; //执行完就关闭
break;
case Pixel_7:
rainbowCycle(50); //均匀分布彩虹
pixel_state = false; //执行完就关闭
break;
case Pixel_8:
theaterChaseRainbow(500);
pixel_state = false; //执行完就关闭
break;
case Pixel_9:
Colorfulstreamers();
pixel_state = false; //执行完就关闭
break;
case Pixel_10:
gradient();
pixel_state = false; //执行完就关闭
break;
case Pixel_11:
colorWipe(pixels.Color(255, 0, 0), 50); // Red
pixel_state = false; //执行完就关闭
break;
case Pixel_12:
colorWipe(pixels.Color(0, 255, 0), 50); // Green
pixel_state = false; //执行完就关闭
break;
default:
break;
}
}
else if (pixel_state == 0)
{
pixels.clear(); // Set all pixel colors to 'off'
pixels.show(); // Send the updated pixel colors to the hardware.
}
}
uint32_t Wheel(byte WheelPos) {
WheelPos = 255 - WheelPos;
if(WheelPos < 85) {
return pixels.Color(255 - WheelPos * 3, 0, WheelPos * 3);
}
if(WheelPos < 170) {
WheelPos -= 85;
return pixels.Color(0, WheelPos * 3, 255 - WheelPos * 3);
}
WheelPos -= 170;
return pixels.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}
// Fill pixels pixels one after another with a color. Strip is NOT cleared
// first; anything there will be covered pixel by pixel. Pass in color
// (as a single 'packed' 32-bit value, which you can get by calling
// pixels.Color(red, green, blue) as shown in the loop() function above),
// and a delay time (in milliseconds) between pixels.
void colorWipe(uint32_t color, int wait) {
for(int i=0; i<pixels.numPixels(); i++) { // For each pixel in pixels...
pixels.setPixelColor(i, color); // Set pixel's color (in RAM)
pixels.show(); // Update pixels to match
delay(wait); // Pause for a moment
}
}
void whiteOverRainbow(int whiteSpeed, int whiteLength) {
if(whiteLength >= pixels.numPixels()) whiteLength = pixels.numPixels() - 1;
int head = whiteLength - 1;
int tail = 0;
int loops = 3;
int loopNum = 0;
uint32_t lastTime = millis();
uint32_t firstPixelHue = 0;
for(;;) { // Repeat forever (or until a 'break' or 'return')
for(int i=0; i<pixels.numPixels(); i++) { // For each pixel in pixels...
if(((i >= tail) && (i <= head)) || // If between head & tail...
((tail > head) && ((i >= tail) || (i <= head)))) {
pixels.setPixelColor(i, pixels.Color(0, 0, 0, 255)); // Set white
} else { // else set rainbow
int pixelHue = firstPixelHue + (i * 65536L / pixels.numPixels());
pixels.setPixelColor(i, pixels.gamma32(pixels.ColorHSV(pixelHue)));
}
}
pixels.show(); // Update pixels with new contents
// There's no delay here, it just runs full-tilt until the timer and
// counter combination below runs out.
firstPixelHue += 40; // Advance just a little along the color wheel
if((millis() - lastTime) > whiteSpeed) { // Time to update head/tail?
if(++head >= pixels.numPixels()) { // Advance head, wrap around
head = 0;
if(++loopNum >= loops) return;
}
if(++tail >= pixels.numPixels()) { // Advance tail, wrap around
tail = 0;
}
lastTime = millis(); // Save time of last movement
}
}
}
void pulseWhite(uint8_t wait) {
for(int j=0; j<256; j++) { // Ramp up from 0 to 255
// Fill entire pixels with white at gamma-corrected brightness level 'j':
pixels.fill(pixels.Color(0, 0, 0, pixels.gamma8(j)));
pixels.show();
delay(wait);
}
for(int j=255; j>=0; j--) { // Ramp down from 255 to 0
pixels.fill(pixels.Color(0, 0, 0, pixels.gamma8(j)));
pixels.show();
delay(wait);
}
}
void rainbowFade2White(int wait, int rainbowLoops, int whiteLoops) {
int fadeVal=0, fadeMax=100;
// Hue of first pixel runs 'rainbowLoops' complete loops through the color
// wheel. Color wheel has a range of 65536 but it's OK if we roll over, so
// just count from 0 to rainbowLoops*65536, using steps of 256 so we
// advance around the wheel at a decent clip.
for(uint32_t firstPixelHue = 0; firstPixelHue < rainbowLoops*65536;
firstPixelHue += 256) {
for(int i=0; i<pixels.numPixels(); i++) { // For each pixel in pixels...
// Offset pixel hue by an amount to make one full revolution of the
// color wheel (range of 65536) along the length of the pixels
// (pixels.numPixels() steps):
uint32_t pixelHue = firstPixelHue + (i * 65536L / pixels.numPixels());
// pixels.ColorHSV() can take 1 or 3 arguments: a hue (0 to 65535) or
// optionally add saturation and value (brightness) (each 0 to 255).
// Here we're using just the three-argument variant, though the
// second value (saturation) is a constant 255.
pixels.setPixelColor(i, pixels.gamma32(pixels.ColorHSV(pixelHue, 255,
255 * fadeVal / fadeMax)));
}
pixels.show();
delay(wait);
if(firstPixelHue < 65536) { // First loop,
if(fadeVal < fadeMax) fadeVal++; // fade in
} else if(firstPixelHue >= ((rainbowLoops-1) * 65536)) { // Last loop,
if(fadeVal > 0) fadeVal--; // fade out
} else {
fadeVal = fadeMax; // Interim loop, make sure fade is at max
}
}
for(int k=0; k<whiteLoops; k++) {
for(int j=0; j<256; j++) { // Ramp up 0 to 255
// Fill entire pixels with white at gamma-corrected brightness level 'j':
pixels.fill(pixels.Color(0, 0, 0, pixels.gamma8(j)));
pixels.show();
}
delay(1000); // Pause 1 second
for(int j=255; j>=0; j--) { // Ramp down 255 to 0
pixels.fill(pixels.Color(0, 0, 0, pixels.gamma8(j)));
pixels.show();
}
}
delay(500); // Pause 1/2 second
}
// Theater-marquee-style chasing lights. Pass in a color (32-bit value,
// a la pixels.Color(r,g,b) as mentioned above), and a delay time (in ms)
// between frames.
void theaterChase(uint32_t color, int wait) {
for(int a=0; a<10; a++) { // Repeat 10 times...
for(int b=0; b<3; b++) { // 'b' counts from 0 to 2...
pixels.clear(); // Set all pixels in RAM to 0 (off)
// 'c' counts up from 'b' to end of pixels in steps of 3...
for(int c=b; c<pixels.numPixels(); c += 3) {
pixels.setPixelColor(c, color); // Set pixel 'c' to value 'color'
}
pixels.show(); // Update pixels with new contents
delay(wait); // Pause for a moment
}
}
}
// Rainbow cycle along whole pixels. Pass delay time (in ms) between frames.
void rainbow(int wait) {
// Hue of first pixel runs 3 complete loops through the color wheel.
// Color wheel has a range of 65536 but it's OK if we roll over, so
// just count from 0 to 3*65536. Adding 256 to firstPixelHue each time
// means we'll make 3*65536/256 = 768 passes through this outer loop:
for(long firstPixelHue = 0; firstPixelHue < 3*65536; firstPixelHue += 256) {
for(int i=0; i<pixels.numPixels(); i++) { // For each pixel in pixels...
// Offset pixel hue by an amount to make one full revolution of the
// color wheel (range of 65536) along the length of the pixels
// (pixels.numPixels() steps):
int pixelHue = firstPixelHue + (i * 65536L / pixels.numPixels());
// pixels.ColorHSV() can take 1 or 3 arguments: a hue (0 to 65535) or
// optionally add saturation and value (brightness) (each 0 to 255).
// Here we're using just the single-argument hue variant. The result
// is passed through pixels.gamma32() to provide 'truer' colors
// before assigning to each pixel:
pixels.setPixelColor(i, pixels.gamma32(pixels.ColorHSV(pixelHue)));
}
pixels.show(); // Update pixels with new contents
delay(wait); // Pause for a moment
}
}
// Slightly different, this makes the rainbow equally distributed throughout
void rainbowCycle(uint8_t wait) {
uint16_t i, j;
for(j=0; j<256*5; j++) { // 5 cycles of all colors on wheel
for(i=0; i< pixels.numPixels(); i++) {
pixels.setPixelColor(i, Wheel(((i * 256 / pixels.numPixels()) + j) & 255));
}
pixels.show();
delay(wait);
}
}
// Rainbow-enhanced theater marquee. Pass delay time (in ms) between frames.
void theaterChaseRainbow(int wait) {
int firstPixelHue = 0; // First pixel starts at red (hue 0)
for(int a=0; a<30; a++) { // Repeat 30 times...
for(int b=0; b<3; b++) { // 'b' counts from 0 to 2...
pixels.clear(); // Set all pixels in RAM to 0 (off)
// 'c' counts up from 'b' to end of pixels in increments of 3...
for(int c=b; c<pixels.numPixels(); c += 3) {
// hue of pixel 'c' is offset by an amount to make one full
// revolution of the color wheel (range 65536) along the length
// of the pixels (pixels.numPixels() steps):
int hue = firstPixelHue + c * 65536L / pixels.numPixels();
uint32_t color = pixels.gamma32(pixels.ColorHSV(hue)); // hue -> RGB
pixels.setPixelColor(c, color); // Set pixel 'c' to value 'color'
}
pixels.show(); // Update pixels with new contents
delay(wait); // Pause for a moment
firstPixelHue += 65536 / 90; // One cycle of color wheel over 90 frames
}
}
}
7 - 定时报警的时钟
使用RP2040微控制器芯片中内置的RTC功能来自动走时,每次编译程序时都重新设置为当前电脑时间,因此掉电后时间都将重置,在时钟运行时,每间隔一段时间就从RTC获取时间来刷新屏幕的显示,以此达到动态的效果,在时钟界面按确定键可进入设置菜单,选择设置时间或闹钟,进入设置时间或闹钟后,都以当前时间作为初始值,按下左右键可移动光标选择需要编辑的时间,按确定键切换编辑/选择模式,此时按下左右键可减小、增大对应的数值,最后设置完成将光标移动到Return处,按确定键,即可返回时钟界面,此时界面已经刷新为刚才设置的时间,若是设置的闹钟,则还是原来的时间,但是在闹钟时间到之后,蜂鸣器会滴一下提示时间到。
void M_TimeClock_proc(void) //7定时时钟的界面处理
{
if (key_msg.pressed) //如果被按下
{
key_msg.pressed = false;
switch (key_msg.id) //选择按键的号码->左键/右键/确定键
{
case 0:
case 1:
ui_state = S_DISAPPEAR;
ui_index = M_MainMenu; //进入主菜单
break;
case 2:
ui_state = S_DISAPPEAR;
ui_index = M_TimeClock_Menu; //进入时钟菜单界面
break;
default:
break;
}
}
if ((millis() > displayRTCTime_timeout) || (displayRTCTime_timeout == 0))
{
rtc_get_datetime(&currTime);
DateTime now = DateTime(currTime);
time_t utc = now.get_time_t();
sprintf(buf_Time1, "%.2d:%.2d", hour(utc), minute(utc)); //时间hh:mm
sprintf(buf_Time2, "%.2d", second(utc));
buf_Week = weekday(utc); //0为星期一
sprintf(buf_Time3, "%d-%.2d-%.2d", year(utc), month(utc), day(utc));
displayRTCTime_timeout = millis() + DISPLAY_RTC_INTERVAL;
}
TimeClock_ui_show();
}
void M_TimeClock_EDIT_proc(void) //时钟的编辑处理
{
if (key_msg.pressed) //判断哪个键按下
{
key_msg.pressed = false;
switch (key_msg.id)
{
case 0: //左键按下
if (TimeClock_EDIT_ing) //如果在编辑中
{
switch (TimeClock_EDIT_select) //选择编辑的项目
{
case TimeClock_EDIT_YY: //年份减
currTime.year--;
break;
case TimeClock_EDIT_MM1: //月份减
currTime.month--;
break;
case TimeClock_EDIT_DD: //日减
currTime.day--;
break;
case TimeClock_EDIT_HH: //小时减
currTime.hour--;
break;
case TimeClock_EDIT_MM2: //分钟减
currTime.min--;
break;
case TimeClock_EDIT_SS: //秒减
currTime.sec--;
break;
default:
break;
}
}
else //不在编辑中
{
if (TimeClock_EDIT_select != 0)
{
TimeClock_EDIT_select--;
}
}
break;
case 1: //右键按下
if (TimeClock_EDIT_ing) //如果在编辑中
{
switch (TimeClock_EDIT_select) //选择编辑的项目
{
case TimeClock_EDIT_YY: //年份加
currTime.year++;
break;
case TimeClock_EDIT_MM1: //月份加
currTime.month++;
break;
case TimeClock_EDIT_DD: //日加
currTime.day++;
break;
case TimeClock_EDIT_HH: //小时加
currTime.hour++;
break;
case TimeClock_EDIT_MM2: //分钟加
currTime.min++;
break;
case TimeClock_EDIT_SS: //秒加
currTime.sec++;
break;
default:
break;
}
}
else //不在编辑中
{
if (TimeClock_EDIT_select != TimeClock_EDIT_OK)
{
TimeClock_EDIT_select++;
}
}
break;
case 2: //中间键按下,切换是否在编辑中
if (TimeClock_EDIT_select == TimeClock_EDIT_OK) //如果选中返回键
{
ui_state = S_DISAPPEAR;
ui_index = M_TimeClock; //退回时钟
//时间就设置时间,闹钟就设置闹钟
TimeClock_EDIT_ing = 0; //设置为不编辑
TimeClock_EDIT_select = 0;
if (TimeClock_select == 0) //设置时钟
{
rtc_set_datetime(&currTime);
}
else if (TimeClock_select == 1)
{
rtc_set_alarm(&currTime, rtcAlarm_Callback);
}
}
else
{
TimeClock_EDIT_ing = !TimeClock_EDIT_ing;
}
break;
default:
break;
}
int8_t DayMax = RetuenDayMax(currTime.year, currTime.month);
//限制大小值
if (currTime.month == 0)
{
currTime.month = 12;
}
else if (currTime.month == 13)
{
currTime.month = 1;
}
if (currTime.day == 0)
{
currTime.day = 1;
}
else if (currTime.day > DayMax)
{
currTime.day = DayMax;
}
if (currTime.hour == -1)
{
currTime.hour = 23;
}
else if (currTime.hour == 24)
{
currTime.hour = 0;
}
if (currTime.month == -1)
{
currTime.month = 59;
}
else if (currTime.month == 60)
{
currTime.month = 0;
}
if (currTime.sec == -1)
{
currTime.sec = 59;
}
else if (currTime.sec == 60)
{
currTime.sec = 0;
}
}
TimeClock_EDIT_ui_show();
}
8 - GIF播放功能
在网络上找到一款开源的 image2cpp 工具,可以将单幅的图像转化为用于OLED显示的C数组,可同时导入多幅图像,一键生成,感觉比较好用,就把网页离线保存了,并且翻译为中文方便使用。
在使用时,只需确定图片的数量,再使用for循环读取图片数组,进行刷新屏幕显示即可,当刷新帧率足够高时,即可看出来就像是连续播放的视频(GIF)
for (uint16_t i = 0; i < 899; i++)
{
show_badapple(i);
delay(20);
}
void show_badapple(int i)
{
// u8g2.setDrawColor(1);
u8g2.drawBox(20, 0, 88, 64);
// u8g2.setDrawColor(0);
u8g2.drawXBM(20, 0, 88, 64, Gif_BadApple[i]);
// u8g2.setDrawColor(1);
u8g2.sendBuffer();
}
五、遇到的主要难题及解决方法
基本上没有遇到特别困难的问题,主要还是一些逻辑的判断跳转操作,包括菜单,按键,界面动画的实现,同时参考了非常多的资料和大佬的开源方案,得益于Arduino强大的生态环境,很多问题也都可以在互联网上寻找到。
六、后续完善计划
作为一个基本能够使用的多功能树莓派PICO,感觉已经足够,但是还是有许多不足之处可以完善,比如在软件方面仍然有不少的BUG,界面不够精美,操作不太完善等,由于时间关系暂未解决,后续的话有时间再抽空完善一下,这样感觉也会好一点。
工程源码 链接:https://pan.baidu.com/s/1N-c4lgtWCUfYqFamJ-DEQw?pwd=yhxt
提取码:yhxt
七、参考资料
.Arduino https://www.arduino.cc/
PlatformIO https://platformio.org/
.image2cpp http://javl.github.io/image2cpp/
.MusicEncode https://blog.csdn.net/weixin_39663258/article/details/111779310
.丝滑OLED动画教程开源 https://space.bilibili.com/23354755/channel/collectiondetail?sid=837679
.让数据显示更直观——OLED曲线显示 https://zhuanlan.zhihu.com/p/516342880
.ESP32+Arduino+OLED+u8g2播放视频 https://blog.csdn.net/m0_46079750/article/details/128857657