树莓派

您所在的位置:网站首页 4*4*4光立方制作教程 树莓派

树莓派

2023-09-06 07:00| 来源: 网络整理| 查看: 265

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& 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 = 8 2 \times 4 = 8 2×4=8 种方式显示一个图案。

2种视角:沿着轴的正向还是负向4种旋转角度:0、90、180、270 // file: src/utility/image_lib.cpp std::map 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& 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& 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=xFromxEnd:面的目标位置,即面x=xEndclearXFrom:是否清空原来的面 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很难确定,而且影响亮度的因素比较多。

但是经过测试, C 1 ≈ 100 , C 2 ≈ 200 C1 \approx 100,C2 \approx 200 C1≈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灯,那每次扫描的时间将是 50 u s × 256 = 12800 u s = 12.8 m s 50us \times 256 = 12800us = 12.8ms 50us×256=12800us=12.8ms,这个时间已经太长了,一个发光的LED灯,经过这个时间基本已经很暗或者熄灭了。

刚写程序时一直困扰在这里,每次点亮的LED灯变多时,LED灯都会特别暗,1个LED灯时特别刺眼,200个LED灯就已经明显变暗了。本来都想放弃了呢,后来,逐一判断到底是哪一条语句这么耗时,一开始以为是 digitalWrite函数的原因或者74HC154芯片反应慢之类的,后来才定位到sleep_for(nanoseconds(100))这个延时语句上。然后就去网上搜了一下,了解到精确的纳秒级暂停目前很难实现的,因为执行到暂停语句会牵扯到中断、时间片切换,还有内核调用(要从用户空间切换到内核再返回)(大概是这些吧,我不是专业的。。。),反正意思就是,你想暂停几纳秒、几十几百纳秒,做不到!!!

可以看一下以下两个网页

https://frenchfries.net/paul/dfly/nanosleep.htmlhttps://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& events) { events_ = events; } protected: std::vector 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 -------------------------------- imageCode 5 4 3 2 1 viewDirection scanDirection rotate together interavl1 interval2 X_DESCEND X_ASCEND ANGLE_0 1 125 125 ------------------------------- Drop Line -------------------------------- IMAGE_FILL viewDirection dropDirection lineParallel rotate together interval1 interval2 X_ASCEND X_ASCEND PARALLEL_Y ANGLE_0 3 30 30 X_ASCEND X_DESCEND PARALLEL_Y ANGLE_0 3 30 30 X_ASCEND X_ASCEND PARALLEL_Z ANGLE_0 3 30 30 X_ASCEND X_DESCEND PARALLEL_Z ANGLE_0 3 30 30 Z_ASCEND Z_ASCEND PARALLEL_X ANGLE_0 3 30 30 Z_ASCEND Z_DESCEND PARALLEL_X ANGLE_0 3 30 30 Z_ASCEND Z_ASCEND PARALLEL_Y ANGLE_0 3 30 30 Z_ASCEND Z_DESCEND PARALLEL_Y ANGLE_0 3 30 30 以


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3