2988|3

26

帖子

12

TA的资源

一粒金砂(中级)

楼主
 

【得捷电子Follow me第1期】基于Arduino 玩转RP2040 综合贴(完整内容) [复制链接]

  本帖最后由 genvex 于 2023-6-27 16:50 编辑

 

 


本项目将包括以下内容:

任务一、RP2040的Arduino开发环境建立

     安装板块的支持包

任务二、任务2:驱动外设     

    顺便完成了oled_ssd1036的驱动,强大绘图函数,原生支持中文。

任务3:同步网络时间

   踏破了很多坑,深入学习了时间处理的问题。

任务4:实现定位功能

    自己写了一个专门北斗卫星NAME数据的解析库,顺利完成了定位功能。

任务5:扩展任务

     天气预报站,代码雨,贪吃蛇游戏等。 


任务1:熟悉micropython的基本语法(Arduio更精彩)

     早前开始学编程是从Micropython 开始学习的,发现Micropython在完成一些小型项目还是可以的,可以快速的完成特定的小功能,但是对于一些大型的多任务项目,实现起来就比较吃力了(可能是没学到家),后来就转移到更卷的学习领域,开始学习Arduino来完成一些项目。

经过多次的尝试,Arduino 拥有全球丰富的开源生态,各种野生玩家,玩得不意义乐乎。几乎每个开发板厂家都有自己的一套专业的开发环境,但是从一个Ide转到另一个Ide都是需要学习成本,而且成本还不低,没几个月的浸淫是玩不什么花来的。 然而Arduino提供的平台然所有的开发板可以顺利的连接在一起,可以使用同一种“方言”进行交流,而且性能也不差,跟各自的官方性能相差无几,却不需要重新花费大量的时间去熟悉一套的编程风格和编译环境。所以作为创客必须学会Arduino ,这样就可以轻松地游走在各种开发板之间。

   相比esp32,RP2040的Arduino生态稍微弱一些,官方的pico的Arduino官方支持好像不太给力,但是却有个很给里的第三支持把官方该干的事情都干了。earlephilhower 大佬:具体使用方法参见:

链接已隐藏,如需查看请登录或者注册
。这个第三方板卡支持库,它极大的丰富拓展了PICO的功能,其中一个很牛的特性就是可以实现核心主频从133MHz超频到240MHz,这样就可以达到与esp32相同主频,同时还支持了FreeRTOS实时多任务的性能。本次项目就基于这库来完成所有任务的,可能是本次活动唯一使用Arduino来完成所有项目参赛选手。因为辅导课程的老师也说了用Arduino来完成这些项目会比用Micropython吃力很多,没有办法,因为已经没办法再回到Micropython的时光了,只有义无反顾地去拥抱Arduino的星辰大海,如果你想体验使用Arduino来玩转RP2040,本项目可以让你领略一番别样的风味。

 

扩展板注释图,方便设置引脚,你值得拥有: 

 

 


 

任务二、任务2:驱动外设

驱动LED、OLED显示屏、蜂鸣器等外设。

   选择使用Arduino来完成本次任务,最主要的原因是我发现了一个非常好用的图形驱动库——LovyanGFX,它不仅可以驱动常见的LCD屏幕,同时还可以支持本项目用到的SSD1306。这个库内置了中文支持,使得本项目中的中文显示毫不费力,这对于中文显示需求非常重要。

LovyanGFX是由日本业界大神Lovyan开发的,它的使用方法跟TFT_eSPI差不多。实际上,这个库也是受TFT_eSPI启发,深度改造而来。如果你有试用过TFT_eSPI的经验,迁移过来也是比较轻松的。此外,基于LovyanGFX开发的许多ESP32的应用程序,也可以用在我们的RP2040上。在本次项目中,我完成了SSD1306的适配,并且制作了一个用户配置文件,拿来就可以直接使用了,非常方便。这就是我孤身一人寂寞地走来的原因,希望你也能从中受到启发,这个项目能为你今后的工作带来便利。

 

 

SSD1306驱动支持文件

#pragma once

#define LGFX_USE_V1

#include <LovyanGFX.hpp>

// 在ESP32中单独设置使用LovyanGFX时的设置示例

/*
//ESP 32中独立设定使用 lovyangfx 时的设定示例请复制这个文件,给它一个新的名字,并根据您的环境更改您的设置。
创建的文件可以从用户程序中包含。
复制的文件可以放在图书馆的 lgfx_user 文件夹中使用,但是/n请注意,这可能会在库更新时被删除。
如果您希望安全运行,请创建备份或将其放在用户项目的文件夹中。

*/

//从 lgfx_device 派生并创建一个进行自己设置的类。
class SSD1306 : public lgfx::LGFX_Device

