Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Светодиоды типа 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;
Но мы сделаем это с заделом на будущее. Снова щелкаем правой клавишей мыши на имени компонента и выбираем 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)
{
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);
}
}
В заключении приведу screenshot измерителя ресурсов для данного проекта:
Мы израсходовали 4,4% ресурсов UDB. Это означает, что в данном микроконтроллере мы сможем разместить около 20 модулей ws2812. В микроконтроллерах PSOC 4200, например, CY8C4245PVI-482 скорей всего удастся поместить три таких модуля.
И в заключении — осциллограммы снятые на работающем макете и небольшое видео.
Проект размещен на Github.