Comments 40
Сам задумывался над чем-то подобным. Только знаний С++ для реализации не хватило.
Частично решал проблему с помощью макросов вроде:
#define WRITE_BITFIELD(MOD, N, REG, FIELD, VAL) { (MOD ## N)->REG = ((MOD ## N)->REG &= ~(MOD ## _ ## REG ## _ ## FIELD)) | \ (uint32_t)(VAL) << (MOD ## _ ## REG ## _ ## FIELD ## _ ## Pos); }
Что выливалось в код:
WRITE_BITFIELD(I2C, 1, TIMINGR, PRESC, 0x0B)
Не оптимальное решение, но от ошибок вроде записи не в тот регистр спасало.
Теперь можно и ряд других ошибок отлавливать.
Можно еще доработать подход и использовать его для работы с регистрами различных ИС подключаемых посредством I2C или SPI, или пр. Писать придется в переменную, а не по адресу. А уже потом эту переменную передавать.
18 линий ассемблера/facepalm, с каким пор строки стали называться линиями?
Эта статья великолепна! У меня наконец появилась ссылка, которой можно кидать в тех, кто говорит, что плюсы это оверхед.
Мне больше понравилась вот эта статья трехлетней давности:
habr.com/ru/post/357910
Ваша статья прекрасна, я отметил что для общения с фанатиками она точно не подойдет.
Добрый день!
Понравилась Ваша статья. Это очень здорово, когда используя строгую типизацию, минимизируется вероятность ошибки. Тоже хотелось получить нечто подобное, но с возможностью в Runtime менять адрес регистра, и/или значение полей. Тоже хотелось, чтобы была возможно модификации сразу нескольких полей. Как считаете, можно ли что-либо изменить/оптимизировать в этом коде: https://github.com/hwswdevelop/MemoryMappedRegAccess
Если можно, в личку или на e-mail: evgeny@vrnnet.ru, +79003030374 (Telegram, WhatsApp).
Не думали-ли вы над реализаций различных представлений для одних и тех же полей? Например, в одних случаях порт ввода/вывода полезно представить как набор отдельных пинов. В других, на него может понадобиться выводить 8-битное целое число. Насколько я понимаю, сейчас доступен только второй вариант, в то время как со стандартным подходом можно реализовать оба варианта.
Про остальное пока не думал, задача была, по большому счету сделать так, чтобы студенты ошибок меньше делали. А то на это уходило много времени у меня, ещё ведь нормальная работа есть, помимо обучения. Хотелось минимизировать трату времени на студентов, которые документацию читают сквозь пальцы.
#include "gpioaregisters.hpp" //for GPIOA
#include "rccregisters.hpp" //for RCC
int main()
{
RCC::AHB1ENR::GPIOAEN::Enable::Set() ;
GPIOA::MODER::MODER15::Output::Set() ;
}
Шаблон используется только при установке сразу нескольких битов, но опять же можно это перенести в функцию, чтобы было понятнее, тому кто с шаблонами не знаком, на Medium оптимизации уже точно передачи параметров через стек не будет, так как они могут быть вычислены компилятором.
Без комментариев тут не обойтись. Хотя код всего-то устанавливает скорость работы порта GPIOA.0 в значение 40 Мгц
тут не совсем так, скорее даже совсем не так. Порт как работал со «скоростью» шины AHB так и будет работать. Регистр OSPEEDR определяет скорость нарастания фронта на выводе настроенном как выход. Кстати, в более новых поделиях этой фирмы, они заменили конкретные цифры на абстракции вида VeryLow\Low\Medium\High или их вариации.
Но, как я уже говорил выше, не все производители заботятся о своих потребителях, поэтому не у всех в файле SVD описаны перечисления, из-за этого для ST микроконтроллеров все перечисления, после генерации выглядят примерно так
я наверно Вас удивлю сейчас, но для некоторых МК st не то что не описали перечисления, они некоторые периферийные модули забыли описать. Так что полностью автоматизировать вряд ли получится. Для примера реальная история с stm32l4r4zi и попыткой найти DMAMUX в svd файле:
с момента публикации прошло полгода, исправления так и не было… за
это время были найдены ещё несколько ошибок в этом файле.
Кстати, я не смог и просто Си заголовочник найти для STM32L4.
Из-за своего названия этот регистр много путаницы вносит в понимание работы такого просто модуля, как GPIO.
как правило заголовочники можно вытащить из IDE, которая пользуется отдельными паками для вендоров (Keil/Segger Embedded Studio например), ну или выкачивая монструозные библиотеки HAL\LL на все семейство.
Да, но там же в спецификации есть и параметр Максимальная частота. Т. Е. установка битов влияет на оба параметры, и они взаимосвязаны. Можно ли сказать, что просто максимальную частоту?
но опять же, смотрите. Вот выставили мы например значение 00 в регистр. Это же не означает, что МК не сможет и не позволит вывести через данный пин сигнал в 40МГц. Меандр будет на выходе, но очень уж близкий к синусу. Если Вам нужно работать с цифровым интерфейсом, то это проблема. Вот и получается, что это не сколько частота работы (максимальная\разрешенная), сколько признак того насколько меандр выведенный с данного пина будет меандром.
я надеюсь Вы понимаете что я не пытаюсь придираться)
Например для таймера это поле называется CEN, и просто запускает таймер в работу. А вот для qspi есть сразу четыре внутренних триггера. Которые явно зависят от состояния полей. Установка QUADSPI_CR_EN — лишь разрешает работу этих триггеров, всё остальное требует строгой последовательности работы с регистрами и данными.
Причём для qspi, также как и для sd интерфейса — ошибки в алгоритме в работе с регистрами приводят к потери информации.
Получается что ваш метод позволяет работать только с синхронными интерфейсами, которых не так уж и много.
Пытался сделать что-то подобное, правда, дальше экспериментов дело не дошло.
Вместо параметра шаблона, определяющего режим доступа и enable_if
банально наследовал регистры от от чего-то вроде RegR
с определенным методом get
и/или RegW
с методом set
.
Также я обнаружил вот такую проблему. Например, мы хотим работать с GPIO используя номера пинов в рантайме. И здесь поле регистра уже не получится определить на этапе компиляции.
template<uint32_t address, size_t size, typename AccessMode>
struct RegisterBase
наверно лучше uintptr_t address
Я что-то не уловил. Можете уточнить. Кроме того, насколько я помню, этот тип поддерживается опционально, т.е. компилятор может его и не поддерживать.
Во вторых, будут проблемы, если ваш код когда-нибудь попадёт, например, на машину с 64-битной адресацией.
Насчёт поддержки в реальности на всех платформах не знаю, но если sizeof(uint32_t) == sizeof(void*), то не добавить sizeof(uintptr_t) == sizeof(uint32_t) как-то тупо.
Ну и выглядит это как «защита детей», когда на все что только можно клеятся защитные шалабушки, чтобы не ударился. В результате дети становятся непуганные и несамостоятельные. А если бы пару раз вдарившись, стали бы думать прежде чем делать.
Кроме того, производители софта не торопятся делиться им бесплатно. Откуда у вас KEIL и IAR на cm4? В суровой реальности, если софт не краденный, то это gcc + эклипс или иной редактор.
Откуда у вас KEIL и IAR на cm4? В суровой реальности, если софт не краденный, то это gcc + эклипс или иной редактор.Всё не так однозначно, у IAR есть триальная лицензия с ограничением по размеру прошивки, пригодная для учебных целей.
assert(value < ((1 << size) - 1)) ;
Не совсем понял, эта проверка для поля шириной, например, 3 бита не разрешит ввести значение 7. Так и задумано? Или я что-то недогнал по позднему времени?
template<std::uint32_t base_addr>
class PerBase
{
public:
static inline void* get_ptr() { return (reinterpret_cast<void*>(base_addr)); }
};
template<typename BaseType, volatile std::uint32_t BaseType::periph_t::* field, typename RegType>
class RegBase
{
public:
RegType value;
void Read()
{
value.dw = get_value();
}
void Write()
{
set_value(value.dw);
}
static std::uint32_t get_value()
{
return (reinterpret_cast<typename BaseType::periph_t*>(BaseType::get_ptr()))->*field;
}
static void set_value(std::uint32_t value)
{
(reinterpret_cast<typename BaseType::periph_t*>(BaseType::get_ptr()))->*field = value;
}
};
template <typename Register, typename Type, std::uint32_t width, std::uint32_t offset>
class ValBase
{
private:
constexpr static std::uint32_t Mask = ((1U << width) - 1U);
constexpr static std::uint32_t Offset = offset;
public:
static Type Get()
{
std::uint32_t value = Register::get_value();
return static_cast<Type>((value >> Offset) & Mask);
}
static void Set(Type value)
{
std::uint32_t old_value = Register::get_value() & ~(Mask << Offset);
std::uint32_t new_value = (static_cast<std::uint32_t>(value) << Offset);
Register::set_value(old_value | new_value);
}
};
/* Reset and clock control */
class RCC_REG : public PerBase<RCC_BASE>
{
public:
struct periph_t
{
__O std::uint32_t CR;
__O std::uint32_t CFGR;
__O std::uint32_t CIR;
__IO std::uint32_t APB2RSTR;
__IO std::uint32_t APB1RSTR;
__IO std::uint32_t AHBENR;
__IO std::uint32_t APB2ENR;
__IO std::uint32_t APB1ENR;
__O std::uint32_t BDCR;
__O std::uint32_t CSR;
};
union CR_t
{
uint32_t dw;
struct
{
bool HSION : 1; /* Internal High Speed clock enable */
bool HSIRDY : 1; /* Internal High Speed clock ready flag */
std::uint32_t : 1;
std::uint32_t HSITRIM : 5; /* Internal High Speed clock trimming */
std::uint32_t HSICAL : 8; /* Internal High Speed clock Calibration */
bool HSEON : 1; /* External High Speed clock enable */
bool HSERDY : 1; /* External High Speed clock ready flag */
bool HSEBYP : 1; /* External High Speed clock Bypass */
bool CSSON : 1; /* Clock Security System enable */
std::uint32_t : 4;
bool PLLON : 1; /* PLL enable */
bool PLLRDY : 1; /* PLL clock ready flag */
std::uint32_t : 6;
} bt;
};
union CFGR_t
{
struct
{
std::uint32_t SW : 2; /* System clock Switch */
std::uint32_t SWS : 2; /* System Clock Switch Status */
std::uint32_t HPRE : 4; /* AHB prescaler */
std::uint32_t PPRE1 : 3; /* APB Low speed prescaler (APB1) */
std::uint32_t PPRE2 : 3; /* APB High speed prescaler (APB2) */
std::uint32_t ADCPRE : 2; /* ADC prescaler */
std::uint32_t PLLSRC : 1; /* PLL entry clock source */
std::uint32_t PLLXTPRE : 1; /* HSE divider for PLL entry */
std::uint32_t PLLMUL : 4; /* PLL Multiplication Factor */
std::uint32_t OTGFSPRE : 1; /* USB OTG FS prescaler */
std::uint32_t : 1;
std::uint32_t MCO : 3; /* Microcontroller clock output */
std::uint32_t : 5;
} bt;
uint32_t dw;
};
public:
class CR : public RegBase<RCC_REG, &periph_t::CR, CR_t>
{
public:
using HSION = ValBase<CR, bool, 1, 0>;
using HSIRDY = ValBase<CR, bool, 1, 1>;
using HSITRIM = ValBase<CR, std::uint32_t, 5, 3>;
};
/*Clock configuration register (RCC_CFGR)*/
class CFGR : public RegBase<RCC_REG, &periph_t::CFGR, CFGR_t>
{
public:
using SWS = ValBase<CFGR, std::uint32_t, 2, 2>;
};
};
Как пример использования (абстрактный код, использовался для проверок компиляции):
STMX::RCC_REG::CR cr;
cr.Read();
cr.value.bt.HSITRIM = 5;
cr.value.bt.HSION = true;
cr.Write();
while (STMX::RCC_REG::CFGR::SWS::Get() == 1);
После компиляции получил следующее:
080000ec <App::entry_point()>:
_ZN3App11entry_pointEv():
80000ec: 4907 ldr r1, [pc, #28] ; (800010c <App::entry_point()+0x20>)
80000ee: 680b ldr r3, [r1, #0]
80000f0: f003 0206 and.w r2, r3, #6
80000f4: f042 0229 orr.w r2, r2, #41 ; 0x29
80000f8: f362 0307 bfi r3, r2, #0, #8
80000fc: 4a04 ldr r2, [pc, #16] ; (8000110 <App::entry_point()+0x24>)
80000fe: 600b str r3, [r1, #0]
8000100: 6813 ldr r3, [r2, #0]
8000102: f3c3 0381 ubfx r3, r3, #2, #2
8000106: 2b01 cmp r3, #1
8000108: d0fa beq.n 8000100 <App::entry_point()+0x14>
800010a: e7fe b.n 800010a <App::entry_point()+0x1e>
800010c: 40021000 .word 0x40021000
8000110: 40021004 .word 0x40021004
Как ни крутил, так и не смог избавиться от хранения 2-х адресов в памяти.
Да, пока что без уникальных перечислений, но я только базис сделал.
Основная цель — уменьшение кода в более обычных сценариях вида «прочитать-модифицировать_поля-записать-модифицировать-записать».
Позже модифицирую свой генератор под данные шаблоны — удобнее, чем имеющиеся в данный момент.
И да — код не претендует на образцовость, поскольку с шаблонами знаком поверхностно…
Безопасный доступ к полям регистров на С++ без ущерба эффективности (на примере CortexM)