Запускаем .NET nanoFramework на Raspberry Pi Pico

image

Платформа .NET nanoFramework позволяет разрабатывать приложения на C# для различных микроконтроллеров. В предыдущей публикации работали с ESP32 и STM32. Одна из замечательных особенностей .NET nanoFramework заключается в возможности запускать среду исполнения поверх интерфейса POSIX в Win32 для Unit-тестирования. Это означает быструю возможность переноса среды nanoFramework Runtime на любую операционную систему поддерживаемую POSIX стандартом. Именно таким образом, в качестве эксперимента, .NET nanoFramework был перенесен на микроконтроллер Raspberry Pi Pico, для запуска поверх операционной системы реального времени (RTOS) Apache NuttX. Как это было реализовано прошу под кат.
Что такое .NET nanoFramework можно почитать пост платформа для разработки приложений на C# для микроконтроллеров. Для начала рассмотрим что из себя представляет Raspberry Pi Pico.


Raspberry Pi Pico — это недорогая платформа для разработки на микроконтроллере RP2040. Два ядра ARM Cortex-M0+ с тактовой частотой 133 МГц обеспечат необходимую производительность для ваших устройств, роботов и других изобретений, где важен баланс производительности с низким энергопотреблением.

Raspberry Pi Pico nanoFramework
Плата Raspberry Pi Pico

Микроконтроллер Pico входит в аналогичный сегмент микроконтроллеров, что и ESP32, но обладает своими уникальными «фишками». Уникальная возможность Pico — подсистема программируемого ввода/вывода (Programmable I/O), с помощью которой можно реализовать произвольный аппаратный интерфейс: например, шину SD-карты или вывод VGA. Для подключения модулей предусмотрены аппаратные интерфейсы UART, SPI и I2C.

На плате присутствует чип Flash памяти на 2 Mб, кнопка BOOTSEL для перевода микроконтроллера в режим загрузчика, светодиод на 25 контакте и стабилизатор напряжения, позволяющий питать Pico от источника питания с напряжением от 1.8 до 5.5 В. Плата Raspberry Pi Pico питается через разъём micro-USB или контакт VSYS.

Для создания приложений используется язык Си, C++ или MicroPython.


NuttX_logo.png
NuttX — это операционная система реального времени (RTOS), специально разработанная для использования во встраиваемых системах с микроконтроллерами или процессорами разрядностью от 8 до 32 бит. Благодаря гибкой возможности настройки NuttX, разработчик может включать в образ только те модули, которые действительно обходимыми для проекта. Одним из главный принципов NuttX является соответствие стандартам POSIX и ANSI. Благодаря этому определяется общий интерфейс для различных операционных систем, что способствует переносимости, повторному использованию кода и поддержки приложений, использующих POSIX и ANSI. Дополнительные стандартные API из Unix и других RTOS (такая как VxWorks) адаптированы для функциональности, недоступны в соответствие со стандартами, и низкоуровневым окружением для встраиваемых систем (как fork ()).

Список поддерживаемых платформ достаточно большой, в него входят процессоры ARM Cortex, Atmel AVR, Intel, RISC-V, STM32, и т.д. Среди поддерживаемых МК присутствует серия ESP32 от Espressif. Сама компания Espressif активно инвестирует в проект NuttX.

NuttX распространяется под свободной лицензией Apache 2.0 License.


Образ NuttX для RP2040 поддерживает интерфейсы UART, I2C, SPI, PIO (RP2040 Programmable I/O), и т.д. Так же в зависимости от подключаемых модулей будут доступны LCD экраны: ssd1306 (I2C), lcd1602 (I2C), st7735 (SPI). Поддерживается сеть Ethernet (модуль ENC28J60 на SPI).

Более детально о поддержке МК RP2040 можно ознакомится на странице incubator-nuttx/raspberrypi-pico.

Соберем минимальный образ для RP2040 с NuttShell в Ubuntu 20.04, задаваемый параметр для сборки — usbnsh. NuttShell это аналог консоли как в Linux. Параметр usbnsh задействует порт USB CDC/ACM для работы с NuttShell.

Скрипт сборки:

$ sudo apt-get update
$ sudo apt-get install -y git
$ sudo apt-get install -y \
bison flex gettext texinfo libncurses5-dev libncursesw5-dev \
gperf automake libtool pkg-config build-essential gperf genromfs \
libgmp-dev libmpc-dev libmpfr-dev libisl-dev binutils-dev libelf-dev \
libexpat-dev gcc-multilib g++-multilib picocom u-boot-tools util-linux
$ sudo apt-get install -y kconfig-frontends
$ sudo apt-get install -y gcc-arm-none-eabi binutils-arm-none-eabi
$ sudo reboot now
$ mkdir -p /usr/share/pico
$ cd /usr/share/pico
$ git clone https://github.com/raspberrypi/pico-sdk
$ cd /usr/share/pico/pico-sdk
$ git reset --hard 26653ea81e340
$ export PICO_SDK_PATH=/usr/share/pico/pico-sdk
$ mkdir -p /usr/share/pico/nuttxspace
$ cd /usr/share/pico/nuttxspace
$ git clone https://github.com/apache/incubator-nuttx.git nuttx
$ git clone https://github.com/apache/incubator-nuttx-apps apps
$ cd /usr/share/pico/nuttxspace/nuttx
$ ./tools/configure.sh -l raspberrypi-pico:usbnsh
$ make -j


Более подробно процесс установки Getting Started » Installing.

После компиляции по пути /usr/share/pico/nuttxspace/nuttx появится файл nuttx.uf2.

Для загрузки прошивки в PiPico, необходимо на плате нажать кнопку BOOTSEL и подключить плату по USB порту.

Прошивка Pi Pico
bootFlash Raspberry Pi Pico


И в операционной системе появится USB-диск:

Raspberry Pi Pico USB Flash
USB-диск в Windows 7 для загрузки прошивки Raspberry Pi Pico

Копируем в корень диска F:\ файл nuttx.uf2. После копирования файла, плата автоматически прошьет свою Flash-память и перегрузится.

С списке устройств отобразится COM-устройство, в данном случае COM8. Обратите внимание, что для Windows 7 штатный драйвер CDC/ACM отсутствует. Драйвер доступен только для Windows 10+ и Linux. В Linux консоль PiPico появится как устройство ttyACMx, например /dev/ttyACM0.

Открываем порт на скорости 115200 бод. Для доступа к PiPico совершаем быстрое двойное нажатие на Enter, и вводим несколько команд.

Raspberry Pi Pico nuttx
NuttShell консоль ОС NuttX на Raspberry Pi Pico

NuttX успешно запущен и работает на Raspberry Pi Pico!


.NET nanoFramework может работать с невероятными минимальными требованиями, для запуска необходимо всего 256 КБ флэш-памяти и 64 КБ ОЗУ. Pi Pico удовлетворяет этим требования более чем необходимо, сам чип RP2040 содержит 264 КБ ОЗУ, на самой плате разведена Flash-память на 2 МБ.

Для Unit-тестирования приложений для .NET nanoFramework разработчики обеспечили запуск nanoCLR в контексте Win32 благодаря соответствие POSIX стандарту.

nanoFramework Win32
Запуск .NET nanoFramework Win32 для тестирования

Matheus Castello пришла в голову замечательная идея, раз nanoCLR работает поверх RTOS. А разработчики NuttX объявили о поддержке Raspberry Pi Pico, то почему бы не запустить nanoFramework на NuttX одной из RTOS, вместо трудоемкого нативного портирования nanoCLR. По факту так таковой перенос, как переписывание уровня HAL под определенную аппаратную платформу не требуется, необходимо только обеспечить соответствие вызываемым функциям в рамках POSIX стандарта.

Порт nanoFramework для win32 запускается как консольное приложение. На самом деле запуск nanoCLR на Raspberry Pi Pico был второстепенной задачей, основная задача заключалась в запуске nanoCLR в окружение Linux. В процессе миграции Matheus Castello переписал части, написанные для win32, в переносимые стандартные вызовы C/C++ и вызовы POSIX. Так же были добавлены директивы препроцессора #if defined (__linux__), чтобы охватить некоторые конкретные случаи для приложения Linux. В некоторых блоках кода можно найти что-то вроде #if defined (_WIN32) || defined (__linux__):

// provide platform dependent delay to CLR code
#if defined(_WIN32) || defined(__linux__) || defined(__nuttx__)
#define OS_DELAY(milliSecs) ;
#else
#define OS_DELAY(milliSecs) PLATFORM_DELAY(milliSecs)
#endif


Ядро nanoCLR достаточно переносимо, но в некоторых случаях приходится выполнять тот или иной платформозависимый вызов. В этом случае вызов в nanoCLR будет реализован в коде, зависящем от платформы. Поскольку использовался проект win32 в качестве основы, то использовались те же define вызовы.

Порт для POSIX доступен по ссылке dotnuttx/nf-Community-Targets/tree/linux/posix.

После запуска nanoCLR в Linux, для связывания бинарника NuttX с приложением в образ сборки были добавлены файлы Kconfig, Make.defs и Makefile (Commit: a5e837c).

В файл Makefile были добавлены относительные пути к исходному коду локального форка nf-interpreter и необходимые флаги/директивы препроцессора для компилятора. В конфигурацию NuttX для компиляции ядра были добавлены следующие настройки для обеспечения поддержки C++:

# C++
CONFIG_HAVE_CXX=y
CONFIG_LIBCXX=y


Интересная проблема которая возникла, заключалась в использование различных заголовочных файлов, которые должны работать и вести себя одинаково как Linux, так и в NuttX. Но в NuttX некоторые include, не «подготовлены» для использования в C++. Пример использования utsname.h для получения информации о системе:

#if defined(__linux__) || defined(__nuttx__) 
#include 
#endif


Если мы проверим header в Linux, то увидим, что в самом начале файла будет следующий фрагмент:

#ifndef    _SYS_UTSNAME_H
#define    _SYS_UTSNAME_H    1

#include 

__BEGIN_DECLS


Где __BEGIN_DECLS развертывается в:

/* C++ needs to know that types and declarations are C, not C++.  */
#ifdef    __cplusplus
# define __BEGIN_DECLS    extern "C" {
# define __END_DECLS    }
#else
# define __BEGIN_DECLS
# define __END_DECLS
#endif


В NuttX utsname.h нет блока __cplusplus на случай использования C++, что приводит к неопределенным ошибкам ссылок во время сборки. Для решения этой проблемы был добавлен код:

#if defined(__linux__)
#include 
#endif

#if defined(__nuttx__)
extern "C" {
#include 
}
#endif


Теперь можно запустить .NET nanoFramework на Raspberry Pi Pico, благодаря поддержки платы в проекте NuttX.
В NuttX не предоставляются GPIO во время исполнения программы, а предварительно конфигурируются во время сборки (над этим еще ведется работа, чтобы изменить). На данный момент для использования доступны только следующие контакты GPIO:

GP25 = GpioPinDriveMode_Output;
GP2 = GpioPinDriveMode_Output;
GP3 = GpioPinDriveMode_Output;
GP4 = GpioPinDriveMode_Output;
GP5 = GpioPinDriveMode_Output;

GP6 = GpioPinDriveMode_Input;
GP7 = GpioPinDriveMode_Input;
GP8 = GpioPinDriveMode_Input;
GP9 = GpioPinDriveMode_Input;

Демонстрационное приложение будет мигать светодиодом (GP25 — onboard LED) и выводить в консоль  отладки служебную информацию при нажатии на кнопку GP6 (pin 9).

Raspberry Pi Pico nanoFramework
Схема подключения кнопки к Raspberry Pi Pico

В качестве примера возьмем проект nanoFrameworkPOSIX-samples/PiPico. Файл Program.cs:

using System.Diagnostics;
using System.Threading;
using System.Device.Gpio;
using nanoFramework.Runtime.Native;

GpioController gpioController = new GpioController();
// GP25 (onboard LED)
GpioPin onBoardLED = gpioController.OpenPin(25, PinMode.Output);
// GP6 (pin 9)
GpioPin button = gpioController.OpenPin(6, PinMode.Input);

while (true)
{
    // blink
    onBoardLED.Toggle();

    // check if button is pressed
    if (button.Read() == PinValue.High)
    {
        Debug.WriteLine($"Running nanoFramework on {SystemInfo.TargetName}");
        Debug.WriteLine($"Platform: {SystemInfo.Platform}");
        Debug.WriteLine($"Firmware info: {SystemInfo.OEMString}");        
    }

    Thread.Sleep(500);
}


Выполним сборку в VS2019, меню Build > Build PiPico. В папке \PiPico\bin\Debug потребуются только файлы с расширением .pe, а именно:

  • mscorlib.pe
  • nanoFramework.Runtime.Events.pe
  • nanoFramework.Runtime.Native.pe
  • PiPico.pe
  • System.Device.Gpio.pe


Приложение для Raspberry Pi Pico можно загрузить двумя путями. Первой, создать файл прошивки .uf2 содержащий NuttX, среду nanoCLR, исполняемое приложение .NET, и остальные модули. Второй, RP2040 может загружать программу с SD-карты памяти посредством интерфейса SPI.

Создадим единый бинарник прошивки, по первому варианту. Для создания прошивки воспользуемся готовым docker-контейнером.

Загрузим образ dotnuttx/generate-pico-uf2, создадим volume nf-debug и скопируем в него файлы .pe полученные при сборке проекта.

$ docker pull dotnuttx/generate-pico-uf2:latest
$ docker volume create --name nf-debug


В каталоге /var/lib/docker/volumes/nf-debug/_data должны быть следующие файлы:

root@ubuntuvm:~# ls -l /var/lib/docker/volumes/nf-debug/_data
total 52
-rw-r--r-- 1 root root 31668 Jun 19  2021 mscorlib.pe
-rw-r--r-- 1 root root  3412 Jun 19  2021 nanoFramework.Runtime.Events.pe
-rw-r--r-- 1 root root  1496 Jun 19  2021 nanoFramework.Runtime.Native.pe
-rw-r--r-- 1 root root   772 May 23  2022 PiPico.pe
-rw-r--r-- 1 root root  5684 Jun 19  2021 System.Device.Gpio.pe


Теперь создадим контейнер и запустим компиляцию прошивки:

$ docker run --rm -it -v nf-debug:/nf dotnuttx/generate-pico-uf2


По итогу в папке /var/lib/docker/volumes/nf-debug/_data появится файл прошивки dotnetnf.uf2.

Для загрузки прошивки в PiPico, необходимо нажать кнопку BOOTSEL и подключить плату по USB порту, как делали выше при прошивки RTOS NuttX. Копируем в корень USB-диска файл dotnetnf.uf2. После копирования файла плата автоматически прошьет свою Flash-память и перегрузится.

Raspberry Pi Pico nanoFramework
Работа приложения nanoFramework в NuttX

Увидим мигание светодиода, .NET nanoFramework успешно запущен на Raspberry Pi Pico!


Второй вариант немного сложнее, но более универсальный. В первом варианте, при любом изменении программного кода необходимо заново компилировать целиком NuttX вместе с приложением, что несколько неудобно. Вариант с SD-картой подразумевает однократную загрузку в Pi Pico исполнительной среды nanoFramework Runtime, с последующим запуском приложения с SD-карты. На SD-карту копируются файлы с расширением *.pe. Соответственно изменение приложения не затрагивает перепрошивку платы. Дополнительно для реализации данного варианта потребуется модуль чтения SD-карт на SPI интерфейсе.

Для начала загрузим nanoFramework Runtime в Raspberry Pi Pico, для того скачаем файл dotnet-nf.rp2040-Nuttx.2646 со странице POSIX nanoFramework, и скопируем его на плату в режиме прошивки (подключение при нажатой кнопки BOOTSEL).

Теперь подключим модуль SD-карт памяти как указано на схеме:

Raspberry Pi Pico nanoFramework
Схема подключения модуля SD-карт к Raspberry Pi Pico

Raspberry Pi Pico nanoFramework
Raspberry Pi Pico на макетной плате

Таблица подключения:

      SD card slot   Raspberry Pi Pico
       DAT2          (NC)
       DAT3/CS ----- GP17 (SPI0 CSn)        (Pin 22)
       CMD /DI ----- GP19 (SPI0 TX - MOSI)  (Pin 25)
       VDD     ----- 3V3 OUT                (Pin 36)
       CLK/SCK ----- GP18 (SPI0 SCK)        (Pin 24)
       VSS     ----- GND                    (Pin 3 or 38 or ...)
       DAT0/DO ----- GP16 (SPI0 RX - MISO)  (Pin 21)
       DAT1          (NC)


Заменим содержимое Program.cs из примера выше, на следующий код:

using System.Threading;
using System.Device.Gpio;

GpioController gpioController = new GpioController();
// GP25 (onboard LED)
GpioPin onBoardLED = gpioController.OpenPin(25, PinMode.Output);
// GP6 (pin 9)
GpioPin button = gpioController.OpenPin(6, PinMode.Input);

while (true)
{
    // check if button is pressed
    if (button.Read() == PinValue.Low)
    {
        onBoardLED.Write(PinValue.High);
    }
    else
    {
        onBoardLED.Write(PinValue.Low);
    }

    Thread.Sleep(200);
}


Несмотря на работу консоли NuttShell через COM-порт, вывод сообщений используя метод Debug.WriteLine () не заработал, поэтому поведение программы было изменено. При нажатие на кнопку GP6 (pin 9) загорается светодиод GP25 (onboard LED) на плате, при отпускание выключается.

Далее, SD-карту памяти (желательно брать минимального размера, карта объемом 8 Гб подошла) необходимо отформатировать в FAT32 и скопировать в корень сборки, файлы *.pe (mscorlib.pe, nanoFramework.Runtime.Events.pe, nanoFramework.Runtime.Native.pe, PiPico.pe, System.Device.Gpio.pe).

Вставляем SD-карту памяти со сборками в адаптер и подключаем Pi Pico по USB интерфейсу. Запускаем терминал, открываем порт COM8, на нажимаем на кнопку.

Raspberry Pi Pico nanoFramework
Работа приложения nanoFramework в NuttX

Если вам интересно более детально узнать какие изменения были внесены для возможности запуска nanoFramework в NuttX и как это работает, пишите в комментариях. Благодарю за внимание.

coe2kha8u8_pypip-2k3wk3ppa0.png

© Habrahabr.ru