{
/*
您可以将类名称从“LGFX”更改为另一个名称。
如果与AUTODETECT配合使用,请将其更名为LGFX以外的名称,因为使用的是“LGFX”。
另外,如果同时使用多个面板,请为每个面板命名不同的名称。
※更改类名称时,构造函数的名称也必须更改为相同的名称。
名字的命名方法可以自由决定,但设想设定增加的情况,
例如,在ESP32DevKit-C中进行SPI连接的ILI 9341的设定的情况下,
LGFX_DevKitC_SPI_ILI 9341。
像这样的名字,让文件名和类名一致,在使用时变得迷茫不了。
//*/


//提供与要连接的面板嘈拖嗥ヅ涞氖道�

//lgfx::Panel_ILI9341     _panel_instance;
//lgfx::Panel_SH110x      _panel_instance; // SH1106, SH1107
lgfx::Panel_SSD1306     _panel_instance;
//lgfx::Panel_ST7789      _panel_instance;


//提供与要连接的面板类型相匹配的实例。
 // lgfx::Bus_SPI       _bus_instance;   // SPI总线的实例
lgfx::Bus_I2C       _bus_instance;   // I2C总线的实例(仅ESP32)
//lgfx::Bus_Parallel8 _bus_instance;   // 8ビットパラレルバスのインスタンス (ESP32のみ)


//如果可以进行背光控制,则提供实例。(如无必要删除)
//  lgfx::Light_PWM     _light_instance;

//提供与触摸屏类型匹配的实例。(如无必要删除)
//  lgfx::Touch_FT5x06      _touch_instance; // FT5206, FT5306, FT5406, FT6206, FT6236, FT6336, FT6436
//lgfx::Touch_GT911       _touch_instance;
//lgfx::Touch_STMPE610    _touch_instance;
//lgfx::Touch_XPT2046     _touch_instance;

public:

  //创建构造函数,在这里进行各种设置。如果你改变了类的名字,构造函数也应该指定相同的名字。
  SSD1306(void)
  {
    { //进行总线控制的设置。
      auto cfg = _bus_instance.config();  // 获取总线设置的结构。

// SPI设置
      //cfg.spi_host = VSPI_HOST;     // 选择要使用的SPI (vspi_host or hspi_host)
      //cfg.spi_host = HSPI_HOST;     // 选择要使用的SPI (vspi_host or hspi_host)
      //cfg.spi_mode = 0;             // 设置SPI通信模式(0到3)
/*
HSPI和VSPI并不是网友们认为的high-speed SPI 和Very High-speed SPI,HSPI、VSPI是一样的,
只不过是换个名字用于区分,SPI相当于SPI0或SPI1,HSPI相当于SPI2,VSPI相当于SPI3。
ESP32 共有 4 个 SPI 控制器 SPI0、SPI1、SPI2、SPI3,用于连接支持 SPI 协议的设备。
SPI0 控制器作为 cache 访问獠看娲⒌ピ涌谑褂谩�
SPI1 作为主机使用。
SPI2 和 SPI3 控制器既可作为主机使用又可作为从机使用。作主机使用时,每个 SPI 控制器可以使用多个片选信号 (CS0 ~ CS2) 来连接多个 SPI 从机设备。
SPI1 ~ SPI3 控制器共享两个 DMA 通道。
*/
/*      

      cfg.freq_write = 40000000;    // 发送时的SPI时钟(最大80 MHz,80 MHz四舍五入为整数)
      cfg.freq_read  = 16000000;    // 接收时的SPI时钟
      cfg.spi_3wire  = true;        // 用MOSI引脚进行接收时,设定为true。
      cfg.use_lock   = true;        // 如果使用事务锁定,则设置true
      cfg.dma_channel = 1;          // 设置DMA通道(1or 2.0=disable)设置要使用的DMA通道(0=不使用DMA)
      cfg.pin_sclk = 36;            // 设置 SPI 的 sclk 引脚编号
      cfg.pin_mosi = 35;            // 设定 SPI 的 mosi 引脚编号
      cfg.pin_miso = -1;            // 设置SPI的MISO引脚编号(-1=disable)
      cfg.pin_dc   = 33;            // 设置SPI的D/C引脚编号(-1 = disable)
     // 使用与 sd 卡共用的 SPI 总线时,请不要省略 miso,一定要设置 miso。
*/
      // I2C设置
      cfg.i2c_port    = 0;          //  (0 or 1)
      cfg.freq_write  = 400000;     // 写速
      cfg.freq_read   = 400000;     // 收速
      cfg.pin_sda     = 8;         // SDA引脚编号
      cfg.pin_scl     = 9;         // SCL引脚编号
      cfg.i2c_addr    = 0x3C;       // I2C地址



      _bus_instance.config(cfg);    // 将设定值反映在总线上。
      _panel_instance.setBus(&_bus_instance);      // 把总线设置在面板上。
    }

    { // 进行显示面板控制的设置。
      auto cfg = _panel_instance.config();    // 取得显示面板设定用的构造濉�
      cfg.pin_cs           =    -1;  // CS连接的引脚编号(-1=disable)
      cfg.pin_rst          =    -1;  // RST连接的引脚编号(-1=disable)
      cfg.pin_busy         =    -1;  // BUSY连接的引脚编号(-1=disable)

      // ※ 以下的设定值是根据每个面板设定的一般的初始值,不清楚的项目请试着删除。

      cfg.memory_width     =   128;  // 驱动IC所支持的最大宽度
      cfg.memory_height    =   64;  // 驱动IC所支持的最大高度
      cfg.panel_width      =   128;  // 实际上允镜目矶�
      cfg.panel_height     =    64;  // 实际可显示的高度
      cfg.offset_x         =    0;  // 掌控板 需要 面板的X方向偏移量
//      cfg.offset_x         =    0;  // 面板的X方向偏移量
      cfg.offset_y         =    0;  // 面板的Y方向偏移量
      cfg.offset_rotation  =     2;  // 旋转方向上的值的偏移0到7(4到7是上下反转)
      cfg.dummy_read_pixel =     8;  // 像素读取前虚拟读取的位数
      cfg.dummy_read_bits  =     1;  // 像素以外的数据读取前的虚拟读取的位数
      cfg.readable         = false;  // 如果可以读取数据则设置为 true
      cfg.invert           = false;  // 面板的明暗反转的情况下设定为真
      cfg.rgb_order        = false;  // 如果面板中的红色和蓝色被替换,则设置为true
      cfg.dlen_16bit       = false;  // 在以16位为单位发送数据长度的面板的情况下,设定为true
      cfg.bus_shared       = false;  // 如果您与SD卡共享总线,则设置为true(通过drawJpgFile等进行总线控制)

      _panel_instance.config(cfg);
    }

    setPanel(&_panel_instance); // 设置要使用的面板。
  }
};

