Из комментариев к статьям я заметил, что некоторые читатели полагают, что Redd и ПЛИС — как Ленин и Партия. Что они неразрывно связаны. На самом деле всё совсем не так. Просто начать разговор о комплексе Redd хотелось с чего-то интересного, а что может быть интереснее, чем ПЛИС? Ну, а начав разговор, прерываться на полуслове глупо. И вот, наконец, большой логический блок завершён. И чтобы показать, что ПЛИС — это далеко не весь Redd, предлагаю сделать ориентировочно три статьи о вещах, не связанных с ними. Ну, а завершив этот блок, уже перейти к ПЛИСовой практике.
Введение
Самое удивительное — это то, что как только я решил сделать лирическое отступление на прочие темы, доброе начальство бросило меня в тяжкий бой на проект, где работа идёт с языком VHDL и ПЛИС Xilinx. Во-первых, именно поэтому давно я не брал в руки перо вообще, а во-вторых, понятно, что подготовка практических статей требует большого количества экспериментов. Заниматься же одновременно VHDL/Verilog и Xilinx/Altera несколько сложно. Так что перерыв в рассказах о ПЛИС сделать всё равно бы пришлось.
Итак. В первой статье цикла мы уже рассматривали структурную схему комплекса Redd. Давайте сделаем это ещё раз.
В сегодняшней статье специалисты по ОС Linux вряд ли найдут много ценной информации, но поверхностно пробежаться по картинкам всё-таки стоит. Те же, кто как и я, привык к работе из ОС Windows, найдут перечень готовых методик, позволяющих работать с комплексом. В общем, эта статья приведёт навыки тех и других групп читателей к общему знаменателю.
- Разработка простейшей «прошивки» для ПЛИС, установленной в Redd, и отладка на примере теста памяти.
- Разработка простейшей «прошивки» для ПЛИС, установленной в Redd. Часть 2. Программный код.
- Разработка собственного ядра для встраивания в процессорную систему на базе ПЛИС.
- Разработка программ для центрального процессора Redd на примере доступа к ПЛИС.
- Первые опыты использования потокового протокола на примере связи ЦП и процессора в ПЛИС комплекса Redd.
- Веселая Квартусель, или как процессор докатился до такой жизни.
- Методы оптимизации кода для Redd. Часть 1: влияние кэша.
- Методы оптимизации кода для Redd. Часть 2: некэшируемая память и параллельная работа шин.
- Экстенсивная оптимизация кода: замена генератора тактовой частоты для повышения быстродействия системы.
Блоки UART (последовательные порты)
На структурной схеме мы видим контроллер FT4232, реализующий 4 последовательных порта (UART):
Но если рассуждать чуть более глобально, то у комплекса Redd не четыре, а шесть последовательных портов. Просто упомянутые четыре имеют уровни КМОП, а ещё два — припаяны на материнской плате, ведь в основе комплекса заложен обыкновенный PC.
Соответственно, у них уровни – RS232 (плюс-минус 12 вольт). Порты RS232 — с ними всё понятно, они выведены в виде двух стандартных разъёмов DB-9,
а где искать линии c уровнями КМОП? В целом — на общем разъёме. Его цоколёвка приведена на схеме электрической принципиальной. Есть там, среди прочего, и контакты, соответствующие UART.
Внешне этот разъём выглядит следующим образом:
Как его использовать — зависит от задачи. Можно для подключения каждого устройства изготавливать жгут. Такой подход пригодится, если кто-то использует комплекс Redd для тестирования периодически изготавливаемых устройств одного и того же вида. Но основное назначение комплекса — всё-таки отладка разрабатываемого оборудования. И в этом случае проще подключаться к нему по временной схеме. Такая временная схеме видна на заставках ко всем статьям: прямо в разъём вставлены Aruino-проводочки. Само собой, отсчитывать контакты — то ещё удовольствие, а если они случайно вылетят — восстанавливать коммутацию настолько сложно, что проще подключить всё заново с нуля; поэтому для облегчения жизни имеется переходная плата, к которой можно подключаться хоть при помощи двухрядных разъёмов, хоть теми же Arduino-проводочками.
Программный доступ к UART
Последовательный порт — это устоявшийся, хорошо стандартизированный элемент, поэтому работа с ним идёт не через какие-то специфичные библиотеки FTDI, а через стандартные средства. Давайте рассмотрим, как эти средства выглядят в ОС Linux.
Имена портов
Из ряда статей и форумов в сети следует, что имена портов, обеспечиваемых переходниками USB-Serial, имеют формат /dev/ttyUSB0, /dev/ttyUSB1 и так далее. В ОС Linux все устройства можно просмотреть, используя те же команды, что и для просмотра обычных каталогов (собственно, устройства – это те же файлы). Давайте просмотрим, какие же имена есть в нашей системе. Подаём команду:
ls /dev/
Интересующие нас имена я выделил красной рамкой. Что-то много их. Какой порт чему соответствует? Кто хорошо ориентируется в Linux, знает тысячи заклинаний на все случаи жизни. Но тем, кто работал ещё с Windows 3.1 (ну и параллельно с тогда ещё вполне бодренькой старушкой RT-11), это запомнить всё равно сложно, с возрастом новое запоминается тяжелее. Поэтому проще каждый раз всё находить, пользуясь простыми путями. И вход на этот простой путь я выделил зелёной рамкой. Условный подкаталог serial. Сейчас мы смотрим пространство имён /dev/. А посмотрим пространство /dev/serial:
Отлично! Углубляемся в иерархию, смотрим пространство /dev/serial/by-id. Только, забегая вперёд, скажу, что для правильного отображения надо использовать команду ls с ключом –l (спасибо моему начальнику за разъяснения). То есть, даём команду:
ls –l /dev/serial/by-id
С одной стороны, всё прекрасно. Теперь мы знаем, каким именам в пространстве /dev/ttyUSBX соответствует какое устройство. В частности, порты, организованные мостом FT4232 (Quad), имеют имена от ttyUSB3 до ttyUSB6. Но с другой, при рассмотрении этого участка я понял, что в Париже в палате мер и весов обязательно должна иметься комната, в которой размещён эталон бардака… Потому что как-то надо уметь измерять его величину. Ну, допустим, отсутствие портов /dev/ttyUSB0 и /dev/ttyUSB1 легко можно объяснить. Но как объяснить, что «родные» порты на базе отродясь установленного моста FTDI нумеруются с тройки, а вставленный под конкретный проект сторонний контроллер Prolific занял порт с номером 2? Как можно в такой обстановке работать? Завтра кто-то воткнёт в комплекс ещё какой-то контроллер (благо комплекс допускает одновременную работу разных групп разработчиков с разным оборудованием), и порты снова съедут. Какие же порты прописывать нам в конфигурационный файл для рабочего приложения?
Оказывается, всё не так плохо. Во-первых, жёлтое имя /dev/ttyUSB3 и голубое имя /dev/serial/by-id/usb-FTDI_Quad_RS232-HS-if00-port0 – это два псевдонима одного и того же устройства. И второй вариант тоже можно подавать как имя порта, а он уже более постоянный, чем первый. Правда, и в этом случае всё несколько нехорошо. В комплекс могут воткнуть внешний контроллер на базе FT4232, и уже надо будет разбираться с их нумерацией. И вот тут нам на помощь приходит «во-вторых». А именно ещё один альтернативный вариант именования. Мы помним, что каталог /dev/serial содержал не только подкаталог /by-id, но и подкаталог /by-path. Проверяем его содержимое (оно размещено в нижней части следующего рисунка, под красной чертой).
Здесь всё привязано к физической архитектуре. А я уже многократно говорил, что все контроллеры внутри комплекса припаяны на платы, поэтому внутренняя иерархия не изменится. Таким образом, имя /dev/serial/by-path/pci-0000:00:15.0-usb-0:6.5:1.0-port0 будет самым жёстким.
Итого, имеем следующий путь для поиска имени порта (его следует проделать единожды, результаты для своего экземпляра комплекса можно вынести в таблицу и использовать постоянно):
- Подать команду ls –l /dev/serial/by-id.
- Подать команду ls –l /dev/serial/by-path.
- Из результатов пункта 1 найти имя порта, соответствующее требуемому порту требуемого моста. Найти такое же имя порта в результатах пункта 2. Взять физическое имя, соответствующее этому пункту.
Для портов, обслуживаемых контроллером на материнской плате, всё чуть сложнее. Здесь не получится проделать путь от простейшей команды «ls /dev», а придётся кое-что запомнить (ну, или хотя бы запомнить, что за справкой можно обращаться сюда). Везде говорится, что типовые имена портов – ttyS0-ttyS3. Остаётся вопрос, на каких именах располагаются реальные порты в нашей системе? Я обнаружил следующее заклинание, отвечающее на данный вопрос:
ls /sys/class/tty/*/device/driver
Вот ответ системы на него:
Получается, что нам надо пользоваться именами /dev/ttyS2 и /dev/ttyS3. Почему — я не знаю. Но радует одно: здесь особых изменений не предвидится, поэтому эти константы можно запомнить и использовать, не боясь, что они изменятся.
Разработка программного кода
При разработке стоит воспользоваться замечательным руководством Serial Programming Guide for POSIX Operating Systems (первая попавшаяся прямая ссылка https://www.cmrr.umn.edu/~strupp/serial.html, но сколько она проживёт – не знает никто). Особенно важно, что там рассказывается, как работать с полным набором сигналов, ведь порты в комплексе реализованы полноценные. Правда, сегодня мы будем использовать только линии Tx и Rx.
Обычно я привожу в качестве результатов осциллограммы, но сейчас получилось так, что я нахожусь практически в реальных условиях: комплекс расположен там, куда руки не дотянутся, так что щуп осциллографа подключить не получится. Чтобы увидеть хоть какой-то результат, по моей просьбе коллеги добавили в комплекс пару проводочков по следующей классической схеме:
Попробуем произвести передачу с одного порта на другой. В нашем случае, соединены порты /dev/serial/by-path/pci-0000:00:15.0-usb-0:6.5:1.2-port0 и /dev/serial/by-path/pci-0000:00:15.0-usb-0:6.5:1.3-port0.
Как пишутся программы для центрального процессора Redd, мы уже рассматривали в одной из предыдущих статей, поэтому сегодня ограничимся просто текстом программы, написанным под впечатлением от документа Serial Programming Guide for POSIX Operating Systems. Собственно, главный интересный момент там состоит в переключении стратегии приёма на неблокирующее чтение, остальное – тривиально. Тем не менее, учитывая бардак в примерах в сети на эту тему, лучше даже тривиальный образец под рукой всё-таки иметь (далее будет показано, что даже пример на базе этого замечательного документа, и тот вышел не на 100% работающим, приведённый ниже код отличается от описанных в нём канонов одной строкой, но об этом — ниже).
#include
#include /* UNIX standard function definitions */
#include /* File control definitions */
#include /* Error number definitions */
#include /* POSIX terminal control definitions */
int OpenUART(const char* portName, speed_t baudRate)
{
// Открыли порт
int fd = open(portName, O_RDWR | O_NOCTTY | O_NDELAY);
// А он не открылся
if (fd == -1)
{
return fd;
}
// Переключаем ввод на неблокирующий
fcntl(fd, F_SETFL, FNDELAY);
// Задаём скорость порта
termios options;
tcgetattr(fd, &options);
// Говорят, что можно использовать не только типовые
// константы, но и значения. Заодно проверим...
cfsetspeed(&options, baudRate);
// Попутно настроим прочие параметры...
// 1 стоп бит, нет контроля чётности, 8 бит в байте
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
options.c_cflag &= ~CSIZE;
options.c_cflag |= CS8;
options.c_cflag |= (CLOCAL | CREAD);
// Всё, установили...
tcsetattr(fd, TCSANOW, &options);
return fd;
}
int main()
{
printf("hello from ReddUARTTest!n");
int fd1 = OpenUART("/dev/serial/by-path/pci-0000:00:15.0-usb-0:6.5:1.3-port0", 9600);
int fd2 = OpenUART("/dev/serial/by-path/pci-0000:00:15.0-usb-0:6.5:1.2-port0", 9600);
if ((fd1 != -1) && (fd2 != -1))
{
static const unsigned char dataForSend[] = {0xff,0xfe,0xfd,0xfb};
// Посылаем данные в один порт
write(fd1, dataForSend, sizeof(dataForSend));
unsigned char dataForReceive[128];
ssize_t cnt = 0;
// Будем подсчитывать число попыток чтения, чтобы убедиться,
// что мы прочли не за одну блокирующую попытку
int readSteps = 0;
// Мы должны принять хотя бы столько, сколько передали
while (cnt < (ssize_t)sizeof(dataForSend))
{
readSteps += 1;
ssize_t rd = read(fd2, dataForReceive + cnt, sizeof(dataForReceive) - cnt);
// Не считалось - уснули, чтобы не грузить процессор
if (rd <= 0)
{
usleep(1000);
}
else
// Иначе - учли считанное
{
cnt += rd;
}
}
// Отобразили результат
printf("%d read operationsn", readSteps);
printf("Read Data: ");
for (unsigned int i = 0; i < cnt; i++)
{
printf("%X ", dataForReceive[i]);
}
printf("n");
}
else
{
printf("Error with any port open!n");
}
// Закрываем порты
if (fd1 != -1)
{
close(fd1);
}
if (fd2 != -1)
{
close(fd2);
}
return 0;
}
Запускаем – получаем предсказуемый результат:
hello from ReddUARTTest!
14 read operations
Read Data: FF FE FD FB
Видно, что 4 байта принялись за 14 попыток, то есть, чтение было не блокирующим. Иногда система возвращала состояние «нет новых данных», и программа уходила в сон на одну миллисекунду.
В общем, всё хорошо, но без осциллографа я не могу быть уверен, что два порта на базе одной микросхемы действительно устанавливают скорость. Я уже нажигался на том, что скорость была одинаковая (на то он и один контроллер), но не та, которую я заказал. Давайте хоть как-то проверим, что она хотя бы управляется. Для этого я задам скорость принимающего порта вдвое больше, чем у передающего. А зная физику процесса передачи данных, можно предсказать, как эти данные исказятся при приёме. Давайте рассмотрим передачу байта 0xff в графическом виде. S – стартовый бит (там всегда ноль) бит, P – стоповый бит (там всегда единица), 0-7 – биты данных (для константы 0xFF – все единицы).
А теперь наложим на этот рисунок вид, как всё будет рассматриваться приёмником, работающем на вдвое большей скорости:
Прекрасно. Приняться должно значение «1111 1110» (данные же идут младшим битом вперёд), то есть, 0xFE. Вторая половина переданного значения не влияет на приём, так как единицы соответствуют тишине в линии. То есть, мы передали один байт, придёт тоже один байт.
Построим такой же график для проверки, что будет соответствовать переданному значению 0xFE:
Ожидать следует значения «1111 1000» или 0xF8. Ну давайте ещё для закрепления материала проверим, что ждать при переданном значении 0xFD:
Получаем значение 0xE6. Ну, и для переданного значения 0xFB получаем принимаемое 0x9E (можете построить график и убедиться в этом сами). Замечательно! Меняем в тестовом приложении одну единственную строку, заменив скорость 9600 на 19200:
int fd2 = OpenUART("/dev/serial/by-path/pci-0000:00:15.0-usb-0:6.5:1.2-port0", 19200);
Запускаем и получаем такой результат работы:
hello from ReddUARTTest!
9 read operations
Read Data: FE F8 E6 9E
К слову, я не зря провёл эту проверку. Сначала я использовал другие функции установки скорости (пару cfsetispeed/cfsetospeed), и они не работали! Благодаря данному тесту, проблема была своевременно выявлена и устранена. При работе с аппаратурой никогда нельзя доверять интуиции. Всё следует проверять!
Управление силовыми линиями 220 вольт
Вообще, силовые линии 220 вольт не относятся к теме статьи (мосты FTDI), но зато относятся к теме данного раздела (последовательные порты). Давайте вскользь рассмотрим их.
Когда мы перечисляли порты, мы видели такое имя:
Это виртуальный последовательный порт. Он настолько виртуален, что не важно, какие у него заданы параметры (скорость порта, число битов, формат чётности и т.п.). Какие бы параметры у него ни были заданы, он всё равно будет прекрасно обрабатывать команды. И именно эти команды управляют силовыми розетками на корпусе комплекса.
При разработке системы команд было решено отказаться от сложных командных интерфейсов. Управление идёт одним байтом, без обрамления строк и прочих изысков, хотя байт – текстовый (чтобы его удобно было передавать из терминала при отладке). Такая лаконичность легко объясняется: строковый интерфейс позволяет бороться с помехами в незащищённом UART-канале. Но в нашем случае, физически работа идёт через USB-канал, который защищён циклическим контрольными кодами. Обработка же обратного потока требует или написания дополнительного кода, или постоянного сброса буферов, что не всегда удобно. Именно поэтому нет никаких реперов у строк, нет никаких ответов. Считается, что канал устойчив. Если требуется получить ответ, его можно явно запросить. То есть, работоспособность блока всегда легко можно проверить, послав вдогонку с командой дополнительный байт.
Рассмотрим команды, которые можно послать:
Команда | Назначение |
---|---|
'A' | Включить первую розетку |
'a' | Выключить первую розетку |
'B' | Включить вторую розетку |
'b' | Выключить вторую розетку |
'C' | Включить третью розетку (если имеется) |
'c' | Выключить третью розетку (если имеется) |
'?' | Вернуть состояние розеток |
Команда ‘?’ (знак вопроса) — единственная, которая возвращает отклик. В ответ на неё всегда приходит 3 байта, каждый из которых соответствует состоянию одной из розеток. Собственно, состояния соответствуют командам. Например, ‘abc’ — все три розетки сейчас выключены, ‘Abc’ — первая включена, вторая и третья выключены и т.п.
Для экспериментов с этой подсистемой предлагаю не писать специальную программу (она ничем не отличается от приведённой ранее, только данные, посылаемые в порты, будут другими), а воспользоваться средствами ОС и поиграть с розетками интерактивно.
После массы экспериментов с отслеживанием порта через команду cat и посылкой команд в параллельном окне с помощью программы echo, я понял, что почему-то в паре ssh-терминалов на базе putty я не могу добиться результатов (даже играя с теми портами, с которыми только что прекрасно экспериментировал своей программой). Поэтому пришлось установить стандартную программу minicom. Напомню команду установки:
sudo apt-get minicom
Дальше запускаем её командой:
minicom –D /dev/ttyACM0
Имя порта короткое, так как при ручных опытах его ввести проще всего. При программной работе, как всегда, лучше использовать имя, привязанное к иерархии аппаратуры. Ещё раз обращаю внимание, что никакие другие параметры порта я не настраиваю потому, что он — виртуальный. Он заработает при любых настройках.
Дальше нажимаем в терминале знак вопроса и моментально (без перевода строки) получаем отклик
Это значит, что все розетки в данный момент отключены. Допустим, мы хотим включить вторую розетку. Нажимаем заглавную ‘B’. На экране нет никакой реакции. Снова нажимаем ‘?’, получаем новую строку с ответом:
Всё работает. Не забываем отключить 220 вольт (командой ‘b’). Можно выходить из терминала, последовательно нажав ctrl+A, затем – X. Эксперимент завершён.
Шины SPI и I2C
Шины SPI (которая может работать также в режиме Quad-SPI) и I2C реализованы в комплексе при помощи универсальных мостов. То есть, в целом, в комплексе имеется два моста, каждый из которых может быть включён либо в режиме SPI, либо в режиме I2C. На структурной схеме соответствующий участок выглядит так:
Суть же включения конечных шин видна из схемы электрической принципиальной. Рассмотрим только один из двух контроллеров:
Таким образом, электрически шины SPI и I2C никак не пересекаются. Ограничения на их совместное использование определяются только ограничениями, заложенными фирмой FTDI в контроллер FT4222H. К сожалению, документация гласит, что одновременно может быть активен только один интерфейс:
Как управлять линиями CFG1_0..CFG1_1 и CFG2_0..CFG2_1, мы познакомимся в следующей статье. Сейчас считаем, что все они занулены.
В целом, работа с контроллером очень хорошо описана в документе FT4222H USB2.0 TO QUADSPI/I2C BRIDGE IC, поэтому рассматривать особенности режимов работы контроллеров мы не будем. Всё очень ясно из упомянутого документа.
Что касается программной поддержки, то её описание можно прочесть в не менее замечательном документе AN_329 User Guide For LibFT4222. Мы уже дважды работали с мостом от FTDI: во второй половине этой статьи и во второй же половине этой. Поэтому сопоставляя данный документ с этими статьями, можно быстро разобраться и начать писать свой код. Давайте я просто покажу опорный код, посылающий данные в шину SPI, не останавливаясь на деталях его реализации, уж больно всё похоже на уже разобранную работу с FT2232.
#include "../ftd2xx/ftd2xx.h"
#include "../LibFT4222/inc/LibFT4222.h"
void SpiTest (int pos)
{
FT_HANDLE ftHandle = NULL;
FT_STATUS ftStatus;
FT4222_STATUS ft4222Status;
// Открываем устройство
ftStatus = FT_Open(pos, &ftHandle);
if (FT_OK != ftStatus)
{
// open failed
printf ("error: Cannot Open FTDI Devicen");
return;
}
ft4222Status = FT4222_SPIMaster_Init(ftHandle, SPI_IO_SINGLE, CLK_DIV_4, CLK_IDLE_LOW, CLK_LEADING, 0x01);
if (FT4222_OK != ft4222Status)
{
printf ("error: Cannot switch to SPI Master Moden");
// spi master init failed
return;
}
uint8 wrBuf [] = {0x9f,0xff,0xff,0xff,0xff,0xff,0xff};
uint8 rdBuf [sizeof (wrBuf)];
uint16 dwRead;
ft4222Status = FT4222_SPIMaster_SingleReadWrite (ftHandle,rdBuf,wrBuf,sizeof (wrBuf),&dwRead,TRUE);
if (FT4222_OK != ft4222Status)
{
printf ("error: Error on ReadWriten");
} else
{
printf ("received: ");
for (int i=0;i<6;i++)
{
printf ("0x%X ",rdBuf[i]);
}
printf ("n");
}
FT4222_UnInitialize(ftHandle);
FT_Close(ftHandle);
}
Детали шины SPI
Разработчики кода для микроконтроллеров зачастую используют шину SPI, как генератор заранее известной частоты. Действительно, импульсы, генерируемые чисто программно через линии GPIO, зависят от многих факторов. Во-первых, ветвление, закручивающее цикл, требует затрат тактов процессора. Во-вторых, в работу процессора могут вмешиваться прерывания, работа DMA и прочие непредвиденные факторы. SPI же более-менее стабилен, знай себе успевай байты в буфер подкладывать. Типичное применение блока SPI, не имеющее к этому самому SPI прямого отношения, — управление RGB светодиодами, для которых очень важна точность задания длительности, импульсов.
К сожалению, для мостов FTDI всё это неприемлемо. Приведённый выше фрагмент кода сформирует вот такие импульсы на шине:
Правила работы SPI при этом не нарушаются, с точки зрения этой шины, всё работает корректно. Просто имейте в виду, что привычные на контроллерах нестандартные решения здесь не сработают. Правда, у комплекса имеется масса свободных разъёмов USB. Все нестандартные блоки могут быть разработаны отдельно и подключены к ним.
Детали шины I2C
Единственное, что имеет смысл указать, отсутствие подтягивающих резисторов для шины I2C на стороне комплекса. Но это нормально: на стороне рабочего устройства подтяжка всё равно имеется. В наше время подтяжка может быть к любым напряжениям, поэтому вполне логично, что она задаётся на целевом устройстве.
Заключение
Сегодня мы получили практические навыки работы с шинами, реализуемыми мостами FTDI. В целом, работа с ними стандартна, просто все знания сведены в единую статью, чтобы не искать их по крупицам. В следующий же раз мы рассмотрим модуль, управляющий нестандартными устройствами, реализованный на базе контроллера STM32. На структурной схеме ему соответствует вот такой участок:
Но реально там всё чуть более интересно…