image

В своей жизни я обожаю как минимум три вещи: это C# (как и .NET в целом), интересное железо и одноплатные компьютеры. В Embedded-системах на Linux обычно принято писать код на C/C++ для решения чувствительных к производительности задач и интерпретируемых Lua/Python для быстрого прототипирования, которые стали популярны в встраиваемых устройствах сравнительно недавно. Однако о нативной разработке под одноплатники на C# практически ничего не слышно и я решил исправить это недоразумение! В сегодняшнем материале: рассмотрим, какие платформы .NET нам доступны на одноплатниках, научимся работать с GPIO и SPI в юзерспейсе, а также напишем практическое приложение, которое реализовывает драйвер дисплея и выводит на экран определенное изображение.

Предисловие


Одноплатники уже давно вошли в повседневную жизнь многих DIY-щиков, сисадминов и людей, которые интересуются мини-компьютерами. Казалось бы, одну и ту же задачу можно решить несколькими методами на самых разных языках: кто-то предпочитает писать нативный код на тех же плюсах, а особо прожженные — на Plain-C и ассемблере, стараясь получить максимальную производительность, а кто-то хочет сразу перейти к реализации своего устройства не заморачиваясь с подробным изучением того, как чип работает «под капотом» и какие шины существуют, ограничиваясь использованием готовых библиотек.

image

Но я лично очень люблю C# за его максимальную гибкость, позволяющую оптимизировать некоторые обращения к памяти путем получения прямых указателей на данные, умеет в удобные темплейты, а также имеет механизм для маршаллинга (прямой импорт функций из библиотек, возможность создать нативный трамплин на управляемый делегат, возможность быстрого копирования из unmanaged в managed окружение и т. п.). Потому всегда думал: почему бы его не использовать в своих embedded-проектах на базе одноплатников?

image

Сейчас .NET можно накатить на большинство современных одноплатников, за исключением самых слабых с 64Мб ОЗУ «бутербродом» на чипе (AllWinner F1C100s, AllWinner V3s, некоторые MStar и т. п.). Доступно два рантайма, которые предлагают разные профили и соответственно, разный функционал.

  • dotnet — официальный рантайм, который реализует профиль .NET Core (ой, простите, так уже не модно, теперь это просто .NET). Предоставляет весь современный базовый функционал дотнета вкупе с современными версиями самого C#, но в нём нет, например, Windows Forms для UI (если вы используете полноценные «иксы» и GTK), и System.Drawing для обработки графики и отрисовки текста. Это эталонная реализация дотнета и его можно без проблем накатить на любой одноплатник, для которого есть достаточно свежий Linux.
  • Mono — альтернативная реализация .NET Framework для Linux, ранее активно использовалась в Unity. В отличии от .NET Core, может работать и на более старых одноплатниках на прошлых версиях дистрибутивов Linux, в том числе и самой первой Raspberry Pi. Считается более медленной, чем dotnet, зато имеет значительно большую функциональность, почти идентичную фреймворку на Windows.

В сегодняшней статье мы будем писать программу на C# для OrangePi One, которая должна инициализировать дисплей из юзерспейса и выводить на него определенные данные. В качестве профиля используем .NET Framework 4 (да, я порой старомоден), а одноплатником выступит OrangePi One в стоковой конфигурации ядра, без правок devicetree, где по умолчанию у нас доступен spidev без аппаратных чипселектов, доступ к GPIO из /sys/ и i2cdev.

Настраиваем окружение


Для начала нам нужен образ системы для нашего одноплатника. Какой — выбирать вам. Для большинства устройств на чипсетах AllWinner доступны образы с ядром 3.x, которые более стабильны, но не используют devicetree и не входят в мейнлайн и 5.x, так называемый мейнлайн, но там всё ещё есть некоторые нюансы. Я выбрал Ubuntu Xenial с ядром 5.3.5.

image

Теперь самое время накатить рантайм, что мы и делаем командой:

apt-get install mono-all

Обратите внимание, Mono громоздкий и с учетом всех зависимостей может устанавливаться минут 30, если у вас достаточно медленная флэшка. Всё, теперь устройство готово к запуску программ на дотнете, нашу программу можно запустить следующей командой:

