Строка 27: Модификатор доступа для Main -> public(для Framework, в .NET, на сколько помню, допускаются другие модификаторы)
Строка 27: Сделать Main асинхронным
Строка 29: Предполагаем, что db.GetOrders() - репозиторий, возвращающий IQueryable. Т.к. IEnumerable является базовым для IQueryable, то все последующие использования orders(из-за полиморфизма) приведут к обращению к источнику данных(например, БД). Думаю, что стоило привести к чему-то "материальному", типа ToListAsync()
Строка 34: Потенциальный Race Condition. Значение i будет не детерминировано. Необходимо использовать Interlocked.Increment, либо примитивы синхронизации
Строка 35: Переменная i будет захвачена и преобразована в отдельный объект, поэтому вывод значения i будет разниться - возможно напечататься только последнее значение либо будут повторы. Стоит завести отдельную переменную для передачи на вывод
Строка 35: Если предположить, что п.3 верен, то на каждой итерации будет запрос к источнику данных(БД)
Строка 38: Лучше использовать асинхронный WhenAll()
SPI1->CR1|=SPI_CR1_SSM;//Программный NSS
SPI1->CR1|=SPI_CR1_SSI;//NSS - high
SPI1->CR2|=SPI_CR2_TXDMAEN;//Разрешить запросы DMA
SPI1->CR1|=SPI_CR1_SPE;//включить SPI1
Для установки 1 бита каждый раз вычитывать volatile-регистр — ненужный расход памяти и тактов. Компилятор не будет это оптимизировать.
Лучше свернуть к 1 записи:
SPI1->CR1|= SPI_CR1_SSM | ... ;
Обращение к BSRR регистру неправильно:
#define CS_SET GPIOA->BSRR |= GPIO_BSRR_BS2
Этот регистр только для записи. Читать его ненужно — только записывать:
Когда был карантин и особо было делать нечего, то также подумал сделать для ARM Cortex свою IDE с перфокартами и канифолью. Начал пилить на C# и WPF, но спустя некоторое время понял, что в одно лицо все хотелки будет реализовать сложно (и/или долго).
Размышляя над целесообразностью дальнейшей разработки, вспомнил, что натыкался на материалы(раз, два), где к Visual Studio Code прикручивали компилятор GCC ARM вместе с отладкой. Однако для каждого проекта требовалось создание служебных файлов как для самой среды VSCode, так и для программируемого камня: мэйкфайл, стартап, скрипт линкера.
В итоге принял решение, что, конечно, своя IDE — это хорошо, но реализовывать то, что уже существует необходимости нет. Поэтому написал свое расширение для VSCode, которое автоматизирует создание проекта под определенный контроллер, его сборку и загрузку(OpenOCD или JLink), а также дебаг с помощью другого расширения.
Примерно месяц у меня ушел на базовый, но достаточный, функционал. С учетом того, что раньше с АПИ VSCode дел не имел и не писал на TypeScript. Сейчас потихоньку дополняю — недавно добавил pdf-viewer от Мозилы для просмотра мануалов и даташитов, которые автоматически загружаются для выбранного контроллера. Это заняло у меня несколько вечеров. Если бы все писал сам, то…
В VSCode полно расширений самых разных мастей и калибров: от подсветки кода и дополнения синтаксиса до аналога Тиндера. В основном это опенсорсные проекты, расположеные на гитхабе и среди них наверняка найдутся те, которые хотя бы частично, подойдут по функционалу и, при желании, их несложно переделать под свои нужды. Кроме того, насколько я знаю, можно сделать собственную сборку этого редактора, включив в нее только то, что необходимо.
Помимо VSCode существует множество других сред, типа Sublime, VIM, о которых, Автор наверняка знает. В них уже реализован необходимый минимум для работы: GUI, работа с файлами, вкладки с исходниками и т.д. Поэтому можно сосредоточится только на недостающем функционале.
Мне кажется, что это немного «странный» фреймворк. С одной стороны, написано на c++, но используется только ключевое слово class и оператор :: — разрешение контекста.
Очень много дублирования кода и неоптимального обращения к регистрам контроллера, например:
Исходник
if (pin < 8) {
// Установим регистр CRL (CNF & MODE).
uint8_t pinmode = pin * 4;
uint8_t pincnf = pinmode + 2;
if (speed == GPIO_LOW) { instance.regs->CRL |= (0x2 << pinmode); }
else if (speed == GPIO_MID) { instance.regs->CRL |= (0x1 << pinmode); }
else if (speed == GPIO_HIGH) { instance.regs->CRL |= (0x3 << pinmode); }
// Две записи подряд в один и тот же регистр
if (type == GPIO_PUSH_PULL) { instance.regs->CRL &= ~(0x1 << pincnf); }
else if (type == GPIO_OPEN_DRAIN) { instance.regs->CRL |= (0x1 << pincnf); }
}
else {
// Установим регистр CRH.
uint8_t pinmode = (pin - 8) * 4;
uint8_t pincnf = pinmode + 2;
if (speed == GPIO_LOW) { instance.regs->CRH |= (0x2 << pinmode); }
else if (speed == GPIO_MID) { instance.regs->CRH |= (0x1 << pinmode); }
else if (speed == GPIO_HIGH) { instance.regs->CRH |= (0x3 << pinmode); }
if (type == GPIO_PUSH_PULL) { instance.regs->CRH &= ~(0x1 << pincnf); }
else if (type == GPIO_OPEN_DRAIN) { instance.regs->CRH |= (0x1 << pincnf); }
}
Да, в контроллерах ресурсы довольно ограничены и конечны, но 640 байт — это действительно мало. Сейчас есть экземпляры с 1мб+.
Я правильно улавливаю мысль?
Если я правильно понял, то да, можно так. Создать массив -> передать его адрес функции для приема данных с датчика -> обработать данные, если нужно -> передать адрес для функции вывода на дисплей.
Согласен, что с регистрами тактирования, в принципе, можно использовать только запись. Однако, некоторые из них содержат дефолтные значение, но и это, естественно, можно учесть.
И вот тут действительно одна ассемблерная команда…
Здесь тоже должно быть соблюдено довольно много условий, чтобы действительно была одна команда :)
Должна происходить только запись, без чтения
В одном регистре общего назначения должно содержатся значение для записи, которое туда попало в результате предыдущих операций, и которое подходит для текущей, что маловероятно
В другом регистре общего назначения должен быть адрес целевого регистра, причем с максимальным оффсетом 2 в 12 степени
Если, например:
Apb1Enabler::Write();
// Значение для записи в APB2 совпадает c APB1
// и APB2 смещен на +4, относительно APB1
Apb2Enabler::Write();
То, действительно, в APB2ENR запись произойдет в 1 инструкцию
Да, все было примерно так, как Вы описываете. Пока, действительно, в одном случае стало необходимо что-то выключать, что-то оставлять(энергосбережение), в другом — просто нехватка памяти. А, поскольку, класс Pinlist был сделан, то от него до этого кода совсем чуть-чуть :)
По поводу других битов — все ведь известно на этапе, поэтому можно сделать описание периферии один раз и забыть.
Условный пример:
// Вариантов ремапа может быть много, но это все также можно отобразить в коде
template<auto identifier, bool isDMA, bool isRemap>
class SPI{
using power = Valuelist<
isDMA ? DMAEN : 0, // Значение для 1 регистра тактирования
identifier == 1 ? SPI1EN : identifier == 2 ? SPI2EN : SPI3EN, // Для 2 регистра
isRemap ? GPIOAEN : GPIOBEN // Для 3 регистра
>
};
Это можно немного автоматизировать, написав парсер(по крайней мере с stm), который сам будет дописывать это свойство в периферию.
И пример, с допущением, что описана вся периферия в таком стиле:
Малопотребляющее устройство, в котором UART используется только для выхода из сна, в остальное время он отключен
using listInit = powerlist<spi1, led1, led2, btn1, btn2>;
using listWake = powerlist<uart>;
Power::Enable<listInit >();
// User Code
// Произойдет только включение uart,
//Порт для TX и RX останется включенным, если был до этого включен, в противном - включится
// Так же и для DMA, если оно нужно
Power::Keep<listWake, listInit>();
Sleep();
// Произойдет обратная ситуация - отключится только uart и его пины
// если они не задействованы в другой периферии.
Power::Keep<listInit, listWake>();
При этом, неважно какой задействован uart, пины и т.д. Все решится само на этапе компиляции. Можно даже сменить uart на просто кнопку. В самом пользовательском коде ничего не поменяется.
Цель, как раз, была противоположна — максимально, по возможности, абстрагироваться от контроллера, его периферии и внешних устройств.
В контроллерах arm-cortex, прежде, чем использовать периферийный блок, необходимо произвести следующие действия:
Включить тактирование
Произвести настройку
В статье говорится о том, как эффективно и обобщенно реализовать пункт 1, независимо от контроллера.
Вместе с тем, совсем без каких-либо примеров, — текст был бы очень сложен для восприятия, поэтому введено уточнение: stm32f103c8, SPI и USART, при этом данные пересылаются с использованием DMA.
Для функционирования аппаратных интерфейсов требуется устройство ввода-вывода, т.е. пины. Эти пины могут относится к разным портам (Порт А, Порт В, ...), которые также требуется включить(подать тактирование).
Если немного перефразировать исходную задачу под Ваши условия, то: Требуется выводить температуру от датчика, подключенного по UART, на семисегментный индикатор по SPI через сдвиговый регистр 74HC595.
Разбор этой задачи в статье закончился на этапе включении тактирования UART, SPI и сопутствующей периферии. Показаны различные способы это сделать, в том числе на C++.
Если вопрос именно в метапрограммировании, то вот несколько очень полезных ссылок: раз, два, три.
Да, спор на все времена — макросы vs шаблоны.
Возможно, конкретно в этом случае проще было сделать с использованием дефайнов, но все используемые метафункции уже были готовы для класса Pinlist, который значительно сложнее. Думаю, что его также эффективно повторить макросами не получится. Там есть и проверка на уникальность, возможность манипуляции одним пином, разделение списка пинов с обоих сторон и т.д.
Ремап также можно победить, если использовать static_assert времени компиляции. А условия для периферии и пинов придется прописывать либо с мануалом в руках, либо парсить документы производителя.
У меня не получилось, если речь идет про сравнение прямой записи в регистры и «функции инициализации» :) Ссылка
Во втором случае добавляется повторная модификация volatile регистров, что, естественно, увеличит размер листинга.
Да, конкретно в этом случае выигрыш не сильно заметен. Но если добавить около 100 строк кода, то получится примерно Pinlist. Который позволяет скопом инициализировать пины:
int main(){
Pinlist<spi1, uart1, spi2, pin1, led, i2c2, uart3>::Init();
}
Будет сформирован список используемых портов, пины будут распределены по этим портам, сформированы значения для записи и будет произведена однократная модификация каждого регистра. Т.е. если вся периферия сидит на 1 порте А, то только в него и произойдет запись, если на 3 — соответственно 3. Плюс включение тактирования, плюс включение прерываний — в сумме может набежать значительно.
Как пример: есть M0 с 16кБ флеша. Из них 6 на бутлоадер. Ну не ровно 6, а на пару десятков байт больше. Соответственно, получается, что занята еще одна страница памяти и на пользовательский код остается уже на целых 10% меньше. Можно было попробовать решить задачу, используя ассемблер, а можно вот так.
Для этого варианта можно ввести структуру, в которой, естественно, можно задавать все руками:
Структура
template<auto _portMosi, auto _pinMosi, auto _portMiso, auto _pinMiso>
struct spiPins{
static constexpr auto portMosi = _portMosi;
static constexpr auto pinMosi = _pinMosi;
static constexpr auto portMiso = _portMiso;
static constexpr auto pinMiso = _pinMiso;
};
И передавать ее в класс
Класс SPI
template<typename pins>
struct SPI{
using power = Valuelist<
0,
pins::portMosi | pins::portMiso,
0>;
using pin = Typelist<
Pin<pins::portMosi, pins::pinMosi, AF>,
Pin<pins::portMiso, pins::pinMiso, AF>
>;
};
И, собственно, в пользовательском коде особо ничего не изменится:
int main(){
using pins = spiPins<1,2,1,4>;
using spi = SPI<pins>;
Power<spi>::Enable();
Pinlist<spi>::Init();
}
Я намеренно упростил реализацию периферии, чтобы не перегружать статью.
В данный момент я использую примерно такой концепт для периферии:
Периферия
template<auto identifier, bool isDMA, bool isRemap>
struct Peripheral{
// В зависимости от шаблона, сюда подставляются необходимые значения
using power = Valuelist<
isDMA ? DMAEN : 0,
identifier == 1 ? PeripheralBitEn1 : PeripheralBitEn2,
isRemap ? AFIOEN : 0 | identifier == 1 ? GPIOAEN : GPIOBEN>;
// С пинами - аналогичная ситуация. На этапе компиляции известен и порт и номер и настройка
using pins = Typelist<Pin<GPIO, n, AF>, Pin<GPIO, y, INPUT>>;
//
using interrupts = Valuelist<isDMA ? IRQ1 : IRQ2>;
};
Далее, для всего этого есть три соответствующих класса:
Классы инициализации
template<typename... Peripherals> // Тот, что описан в статье.
struct Power{
static void Enable();
};
template<typename... Peripherals> // Похожий класс для включения/выключения прерываний
struct Interrupts{
static void Enable();
};
template<typename... Peripherals> // Самый интересный - проверка повторов, распределение по портам и т.д.
struct Pinlist{
static void Init();
};
Пример использования:
using peripheral1 = Peripheral<1, true, true>;
using peripheral2 = Peripheral<2, false, false>
using initList = periph<peripheral1 , peripheral2>;
int main(){
Power<initList>::Enable();
Pinlist<initList>::Init();
Interrupts<initList>::Enable();
}
Выделил бы следующие проблемы:
Строка 27: Модификатор доступа для Main -> public(для Framework, в .NET, на сколько помню, допускаются другие модификаторы)
Строка 27: Сделать Main асинхронным
Строка 29: Предполагаем, что db.GetOrders() - репозиторий, возвращающий IQueryable. Т.к. IEnumerable является базовым для IQueryable, то все последующие использования orders(из-за полиморфизма) приведут к обращению к источнику данных(например, БД). Думаю, что стоило привести к чему-то "материальному", типа ToListAsync()
Строка 34: Потенциальный Race Condition. Значение i будет не детерминировано. Необходимо использовать Interlocked.Increment, либо примитивы синхронизации
Строка 35: Переменная i будет захвачена и преобразована в отдельный объект, поэтому вывод значения i будет разниться - возможно напечататься только последнее значение либо будут повторы. Стоит завести отдельную переменную для передачи на вывод
Строка 35: Если предположить, что п.3 верен, то на каждой итерации будет запрос к источнику данных(БД)
Строка 38: Лучше использовать асинхронный WhenAll()
Строка 43: Также лучше использовать Task.Delay()
Зачем конструкции вида:
Для установки 1 бита каждый раз вычитывать volatile-регистр — ненужный расход памяти и тактов. Компилятор не будет это оптимизировать.
Лучше свернуть к 1 записи:
Обращение к
BSRR
регистру неправильно:Этот регистр только для записи. Читать его ненужно — только записывать:
Размышляя над целесообразностью дальнейшей разработки, вспомнил, что натыкался на материалы(раз, два), где к Visual Studio Code прикручивали компилятор GCC ARM вместе с отладкой. Однако для каждого проекта требовалось создание служебных файлов как для самой среды VSCode, так и для программируемого камня: мэйкфайл, стартап, скрипт линкера.
В итоге принял решение, что, конечно, своя IDE — это хорошо, но реализовывать то, что уже существует необходимости нет. Поэтому написал свое расширение для VSCode, которое автоматизирует создание проекта под определенный контроллер, его сборку и загрузку(OpenOCD или JLink), а также дебаг с помощью другого расширения.
Примерно месяц у меня ушел на базовый, но достаточный, функционал. С учетом того, что раньше с АПИ VSCode дел не имел и не писал на TypeScript. Сейчас потихоньку дополняю — недавно добавил pdf-viewer от Мозилы для просмотра мануалов и даташитов, которые автоматически загружаются для выбранного контроллера. Это заняло у меня несколько вечеров. Если бы все писал сам, то…
В VSCode полно расширений самых разных мастей и калибров: от подсветки кода и дополнения синтаксиса до аналога Тиндера. В основном это опенсорсные проекты, расположеные на гитхабе и среди них наверняка найдутся те, которые хотя бы частично, подойдут по функционалу и, при желании, их несложно переделать под свои нужды. Кроме того, насколько я знаю, можно сделать собственную сборку этого редактора, включив в нее только то, что необходимо.
Помимо VSCode существует множество других сред, типа Sublime, VIM, о которых, Автор наверняка знает. В них уже реализован необходимый минимум для работы: GUI, работа с файлами, вкладки с исходниками и т.д. Поэтому можно сосредоточится только на недостающем функционале.
Очень много дублирования кода и неоптимального обращения к регистрам контроллера, например:
Как минимум, можно сократить в 3 раза:
Очень много проверок в рантайме, наподобие таких:
которых, думаю, лучше избегать.
arm-none-eabi-gcc 10.1.1 20200529 (release)
Если я правильно понял, то да, можно так. Создать массив -> передать его адрес функции для приема данных с датчика -> обработать данные, если нужно -> передать адрес для функции вывода на дисплей.
Только разрешенные методы.
Здесь тоже должно быть соблюдено довольно много условий, чтобы действительно была одна команда :)
Если, например:
То, действительно, в APB2ENR запись произойдет в 1 инструкцию
По поводу других битов — все ведь известно на этапе, поэтому можно сделать описание периферии один раз и забыть.
Условный пример:
Это можно немного автоматизировать, написав парсер(по крайней мере с stm), который сам будет дописывать это свойство в периферию.
И пример, с допущением, что описана вся периферия в таком стиле:
Малопотребляющее устройство, в котором UART используется только для выхода из сна, в остальное время он отключен
При этом, неважно какой задействован uart, пины и т.д. Все решится само на этапе компиляции. Можно даже сменить uart на просто кнопку. В самом пользовательском коде ничего не поменяется.
В контроллерах arm-cortex, прежде, чем использовать периферийный блок, необходимо произвести следующие действия:
В статье говорится о том, как эффективно и обобщенно реализовать пункт 1, независимо от контроллера.
Вместе с тем, совсем без каких-либо примеров, — текст был бы очень сложен для восприятия, поэтому введено уточнение: stm32f103c8, SPI и USART, при этом данные пересылаются с использованием DMA.
Для функционирования аппаратных интерфейсов требуется устройство ввода-вывода, т.е. пины. Эти пины могут относится к разным портам (Порт А, Порт В, ...), которые также требуется включить(подать тактирование).
Если немного перефразировать исходную задачу под Ваши условия, то:
Требуется выводить температуру от датчика, подключенного по UART, на семисегментный индикатор по SPI через сдвиговый регистр 74HC595.
Разбор этой задачи в статье закончился на этапе включении тактирования UART, SPI и сопутствующей периферии. Показаны различные способы это сделать, в том числе на C++.
Если вопрос именно в метапрограммировании, то вот несколько очень полезных ссылок: раз, два, три.
Возможно, конкретно в этом случае проще было сделать с использованием дефайнов, но все используемые метафункции уже были готовы для класса Pinlist, который значительно сложнее. Думаю, что его также эффективно повторить макросами не получится. Там есть и проверка на уникальность, возможность манипуляции одним пином, разделение списка пинов с обоих сторон и т.д.
Ремап также можно победить, если использовать static_assert времени компиляции. А условия для периферии и пинов придется прописывать либо с мануалом в руках, либо парсить документы производителя.
Во втором случае добавляется повторная модификация volatile регистров, что, естественно, увеличит размер листинга.
Только я предлагаю вынести тактирование и пины за пределы периферии, чтобы код стал еще меньше.
Рассмотрим, для упрощения spi и пины, без дма и т.д.
В коде, пин выглядит примерно так:
Далее spi
И пинлист
Будет сформирован список используемых портов, пины будут распределены по этим портам, сформированы значения для записи и будет произведена однократная модификация каждого регистра. Т.е. если вся периферия сидит на 1 порте А, то только в него и произойдет запись, если на 3 — соответственно 3. Плюс включение тактирования, плюс включение прерываний — в сумме может набежать значительно.
Как пример: есть M0 с 16кБ флеша. Из них 6 на бутлоадер. Ну не ровно 6, а на пару десятков байт больше. Соответственно, получается, что занята еще одна страница памяти и на пользовательский код остается уже на целых 10% меньше. Можно было попробовать решить задачу, используя ассемблер, а можно вот так.
И передавать ее в класс
И, собственно, в пользовательском коде особо ничего не изменится:
В данный момент я использую примерно такой концепт для периферии:
Далее, для всего этого есть три соответствующих класса:
Пример использования:
Но, да, конечно, — трехэтажные макросы и метафункции выглядят монструозно, с этим трудно спорить.