Реализация связки прошивки ПЛИС, ПО микроконтроллера 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%x\n\r", 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%08x\n", (uint32_t)mes, (uint32_t)(mes >> 32));
        switch ((uint32_t)mes) {
          case LED_NUMBER:
            printf("Active led %lu\n", (uint32_t)(mes >> 32));
            break;
          case SWITCH_COUNT:
            printf("Led switched %lu times\n", (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%08x\n", (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 в своих проектах. В данной статье продемонстрировано, что реализовать такую систему не так уж и сложно. Можно даже добавить несколько микроконтроллеров и связать их вместе подобным образом.

Система управления, созданная на базе изложенных выше принципов, была внедрена автором в один из электронных приборов и доказала свою работоспособность в реальном мире.

Комментарии (4)


  1. igor_suhorukov
    18.04.2018 09:35
    -1

    Вопрос такого подхода в цене разработке, качестве результата и времени.

    И есть задачи, которые стоит решать уже на уровне ПЛИС, так как никакого микроконтроллера может не хватить для грамотного параллельного управления разной высокочастотной электроникой, например, драйверами шаговых приводов с обработкой данных энкодеров и регуляторами скоростей. В таких задачах процессор бывает просто лишним.

    Вряд ли кому-либо удастся быстро и качественно повторить функционал LinuxCNC.
    В проекте MachineKit эта задача решается за счет real time ядра linux и программ для сопроцессоров Programmable Real-time Unit (PRU) на чипе AM335x.


  1. jok40
    18.04.2018 13:16

    Не совсем понятна необходимость в NIOS-процессоре в середине цепочки. HPS может взаимодействовать с FPGA напрямую и программу в нём можно дебажить без проблем.


  1. Fandir
    18.04.2018 17:49
    +1

    Аналогичный вопрос нафига юзать ущербный NIOS, если вы используете SoC систему, где целых 2 900 МГц АРМа на которых вполне можно запустить Linux с GUI, если вам АРМ не нужен нафига было брать систему с SoC она как бы не бюджетная)


  1. Mogwaika
    19.04.2018 16:36

    А из линукса с плисой можно как-нибудь проще общаться?
    Поднять какой-нибудь spi с двух сторон и общаться через него хоть через скрипты на питоне и чтобы готовые драйверы использовать?