1987年4月2日, IBM 推出了 PS/2,这是Personal System/2 Model 80 (IBM 8580)的缩写。在这个电脑上引入了VGA(VideoGraphics Array)接口,它成为模拟信号的电脑显示标准。 VGA接口共有15针,分成3排,每排5个孔,显卡上应用最为广泛的接口类型,绝大多数显卡都带有此种接口。它传输红、绿、蓝模拟信号以及同步信号(水平和垂直信号),从块头巨大的CRT显示器时代开始,VGA接口就被使用,并且一直沿用至今。 VGA 公头 简单的说,工作原理是:RG B 三个Pin 给出每一个点的颜色组成信息,显示器采样后即按照给出值显示,当显示完一行后,行同步信号通知显示器换行,如此进行,当一帧显示完成后场同步信号通知显示器这一帧结束了,请从最上面再进行显示。 对于我们来说,只要单片机足够快就可以模拟出 VGA 信号从而达到显示的目的。这次制作一个 VGA 转接板,配合 FireBeetle 在显示器上显示当前时间。 首先,本项目基于开源图形库FabGL【参考1】 ,它是设计给ESP32的图形库。它实现了多个显示驱动程序,例如VGA接口的显示器以及I2C和SPI 接口的液晶屏)。此外FabGL还可以从PS/2键盘和鼠标获取输入,方便实现简单的交互。硬件设计上 GPIO21/22 用作红色信号输出;GPIO18/19用作绿色信号输出;GPIO4/5用作蓝色信号输出;GPIO23用于 HSync;GPIO15用于VSync(定义在vga16controller.h)。每一种颜色使用2个电阻构成简单的 DAC 电路,因此可以显示 2^6=64种颜色。 最终给FireBeetle设计了一个 VGA Shield 如下: 此外,引出所有的IO方便日后扩展其他功能。
此外,还有一个I2S的 DAC输出,为日后音频输出预留
除了 FireBeetle提供电力,板子上还有一个 USB接口,可以直接将USB插入此处供电。
PCB 布线如下:
3D预览如下:
制作好的 PCB 如下:
用于颜色显示的电阻是必须的,其他的没有焊接。安装之后的样子: 接下来编写代码,基本原理是使用 FabGL 的终端(Terminal,相当于一个 ASCII 字符显示器),在上面使用 ASCII绘制转动的地球;此外,通过 WIFI 获得阿里NTP服务器提供的日期时间信息,一起输出到VGA接口上显示在屏幕上。
- #include "fabgl.h"
- #include "vtanimations.h"
- #include <WiFi.h>
-
- // VGA 显示
- fabgl::VGA16Controller DisplayController;
- fabgl::Terminal Terminal;
-
- const char *ssid = "labz_001665"; //网络名称
- const char *password = "12345678"; //网络密码
-
- // 使用阿里的 NTP 获得当前时间
- const char* ntpServer = "ntp.aliyun.com";
- // 时区修正,我们在东八区
- const long gmtOffset_sec = 8 * 60 * 60;
- // 夏令时修正
- const int daylightOffset_sec = 0;
-
- void setup() {
- struct tm timeinfo;
- Serial.begin(115200);
-
- // Connect to Wi-Fi
- Serial.print("Connecting to ");
- Serial.println(ssid);
- WiFi.begin(ssid, password);
- while (WiFi.status() != WL_CONNECTED) {
- delay(500);
- Serial.print(".");
- }
- Serial.println("");
- Serial.println("WiFi connected.");
- delay(2000);
- Serial.println("Connect to NTP server");
- // 从 NTP Server 获得时间
- configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
- // 如果没有成功获取,那么重启 ESP32
- if (!getLocalTime(&timeinfo)) {
- Serial.println("Failed to obtain time, retry");
- delay(5000);
- ESP.restart();
- }
- Serial.println("Got time");
- // 取得时间后即可断开 WIFI
- WiFi.disconnect(true);
- WiFi.mode(WIFI_OFF);
-
-
- DisplayController.begin();
- // 设定分辨率
- DisplayController.setResolution(VGA_640x480_60Hz);
-
- // 创建 Terminal
- Terminal.begin(&DisplayController);
- Terminal.enableCursor(true);
-
- // 背景为黑,文字绿色
- Terminal.write("\e[40;92m");
- // 请屏幕
- Terminal.write("\e[2J");
-
- Terminal.write("\e[20h");
- //Terminal.write("\e[92m");
- // 关闭光标
- Terminal.enableCursor(false);
-
- }
-
- void loop() {
-
- int i = 0;
- while (i < sizeof(vt_animation) - 4) {
- // 如果当前要发送回归第一行的命令,就输出当前时间
- if (vt_animation[i] == 0x1B && vt_animation[i + 1] == 0x5B && vt_animation[i + 2] == 0x48) {
- struct tm timeinfo;
- // 取得当前时间
- getLocalTime(&timeinfo);
-
- char buf[60];
- // 年月日
- sprintf(buf, "\e[10;56H%d/%02d/%02d", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday);
- Serial.println(buf);
- Terminal.write(buf);
- //小时分钟秒
- sprintf(buf, "\e[14;56H%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
- Serial.println(buf);
- Terminal.write(buf);
-
- // 输出回归第一行的命令
- Terminal.write(vt_animation[i]);
- Terminal.write(vt_animation[i + 1]);
- Terminal.write(vt_animation[i + 2]);
- // 跳过这个命令
- i = i + 3;
- }
- Terminal.write(vt_animation[i]);
- i++;
- }
- }
复制代码
这是在HP 显示器上测试的结果
参考:
|