Платформа .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
Raspberry Pi Pico — это недорогая платформа для разработки на микроконтроллере RP2040. Два ядра ARM Cortex-M0+ с тактовой частотой 133 МГц обеспечат необходимую производительность для ваших устройств, роботов и других изобретений, где важен баланс производительности с низким энергопотреблением.
Плата Raspberry Pi Pico
Микроконтроллер Pico входит в аналогичный сегмент микроконтроллеров, что и ESP32, но обладает своими уникальными «фишками». Уникальная возможность Pico — подсистема программируемого ввода/вывода (Programmable I/O), с помощью которой можно реализовать произвольный аппаратный интерфейс: например, шину SD-карты или вывод VGA. Для подключения модулей предусмотрены аппаратные интерфейсы UART, SPI и I2C.
На плате присутствует чип Flash памяти на 2 Mб, кнопка BOOTSEL для перевода микроконтроллера в режим загрузчика, светодиод на 25 контакте и стабилизатор напряжения, позволяющий питать PiPico от источника питания с напряжением от 1.8 до 5.5 В. Плата PiPico питается через разъём micro-USB или контакт VSYS.
Для PiPico создаются приложения на языках Си, C++ или MicroPython.
Что такое Apache NuttX?
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 для Raspberry Pi Pico
Образ 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 порту.
В операционной системе появится USB-диск:
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, и вводим несколько команд.
NuttShell консоль ОС NuttX на Raspberry Pi Pico
NuttX успешно запущен и работает на Raspberry Pi Pico!
Запуск .NET nanoFramework -> NuttX
.NET nanoFramework может работать с невероятными минимальными требованиями, для запуска необходимо всего 256 КБ флэш-памяти и 64 КБ ОЗУ. PiPico удовлетворяет этим требованиям более чем необходимо, сам чип RP2040 содержит 264 КБ ОЗУ, на самой плате разведена Flash-память на 2 МБ.
Для Unit-тестирования приложений для .NET nanoFramework разработчики обеспечили запуск nanoCLR в контексте Win32 благодаря соответствию POSIX стандарту.
Запуск .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 <sys/utsname.h>
#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 <sys/utsname.h>
#endif
#if defined(__nuttx__)
extern "C" {
#include <sys/utsname.h>
}
#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
В качестве примера возьмем проект 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
Создание прошивки NuttX вместе с приложением
Приложение для 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-память и перегрузится.
Работа приложения nanoFramework в NuttX
Увидим мигание светодиода, .NET nanoFramework успешно работает на Raspberry Pi Pico!
Запуск приложения используя SD-карту памяти
Второй вариант немного сложнее, но более универсальный. В первом варианте, при любом изменении программного кода необходимо заново компилировать целиком NuttX вместе с приложением, что несколько неудобно. Вариант с SD-картой подразумевает однократную загрузку в PiPico исполнительной среды nanoFramework Runtime, с последующим запуском приложения с SD-карты. На SD-карту копируются файлы с расширением *.pe. Соответственно изменение приложения не затрагивает перепрошивку платы. Дополнительно для реализации данного варианта потребуется модуль чтения SD-карт на SPI интерфейсе.
Для начала загрузим nanoFramework Runtime в Raspberry Pi Pico, для того скачаем файл dotnet-nf.rp2040-Nuttx.2646 со странице POSIX nanoFramework, и скопируем его на плату в режиме прошивки (подключение при нажатой кнопки BOOTSEL).
Теперь подключим модуль SD-карт памяти как указано на схеме:
Схема подключения модуля SD-карт к Raspberry Pi Pico
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-карту памяти со сборками в адаптер и подключаем PiPico по USB интерфейсу. Запускаем терминал, открываем порт COM8, и нажимаем на кнопку.
Работа приложения nanoFramework в NuttX
Если вам интересно более детально узнать какие изменения были внесены для возможности запуска nanoFramework в NuttX и как это работает, пишите в комментариях. Благодарю за внимание.
Ресурсы
- Running .NET on Raspberry Pi Pico — Matheus Castello
- Porting .NET nanoFramework for Linux and NuttX — Matheus Castello
- Apache NuttX — GitHub
- NuttX Documentation — WiKi
- incubator-nuttx raspberrypi-pico — GitHub
- dotnuttx/nanoFrameworkPOSIX-samples — GitHub
- POSIX nanoFramework v2.7.0.3 — GitHub