mono assembly.exe

Давайте же перейдём к фактической реализации нашей программы и узнаем как работать с периферией устройства!

GPIO


Начинаем с GPIO или «ногодрыга». В Linux есть удобный интерфейс, позволяющий экспортировать пины общего назначения в юзерспейс и рулить ими прямо из sysfs, в том числе и из терминала! Для реализации софтварного SPI или быстрого опроса цифровых пинов такой способ не подойдет — слишком большой оверхед, но для моргания светодиодами, обработки кнопок или… программного ногодрыга чипселектом — вполне подойдет :)

image

Как я и говорил выше, GPIO сначала нужно сделать видимым в sysfs — т. е. экспортировать, путём записи номера нужного пина в «файл» /sys/class/gpio/export. Посчитать ID нужного пина можно с помощью простой формулы: (позиция буквы в алфавите — 1) * 32 + номер пина. То есть, для PA10 ID будет 10. При ошибке, системный вызов close выбросит ошибку, а поток в C# — IOException.

    public static bool Export(int id)
        {
            try
            {
                File.WriteAllText("/sys/class/gpio/export", id.ToString());
                Log.WriteLine("Exported GPIO {0}", id);

               return true;
            }
            catch (IOException e)
            {
                Log.WriteLine("Failed to export GPIO, assuming they are already exported");

               return false;
            }
        }

После этого, по пути /sys/class/gpio/gpio10/ появится директория с файлами direction, куда нужно записать направление нашего пина («in» — ввод, «out» — вывод) и value, куда мы будем записывать или читать значение пина. Реализовать управление пином можно так:

    public GPIO(int id, bool isOutput)
        {
            Mode = isOutput;

            basePath = string.Format("/sys/class/gpio/gpio{0}/", id);

            if (!Directory.Exists(basePath))
                throw new ArgumentException("GPIO not available");

            File.WriteAllText(basePath + "direction", Mode ? "out" : "in");
        }

        public bool ReadValue()
        {
            return File.ReadAllText(basePath + "value") == "1";
        }

        public void SetValue(bool val)
        {
            state = val;

            File.WriteAllText(basePath + "value", val ? "1" : "0");
        }

Да, всё так просто! Мигалка светодиодом в нашем случае будет выглядеть так:

    GPIO.Export(10);
    GPIO gpio = new GPIO(10, true);

    while(true)
    {
        gpio.SetValue(true);
        Thread.Sleep(1000);
        gpio.SetValue(false);
        Thread.Sleep(1000);
    }

Переходим к чему посложнее, а именно к SPI из всё того-же юзерспейса!

SPI


Для управления SPI нам потребуется вызов ioctl, который позволяет отправлять устройству различные пакеты с описанием команд. Для этого нам пригодится PInvoke:

    internal sealed class NativeCalls
    {
        public struct SPIDevIOCTransfer
        {
            public ulong txBufPointer;
            public ulong rxBufPointer;

            public uint length;
            public uint speedHz;

            public ushort delayUsecs;
            public byte bitsPerWord;
            public byte csChange;

            public uint pad;
        }

        [DllImport("libc", EntryPoint = "ioctl", CallingConvention = CallingConvention.Cdecl, SetLastError = true)]
        public static extern int ioctl(IntPtr handle, uint request, IntPtr data);
    }

Для каждой аппаратной шины SPI создаётся одно устройство spidev. В случае OrangePi One, по умолчанию экспортирована только одна шина (поскольку и SPI-контроллер на гребенке лишь один) — spidev0.0. Для начала открываем наше устройство для записи:

        public SPIBus(string debugName)
        {
            DebugName = debugName;
            
            device = File.OpenWrite("/dev/spidev0.0");
        }

