Использование UDB в микроконтроллерах PSOC 4 и 5LP Infineon (Cypress) для управления светодиодами WS2812

  • Tutorial

Светодиоды типа WS2812 очень популярны, ими удобно управлять, передавая по одному проводу команды для сотен светодиодов. Они имеют, с одной стороны, очень простой протокол, а с другой стороны, в микроконтроллерах нет аппаратных интерфейсов для этого протокола и его приходится формировать программно управляя выводом микроконтроллера. В этом посте я расскажу, как с помощью UDB микроконтроллеров серии PSOC 4 и PSOC 5LP компании Infineon сделать периферийный модуль для управления этими светодиодами.

Компонент UDB позволяет создавать достаточно сложные аппаратные модули. Данные модули можно проектировать несколькими разными способами: нарисовать схему, нарисовать блок-схему машины состояний, а также, описать с помощью языка высокого уровня Verilog. Я буду использовать язык Verilog, так как он позволяет воспользоваться всеми возможностями UDB.

На данном рисунке показана блок-схема микроконтроллера PSOC 5LP Family.

На рисунке виден блок Universal Digital Block Array, который связан через внутренние шины Digital Interconnect и System Bus со всеми компонентами микроконтроллера.

Для того чтобы начать разработку, необходимо скачать и установить PSOC Creator. Эта IDE абсолютно бесплатна, работает как с компилятором GCC, так и Keil.

После установки и запуска вы увидите стартовую страницу.

Нажимаем File->New->Project и попадаем в окно создания нового проекта.

В этом окне необходимо выбрать целевое устройство. Это может быть оценочная плата, например, CY8CKIT-059. Но чтобы эта плата появился в списке, необходимо скачать и установить соответствующий пакет.

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

Следует заметить, что микроконтроллеры PSOC 4 и PSOC 5LP могут работать в диапазоне напряжений питания от 1.7 до 5.5 вольт. Таким образом, мы сможем напрямую подключить микроконтроллер к светодиоду WS2812 без дополнительного буфера.

Выбираем оценочную плату CY8CKIT-059, жмем Next>. В следующем окне надо выбрать то, с чего мы начнем разработку. Это может быть один из примеров, пустой проект или один из предварительно сохраненных шаблонов. Выбираем пустой проект.

Жмем Next>, в следующем окне надо выбрать имя проекта, место его расположения и имя рабочего пространства.

Нажимаем кнопку Finish и попадаем на закладку схемы проекта.

На панели слева, двойным щелчком, можно открыть файл main.c. Мастер создания нового проекта уже наполнил минимально необходимым содержимым этот файл.

Нажав SHIFT+F6 можно скомпилировать проект, компиляция должна пройти без ошибок.

Для создания нового компонента на вкладке слева жмем закладку Components.

Затем щелкаем правой клавишей на строчке Project и выбираем пункт меню Add Component Item.

Первое, что нам нужно создать, это символ компонента. Поэтому выбираем пункт Symbol Wizard, прописываем имя компонента, Cypress рекомендует включать номер версии в имя компонента таким образом «Имя_компонента»_v1_0, цифры 1_0 в дальнейшем можно изменять в зависимости от версии компонента.

Нажимаем Create New и попадаем в мастер создания символа компонента, где создаем четыре вывода: вход тактового сигнала, асинхронного сброса, выход для управления светодиодами и выход сигнала прерывания или DMA, который будет устанавливаться в «1» при опустошении буфера FIFO. Выводимые данные мы будем записывать в оперативную память, поэтому на схеме нет выводов для входных данных.

Жмем OK и попадаем на вкладку с созданным символом. Щелкаем на пустом месте листа правой клавишей мыши и выбираем пункт PROPERTIES.

Здесь нам нужно ввести два имени Doc.ApiPrefix и Doc.DefaultInstanceName.

Жмем OK, затем снова щелкаем правой клавишей мыши на пустом месте листа символа и выбираем пункт меню Generate Verilog:

Тут просто нажимаем кнопку Generate.

Двойным щелчком открываем Verilog файл.

Затем выбираем пункт меню Tools->DataPath Config Tool.

В DataPath Config Tool открываем только что созданный Verilog файл.

Теперь нам нужно добавить DataPath в наш Verilog файл. Выбираем пункт меню Edit->New DataPath.

В этом окне нам нужно ввести имя Datapath и выбрать разрядность DataPath, 8 бит нам будет достаточно.

Мастер создал Datapath, заполненный значениями по умолчанию:

