Решил написать очередную статью, которая была бы полезна начинающим разработчикам в области ПЛИСоводства. Очень долго откладывал момент публикации, сам материал подготовил еще несколько месяцев назад, а вот сесть и написать всё это в целую статью как-то не доходили руки. Но вот наконец-то появилось свободное время, поэтому всех заинтересовавшихся приглашаю к прочтению.
Проект называется «глупые часы», поскольку он ничего не умеет, кроме как считать время и дату и выводить их на дисплей. В первую очередь проект рассчитан на студентов старших курсов и новичков. В нём нет никаких необычных блоков, не используются вендорные IP-ядра, и уж тем более нет сложных интерфейсов обмена (типа PCIe, Ethernet, USB и т.д.).
В этот раз проект на программируемых логических интегральных схемах (ПЛИС) будет примитивен и предельно прост, а я постараюсь рассказать обо всех трудностях, с которыми пришлось столкнуться при выполнении поставленной задачи.
Исходные данные
Отладочная плата: все тот же небольшой и простенький девкит на базе ПЛИС Spartan3E. Его характеристики:
Основные особенности:
- FPGA Spartan3E (XC3S500E-4PQ208C) — 500К логических вентилей,
- Источник тактовой частоты CLK = 50 MHz,
- Внешняя память 64M SDRAM,
- SPI Flash (M25P80) для хранения прошивки ПЛИС,
- Матрица светодиодов 8х8, линейка светодиодов 8 шт.,
- 8 переключателей и 5 кнопок,
- Разъемы для подключения LED-дисплеев,
- Разъем VGA для подключения дисплея,
- Таймер DS1302,
- Дисплей LCD1602,
- Разъемы PS/2, и т. д.
Из этого списка нам потребуется непосредственно сама FPGA, которая будет всем этим делом управлять. Также необходимы два устройства (микросхема DS1302 и дисплей LCD1602) и несколько триггеров-переключателей.
DS1302 — таймер с последовательным интерфейсом передачи (произв. Maxim Integrated),
LCD1602 — ЖК дисплей с возможностью вывода двух строк по 16 символов (произв. Noname).
Ниже приведена структурная схема соединений:
Перейдем к описанию используемых микросхем.
Таймер
DS1302 представляет собой небольшую микросхему, использующую для обмена данными три линии, которые образуют последовательный интерфейс.
- CE — clock enable, разрешает тактирование по линии SCLK.
- SCLK — тактовый сигнал (максимальная частота работы = 2 МГц).
- I/O — порт ввода/вывода, по которому в последовательном виде передаются данные и команды.
Главная особенность микросхемы заключается в том, что ход времени не прекращается, даже если ПЛИС и дисплей отключить от питания. Таймер позволяет считать секунды, минуты, часы, дни недели, числа, месяцы, а также годы, то есть полный набор «часов».
Также микросхема хранит последние «прошитые» значения времени и даты. Но сама по себе микросхема «глупая», то есть ей необходимо задать все параметры, включая день недели. Внутри таймера находится счетчик, который правильным образом инкрементирует значения времени и даты, если в микросхему все было прошито должным образом.
К микросхеме извне подключен кварцевый генератор, настроенный на частоту f = 32.768 kHz.
На следующем рисунке приведен интерфейс обмена данными в режиме чтения и записи:
Как это работает?
a) Для того, чтобы записать или считать из таймера какое-либо значение, необходимо в момент передачи выставить на вход CE высокий уровень (логическая 1),
b) В зависимости от операции чтения или записи, при высоком уровне CE необходимо подать 15 или 16 тактовых импульсов, соответственно. Операция записи данных со входа I/O происходит по нарастающему фронту тактового сигнала SCLK, а операция чтения данных происходит по спадающему фронту сигнала SCLK. Данные передаются в порядке от младшего бита к старшему.
c) Первый байт в процессе передачи — всегда служебный и всегда «записывается» в таймер. Это означает, что он выбирает тип и направление переданной команды. Нулевой бит R/W отвечает за тип операции: «чтение» — при высоком уровне, «запись» — при низком уровне сигнала. Битовое поле с 1 до 5 задает адрес команды или внутренней памяти таймера. Шестой бит определяет работу с регистрами таймера или внутренней памятью: «память» — при высоком логическом уровне, «таймер» — при низком логическом уровне. Седьмой бит всегда должен быть установлен в 1 (если случайным образом седьмой бит оказался равен 0, то таймер не отреагирует на команду).
d) Второй байт — данные, записываемые в таймер или считываемые из него.
На следующем рисунке приведена таблица команд таймера:
Как работать с таблицей?
Из таблицы видно, что таймер обрабатывает данные в двоично-десятичном формате (BCD). В связи с этим в дальнейшем на ПЛИС придется написать преобразователь данных из двоичного формата в двоично-десятичный.
Пример: чтобы считать из таймера значение секунд, необходимо сформировать командный байт 0x81. Чтобы записать значение минут, необходимо сформировать командный байт 0x82.
При работе с микросхемой таймера есть небольшая тонкость, из-за которой я целый день убил на поиски проблем и даже пару раз переписал контроллер таймера на ПЛИС. Поэтому призываю всех — внимательнее читайте даташиты. :)
Для записи даты и времени в микросхему необходимо в первую очередь «разрешить запись». Видите в таблице по адресам 0x8F и 0x8E бит WP (write protect)? По умолчанию — бит находится в 0, что означает запрет на любые операции записи. Следовательно, для записи в микросхему даты и времени необходимо в первую очередь перевести WP в состояние логической единицы. Делается это посылкой двух байт друг за другом (команда и данные): 0x8E 0x80. После этого таймер станет послушным и разрешит писать в себя информацию.
Также обратите внимание на 7 бит в регистре секунд — CH (clock halt). Таймер останавливается, если этот бит находится в высоком состоянии. Поэтому при первой записи в регистр секунд нужно обнулить самый старший бит.
ЖК дисплей
LCD1602 это обычный ЖК дисплей, позволяющий выводить две строки по 16 символов, использует параллельный интерфейс обмена. Интерфейс примитивен и прост.
- DB[7:0] — параллельная шина команд/данных.
- RS — register select, сигнал выбора: '0' — команды, '1' — данные.
- R/W — направление передачи данных ('0' — запись / '1' — чтение).
- E — enable, синхронизация, выставляется в логическую единицу в момент передачи по линии DB
Я использовал дисплей в режиме записи, то есть порт R/W = 0 всегда. В связи несколько упрощается обмен данными, как и сам контроллер, реализуемый на ПЛИС.
Как работать с дисплеем?
Перед каждым первым выводом на дисплей необходимо произвести его инициализацию набором команд, которые приведены в даташите.
Для моей реализации контроллера дисплея это был набор из четырех команд, выполняемых последовательно одна за другой при каждом запуске прошивки. На линию DB необходимо подать несколько байт инициализации:
0x00 (либо 0x01) — очищает дисплей.
0x38 — устанавливает количество линий и размер шрифта (мне нужно 2 линии по 16 символов с максимальным размером символов).
0x0C — устанавливает курсор в положение on/off. («вкл.» в моем случае).
0x06 — устанавливает направление движения курсора и сдвиг по дисплею (вправо, т.е. инкремент счетчика сдвига).
При этом сигналы RS и R/W должны быть в состоянии логического нуля.
После процесса инициализации по линии DB по очереди передаются команды и данные. Команда определяет положение символа на дисплее, данные определяют символ из таблицы, приведенной ниже.
Например, для записи символа "Q" необходимо записать по линии DB число 0x51. Таким образом, вся передача данных на дисплей сводится к выбору позиции и символа.
Проект на ПЛИС
Перейдем к реализации проекта на ПЛИС. Язык программирования, используемый в проекте — VHDL. Отличается от остальных своей простотой и в то же время громоздкостью конструкций.
Для того, чтобы часы начали тикать, а дисплей отображать дату и время нужно сделать несколько действий, которые, в принципе, делаются для каждого проекта на ПЛИС. Требуется:
- два независимых контроллера — для DS1302 и для LCD1602.
- две тестовые связки — для установки начального времени и для инициализации дисплея.
- файл верхнего уровня (top level), связывающий все это вместе.
- файл распиновки выводов ПЛИС (UCF файл).
UCF
Начнем с простого. Определим пины, которые будут использоваться в проекте.
## CLK 50 MHz
NET "CLK" LOC = "P183" | IOSTANDARD = LVCMOS33 ;
## Switches
NET "RESET" LOC = "P148" | IOSTANDARD = LVTTL | PULLUP ; ## SW<0>
NET "START" LOC = "P130" | IOSTANDARD = LVTTL | PULLUP ; ## SW<3>
NET "RESTART" LOC = "P124" | IOSTANDARD = LVTTL | PULLUP ; ## SW<4>
NET "TEST_LCD" LOC = "P118" | IOSTANDARD = LVTTL | PULLUP ; ## SW<5>
## LCD ports
NET "LCD_RS" LOC = "P77" | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST ;
NET "LCD_RW" LOC = "P68" | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST ;
NET "LCD_EN" LOC = "P65" | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST ;
NET "LCD_DT<0>" LOC = "P49" | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST ;
NET "LCD_DT<1>" LOC = "P48" | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST ;
NET "LCD_DT<2>" LOC = "P40" | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST ;
NET "LCD_DT<3>" LOC = "P50" | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST ;
NET "LCD_DT<4>" LOC = "P62" | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST ;
NET "LCD_DT<5>" LOC = "P98" | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST ;
NET "LCD_DT<6>" LOC = "P64" | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST ;
NET "LCD_DT<7>" LOC = "P63" | IOSTANDARD = LVTTL | DRIVE = 8 | SLEW = FAST ;
# SERIAL PORTS VIA HEADEX
NET "T_CE" LOC = "P147" | IOSTANDARD = LVTTL | SLEW = SLOW | DRIVE = 8 ;
NET "T_CK" LOC = "P146" | IOSTANDARD = LVTTL | SLEW = SLOW | DRIVE = 8 ;
NET "T_DT" LOC = "P145" | IOSTANDARD = LVTTL | SLEW = SLOW | DRIVE = 8 ;
В UCF файле задается название порта, используемое в файле верхнего уровня, пин, к которому он подключен, а также остальные параметры (электрический тип, уровень тока, скорость драйвера, подтяжка и т.д.). Выше были описаны сигналы для подключения к таймеру и ЖК дисплею, поэтому приведу описание недостающих портов, подключенных к переключателям.
- RESET — '0' — глобальный сброс ПЛИС.
- START — '1' — старт для ЖК дисплея (запускает инициализацию и процесс передачи данных).
- RESTART — '0' — перезапуск таймера (переводит дату и время в состояние, определенное в памяти ПЛИС).
- TEST_LCD — порт для тестовых целей, '0' — выводит данные с таймера, '1' выводит статическое сообщение («hello habr').
Второй подводный камень, с которым я столкнулся — разработчики девкита забыли подключить микросхему таймера к ПЛИС. Но, к счастью, они вывели сериальный интерфейс на штырьки, которые я соединил проводниками с другими штырями, подключенными непосредственно к ПЛИС.
HDL
Далее пишется контроллер таймера и контроллер дисплея. Приводить полный текст на VHDL здесь нет смысла, но расскажу о принципах обмена данными. При первичном запуске (когда все управляющие сигналы выставлены и снят reset) производится инициализация ЖК дисплея, а в таймер записываются константы даты и времени, определенные в файле верхнего уровня. Ниже представлен код функции преобразования данных из двоичного формата в двоично-десятичный:
---------------- INTEGER TO STD_LOGIC_VECTOR TO BCD CONVERTER ----------------
constant n : integer:=8;
constant q : integer:=2;
function to_bcd ( bin : std_logic_vector((n-1) downto 0) ) return std_logic_vector is
variable i : integer:=0;
variable j : integer:=1;
variable bcd : std_logic_vector(((4*q)-1) downto 0) := (others => '0');
variable bint : std_logic_vector((n-1) downto 0) := bin;
begin
for i in 0 to n-1 loop -- repeating 8 times.
bcd(((4*q)-1) downto 1) := bcd(((4*q)-2) downto 0); --shifting the bits.
bcd(0) := bint(n-1);
bint((n-1) downto 1) := bint((n-2) downto 0);
bint(0) :='0';
l1: for j in 1 to q loop
if(i < n-1 and bcd(((4*j)-1) downto ((4*j)-4)) > "0100") then --add 3 if BCD digit is greater than 4.
bcd(((4*j)-1) downto ((4*j)-4)) := bcd(((4*j)-1) downto ((4*j)-4)) + "0011";
end if;
end loop l1;
end loop;
return bcd;
end to_bcd;
На следующем рисунке приведены временные диаграммы, полученные в процессе отладки контроллера таймера. Жирными линиями выделен последовательный интерфейс передачи. Тройка линий data_i, data_o, data_t подключаются к IOBUF порта ввода/вывода в файле верхнего уровня.
Внутри ПЛИС в бесконечном цикле начинает опрашивать регистры таймера, отвечающие за установку времени (0x80-0x8D). Опрос регистров производится по кругу, начиная с годов и заканчивая секундами. После каждого считывания из таймера, данные заносятся в регистры ПЛИС, носящие характерные названия (секундный, минутный и т.д.). После приема сигнала разрешения от контроллера ЖК дисплея, данные из контроллера таймера передаются в управляющий узел для контроллера дисплея. В зависимости от типа регистра таймера и его значения, данные преобразуются в необходимый для дисплея формат и задают необходимую позицию на дисплее путем выставления соответствующего адреса. Контроллер ЖК дисплея принимает данные с таймера и записывает их в двухпортовую 8х32 RAM-память в ПЛИС, выполненную на распределенной логике кристалла (ячейки SLICEM). Затем контроллер считывает всю информацию из памяти и тем самым обновляет символы на ЖК дисплее. Весь проект работает на относительно высокой частоте 50 МГц, поэтому никаких задержек при счёте секунд глазом увидеть нереально.
На следующем рисунке приведено размещение проекта в кристалле ПЛИС.
Зелеными линиями обозначены подключения портов ввода/вывода к внутренним ресурсам ПЛИС, Синяя область — область логических вентилей, красные прямоугольники — связка встроенных перемножителей 18х18 и блочной памяти RAMB16K, оранжевые квадраты — блоки синтеза тактовой частоты DCM (digital clocking manager). Прямоугольники, выделенные фиолетовым цветом — „p-block“, в границах которого разводится тот или иной компонент проекта при включенной опции „Keep Hierarchy“. Выделенные белым цветом области — ячейки контроллеров и тестовых модулей. Остальные p-блоки остались от моего старого проекта на ПЛИС, их я решил не удалять (а вдруг кто захочет в сапёр поиграть?).
Результат:
Итак, в ходе работы была создана прошивка ПЛИС для связи таймера с ЖК дисплеем.
С помощью САПР Aldec Active-HDL проводилось временное моделирование проекта, а также все написание кода и моделей. В среде Xilinx ISE Design Suite проходил синтез и трассировка проекта на FPGA Spartan3E. Также применялись вспомогательные средства: PlanAhead — используется для распиновки, расположения узлов проекта в кристалле и трассировки. ChipScope Pro — используется для отладки в реальном железе путем загрузки в ПЛИС специальных отладочных ядер.
Автоматическая документация проекта
Так получилось, что во время создания этого проекта, у нас на работе была предложена очередная попытка внедрения документации исходного кода. Одним из решений предлагалось использовать средство автоматической документации Doxygen. Мне очень нравится, как в этом приложении строятся графы иерархии. Для HDL-проектов это очень важный момент. На хабре есть несколько статей, посвященных этому инструменту. В связке с HTML Help Workshop приложение Doxygen позволяет генерировать всего один файл документации в формате CHM.
К сожалению, попытка внедрения провалилась, но опыт работы с инструментом остался. Поэтому для проекта „глупых“ часов на ПЛИС я решил воспользоваться автоматическим документированием. На гитхабе в папке с проектом лежит настроечный файл для DoxyWizard и результат его работы. Вот одна страница из того, что получилось в процессе документирования.
Процесс отладки в ChipScope:
Исходный код на github — LCD Timer on FPGA.
Видео-демонстрация работоспособности проекта:
Не удивляйтесь дате, отображаемой на дисплее. Материал отснял и подготовил давно, а до статьи руки только сейчас дошли. Переснимать лениво… :)
Похожие проекты:
- Проект на базе микроконтроллера AVR.
- Проект на базе ардуино.
Комментарии (5)
woddy
22.11.2015 21:14+1Про rtc на 1302 не знаю. У разных таймеров заточенных на i2c шину есть серьезный баг. Если пропало напряжение в середине транзакции, то данные в таймере бьются. Вероятно какая-то из команд распознается как запись. Потому приходится один раз в момент включения вычитывать данные из RTC а дальше тикать кварцем контроллера, сверяясь раз в сутки например
capitanov
22.11.2015 22:14С пропаданием напряжения иметь дело не приходилось. Но плату оставлял включенной более, чем на сутки, предварительно записав время реальное и отображаемое. Спустя ~30-31 час на дисплее отображались нужные значения, которые должны были натикать за это время в ds1302. :)
woddy
22.11.2015 22:31Условия у меня были такие. опрос таймера = обновление дисплея = 200гц. Если раз 10-20-30 дернуть питание, то происходил сбой и часы начинали показывать фигню (требовалось выставлять занова). Долго не мог понять в чем же причина. Баг вероятностный, надо суметь «попасть». Иногда с 10 раза сбрасывалось иногда с 50го.
capitanov
22.11.2015 23:05Думаю, ваше предположение верно о том, что команда чтения воспринималась как запись. Скорее всего портилось содержимое внутренних регистров таймера, поэтому он начинал сбоить. В процессе отладки проекта на первых стадиях мне иногда удавалось словить иероглифы, но причина была не в питании, а в неверной скорости записи/чтения из микросхемы.
Dioniss81
Как то запускали графический дисплей без своего контроллера вывода — там были только драйвера строк-столбцов. Вот там бы ПЛИС пригодилась :-)
А в целом статья интересная. Конечно, в производственной практике не применимо, чисто академический интерес.