1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/DeweyDu-LedCube

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
В этом репозитории не указан файл с открытой лицензией (LICENSE). При использовании обратитесь к конкретному описанию проекта и его зависимостям в коде.
Клонировать/Скачать
Внести вклад в разработку кода
Синхронизировать код
Отмена
Подсказка: Поскольку Git не поддерживает пустые директории, создание директории приведёт к созданию пустого файла .keep.
Loading...
README.md

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 构建

二、核心类LedCube解析(src/driver/cube.h)

程序运行大概的流程:

image-20200513124815442

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轴正向的方向观察该图像。 Вращение:
  • Поворот: поддержка:
    • ANGLE_0: без поворота;
    • ANGLE_90: поворот по часовой стрелке на 90 градусов;
    • ANGLE_180: поворот по часовой стрелке на 180 градусов;
    • ANGLE_270: поворот по часовой стрелке на 270 градусов.

То есть, на любой вертикальной плоскости, перпендикулярной осям 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 }) }
};

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) Указать состояние для каждого из восьми светодиодов в строке или столбце:

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 });

2.8 Освещение / затемнение прямой линии в пространстве

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);

Для заданных начальной и конечной точек линии эта функция возвращает все точки (целые координаты) на этой линии.

После получения всех точек необходимо установить состояние светодиодов в этих точках.

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.
  • Когда 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)). Затем я поискал информацию в интернете и узнал, что точное выполнение наносекундных пауз сейчас трудно реализовать, потому что выполнение до строки задержки включает в себя прерывания, переключение временных интервалов и вызовы ядра (вероятно, это они, я не специалист…), в любом случае, смысл в том, что вы хотите сделать паузу на несколько наносекунд, несколько десятков или сотен наносекунд — это невозможно!

Можете посмотреть следующие две веб-страницы:

  1. https://frenchfries.net/paul/dfly/nanosleep.html
  2. 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, фактически большинство спецэффектов имеют четыре или пять параметров, и разные комбинации параметров могут создавать разные эффекты, хотя это один и тот же тип спецэффектов.)

Четыре. Файлы 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>
  1. Строки, начинающиеся с <#, являются комментариями, их следует игнорировать, то есть <#, <#> и т. д. являются комментариями.
  2. Между строками <COMMENT> и <END_COMMENT> находятся комментарии, которые следует игнорировать.
  3. Регистр не учитывается.
  4. <EVENTS> и <END_EVENTS> содержат серию <EVENT>.
  5. Строка, начинающаяся с <EVENT>, представляет собой набор параметров спецэффектов (обратите внимание на пробел).
  6. <IMAGESCODE> и END_IMAGESCODE> содержат серию <CODE>.
  7. Строка, начинающаяся с <CODE>, является кодом изображения, например, Letter_A или A представляют изображение буквы A, IMAGE_FILL представляет заполненный квадрат 8x8, NUM_0 или 0 представляют изображение цифры 0 и другие пользовательские коды изображений.
  8. <EML> указывает на вставку других файлов EML здесь.
  9. <Script> указывает на вставку файлов script (также является определённым типом файла, принадлежащим к языку сценариев, одна строка представляет одну инструкцию, каждая инструкция функции в классе LedCube).
  10. <END> означает конец этого спецэффекта.
  11. <END><END> означает завершение файла, игнорируйте всё последующее содержимое.

Разбор файлов EML имеет относительно низкую отказоустойчивость, он просто выполняет простую проверку синтаксиса и должен гарантировать отсутствие синтаксических ошибок в переданных файлах EML. Шесть, демонстрация (видео).

END

Комментарии ( 0 )

Вы можете оставить комментарий после Вход в систему

Введение

Гоунг лифан использует управление через чип 74HC154 на Raspberry Pi. Более 20 различных спецэффектов. Развернуть Свернуть
Отмена

Обновления

Пока нет обновлений

Участники

все

Недавние действия

Загрузить больше
Больше нет результатов для загрузки
1
https://api.gitlife.ru/oschina-mirror/DeweyDu-LedCube.git
git@api.gitlife.ru:oschina-mirror/DeweyDu-LedCube.git
oschina-mirror
DeweyDu-LedCube
DeweyDu-LedCube
master