Область 1 — это 8 команд, которые может выполнять DataPath. Область 2 — это маски. Область 3 — настройка режима работы элементов Datapath.

Настал момент, когда нам надо разобраться с тем, как устроен и работает этот самый DataPath. UDB состоит: из блока тактовых сигналов, асинхронного сброса, регистра Status для чтения состояния и регистра Control для записи управляющих сигналов UDB.

Также в него входят две PLD матрицы на 12 входов и 4 выхода.

И сердцем UDB является DataPath. Можно сказать, что это очень примитивный микроконтроллер. У него есть: ALU, память программ из восьми ячеек, четыре регистра общего назначения, два буфера глубиной четыре байта организованных как FIFO, сдвиговый регистр, две маски, а также два блока сравнения и два блока проверки на равенство 0x00 и 0xFF регистров A0, A1. Следует также добавить, что эти блоки можно объединять и организовывать 16, 24, 32 битную обработку данных. Микроконтроллер может писать и читать, как в регистры A0,A1,D0,D1, так и в FIFO.

В datasheet к светодиоду WS2812 указаны следующие требования к временных характеристикам сигнала.

Старший бит посылается первым
Старший бит посылается первым

Теперь попробуем описать, что мы хотим от нашего модуля. Если FIFO пуст, модуль находится в режиме ожидания. Как только мы записываем первый байт в FIFO, наш модуль начинает формирование импульса сброса. После окончания импульса сброса, читает байт из FIFO и отправляет его. И так все четыре байта, до тех пор, пока FIFO не опустеет. Флаг пустого FIFO мы будем использовать для формирования сигнала прерывания или DMA. Если очередная порция данных будет записана до того, как закончится передача взятого из FIFO байта, то импульс сброса формироваться не будет.

Рисуем блок схему для нашего модуля состоящую из 8 состояний:

Выбираем длительность такта 200ns, такой интервал будет удовлетворять временным ограничениям указанным в datasheet WS2812B. Длительность импульса сброса в таком случае составит 255*0.2µs= 51µs. Заполним необходимые поля в DataPath Configuration Tool.

Первая команда — это состояние ожидания записи данных в FIFO. Одновременно мы обнуляем операцией Исключающее ИЛИ регистр A1. Функция выбрана XOR, источник A и B регистр A1, запись регистра A1 выбрана из ALU.

На вторую команду мы переходим по сигналу FIFO empty == 0. То есть в FIFO поступили данные, в этот же момент мы устанавливаем выходной сигнал в «0». Во второй команде мы увеличиваем на единицу A1 до тех пор, пока он не примет значение 0xFF. После этого переходим к команде три, устанавливая выходной сигнал в «1».

В команде три мы загружаем байт из FIFO в A0 и загружаем счетчик бит из D1 в A1, и переходим к команде четыре.

В команде четыре и пять DataPath ничего не делает. В команде шесть мы делаем декремент счетчику бит и переходим к команде семь, установив выход в «1». Надо понимать, что когда мы устанавливаем выходной сигнал или флаг, данные изменятся только в следующем такте.

В команде семь происходит ветвление по трем адресам. Если счетчик бит A1 != 0, то мы переходим к команде восемь, чтобы сформировать еще один такт выходного сигнала «1». Если A1 == 0 и FIFO пуст, переходим к первой команде ожидания поступления данных в FIFO. Если FIFO не пуст, то переходим к команде три, загрузка данных из FIFO в A0 и счетчика бит A1 из D1.

Нам также надо включить маску 0 и присвоить ей значение 0x80. При проверке на равенство регистра A0 с маской 0x80, регистру D0 равному 0x80 это даст значение передаваемого бита. FIFO у нас по умолчанию сконфигурированы на ввод данных в DataPath.

В завершении пропишем значения по умолчанию для регистров D0 (маска старшего бита) = 8'h80 и D1 (счетчик бит) = 0'h08, используя пункт меню View->Initial Register Values.

Сохраняем (Ctrl+S) и закрываем Datapath Configuration Tool. Нам осталось написать логику работы машины состояний в Verilog файле. Сначала присвоим сигналам модуля имена используемых нами регистров и флагов.

