Нам предстоит сделать предпоследний шаг в практическом освоении работы с UDB. Сегодня мы будем вести разработку не при помощи автоматизированного UDB Editor, а в полуручном режиме, с использованием Datapath Config Tool. Очень хорошим подспорьем в освоении этого инструмента является документ AN82156 — PSoC 3, PSoC 4, and PSoC 5LP – Designing PSoC Creator Components with UDB Datapaths. Собственно, я сам учился по нему.
Возможно, при прочтении наших переводов документации на UDB, кто-то пробовал воспроизвести знания оттуда на практике и заметил, что в UDB Editor доступна не вся функциональность, описанная в публикациях. Это связано с тем, что разработчики не стали помещать в UDB Editor некоторые особо громоздкие механизмы. Авторы AN82156 утверждают, что через UDB Editor не получится сделать следующие вещи:
- организовать параллельный ввод и вывод данных;
- организовать динамическое управление FIFO;
- реализовать инверсию тактового сигнала FIFO;
- реализовать функцию CRC;
- реализовать функцию PRS;
- реализовать выбор входящего переноса;
- реализовать динамический входящий перенос.
От себя я добавлю, что не нашёл, как реализовать в UDB Editor перестановку нибблов.
Если эти функции необходимы в проекте, потребуется создать собственный код на языке Verilog. Я специально использовал слово «создать», а не «написать». Знать этот язык программирования достаточно на уровне чтения. В смысле, надо понимать, какая конструкция для чего нужна. А уметь писать с нуля — всегда полезно, но для изложенного в этой статье данный навык не является обязательным.
В качестве решаемой задачи, я выбрал полусинтетический случай. В общем и целом, я решил вывести какие-нибудь данные в параллельный порт, а в частности, из того, что лежит под рукой, параллельный порт есть у текстового ЖК-дисплея. Я вытащил его три года назад из 3D-принтера MZ3D, когда пересаживал последнего на STM32. Поэтому случай и полусинтетический: сегодня такие индикаторы обычно имеют вход I2C, и подключение через ворох проводов в реальной жизни им не нужно. Однако параллельные порты у современных ЖКД тоже имеются, так что использовать их для повторения эксперимента сможет каждый.
Рассмотрим схему включения дисплея, взятую с сайта reprap.org (это было нелегко, мой провайдер блокирует данный сайт, как и ряд других технических, мотивируя это тем, что тот живёт на одном IP с кем-то заблокированным).
Прекрасная схема! Во-первых, мне не надо думать насчёт чтения: данные в ЖКД могут только писаться (линия R/W заземлена и недоступна на разъёме). Во-вторых, данные идут в 4х-битном формате, значит, мы сможем не только отработать параллельный вывод, но и проверить работу функции перестановки нибблов.
Создание проекта
Итак, запускаем PSoC Creator и выбираем File->New->Project:
Дальше я выбираю свою макетную плату:
Далее — пустая схема:
Проект назову LCDTest2:
Теперь, как и раньше, идём на вкладку Components:
И, выбрав проект, нажимаем правую кнопку «мыши», после чего выбираем Add Component Item.
И вот здесь надо выбрать Symbol Wizard. Имя дадим… Ну, скажем, LCD4bit.
Я назначил символу следующие порты:
clk — это вход тактовой частоты. Порты с префиксом LCD — это стандартные порты ЖК-дисплея. hungry – выходы, сообщающие блоку DMA, что в FIFO имеется свободное место, идея рассматривалась в статье про управление RGB светодиодами. Нажимаем OK, получаем символ.
Теперь на основе этого символа следует сгенерить Verilog шаблон. Нажимаем в окрестностях символа правую кнопку «мыши» и в контекстном меню выбираем Generate Verilog.
У нас получился шаблон, показанный на рисунке ниже (в текстовом виде он пока не имеет никакого смысла):
Нам создали модуль и какие-то участки. Но пока что не создали Datapath. Чтобы добавить его, идём в дерево проекта, выбираем файл LCD4bit.v, нажимаем правую кнопку «мыши» и в появившемся контекстном меню выбираем Datapath Config Tool:
Перед нами открывается окно, которое я пока покажу лишь частично:
Прошу любить и жаловать, редактор Datapath. В нём имеются все биты, которые описывались в переводе фирменной документации. Но этих битов так много, что в первые дни я на него смотрел, но боялся что-либо предпринять. Посмотрю-посмотрю и выйду. И только через некоторое время, привыкнув, начал пытаться что-то делать. Собственно, поэтому я и привёл только часть окна. Зачем пугать всех раньше времени? Пока же нам надо просто создать Datapath, поэтому мы выбираем пункт меню Edit->New Datapath:
Какой вариант выбрать в появившемся диалоге?
Вопрос чуть более серьёзен, чем кажется. Давайте я даже следующий абзац выделю, чтобы никто не попадался (я сам попался, а потом видел вопросы в сети от попавшихся, причём им никто не отвечал толком, а ответ есть в AN82156, надо только читать не по диагонали, так как там это написано короткой незаметной фразой).
Если планируется работать с параллельными данными, то выбирать надо обязательно вариант CY_PSOC3_DP. Ни один другой вариант не будет содержать портов для подключения параллельных данных.
Итак. Пусть экземпляр у нас зовут LCD_DP:
Нажимаем OK и пока что закрываем Datapath Config Tool, согласившись на сохранение результата. Мы вернёмся сюда позже.
Наш Verilog код расширился. Теперь в нём есть Datapath. Начало у него совершенно нечитаемое. Это не страшно, его настраивает Datapath Config Tool.
А конец описания Datapath будем править мы. Наш участок выглядит так
)) LCD_DP( /* input */ .reset(1'b0), /* input */ .clk(1'b0), /* input [02:00] */ .cs_addr(3'b0), /* 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(), /* output */ .cl0(), /* output */ .z0(), /* output */ .ff0(), /* output */ .ce1(), /* output */ .cl1(), /* output */ .z1(), /* output */ .ff1(), /* output */ .ov_msb(), /* output */ .co_msb(), /* output */ .cmsb(), /* output */ .so(), /* output */ .f0_bus_stat(), /* output */ .f0_blk_stat(), /* output */ .f1_bus_stat(), /* output */ .f1_blk_stat(), /* input */ .ci(1'b0), // Carry in from previous stage /* output */ .co(), // Carry out to next stage /* input */ .sir(1'b0), // Shift in from right side /* output */ .sor(), // Shift out to right side /* input */ .sil(1'b0), // Shift in from left side /* output */ .sol(), // Shift out to left side /* input */ .msbi(1'b0), // MSB chain in /* output */ .msbo(), // MSB chain out /* input [01:00] */ .cei(2'b0), // Compare equal in from prev stage /* output [01:00] */ .ceo(), // Compare equal out to next stage /* input [01:00] */ .cli(2'b0), // Compare less than in from prv stage /* output [01:00] */ .clo(), // Compare less than out to next stage /* input [01:00] */ .zi(2'b0), // Zero detect in from previous stage /* output [01:00] */ .zo(), // Zero detect out to next stage /* input [01:00] */ .fi(2'b0), // 0xFF detect in from previous stage /* output [01:00] */ .fo(), // 0xFF detect out to next stage /* input [01:00] */ .capi(2'b0), // Software capture from previous stage /* output [01:00] */ .capo(), // Software capture to next stage /* input */ .cfbi(1'b0), // CRC Feedback in from previous stage /* output */ .cfbo(), // CRC Feedback out to next stage /* input [07:00] */ .pi(8'b0), // Parallel data port /* output [07:00] */ .po() // Parallel data port );
Страшно? Сейчас разберёмся, что к чему — перестанет быть страшно. На самом деле, в этом тексте имеется три чётко выраженных группы. Давайте вспоминать перевод документации. Как выглядел Datapath на рисунке? Я сразу отмечу на рисунке места, к которым относятся группы «1», «2» и «3».
Собственно, первая группа портов в verilog коде — это входы. Сравните имена на выходе мультиплексора входов («1» на рисунке) и имена сигналов в коде.
Сейчас все входы занулены. Мы должны будем подключить тактовый вход и сможем пробросить до шести входных линий, как это делали в UDB Editor. Вот эти входы:
/* input */ .reset(1'b0), /* input */ .clk(1'b0), /* input [02:00] */ .cs_addr(3'b0), /* 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),
Вторая группа — выходы. Имена в коде также совпадают с именами входов выходного мультиплексора «2»:
/* output */ .ce0(), /* output */ .cl0(), /* output */ .z0(), /* output */ .ff0(), /* output */ .ce1(), /* output */ .cl1(), /* output */ .z1(), /* output */ .ff1(), /* output */ .ov_msb(), /* output */ .co_msb(), /* output */ .cmsb(), /* output */ .so(), /* output */ .f0_bus_stat(), /* output */ .f0_blk_stat(), /* output */ .f1_bus_stat(), /* output */ .f1_blk_stat(),
Третья группа есть только у данного вида Datapath (у остальных она отсутствует, поэтому отсутствуют и параллельные данные). Это внутренние сигналы Datapath, через которые можно самостоятельно производить объединение в цепочки либо иные полезные действия. Имена в коде также совпадают с именами внутренних сигналов, разбросанных по рисунку. Мы через один из них (последний в списке, его зовут po) будем выводить параллельные данные напрямую на ножки микросхемы.
/* input */ .ci(1'b0), // Carry in from previous stage /* output */ .co(), // Carry out to next stage /* input */ .sir(1'b0), // Shift in from right side /* output */ .sor(), // Shift out to right side /* input */ .sil(1'b0), // Shift in from left side /* output */ .sol(), // Shift out to left side /* input */ .msbi(1'b0), // MSB chain in /* output */ .msbo(), // MSB chain out /* input [01:00] */ .cei(2'b0), // Compare equal in from prev stage /* output [01:00] */ .ceo(), // Compare equal out to next stage /* input [01:00] */ .cli(2'b0), // Compare less than in from prv stage /* output [01:00] */ .clo(), // Compare less than out to next stage /* input [01:00] */ .zi(2'b0), // Zero detect in from previous stage /* output [01:00] */ .zo(), // Zero detect out to next stage /* input [01:00] */ .fi(2'b0), // 0xFF detect in from previous stage /* output [01:00] */ .fo(), // 0xFF detect out to next stage /* input [01:00] */ .capi(2'b0), // Software capture from previous stage /* output [01:00] */ .capo(), // Software capture to next stage /* input */ .cfbi(1'b0), // CRC Feedback in from previous stage /* output */ .cfbo(), // CRC Feedback out to next stage /* input [07:00] */ .pi(8'b0), // Parallel data port /* output [07:00] */ .po() // Parallel data port );
Итак. По мере работы нам придётся подключить часть этих входов и выходов к собственным сущностям, а остальные — просто оставить в том виде, в каком нам их создали.
Использование UDB Editor как справочника
И вот у нас получилась заготовка, мы знаем, куда и что нам предстоит вписать. Осталось понять, что именно мы туда будем вписывать. Так получилось, что я пользуюсь языком Verilog не каждый день, поэтому в общих чертах всё помню, а писать с нуля для меня это всегда является стрессовой ситуацией. Когда проект уже идёт — оно всё вспоминается, но если через пару месяцев простоя начать что-то с чистого листа, разумеется, я уже не помню деталей синтаксиса данного конкретного языка. Поэтому я предлагаю попросить среду разработки помочь нам.
UDB Editor для самоконтроля строит Verilog код. Воспользуемся тем фактом, что компоненты, которые не задействованы в основной схеме, не компилируются, поэтому мы вполне можем создать вспомогательный компонент в UDB Editor, и он не будет попадать в выходной код. Мы нарисуем там автомат, мы произведём черновую настройку входов и выходов Datapath, а потом просто перенесём автоматически созданный текст в наш verilog модуль и творчески всё доработаем. Это же гораздо проще, чем вспоминать детали синтаксиса Verilog и писать всё с нуля (хотя, кто постоянно пользуется Verilog-ом, тому, разумеется, будет проще написать именно с нуля: творческая доработка, как мы скоро увидим, хоть и проста, но требует времени).
Итак, начинаем делать вспомогательный компонент. Привычным движением руки добавляем новый элемент в проект:
Это будет документ UDB, назовём его UDBhelper:
Пора подумать об автомате, который мы разместим на созданном листе. Для этого надо рассмотреть, какую временную диаграмму мы должны сформировать с его помощью:
Итак. Сначала следует установить сигнал RS (так как R/W припаян к нулю аппаратно). Далее следует выждать tAS, после чего поднять сигнал E и установить данные (установка данных относительно положительного фронта E не лимитирована). Данные должны находиться на шине не менее, чем tDSW, после чего следует уронить сигнал E. Данные должны оставаться на шине ещё как минимум tDHW, а RS — как минимум tAH.
RS — это флаг «команда или данные». Если RS равен нулю, то пишется команда, если единице — данные.
Предлагаю через FIFO0 посылать команды, а через FIFO1 — данные. В рамках текущей задачи это ничему не противоречит. Тогда предложенный мною конечный автомат будет иметь следующий вид:
В состоянии Idle автомат находится, пока ни в одном из FIFO нет данных. Если появились данные в FIFO0, он идёт в состояние LoadF0, где в будущем будет принимать данные из FIFO0 в A0.
Пока передаются команды, данные слать не стоит. Поэтому условие приёма данных будет ниже по приоритету, чем условие приёма команд.
Данные принимаются в A1 в состоянии LoadF1 (из FIFO1 они могут попасть только в регистр A1 и не могут попасть в регистр A0), а затем копируются из A1 в A0 в состоянии A1toA0.
Каким бы путём мы ни шли к месту схождения стрелок, у нас есть данные в A0. Они уже выведены в параллельный порт. Взводим E (в состоянии E_UP1), роняем E (в состоянии E_DOWN1). Далее у нас будет состояние для обмена нибблов (SWAP), после чего E снова поднимается (E_UP2). На этом я исчерпал восемь состояний, которые можно закодировать тремя битами. А мы помним, что у ОЗУ динамической конфигурации Datapath всего три адресных входа. Можно было бы применить некоторые хитрости, но статья и так получается большая. Поэтому просто второй раз E будем ронять в состоянии Idle. Тогда нам вполне хватит восьми состояний.
На лист также положим Datapath и назначим его входы и выходы уже привычным по предыдущим статьям способом. Вот входы:
Вот выходы:
Ничего нового, всё уже описывалось в предыдущих статьях цикла. Итак, у нас получилась заготовка, на основе которой мы сможем сделать что-то своё. Правда, чтобы убедиться, что всё собирается, нам надо вывести нашу систему на верхний уровень проекта, иначе никакие ошибки не будут находиться. А при первичных опытах без ошибок не получится. Поэтому сделаем ещё одно вспомогательное действие.
Описание, как делается схема, выходит за рамки описания работы с UDB. Я просто покажу, какая схема получилась у меня. Блок DMA только один: при посылке команд в ЖКД надо выдерживать большие паузы, поэтому всё равно это проще делать программно. Для других применений просто можно поставить второй блок DMA по аналогии, задействовав сигнал hungry0.
Чтобы точно уложиться во временные рамки, я выбрал тактовую частоту, равную одному мегагерцу. Можно было бы взять частоту и повыше, но данные передаются по длинным проводам в условиях высоких помех, поэтому время на установку данных до и после строба лучше взять с запасом. Если кто-то будет повторять мои опыты на той же макетной плате — не используйте порт P3.2: на плате к этой ножке припаян конденсатор. Я полчаса убил, пока выявил, почему у меня не формируется импульс E, который я сначала туда подключил. Перекинул на P3.1 — всё сразу заработало. У меня шина данных идёт на P3.7-P3.4, RS идёт на P3.3, поэтому E исходно шла на P3.2…
Ну вот. Теперь, если попробовать скомпилировать проект, получим вполне предсказуемые ошибки
Значит, система пытается собирать что-то. Но ей пока нечего собрать. Приступаем к копированию кода. Для этого в UDB Editor переключаемся на вкладку Verilog (эта вкладка расположена под окном с листом UDB Editor):
Что там знакомое? В самом конце текста есть тело автомата. Давайте начнём перенос с него.
/* ==================== State Machine: SM ==================== */ always @ (posedge clock) begin : Idle_state_logic case(SM) Idle : begin if (( !F0empty ) == 1'b1) begin SM <= LoadF0 ; end else if (( !F1empty ) == 1'b1) begin SM <= LoadF1 ; end end LoadF0 : begin if (( 1'b1 ) == 1'b1) begin SM <= E_Up1 ; end end E_Up1 : begin if (( 1'b1 ) == 1'b1) begin SM <= E_Down1 ; end end E_Down1 : begin if (( 1'b1 ) == 1'b1) begin SM <= SWAP ; end end SWAP : begin if (( 1'b1 ) == 1'b1) begin SM <= E_UP2 ; end end E_UP2 : begin if (( 1'b1 ) == 1'b1) begin SM <= Idle ; end end LoadF1 : begin if (( 1'b1 ) == 1'b1) begin SM <= A1toA0 ; end end A1toA0 : begin if (( 1'b1 ) == 1'b1) begin SM <= E_Up1 ; end end default : begin SM <= Idle; end endcase end
Сверху для этого кода имеются объявления (имена для состояний, цепи для Datapath, регистр, кодирующий состояние автомата). Переносим их в соответствующий
/* ==================== Wire and Register Declarations ==================== */ localparam [2:0] Idle = 3'b000; localparam [2:0] LoadF0 = 3'b001; localparam [2:0] LoadF1 = 3'b010; localparam [2:0] E_Up1 = 3'b100; localparam [2:0] A1toA0 = 3'b011; localparam [2:0] E_Down1 = 3'b101; localparam [2:0] SWAP = 3'b110; localparam [2:0] E_UP2 = 3'b111; wire hungry0; wire F0empty; wire hungry1; wire F1empty; wire Datapath_1_d0_load; wire Datapath_1_d1_load; wire Datapath_1_f0_load; wire Datapath_1_f1_load; wire Datapath_1_route_si; wire Datapath_1_route_ci; wire [2:0] Datapath_1_select; reg [2:0] SM;
Ну, и
/* ==================== Assignment of Combinatorial Variables ==================== */ assign Datapath_1_d0_load = (1'b0); assign Datapath_1_d1_load = (1'b0); assign Datapath_1_f0_load = (1'b0); assign Datapath_1_f1_load = (1'b0); assign Datapath_1_route_si = (1'b0); assign Datapath_1_route_ci = (1'b0); assign Datapath_1_select[0] = (SM[0]); assign Datapath_1_select[1] = (SM[1]); assign Datapath_1_select[2] = (SM[2]);
Пришла пора подключить Datapath. Код, перенесённый из UDB Editor, хорош для машинной правки, но не очень — для ручной. Там создаются цепи, которые одним концом подключаются ко входам Datapath, а другим — к константам. Но в коде, созданном Datapath Configuration Tool (который делает всё для ручной работы), все входы уже подключены к нулевым константам напрямую. Так что я подключу только те линии, которые не являются константами, а всё, что относится к пробросу констант вырежу из перенесённого текста. Подключение получилось таким (цветом выделены места, которые я отредактировал относительно автоматически созданного в Datapath Configuration Tool):
)) LCD_DP( /* input */ .reset(1'b0), /* input */ .clk(clk), /* input [02:00] */ .cs_addr(SM), /* 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(), /* output */ .cl0(), /* output */ .z0(), /* output */ .ff0(), /* output */ .ce1(), /* output */ .cl1(), /* output */ .z1(), /* output */ .ff1(), /* output */ .ov_msb(), /* output */ .co_msb(), /* output */ .cmsb(), /* output */ .so(), /* output */ .f0_bus_stat(hungry0), /* output */ .f0_blk_stat(F0empty), /* output */ .f1_bus_stat(hungry1), /* output */ .f1_blk_stat(F1empty),
С параллельными данными чуть сложнее. У Datapath порт восьмибитный, а наружу надо вывести только четыре из них. Поэтому заводим вспомогательную цепь и подключаем на выход только её половину:
wire [7:0] tempBus; assign LCD_D = tempBus[7:4];
И подключаем всё так:
/* input [07:00] */ .pi(8'b0), // Parallel data port /* output [07:00] */ .po( tempBus) // Parallel data port );
Пробуем собрать (Shift+F6 либо через пункт меню Build->Generate Application). Получаем ошибку:
У нас есть порты hungry0 и hungry1 (появились при создании компонента), а также одноимённые цепи (появились при перетягивании из образца). Просто удалим эти цепи (оставив порты). И где-то просочился сигнал clock, а у нас эта цепь называется clk.
После удаления всех лишних цепей (тех, которые исходно пробрасывали нулевые константы на входы Datapath, а также hungry0 и hungry1), получаем такой код начала нашего файла:
// Your code goes here /* ==================== Wire and Register Declarations ==================== */ localparam [2:0] Idle = 3'b000; localparam [2:0] LoadF0 = 3'b001; localparam [2:0] LoadF1 = 3'b010; localparam [2:0] E_Up1 = 3'b100; localparam [2:0] A1toA0 = 3'b011; localparam [2:0] E_Down1 = 3'b101; localparam [2:0] SWAP = 3'b110; localparam [2:0] E_UP2 = 3'b111; wire F0empty; wire F1empty; reg [2:0] SM; /* ==================== Assignment of Combinatorial Variables ==================== */ wire [7:0] tempBus; assign LCD_D = tempBus[7:4];
А при замене clock на clk в теле автомата я заодно выкину все строки, которые хороши при автогенерации, но при ручной правке только создают путаницу (все сравнения, дающие безусловный результат TRUE и прочее). В частности, в примере ниже можно вычеркнуть около половины строк (а некоторые begin/end — по желанию, иногда они понадобятся, ведь мы будем добавлять действия, их я выделил цветом):
После причёсывания по приведённому выше принципу (и замены clock на clk) остаётся такое тело
always @ (posedge clk) begin : Idle_state_logic case(SM) Idle : begin if (( !F0empty ) == 1'b1) begin SM <= LoadF0 ; end else if (( !F1empty ) == 1'b1) begin SM <= LoadF1 ; end end LoadF0 : begin SM <= E_Up1 ; end E_Up1 : begin SM <= E_Down1 ; end E_Down1 : begin SM <= SWAP ; end SWAP : begin SM <= E_UP2 ; end E_UP2 : begin SM <= Idle ; end LoadF1 : begin SM <= A1toA0 ; end A1toA0 : begin SM <= E_Up1 ; end default : begin SM <= Idle; end endcase end
Теперь нам при компиляции говорят, что цепи LCD_E и LCD_RS не подключены. Собственно, это правда:
Пришла пора добавить конечному автомату действий. Заменим объявления соответствующих неподключённым цепям портов на reg, так как мы будем писать в них в теле автомата (таков синтаксис языка Verilog, если пишем — данные должны защёлкнуться, для этого нужен триггер, а его даёт ключевое слово reg):
module LCD4bit ( output hungry0, output hungry1, output [3:0] LCD_D, output reg LCD_E, output reg LCD_RS, input clk );
И наполним автомат действиями. Логику я уже проговаривал выше, когда рассматривал граф переходов автомата, поэтому покажу только результат:
always @ (posedge clk) begin : Idle_state_logic case(SM) Idle : begin LCD_E <= 0; if (( !F0empty ) == 1'b1) begin SM <= LoadF0 ; LCD_RS <= 0; end else if (( !F1empty ) == 1'b1) begin SM <= LoadF1 ; LCD_RS <= 1; end end LoadF0 : begin SM <= E_Up1 ; end E_Up1 : begin SM <= E_Down1 ; LCD_E <= 1'b1; end E_Down1 : begin SM <= SWAP ; LCD_E <= 1'b0; end SWAP : begin SM <= E_UP2 ; end E_UP2 : begin SM <= Idle ; LCD_E <= 1; end LoadF1 : begin SM <= A1toA0 ; end A1toA0 : begin SM <= E_Up1 ; end default : begin SM <= Idle; end endcase end
С этого момента проект начинает собираться. Но работать он пока не будет. Пока что я лихо проговаривал: «В этом состоянии у нас загрузится регистр из FIFO», «В этом скопируется A1 в A0», «В этом переставятся нибблы». В общем, говорил я много, а действий пока не было. Пришла пора их выполнить. Смотрим, как у нас закодировались состояния:
localparam [2:0] Idle = 3'b000; localparam [2:0] LoadF0 = 3'b001; localparam [2:0] LoadF1 = 3'b010; localparam [2:0] E_Up1 = 3'b100; localparam [2:0] A1toA0 = 3'b011; localparam [2:0] E_Down1 = 3'b101; localparam [2:0] SWAP = 3'b110; localparam [2:0] E_UP2 = 3'b111;
Вновь открываем Datapath Configuration Tool:
И начинаем править строки CFGRAM. При правке следует держать перед глазами схему Datapath, а именно:
Красными рамками на рисунке ниже (и стрелками на рисунке выше) я выделил исправленные участки (и путь прохождения данных) для состояния LoadF0 (код 001, то есть, Reg1). Комментарии я также вписывал вручную. В A0 должно попасть содержимое F0.
Зелёными рамками и стрелками я обозначил настройки и путь для состояния LoadF1 (код 010 — Reg2).
Синими рамками и стрелками я обозначил настройки и путь для состояния A1toA0 (код 011 — Reg3).
Фиолетовыми рамками и стрелками я обозначил настройки и путь для состояния SWAP (код 110 — Reg6).
Наконец, оранжевыми стрелками показан путь параллельных данных. А действий для них не совершается никаких. Они всегда выходят из SRCA. У нас почти всегда в качестве SRCA выбран A0: данные выходят из A0. Вот для перенаправления входных данных пришлось бы выполнять массу вспомогательных действий, но мы же не принимаем никаких данных, так что здесь нам эти действия не нужны, а все желающие найдут их перечень в AN82156. Также нам не требуется править никаких статических настроек Datapath, поэтому закрываем Datapath Config Tool.
Всё. Задуманная аппаратура завершена. Приступаем к разработке кода на Си. Для этого идём на вкладку Source и правим файл main.c.
Штатная инициализация ЖКД и вывод символов «ABC» выглядят так (напомню, команды уходят в FIFO0, между командами документация требует вставлять паузы, а данные уходят в FIFO1, про паузы между данными я ничего не нашёл):
volatile uint8_t* pFIFO0 = (uint8_t*) LCD4bit_1_LCD_DP__F0_REG; volatile uint8_t* pFIFO1 = (uint8_t*) LCD4bit_1_LCD_DP__F1_REG; pFIFO0[0] = 0x33; CyDelay (5); pFIFO0[0] = 0x33; CyDelay (100); pFIFO0[0] = 0x33; CyDelay (5); pFIFO0[0] = 0x20; CyDelay (5); pFIFO0[0] = 0x0C; // включили экран CyDelay (50); pFIFO0[0] = 0x01; // Очистили экран CyDelay (50); pFIFO1[0] = 'A'; pFIFO1[0] = 'B'; pFIFO1[0] = 'C';
Что такое? Почему на экране только первый символ?
А если между выводом данных добавить задержки — всё хорошо:
Осциллографу для такой работы не хватит каналов. Проверяем работу на логическом анализаторе. Процесс записи данных выглядит следующим образом.
Все данные — на месте (три пары посылок). Время на установку и защёлкивание данных выделяется в достаточном объёме. В общем, с точки зрения временных диаграмм — всё сделано верно. Научная задача решена, желаемые временные диаграммы формируются. Вот инженерная — нет. Виной всему — медлительность процессора, установленного в ЖКД. Между байтами надо добавить задержки.
Задержки мы будем формировать при помощи семибитного счётчика, заодно потренируемся добавлять его в такую систему. Пусть мы будем находиться в состоянии Idle не менее, чем некое заданное время, а семибитный счётчик будет нам это время отмерять. И вновь мы будем не писать, а создавать код. Поэтому снова идём на вспомогательный компонент UDB Editor и добавляем на лист счётчик, настроив его параметры следующим образом:
Этот счётчик будет работать всегда (Enable присвоено значение 1). Но загружаться он будет, когда автомат находится в состоянии E_UP2 (после которого мы сразу провалимся в состояние Idle). Линия Count7_1_tc взведётся в 1, когда счётчик досчитает до нуля, что мы сделаем дополнительным условием выхода из состояния Idle. На рисунке также вписано значение периода, но в Verilog коде мы его не найдём. Его придётся вписывать в код на Си. Но сначала переносим автоматически сгенерированный Verilog код, переключившись на вкладку Verilog. В первую очередь, счётчик следует подключить (этот код мы видим в начале файла и перенесём тоже в начало):
`define CY_BLK_DIR "$CYPRESS_DIR..psoccontentCyComponentLibraryCyComponentLibrary.cylibCount7_v1_0" `include "$CYPRESS_DIR..psoccontentCyComponentLibraryCyComponentLibrary.cylibCount7_v1_0Count7_v1_0.v"
Как выполняется творческая доработка линий и констант уже описывалось, поэтому я просто покажу результат. Вот добавленные в итоге цепи и присвоения (остальные задавали константы, поэтому я их выкинул):
wire Count7_1_load; wire Count7_1_tc; assign Count7_1_load = (SM==E_UP2);
А вот сам счётчик, помещаемый в конце файла. Все константы присваиваются портам прямо в этом объявлении:
Count7_v1_0 Count7_1 ( .en(1'b1), .load(Count7_1_load), .clock(clk), .reset(1'b0), .cnt(), .tc(Count7_1_tc)); defparam Count7_1.EnableSignal = 1; defparam Count7_1.LoadSignal = 1;
Чтобы этот счётчик включился в работу, автомату добавляем дополнительное условие для выхода из состояния Idle:
case(SM) Idle : begin LCD_E <= 0; if (( !F0empty ) == 1'b1) begin SM <= LoadF0 ; LCD_RS <= 0; end else if (( !F1empty &Count7_1_tc ) == 1'b1) begin SM <= LoadF1 ; LCD_RS <= 1; end end
API для добавленного таким образом счётчика не создаётся, поэтому в функцию main добавляем две волшебных строки, которые я сформировал по образу и подобию увиденного в API от прошлых проектов (первая строка задаёт загружаемое значение счёта, тот самый Load, вторая запускает счётчик):
*((uint8_t*)LCD4bit_1_Count7_1_Counter7__PERIOD_REG) = 0x20; *((uint8_t*)LCD4bit_1_Count7_1_Counter7__CONTROL_AUX_CTL_REG) |= 0x20; // Start
Анализатор показывает, что в доработанном случае задержки налицо:
На ЖКД также есть все три символа.
Но программный вывод символов в реальной жизни неприемлем. Если просто добавлять их в FIFO, произойдёт переполнение. Ждать опорожнения FIFO — это значит создавать большие задержки для процессорного ядра. Процессор работает на частоте 72 МГц, а данные выводятся за 7-8 тактов на частоте 1 МГц. Поэтому в реальной жизни текст надо выводить средствами DMA. Именно здесь нам пригодится принцип «Запустил и забыл». Все задержки для временной диаграммы нам сформирует UDB, а готовность FIFO к приёму данных нам будет определять контроллер DMA. От процессорного ядра требуется только сформировать строку в памяти и настроить DMA, после чего оно может заниматься другими задачами, совершенно не заботясь о выводе в ЖКД.
static const char line[] = "This is a line"; /* Defines for DMA_D */ #define DMA_D_BYTES_PER_BURST 1 #define DMA_D_REQUEST_PER_BURST 1 /* Variable declarations for DMA_D */ /* Move these variable declarations to the top of the function */ uint8 DMA_D_Chan; uint8 DMA_D_TD[1]; /* DMA Configuration for DMA_D */ DMA_D_Chan = DMA_D_DmaInitialize(DMA_D_BYTES_PER_BURST, DMA_D_REQUEST_PER_BURST, HI16(line), HI16(LCD4bit_1_LCD_DP__F1_REG)); DMA_D_TD[0] = CyDmaTdAllocate(); CyDmaTdSetConfiguration(DMA_D_TD[0], sizeof(line)-1, CY_DMA_DISABLE_TD, CY_DMA_TD_INC_SRC_ADR); CyDmaTdSetAddress(DMA_D_TD[0], LO16((uint32)line), LO16((uint32)LCD4bit_1_LCD_DP__F1_REG)); CyDmaChSetInitialTd(DMA_D_Chan, DMA_D_TD[0]); CyDmaChEnable(DMA_D_Chan, 1);
На экране имеем:
Заключение
На полусинтетическом, но близком к реальным задачам, примере мы освоили механизм разработки кода для UDB при помощи альтернативного механизма — Datapath Config Tool. Этот механизм, в отличие от UDB Editor, даёт доступ абсолютно ко всем возможностям управления UDB, но работа с ним сложнее, чем с UDB Editor. Тем не менее предложенный автором статьи метод позволяет не писать код с нуля, а просто создавать его, опираясь на вспомогательный код, созданный всё тем же UDB Editor.
Получившийся при написании статьи тестовый проект можно взять тут.
Источник