Реализация связки прошивки ПЛИС, ПО микроконтроллера NIOS и управляющего ПО под Linux на базе Altera Cyclone V SoC с использованием Avalon Mailbox для создания на их основе распределенной системы управления.
Введение
В распределенной системе управления приходится решать множество разных задач на разных уровнях.
Некоторые задачи целесообразно решать на уровне встраиваемого ПК с полноценной ОС. Полноценная ОС хороша тем, что в ней уже реализовано и отлажено много полезных инструментов, таких как многопоточность, готовые драйвера, библиотеки, разные фреймворки и прочее. И все это можно разрабатывать на языках высокого уровня, особо не вдаваясь в детали реализации на нижнем уровне.
Есть задачи, которые удобно решать на уровне микроконтроллера (далее – МК), либо вообще без ОС (bare-metal), либо с минималистичными ОС реального времени. Здесь ключевую роль играет возможность отладки софта внутри ОС по JTAG и отслеживание происходящего на уровне периферии МК на любом break-point`е.
И есть задачи, которые стоит решать уже на уровне ПЛИС, так как никакого микроконтроллера может не хватить для грамотного параллельного управления разной высокочастотной электроникой, например, драйверами шаговых приводов с обработкой данных энкодеров и регуляторами скоростей. В таких задачах процессор бывает просто лишним.
Количество исполнительных устройств и их различных функций в системе управления сильно возрастает, если речь идет о разработке прибора, например, с трех-степенным манипулятором, парой-тройкой серводвигателей, дюжиной дискретных устройств, кучей периферии на всех популярных интерфейсах (SPI, I2C, UART и т.д.) и сложной логикой с математическим анализом внутри. И всю систему управления очень удобно расположить вообще на одном кристалле, что собственно и будет сделано. В итоге все три уровня управления ПК-МК-ПЛИС и их взаимодействия переместятся внутрь кристалла.
В таком случае неизбежно возникает задача создания транспортного уровня, связывающего всю эту сложную логику между собой. Для связки МК-ПЛИС это решается, фактически, созданием очередного периферийного устройства на общей шине МК со своим набором регистров. Но задача создания транспортного уровня ПК-МК будет решаться немного иначе.
Для экспериментов нам понадобится реальная или виртуальная машина с Ubuntu 16.04 на борту.
Исходные тексты всех программ доступны на GitHub.
Архитектура системы управления
Представим, что все исполнительные устройства системы управления ПК-МК-ПЛИС сводятся к параллельным портам ввода-вывода. Для примера в качестве датчиков и исполнительных устройств ограничимся набором кнопок и светодиодов, а управлять ими будем из командной строки терминала.
Все элементы ПЛИС, в том числе МК, будем синтезировать. Часть ПК уже интегрирована в чип и выполнена на базе Cortex A9, шины которого выведены в ПЛИС и могут быть использованы напрямую. Поэтому все, что придется сделать, это подключить к ядру ОС модули, необходимые для связи с синтезированными узлами в ПЛИС через стандартные средства.
В качестве аппаратной платформы используем комплект DE0-Nano-SoC.
Получение прошивки ПЛИС
Возьмем за основу базовый проект my_first_hps-fpga_base из набора DE0-Nano-SoC CD-ROM (rev.B0 / rev.C0 Board). Проект содержит в себе предварительно настроенную среду с правильно выставленными портами ПЛИС, готовым блоком Cyclone V Hard Processor System с настроенными параметрами памяти и набором вспомогательных элементов в Qsys. Для работы с проектом нам понадобится Quartus Prime 15.1 с пакетом поддержки Cyclone V и SoC Embedded Design Suite.
Внесем некоторые изменения в проект. Добавим ядро NIOS, память для него (16 Кб 32-битной ширины) и порт JTAG. Укажем в параметрах NIOS адреса векторов из добавленной памяти.
Avalon Mailbox является симплексным, поэтому нам нужно два модуля (наподобие RX и TX линий обычного UART). Сигнал прерывания каждого из модулей нужно подключить на тот процессор, для которого модуль является приемным.
Добавим по одному порту (8 бит) ввода и вывода для дальнейшего тестирования системы.
После добавления всех элементов можно сделать автоматический подбор адресов и прерываний.
Создадим порты для кнопок и светодиодов в коде верхнего модуля.
// Ports
wire [7:0] port_out;
assign LED = port_out;
wire [7:0] port_in;
assign port_in = {{2{1'b0}}, SW, KEY};
Подключим порты к soc_system.
// FPGA Partion
.port_out_export(port_out), // port_out.export
.port_in_export(port_in), // port_in.export
Соберем проект и получим прошивку ПЛИС, на базе которой будем дальше работать.
Алгоритм
Итак, создадим систему, которая будет делать следующее:
- При включении тумблера активируется таймер;
- По таймеру с частотой 1 Гц будет загораться один из светодиодов по порядку;
- По нажатию кнопки будет меняться направление;
- При получении команды READ из ПК будет отправляться номер текущего активного светодиода на стандартную консоль Linux;
- При получении команды WRITE из ПК будет меняться текущий активный диод;
- При получении команды REVERSE из ПК будет меняться направление, так же, как от кнопки;
- По нажатию другой кнопки на консоль ПК будет отправляться количество переключений светодиодов с момента последнего реверса.
На стороне МК
В среде NIOS II EDS, которая по сути – Eclipse со всеми нужными плагинами, создадим новый проект soc_nios из шаблона “NIOS II Application and BSP”. В результате получится два проекта: непосредственно прошивка и BSP.
В первую очередь нужно сразу собрать BSP, но не традиционным способом. Вместо этого в контекстном меню проекта soc_nios_bsp нужно выбрать в меню NIOS II пункт BSP Editor и включить опции enable_small_c_library и enable_reduced_device_drivers, чтобы прошивка особо не разрасталась. Затем собрать, нажав Generate. В дальнейшем, так как параметры сборки сохранятся, пересобрать BSP можно просто выбором в меню NIOS II пункта Generate BSP.
В файле system.h из проекта BSP можно увидеть все параметры периферии МК, которые были добавлены ранее в схему Qsys.
Более подробно о NIOS и о том, как собирать для него проекты, можно почитать тут.
Для решения задачи на уровне МК нам понадобятся:
- Обработчик прерываний от таймера;
void TIMER_0_ISR(void* context) { IOWR_ALTERA_AVALON_TIMER_STATUS(TIMER_0_BASE, 0); IOWR_ALTERA_AVALON_TIMER_CONTROL(TIMER_0_BASE, ALTERA_AVALON_TIMER_CONTROL_CONT_MSK); led += step; if(led > LED_MAX) { led = 0; } if(led < 0) { led = LED_MAX; } IOWR_ALTERA_AVALON_PIO_DATA(PORT_OUT_0_BASE, (1 << led)); count++; IOWR_ALTERA_AVALON_TIMER_CONTROL(TIMER_0_BASE, ALTERA_AVALON_TIMER_CONTROL_CONT_MSK | ALTERA_AVALON_TIMER_CONTROL_ITO_MSK); }
- Обработчик прерываний от Mailbox;
void MAILBOX_HPS2NIOS_ISR(void* context) { IOWR_ALTERA_AVALON_MAILBOX_INTR(MAILBOX_SIMPLE_HPS2NIOS_BASE, 0); //NOTE: Order is important! CMD register should be read after PTR register buffer[1] = IORD_ALTERA_AVALON_MAILBOX_PTR(MAILBOX_SIMPLE_HPS2NIOS_BASE); buffer[0] = IORD_ALTERA_AVALON_MAILBOX_CMD(MAILBOX_SIMPLE_HPS2NIOS_BASE); alt_printf("Reading: 0x%x 0x%xnr", buffer[0], buffer[1]); newMail = true; IOWR_ALTERA_AVALON_MAILBOX_INTR(MAILBOX_SIMPLE_HPS2NIOS_BASE, ALTERA_AVALON_MAILBOX_SIMPLE_INTR_PEN_MSK); }
- Парсер сообщений и функция записи в Mailbox;
- Функции опроса кнопок и управления светодиодами.
Осталось собрать проект. Размер прошивки NIOS должен составить меньше 16 Кб.
Для тестирования прошивки на железе нужно создать новую конфигурацию отладчика. После прошивки ПЛИС из Quartus Programmer в меню Debug Configurations выбираем вариант NIOS II Hardware, обновляем все интерфейсы и во вкладке Target Connections находим jtaguart_1. Это тот самый JTAG для NIOS, который мы ранее добавили в Qsys.
Теперь можно запускать отладку из Eclipse. Если все сделано правильно, в консоли NIOS II должно появится сообщение «Turn the switch ON to activate the timer».
На стороне ПК
Установка ОС Linux на плату
Подробным образом весь процесс описан здесь в разделах с 1 по 10. Рекомендуется использовать более свежие свежие версии тулчейна, бутлоадера и ядра, чем те, которые можно найти по ссылке. Обратите внимание, что для сборки данной версии бутлоадера не подойдет тулчейн с компилятором выше 6-й версии.
Для генерации device tree вместо предложенной утилиты sopc2dts лучше использовать скрипт sopc2dts.jar, причем можно указать сразу --type dtb.
Для получения системы настоятельно рекомендуется использовать самый свежий Buildroot. Для сборки необходимо принудительно указать переменные окружения CC как путь к arm-linux-gnueabihf-gcc и CXX как путь к arm-linux-gnueabihf-g++ из тулчейна. Далее ввести используемые версии компилятора, ядра и библиотеки (их подскажет сама система в процессе сборки). В настройках тулчейна при конфигурации Buildroot надо обязательно указать путь к тулчейну, а также префикс $(ARCH)-linux-gnueabihf и включить поддержку SSP, RPC и C++.
Для удобства можно добавить в Buildroot пакеты nano, mc и openssh.
Далее, собирать весь софт верхнего уровня будем в Eclipse с плагином GNU MCU Eclipse plug-in. Создадим новый workspace для ARM проектов и в глобальных настройках Eclipse в разделе Workspace Tools Path укажем соответствующий путь к установленной версии Linaro.
Драйвер
Первым делом сделаем драйвер для Mailbox`ов. Создадим в Eclipse новый проект nios_mailbox из шаблона «Hello World ARM C Project».
В настройках проекта выключим опции «Use default build command» и «Generate Makefiles automatically», так как для сборки модуля ядра понадобится команда make TARGET=nios_mailbox TARGET_DIR=Default. Добавим в переменные окружения две новых записи CROSS_COMPILE и KDIR, указывающие на полный путь с префиксом тулчейна и путь к исходникам ядра соответственно. В список дефайнов надо добавить __GNUC__, __KERNEL__ и MODULE. Всё. Теперь можно писать код.
Модуль ядра будет реагировать на прерывание из железа и должен как-то сообщать об этом миру приложений. Для этой цели нам понадобится создать свой сигнал.
#define NIOS_MAILBOX_REALTIME_SIGNO 44
Драйвер будем создавать на базе platform_device, каждый Mailbox будет как miscdevice, а в конечном итоге в системе будет виден как файл устройства в каталоге /dev. Более подробно о драйверах и вообще можно почитать тут. Важно понимать, что Mailbox`ов у нас может быть теоретически сколько угодно, а драйвер на всех один, и он должен все их инициализировать и пронумеровать.
Если особо не вдаваться в детали, то разработка драйвера сводится к реализации стандартных операций чтения и записи в файл, плюс небольшой бонус в виде специальной функции ioctl(), которая нужна для проброса драйверу id процесса, который его использует. Это нужно для того, чтобы знать, какому процессу в системе сигнализировать о возникновении аппаратного прерывания. Сам обработчик прерывания выглядит довольно просто и очень похож на свой аналог в NIOS.
static irq_handler_t nios_mailbox_isr(int irq, void *pdev)
{
struct nios_mailbox_dev *dev = (struct nios_mailbox_dev*)platform_get_drvdata(pdev);
spin_lock(&dev->lock);
//NOTE: Order is important! CMD register should be read after PTR register
dev->data[1] = ioread32(dev->regs + ALTERA_AVALON_MAILBOX_SIMPLE_PTR_OFST * sizeof(u32));
dev->data[0] = ioread32(dev->regs + ALTERA_AVALON_MAILBOX_SIMPLE_CMD_OFST * sizeof(u32));
spin_unlock(&dev->lock);
if(dev->task)
{
send_sig_info(dev->sinfo.si_signo, &dev->sinfo, dev->task);
}
return (irq_handler_t)IRQ_HANDLED;
}
Осталось собрать проект. Для этого нам нужно написать специальный Makefile. Выглядеть он будет так.
all:
@echo 'KDIR=$(KDIR)'
@echo 'CROSS_COMPILE=$(CROSS_COMPILE)'
@if [ ! -d $(CURDIR)/$(TARGET_DIR) ]; then mkdir $(CURDIR)/$(TARGET_DIR); fi
cp $(TARGET).c $(CURDIR)/$(TARGET_DIR)
cp $(TARGET).h $(CURDIR)/$(TARGET_DIR)
cp Kbuild $(CURDIR)/$(TARGET_DIR)
$(MAKE) -C $(KDIR) ARCH=arm M=$(CURDIR)/$(TARGET_DIR)
clean:
rm -rf main $(CURDIR)/$(TARGET_DIR)
И еще нам понадобится создать файл Kbuild с одной строчкой.
obj-m := $(TARGET).o
Соберем проект традиционным способом. В результате получим модуль ядра nios_mailbox.ko, который скопируем на систему и установим при помощи insmod. Если все сделано правильно, в консоли Linux, открытой по USB, при нажатии соответствующей кнопки на плате должно появится сообщение от ядра "[ .........] NIOS Mailbox new mail!".
Конечно, в драйвер надо бы добавить буфер для принимаемых данных по прерыванию, так как чтение прикладной программой может не успевать за потоком данных от железа. И сам драйвер лучше собирать с опцией stripped, для экономии места в embedded-системе. Однако, эти вопросы оставим читателю для самостоятельного изучения.
Приложение
Вот мы и добрались до написания консольного приложения. Создадим в Eclipse новый проект soc_test из шаблона «Hello World ARM C++ Project». В разделе Settings в Target Processor выберем архитектуру cortex-a9, в Cross ARM GNU G++ Linker добавим -pthread. Во вкладке Build Artifact можно убрать расширение файла. Все остальные настройки можно оставить по-умолчанию.
Для решения задачи на уровне приложения нам понадобятся:
- Обработчик сигнала;
void Nios::mailbox_nios2hps_signal_handler(int signo, siginfo_t *info, void *unused) { if(info->si_signo == NIOS_MAILBOX_REALTIME_SIGNO) { sem_post(&mailbox_nios2hps_signal_semaphore); } }
Парсер сообщений от Mailbox;
void *Nios::mailbox_nios2hps_data_reader(void *args) { uint64_t mes; while(1) { while(sem_wait(&mailbox_nios2hps_signal_semaphore)); if(lseek(mailbox_nios2hps, 0, SEEK_SET) != 0) { cerr << "Failed to seek mailbox_nios2hps to proper location" << endl; continue; } read(mailbox_nios2hps, &mes, sizeof(mes)); printf("[HARDWARE] Reading: 0x%08x 0x%08xn", (uint32_t)mes, (uint32_t)(mes >> 32)); switch ((uint32_t)mes) { case LED_NUMBER: printf("Active led %lun", (uint32_t)(mes >> 32)); break; case SWITCH_COUNT: printf("Led switched %lu timesn", (uint32_t)(mes >> 32)); break; default: break; } } return NULL; }
- Функция отправки сообщений в Mailbox;
void Nios::mailbox_hps2nios_write(uint64_t mes) { if(lseek(mailbox_hps2nios, 0, SEEK_SET) != 0) { cerr << "Failed to seek mailbox_hps2nios to proper location" << endl; } else { printf("[HARDWARE] Writing: 0x%08x 0x%08xn", (uint32_t)mes, (uint32_t)(mes >> 32)); write(mailbox_hps2nios, &mes, sizeof(mes)); } }
- Процедура настройки с файлами устройств, которые появились после установки драйвера;
Nios::Nios () { struct sigaction backup_action; pid = getpid(); mailbox_nios2hps = open("/dev/nios_mailbox_0", O_RDONLY); if(mailbox_nios2hps < 0) { cerr << "Could not open "/dev/nios_mailbox_0"..." << endl; exit(1); } memset(&mailbox_nios2hps_action, 0, sizeof(struct sigaction)); mailbox_nios2hps_action.sa_sigaction = mailbox_nios2hps_signal_handler; mailbox_nios2hps_action.sa_flags = SA_SIGINFO | SA_NODEFER; sigaction(NIOS_MAILBOX_REALTIME_SIGNO, &mailbox_nios2hps_action, &backup_action); if(ioctl(mailbox_nios2hps, IOCTL_SET_PID, &pid)) { cerr << "Failed IOCTL_SET_PID" << endl; close(mailbox_nios2hps); sigaction(NIOS_MAILBOX_REALTIME_SIGNO, &backup_action, NULL); exit(1); } mailbox_hps2nios = open("/dev/nios_mailbox_1", (O_WRONLY | O_SYNC)); if(mailbox_hps2nios < 0) { cerr << "Could not open "/dev/nios_mailbox_1"..." << endl; close(mailbox_nios2hps); sigaction(NIOS_MAILBOX_REALTIME_SIGNO, &backup_action, NULL); exit(1); } pthread_create(&nios2hps_data_reader_thread, NULL, mailbox_nios2hps_data_reader, NULL); }
- Парсер консольных команд.
Осталось собрать проект. В результате получим исполняемый файл для архитектуры ARM-9, который скопируем на систему. Если все сделано правильно, то после запуска в консоли появится сообщение «Enter command (»read"(«r»), «write»(«w»), «reverse»), «q» to exit".
Запуск и проверка системы
Инсталляцию модуля ядра добавляем в автозагрузку Linux.
Соберем новую версию прошивки NIOS, убрав из программы весь отладочный вывод в JTAG. Преобразуем прошивку в hex формат запуском в SoC EDS 15.1 Command Shell команды «elf2hex --input=soc_nios.elf --output=soc_nios.hex --width=32 --base=0x4000 --end=0x7fff --record=4». Полученную прошивку нужно добавить как файл инициализации для памяти NIOS в Qsys, затем пересобрать Qsys, пересобрать проект ПЛИС и записать новую прошивку на карту памяти.
Загружаемся и сразу запускаем тестовое приложение. И если все было сделано правильно, то получаем рабочую систему.
Выводы
Не бойтесь использовать такие сложные связки как ПЛИС-МК-ПК на основе SoC в своих проектах. В данной статье продемонстрировано, что реализовать такую систему не так уж и сложно. Можно даже добавить несколько микроконтроллеров и связать их вместе подобным образом.
Система управления, созданная на базе изложенных выше принципов, была внедрена автором в один из электронных приборов и доказала свою работоспособность в реальном мире.
Автор: maze009