LED CUBE. (Driven by RaspberryPi and 74HC154 chip)
【驱动程序 + 20多种特效】
├── effects_list
│ ├── group
│ │ └── fireworks.eml
│ ├── list.eml # 自定义的文件类型(.eml),描述每种特效的参数
│ └── script
│ └── script1
├── led_cube # 可执行程序
├── README.md
├── src # 源码
│ ├── driver
│ │ ├── cube.cpp
│ │ ├── cube_extend.cpp
│ │ ├── cube_extend.h
│ │ ├── cube.h # 核心类(驱动光立方)
│ │ ├── script.cpp
│ │ ├── script.h
│ │ ├── x_74hc154.cpp
│ │ └── x_74hc154.h
│ ├── effect # 特效目录(一种特效对应一个类)
│ │ ├── breath_cube.cpp
│ │ ├── breath_cube.h
│ │ ├── cube_size_from_inner.cpp
│ │ ├── cube_size_from_inner.h
│ │ ├── cube_size_from_vertex.cpp
│ │ ├── cube_size_from_vertex.h
│ │ ├── drop_line.cpp
│ │ ├── drop_line.h
│ │ ├── drop_point.cpp
│ │ ├── drop_point.h
│ │ ├── drop_text_point.cpp
│ │ ├── drop_text_point.h
│ │ ├── effect.h # 所有特效类的基类
│ │ ├── fireworks_from_center.cpp
│ │ ├── fireworks_from_center.h
│ │ ├── function_graph.cpp
│ │ ├── function_graph.h
│ │ ├── layer_scan.cpp
│ │ ├── layer_scan.h
│ │ ├── random_drop_point.cpp
│ │ ├── random_drop_point.h
│ │ ├── random_height.cpp
│ │ ├── random_height.h
│ │ ├── random_light.cpp
│ │ ├── random_light.h
│ │ ├── rise_and_fall
│ │ ├── snake.cpp
│ │ ├── snake.h
│ │ ├── text_scan.cpp
│ │ ├── text_scan.h
│ │ ├── wander_edge.cpp
│ │ ├── wander_edge.h
│ │ ├── wander_edge_join_auto_inc.cpp
│ │ ├── wander_edge_join_auto_inc.h
│ │ ├── wander_edge_join.cpp
│ │ └── wander_edge_join.h
│ ├── main.cpp
│ └── utility
│ ├── coordinate.h
│ ├── enum.h
│ ├── ExpressionEvaluator.cpp
│ ├── ExpressionEvaluator.h
│ ├── image_lib.cpp
│ ├── image_lib.h
│ ├── snake.cpp
│ ├── snake.h
│ ├── utils.cpp
│ └── utils.h
└── xmake.lua # 使用 xmake 构建
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();
}
下面介绍以下该类对外提供的接口:
初始化。
事实上,整个程序,只有一个LedCube的全局对象,定义在main函数所在的文件中,在其他地方通过extern关键字进行声明:
// main.cpp
LedCube cube;
// other files
extern LedCube cube;
在主函数调用setup()
函数,用于初始化74HC154芯片
、熄灭所有LED灯等。
对光立方做一系列修改后,只有调用update()
函数,才能真正起作用。
退出函数,执行清理工作,正常退出的话,会由析构函数调用。
非正常退出,比如捕获到Ctrl+C
发出的SIGINIT
信号,应该主动调用该函数进行清理,否则程序退出时可能有一些LED仍然亮着。
熄灭所有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;
可以是垂直于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轴正向的方向观察该图像。 Вращение:
То есть, на любой вертикальной плоскости, перпендикулярной осям x, y или z, можно отобразить рисунок 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 }) }
};
(1) Полностью осветить или затемнить строку или столбец:
void lightRowXY(int x, int y, LedState);
void lightRowYZ(int y, int z, LedState);
void lightRowXZ(int x, int z, LedState);
(2) Указать состояние для каждого из восьми светодиодов в строке или столбце:
void lightRowXY(int x, int y, const std::array<LedState,8>& states);
void lightRowYZ(...);
void lightRowXZ(...);
// Пример ниже: строка с x == 5 и y == 7 будет освещена, светодиоды будут включаться через один
// LED_ON == 1, означает включение
// LED_OFF == 0, означает выключение
lightXY(5, 7, { 1, 0, 1, 0, 1, 0, 1, 0 });
void lightLine(const Coordinate& start, const Coordinate& end, LedState state);
start
: начальная точка линии (x1, y1, z1)
;end
: конечная точка линии (x2, y2, z2)
.Эта функция фактически вызывает функцию getLine3D
в файле src/utility/utils.h
.
Используется алгоритм генерации линий Брезенхема.
void getLine3D(const Coordinate& start, const Coordinate& end, std::vector<Coordinate>& line);
Для заданных начальной и конечной точек линии эта функция возвращает все точки (целые координаты) на этой линии.
После получения всех точек необходимо установить состояние светодиодов в этих точках.
void lightSquare(const Coordinate& A, const Coordinate& B, FillType fillType);
AB
: диагональ прямоугольника;fillType
: тип заполнения:
FILL_SOLID
: сплошное заполнение;FILL_SURFACE
: поверхностное заполнение (без заполнения внутри);FILL_EDGE
: только границы (без заполнения поверхности и внутри).void lightCube(const Coordinate& A, const Coordinate& B, FillType fillType);
AB
: диагональ параллелепипеда;fillType
: тип заполнения:
FILL_SOLID
: сплошное заполнение;FILL_SURFACE
: заполнение только поверхности (не заполнение внутри);FILL_EDGE
: только границы (заполнение поверхности и внутри отсутствует).void copyLayerX(int xFrom, int xEnd, bool clearXFrom = false);
void copyLayerY(...);
void copyLayerZ(...);
xFrom
: исходное положение поверхности, то есть поверхность x = xFrom
;xEnd
: целевое положение поверхности, то есть поверхность x = xEnd
;clearXFrom
: очистить ли исходную поверхность:
true
: переместить;false
: скопировать.void setLoopCount(int count) {
this->loopCount = count;
}
Эффект заключается в управлении яркостью светодиодов.
Здесь предполагается наличие двух пороговых значений $ 0 < C1 < C2 < +\infty$:
count < C1
, яркость уменьшается с уменьшением значения count
.count > C2
, яркость увеличивается с увеличением значения count
.C1 < count < C2
светодиоды достаточно яркие, и изменение яркости незаметно для глаза.Здесь C1
и C2
трудно определить, и существует множество факторов, влияющих на яркость.
Однако после тестирования было обнаружено, что $C1 \approx 100$, $C2 \approx 200$.
Фактически, значение count
здесь влияет на время, в течение которого каждый светодиод остаётся включённым. Поскольку в любой момент времени может быть включён только один светодиод, фоновый поток непрерывно сканирует весь световой куб, выполняя 512 циклов и последовательно проверяя, нужно ли включать каждый светодиод.
Каждый включенный светодиод будет «приостановлен» на короткое время (очень короткое), а затем выключен, чтобы включить следующий светодиод, который должен быть включен.
«Приостановка» здесь реализуется с помощью пустого цикла:
// Здесь loopCount устанавливается с помощью setLoopCount (int count).
for (int i = 0; i < loopCount; ++i) {
// ;
}
На Raspberry Pi расчётное время выполнения пустого цикла составляет 5–6 нс, а значение по умолчанию для loopCount
равно 150, что эквивалентно приостановке на 800 нс.
Значение loopCount
, будь оно больше или меньше, приведёт к затемнению светодиодов, причём слишком большое значение также вызовет другие побочные эффекты, такие как:
loopCount
меньше: время включения каждого светодиода становится короче, и они кажутся более тусклыми. Однако после тестирования было обнаружено, что при значениях loopCount
от 100 до 200 изменение яркости светодиодов незначительно, и значения менее 100 или даже 50 могут привести к заметному затемнению. При значениях loopCount
около 5 светодиоды практически полностью перестают светиться.loopCount
больше: время включения каждого светодиода увеличивается, но также увеличивается время сканирования светового куба за один раз, что приводит к увеличению промежутка времени между включениями каждого светодиода, то есть времени, когда светодиод не получает питание, что также приводит к затемнению светодиода.loopCount
слишком велико, возникает ещё один побочный эффект: яркость светодиода зависит от количества включённых светодиодов в световом кубе. Чем больше светодиодов включено, тем дольше время сканирования одного светового куба (только включённые светодиоды выполняют «паузу»), и чем больше интервал времени между включениями, тем больше затемняются светодиоды. Здесь используется пустой оператор цикла для выполнения задержки («паузы»), потому что только так можно добиться задержки наносекундного уровня (хотя и не очень точной).Если использовать sleep(), usleep(), nanosleep() — особенно nanosleep(), хотя цель функции — пауза наносекундной длительности, но длительность паузы у всех них больше микросекунды (на Raspberry Pi — 50 микросекунд).
Включая C++11, std::this_thread::sleep_for(std::chrono::nanoseconds(xxx));
То есть, даже если я пишу программу sleep_for(nanoseconds(1)), чтобы сделать паузу в 1 нс, на самом деле будет пауза около 50 мкс, то есть параметр от 0 до 50 000, вся программа будет делать паузу примерно на 50 мкс. Это уже слишком долго, одного светодиода за это время уже почти не видно или он полностью погаснет.
Когда я только начал писать программу, меня всё время беспокоила эта проблема: когда количество одновременно включаемых светодиодов увеличивалось, светодиоды становились всё темнее, один светодиод был особенно ярким, а с 200 светодиодами разница была уже очевидна. Я уже собирался сдаться, но потом постепенно понял, какая именно строка кода вызывает задержку, сначала я думал, что это из-за функции 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, фактически большинство спецэффектов имеют четыре или пять параметров, и разные комбинации параметров могут создавать разные эффекты, хотя это один и тот же тип спецэффектов.)
Чтобы было удобнее создавать различные параметры (одного и того же типа) спецэффектов, я создал новый текстовый файл формата 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. Шесть, демонстрация (видео).
END
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Комментарии ( 0 )