树莓派-光立方
LED CUBE. (Driven by RaspberryPi and 74HC154 chip)
【驅動程序 + 20多種特效】【C++】
一、GitHub地址
Leopard-C/LedCube
二、原理圖
原理圖(pdf)
我用立創EDA自己畫的,并不專業,不過還是比較清晰的。
制作教程,參考視頻:BV1Ex411C718
演示視頻:
BV1Kz411B7KT
三、核心類LedCube解析(src/driver/cube.h)
程序運行大概的流程:
類LedCube中有一個后臺線程,不停的掃描光立方。實際上,任何時刻,都只有一個LED燈被點亮,但是利用人眼的視覺暫留原理,只要掃描得足夠快,就能看到多個LED燈被點亮。
static void backgroundThread();類中有兩個三維數組,存儲坐標(z, x, y)處的LED燈的狀態。
using LedState = char; enum LED_State : char { LED_OFF = 0, LED_ON = 1 };// [z][x][y], 用于后臺掃描線程,真正表示光立方的狀態 LedState leds[8][8][8]; // 緩沖區,用于主線程 LedState ledsBuff[8][8][8];類中提供的對LED燈的操作,都是對ledsBuff數組的修改,而后臺掃描線程使用的是leds數組。
只有調用update()函數,將ledsBuff一次性拷貝到leds數組,才能真正改變光立方的狀態。
void LedCube::update() {mutex_.lock();memcpy(leds, ledsBuff, 512);mutex_.unlock(); }下面介紹以下該類對外提供的接口:
2.1 setup()
初始化。
事實上,整個程序,只有一個LedCube的全局對象,定義在main函數所在的文件中,在其他地方通過extern關鍵字進行聲明:
// main.cpp LedCube cube;// other files extern LedCube cube;在主函數調用setup()函數,用于初始化74HC154芯片、熄滅所有LED燈等。
2.2 update()
對光立方做一系列修改后,只有調用update()函數,才能真正起作用。
2.3 quit()
退出函數,執行清理工作,正常退出的話,會由析構函數調用。
非正常退出,比如捕獲到Ctrl+C發出的SIGINIT信號,應該主動調用該函數進行清理,否則程序退出時可能有一些LED仍然亮著。
2.4 clear()
熄滅所有LED燈。
2.5 修改(x,y,z) 處LED燈狀態
LedState& operator()(int x, int y, int z); LedState& operator()(const Coordinate& coord);如何使用:
LedCube cube; cube(2, 5, 7) = LED_ON; cube(6, 6, 3) = LED_OFF; Coordinate coord = { 1, 4, 5 }; cube(coord) = LED_OFF;2.6 點亮某一個面(Layer)
可以是垂直于x或y或z軸的任何一個面。
(1)整個面的LED燈狀態相同
void lightLayerX(int x, LedState state); void lightLayerY(int y, LedState state); void lightLayerZ(int z, LedState state);(2)顯示圖像
void lightLayerX(int x, const std::array<std::array<char,8>>& image); void lightLayerY(...); void lightLayerZ(...);其中參數image是一個8x8的數組,剛好對應光立方的一個面(8x8=64個LED燈)。
(2)顯示圖像(指定圖像在圖像庫的編碼)
如顯示數字、字母、和自定義的圖案。
void lightLayerX(int x, int imageCode, Direction viewDirection, Angle rotate); void lightLayerY(...) void lightLayerZ(...)- imageCode:圖像編碼,在src/utility/image_lib.cpp中可以找到,即std::map的鍵。
- viewDirection:從哪個方向觀察這個圖像,如X_ASCEND表示沿著x軸正向的方向觀察該圖像。
- rotate:旋轉,支持:
- ANGLE_0:不旋轉
- ANGLE_90:順時針旋轉90度
- ANGLE_180:順時針旋轉180度
- ANGLE_270:順時針旋轉270度
也就是說,在任何一個垂直于x或y或z軸的面上,都可以有 2×4=82 \times 4 = 82×4=8 種方式顯示一個圖案。
- 2種視角:沿著軸的正向還是負向
- 4種旋轉角度:0、90、180、270
2.7 點亮一行或一列
(1)一行或一列全部點亮,或者全部熄滅
void lightRowXY(int x, int y, LedState); void lightRowYZ(int y, int z, LedState); void lightRowXZ(int x, int z, LedState);(2)分別指定一行或一列8個LED燈的狀態
void lightRowXY(int x, int y, const std::array<LedState,8>& states); void lightRowYZ(...); void lightRowXZ(...);// 例如下面一行代碼,將點亮 x==5 && y==7 那一列的LED燈,隔一個亮一個 // LED_ON==1,表示點亮 // LED_OFF==0, 表示熄滅 lightXY(5, 7, { 1, 0, 1, 0, 1, 0, 1, 0 });2.8 點亮/熄滅一條空間直線
void lightLine(const Coordinate& start, const Coordinate& end, LedState state);- start:線的起點 (x1, y1, z1)
- end:線的終點(x2, y2, z2)
該函數實際上調用了src/utility/utils.h中的getLine3D函數。
使用的是 Bresenham生成線 算法。
void getLine3D(const Coordinate& start, const Coordinate& end, std::vector<Coordinate>& line);給定線段的起點和終點,該函數會返回這條線段上的所有點(整數坐標)。
獲取到所有點后,設置些點處的LED燈的狀態即可。
2.9 繪制正方形 / 矩形
void lightSquare(const Coordinate& A, const Coordinate& B, FillType fillType);- AB:矩形的對角線
- fillType:填充類型
- FILL_SOLID:實心
- FILL_SURFACE:實心
- FILL_EDGE:邊界(無填充)
2.10 繪制立方體 / 長方體
void lightCube(const Coordinate& A, const Coordinate& B, FillType fillType);- AB:長方體的對角線
- fillType:填充類型
- FILL_SOLID:實心
- FILL_SURFACE:只填充面(不填充內部)
- FILL_EDGE:只有邊界(面和內部均無填充)
2.11 復制 / 移動一個面
void copyLayerX(int xFrom, int xEnd, bool clearXFrom = false); void copyLayerY(...); void copyLayerZ(...);- xFrom:面的原始位置,即面x=xFrom
- xEnd:面的目標位置,即面x=xEnd
- clearXFrom:是否清空原來的面
- true:移動
- false:復制
2.12 setLoopCount(int count)
void setLoopCount(int count) {this->loopCount = count; }達到的效果是:控制燈的明暗程度。
這里假設有兩個閾值, $ 0 < C1 < C2 < +\infty$
- 當count < C1時,count越小,LED燈越暗
- 當count > C2時,count越大,LED燈越暗
- 當C1 < count < C2時,LED比較亮,且亮度變化不大,肉眼無法辨別。
這里的C1,C2很難確定,而且影響亮度的因素比較多。
但是經過測試,C1≈100,C2≈200C1 \approx 100,C2 \approx 200C1≈100,C2≈200
這里的count實際上影響的是每個LED燈點亮的時間。因為任何一個時間都只有一個LED燈被點亮,后臺線程在不斷掃描整個光立方,即循環512次,逐一判斷每個LED燈是否需要點亮。
每個LED燈被點亮后都會暫停一段時間(很短),然后熄滅該LED燈,去點亮下一個需要被點亮的LED燈。
這里的暫停一段時間是通過空語句循環實現的
// 這里的loopCount,就是通過setLoopCount(int count)設置的 for (int i = 0; i < loopCount; ++i) {// ; }在樹莓派上,根據測算,一次空語句循環需要5~6ns,默認的loopCount=150,也就是相當于暫停800ns。
loopCount越大或越小都會導致LED偏暗,而且過大時還會有其他副作用,如下:
- loopCount越小:每個LED燈被點亮的時間越短,看起來越暗。但是經過測試,loopCount在100~200之間LED燈的亮度變化不大,小于100,甚至說小于50才會觀察到變暗。在loopCount在5左右時,LED基本完全不亮。
- loopCount越大,每個LED燈被點亮的時間越長,但是,相應的,對光立方進行一次掃描耗時也越長,這就導致每個LED燈兩次被點亮之間的間隔變長,即不供電的時間變長,這也會導致LED燈看起來偏暗。
- loopCount越大,還有一個副作用,就是LED燈的亮度和當前光立方中被點亮的LED燈數量有關。被點亮的LED燈越多,掃描一次光立方的時間越長(只有在被點亮的LED燈處會執行暫停程序,如果某個LED燈為熄滅狀態,直接跳過),再加之每次“暫停”的時間很長,因此出現的一個現象就是,被點亮的LED燈少時,LED燈特別亮,被點亮的LED燈多時,LED燈特別暗,對比十分明顯。
這里之所以使用空語句循環來執行延時(“暫停”),是因為只有這樣才能做到納秒級延時(雖然并不精確)。
如果使用sleep()、usleep()、nanosleep()、,尤其是nanosleep(),雖然函數的目的時暫停納秒級的時間,但是其暫停時間都在微秒以上(在樹莓派上50微秒)。
包括C++11提供的,std::this_thread::sleep_for(std::chrono::nanoseconeds(xxx));
也就是說,即使我寫的程序是 sleep_for(nanoseconds(1))之類的,想要暫停1ns,實際上也會暫停50微秒,也就是這個參數在0~50000之間,程序全都會暫停50微秒左右。這可就太可怕了,如果需要同時點亮256個LED燈,那每次掃描的時間將是50us×256=12800us=12.8ms50us \times 256 = 12800us = 12.8ms50us×256=12800us=12.8ms,這個時間已經太長了,一個發光的LED燈,經過這個時間基本已經很暗或者熄滅了。
剛寫程序時一直困擾在這里,每次點亮的LED燈變多時,LED燈都會特別暗,1個LED燈時特別刺眼,200個LED燈就已經明顯變暗了。本來都想放棄了呢,后來,逐一判斷到底是哪一條語句這么耗時,一開始以為是 digitalWrite函數的原因或者74HC154芯片反應慢之類的,后來才定位到sleep_for(nanoseconds(100))這個延時語句上。然后就去網上搜了一下,了解到精確的納秒級暫停目前很難實現的,因為執行到暫停語句會牽扯到中斷、時間片切換,還有內核調用(要從用戶空間切換到內核再返回)(大概是這些吧,我不是專業的。。。),反正意思就是,你想暫停幾納秒、幾十幾百納秒,做不到!!!
可以看一下以下兩個網頁
四、特效
Effect基類,其他特效類都繼承自該類,需要重寫以下兩個虛函數
// 特效是如何顯示的 virtual void show();// 從一個文件流(文件指針fp當前位置,可能并非文件頭)解析特效的參數 virtual bool readFromFP(FILE* fp);每個特效基本上都有一個Event類,用于描述一組特效參數。
下面以src/effect/layer_scan.h為例
LayerScanEffect類實現的特效是:按照某一個圖案逐層(沿x軸或y軸或z軸)掃描光立方。
// 關于Event部分的代碼 class LayerScanEffect : public Effect { public: struct Event {Event(Direction view, Direction scan, Angle r, int together, int interval1, int interval2);Direction viewDirection;Direction scanDirection;Angle rotate;int together;int interval1;int interval2;};void setEvents(const std::vector<Event>& events) {events_ = events;}protected:std::vector<Event> events_; };這里Event類的成員變量的意思是:
- viewDirection:視角,就是你注視該圖案的方向,沿哪個軸的哪個方向(X_ASCEND、X_DESCNED、Y_ASCEND、Y_DESCEND、Z_ASCEND、Z_DESCEND)
- scanDirection:掃描的方向(圖案移動的方向)
- rotate:圖案的旋轉角度
- together:一次移動多少層
- interval1:每次移動的時間間隔(單位毫秒)
- interval2:掃描結束后暫停的時間(單位毫秒)
(PS. 基本上每個特效都至少有interval,interval2兩個參數,事實上,大多數特效都有四五個甚至更多參數,通過不同參數的組合,即一個Event對象,可以顯示出不一樣的效果,雖然是同一類特效)
四、EML文件
為了更方便的創造出不同參數的(同一大類)特效,我創造了一種新的文本文件類型EML,Effect Markup Language。每個特效類都支持從eml文件讀取參數。
下面看一個簡單的eml例子:
<##>------------------------------- Count Down --------------------------------<LayerScan> <IMAGESCODE><####> imageCode<CODE> 5<CODE> 4<CODE> 3<CODE> 2<CODE> 1 <END_IMAGESCODE> <EVENTS><#####> viewDirection scanDirection rotate together interavl1 interval2<EVENT> X_DESCEND X_ASCEND ANGLE_0 1 125 125 <END_EVENTS> <END><##>------------------------------- Drop Line --------------------------------<DropLine> <IMAGESCODE><CODE> IMAGE_FILL <END_IMAGESCODE> <EVENTS><#####> viewDirection dropDirection lineParallel rotate together interval1 interval2<EVENT> X_ASCEND X_ASCEND PARALLEL_Y ANGLE_0 3 30 30<EVENT> X_ASCEND X_DESCEND PARALLEL_Y ANGLE_0 3 30 30<EVENT> X_ASCEND X_ASCEND PARALLEL_Z ANGLE_0 3 30 30<EVENT> X_ASCEND X_DESCEND PARALLEL_Z ANGLE_0 3 30 30<EVENT> Z_ASCEND Z_ASCEND PARALLEL_X ANGLE_0 3 30 30<EVENT> Z_ASCEND Z_DESCEND PARALLEL_X ANGLE_0 3 30 30<EVENT> Z_ASCEND Z_ASCEND PARALLEL_Y ANGLE_0 3 30 30<EVENT> Z_ASCEND Z_DESCEND PARALLEL_Y ANGLE_0 3 30 30 <END_EVENTS> <END><END><END>解析eml文件的容錯能力比較低,只會簡單地進行語法檢查,應該保證傳入的eml文件沒有語法錯誤。
六、展示(圖片)
(光立方做的比較丑,emmm,關鍵是特效代碼嘛!)
(第一次使用錫焊,足足用了兩卷焊錫,一開始經常焊不上,掉的錫得有1/3,后來慢慢掌握了技巧。孰能生巧,第一次使用錫焊就焊了1000多個焊點,學會了錫焊,哈哈哈!)
七、展示(視頻)
BV1Kz411B7KT
【光立方】【樹莓派】特效展示,20+種
END
leopard.c@outlook.com
總結
- 上一篇: 前端学习(2636):文件缺失
- 下一篇: 工作86:防抖和节流的问题