Cyclone 10 LP, WS2812, SK6812RGBW

Небольшая статья о простом тесте управляемых светодиодов WS2812 и SK6812RGBW, как их правильно подключить к 3,3 В IO ПЛИС и как этим всем крайне просто можно управлять из приложения на ПК.

Посмотрев OpenSource драйверы для WS2812, я несколько удивился — на Verilog нашелся только один и тот показался не шибко оптимальным, поэтому было решено написать свой. Со своими костылями и багами. :)

Так как под рукой есть неплохая отладочная плата Intel Cyclone 10 LP FPGA Evaluation Kit, то на ней и буду тестировать. В закромах нашлась ардуиновская плата-щит-шилд для прототипирования (крайне паршивой разводки, покупать себе такую не советую! Выиграл пару штук на аукционе eBay, но вот покупать такое я точно больше не буду и вам не советую!) — ардуино-стайл проект по морганию светодиодами на ПЛИС должен удасться на славу! ;D

B64-0

Железячная часть

Начнём с самого простого — подключим UART на любые свободные IO 3,3 В. На этой плате все IO с уровнями 3,3 В, я подключил к GPIO0 (Tx) и GPIO1 (Rx). На своей отладочной плате с этим будьте внимательны!

B64-1

Следующий шаг — это согласование логического уровней для светодиодов. Им требуется по даташиту 0,7*Vdd для логической 1-цы, а это = 3,5 В при питании 5 В. На ардуино-форумах лепят как попало — может и так заработать, если питание просядет, и тогда 3,3 В будет хватать, иногда советуют диод поставить по питанию светодиодов, чтобы искусственно занизить их питание, и другие различные костыли. Мы так делать не будем, а сделаем по уму:

B64-2

Обычный шинный повторитель-согласователь уровней SN74AVC1T45, который имеет раздельное питание для портов и вход переключения направления. Внутри он прост и незатейлив:

B64-21

Работает только в одну сторону, задержка сигнала в худшем случае 3,3 нс (при питании 1,2 В), скорость передачи до 500 Мбит (1,8 В и 3,3 В согласование) и имеет удобный корпус SOT как для пайки в любительском применении, так и DSBGA для более компактных устройств.

Спаял на плате 2 таких согласователя и по 3 шт каждого типа светодиодов, вышло как-то так:

B64-3

По питанию 5 В добавил с запасом емкостей 10 мкФ, один 10 мкФ у микросхем по 3,3 В питанию, а также у каждого ряда светодиодов по 0,1 мкФ:

B64-4

Также на фото выше можно видеть, что все питающие дорожки я продублировал, потому что плата имеет кривую разводку — все тонкими линиями разведено и зачастую довольно длинными…

В итоге спаянный макет для тестирования светодиодов выглядит вот так:

B64-5

Но это не всё, на светодиоды сверху наклеена светорассеивающая полоска пластика, подобные стоят в ЖКИ телевизорах или мониторах, для распределения и выравнивания подсветки матрицы:

B64-6

ПЛИСовая часть

Теперь дело за «прошивкой» для ПЛИС. Общая структура получилась следующая (кликабельно):

Условно всё можно разбить на 4 основных модуля:

  • UART — самый простой вариант для отправки\приёма данных или команд (взят из статьи Реализация стабильного UART с минимальными изменениями);
  • LedDataSelector — простейшая реализация модуля для разбора принятых данных из UART и распределение принятой «структуры» в ту или иную память светодиодов;
  • RAM_2P — двухпортовая память для хранения состояния светодиодов всей линейки, их тут две — на каждый тип светодиодов свой модуль памяти (практически без изменений из примера True Dual-Port RAM with a Single Clock);
  • WS2812\SK6812RGBW — собственно, сами драйверы светодиодов, автоматически отправляющие данные на всю линейку светодиодов и автоматически считывающие необходимые значения из памяти по инкриминируемому адресу;

Модуль UART я рассматривать не буду, это и так хорошо сделано автором в его статье.

Следующий модуль — LedDataSelector. Его код:


module LedDataSelector
(
input wire clock,
input wire reset,

input wire [7:0] UART_Rx,
input wire UART_RxReady,

output reg [31:0] LED0_Data,
output reg [31:0] LED0_Addr,
output reg LED0_Write,

output reg [31:0] LED1_Data,
output reg [31:0] LED1_Addr,
output reg LED1_Write
);