蜂鸣器—演奏生日歌

 

// The speaker will play the tune to Happy Birthday continuously
// Author: Tony DiCola
// License: MIT (https://opensource.org/licenses/MIT)

#include <Arduino.h>

#ifdef USE_TINYUSB
// For Serial when selecting TinyUSB.  Can't include in the core because Arduino IDE
// will not link in libraries called from the core.  Instead, add the header to all
// the standard libraries in the hope it will still catch some user cases where they
// use these libraries.
// See https://github.com/earlephilhower/arduino-pico/issues/167#issuecomment-848622174
#include <Adafruit_TinyUSB.h>
#endif

// pin_buzzer should be defined by the supported variant e.g CPlay Bluefruit or CLUE.
// Otherwise please define the pin you would like to use for tone output
#ifndef PIN_BUZZER
#define PIN_BUZZER    20
#endif

uint8_t const pin_buzzer = PIN_BUZZER;

// A few music note frequencies as defined in this tone example:
//   https://www.arduino.cc/en/Tutorial/toneMelody
#define NOTE_C4  262
#define NOTE_CS4 277
#define NOTE_D4  294
#define NOTE_DS4 311
#define NOTE_E4  330
#define NOTE_F4  349
#define NOTE_FS4 370
#define NOTE_G4  392
#define NOTE_GS4 415
#define NOTE_A4  440
#define NOTE_AS4 466
#define NOTE_B4  494
#define NOTE_C5  523
#define NOTE_CS5 554
#define NOTE_D5  587
#define NOTE_DS5 622
#define NOTE_E5  659
#define NOTE_F5  698
#define NOTE_FS5 740
#define NOTE_G5  784
#define NOTE_GS5 831
#define NOTE_A5  880
#define NOTE_AS5 932
#define NOTE_B5  988

// Define note durations.  You only need to adjust the whole note
// time and other notes will be subdivided from it directly.
#define WHOLE         2200       // Length of time in milliseconds of a whole note (i.e. a full bar).
#define HALF          WHOLE/2
#define QUARTER       HALF/2
#define EIGHTH        QUARTER/2
#define EIGHTH_TRIPLE QUARTER/3
#define SIXTEENTH     EIGHTH/2

// Play a note of the specified frequency and for the specified duration.
// Hold is an optional bool that specifies if this note should be held a
// little longer, i.e. for eighth notes that are tied together.
// While waiting for a note to play the waitBreath delay function is used
// so breath detection and pixel animation continues to run.  No tones
// will play if the slide switch is in the -/off position or all the
// candles have been blown out.
void playNote(int frequency, int duration, bool hold = false, bool measure = true) {
  (void) measure;

  if (hold) {
    // For a note that's held play it a little longer than the specified duration
    // so it blends into the next tone (but there's still a small delay to
    // hear the next note).
    tone(pin_buzzer, frequency, duration + duration / 32);
  } else {
    // For a note that isn't held just play it for the specified duration.
    tone(pin_buzzer, frequency, duration);
  }

  delay(duration + duration / 16);
}

