Fig. tomado de www.extremetech.com/wp-content/uploads/2016/07/MegaProcessor-Feature.jpgBuena salud a todos!
En un artículo anterior, examiné la cuestión del acceso a los registros de un microcontrolador con un núcleo CortexM en C ++ y mostré soluciones simples a algunos de los problemas.
Hoy quiero mostrar la idea de cómo hacer que el acceso al registro y sus campos sea seguro sin sacrificar la eficiencia, usando clases C ++ generadas a partir de archivos SVD.
Todos los que te interesan, bienvenidos a cat. Habrá mucho código.
Introduccion
En un artículo de
C ++ Hardware Register Access Redux , Ken Smith mostró cómo trabajar de manera segura y eficiente con los registros, e incluso lo mostró con
github.com/kensmith/cppmmio como ejemplo.
Luego, varias personas desarrollaron esta idea, por ejemplo,
Niklas Hauser hizo una revisión maravillosa y sugirió varias formas más de acceder de forma segura a los registros.
Algunas de estas ideas ya se han implementado en varias bibliotecas, en particular en
modm . Para bien, puedes usar todas estas oraciones en la vida real. Pero en el momento del desarrollo de estas bibliotecas, las descripciones de periféricos y registros acababan de comenzar a estandarizarse y, por lo tanto, algunas cosas se hicieron con el objetivo de que el trabajo principal en la descripción de los registros fuera con el programador. Además, algunas soluciones no son efectivas en términos de código y recursos de microcontroladores.
Hoy, cada fabricante de un microcontrolador ARM proporciona una descripción de todos los registros en formato SVD. Los archivos de encabezado se pueden generar a partir de estas descripciones; por lo tanto, es posible crear no una descripción simple de los registros, sino una descripción más compleja, pero al mismo tiempo, lo que aumentará la confiabilidad de su código. Y es genial que el archivo de salida pueda estar en cualquier lenguaje, C, C ++ o incluso
DPero tomemos en orden lo que generalmente es un acceso seguro a los registros y por qué es necesario. La explicación puede mostrarse en ejemplos sintéticos simples, muy probablemente improbables, pero bastante posibles:
int main(void) {
Todos estos casos son posibles en la práctica, y definitivamente vi algo así de mis alumnos. Sería genial si pudieras prohibir cometer tales errores.
También me parece que es mucho más agradable cuando el código se ve bien y no necesita comentarios. Por ejemplo, incluso si conoce bien el microcontrolador STM32F411, no siempre es posible comprender lo que sucede en este código:
int main() { uint32 temp = GPIOA->OSPEEDR ; temp &=~ GPIO_OSPEEDR_OSPEED0_Msk ; temp = (GPIO_OSPEEDR_OSPEED0_0 | GPIO_OSPEEDR_OSPEED0_1) ; GPIOA->OSPEEDR = temp; }
Ningún comentario aquí no puede hacer. El código establece la frecuencia de funcionamiento del puerto GPIOA.0 al máximo (aclaración de
mctMaks : de hecho, este parámetro afecta el tiempo de subida del frente (es decir, su inclinación) y significa que el puerto puede procesar una señal digital normalmente en un determinado (Muy bajo \ Bajo \ Medio \ Alta) frecuencia).
Tratemos de deshacernos de estas deficiencias.
Registrar abstracción
En primer lugar, debe averiguar cuál es el registro desde el punto de vista del programador y el programa.
El registro tiene una dirección, longitud o tamaño, modo de acceso: algunos registros pueden escribirse, otros solo pueden leerse y la mayoría pueden leerse y escribirse.
Además, el registro se puede representar como un conjunto de campos. Un campo puede constar de uno o varios bits y está ubicado en cualquier parte del registro.
Por lo tanto, las siguientes características de campo son importantes para nosotros: longitud o tamaño (
ancho o
tamaño ), desplazamiento desde el comienzo del registro (
desplazamiento ) y valor.
Los valores de campo son el espacio de todas las cantidades posibles que el campo puede tomar y depende de la longitud del campo. Es decir si el campo tiene una longitud de 2, entonces hay 4 valores de campo posibles (0,1,2,3). Al igual que el registro, los campos y los valores de campo tienen un modo de acceso (lectura, escritura, lectura y escritura)
Para hacerlo más claro, tomemos el registro TIM1 CR1 del microcontrolador STM32F411. Esquemáticamente, se ve así:

- Bit 0 CEN: Habilitar contador
0: Contador habilitado: Deshabilitar
1: Contador apagado: Habilitar
- UDIS Bit 1: Activar / Desactivar evento UEV
0: Evento UEV habilitado: Habilitar
1: evento UEV desactivado: deshabilitado
- Bit 2 de URS: Seleccione fuentes de generación de eventos UEV
0: UEV se genera al desbordarse o al configurar el UG: cualquier bit
1: UEV se genera solo en desbordamiento: desbordamiento
- Bit 3 OPM: Operación única
0: el temporizador continúa contando más después del evento UEV: ContinueAfterUEV
1: el temporizador se detiene después del evento UEV: StopAfterUEV
- Bit 4 DIR: Dirección de conteo
0: Cuenta directa: Upcounter
1: Cuenta atrás : cuenta atrás
- Bit 6: 5 CMS: Modo de alineación
0: Modo de alineación 0: CenterAlignedMode0
1: Modo de alineación 1: CenterAlignedMode1
2: Modo de alineación 2: CenterAlignedMode2
3: Modo de alineación 3: CenterAlignedMode3
- Bit 7 APRE: Modo de precarga para registro ARR
0: el registro TIMx_ARR no está protegido: ARRNotBuffered
1: el registro TIMx_ARR no está protegido: ARRBuffered
- Bit 8: 9 CKD: divisor de reloj
0: tDTS = tCK_INT: ClockDevidedBy1
1: tDTS = 2 * tCK_INT: ClockDevidedBy2
2: tDTS = 4 * tCK_INT: ClockDevidedBy4
3: Reservado: Reservado
Aquí, por ejemplo, CEN es un campo de 1 bit con un desplazamiento de 0 relativo al comienzo del registro. Y
Enable (1) y
Disable (0) son sus posibles valores.
No nos concentraremos en lo que cada campo de este registro es específicamente responsable, es importante para nosotros que cada campo y valor de campo tenga un nombre que lleve una carga semántica y desde el cual podamos entender en principio lo que hace.
Debemos tener acceso tanto al registro como al campo y su valor. Por lo tanto, en una forma muy aproximada, la abstracción de registros se puede representar mediante las siguientes clases:

Además de las clases, también es importante para nosotros que los registros y los campos individuales tengan ciertas propiedades, el registro tiene una dirección, tamaño, modo de acceso (solo lectura, solo escritura o ambos).
El campo tiene tamaño, desplazamiento y también modo de acceso. Además, el campo debe contener un enlace al registro al que pertenece.
El valor del campo debe tener un enlace al campo y un atributo adicional: el valor.
Por lo tanto, en una versión más detallada, nuestra abstracción se verá así:

Además de los atributos, nuestra abstracción debe tener métodos de modificación y acceso. Por simplicidad, nos limitamos a los métodos de instalación / escritura y lectura.

Cuando decidimos la abstracción de casos, debemos verificar cómo esta abstracción corresponde a lo que se describe en el archivo SVD.
Archivo de descripción de vista del sistema (SVD)
El formato de descripción de presentación del sistema CMSIS (CMSIS-SVD) es una descripción formal de los registros de microcontroladores basados en el procesador ARM Cortex-M. La información contenida en las descripciones de la representación del sistema, prácticamente corresponde a los datos en los manuales de referencia para dispositivos. La descripción de los registros en dicho archivo puede contener información de alto nivel y el propósito de un solo bit del campo en el registro.
Esquemáticamente, los niveles de detalle en dicho archivo se pueden describir mediante el siguiente esquema,
tomado en el sitio web de Keil :

Descripción Los archivos SVD son suministrados por los fabricantes y se utilizan durante la depuración para mostrar información sobre el microcontrolador y los registros. Por ejemplo, el IAR los usa para mostrar información en el panel Ver-> Registros. Los archivos mismos están en la carpeta Archivos de programa (x86) \ IAR Systems \ Embedded Workbench 8.3 \ arm \ config \ debugger.
Clion de JetBrains también usa archivos svd para mostrar información de registro durante la depuración.
Siempre puede descargar descripciones de los sitios del fabricante.
Aquí puede tomar el archivo SVD para el microcontrolador STM32F411En general, el formato SVD es un estándar que admiten los fabricantes. Veamos cuáles son los niveles de descripción en SVD.
En total, se distinguen 5 niveles, nivel de dispositivo, nivel de microcontrolador, nivel de registro, nivel de campo, nivel de valores enumerados.
- Nivel del dispositivo : la descripción del nivel superior de la vista del sistema es el dispositivo. En este nivel, se describen las propiedades relacionadas con el dispositivo en su conjunto. Por ejemplo, el nombre, la descripción o la versión del dispositivo. La unidad mínima direccionable, así como la profundidad de bits del bus de datos. Los valores predeterminados para los atributos del registro, como el tamaño del registro, el valor de reinicio y los permisos de acceso, se pueden establecer para todo el dispositivo en este nivel y se heredan implícitamente en los niveles inferiores de descripción.
- Nivel de microcontrolador: la sección CPU describe el núcleo del microcontrolador y sus características. Esta sección es obligatoria si el archivo SVD se usa para crear el archivo de encabezado del dispositivo.
- Capa periférica : un dispositivo periférico es una colección con nombre de registros. Un dispositivo periférico se asigna a una dirección base específica en el espacio de direcciones del dispositivo.
- Nivel de registro : un registro es un recurso programable con nombre que pertenece a un dispositivo periférico. Los registros se asignan a una dirección específica en el espacio de direcciones del dispositivo. La dirección es relativa a la dirección periférica base. Además, para el registro, se indica el modo de acceso (lectura / escritura).
- Nivel de campo : como se mencionó anteriormente, los registros se pueden dividir en partes de bits de diferente funcionalidad: campos. Este nivel contiene los nombres de los campos que son únicos dentro del mismo registro, su tamaño, las compensaciones relativas al comienzo del registro, así como el modo de acceso.
- El nivel de los valores de campo enumerados : de hecho, estos son valores de campo con nombre que se pueden usar por conveniencia en C, C ++, D, etc.
De hecho, los archivos SVD son archivos xml normales con una descripción completa del sistema. Hay convertidores de archivos svd a código C, por ejemplo
aquí , que generan encabezados y estructuras compatibles con C para cada periferia y registro.
También hay un analizador de archivos SVD
cmsis-svd escrito en Phyton que hace algo así como deserializar datos de un archivo en objetos de clase Phython, que luego se usan convenientemente en su programa de generación de código.
Un ejemplo de la descripción del registro del microcontrolador STM32F411 se puede ver debajo del spoiler:
Ejemplo de registro CR1 temporizador TIM1 <peripheral> <name>TIM1</name> <description>Advanced-timers</description> <groupName>TIM</groupName> <baseAddress>0x40010000</baseAddress> <addressBlock> <offset>0x0</offset> <size>0x400</size> <usage>registers</usage> </addressBlock> <registers> <register> <name>CR1</name> <displayName>CR1</displayName> <description>control register 1</description> <addressOffset>0x0</addressOffset> <size>0x20</size> <access>read-write</access> <resetValue>0x0000</resetValue> <fields> <field> <name>CKD</name> <description>Clock division</description> <bitOffset>8</bitOffset> <bitWidth>2</bitWidth> </field> <field> <name>ARPE</name> <description>Auto-reload preload enable</description> <bitOffset>7</bitOffset> <bitWidth>1</bitWidth> </field> <field> <name>CMS</name> <description>Center-aligned mode selection</description> <bitOffset>5</bitOffset> <bitWidth>2</bitWidth> </field> <field> <name>DIR</name> <description>Direction</description> <bitOffset>4</bitOffset> <bitWidth>1</bitWidth> </field> <field> <name>OPM</name> <description>One-pulse mode</description> <bitOffset>3</bitOffset> <bitWidth>1</bitWidth> </field> <field> <name>URS</name> <description>Update request source</description> <bitOffset>2</bitOffset> <bitWidth>1</bitWidth> </field> <field> <name>UDIS</name> <description>Update disable</description> <bitOffset>1</bitOffset> <bitWidth>1</bitWidth> </field> <field> <name>CEN</name> <description>Counter enable</description> <bitOffset>0</bitOffset> <bitWidth>1</bitWidth> </field> </fields> </register> <register>
Como puede ver, existe toda la información necesaria para nuestra abstracción, excepto la descripción de los valores de bits específicos de los campos.
No todos los fabricantes quieren dedicar tiempo a una descripción completa de su sistema, por lo tanto, como puede ver, ST no quería describir los valores de campo y transfirió esta carga a los programadores de los clientes. Pero TI se ocupa de sus clientes y describe el sistema por completo, incluidas las descripciones de los valores de campo.
Lo anterior muestra que el formato de la descripción SVD es muy consistente con nuestra abstracción de casos. El archivo contiene toda la información necesaria para describir completamente el registro.
Implementación
Registrarse
Ahora que hemos hecho una abstracción del registro y tenemos una descripción de los registros en forma de svd de los fabricantes que es ideal para esta abstracción, podemos ir directamente a la implementación.
Nuestra implementación debe ser tan efectiva como el código C y fácil de usar. Me gustaría que el acceso a los registros se vea lo más claro posible, por ejemplo, así:
if (TIM1::CR1::CKD::DividedBy2::IsSet()) { TIM1::ARR::Set(10_ms) ; TIM1::CR1::CEN::Enable::Set() ; }
Recuerde que para acceder a la dirección de registro de enteros, debe usar reinterpret_cast:
*reinterpret_cast<volatile uint32_t *>(0x40010000) = (1U << 5U) ;
La clase de registro ya se ha descrito anteriormente, debe tener una dirección, tamaño y modo de acceso, así como dos métodos
Get()
y
Set()
:
Pasamos la dirección, la longitud del registro y el modo de acceso a los parámetros de la plantilla (también es una clase). Usando el mecanismo
SFINAE , es decir, la
enable_if
enable_if, "desecharemos" las funciones de acceso
Set()
u
Get()
para los registros que no deberían admitirlas. Por ejemplo, si el registro es de solo lectura, entonces pasaremos
ReadMode
tipo
ReadMode
al parámetro de plantilla,
enable_if
verificará si el acceso es el sucesor de
ReadMode
y, de lo contrario, creará un error controlado (no se puede mostrar el tipo T), y el compilador no incluirá el método
Set()
para tal registro. Lo mismo ocurre con un registro destinado a escritura únicamente.
Para el control de acceso usaremos las clases:
Los registros vienen en diferentes tamaños: 8, 16, 32, 64 bits. Para cada uno de ellos, establecemos nuestro tipo:
Tipo de registros según tamaño template <uint32_t size> struct RegisterType {} ; template<> struct RegisterType<8> { using Type = uint8_t ; } ; template<> struct RegisterType<16> { using Type = uint16_t ; } ; template<> struct RegisterType<32> { using Type = uint32_t ; } ; template<> struct RegisterType<64> { using Type = uint64_t ; } ;
Después de eso, para el temporizador TIM1, puede definir el registro CR1 y, por ejemplo, el registro EGR de esta manera:
struct TIM1 { struct CR1 : public RegisterBase<0x40010000, 32, ReadWriteMode> { } struct EGR : public RegisterBase<0x40010014, 32, WriteMode> { } } int main() { TIM1::CR1::Set(10) ; auto reg = TIM1::CR1::Get() ;
Como el compilador muestra el método
Get()
solo para los registros en los que el modo de acceso se hereda de
ReadMode
, y los métodos
Set()
para los registros en los que el modo de acceso se hereda de
WriteMode
, en caso de uso incorrecto de los métodos de acceso, recibirá un error en la etapa de compilación. Y si utiliza herramientas de desarrollo modernas, como Clion, incluso en la etapa de codificación verá una advertencia del analizador de código:

Bueno, el acceso a los registros ahora se ha vuelto más seguro, nuestro código no le permite hacer cosas que son inaceptables para este registro, pero queremos ir más allá y referirnos no a todo el registro, sino a sus campos.
Campos
El campo en lugar de la dirección tiene un valor de desplazamiento relativo al comienzo del registro. Además, para conocer la dirección o el tipo al que debe llevarse el valor del campo, debe tener un enlace al registro:
Después de eso, ya es posible hacer lo siguiente:
struct TIM1 { struct CR1 : public RegisterBase<0x40010000, 32, ReadWriteMode> { using CKD = RegisterField<TIM1::CR1, 8, 2, ReadWriteMode> ; using ARPE = RegisterField<TIM1::CR1, 7, 1, ReadWriteMode> ; using CMS = RegisterField<TIM1::CR1, 5, 2, ReadWriteMode> ; using DIR = RegisterField<TIM1::CR1, 4, 1, ReadWriteMode> ; using OPM = RegisterField<TIM1::CR1, 3, 1, ReadWriteMode> ; using URS = RegisterField<TIM1::CR1, 2, 1, ReadWriteMode> ; using UDIS = RegisterField<TIM1::CR1, 1, 1, ReadWriteMode> ; using CEN = RegisterField<TIM1::CR1, 0, 1, ReadWriteMode> ; } } int main() {
Aunque todo se ve bastante bien en general, todavía no está del todo claro qué
TIM1::CR1::CKD::Set(2)
, ¿qué significan los dos mágicos pasados a la función
Set()
? ¿Y qué significa el número devuelto por el
TIM1::CR1::CEN::Get()
?
Pase sin problemas a los valores de campo.
Valor del campo
La abstracción de un valor de campo es esencialmente también un campo, pero capaz de aceptar solo un estado. Los atributos se agregan a la abstracción del campo: el valor real y un enlace al campo. El método
Set()
para establecer el valor del campo es idéntico al método
Set()
para establecer el campo, excepto que el valor en sí no necesita pasar al método, se conoce de antemano, solo necesita establecerse. Pero el método
Get()
no tiene ningún sentido; en cambio, es mejor verificar si este valor está establecido o no, reemplazar este método con el método
IsSet()
.
El campo de registro ahora se puede describir mediante un conjunto de sus valores:
Valores de los campos de registro CR1 del temporizador TIM1 template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_CKD_Values: public RegisterField<Reg, offset, size, AccessMode> { using DividedBy1 = FieldValue<TIM_CR_CKD_Values, 0U> ; using DividedBy2 = FieldValue<TIM_CR_CKD_Values, 1U> ; using DividedBy4 = FieldValue<TIM_CR_CKD_Values, 2U> ; using Reserved = FieldValue<TIM_CR_CKD_Values, 3U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_ARPE_Values: public RegisterField<Reg, offset, size, AccessMode> { using ARRNotBuffered = FieldValue<TIM_CR_ARPE_Values, 0U> ; using ARRBuffered = FieldValue<TIM_CR_ARPE_Values, 1U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_CMS_Values: public RegisterField<Reg, offset, size, AccessMode> { using CenterAlignedMode0 = FieldValue<TIM_CR_CMS_Values, 0U> ; using CenterAlignedMode1 = FieldValue<TIM_CR_CMS_Values, 1U> ; using CenterAlignedMode2 = FieldValue<TIM_CR_CMS_Values, 2U> ; using CenterAlignedMode3 = FieldValue<TIM_CR_CMS_Values, 3U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_DIR_Values: public RegisterField<Reg, offset, size, AccessMode> { using Upcounter = FieldValue<TIM_CR_DIR_Values, 0U> ; using Downcounter = FieldValue<TIM_CR_DIR_Values, 1U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_OPM_Values: public RegisterField<Reg, offset, size, AccessMode> { using ContinueAfterUEV = FieldValue<TIM_CR_OPM_Values, 0U> ; using StopAfterUEV = FieldValue<TIM_CR_OPM_Values, 1U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_URS_Values: public RegisterField<Reg, offset, size, AccessMode> { using Any = FieldValue<TIM_CR_URS_Values, 0U> ; using Overflow = FieldValue<TIM_CR_URS_Values, 1U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_UDIS_Values: public RegisterField<Reg, offset, size, AccessMode> { using Enable = FieldValue<TIM_CR_UDIS_Values, 0U> ; using Disable = FieldValue<TIM_CR_UDIS_Values, 1U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_CEN_Values: public RegisterField<Reg, offset, size, AccessMode> { using Disable = FieldValue<TIM_CR_CEN_Values, 0U> ; using Enable = FieldValue<TIM_CR_CEN_Values, 1U> ; } ;
Entonces el registro CR1 ya se describirá de la siguiente manera:
struct TIM1 { struct CR1 : public RegisterBase<0x40010000, 32, ReadWriteMode> { using CKD = TIM_CR1_CKD_Values<TIM1::CR1, 8, 2, ReadWriteMode> ; using ARPE = TIM_CR1_ARPE_Values<TIM1::CR1, 7, 1, ReadWriteMode> ; using CMS = TIM_CR1_CMS_Values<TIM1::CR1, 5, 2, ReadWriteMode> ; using DIR = TIM_CR1_DIR_Values<TIM1::CR1, 4, 1, ReadWriteMode> ; using OPM = TIM_CR1_OPM_Values<TIM1::CR1, 3, 1, ReadWriteMode> ; using URS = TIM_CR1_URS_Values<TIM1::CR1, 2, 1, ReadWriteMode> ; using UDIS = TIM_CR1_UDIS_Values<TIM1::CR1, 1, 1, ReadWriteMode> ; using CEN = TIM_CR1_CEN_Values<TIM1::CR1, 0, 1, ReadWriteMode> ; } ; }
Ahora puede establecer y leer directamente el valor del campo de registro: por ejemplo, si desea habilitar el temporizador en la cuenta, simplemente llame al método
Set()
en el valor
Enable
del campo CEN del registro CR1 del temporizador TIM1:
TIM1::CR1::CEN::Enable::Set() ;
. En el código, se verá así:
int main() { if (TIM1::CR1::CKD::DividedBy2::IsSet()) { TIM1::ARR::Set(100U) ; TIM1::CR1::CEN::Enable::Set() ; } }
A modo de comparación, lo mismo con el encabezado C: int main() { if((TIM1->CR1 & TIM_CR1_CKD_Msk) == TIM_CR1_CKD_0) { TIM1->ARR = 100U ; regValue = TIM1->CR1 ; regValue &=~(TIM_CR1_CEN_Msk) ; regValue |= TIM_CR1_CEN ; TIM1->CR1 = regValue ; } }
Entonces, se realizan las principales mejoras, podemos tener acceso simple y comprensible al registro, sus campos y valores. , , , , .
, . , :
int main() { uint32_t regValue = TIM1->CR1 ; regValue &=~(TIM_CR1_CKD_Msk | TIM_CR1_DIR) ; regValue |= (TIM_CR1_CEN | TIM_CR1_CKD_0 | TIM_CR1_CKD_0) ; TIM1->CR1 = regValue ; }
Set(...)
, , . Es decir :
int main() {
, , , , .
. :
, :
- , .
- .
constexpr , :
Set()
IsSet()
:
, :
int main() {
, - , , , ,
FieldValueBaseType
. ,
FieldValueBaseType
:
template<uint32_t address, size_t size, typename AccessMode, typename FieldValueBaseType, typename ...Args> class Register { private:
, SFINAE , , , , , .
CR1 TIM1, :
struct TIM1 { struct TIM1CR1Base {} ; struct CR1 : public RegisterBase<0x40010000, 32, ReadWriteMode> { using CKD = TIM_CR_CKD_Values<TIM1::CR1, 8, 2, ReadWriteMode, TIM1CR1Base> ; using ARPE = TIM_CR_ARPE_Values<TIM1::CR1, 7, 1, ReadWriteMode, TIM1CR1Base> ; using CMS = TIM_CR_CMS_Values<TIM1::CR1, 5, 2, ReadWriteMode, TIM1CR1Base> ; using DIR = TIM_CR_DIR_Values<TIM1::CR1, 4, 1, ReadWriteMode, TIM1CR1Base> ; using OPM = TIM_CR_OPM_Values<TIM1::CR1, 3, 1, ReadWriteMode, TIM1CR1Base> ; using URS = TIM_CR_URS_Values<TIM1::CR1, 2, 1, ReadWriteMode, TIM1CR1Base> ; using UDIS = TIM_CR_UDIS_Values<TIM1::CR1, 1, 1, ReadWriteMode, TIM1CR1Base> ; using CEN = TIM_CR_CEN_Values<TIM1::CR1, 0, 1, ReadWriteMode, TIM1CR1Base> ; } ; }
, , . , , , .
, :
:
int main(void) {
, , .
, , , ?
, ++, , 1 :
: int main() { uint32_t res = RCC->AHB2ENR; res &=~ RCC_AHB1ENR_GPIOAEN_Msk ; res |= RCC_AHB1ENR_GPIOAEN ; RCC->AHB2ENR = res ; res = GPIOA->MODER ; res &=~ (GPIO_MODER_MODER5 | GPIO_MODER_MODER4 | GPIO_MODER_MODER1) ; res |= (GPIO_MODER_MODER5_0 | GPIO_MODER_MODER4_0 | GPIO_MODER_MODER1_0) ; GPIOA->MODER = res ; GPIOA->BSRR = (GPIO_BSRR_BS5 | GPIO_BSRR_BS4 | GPIO_BSRR_BS1) ; return 0 ; }
++: int main() { RCC::AHB1ENR::GPIOAEN::Enable::Set() ; GPIOA::MODERPack< GPIOA::MODER::MODER5::Output, GPIOA::MODER::MODER4::Output, GPIOA::MODER::MODER1::Output>::Set() ; GPIOA::BSRRPack< GPIOA::BSRR::BS5::Set, GPIOA::BSRR::BS4::Set, GPIOA::BSRR::BS1::Set>::Write() ; return 0 ; }
IAR. : :
:

C++ :

18 , , .
, :

13 .
++ :

: , .
, . , ?
Tenemos acceso confiable, conveniente y rápido a los registros. Queda una pregunta. Cómo describir todos los registros, también hay menos de cien para el microcontrolador. Este es el tiempo que lleva describir todos los registros, porque puede cometer muchos errores en un trabajo tan rutinario. Sí, no necesita hacer esto manualmente. En su lugar, utilizaremos el generador de código del archivo SVD, que, como indiqué anteriormente al principio del artículo, cubre completamente la abstracción de registro que acepté.Finalicé el guión de un colega, que, en base a esta idea, hizo casi lo mismo, pero un poco más fácil usando enum en lugar de clases para valores de campo. El script está hecho solo para probar y verificar ideas, por lo tanto, no es óptimo, pero le permite generar algo como esto.
A quién le importa el guión está aquíResumen
, , . , gpioa rcc, :
#include "gpioaregisters.hpp"
, SVD , , .
, , , SVD , - ST , :
template <typename Reg, size_t offset, size_t size, typename AccessMode, typename BaseType> struct GPIOA_MODER_MODER_Values: public RegisterField<Reg, offset, size, AccessMode> { using Value0 = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 0U> ; using Value1 = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 1U> ; using Value2 = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 2U> ; using Value3 = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 3U> ; } ;
, Value, - :
template <typename Reg, size_t offset, size_t size, typename AccessMode, typename BaseType> struct GPIOA_MODER_MODER_Values: public RegisterField<Reg, offset, size, AccessMode> { using Input = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 0U> ; using Output = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 1U> ; using Alternate = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 2U> ; using Analog = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 3U> ; } ;
.
, ST , 0.
, , enum .
, .
IAR 8.40.1
« Online GDB»PS:
putyavka RegisterField::Get()
Ryppka assert.
Typesafe Register Access in C++One Approach to Using Hardware Registers in C++SVD Description (*.svd) Format