localparam STATE_RX_0_BYTE = 3'd0;
localparam STATE_RX_OTHER = 3'd1;

reg [2:0] current_state = STATE_RX_0_BYTE;
reg [4:0] current_byte = 0;

reg [31:0] LED_Data;
reg [31:0] LED_Addr;

always @(posedge UART_RxReady or posedge reset)
begin
if(reset)
begin
LED0_Data <= 32'd0;
LED0_Addr <= 32'd0;

LED1_Data <= 32'd0;
LED1_Addr <= 32'd0;

current_state <= STATE_RX_0_BYTE;
end
else
begin
case (current_state)
STATE_RX_0_BYTE:
begin
current_byte <= 1;
LED0_Write <= 1'b0;
LED1_Write <= 1'b0;
LED_Addr <= {24'd0, UART_Rx[7:0]};
current_state <= STATE_RX_OTHER;
end

STATE_RX_OTHER:
begin
if(UART_RxReady)
begin
case (current_byte)
1:
LED_Addr <= {16'd0, UART_Rx[7:0], LED_Addr[7:0]};
2:
LED_Addr <= {8'd0, UART_Rx[7:0], LED_Addr[15:0]};
3:
LED_Addr <= {UART_Rx[7:0], LED_Addr[23:0]};
4:
LED_Data <= {24'd0, UART_Rx[7:0]};
5:
LED_Data <= {16'd0, UART_Rx[7:0], LED_Data[7:0]};
6:
LED_Data <= {8'd0, UART_Rx[7:0], LED_Data[15:0]};
7:
begin
LED_Data = {UART_Rx[7:0], LED_Data[23:0]};

if(LED_Addr[31] == 1'b1)
begin
LED_Addr[31] = 1'b0;
LED1_Data <= LED_Data;
LED1_Addr <= LED_Addr;
LED1_Write <= 1'b1;
end
else
begin
LED0_Data <= LED_Data;
LED0_Addr <= LED_Addr;
LED0_Write <= 1'b1;
end

current_state <= STATE_RX_0_BYTE;
end
endcase // current_byte

current_byte <= current_byte + 1;
end
end

endcase // current_state
end
end

endmodule

И получившийся автомат конечных состояний крайне прост:
B64-8

Логика работы следующая — ждём фронта сигнала UART_RxReady, который обозначает, что UART принял новый байт данных, если состояние конечного автомата STATE_RX_0_BYTE , то сбрасываются все внутренние регистры и записывается принятый байт как младший для LED_Addr (адрес светодиода).

Здесь стоит забежать вперёд и для прозрачности понимания описать, какой формат имеет посылка данных по UART:


[StructLayout(LayoutKind.Explicit, Pack = 1)]
struct LedData
{
[MarshalAs(UnmanagedType.U4)]
[FieldOffset(0)]
public UInt32 addr;

[MarshalAs(UnmanagedType.U4)]
[FieldOffset(4)]
public UInt32 color; // RGBW (0...31)
}

Да, да, банально 2-ва unsigned Int-32 подряд. Последовательно отсылаются в UART, начиная с младшего байта, сначала адрес, потом данные (цвет светодиода), причём для WS2812 используются только первые 24-бита данных, т. к. белого отдельного в них нет, а формат для упрощения реализации можно использовать такой же, как и для SK6812RGBW.

Так вот, из состояния  STATE_RX_0_BYTE автомат переходит в следующее состояние — STATE_RX_OTHER, и здесь просто тупо принимаются последующие байты и последовательно записываются в свои места адреса и данных, а по приему 8-го (последнего для 1-й структуры) на основе принятого адреса (смотрится наличие 1-цы в самом старшем разряде) и выставляются принятые данные (цвет) в RAM для WS2812 (старший бит адреса = 0) или в RAM для SK6812RGBW (старший бит адреса = 1, перед записью он сбрасывается).

Логическая реализация модуля в графическом виде (RTL Viewer) выглядит, конечно, монструозно в сравнении с HDL описанием на Verilog:

B64-10

По хорошему, здесь необходимо реализовать ещё две вещи, которые просто необходимы в боевом применении, а не на столе в идеальных условиях:

  • Таймаут приёма — если n-времени не было принято ожидаемое количество байт для одной структуры, то состояние конечного автомата необходимо сбросить в изначальное, иначе при неожиданном прекращении передачи и последующим её возобновлением данные уже будут сдвинуты и ПЛИС будет «принимать» совсем не то, что в неё передаётся;
  • Контрольная сумма — как дополнение первого, если время не вышло или банально были помехи на линии, то от приёма мусора или некорректных данных защитит добавление контрольной суммы (даже банального сложения всего с переполнением в один байт хотя бы).

Но эти пункты я оставлю на реализацию Вами. :)

Модуль RAM_2P — ничего интересного в целом… код:


module RAM_2P
#(
parameter ADDR_WIDTH = 32, // Сколько бит содержит адресация
parameter DATA_WIDTH = 32 // Сколько бит содержат данные
)
(
input [DATA_WIDTH-1:0] data_a, data_b,
input [ADDR_POINTER_WIDTH:0] addr_a, addr_b,
input we_a, we_b,
input clock,
output reg [DATA_WIDTH-1:0] rd_a, rd_b
);

localparam integer ADDR_POINTER_WIDTH = $clog2(ADDR_WIDTH);

reg [DATA_WIDTH-1:0] ram[ADDR_WIDTH-1:0];

always @ (posedge clock)
begin
if (we_a)
begin
ram[addr_a] <= data_a;
rd_a <= data_a;
end
else
begin
rd_a <= ram[addr_a];
end
end

always @ (posedge clock)
begin
if (we_b)
begin
ram[addr_b] <= data_b;
rd_b <= data_b;
end
else
begin
rd_b <= ram[addr_b];
end
end

endmodule

Из изменений оригинального примера здесь только добавление параметров для задания битности адреса и данных.

Логическая реализация модуля памяти в графическом виде (RTL Viewer) крайне проста и в целом представляет из себя использование встроенного блока SYNC_RAM:

B64-9

Ну и последний модуль для WS2812\SK6812RGBW, ради которого всё это и затевалось. В нём так же реализован конечный автомат, но уже побольше состояний, чем 2 :) Его работа выглядит вот так (здесь не показан только синхронный сброс с любого состояния в STATE_RESET):

B64-11

По состояниям, если кратко описать, что, как и зачем, логика работы следующая:

  • STATE_RESET — состояние, в котором сбрасываются все счетчики и выходная линия ws_data в 0, после чего устанавливается следующим состояние STATE_PREPARE_LATCH;
  • STATE_PREPARE_LATCH — состояние, в течение которого держится 1-ца на линии new_data_req, и оно длится заданное количество тактов параметром PREPARE_LATCH_DELAY, после чего устанавливается следующим состояние STATE_LATCH;
  • STATE_LATCH — в этом состоянии происходит защелкивание данных с шины color_rgb во внутренние регистры цветов, устанавливается первый передаваемый цвет (зелёный) и устанавливается следующим состояние STATE_PREPARE_TRANSMIT;
  • STATE_PREPARE_TRANSMIT — здесь сбрасываются счетчики передаваемого бита цвета и тактов, а также на основе текущего цвета задаётся регистр передаваемого цвета led_current_color, после чего устанавливается следующим состояние STATE_TRANSMIT;
  • STATE_TRANSMIT — в этом состоянии на основе счетчика тактов генерируется протокол для управления светодиодом и отправляется в выходную линию ws_data, каждый бит передаётся в заданный период, который устанавливается параметром CLOCK_CYCLE_COUNT (количество тактов на период), а также значение выходной линии задаётся параметрами T1H_CYCLE_COUNT (сколько держать в 1 выходную линию, если текущий бит = 1) и T0H_CYCLE_COUNT (сколько держать в 1 выходную линию, если текущий бит = 0). После того, как счёт текущего бита доходит до 0 (передача ведётся начиная со старших бит), производится проверка, был ли это последний цвет текущего светодиода, если нет — устанавливается следующий цвет и устанавливается состояние STATE_PREPARE_TRANSMIT, если был последний, то проверяется, был ли это последний светодиод, если нет — увеличивается адрес текущего светодиода и устанавливается состояние STATE_PREPARE_LATCH для считывания следующего значения данных, если же это был последний светодиод, то устанавливается состояние STATE_SEND_RESET;
  • STATE_SEND_RESET — это состояние, простой счётчик паузы между посылками данных на всю линейку светодиодов, в нём выходная линия ws_data устанавливается в 0 на время, заданное параметром RESET_CYCLE_COUNT, светодиоды на этот момент принимают переданные в них данные и отображают соответствующие цвета, после окончания паузы устанавливается состояние STATE_RESET и всё повторяется по кругу;

Код для SK6812RGBW (аналогичен WS2812 за исключением того, что отправляется не 32 бита, а 24, и нет работы с байтом «белого цвета»):


module SK6812RGBW
#(
parameter LEDS_NUM = 3, // Сколько светодиодов
parameter PREPARE_LATCH_DELAY = 10, // сколько тактов ожидать новые данные
parameter CLOCK_FRQ = 50_000_000 // Частота тактового сигнала
)
(
input wire clock,
input wire reset,
input wire [31:0] color_rgbw, // Используется все биты, по байтам на каждый цвет (младший - R, старший - W)
output reg new_data_req,
output reg [LED_ADDR_WIDTH-1:0] current_ledN,
output reg ws_data
);

localparam integer CLOCK_CYCLE_COUNT = CLOCK_FRQ / 800_000;
localparam integer T0H_CYCLE_COUNT = 0.35 * CLOCK_CYCLE_COUNT;
localparam integer T1H_CYCLE_COUNT = 0.9 * CLOCK_CYCLE_COUNT;
localparam integer RESET_CYCLE_COUNT = 600 * CLOCK_CYCLE_COUNT; // с запасом
localparam integer LED_ADDR_WIDTH = $clog2(LEDS_NUM);
localparam integer CLK_COUNTER_WIDTH = $clog2(RESET_CYCLE_COUNT);

localparam STATE_RESET = 3'd0;
localparam STATE_PREPARE_LATCH = 3'd1;
localparam STATE_LATCH = 3'd2;
localparam STATE_PREPARE_TRANSMIT = 3'd3;
localparam STATE_TRANSMIT = 3'd4;
localparam STATE_SEND_RESET = 3'd5;

reg [CLK_COUNTER_WIDTH-1:0] clk_counter;
reg [2:0] current_state = STATE_RESET;
reg [2:0] current_color;
reg [2:0] current_bit;

reg [7:0] led_red;
reg [7:0] led_green;
reg [7:0] led_blue;
reg [7:0] led_white;
reg [7:0] led_current_color;

always @ (posedge clock)
begin
if (reset)
begin
ws_data <= 0;
current_state <= STATE_RESET;
end
else
begin
case (current_state)
STATE_RESET:
begin
ws_data <= 0;
clk_counter <= 0;
current_ledN <= 0;
current_state <= STATE_PREPARE_LATCH;
end

STATE_PREPARE_LATCH:
begin
new_data_req = PREPARE_LATCH_DELAY)
current_state <= STATE_LATCH;
else
clk_counter <= clk_counter + 1'b1;
end

STATE_LATCH:
begin
new_data_req <= 0;
led_red <= color_rgbw[7:0];
led_green <= color_rgbw[15:8];
led_blue <= color_rgbw[23:16];
led_white <= color_rgbw[31:24];

current_color <= 0; // зеленый
current_state <= STATE_PREPARE_TRANSMIT;
end

STATE_PREPARE_TRANSMIT:
begin
clk_counter <= 0;
current_bit <= 3'd7;

if(current_color == 2'd0) // зеленый
led_current_color <= led_green;
else if(current_color == 2'd1) // красный
led_current_color <= led_red;
else if(current_color == 2'd2) // синий
led_current_color <= led_blue;
else if(current_color == 2'd3) // белый
led_current_color <= led_white;

current_state = T1H_CYCLE_COUNT)
ws_data = T0H_CYCLE_COUNT)
ws_data <= 0;
else
ws_data = CLOCK_CYCLE_COUNT)
begin
clk_counter <= 0;

if(current_bit == 3'd0)
begin
if(current_color == 2'd3)
begin
if(current_ledN == LEDS_NUM)
current_state <= STATE_SEND_RESET;
else
begin // следующий светодиод
current_ledN <= current_ledN + 1'd1;
current_color <= 0; // зеленый
clk_counter <= 0;
current_state <= STATE_PREPARE_LATCH;
end
end
else
begin
current_color <= current_color + 1'b1;
current_state <= STATE_PREPARE_TRANSMIT;
end
end
else
current_bit <= current_bit - 1'b1;
end
else
clk_counter <= clk_counter + 1'b1;
end

STATE_SEND_RESET:
begin

if(clk_counter < RESET_CYCLE_COUNT)
begin
clk_counter <= clk_counter + 1'b1;
ws_data <= 0;
end
else
current_state <= STATE_RESET;
end
endcase // current_state
end
end

endmodule

Логическая реализация модуля в графическом виде (RTL Viewer):

B64-12

Можно зашить получившийся проект в ПЛИС и … ничего не светится. Правильно. :) Надо ведь теперь задать в памяти значения, чего выводить-то…

Примечание: Разве что можно инициализировать память начальными значениями, отличными от нуля, таким способом (добавить в RAM_2P модуль):


initial begin

$readmemb("raminit.txt", ram);

end

Файл raminit.txt должен иметь вид (каждый элемент памяти на своей строке в порядке возрастания, данные в HEX формате):

00АА0033
BB00CC00
FFAA2700
...

WPF\C# часть

Ну и последняя часть, самая простая в целом — написание тестового приложения, в котором можно будет задавать цвет\оттенок, оно будет отправлять необходимые структуры в UART.

Само приложение состоит из следующих классов и, собственно, описания управляющей структуры:

B64-13

Большая часть представленных выше классов — вспомогательные, визуальные или банально конвертеры величин для XAML. Основным же является единственный статичный класс — ColorGen, в метод которого передаётся массив цветовых свойств и возвращается массив байт, который уже просто отсылается в UART без каких либо преобразований.

Вот один из методов для преобразования свойств типа Color (из System.Windows.Media) в массив байт для светодиодов :


public static byte[] GenWS2812(Color[] cc)
{
UInt32 count = 0;
if (cc.Length > 0)
{
byte[] data = new byte[cc.Length * 4 * 2]; // кол-во светодиодов * 4 байта в UINT32 * кол-во параметров в структуре

foreach (Color c in cc)
{
byte r = (byte)(c.R * c.ScA);
byte g = (byte)(c.G * c.ScA);
byte b = (byte)(c.B * c.ScA);
LedData led = new LedData() { addr = count, color = (UInt32)(r + (g << 8) + (b << 16)) };
byte[] data_b = getBytes(led);
data_b.CopyTo(data, count * 4 * 2);
count++;
}

return data;
}
return null;
}

Метод сделан универсальным, на основе длины входного массива сс генерируется необходимой длины массив байт и он уже заполняется по образу структуры LedData. Напомню, структура LedData, описывающая один светодиод, выглядит так:


[StructLayout(LayoutKind.Explicit, Pack = 1)]
struct LedData
{
[MarshalAs(UnmanagedType.U4)]
[FieldOffset(0)]
public UInt32 addr;

[MarshalAs(UnmanagedType.U4)]
[FieldOffset(4)]
public UInt32 color; // RGBW (0...31)
}

В целом больше ничего примечательного в приложении по коду нет. Размечаем интерфейс в XAML (а-ля веббер):

B64-14

Компилируем, запускаем:

B64-15

Пруф-оф-ворк

Настало время проверять работоспособность! И оно работает как и ожидалось (тест WS2812):

B64-16

Или тест белых светодиодов в SK6812RGBW (RGB часть аналогична WS2812):

B64-17

Или вот несколько попыток сфотографировать на телефон, но увы, ШИМ светодиодов (хоть и аж 400 Гц) даёт о себе знать — или все пересвечено, но без полос или цвет лучше передаётся, но с полосами:

Это слайд-шоу требует JavaScript.

Если есть сомнения в работоспособности — качаем проект, проверяем сами. ;) Найдете баги — буду рад, если сообщите!

Ссылки

Реклама

Добавить комментарий

Please log in using one of these methods to post your comment:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s