// Song to play when the candles are blown out.
void celebrateSong() {
  // Play a little charge melody, from:
  //  https://en.wikipedia.org/wiki/Charge_(fanfare)
  // Note the explicit boolean parameters in particular the measure=false
  // at the end.  This means the notes will play without any breath measurement
  // logic.  Without this false value playNote will try to keep waiting for candles
  // to blow out during the celebration song!
  playNote(NOTE_G4, EIGHTH_TRIPLE, true, false);
  playNote(NOTE_C5, EIGHTH_TRIPLE, true, false);
  playNote(NOTE_E5, EIGHTH_TRIPLE, false, false);
  playNote(NOTE_G5, EIGHTH, true, false);
  playNote(NOTE_E5, SIXTEENTH, false);
  playNote(NOTE_G5, HALF, false);
}

void setup() {
  // Initialize serial output and Circuit Playground library.
  Serial.begin(115200);

  pinMode(pin_buzzer, OUTPUT);
  digitalWrite(pin_buzzer, LOW);
}

void loop() {
  // Play happy birthday tune, from:
  //  http://www.irish-folk-songs.com/happy-birthday-tin-whistle-sheet-music.html#.WXFJMtPytBw
  // Inside each playNote call it will play a note and drive the NeoPixel animation
  // and check for a breath against the sound sensor.  Once all the candles are blown out
  // the playNote calls will stop playing music.
  playNote(NOTE_D4, EIGHTH, true);
  playNote(NOTE_D4, EIGHTH);
  playNote(NOTE_E4, QUARTER);       // Bar 1
  playNote(NOTE_D4, QUARTER);
  playNote(NOTE_G4, QUARTER);
  playNote(NOTE_FS4, HALF);         // Bar 2
  playNote(NOTE_D4, EIGHTH, true);
  playNote(NOTE_D4, EIGHTH);
  playNote(NOTE_E4, QUARTER);       // Bar 3
  playNote(NOTE_D4, QUARTER);
  playNote(NOTE_A4, QUARTER);
  playNote(NOTE_G4, HALF);          // Bar 4
  playNote(NOTE_D4, EIGHTH, true);
  playNote(NOTE_D4, EIGHTH);
  playNote(NOTE_D5, QUARTER);       // Bar 5
  playNote(NOTE_B4, QUARTER);
  playNote(NOTE_G4, QUARTER);
  playNote(NOTE_FS4, QUARTER);      // Bar 6
  playNote(NOTE_E4, QUARTER);
  playNote(NOTE_C5, EIGHTH, true);
  playNote(NOTE_C5, EIGHTH);
  playNote(NOTE_B4, QUARTER);       // Bar 7
  playNote(NOTE_G4, QUARTER);
  playNote(NOTE_A4, QUARTER);
  playNote(NOTE_G4, HALF);          // Bar 8

  celebrateSong();

  // One second pause before repeating the loop and playing
  delay(1000);
}

 

任务3:同步网络时间

   踏破了很多坑,深入学习了时间处理的问题。

 

  在这个项目中,最花费我时间的环节是同步网络时间。本来以为将ESP32的时间同步方案直接搬过来就可以轻易解决问题,但是实际情况却远不如想象。我花费了很多的时间进行探索,最终总结出了以下的解决方案。虽然也许会有更加轻松的解决方案出现,但是在反复测试的过程中,我认为这个方案是目前最稳定的。在探索的过程中,我也不断地学习了有关时间处理的问题。如果你也有类似的疑惑,下面的参考资料可以帮到你。

 

网络时间同步

#include <LovyanGFX.hpp>
#include "PICO_SSD1306.hpp"
static SSD1306 lcd;
static LGFX_Sprite canvas(&lcd);  // オフスクリーン描画用バッファ

#include <WiFiUdp.h>
#include <NTPClient.h>
#include <WiFi.h>
#include <TimeLib.h>

//太空人素材
#include "img/pangzi/i0.h"
#include "img/pangzi/i1.h"
#include "img/pangzi/i2.h"
#include "img/pangzi/i3.h"
#include "img/pangzi/i4.h"
#include "img/pangzi/i5.h"
#include "img/pangzi/i6.h"
#include "img/pangzi/i7.h"
#include "img/pangzi/i8.h"
#include "img/pangzi/i9.h"