Драйвер spidev работает по принципу транзакций — вы посылаете IOCTL с запросом SPI_IOC_MESSAGE (в оригинале это макрос с возможностью послать сразу несколько транзакций в драйвер) и указателем на структуру spi_ioc_transfer с описанием отправляемых или получаемых данных, а драйвер уже сам решает что и когда отправить, при этом вызов ioctl — блокирующий, то есть управление в поток вернется только когда драйвер завершит работу. Но есть нюанс — драйвер SPI у чипсетов AllWinner не может отправлять более 128-байт (на AllWinner A10/A13 — 64-байт) данных за транзакцию, поэтому большой массив данных придётся разбивать на несколько мелких:

        public unsafe bool Transfer(byte[] data)
        {
            NativeCalls.SPIDevIOCTransfer transferDesc = new NativeCalls.SPIDevIOCTransfer();

            fixed (void* ptr = &data[0]) // Запрещаем GC перемещать массив data и получаем указатель на первый элемент
            {
                int numTrans = data.Length / 128;
                bool isSucceded = true;
                
                // Разбиваем данные на несколько транзакций
                for(int i = 0; i < numTrans; i++)
                {
                    transferDesc.txBufPointer = ((ulong)ptr) + ((ulong)(i * 128)); // Считаем смещение в нашем массиве байтов в буфере для отправки
                    transferDesc.bitsPerWord = 8; // Указываем число битов в одном машинном слове (8 для AllWinner)
                    transferDesc.length = 128; // Указываем длину транзакции
                    transferDesc.speedHz = DesiredFrequency; // Указываем желаемую максимальную скорость транзакции. Это не значит, что SPI-контроллер именно с такой скоростью будет отправлять и получать данные.
                    if (i == numTrans - 1)
                        transferDesc.length = (uint)(data.Length - (i * 128)); // Иногда размер последней транзакции не кратен всему массиву данных и длину нужно обрезать - иначе ioctl вылезет за границы массива и это приведет к SEGFAULT.

                    isSucceded = NativeCalls.ioctl(device.SafeFileHandle.DangerousGetHandle(), IOCTLMessage, new IntPtr(&transferDesc)) >= 0; // Наконец-то, отправляем транзакцию в драйвер. DangerousGetHandle получает десктриптор файла в Linux.

                    if (!isSucceded)
                        break;
                }

                return isSucceded;
            }
        }

Уже в шоке от обилия указателей в коде на шарпе? :) Надеюсь, комментарии помогут вам разобраться.

Тоже самое и для чтения данных с шины, только вместо txBufPointer — rxBufPointer.

        public unsafe void Receive(byte[] data, int length)
        {
            NativeCalls.SPIDevIOCTransfer transferDesc = new NativeCalls.SPIDevIOCTransfer();

            fixed(byte* ptr = &data[0])
            {
                transferDesc.rxBufPointer = (ulong)ptr;
                transferDesc.bitsPerWord = 8;
                transferDesc.length = (uint)length;
                transferDesc.speedHz = DesiredFrequency;

                if (NativeCalls.ioctl(device.SafeFileHandle.DangerousGetHandle(), IOCTLMessage, new IntPtr(&transferDesc)) < 0)
                    throw new ArgumentException("Receive failed");
            }
        }

Пример работы прост до безобразия:

    SPI spi = new SPI("test");
    spi.Transfer(0x04);
    spi.Receive(id, id.Length);
    Console.WriteLine("{0} {1} {2} {3}", id[0], id[1], id[2], id[3]);

Имея GPIO и SPI уже можно переходить к реализации чего-то более конкретного!

Дисплей


В качестве дисплея я буду использовать стандартную дешёвую 2.4" матрицу с разрешением 240x320 и контроллером ST7789 с интерфейсом SPI. Для использования дисплея с питанием 3.3В нужно поставить перемычку на позиции J1.

image

Для подключения такого дисплея, достаточно всего лишь 4 (5, если нужен чипселект) сигнальные линии на 40-пиновой гребенке RPi One, плюс один для ШИМ (если нужно регулировать подсветку) и два на питание. Обратите внимание, что лучше сдуть гребенку и паяться к одноплатнику напрямую — у меня из-за китайских дюпонтов постоянно помехи на дисплее и мусор на шине.

