小王同学的多功能STEP Pico
2023年寒假在家一起练(1)-使用STEP Pico的多项目开发,学习RP2040MCU的使用
标签
嵌入式系统
Arduino
树莓派RP2040
寒假一起练
six
更新2023-03-28
河南科技大学
475

项目总结报告

2023寒假一起练平台(1)- 基于STEP Pico的嵌入式系统学习平台

      本次项目有多个项目可以选择,由于单个项目较为容易,于是便产生了将所有内容合并到一起(超多功能,一次满足!)

项目1 - 制作一个反应测试器

FhaVzkl6F4NlVGvpgJ08rYHfyYLp

项目2 - 制作一个交通灯控制器

FpgauDm2Jh2e0ZTmzadKXzKNo4Ue

项目3 - 制作一个音乐播放器

Foz4FDZfM3fJxrRcmUurqiFIs0Vc

项目4 - 制作一个电压表

Fj3hIvPda09l3QWcTthkops6FFvb

项目5 - 制作一个水平仪

FqXEQF4PfjAxkv0RaWWPfWw5B-dN

项目6 - 制作一个节日彩灯

FvvMKyJJj0pNdv3nowDEJZNs9q0j

项目7 - 制作一个定时报警的时钟

FtUaL9VoxI_v42pymmAjMXop9TJVFjRsiSAYGqVdwSaOthzj5--PAkyT

此外,还增加了gif播放功能

Fij0JltWZNTnChchmEfxFk9vfjyO

由于希望所有功能都集成在一个固件上,所以进行菜单的选择也是必不可少的

Fjq-Xhq_MLEoN3vLYA_DeXZkmQjn

 

 

一、项目介绍

      本次项目是基于硬禾学堂推出的基于树莓派Pico的嵌入式系统学习平台,专为嵌入式系统学习而设计,其可以通过C/C++以及MicroPython编程来学习嵌入式系统的工作原理和应用。

      其主控采用Raspberry PI的RP4020微控制器芯片,具有灵活的数字接口,该芯片搭载了ARM Cortex M0+双核处理器,高达133MHz的运行频率,内置了256KB SRAM以及2MB闪存,多达26个多功能的GPIO引脚。在软件上,可使用官方提供的C/C++SDK,当然也可以使用MicroPython和Arduino进行开发,配有完善的开发资料教程,方面用户快速入门并嵌入进产品中。

FgTH28pLI80kAVQXu161Fx1wotU9

 板卡硬件:

  • 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数组,可同时导入多幅图像,一键生成,感觉比较好用,就把网页离线保存了,并且翻译为中文方便使用。

FnspCik-4myLzHnZfo8t7Klmb3Yw

FlSzEcaa3TCzmr5W4mTA-KJCWAUP

       在使用时,只需确定图片的数量,再使用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

 

附件下载
pico_Project.rar
包含主要的源码及可直接下载的固件,由于整个工程过大,上传不了,已上传至百度网盘,链接:https://pan.baidu.com/s/1N-c4lgtWCUfYqFamJ-DEQw?pwd=yhxt 提取码:yhxt
团队介绍
普通在校大学生
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号