uint8_t *spacemen[10] = {
  (uint8_t *)i0,
  (uint8_t *)i1,
  (uint8_t *)i2,
  (uint8_t *)i3,
  (uint8_t *)i4,
  (uint8_t *)i5,
  (uint8_t *)i6,
  (uint8_t *)i7,
  (uint8_t *)i8,
  (uint8_t *)i9,
};

// Replace with your network credentials
const char *ssid = "your ssid";
const char *password = "your password;
// Define NTP Client to get time
const long utcOffsetInSeconds = 3600 * 8;   // 中国时差快8小时
char hour_char[30];

WiFiUDP ntpUDP;
// NTPClient timeClient(ntpUDP, "pool.ntp.org");
NTPClient timeClient(ntpUDP, "ntp6.aliyun.com", utcOffsetInSeconds);
int lastSecond;

/*获取时间的呒菇╓iFiUDP 实例作为,NTPClient的参数,启动WiFi后获取网络时间。
  获取到时间截后,用TimeLib库进行本地时间管理,即可断开网络
方法和esp32差别明显,因为esp32已经将获取时间高度封装,更加简单*/


void setup() {
  // Connect to Wi-Fi
  WiFi.begin(ssid, password);                 // 开始连接WiFi网络
  while (WiFi.status() != WL_CONNECTED) {     // 如果WiFi没有连接成功,则一直等待
    delay(200);                               // 等待1秒钟
    Serial.println("Connecting to WiFi...");  // 在串口上输出正在连接WiFi
  }
  Serial.begin(115200);                                 // 初始化串口,波特率为115200
  timeClient.begin();                                   // 初始化NTP客户端
  timeClient.update();                                  // 从NTP服务器获取时间,光begin是没有用的,前面没有更新时间,导致时间截没有变化。 
  unsigned long epochTime = timeClient.getEpochTime();  // 获取当前时间的时间戳
  setTime(epochTime);                                   // 将本地时间更新为获取到的时间
  WiFi.disconnect();                                    // 断开WiFi连接,节省资源
  lcd.begin();
  lcd.setRotation(2);
  canvas.createSprite(lcd.width(), lcd.height());       // 使用图层可以减少闪烁,显示效果更上档次。 
  canvas.setTextSize(2);
  // canvas.setColorDepth(1);  // 1ビット( 2色)パレットモードに設定
  canvas.setTextDatum(textdatum_t::top_left  );
  // canvas.setFont(&fonts::Orbitron_Light_24  );
  canvas.setTextColor(TFT_BLACK, TFT_WHITE);
}
void loop() {
  static long start = millis();
  static int t = 0;
  t = (t + 1) % 10;
  if (millis() - start >= 100)  //控制刷新时间,不然太快了,非阻塞更新
  {
    canvas.fillScreen(TFT_WHITE);
    canvas.drawJpg(spacemen[t], ~0u, 0, 0);  //其中,~0u 表示使用默认的背景色填充,64 表示图片的宽度,0 表示图片的起始位置的 x 坐标。
    start = millis();
  }
  // Print the current time
  if (lastSecond != second()) {

    // sprintf(hour_char, "%02d:%02d:%02d", hour(), minute(), second());
    sprintf(hour_char, "%02d:%02d", hour(), minute());
    Serial.println("Current UTC time:");
    Serial.println(hour_char);
    lastSecond = second();
  }
  canvas.drawString(hour_char, lcd.width() / 2, 10);
  canvas.pushSprite(0, 0);
}

 

关于时间的问题

tm 结构体中的各个成员代表时间的各个组成部分,使用时需要注意其取值范围。在 Arduino 编程中,我们可以通过 time_t 类型的时间值和 tm 结构体来表示和处理时间。

 

变量的类型为time_t 和 tm结构体 都是什么样子的?

time_t 是C/C++标准库中的一个数据类型,定义在time.h头文件中。time_t 通常被用来表示从UTC时间1970年1月1日0时0分0秒起至现在的秒数,也就是所谓的“时间戳”值。在大多数系统中,time_t 的底层实现是一个32位或64位整数。 tm 结构体也定义在time.h头文件中,它用于表示一个时间的各个组成部分,如年、月、日、时、分、秒等。其定义如下:

struct tm {

int tm_sec; // 秒,范围0~59

int tm_min; // 分,范围0~59

int tm_hour; // 时,范围0~23

int tm_mday; // 一月中的第几天,范围1~31

int tm_mon; // 月,范围0~11,0代表一月,11代表十二月

int tm_year; // 年,自1900年起的年数

int tm_wday; // 一周中的第几天,范围0~6,0代表星期天,1代表星期一,以此类推

int tm_yday; // 一年中的第几天,范围0~365

int tm_isdst; // 夏令时标记,正数表示夏令时,0表示不是夏令时,负数表示不确定是否是夏令时

};

 

time(&now); 和localtime(&now) 分别实现了什么功能