Схема подключения:
VCC -> 3.3V
GND -> Масса
CS -> PA9
RESET — PA10
D/C — PA20
MOSI — PC0
SCK — PC2
LED -> 3.3V

image

Начинаем с подготовки необходимых GPIO. Для управления дисплеем всегда нужен аппаратный RESET и D/C (бит команда/данные). Чипселект необязателен (его можно кинуть на массу), если это будет единственное устройство на шине, однако в случае ST7789 почему-то в таком случае нужно использовать SPI MODE 3.

        private void PrepareGPIO()
        {
            const int DC = 10, Reset = 20, CS = 9;

            GPIO.Export(DC);
            GPIO.Export(Reset);
            GPIO.Export(CS);

            gpioChipSelect = new GPIO(CS, true);
            gpioDataCommand = new GPIO(DC, true);
            gpioReset = new GPIO(Reset, true);

            gpioChipSelect.SetValue(true);
        }

Переходим к реализации коммуникации с дисплеем. Здесь всё просто — ставим CS в низкий уровень, начиная транзакцию, устанавливаем D/C в низкий уровень в случае команды, либо высокий в случае данных и отправляем байт контроллеру, после чего устанавливаем чипселект обратно в высокий уровень.

        private void SendCommand(byte cmd)
        {
            // Acquire bus for transaction
            gpioChipSelect.SetValue(false);

            gpioDataCommand.SetValue(false);
            Board.SPI.Transfer(cmd);
            
            // Release bus
            gpioChipSelect.SetValue(true);
        }

        private void SendData(byte data)
        {
            gpioChipSelect.SetValue(false);

            gpioDataCommand.SetValue(true);
            Board.SPI.Transfer(data);

            gpioChipSelect.SetValue(true);
        }

Теперь дисплей нужно инициализировать. Здесь нужно сконфигурировать регистры контроллера дисплея для установки режима адресации, цветности и порядка байт в пикселях (BGR или RGB).

            // Reset LCM
            gpioReset.SetValue(false);
            Thread.Sleep(10);
            gpioReset.SetValue(true);

            SendCommand(0x01);   //SWRESET
            SendCommand(0x11);   //SLPOUT
            SendCommand(0x3A);   //COLMOD RGB444(12bit) 0x03, RGB565(16bit) 0x05,
            SendData(0x05);  //RGB666(18bit) 0x06
            SendCommand(0x36);   //MADCTL
            SendData(0x14);  //0x08 B-G-R, 0x14 R-G-B
            SendCommand(0x20);   //INVON
            SendCommand(0x13);   //NORON
            SendCommand(0x29);   //DISPON

Если всё сделано правильно — то после этого вы должны увидеть «мусор» на дисплее, поскольку состояние ОЗУ не определено после подачи питания на контроллер (но при сбросе содержимое DRAM останется на месте).

Теперь нам надо установить границы нашего изображения, в пределах которых работает автоинкермент контроллера дисплея. Нужно это для того, чтобы мы могли, например, пнуть уже готовую картинку в DMA-контроллер и уйти заниматься своими делами, а когда картинка отправилась — установить новые границы и нарисовать что-то ещё. В моём случае, всё рисование производится во второй буфер, который затем рисуется на дисплей — поэтому мне нужны размеры всего дисплея сразу:

            SendCommand(0x2A);   // Set X address
            SendData(0);
            SendData(0);
            SendData((byte)(w >> 8));
            SendData((byte)w);

            SendCommand(0x2B);   // Set Y address
            SendData(0);
            SendData(0);
            SendData((byte)(h >> 8));
            SendData((byte)h);

            SendCommand(0x2C);// Start display

После этого, достаточно лишь непрерывно слать изображение на контроллер дисплея и всё будет работать!

        public unsafe void CopyFrom(Bitmap bitmap)
        {
            gpioChipSelect.SetValue(false);

            gpioDataCommand.SetValue(true);
            if (!Board.SPI.Transfer(bitmap.Data))
                throw new ArgumentException("Failed to copy bitmap on to screen");

            gpioChipSelect.SetValue(true);
        }


