日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

树莓派-光立方

發布時間:2023/12/9 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 树莓派-光立方 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

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
// file: src/utility/image_lib.cpp std::map<int, std::array<std::array<char, 8>, 8>> ImageLib::table = {{ '0', util::toBinary({ 0x1C, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x1C }) },{ '1', util::toBinary({ 0x08, 0x18, 0x08, 0x08, 0x08, 0x08, 0x08, 0x1C }) },{ '2', util::toBinary({ 0x1C, 0x22, 0x02, 0x02, 0x1C, 0x20, 0x20, 0x3E }) },// ...{ '9', util::toBinary({ 0x1C, 0x22, 0x22, 0x22, 0x1E, 0x02, 0x22, 0x1C }) },{ 'A', util::toBinary({ 0x00, 0x1C, 0x22, 0x22, 0x22, 0x3E, 0x22, 0x22 }) },{ 'B', util::toBinary({ 0x00, 0x3C, 0x22, 0x22, 0x3E, 0x22, 0x22, 0x3C }) },{ 'C', util::toBinary({ 0x00, 0x1C, 0x22, 0x20, 0x20, 0x20, 0x22, 0x1C }) },// ...{ 'Z', util::toBinary({ 0x00, 0x3E, 0x02, 0x04, 0x08, 0x10, 0x20, 0x3E }) },// 自定義的圖案// 直徑為3的圓{ Image_Circle_Solid_3, util::toBinary({ 0x00, 0x18, 0x3C, 0x7E, 0x7E, 0x3C, 0x18, 0x00 }) },// 8x8的實心矩形(8x8=64個LED燈全部點亮){ Image_Fill , util::toBinary({ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }) }, };

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 200C1100C2200

這里的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))這個延時語句上。然后就去網上搜了一下,了解到精確的納秒級暫停目前很難實現的,因為執行到暫停語句會牽扯到中斷、時間片切換,還有內核調用(要從用戶空間切換到內核再返回)(大概是這些吧,我不是專業的。。。),反正意思就是,你想暫停幾納秒、幾十幾百納秒,做不到!!!

可以看一下以下兩個網頁

  • https://frenchfries.net/paul/dfly/nanosleep.html
  • https://stackoverflow.com/questions/18071664/stdthis-threadsleep-for-and-nanoseconds
  • 四、特效

    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>
  • 以<#開頭的行是注釋行,忽略,也就是說, <#,<#>,<##>,<####>等開頭的都是注釋。
  • <COMMENT>和<END_COMMENT>行之間的所有行都是注釋,忽略。
  • 不區分大小寫
  • <EVENTS> 和 <END_EVENTS>之間是一系列 <EVENT>
  • 以<EVENT>開頭的是一組特效參數(注意有個空格)
  • <IMAGESCODE>和END_IMAGESCODE>之間是一系列<CODE>
  • 以<CODE>開頭的是一個圖案的代碼,如Letter_A或者A都是表示字母A的圖案,IMAGE_FILL表示8x8完全填充的正方形,NUM_0或者0表示數字0的圖案,以及其他自定義的圖案代碼。
  • <EML>表示在此處插入其他eml文件。
  • <Script>表示在此處插入script文件(也是自己定義的一種文件類型,屬于腳本語言,一行表示一條語句,每條語句的功能就是調用LedCube類中的相應的函數)。
  • <END>表示這一種特效結束。
  • <END><END>表示文件結束,忽略之后的所有內容
  • 解析eml文件的容錯能力比較低,只會簡單地進行語法檢查,應該保證傳入的eml文件沒有語法錯誤。

    六、展示(圖片)

    (光立方做的比較丑,emmm,關鍵是特效代碼嘛!)

    (第一次使用錫焊,足足用了兩卷焊錫,一開始經常焊不上,掉的錫得有1/3,后來慢慢掌握了技巧。孰能生巧,第一次使用錫焊就焊了1000多個焊點,學會了錫焊,哈哈哈!)



    七、展示(視頻)

    BV1Kz411B7KT

    【光立方】【樹莓派】特效展示,20+種

    END

    leopard.c@outlook.com

    總結

    以上是生活随笔為你收集整理的树莓派-光立方的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。