time(&now) 是一个C/C++标准库函数,头文件为time.h。它的作用是获取当前系统时间,并将其存储到now变量中,now变量的类型为time_t。

localtime(&now) 也是一个C/C++标准库函数,头文件为time.h。它的作用是将time_t类型的时间转换为本地时间。localtime函数的返回值是一个指向tm结构体的指针,该结构体包含了本地时间的年、月、日、时、分、秒等信息。综合起来,time(&now) 获取当前系统时间并将其存储到now变量中,localtime(&now) 将time_t类型的时间转换为本地时间并返回一个指向tm结构体的指针。在Arduino编程中,我们可以使用time函数和localtime函数来获取和处理时间。

 

strftime(buff, sizeof(buff), "%c", localtime(&now)); 函数的具体使用方法

这个函数是C/C++标准库中的函数,头文件为time.h。在Arduino编程中也可以使用该函数来格式化时间。

strftime 函数用于将时间格式化为字符串,其函数原型为:

size_t strftime(char *str, size_t maxsize, const char *format, const struct tm *timeptr);

其中,各个参数的含义如下:

str:指向存储格式化后的时间字符串的缓冲区。

maxsize:缓冲区的最大大小。

format:指定格式化的样式,为字符串常量,常用的格式化选项请见下表。

timeptr:指向要被格式化的时间,通常是一个 tm 结构体类型的指针。常用的格式化选项如下:格式化选项含义

%a 缩写的星期几名称

%A 周几的全称

%b 缩写的月份名称

%B 月份的全称

%c 标准的日期和时间表示

%d 按两位数字格式显示的月份中的第几天

%H 24 小时制的小时数

%I 12 小时制的小时数

%j 按三位数字格式显示的年份中的第几天

%m 按两位数字格式显示的月份

%M 按两位数字格式显示的分钟数

%p AM 或 PM

%S 按两位数字格式显示的秒数

%U 按两位数字格式显示的年份中的第几周(以周日为一周的第一天)

%w 按数字格式显示的星期几(0 表示周日,1 表示周一,以此类推)

%W 按两位数字格式显示的年份中的第几周(以周一为一周的第一天)

%x 标准的日期表示

%X 标准的时间表示

%y 按两位数字格式显示的年份数字(例如,89 表示 1989 年)

%Y 按四位数字格式显示的年份数字

在 Arduino 编程中,我们可以使用 strftime 函数将 time_t 类型的时间值格式化为字符串。例如,下面的例子将当前时间格式化为“2021年01月21日 03时14分15秒”的字符串并输出:

time_t now;

char buff[80];

time(&now);

strftime(buff, sizeof(buff), "%Y年%m月%d日 %H时%M分%S秒", localtime(&now));

Serial.println(buff);

 

首先使用 time 函数获取当前系统时间并将其存储到 now 变量中。然后使用 strftime 函数将当前时间格式化为字符串并存储到 buff 缓冲区中,格式化的样式为“%Y年%m月%d日 %H时%M分%S秒”(其中,%Y表示四位数的年份,%m表示两位数的月份,%d表示两位数的日子,%H表示24小时制的小时数,%M表示两位数的分钟数,%S表示两位数的秒数)。最后通过 Serial.println 输出格式化后的字符串。


任务4:实现定位功能

自己写了一个专门北斗卫星NAME数据的解析库,顺利完成了定位功能。

    该GPS传感器实际上是由合宙提供的,使用UART进行通信。需要特别注意的是,必须将传感器放置在窗边,最好是巴猓曰竦酶好的卫星信号接收效果。此外,由于北斗的NMEA信息和普通的GPS信号格式有所不同,因此在处理数据时需要先仔细查看数据格式。你需要一个串口通讯模块,如FT232 Type C转UART(TTL)通用串口通讯模块,再加上naviTrack程序,就可以查看通过串口传输的数据。

 

 

效果如下图所示。

 

 

(在我们这的北斗信号真不错)

 

  

 

由于我没有找到直接支持北斗信号的驱动库,所有都是普通GPS的库。因此,我基于北斗数据的格式编写了一个驱动库,封装了解析数据的过程,成功实现了卫星定位功能。这个库也经过了不少的测试,我选择将其开源,供大家使用。

 

 
GPS应用案例

#include "GPS.h"

GPS gps(1, 0);  // 声明一个GPS对象
#include <LovyanGFX.hpp>
#include "PICO_SSD1306.hpp"

static SSD1306 oled;
static LGFX_Sprite spr(&oled);  // 


void setup() {
  Serial.begin(9600);  // 初始化串口
  gps.setup();         // 初始化GPS模块的串口

  oled.begin();
  oled.setRotation(2);
  spr.createSprite(oled.width(), oled.height());       // 使用图层可以减少闪烁,显示效果更上档次。 
  spr.setTextSize(2);
  spr.setFont(&fonts::efontCN_12_b);                   // 加载中文字体
}

