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

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



Введение


Самое удивительное — это то, что как только я решил сделать лирическое отступление на прочие темы, доброе начальство бросило меня в тяжкий бой на проект, где работа идёт с языком 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 <cstdio>
#include <unistd.h>  /* UNIX standard function definitions */
#include <fcntl.h>   /* File control definitions */
#include <errno.h>   /* Error number definitions */
#include <termios.h> /* 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 operations\n", 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 Device\n");
		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 Mode\n");
		// 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 ReadWrite\n");
	} 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. На структурной схеме ему соответствует вот такой участок:



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