Pull to refresh
11
0
Семён Иванов @7bnx

Пользователь

Send message

Выделил бы следующие проблемы:

  1. Строка 27: Модификатор доступа для Main -> public(для Framework, в .NET, на сколько помню, допускаются другие модификаторы)

  2. Строка 27: Сделать Main асинхронным

  3. Строка 29: Предполагаем, что db.GetOrders() - репозиторий, возвращающий IQueryable. Т.к. IEnumerable является базовым для IQueryable, то все последующие использования orders(из-за полиморфизма) приведут к обращению к источнику данных(например, БД). Думаю, что стоило привести к чему-то "материальному", типа ToListAsync()

  4. Строка 34: Потенциальный Race Condition. Значение i будет не детерминировано. Необходимо использовать Interlocked.Increment, либо примитивы синхронизации

  5. Строка 35: Переменная i будет захвачена и преобразована в отдельный объект, поэтому вывод значения i будет разниться - возможно напечататься только последнее значение либо будут повторы. Стоит завести отдельную переменную для передачи на вывод

  6. Строка 35: Если предположить, что п.3 верен, то на каждой итерации будет запрос к источнику данных(БД)

  7. Строка 38: Лучше использовать асинхронный WhenAll()

  8. Строка 43: Также лучше использовать Task.Delay()

  1. Зачем конструкции вида:


    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 |  ... ;

  2. Обращение к BSRR регистру неправильно:


    #define CS_SET GPIOA->BSRR  |=  GPIO_BSRR_BS2

    Этот регистр только для записи. Читать его ненужно — только записывать:


    #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);  }
  }


Как минимум, можно сократить в 3 раза:
Модификация
volatile uint32_t * const reg = pin < 8 ? &instance.regs->CRL : &instance.regs->CRH;

uint8_t pinmode = (pin & 7) * 4;
uint8_t pincnf = pinmode + 2;
uitn32_t mask = 0xF << pinmode;
uint32_t speed = (uint32_t)speed << pinmode;
uint32_t type = (uint32_t)type << pincnf;

*reg = (*reg & (~mask)) | speed | type;  


Очень много проверок в рантайме, наподобие таких:
...
if (pin > 15){}
...
if (pin < 8){}
...

которых, думаю, лучше избегать.
Да, в контроллерах ресурсы довольно ограничены и конечны, но 640 байт — это действительно мало. Сейчас есть экземпляры с 1мб+.

Я правильно улавливаю мысль?


Если я правильно понял, то да, можно так. Создать массив -> передать его адрес функции для приема данных с датчика -> обработать данные, если нужно -> передать адрес для функции вывода на дисплей.
Согласен, что с регистрами тактирования, в принципе, можно использовать только запись. Однако, некоторые из них содержат дефолтные значение, но и это, естественно, можно учесть.
Полностью оградить пользователя от взаимодействия с регистрами :)
Только разрешенные методы.
Это в тексте статьи сделано такое упрощение, в полном коде, в конце, это все учтено :)

class HPower{

protected:

  template<typename SetList, typename ResetList, typename AddressesList>
  static void ModifyRegisters();
}

/**/

class Power: public interfaces::IPower<Power>, public hardware::HPower{
...
friend class IPower<Power>;
  ...
}
То потом можно вызывать просто:
Apb1Enabler::Write();

И вот тут действительно одна ассемблерная команда…


Здесь тоже должно быть соблюдено довольно много условий, чтобы действительно была одна команда :)
  1. Должна происходить только запись, без чтения
  2. В одном регистре общего назначения должно содержатся значение для записи, которое туда попало в результате предыдущих операций, и которое подходит для текущей, что маловероятно
  3. В другом регистре общего назначения должен быть адрес целевого регистра, причем с максимальным оффсетом 2 в 12 степени


Если, например:
Apb1Enabler::Write();
// Значение для записи в APB2 совпадает c APB1
// и APB2 смещен на +4, относительно APB1
Apb2Enabler::Write();