udb8(
        /*  input                   */  .reset(rst),//Входной сигнал сброса
        /*  input                   */  .clk(clk),//Входной тактовый сигнал
        /*  input   [02:00]         */  .cs_addr(state),//Регистр адреса команды DataPath
        /*  input                   */  .route_si(1'b0),
        /*  input                   */  .route_ci(1'b0),
        /*  input                   */  .f0_load(1'b0),
        /*  input                   */  .f1_load(1'b0),
        /*  input                   */  .d0_load(1'b0),
        /*  input                   */  .d1_load(1'b0),
        /*  output                  */  .ce0(send_bit),//Значение отправляемого бита
        /*  output                  */  .cl0(),
        /*  output                  */  .z0(),
        /*  output                  */  .ff0(),
        /*  output                  */  .ce1(),
        /*  output                  */  .cl1(),
        /*  output                  */  .z1(z_count_bit),//Флаг равенству 0 счетчика бит
        /*  output                  */  .ff1(end_ws2812_reset),//Флаг равенству 0xFF регистра A1
        /*  output                  */  .ov_msb(),
        /*  output                  */  .co_msb(),
        /*  output                  */  .cmsb(),
        /*  output                  */  .so(),
        /*  output                  */  .f0_bus_stat(),
        /*  output                  */  .f0_blk_stat(fifo_empty),//Флаг пустого FIFO
        /*  output                  */  .f1_bus_stat(),
        /*  output                  */  .f1_blk_stat()
);

Первые два сигнала — это вход асинхронного сброса и вход тактового сигнала.

Следующий это трех-битный вход адреса памяти команд DataPath. Мы присваиваем этому входу регистр состояний state. Далее идут выходные сигналы UDB: ce0 — это значение выводимого бита. При создании конфигурации DataPath мы включили маску 0 и присвоили ей значение 0x80, получается операция send_bit=(A0 & 0x80==D0) ? 1 : 0;

Флаг z1, проверяет на равенство 0 регистра A1, это у нас счетчик отправляемых бит, присваиваем ему сигнал z_count_bit.

Следующий флаг — ff1, он устанавливается в 1 при равенстве регистра A1 - 0xFF. Этот флаг мы используем при формировании импульса сброса для WS2812, присваиваем ему имя сигнала end_ws2812_reset.

И последний флаг — f0_blk_stat, он устанавливается в 1, когда FIFO 0 пуст. Присваиваем ему имя сигнала fifo_empty.

Нам осталось объявить используемые регистры и флаги, и прописать машину состояний.

    localparam IDLE = 3'h0;
    localparam WS2812_RESET = 3'h1;
    localparam LOAD_A0 = 3'h2;
    localparam CHECK_BIT = 3'h3;
    localparam SEND_BIT = 3'h4;
    localparam DEC_BIT_CNT = 3'h5;
    localparam SHIFT_DATA = 3'h6;
    localparam NOP = 3'h7;

    reg [2:0]state;//Адрес команды
    reg out;//Регистр выходного сигнала
    reg send_tic;//Регистр дополнительного такта
    wire fifo_empty;//Флаг пустого FIFO
    wire send_bit;//значение отправляемого бита
    wire end_ws2812_reset;//Флаг равенства 0xFF регистра A1
    wire z_count_bit;//Флаг равенства 0 счетчика бит
    assign irq=fifo_empty;//Присваиваем выходу прерывания флаг пустого FIFO
    assign ws2812=out;//Присваиваем выходной сигнал регистру выходного сигнала

	always @(posedge clk or posedge rst )//

	begin
		if (rst)
		begin       // Асинхронный сброс
			state <= IDLE;
            out<=1'b1;
		end
		else
		begin
			case (state)
				
			IDLE://Ожидание поступления данных в FIFO
			begin
				if(fifo_empty==1'b0)
				begin
					state <= WS2812_RESET;//Если данные в FIFO поступили, 
                    out<=1'b0;//переходим к команде формирования импульса сброса, выходной сигнал в 0
				end
				else
				begin
                    out<=1'b1;//Если данных в FIFO нет, выходной сигнал в 1
                end
			end
					
			WS2812_RESET://Формирование импульса сброса
			begin
                if(end_ws2812_reset)//Ждем равенства 0xFF регистра A1
                begin
                    state <= LOAD_A0;//Если A1=0xFF, переходим к команде загрузки данных из FIFO
                    out<=1'b1;//Выход в 1
                end
			end
			LOAD_A0://Загрузка байта из FIFO
			begin
                state <= CHECK_BIT;//Переходим к команде проверки значения выводимого бита
			end
			CHECK_BIT://Команда проверки значения выводимого бита
			begin
                send_tic <= 1'b0;//Обнуляем регистр дополнительного такта
                state <= SEND_BIT;//Переходим к команде отправки бита данных
                if(send_bit==1'b0)
                begin
                    out <= 1'b0;//Если выводимый бит 0, устанавливаем выход в 0
                end
			end
			SEND_BIT://Команда отправки бита данных
			begin
                if(send_tic)//Если дополнительный такт уже был
                begin
                    state <= DEC_BIT_CNT;//Переходим к команде декремента счетчика бит
                    out <= 1'b0;//Устанавливаем выходной сигнал в 0
                end
                else
                begin
                    send_tic <= 1'b1;//Если дополнительного такта не было, устанавливаем флаг дополнительного такта
                end
			end
			DEC_BIT_CNT://Команда декремента счетчика бит
			begin
                state <= SHIFT_DATA;//Переходим к команде сдвига выводимого байта влево
			end
			SHIFT_DATA://Команда сдвига выводимого байта влево
			begin
                out<=1'b1;//Выходной сигнал в 1
                if(z_count_bit)//Если счетчик выведенных бит равен 0
                begin
                    if(fifo_empty == 1'b0)//Если FIFO не пуст
                    begin
                        state <= LOAD_A0;//Переходим к загрузке нового байта
                    end
                    else
                    begin
                        state <= IDLE;//Если пуст переходим в режим ожидания прихода данных в FIFO
                    end
                end
                else
                begin
                    state <= NOP;//Если счетчик бит не равен 0, переходим к формированию дополнительного такта
                end
			end
			NOP://Команда дополнительного такта
			begin
                state <= CHECK_BIT;//Переходим к команде проверки значения выводимого бита
			end
		endcase
		end
	end

«Железную» часть мы закончили, осталось написать небольшое API, чтобы микроконтроллер мог взаимодействовать с нашим модулем. Сохраняем и закрываем Vetilog файл.

Создадим заголовочный файл, щелкаем правой клавишей мыши на имени нашего компонента и выбираем Add Component Item.

Ищем API Header File, вписываем имя файла и нажимаем Create.

Двойным щелчком по имени файла на панели слева открываем файл и добавляем в него следующие строки.

#include "cytypes.h"

#define `$INSTANCE_NAME`_SHIFT_MASK 0x80
#define `$INSTANCE_NAME`_NUM_SHIFT_BITS 8
#define `$INSTANCE_NAME`_FIFO_LEVELS 4
#define `$INSTANCE_NAME`_bit_cnt (*(reg8 *)`$INSTANCE_NAME`_udb8_u0__D1_REG)
#define `$INSTANCE_NAME`_shift_mask (*(reg8 *)`$INSTANCE_NAME`_udb8_u0__D0_REG)
#define `$INSTANCE_NAME`_data_fifo (*(reg8 *)`$INSTANCE_NAME`_udb8_u0__F0_REG)
#define `$INSTANCE_NAME`_actl (*(reg8 *)`$INSTANCE_NAME`_udb8_u0__DP_AUX_CTL_REG)

void `$INSTANCE_NAME`_Start(void);

Здесь следует разъяснить, что такое $INSTANCE_NAME. Это то, что мы вводили в свойство Doc.DefaultINstanceName при создании символа компонента. В нашем случае, будет автоматически формироваться имя ws2812_1_bit_cnt, где 1 после ws2812 — это автоматическая нумерация компонентов на схеме. И да, на схеме это имя можно поменять на любое другое. Хитрую кавычку можно ввести так: <ALT>+<96>. Имя регистра, генерируемого при компиляции, состоит из трёх частей, $INSTANCE_NAME — это имя компонента, udb8 — это имя DataPath, которое мы указали при создании нового DataPath и u0__D1_REG — это имя регистра. В случае ошибки, можно посмотреть эти имена в файле cyfitter.h после компиляции.

Функцию Start можно было не создавать, так как после сброса микроконтроллера в регистры D0 и D1, загружаются значения по умолчанию. Также мы сможем в программе, в любой момент, записать необходимые данные в регистр простой записью:

ws2812_1_bit_cnt=8;

Запись байта в FIFO выглядит так:

ws2812_1_data_fifo=0xAA;

Но мы сделаем это с заделом на будущее. Снова щелкаем правой клавишей мыши на имени компонента и выбираем Add Component Item. Затем выбираем API C File, вводим имя и нажимаем кнопку Create.

Двойным щелчком на имени файла, на панели слева открываем созданный файл и вводим в него следующий текст.

#include "`$INSTANCE_NAME`.h"

void `$INSTANCE_NAME`_Start(void)
{
    `$INSTANCE_NAME`_shift_mask=`$INSTANCE_NAME`_SHIFT_MASK;
    `$INSTANCE_NAME`_bit_cnt=`$INSTANCE_NAME`_NUM_SHIFT_BITS;
}

Нажимаем Ctrl+Shift+S, чтобы сохранить все изменения. Теперь мы можем добавить созданный компонент на схему проекта. Переключаемся на вкладку Source слева.

Двойным щелчком по файлу TopDesign.cysh открываем схему проекта, справа мы видим набор поставляемых с Psoc Creator компонентов.

Справа щелкаем на вкладку Default, и видим только что созданный нами компонент.

Перетаскиваем его на схему.

Щелкаем справа на вкладку Cypress, затем выбираем в папке Systems компонент Clock и перетаскиваем его на схему так, чтобы квадратик вывода clk компонента ws2812_1 совпал с квадратиком вывода компонента Clock_1, тогда выводы соединяться.

Щелкаем правой клавишей на компоненте Clock_1 и в выпавшем меню выбираем пункт Configure. В этом же меню можно выбрать пункт Open Datasheet и посмотреть datasheet на этот компонент.

Вписываем частоту 5MHz.

Затем в папке Digital библиотеки компонентов берем компонент Ligic Low и перетаскиваем его на схему, и размещаем рядом с выводом rst компонента ws2812_1. Затем нажимаем клавишу «W» и соединяем вывод rst и Logic Low проводом.

Из папки Ports and Pins берем компонент Digital Output Pin и подсоединяем его к выводу ws2812 нашего компонента, затем щелкаем правой клавишей по компоненту Pin_1, выбираем пункт меню Configure и изменяем имя компонента с Pin_1 на ws2812_port.

Присвоим сигнал ws2812_port порту P1(7), для этого, двойным щелчком по строчке Pins в папке Design Wide Resourse, открываем вкладку назначения выводов и на панели справа, выбираем Port - P1(7).

На вкладке Clocks, двойным щелчком на строчке IMO, открываем страничку конфигурации тактовых частот.

И устанавливаем частоту IMO - 3MHz, так как при такой частоте точность составляет ±1%. Частоту PLL ставим 79MHz. На 80MHz IDE будет ругаться, так как с учетом отклонений, частота будет превышать предельно допустимые 80MHz для данного микроконтроллера.

Так как пост уже затянулся, автоматическую загрузку FIFO компонента через DMA я делать не буду, будем отправлять данные по прерыванию. Следовательно из папки System добавляем на схему компонент Interrupt.

Вызываем меню Configure для компонента isr_1, и даем ему имя isr_ws2812.

Нам еще понадобится компонент Timer, возьмём его в папке Digital->Function. К выводу interrupt подключим еще один компонент Interrupt, который переименуем в isr_timer. Также удалим у компонента Timer тактовый генератор и подключим тактовый вход таймера к компоненту Clock_1, переместив предварительно компонент Clock_1 чуть влево.

Теперь вызовем меню Configure для компонента Timer_1, выберем 16 битный режим, поставим период 14999 для прерывания каждые 3ms и поставим галочку напротив прерывания по переполнению. Почему я выбрал период обновления 3ms? Пересылка одного байта занимает 8*1,2µs=9,6µs всего 100 светодиодов по 3 байта на точку, получаем 2880µs+51µs на импульс сброса. Получаем 2931µs, следовательно, 3ms нам вполне достаточно.

Как видно, компонент таймер может быть создан на базе аппаратного таймера или синтезирован на базе UDB блоков, мы оставим Fixed Function реализацию.

И нам осталось написать небольшой «Hello Habr», кстати, нажав Shift +F6 можно скомпилировать проект, компиляция должна завершиться без ошибок.

Демонстрационная программа формирует бегущую строку на экране 10 на 10 светодиодов, подключенных последовательно Z способом снизу вверх, слева направо (со стороны проводов).

Кадры формируются по сигналу прерывания модуля Timer_1, картинка рисуется в предварительном буфере и по флагу окончания передачи данные копируются в основной буфер. Для увеличения скорости, копирование идет 32 битными словами.

int main(void)
#include "project.h"
#include "font.h"
#include "color.h"

#define LED_WIDTH 10
#define LED_HIGH 10
#define TIMER_ISR_TC_MASK 0x8

struct {
    uint8 buffer[LED_WIDTH*LED_HIGH*3];
    uint8 draw_buffer[LED_WIDTH*LED_HIGH*3];
    volatile uint8 *buffer_ptr;
    volatile uint32 wait_tx :1;
} ws2812_struct;

void writePixelRGB(uint8 x, uint8 y, uint32 color);
void fillScreen(uint32 color);
void copyBuffer(uint32 *src, uint32 *dst, uint32 num);
void writeChar(uint8 column_display, uint8 column_char, uint8 id_char, uint32 color, uint32 background_color);
void scrollStr(uint8 *str, uint16 len, uint32 *char_color, uint32 background_color);

CY_ISR_PROTO(WS2812_HANDLER);
CY_ISR_PROTO(TIMER333HZ_HANDLER);

CY_ISR(TIMER333HZ_HANDLER)
{
    if(Timer_1_STATUS & TIMER_ISR_TC_MASK)
    {
        isr_ws2812_Enable();
        ws2812_struct.wait_tx=0;
    }
        
}
CY_ISR(WS2812_HANDLER)
{
    for(int i=0; i<ws2812_1_FIFO_LEVELS;i++)
    {
        ws2812_1_data_fifo=*ws2812_struct.buffer_ptr;
        if(ws2812_struct.buffer_ptr==&ws2812_struct.buffer[sizeof(ws2812_struct.buffer)-1])
        {
            isr_ws2812_Disable();
            ws2812_struct.wait_tx=1;
            ws2812_struct.buffer_ptr=ws2812_struct.buffer;
            break;
        }
        else
        {
            ws2812_struct.buffer_ptr++;
        }
    }
}

int main(void)
{
    ws2812_struct.buffer_ptr=ws2812_struct.buffer;
    fillScreen(0);
    CyGlobalIntEnable; /* Enable global interrupts. */
    Timer_1_Start();
    ws2812_1_Start();
    isr_ws2812_StartEx(WS2812_HANDLER);
    isr_timer_StartEx(TIMER333HZ_HANDLER);
    
    uint16 delay=0;
    for(;;)
    {
        if((delay++)==20)
        {
            delay=0;
            scrollStr(text, sizeof(text), color , BACKGROUND);
        }
        while(ws2812_struct.wait_tx==0);
        ws2812_struct.wait_tx=0;
        copyBuffer((uint32*)ws2812_struct.draw_buffer,(uint32*)ws2812_struct.buffer,sizeof(ws2812_struct.buffer)/4);            
    }
}
void scrollStr(uint8 *str, uint16 len, uint32 *char_color, uint32 background_color)
{
    static uint16 begin_position=LED_WIDTH-1;
    static uint16 index_position=0;
    static uint16 point_position=0;
    uint16 char_point=begin_position;
    uint16 i=0;
    while(char_point<LED_WIDTH)
    {
        uint8 idx_char=index_position+i;
        if(idx_char==len)
            idx_char=0;
        writeChar(char_point,\
            i==0 ? point_position : 0,\
            str[idx_char],\
            char_color[idx_char],\
            background_color);
        char_point += i==0? FONT_WIDTH-point_position : FONT_WIDTH;
        i++;
    }
    if(begin_position!=0)
        begin_position--;
    else
    {
        if(point_position==(FONT_WIDTH-1))
        {
            point_position=0;
            if(index_position==len-1)
                index_position=0;
            else
                index_position++;
        }
        else
            point_position++;
    }
}
void copyBuffer(uint32 *src, uint32 *dst, uint32 num)
{
    for(uint32 i=0;i<num;i++)
        *(dst++)=*(src++);
}
void writeChar(uint8 column_display, uint8 column_char, uint8 id_char, uint32 char_color, uint32 background_color)
{
    uint8 shift;
    for(int8 j=LED_HIGH-1;j>=0;j--)
    {
        shift=0x80>>column_char;
        for(uint8 i=column_display;i<(column_display+FONT_WIDTH) && i<LED_WIDTH;i++)
        {
            if(font[id_char][j]&shift)
                writePixelRGB(i,FONT_HIGH-j-1,char_color);
            else
                writePixelRGB(i,FONT_HIGH-j-1,background_color);
            shift >>=1;
        }
    }
}
void writePixelRGB(uint8 x, uint8 y, uint32 color)
{
    uint16 point=(LED_HIGH*(LED_WIDTH-x-1)+y)*3;
    ws2812_struct.draw_buffer[point++]=(uint8)(color>>16);
    ws2812_struct.draw_buffer[point++]=(uint8)(color>>8);
    ws2812_struct.draw_buffer[point]=(uint8)color;
}
void fillScreen(uint32 color)
{
    for(uint16 i=0;i<LED_WIDTH*LED_HIGH*3;i+=3)
    {
        ws2812_struct.draw_buffer[i]=(uint8)(color>>16);
        ws2812_struct.draw_buffer[i+1]=(uint8)(color>>8);
        ws2812_struct.draw_buffer[i+2]=(uint8)color;
    }
}

На вкладке измерителя ресурсов видно, что мы израсходовали 4,4% ресурсов UDB. Это означает, что в данном микроконтроллере мы сможем разместить 19-20 модулей ws2812. В микроконтроллере серии PSOC 4200, например, CY8C4245PVI-482 в котором всего четыре UDB блока, удастся разместить 2-3 таких модуля.

И в заключении — осциллограммы снятые на работающем макете и небольшое видео.

Импульс Сброса
Импульс Сброса
Передача «1» и «0»
Передача «1» и «0»

Проект размещен на Github

Реклама
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее

Комментарии 31

    0
    На данном рисунке показана блок-схема микроконтроллера PSOC 5LP Family

    В приведенном линке 4 серия, а не 5.
    Вообще прикольно конечно, но на STM32 SPI например (это точно 20 Mbit/s получается) все это, несколько строчек кода. И за треть цены.
      0

      atmega4809 и др. тоже имеет FPGA часть (Configurable Custom Logic).

        0
        Я скорее к тому, что для данного протокола FPGA не нужен, достаточно SPI.
        Впрочем статья неплохая, как пример (template) проекта. На этих блоках ,USB контроллер например реализовать, что нибудь посложнее чем в статье.
          0
          Когда надо будет управлять 20 000 SW2812B, да еще одновременно просчитывать кадр, то SPI может не хватить. Но это, как всегда, разговор ни о чем, «идеальных» решений не существует, кому-то хватит SPI, кому-то понадобиться что-то помощнее.
            0
            На 20 тысяч вам никакого контроллера не хватит. Посчитайте сколько нужно параллельных каналов для приличного FPS.И указанного в статье Вам тоже точно не хватит. На 20 тысяч led используют совсем другие решения. Вот для матрицы 64х32х2
            github.com/sdima1357/stm32f407vg_RGB64x32x2_LED_PANEL
            Если ещё больше, то fpga конечно.
              0
              Я считал, надо 20 модулей описанных в статье, и они уместятся в этом микроконтроллере. Я думаю, если разрабатывать сразу для управления таким количеством, то можно уместить и больше, экономя на общих ресурсах для всех модулей.
                0
                20 модулей по 64 led это совсем не 20000 led. Вам надо вот это на 20000 www.aliexpress.com/item/32331955683.html
                  0
                  20 модулей, описанных в статье, могут управлять 1024*20=20 480 светодиодов.
                    –1
                    Думаю что у Вас заметно дороже выйдет, чем готовая матрица. Их еще кормить данными нужно. Откуда то.
                      +1
                      Повторюсь, я не претендую на «идеальное» решение. Доводами, которые я привожу, я всего лишь хочу показать, что требования бывают разные и те решения, которые предлагаете вы, могут не подойти.
                        –1
                        Так Вы же спецификацию не приводили. А то что было в примере — можно реализовать и проще и дешевле. Или приведите целевую спецификацию. И практически гарантировано, что Infinia PSOC + WS2812 — не самое оптимальное решение.
                          0
                          Это туториал, я всего лишь упомянул возможность легкого расширения как преимущество данного решения.
          0
          На сколько я смог вникнуть, в этом решении не все так просто как кажется, так как нет синхронизации между таймером и SPI. И перед отправкой данных надо перезапускать таймер. И на счёт формирования импульса сброса ничего не написано.
            0
            Вам прям код накидать? На 20 MHz
            Сброс — 25 нулевых бит.
            0 это 8 единиц 17 нулей
            1 это 17 единиц 8 нулей

            Запускаем circular DMA с double buffer. В прерывании рисуем следующую часть
              +1
              Мы в ветке про Atmega4809.
                0
                25 нулевых бит при 20МГц — это вроде как бы 1,25мкс. Импульс сброса — 50мкс.
                  0
                  Да, действительно не обратил внимания. Резет длиннее. Но это не усложняет код. Он просто длиннее.
                    0
                    Получается в вашем варианте вы вынуждены минимум в 25 раз увеличить фреймбуфер, проводить дополнительную обработку данных, чтобы преобразовать их из RGB в SPI последовательность. Плюс шину будете занимать в 25 раз дольше при DMA.
                      0
                      Ну так пример то был на видео на 81 led. А то в примере одно, в голове другое.
                        0
                        В видео 100 светодиодов и это просто демо.
          0
          Классная штука. Жаль только нет консольных синтезаторов логики. Ставить винду и этот комбаин для того, чтобы решить проблему сложного АЦП конечно можно. Но пока что проще поставить второй МК из дешевых на платку и написать блокирующий код для него. Думаю, что решить задачу 20 линий в одном цикле ручного ногодрыга всеми лапками одновременно вполне получится. И цена суммарно за два арма выйдет дешевле, чем за один кипарис.
          Вот если такой блок стандартизируют и появится конкуренция, я был бы счастлив.
            0
            По моему мнению, решить задачу ногодрыга на 20 линий по 1000 светодиодов это из разряда высшего искусства и простотой тут совсем не пахнет.
              +1
              1) создаём таблицу состояний пинов
              uint32_t bits[someMemSize]
              2) заполняем оную как надо
              3)
              uint16_t step = 0;
              while(1)
              {
               GPIOA->ODR =  bits[step++];
              if(step ==someMemSize) step = 0;
              updateBits();
              delay();
              }

              Если updateBits() знимает заметную часть времени, то перед ним мы запомним состояние таймера, а в delay вносим корректировку.
              Можно ещё краше — биты править по DMA, который дёргается таймером, а в цикле только пресчитывать состояния. Но тогда не забываем про двойную буферизацию: правим один буфер, а в DMA другой.
              Это сожрёт всю память МК, но нам не страшно — этот кристалл работает только на ногодрыг.
              Вот если бы мне надо было бы по каждой линии принимать решение по результатам ответа на этой линии, было бы больно. Но тут и вентилей в кипарисе может не хватить.
              Кстати! Тут придумал, что нам даже полноразмерный GPIO не нужен. Хватит паровозика сдвиговых регистров с защёлкой (74HC595) и SPI по DMA. Защёлкой рулить с таймера. А в цикле МК только пересчитывать буферы.
                0

                В теории красиво, на практике не забудьте, что эти данные вам надо обновлять, в результате будут плавать временные интервалы. А на 74hc 595 реализация потребует тактовой частоты spi 100mhz, это мало какой контроллер потянет, плюс данные вам нужно сформировать прежде чем отправить в регистр. Может это все и заработает, но это будет очень сложно реализовать, что уже граничит с искусством.

                  0
                  Недооценил масштаб для регистров, да.
                  Но ногодрыг по DMA всё ещё справляется. Правда, нам надо заливать в полученную «видеокарту» 20000*24 бит в 30мс, или меньше 16 мегабит в секунду. Если не использовать кодек, задача сама по себе сложная.
                  Но освоение нового МК, да ещё и с новым IDE и новым языком всё равно выглядит сложнее. Повторюсь, был бы этот блок стандартизирован эти потери времени были бы оправданы. Но только ради WS2812 использовать сложный и закрытый фреймворк не оправдвно.
                    0
                    Странно вы как-то считаете, за 0.2 мкс (чтобы корректно сформировать бит, надо этот бит, длительностью 1.2 мкс, разбить на 6 частей) вам надо заполнить 20 битный регистр, таким образом длительность тактового импульса для 74HC595 будет равняться 0.2 мкс / 20 = 0.01 мкс, что соответствует тактовой частоте 100 МГц.
                      0
                      На 4 (+0, +0,4, +0,85, +12,5). Но всё равно 100MHz. Я считал для прямого ногодрыга, про регистры признал неправоту сразу.
                        0

                        На STM32 DMA цикл записи в порт из памяти занимает 10 тактов системной шины: DMA мифы и реальность. Так что можно прикинуть, какая должна быть тактовая частота у микроконтроллера. На дешёвых STM32, точно не взлетит.

            0
            Возможно статья была бы информативной, но увы, оценить сложно — картинки мыло. Там же можно сделать кликабельные иллюстрации с нормальным разрешением, вставив ссылки поверх изображений
              0
              Полностью с Вами согласен. В Рекомендациях по оформлению постов не сказано как это сделать. Вариант
              <a href="/BigPic"><img src="PreView"/></a>
              не сработал. Буду очень благодарен Вам, если дадите ссылку на то как это сделать.
                +1
                Это всегда пожалуйста:
                • Cначала вставляем картинку с мелким разрешением, но читаемым, чтобы статья не грузилась очень долго
                • Выделяем код вставки картинки и жмем вставить обычную ссылку
                • В ссылку пихаем адрес до полноразмерной картинки



                Тестовая картинка кликабельна:



                Надеюсь понятно объяснил, если вдруг остались вопросы, то пишите в личку, чтобы комментарии под статьей не засорят и не бесить читателей))

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое