【得捷电子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繁华的全球生态,你总会发现一些有趣的灵魂和有趣的事。
欢迎朋友们一起来学习交流,有问题可以在下方留言,我们一起讨论一起进步,项目中不足的地方,请求大佬帮忙指点,提出更好的解决方案。
项目全部代码开源,服用过程如有问题欢迎交流讨论:
项目全部代码
|