Доступ к шинам комплекса Redd, реализованным на мостах FTDI

Мы закончили большой теоретический блок, показывающий, как можно строить ПЛИС-подсистему для комплекса Redd; как организовывать связь между ПЛИС и центральным процессором комплекса; как легко сохранять скоростные потоки данных в ОЗУ, имеющем прямую связь с ПЛИС, для последующей их неспешной перекачки к центральному процессору (или наоборот, помещать данные в это ОЗУ для последующей быстрой выдачи в канал). Мы рассмотрели методики трассировки работы процессора Nios II. Мы умеем оптимизировать быстродействие процессорной системы на базе Nios II, чтобы работа шла максимально эффективно. В общем, мы изучили весь необходимый минимум теории, и пора бы перейти к практике, спроектировав не очень сложное, но практически полезное устройство… Но имеется одно НО.

Из комментариев к статьям я заметил, что некоторые читатели полагают, что Redd и ПЛИС — как Ленин и Партия. Что они неразрывно связаны. На самом деле всё совсем не так. Просто начать разговор о комплексе Redd хотелось с чего-то интересного, а что может быть интереснее, чем ПЛИС? Ну, а начав разговор, прерываться на полуслове глупо. И вот, наконец, большой логический блок завершён. И чтобы показать, что ПЛИС — это далеко не весь Redd, предлагаю сделать ориентировочно три статьи о вещах, не связанных с ними. Ну, а завершив этот блок, уже перейти к ПЛИСовой практике.

Доступ к шинам комплекса Redd, реализованным на мостах FTDI

Введение

Самое удивительное — это то, что как только я решил сделать лирическое отступление на прочие темы, доброе начальство бросило меня в тяжкий бой на проект, где работа идёт с языком VHDL и ПЛИС Xilinx. Во-первых, именно поэтому давно я не брал в руки перо вообще, а во-вторых, понятно, что подготовка практических статей требует большого количества экспериментов. Заниматься же одновременно VHDL/Verilog и Xilinx/Altera несколько сложно. Так что перерыв в рассказах о ПЛИС сделать всё равно бы пришлось.

Итак. В первой статье цикла мы уже рассматривали структурную схему комплекса Redd. Давайте сделаем это ещё раз.

В сегодняшней статье специалисты по ОС Linux вряд ли найдут много ценной информации, но поверхностно пробежаться по картинкам всё-таки стоит. Те же, кто как и я, привык к работе из ОС Windows, найдут перечень готовых методик, позволяющих работать с комплексом. В общем, эта статья приведёт навыки тех и других групп читателей к общему знаменателю.

Блоки 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 будет самым жёстким.

Итого, имеем следующий путь для поиска имени порта (его следует проделать единожды, результаты для своего экземпляра комплекса можно вынести в таблицу и использовать постоянно):

  1. Подать команду ls –l /dev/serial/by-id.
  2. Подать команду ls –l /dev/serial/by-path.
  3. Из результатов пункта 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.

Код, посылающий данные в шину SPI.

#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. На структурной схеме ему соответствует вот такой участок:

Но реально там всё чуть более интересно…

 

Источник

FPGA, Redd, контроллеры FTDI, микроконтроллеры, ПЛИС, системное программирование

Читайте также