void loop() {
  GNGGA_t gngga;
  GNRMC_t gnrmc;
  if (gps.gnggaRead(gngga)) {
    // 打印GNGGA和GNRMC数据
    Serial.print("GNGGA Time: ");
    Serial.println(gngga.time);
    Serial.print("Latitude: ");
    Serial.print(gngga.latitude / 100, 6);
    Serial.print(gngga.ns);
    Serial.print(", Longitude: ");
    Serial.print(gngga.longitude / 100, 6);
    Serial.print(gngga.ew);
    Serial.print(", Quality: ");
    Serial.print(gngga.quality);
    Serial.print(", Satellites: ");
    Serial.print(gngga.satellites);
    Serial.print(", HDOP: ");
    Serial.print(gngga.hdop);
    Serial.print(", Altitude: ");
    Serial.print(gngga.altitude);
    Serial.print(gngga.unit1);
    Serial.print(", Geoid Height: ");
    Serial.print(gngga.geoidHeight);
    Serial.print(gngga.unit2);
    Serial.print(", Age: ");
    Serial.print(gngga.age);
    Serial.print(", Differential Correction: ");
    Serial.print(gngga.diffCorr);
    Serial.print(gngga.mode);
    Serial.println();
    
  };  // 读取GNGGA和GNRMC数据

  if (gps.gnrmcRead(gnrmc)) {
    // 打印GNGGA和GNRMC数据
    Serial.print("GNRMC Time: ");
    Serial.println(gnrmc.time);
    Serial.print("Latitude: ");
    Serial.print(gnrmc.latitude / 100, 6);
    Serial.print(gnrmc.ns);
    Serial.print(", Longitude: ");
    Serial.print(gnrmc.longitude / 100, 6);
    Serial.print(gnrmc.ew);
    Serial.print(", Speed: ");
    Serial.print(gnrmc.speed);
    Serial.print(", Direction: ");
    Serial.print(gnrmc.direction);
    Serial.print(", Declination: ");
    Serial.print(gnrmc.declination);
    Serial.print(", Variation: ");
    Serial.print(gnrmc.variation);   
    Serial.print(", Mode: ");
    Serial.print(gnrmc.mode);
    Serial.print(", Validity: ");
    Serial.print(gnrmc.validity);
    Serial.println();
    spr.fillSprite(0);
   spr.setCursor(2, 2);
  spr.print("经度:"+ String(gnrmc.latitude / 100));
  spr.setCursor(2, 30);
  spr.print("纬度:"+String(gnrmc.longitude / 100));
  spr.setCursor(2, 50);
  spr.pushSprite(0,0);

  };  // 读取GNGGA和GNRMC数据
}



任务5:扩展任务

 

   我完成了4个有趣的项目:分别是天气预报站、矩阵代码雨、绘制毕达哥拉斯树和贪吃蛇游戏。天气预报站使用了高德地图提供的天气信息。虽然该信息较为简单,但基本能满足需求,而且相当稳定。即便使用多年前的账号,仍然能正常使用。矩阵代码雨这个小程序非常有趣,没有使用外部支持库,完全独立开发。其实现方式非常巧妙,如果你的头发还挺多的,可以深入研究其中的运行机理。最后,我使用了oled屏和两个按键来完成贪吃蛇游戏。游戏的完整度和视觉效果都很出色,整体感受非常好,特别是按键的清脆声音非常令人愉悦。只需要扩展两个 GPIO 按键,就可以轻松实现这个游戏。

 

黑客帝国Matrix_Rain:

#define LGFX_USE_V1
#include <LovyanGFX.hpp>
#include "PICO_SSD1306.hpp"
SSD1306 lcd;
//

const int width = 16;
const int height = 8;
const int fontSize = 8;
int size = width * height;
#define fieldsize 160
int field[fieldsize] = { 0 };
int len;

// const String str  = "123456789abcdefghijklmnopqrstuvwzyz";
const char str[]  = "123456789abcdefghijklmnopqrstuvwzyz";


void setup(void) {
  lcd.init();  
  lcd.setRotation(2);
  Serial.begin(115200);
  len = strlen(str);
  lcd.setTextSize(1);
  lcd.setTextDatum(textdatum_t::middle_center);
}