То, действительно, в APB2ENR запись произойдет в 1 инструкцию
  movs    r3, value
  ldr     r2, .address
  str     r3, [r2]
  str     r3, [r2, #4]
Да, все было примерно так, как Вы описываете. Пока, действительно, в одном случае стало необходимо что-то выключать, что-то оставлять(энергосбережение), в другом — просто нехватка памяти. А, поскольку, класс 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. Включить тактирование
  2. Произвести настройку

В статье говорится о том, как эффективно и обобщенно реализовать пункт 1, независимо от контроллера.
Вместе с тем, совсем без каких-либо примеров, — текст был бы очень сложен для восприятия, поэтому введено уточнение: stm32f103c8, SPI и USART, при этом данные пересылаются с использованием DMA.

Для функционирования аппаратных интерфейсов требуется устройство ввода-вывода, т.е. пины. Эти пины могут относится к разным портам (Порт А, Порт В, ...), которые также требуется включить(подать тактирование).

Если немного перефразировать исходную задачу под Ваши условия, то:
Требуется выводить температуру от датчика, подключенного по UART, на семисегментный индикатор по SPI через сдвиговый регистр 74HC595.

Разбор этой задачи в статье закончился на этапе включении тактирования UART, SPI и сопутствующей периферии. Показаны различные способы это сделать, в том числе на C++.

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

Ремап также можно победить, если использовать static_assert времени компиляции. А условия для периферии и пинов придется прописывать либо с мануалом в руках, либо парсить документы производителя.
У меня не получилось, если речь идет про сравнение прямой записи в регистры и «функции инициализации» :) Ссылка
Во втором случае добавляется повторная модификация volatile регистров, что, естественно, увеличит размер листинга.
Да, я о том же :)
Только я предлагаю вынести тактирование и пины за пределы периферии, чтобы код стал еще меньше.

Рассмотрим, для упрощения spi и пины, без дма и т.д.
В коде, пин выглядит примерно так:
Пин
template<auto _gpioBase, auto _pinNumber, auto _config = 0>
struct Pin{

  static constexpr auto gpioBase = _gpioBase;
  static constexpr auto pinNumber = _pinNumber;
  static constexpr auto config = _config;

  using power = Valuelist<
    0, // Первый регистр тактирования
    0, // Второй регистр тактирования
    gpioBase == GPIOA_BASE ? 2 : 0 // Выбираем соответствующий порту бит
    >;
  // Задаем новую функцию 
  template<auto configNew>
  using pinFunc = Pin<gpioBase, pinNumber, configNew>;

  // Что-то еще - инициализация, опрос, установка...
};
// И определены, допустим все так
...
using PA12 = Pin<GPIOA_BASE, 12>
...


Далее spi
SPI
template<typename MOSI, typename MISO, typename SCK>
struct SPI{
  using powerSPI = Valuelist<
    0, // Выбираем биты, соответствующие SPI
    16384,
    0
    >;

    // "складываем" значения пинов и собственно SPI
  using power = lists_termwise_or_t<
        typename MOSI::power, 
        typename MISO::power, 
        typename SCK::power, powerSPI>;

   // Задаем функцию пинов
  using pins = Typelist<
        typename MOSI::template pinFunc<AF>, 
        typename MISO::template pinFunc<AF>, 
        typename SCK:: template pinFunc<AF>>;

  //static_assert() - Производим всякие проверки, в т.ч. на корректность пинов 
};


И пинлист
Пинлист
template<typename... Peripherals>
struct Pinlist{

// приводим список <Typelist<Pin1, Pin2...>, ..., Typelist<Pin_n, Pin_m>>
// к виду Typelist<Pin1, Pin2, ..., Pin_n, Pin_m>
  using pins = lists_expand_t<typename Peripherals::pins...>

// Работаем со списком - проверяем уникальность, распределяем пины по портам
  void Init();
};


int main(){
  using spi2 = SPI<PA12, PC2, PC3>;
  using spi4 = SPI<PE4, PE12, PE5>;
 
   // соответственно происходит только reg1 |= GPIOA | GPIOC | GPIOE
  //                                   reg2 |= SPI2 | SPI4
  Power::Enable<spi2, spi4>(); 

// Происходит распределение пинов по всем портам
// В каждый порт будет происходить запись только 1 раз
// Условно 
//GPIOA_CONF = (GPIOA_CONF  & (~maskA)) | conf_Pin12;
//GPIOC_CONF = (GPIOC_CONF  & (~maskC)) | conf_Pin2 | conf_Pin3;
//GPIOE_CONF = (GPIOE_CONF  & (~maskE)) | conf_Pin4 | conf_Pin5 | conf_Pin12;
  Pinlist<spi2, spi4>::Init();

  spi2::Init();
  spi4::Init();

}
Да, конкретно в этом случае выигрыш не сильно заметен. Но если добавить около 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();
}
Если брать за основу код из статьи, то до Pinlist' ов lamerka всего где-то 100 строчек.

Но, да, конечно, — трехэтажные макросы и метафункции выглядят монструозно, с этим трудно спорить.

1

Information

Rating
Does not participate
Registered
Activity