本文作者:默 & 鐵熊
前段時間我在網上看到了一款很有意思的點陣時鐘,它可以播報天氣,查看 YouTube 的訂閱數,還有好看的時間動畫。你可以把它當做普通鬧鐘,也可以連接藍牙把它當做音箱來使用。它的許多功能都很有意思,其中我最喜歡的是它的時間顯示動畫效果,然而它一千多的價格讓我望而卻步,放棄了入手的打算。不過既然身爲創客,我爲什麼不製作一個屬於自己獨一無二的創意網絡時鐘呢?
說幹就幹,於是我就做了一個創意點陣時鐘,先來看一下演示視頻吧:
[video(video-aynePODb-1591445136040)(type-bilibili)(url-https://player.bilibili.com/player.html?aid=243386172)(image-https://ss.csdn.net/p?http://i2.hdslb.com/bfs/archive/878bc2ad7fda3aaef08cae694955dbe722b718d6.jpg)(title-)]
預期目標及功能
- 網絡自動校準時間
- 無網絡連接時及時反饋
- 一鍵配置時鐘網絡
- 自定義精美時間顯示字體
- 時間顯示動畫
- 亮度自動調節
- 時段提示
材料清單
- ESP8266 Wemos mini 開發板 1 塊;
- 杜邦線若干;
- 4 合 1 點陣模塊;
- 激光切割外殼;
- 櫟木滑面仿木紋貼紙。
電路原理圖
電路連接關係如下圖所示:
結構拼裝
將 USB 數據線按下圖所示方向由外殼背部插入開發板,使用熱熔膠將開發板固定到木板上,保持穩定直到熱熔膠凝固,主要熱熔膠不要碰到數據線。
將外殼前部與點陣屏按下圖所示方式放置入面板凹槽,使用熱熔膠固定點陣,保持穩定直到熱熔膠凝固。
然後使用杜邦線按原理圖正確連接電路,並拼接外殼底部與左右兩側,最後將外殼頂部進行封頂。
剪切合適大小的櫟木滑面仿木紋貼紙,粘貼至外殼表面。注意留出點陣位置,可以適當使用刻刀雕刻出 USB 下載接口,以便進行供電及程序下載或更新。
程序設計
下面開始詳細講解程序設計過程。
開發環境
我們使用 Aduino 軟件來編寫本項目的程序,開發板選擇 ESP8266 類型。至於如何在 Arduino 中配置 ESP8266 的開發環境,不在本文的介紹範圍,請自行查閱相關資料。
程序思路
爲了達到我們的預期目標,我們先繪製功能的思維導圖,再根據思維導圖逐步實現創意點陣時鐘的程序設計。
下面我們將具體討論創意點陣時鐘各個子功能是如何實現的。
獲取網絡時間
作爲一個時鐘,最重要的功能當然是顯示時間啦。那麼該如何從網絡獲取時間呢?
下面的例子演示瞭如何獲取網絡時間並將時間保存在變量中,其中 ESP8266WiFi.h
庫的功能是連接網絡,NtpClientLib.h
庫的功能是獲取 NTP 服務器的網絡時間,SimpleTimer.h
庫是用來設置定時器每秒刷新一次時間。該例子並沒有串口打印當前時間,你可以添加串口打印相關代碼用來調試程序。
#include <ESP8266WiFi.h>
#include <NtpClientLib.h>
#include <TimeLib.h>
#include <SimpleTimer.h>
SimpleTimer timer;
const PROGMEM char *ntpServer = "ntp1.aliyun.com";
int8_t timeZone = 8;
volatile int hour_variable;
volatile int minute_variable;
volatile int second_variable;
void Simple_timer() {
hour_variable = NTP.getTimeHour24();
minute_variable = NTP.getTimeMinute();
second_variable = NTP.getTimeSecond();
}
void setup() {
Serial.begin(9600);
WiFi.begin("ssid", "password");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("Local IP:");
Serial.print(WiFi.localIP());
NTP.setInterval(600);
NTP.setNTPTimeout(1500);
NTP.begin(ntpServer, timeZone, false);
timer.setInterval(1000L, Simple_timer);
}
void loop() {
timer.run();
}
點陣屏顯示庫:MD_Parola
MD_Parola
是 MAX7219 點陣屏的模塊化滾動文本顯示庫,其主要特點如下:
- 支持點陣屏顯示文本時左對齊、右對齊或居中對齊;
- 具有文字滾動,進入和退出效果;
- 能夠控制顯示參數和動畫速度;
- 支持硬件 SPI 接口;
- 可以在點陣屏虛擬多個顯示區域;
- 用戶定義的字體和/或單個字符替換;
- 支持雙高顯示;
- 支持在混合顯示文本和圖形。
下面的例子簡單演示瞭如何利用 MD_Parola 滾動顯示字符串,其中 MD_Parola 對象有 4 個參數:分別爲 SPI 管腳 DIN、CLK、CS 及點陣數目。下面我們所做的創意點陣時鐘的顯示功能均由此庫開發。
#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
MD_Parola P = MD_Parola(13,14,12,4); //DIN(D7) CLK(D5) CS(D6)
MD_MAX72XX mx = MD_MAX72XX(13,14,12,4); //DIN(D7) CLK(D5) CS(D6)
void setup() {
mx.begin();
P.begin();
}
void loop() {
if (P.displayAnimate()) {
P.displayScroll("Mixly", PA_LEFT, PA_SCROLL_LEFT, 50);
}
}
點陣位圖取模
要在點陣屏中顯示圖片,首先需要設計點陣圖案(位圖),然後對圖案進行取模操作。點陣取模使用 PCtoLCD2002 取模軟件,取模設置如下:
取模方式爲陰碼、逆向、逐列式,輸出方式爲 16 進制,注意格式設置爲 C51 格式,其餘參數按照默認取模方式設置即可。
位圖顯示函數:display_bitmap()
這裏我們取模的數據格式爲 uint8_t 數組,我們有自定義字體 0~9 和時間分隔符“:”,再加上一些自定義的圖像,這就導致我們有大量的位圖。爲了方便的管理這些位圖,我們使用指針數組 bitmap_data[]
去管理我們的位圖。爲了顯示方便,我們定義了函數 display_bitmap()
,該函數需要 3 個參數,分別爲顯示橫座標 abscissa、位圖寬度 width 及指針數組 bitmap_data[]
中的位置 bitmap_number。需要注意的是我們這裏並沒有指定位圖的高度,因爲我們用到的 MAX7219 點陣屏分辨率爲 8×32,所以這裏我們默認位圖高度爲 8。
#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
MD_Parola P = MD_Parola(13,14,12,4); //DIN(D7) CLK(D5) CS(D6)
MD_MAX72XX mx = MD_MAX72XX(13,14,12,4);
uint8_t bitmap_data1[] = {0x3e, 0x2a, 0x3e};
uint8_t bitmap_data2[] = {0x2e, 0x2a, 0x3e};
uint8_t * bitmap_data[] = {
bitmap_data1
bitmap_data2
……
};
void display_bitmap(int abscissa, int width, int bitmap_number) {
mx.control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);
mx.setBuffer(abscissa, width, bitmap_data[bitmap_number]);
mx.control(MD_MAX72XX::UPDATE, MD_MAX72XX::ON);
}
時間顯示:時、分
MD_Parola 庫中,由於字體過大而且不美觀,導致顯示的時間過長,所以我們需要自定義字體。自定義字體如下圖所示,值得注意的是 0~9 的位圖寬度是 3,分割符“:”的寬度是 1。
自定義字體取模數據如下所示:
uint8_t Small_font_0[] = {0x3e, 0x22, 0x3e};
uint8_t Small_font_1[] = {0x24, 0x3e, 0x20};
uint8_t Small_font_2[] = {0x3a, 0x2a, 0x2e};
uint8_t Small_font_3[] = {0x2a, 0x2a, 0x3e};
uint8_t Small_font_4[] = {0x0e, 0x08, 0x3e};
uint8_t Small_font_5[] = {0x2e, 0x2a, 0x3a};
uint8_t Small_font_6[] = {0x3e, 0x2a, 0x3a};
uint8_t Small_font_7[] = {0x02, 0x02, 0x3e};
uint8_t Small_font_8[] = {0x3e, 0x2a, 0x3e};
uint8_t Small_font_9[] = {0x2e, 0x2a, 0x3e};
uint8_t Small_font_10[] = {0x14};
下面我們分析如何顯示時間,這裏我們只顯示小時和分鐘。
這裏我們有一個小技巧,我們可以把 0~9 的位圖放到指針數組 bitmap_data[]
的 0~9 的位置上,時間分隔符“:”放置在數組序號 10 的位置上。由於前面我們定義了一個顯示位圖的函數 display_bitmap()
,這樣我們不需要通過任何映射就可以顯示數字了,例如 display_bitmap(22, 3, 0)
就顯示 0;display_bitmap(22, 3, 1)
就顯示 1,這樣是不是很方便呢?
爲了分別獲取小時和分鐘的十位及個位,我們需要對其進行除法和取餘操作,例如對小時 9 除 10 得到十位 0(爲什麼不是0.9?這是因爲我們時間變量定義爲整數,一個整數除以另一個整數結果只能爲整數。還是不懂?那你就該補一下C語言基礎知識了。),9 除 10 取餘得到個位 9。由分析我們在合適的位置顯示時間得到了下面的時間顯示函數。
最後,爲了顯示更加美觀,如果小時或分鐘只有一位數,我們就需要進行補零操作,將 1:1 補零變成 01:01。顯示時間的代碼如下:
display_bitmap(22, 3, hour_variable / 10);
display_bitmap(18, 3, hour_variable % 10);
display_bitmap(14, 1, 10);
display_bitmap(12, 3, minute_variable / 10);
display_bitmap(8, 3, minute_variable % 10);
時間顯示:秒
時間在流逝,但是我們上面並沒有顯示秒鐘,那我們怎樣感知時間的進度呢?爲了解決這個問題,我們定義了下面的一系列位圖,注意這裏定義位圖的寬度是 5 不是 8,我們每隔一秒切換一次下面的位圖,看起來是不是像秒針在走動呢?
使用取模軟件分別對上述點陣圖案取模:
uint8_t clock_0[] = {0x1c, 0x22, 0x2e, 0x22, 0x1c};
uint8_t clock_1[] = {0x1c, 0x22, 0x2a, 0x26, 0x1c};
uint8_t clock_2[] = {0x1c, 0x22, 0x2a, 0x2a, 0x1c};
uint8_t clock_3[] = {0x1c, 0x22, 0x2a, 0x32, 0x1c};
uint8_t clock_4[] = {0x1c, 0x22, 0x3a, 0x22, 0x1c};
uint8_t clock_5[] = {0x1c, 0x32, 0x2a, 0x22, 0x1c};
uint8_t clock_6[] = {0x1c, 0x2a, 0x2a, 0x22, 0x1c};
uint8_t clock_7[] = {0x1c, 0x26, 0x2a, 0x22, 0x1c};
前面我們指針數組 bitmap_data[]
的 0~10 位置都用來放置數字了,我們這裏有 8 幅位圖,所以放入指針數組 bitmap_data[]
的 11~18 位置,我們定義一個靜態局部變量Clock_variable
,設置其初始值爲 11,每隔一秒 Clock_variable
變量的值增加 1,並顯示對應序號的位圖,當 Clock_variable
的值爲 19 時,將它重新賦值爲 11,這樣我們就實現了秒錶動畫的設計。程序如下:
static int Clock_variable = 11;
display_bitmap(4, 5, Clock_variable);
Clock_variable = Clock_variable + 1;
if (Clock_variable == 19) {
Clock_variable = 11;
}
上面我們設計了秒錶動畫,但是還有一個問題,由於點陣屏空間限制,我們沒辦法用數字顯示精確的秒數,那怎麼辦呢?我們觀察到,在點陣屏的底部還空了 2 個像素點的高度,所以我們可以在最後一行通過點數顯示精確到秒數。
如上圖所示,最後一行前面有 5 個點,後面有 9 個點,因此秒數爲 59 秒。顯示秒數的代碼如下:
if (second_variable / 10) {
mx.drawLine(7, 22, 7, (23 - second_variable / 10), true);
}
if (second_variable % 10) {
mx.drawLine(7, 14, 7, (15 - second_variable % 10), true);
}
其中 mx.drawLine()
爲繪製線段的函數,它有 4個參數,分別爲:線段起點橫座標、起點縱座標、終點橫座標、終點縱座標,以及顯示狀態(true 點亮線段;false 熄滅線段)。根據我們使用的 4 和 1 點陣座標定義,其中橫座標最大爲 7,縱座標最大爲 31。
當秒數的個位爲 0 的時候將線段清除,重複顯示線段即可顯示當前秒數了。這裏我不對顯示線段的位置、長度與秒數的關係進行分析,留給大家當做思考題活動一下大腦了。
時段圖標顯示
爲了感知一天時間的變化,我們希望不同時間段用不同的圖標進行提示。我們定義了太陽和月亮兩個圖標,它們的寬度都是 8,樣式如下圖所示。
使用取模軟件取模數據如下:
uint8_t sun[] = {0x24, 0x00, 0xbd, 0x3c, 0x3c, 0xbd, 0x00, 0x24};
uint8_t moon[] = {0x38, 0x7c, 0xe2, 0xc0, 0xc4, 0x4e, 0x24, 0x00};
繼續將太陽和月亮的取模數據添加到指針數組 bitmap_data[]
的位置 19 和 20。這裏我們定義在 6 點到 18 點之間,在橫座標爲 31 處顯示太陽,其他時間顯示月亮,程序如下:
if ((hour_variable >= 6) && (hour_variable <= 18)) {
display_bitmap(31, 8, 19);
} else {
display_bitmap(31, 8, 20);
}
一鍵配網:WiFiManager
如果我們在程序裏固定 WiFi 信息,那麼當網絡環境變化時,時鐘將不可用,此時你需要重新修改網絡信息並上傳程序,這無疑是很麻煩的。爲此我們需要一種動態修改網絡信息的辦法,這裏我們使用了 WiFiManager
庫,該庫支持通過網頁對 WiFi 連接進行配置。下面是一個網絡配置的簡單示例,該例子上傳成功後,將啓用一個名爲 ESP8266 的 WiFi 熱點,使用手機連接此熱點即可按提示對網絡進行配置。這裏你也可以使用其他熱點名稱,例如你的作品名稱而不是 ESP8266。需要注意的是,ESP8266 僅支持 2.4G 頻段的 WiFi 網絡,不支持 5G 頻段的 WiFi 網絡。
#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h>
WiFiServer server(80);
void setup(){
WiFiManager wifiManager;
wifiManager.autoConnect("ESP8266");
server.begin();
}
void loop(){
}
WiFi 連接反饋
當網絡環境發生變化時,我們可能需要對網絡重新進行配置,爲此我們定義了下面的位圖用於斷網提示。該位圖的寬度爲 19,看上去像是 WiFi 被外星人劫持了,是不是很生動形象!
使用取模軟件取模數據如下:
uint8_t wifi[] = {0x04, 0x06, 0x13, 0xDB, 0xDB, 0x13, 0x06, 0x04, 0x00, 0x70, 0x18, 0x7d, 0xb6, 0x3c, 0x3c, 0xb6, 0x7d, 0x18, 0x70};
這裏我們使用 !(WiFi.status() != WL_CONNECTED)
語句來判斷網絡連接是否斷開。當 WiFi 連接成功時,!(WiFi.status() != WL_CONNECTED)
返回真,這是我們可以同步時間;當 WiFi 斷開時,!(WiFi.status() != WL_CONNECTED)
返回假,我們在點陣屏上顯示 WiFi 斷開連接提示,然後使用配網函數對網絡進行配置,配網成功後再次顯示正常的時間即可。代碼如下:
if (!(WiFi.status() != WL_CONNECTED)) {
hour_variable = NTP.getTimeHour24();
minute_variable = NTP.getTimeMinute();
second_variable = NTP.getTimeSecond();
} else {
mx.clear();
display_bitmap(25, 19, 21);
WiFiManager wifiManager;
wifiManager.autoConnect("ESP8266");
server.begin();
mx.clear();
}
小狗動畫設計
爲了時鐘富有動態感,我們這裏爲時鐘添加一個小狗的動畫效果,該動畫由兩個寬度爲 8 的動畫幀構成,首先我們先使用取模軟件繪製出這兩幀圖像,再點擊水平鏡像按鈕得到鏡像後的圖像,最後生成字模即可。
使用取模軟件取模數據如下:
uint8_t PROGMEM dog[] = {0x8C, 0x4C, 0xFE, 0x30, 0xB0, 0x70, 0xF0, 0x08, 0x0C, 0x0C, 0xFE, 0x30, 0x30, 0x30, 0xF8, 0x00,};
下面的例子演示將點陣劃分爲兩個區域,區域 0 和區域 1。P.setZone()
函數將點陣劃分爲不同的顯示區域,它有 3 個參數:分別爲區域編號、起始點陣及終止點陣。P.begin()
指定區域數量,參數爲空默認一個區域,這裏我們有兩個顯示區域,故參數爲 2,其中點陣編號與區域的對應關係如下圖所示:
P.setSpriteData()
函數爲精靈動畫的初始化函數,該函數接受 7 個參數:分別爲初始化區域、動畫開始精靈數據、動畫開始精靈寬度、動畫開始精靈幀數、動畫結束精靈數據、動畫結束精靈寬度、動畫結束精靈幀數。
P.displayAnimate()
函數有兩個作用,分別爲反饋顯示狀態和動畫執行函數。當作爲反饋狀態時,動畫顯示完成返回 1,未完成返回 0。當作爲動畫執行函數時,通過不斷調用該函數實現動畫的流暢運行,因此程序需要不斷的調用 P.displayAnimate()
函數。
P.getZoneStatus()
函數作用類似 P.displayAnimate()
函數,不同的是它僅返回區域的顯示狀態。
P.displayZoneText()
函數爲字符串的動畫顯示函數,該函數接受 7 個參數,分別爲:顯示區域、顯示字符串、對齊方式、動畫速度、文本顯示時間、動畫進入效果、動畫退出效果。下面的代碼演示瞭如何在區域顯示精靈動畫。這裏我們顯示字符串爲空、顯示時間爲0,顯示字符串爲空保證了我們僅有小狗動畫沒有文字,顯示時間爲 0 保證了小狗動畫的連貫性。
void setup() {
P.begin(2);
mx.begin();
P.setZone(0, 0, 2);
P.setZone(1, 3, 3);
P.setSpriteData(1, dog, 8, 2, dog, 8, 2);
}
void loop() {
P.displayAnimate();
if (P.getZoneStatus(1)) {
P.displayZoneText(1, "", PA_CENTER, 100, 0, PA_SPRITE, PA_SPRITE);
}
}
自動亮度調節
當我們睡覺以後我們是不會看時間的,此時降低點陣顯示的亮度有助於節能環保,因此我們需要根據時間段自動調節點陣顯示的亮度。下面的代碼在晚上 0~6 點亮度設置爲 1,其他時間亮度設置爲 10。P.setIntensity()
函數爲區域亮度設置函數,其有兩個參數,分別是:顯示區域和亮度值,其中亮度值範圍爲 0~15。
if ((hour_variable >= 0) && (hour_variable < 6)) {
P.setIntensity(0, 1);
P.setIntensity(1, 1);
} else {
P.setIntensity(0, 10);
P.setIntensity(1, 10);
}
代碼組合
最後,按照上述功能之間的邏輯關係,將代碼組合在一起即可。由於篇幅限制,這裏就不放完整的代碼了。
使用說明
首先連接電源,時鐘進行初始化,同時出現如下界面提示配網,此時開發板會自動開啓名爲 ESP8266 的無密碼 WiFi 熱點。
打開手機,連接這個網絡,配網步驟如下圖所示:
配網說明(以安卓手機爲例):
- 打開手機設置選擇 WiFi 設置打開 WiFi;
- 連接時鐘熱點 ESP8266(熱點名由程序設置,可更改爲其他名稱);
- 選擇登錄進入網絡配置頁面;
- 點擊配置 WiFi 進入圖示頁面點擊掃描,掃描附近熱點;
- 選擇 WiFi 輸入 WiFi 密碼;
- 點擊保存等待配網成功。