void loop(void) {
  for(int i=0;i<width;i++) field[i] = 0;
  int index = random(len) % 16;
  field[index] = 15; // brightness 
  for (int i = fieldsize - 1; i > width - 1; i--) {
    if (field[i - width] == 15) field[i] = 15;           // 上层是15的亮度,下一层继承上一层的亮度。 
    if (field[i - width] > 0) field[i - width] -= 1;     // 亮度都在减弱。 
  }
  
  for (int i = 0; i < size; i++) {
    if (field[i + width] == 0) lcd.setTextColor(TFT_BLACK, TFT_BLACK);
    else if (field[i + width] == 15) lcd.setTextColor(TFT_WHITE, TFT_BLACK);
    else lcd.setTextColor(lcd.color565(0, field[i + width] * 15, 0), TFT_BLACK);
    lcd.setCursor(i % width*fontSize, i / width*fontSize);  // 位点的行和列所在的像素位置。 x,y 
    // lcd.drawString(String(string[random(len)]), i % width*16, i / width*16);
    lcd.print(String(str[random(len)]));

  }

delay(30); // run too fast.
}

绘制毕达哥拉斯树:

#define LGFX_USE_V1
#include <LovyanGFX.hpp>
#include "PICO_SSD1306.hpp"
SSD1306 oled;
//
LGFX_Sprite sprite(&oled);
// static LGFX oled;

#define SCREEN_WIDTH 128  // OLED屏幕宽度,以像素为单位
#define SCREEN_HEIGHT 64  // OLED屏幕高度,以像素为单位

int x = 0;
int y = SCREEN_HEIGHT-1;
int length = min(SCREEN_WIDTH, SCREEN_HEIGHT) / 2;  // 树干长度
int angle = -45;                                    // 树干角度
int angle1 = 45;                                    // 左侧枝条角度
int angle2 = -45;                                   // 右侧枝条角度
int depth = 8;                                      // 树的深度

void setup() {
  oled.init();
  oled.setRotation(0);
  sprite.createSprite(SCREEN_WIDTH, SCREEN_HEIGHT);
  sprite.print("hello world");
  sprite.pushSprite(0,0);
}

void loop() {
  sprite.fillSprite(0);
  draw_branch(x, y, length, angle, angle1, angle2, depth);  // 绘制毕达哥拉斯树
  sprite.pushSprite(0, 0);  // 显示缓冲区内容
  delay(1000);              // 暂停1秒
}

void draw_branch(int x, int y, int length, int angle, int angle1, int angle2, int depth) {
  if (depth > 0) {
    int x1 = x + length * cos(radians(angle));
    int y1 = y + length * sin(radians(angle));

    sprite.drawLine(x, y, x1, y1, TFT_WHITE);  // 绘制树干

    draw_branch(x1, y1, length * 0.67, angle + angle1, angle1, angle2, depth - 1);  // 左侧枝条
    draw_branch(x1, y1, length * 0.67, angle + angle2, angle1, angle2, depth - 1);  // 右侧枝条
  }
}

后面的代码就不贴了,可以在文末下载完整代码。

 

(天气预报站)

 

(矩阵代码雨)

 (毕达哥拉斯树)

 

(贪吃蛇游戏) 

 

 


 

 

总结

 

   这项目的工作量比较大,好在主办单位预留的时间很充裕,可以让我自由开展自己的探索。项目系列动作下来,任务难度有浅至深,完成学习后,就可以掌握了rp2040的基本使用。项目中的亮点有时间同步功能、驱动北斗卫星传感器等。在没有现成参考的情况下,获取了网络时间并对时间根据本地时区进行校准。第二自己开发了一个北斗卫星传感器的驱动文件,方便这个传感器的后续使用。另外,扩展任务中的三个小项目可玩性都很高,反正收获满满。

近日,看到了RP2040 picoW 可以使用蓝牙的消息,这样一来picoW就集齐了一直被人诟病的wifi和蓝牙,发展前景愈加广阔(国内的大厂莫名感觉到压力有增加了)。

 这次活动没有使用Micropython来完成项目内容,因为觉得Arduino的效率更高,可以完成一些难度更大的项目,得益于Arduino繁华的全球生态,你总会发现一些有趣的灵魂和有趣的事。

 欢迎朋友们一起来学习交流,有问题可以在下方留言,我们一起讨论一起进步,项目中不足的地方,请求大佬帮忙指点,提出更好的解决方案。

项目全部代码开源,服用过程如有问题欢迎交流讨论:

项目全部代码

 

最新回复

你这从microPython一下跳转到C,难度提升一个数量级,这也太卷了吧   详情 回复 发表于 2023-6-28 11:25
点赞(1) 关注(1)
 
 

回复
举报

7158

帖子

2

TA的资源

版主

沙发
 

给力!你这一套玩下来,用RP2040做一些简单的项目已经不是问题了!

 
 
 

回复

1239

帖子

68

TA的资源

纯净的硅(中级)

板凳
 

厉害了


 
 
 

回复

281

帖子

7

TA的资源

一粒金砂(高级)

4
 

你这从microPython一下跳转到C,难度提升一个数量级,这也太卷了吧

 
 
 

回复
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/9 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2025 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表