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