Поскольку ни один формат изображений не соответствовал моим требованиям (RGB565, без выравнивания), я быстренько накостылил конвертер в самопальный:

        private static unsafe void WriteBitmap(Bitmap bmp, Stream strm)
        {
            BinaryWriter binWriter = new BinaryWriter(strm);

            // Write header
            binWriter.Write((short)0x1337);
            binWriter.Write((short)bmp.Width);
            binWriter.Write((short)bmp.Height);
            binWriter.Write((short)0); // 0 - RGB565, fixed at this moment

            var data = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format16bppRgb565);
            byte[] pixels = new byte[bmp.Width * bmp.Height * 2];
            byte* ptr = (byte*)data.Scan0.ToPointer();

            // Quick endianness swap
            for(int i = 0; i < bmp.Width * bmp.Height; i++)
            {
                pixels[i * 2] = ptr[i * 2 + 1];
                pixels[i * 2 + 1] = ptr[i * 2];
            }

            bmp.UnlockBits(data);

            binWriter.Write(pixels, 0, pixels.Length);
        }

        static void Main(string[] args)
        {
            if(args.Length < 1)
            {
                Console.WriteLine("Usage: Image2Bmp filename");

                return;
            }

            Bitmap bmp = (Bitmap)Image.FromFile(args[0]);
            FileStream strm = File.Create(Path.GetFileNameWithoutExtension(args[0]) + ".bitmap");

            Console.WriteLine("Converting bitmap {0}", args[0]);

            WriteBitmap(bmp, strm);
            strm.Close();
        }


Загрузчик такого формата выглядит так:

        public static unsafe Bitmap Load(string fileName)
        {
            // Read header
            Stream strm = File.OpenRead(fileName);
            BinaryReader binReader = new BinaryReader(strm);
            BmpHeader hdr = new BmpHeader()
            {
                Header = binReader.ReadInt16(),

                Width = binReader.ReadInt16(),
                Height = binReader.ReadInt16(),
                Format = binReader.ReadInt16()
            };

            Bitmap bitmap = new Bitmap(hdr.Width, hdr.Height);
            binReader.Read(bitmap.Data, 0, bitmap.Data.Length);

            return bitmap;
        }


А фактическое использование — так:

            Bitmap bg = BmpLoader.Load("test.bitmap");
            LCD lcd = new LCD();

            while(true)
            {
                lcd.CopyFrom(bg);
            }


Заключение


Как мы видим, писать программы для одноплатников на C# отнюдь не сложно и можно пользоваться всеми приятными фишками языка. Часть кода из этой статьи выдрана из моего сайд-проекта, о котором хочу рассказать вам в ближайшее время — поэтому местами код совсем не причесан, но надеюсь — всё было понятно :)

Исходный код для сегодняшнего материала можно найти здесь.

Также у меня есть канал в Telegram, куда я выкладываю посты с тематикой DIY, ремонта и моддинга, а также программирования под гаджеты прошлых лет и вовремя ссылки на новые статьи.



Читайте также:

А ещё я держу все свои мобилы в одной корзине при себе (в смысле, все проекты у одного облачного провайдера) — Timeweb. Потому нагло рекомендую то, чем пользуюсь сам — вэлкам:

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


  1. bodyawm Автор
    04.07.2024 08:05
    +1

    Ну что дорогие друзья, надеюсь материал был вам интересен и полезен!

    В репо находится код выдранный из одного из моих сайдпроектов, так что местами требует рефакторинга и чистки, но тем не менее должен послужить неплохим примером :)

    В ближайшее время будет интересный материал о таком красавце, как HTC Shift, который я купил у читателя за относительно небольшие деньги. Это очень интересный UMPC с двумя процессорами на борту - ARM и x86, которые работают на двух ОС: Windows Mobile и Windows Vista, а между ними можно переключаться нажатием одной кнопки!

    И конечно-же не забудем о планшете-электронной книге на WinCE, о которой я рассказывал в статье!


  1. bodyawm Автор
    04.07.2024 08:05
    +1

    Друзья! Ещё год назад мне пришла очень интересная идея для DIY-моддинга самопального крутого девайса. Для этого мне нужен будет планшет или дроид-нетбук по типу такого как в посте, но обязательно битый, нерабочий, и т.п, поскольку рабочий девайс мне не хотелось бы разбирать. Помню под одним из прошлых постов читатели предлагали заслать на проект подобный девайс, но сейчас я не могу найти их контакты.


    1. zhka
      04.07.2024 08:05
      +1

      у меня есть плата от ноутбука с первой картинки на wm8650(рабочая) и еще какие-то детали от него. Пожертвую бесплатно, пишите в личку.


  1. SergeyEgorov
    04.07.2024 08:05

    Ссылка на исходный код никуда не ведет...


    1. bodyawm Автор
      04.07.2024 08:05

      Спасибо, пофиксил


  1. Johan_Palych
    04.07.2024 08:05
    +2

    Ubuntu 16.04 LTS Xenial Xerus c April 30 2021 все. Только Extended Security Maintenance.
    Почему не взять свежие образы тут:
    https://www.armbian.com/orange-pi-one/
    Или собрать за 10-15 мин тут:
    https://github.com/armbian/build


    1. bodyawm Автор
      04.07.2024 08:05

      Я ж не просто так написал, что под некоторые одноплатники возможно что и нет апдейтов или их ставить нецелесообразно (например, на устройства с 256Мб ОЗУ).


      1. Johan_Palych
        04.07.2024 08:05
        +2

        Пишем программы на C# для одноплатников: что, как и где на примере Orange Pi One

        У меня был Orange Pi One 1GB - вполне боевая железка. Сейчас стоит от 2 до 3.5 т.р.


        1. bodyawm Автор
          04.07.2024 08:05

          На авито по 500 рублей))


  1. michael108
    04.07.2024 08:05
    +1

    Статья интересная и познавательная. Но есть несколько вопросов:

    1. Что насчет быстродействия? Ежу понятно, что если хочешь очень быстро -- делай на С/С++. Но есть ли инфа, насколько проседает быстродействие под .NET?

    2. Насколько универсальна .NET платформа? Т.е. есть бинарники под все одноплатники или только под некоторые? Меня интересует, в частности, семейство Raspberry Pi. Тут ведь идет завязка на конкретное железо (GPIO, SPI, I2C, ...), для чего обычно используются дополнительные библиотеки (та же spidev, например).

    3.Какая среда наиболее удобна для разработки? На ПК у нас есть Visual Studio со всеми плюшками, что вряд ли будет иметь место на одноплатнике. Поэтому возникает естественный вопрос о кросс-платформенной разработке (но надежды мало :-) ).

    Понятно, что ответы можно нагуглить и самому, но тут больше интересует личный опыт, раз уж человек погрузился в тему.


    1. bodyawm Автор
      04.07.2024 08:05
      +1

      Но есть ли инфа, насколько проседает быстродействие под .NET?

      Учитывая что все общение происходит через юзерспейс, не думаю что дотнет здесь большой боттлнек.

      Насколько универсальна .NET платформа? Т.е. есть бинарники под все одноплатники или только под некоторые? Меня интересует, в частности, семейство Raspberry Pi. Тут ведь идет завязка на конкретное железо (GPIO, SPI, I2C, ...), для чего обычно используются дополнительные библиотеки (та же spidev, например).

      Да, под все. На малинке есть библиотеки, которые напрямую в регистры процессора пишут (кстати, как их защита памяти не кидает в сегфолт?)

      Какая среда наиболее удобна для разработки? На ПК у нас есть Visual Studio со всеми плюшками, что вряд ли будет иметь место на одноплатнике. Поэтому возникает естественный вопрос о кросс-платформенной разработке (но надежды мало :-) ).

      MonoDevelop!


    1. Voland69
      04.07.2024 08:05
      +4

      3. На малине можно запустить VSCode, например, но если малина не 4 и выше то скорость работы вам не понравится - так что Remote Debugger надо настраивать, ИМХО.

      2. Если на одноплатнике линукс, то переносимо в рамках одной архитектуры - но так как работаем с пинами то то что в /sys/, /proc/ и /dev/ все будет одинаково гарантии нет, так что как минимум номера пинов и ссылки на девайсы обязательно выносить в конфиг.

      P.S. есть .NET nanoFramework, очень ограниченное подмножество .NET'а, но работает на ESP32, STM32 и подобных микроконтроллерах.


    1. buldo
      04.07.2024 08:05
      +6

      Нормально всё по быстродействию. Рантайм же все в итоге компилирует в обычный машинной код, а не интерпритирует. Плюс в NET8 многоуровневый оптимизатор, который налету hot paths сильнее оптимизирует.

      Главное не делайте как автор, а используйте NET8 - он значительно быстрее на arm, чем mono, который всегда был глючным.

      Для работы с переферией не изобретайте велосипед, а используйте вот это https://github.com/dotnet/iot

      Для разработки можно использовать или мой рецепт из 2018 года https://habr.com/ru/articles/422141/ или на хабре есть статья, где человек подобное завернул в плагин для vscode и сделал всё по красоте.

      В общем эта статья скорее сборник вредных советов о том, как делать НЕ надо


      1. bodyawm Автор
        04.07.2024 08:05

        Единственное, к чему можно докопаться - это к Mono и к использованию фреймворка как профиля, о чём я честно предупредил. Ни в чем больше проблем не вижу.

        This repository contains the System.Device.Gpio library and implementations for various boards like Raspberry Pi and Hummingboard.

        Ну в дотнете ещё с 4.5 есть, например, матлиба, но это не значит что она подходит для всех кейсов и нет необходимости иногда городить велики. На мтк свой интерфейс к гпио, а не sysfs, на s3c6410 свой и т.д и iot не панацея)

        Лишь бы обосрать)


        1. DieSlogan
          04.07.2024 08:05
          +4

          Зачем обосрать, вы вводную статью делаете, мол, все кому интересно, делай как я.

          Но а для новичков как лучше? Как вы сделали или основным маршрутом?


          1. bodyawm Автор
            04.07.2024 08:05

            Новичку лучше сразу брать кор и гпио по совету @buldo, косвенно об этом в статье я и сам написал.

            Это я дед-пердед с фреймворком в зубах и PImvoke'ами на сисколлы))


        1. buldo
          04.07.2024 08:05
          +1

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

          Реализовывать несколько абстрактных классов и имеешь кучу плюшек.


  1. buldo
    04.07.2024 08:05
    +3

    Честное слово, автор, ну нормально же всё было, классные статьи про древнее железо. А тут вы описываете подходы, которые при использовании навредят моей любимой платформе


    1. bodyawm Автор
      04.07.2024 08:05

      Где конкретика?)


      1. buldo
        04.07.2024 08:05
        +11

        Отвечу сразу вам и @yri066

        Вы говорите как классно писать на net для одноплатников и сразу же показываете использование mono. Не надо так. А то кто-то, кто не знает платформу, возьмёт и пойдёт таким путем, а потом будет говорить, какой net плохой и ужасный.

        Ну и махонькая придирка - код в некоторых местах довольно неканоничен. Яб такое на ревью не пропустил.

        Если вы за подход "критикуешь, предлагай", то могу написать что-то типа статьи-ответа с моим видением "правильного" использования .NET на одноплатниках.


        1. dlinyj
          04.07.2024 08:05
          +7

          Если вы за подход "критикуешь, предлагай", то могу написать что-то типа статьи-ответа с моим видением "правильного" использования .NET на одноплатниках.

          Было бы реально очень круто!


        1. bodyawm Автор
          04.07.2024 08:05
          +1

          Код выдран напрямую из сайдпроекта, там еще нужно порефакторить :)

          Я пока не совсем понимаю придирки к моно насчет стабильности. Понятное дело что под армом жита (вроде) нет, дебажить моно неудобно но в остадьном вроде вполне норм. Положить рантайм в манагед среде у меня не получалось.


          1. buldo
            04.07.2024 08:05
            +3

            У NET выше производительность - среду специально тюнили под arm, GC работает сильно лучше, сейчас доступны режимы работы GC, которые не жрут кучу системной памяти.

            Приложение на NET можно сделать selfhosted и потом затримить - можно не ставить рантайм на sbc.

            У NET есть GenericHost от MS, который умеет интегрироваться с systemd в плане сигнализации состояния демона и системных логов

            У NET есть нормально работающий удалённый отладчик.

            Новые версии пакетов выходят по большей части под NET и standard2.1.

            В конце концов у NET более приятные csproj и способ работы с nuget.

            Начиная с NET7 доступен LibraryImport для более удобной и быстрой работы с unmanaged кодом.

            В общем я хз, зачем сейчас использовать mono.

            P.S. увидел у вас в логе работу с callStack- не надо так - бьёт по производительности и легко может показать ерунду, если метод будет заинлайнен.

            P.P.S. похоже у меня больше горит от того, что вы показали пример нового проекта на mono


            1. DieSlogan
              04.07.2024 08:05

              Ещё в unsafe указатели можно не уходить.


            1. bodyawm Автор
              04.07.2024 08:05

              Да, стекфреймы иногда юзаю для логов но специально атрибутом не инлайню. За селфхостед спасибо))


    1. yri066
      04.07.2024 08:05
      +2

      Статья была интересной, но чем она вредит данной платформе?


  1. Indemsys
    04.07.2024 08:05

    Если открыть сам Visual Studio, том есть много разных вариантов как писать на C# для платформ с Linux и Android.
    Можно консольного типа приложение писать, как в статье, можно делать Blazor Web App (на Rasperry Pi такое есть), можно делать с использованием .NET MAUI (надо искать подходящий одноплатник). Тут бы анализ не помешал этих вариантов. Но консольное приложение видится худшим из них. Тогда уже лучше Arduino взять.

    Но я бы выбрал писать на Java или на C++ под Android в Android Studio.
    Поскольку будет развитое GUI и главное - мощный отладчик. Поскольку при наличии GPT не важно на каком языке писать. А в качестве одноплатника выбрал бы Banana PI BPI-M5, а не Orange. Поскольку для Banana есть данный момент поддерживаемый Андроид, а для Orange нет.


    1. buldo
      04.07.2024 08:05
      +2

      Угу. И огребать с планировщиком андройда, который может прибить твоё приложение просто потому что.

      А как у андроид с доступом к gpio и шинам?

      Вообще на шарпе можно взять Avalonia и нормально писать gui для одноплатника. При этом даже X или Wayland не нужен будет.


      1. Indemsys
        04.07.2024 08:05

        С Avalonia вижу проблему с IDE. Если это просто расширение к VS то будет криво и косо и критически зависеть от качества портирования .NET на конкретном одноплатнике. А в той среде про портирование .NET никто даже не в курсе.
        А вот отладкчик Android Studio покроет все недостатки понимания его планировщика.


        1. buldo
          04.07.2024 08:05
          +2

          Не, какие проблемы. Аналония- это набор библиотек. Плагин для студии просто шаблоны и дизайнер добавляет.

          Нет никакого качества портирования. NET зависит от небольшого набора системных библиотек.

          А если говорить про gui без иксов, то авалония использует skia для отрисовки и просто пишет во фреймбуфер.

          Так что, если на одноплатнике хоть как-то заводится графика, то проблем не будет


    1. DieSlogan
      04.07.2024 08:05

      Тогда уже лучше Arduino взять.

      Тогда можно взять dotnet nano framework. У него GUI это WPF подобный аналог.


  1. sancheolz
    04.07.2024 08:05
    +1

    Не имею понятия что такое профиль dotnet в одноплатниках, но при использовании dotnet на обычных ПК для него все же есть WinForms. Достаточно подключить nuget-пакет Core.System.Windows.Forms. Это порт WinForms из mono в dotnet. С System.Drawing тоже можно работать. Достаточно включить флаг EnableUnixSupport в runtime.template.json.


    1. bodyawm Автор
      04.07.2024 08:05

      Пасиб, никогда не рассматривал гуй на дотнет коре, всегда либо увп, либо сильверлайт, либо винфорс