Usar C ++ y plantillas con un número variable de argumentos al programar microcontroladores

ARM con núcleo Cortex Mx (utilizando STM32F10x como ejemplo)


KDPV El microcontrolador ARM Cortex M3 STM32F103c8t6 se usa ampliamente como un microcontrolador de 32 bits para proyectos de aficionados. Como para casi cualquier microcontrolador, hay un SDK para él, que incluye archivos de encabezado C ++ para determinar la periferia del controlador.

Y allí, el puerto serie, por ejemplo, se define como una estructura de datos, y una instancia de esta estructura se encuentra en el área de direcciones reservada para registros y tenemos acceso a esta área a través de un puntero a una dirección específica.

Para aquellos que no se han encontrado con esto antes, describiré un poco cómo se define, los mismos lectores que están familiarizados con esto pueden omitir esta descripción.

Esta estructura y su instancia se describen a continuación:

/* =========================================================================*/ typedef struct { __IO uint32_t CR1; /*!< USART Control register 1, Address offset: 0x00 */ . . . __IO uint32_t ISR; /*!< USART Interrupt and status register, ... */ } USART_TypeDef; // USART_Type   . /* =========================================================================*/ #define USART1_BASE (APBPERIPH_BASE + 0x00013800) . . . #define USART1 ((USART_TypeDef *) USART1_BASE) #define USART1_BASE 0x400xx000U 

Puede ver más detalles aquí stm32f103xb.h ≈ 800 kB

Y si usa solo las definiciones en este archivo, tiene que escribir así (ejemplo de uso del registro de estado del puerto serie):

 // ---------------------------------------------------------------------------- if (USART1->ISR & (ONE_ISR_FLAG & OTHER_ISR_FLAG)) { } 

Y tiene que usarlo porque las soluciones propietarias existentes conocidas como CMSIS y HAL son demasiado complejas para usar en proyectos de aficionados.

Pero si escribe en C ++, puede escribir así:

 // ---------------------------------------------------------------------------- USART_TypeDef & Usart1 = *USART1; // ---------------------------------------------------------------------------- if (Usart1.ISR & (ONE_ISR_FLAG & OTHER_ISR_FLAG)) { } 

Una referencia mutable se inicializa con un puntero. Esto es un pequeño alivio, pero agradable. Mejor aún, por supuesto, escribir una clase de envoltura pequeña sobre esto, mientras que esta técnica sigue siendo útil.

Por supuesto, me gustaría escribir de inmediato esta clase de envoltura sobre el puerto serie (EUSART - transmisor-receptor universal asincrónico de serie universal extendido), tan atractivo, con características avanzadas, un transceptor asíncrono en serie y poder conectar nuestro pequeño microcontrolador a un sistema de escritorio o portátil, pero microcontroladores Cortex se distingue por un sistema de reloj desarrollado y tendrá que comenzar desde él, y luego configurar los pines de E / S correspondientes para trabajar con periféricos, porque en la serie STM32F1xx, como en patas otros microcontroladores ARM Cortex no sólo se puede configurar los pines del puerto de entrada o salida y trabajar al mismo tiempo con la periferia.

Bueno, comencemos activando el tiempo. El sistema de reloj se llama registros RCC para el control del reloj y también representa una estructura de datos, al puntero declarado al que se le asigna un valor de dirección específico.

 /* =========================================================================*/ typedef struct { . . . } RCC_TypeDef; 

Los campos de esta estructura se declaran así, donde __IO define volátil:

 /* =========================================================================*/ __IO uint32_t CR; 

corresponden a los registros de RCC, y los bits individuales de estos registros están activados o las funciones de reloj de la periferia del microcontrolador. Todo esto está bien descrito en la documentación (pdf) .

Un puntero a una estructura se define como

 /* =========================================================================*/ #define RCC ((RCC_TypeDef *)RCC_BASE) 

Trabajar con bits de registro sin usar el SDK generalmente se ve así:

Aquí está la inclusión del puerto A.

 // ---------------------------------------------------------------------------- RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; 

Puede habilitar dos o más bits a la vez

 // ---------------------------------------------------------------------------- RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN; 

Parece un poco inusual para C ++, o algo inusual. Sería mejor escribir de manera diferente, por ejemplo, usando OOP.

 // ---------------------------------------------------------------------------- Rcc.PortOn(Port::A); 

Se ve mejor, pero en el siglo XXI iremos un poco más lejos, usaremos C ++ 17 y escribiremos usando plantillas con un número variable de parámetros aún más hermosos:

 // ---------------------------------------------------------------------------- Rcc.PortOn<Port::A, Port::B>(); 

Donde Rcc se define así:

 // ---------------------------------------------------------------------------- TRcc & Rcc = *static_cast<TRcc *>RCC; 

A partir de esto, comenzaremos a construir un contenedor sobre los registros del reloj. Primero, definimos una clase y un puntero (enlace) a ella.

Al principio, quería escribir en el estándar C ++ 11/14 usando desempaquetar recursivamente los parámetros de una plantilla de función. Se proporciona un buen artículo sobre esto al final del artículo, en la sección de enlaces.

 // ============================================================================ enum class GPort : uint32_t { A = RCC_APB2ENR_IOPAEN, B = RCC_APB2ENR_IOPBEN, C = RCC_APB2ENR_IOPCEN, }; // ---------------------------------------------------------------------------- class TRcc: public ::RCC_TypeDef { private: TRcc() = delete; ~TRcc() = delete; // ======================================================================== public: template<GPort... port> inline void PortOn(void) //    (inline) { //    -Og  -O0 APB2ENR |= SetBits<(uint32_t)port...>(); } // ------------------------------------------------------------------------ #define BITMASK 0x01 //    ,   #define MASKWIDTH 1 //      .   //          #undef. private: //   (fold)   . template<uint8_t bitmask> inline constexpr uint32_t SetBits(void) { //   ,  GPort  enum // (, , bitmask    ). // static_assert(bitmask < 16, " ."); return bitmask; } template<uint8_t bit1, uint8_t bit2, uint8_t... bit> inline constexpr uint32_t SetBits(void) { return SetBits<bit1>() | SetBits<bit2, bit...>(); } }; #undef BITMASK #undef MASKWIDTH // ------------------------------------------------------------------------ TRcc & Rcc = *static_cast<TRcc *>RCC; 

Considere la llamada a la función de activación del reloj del puerto:

  Rcc.PortOn<GPort::A>(); 

GCC lo implementará en dicho conjunto de comandos:

  ldr r3, [pc, #376] ; (0x8000608 <main()+392>) ldr r0, [r3, #24] orr.w r0, r0, #4 str r0, [r3, #24] 

¿Funcionó? Verifica a continuación

  Rcc.PortOn<GPort::A, GPort::B, GPort::C>(); 

Por desgracia, el no tan ingenuo GCC desplegó la llamada de recursión final por separado:

  ldr r3, [pc, #380] ; (0x8000614 <main()+404>) ldr r0, [r3, #24] orr.w r0, r0, #4 ; APB2ENR |= GPort::A str r0, [r3, #24] ldr r0, [r3, #24] orr.w r0, r0, #28 ; APB2ENR |= Gport::B | GPort::C str r0, [r3, #24] #24] 

En defensa de GCC, debo decir que este no es siempre el caso, sino solo en casos más complejos, que se verán al implementar la clase de puerto de E / S. Bueno, C ++ 17 tiene prisa por ayudar. Reescriba la clase TRCC usando las capacidades de desplazamiento incorporadas.

 // ---------------------------------------------------------------------------- class TRcc: public ::RCC_TypeDef { private: TRcc() = delete; //     ,  ~TRcc() = delete; //    . // ======================================================================== public: template<GPort... port> inline void PortOn(void) //    (inline) { //    -Og  -O0 APB2ENR |= SetBits17<(uint32_t)port...>(); } // ------------------------------------------------------------------------ #define BITMASK 0x01 //    ,   #define MASKWIDTH 1 //      .   //          #undef. private: //   (fold)   . ++ 17. template<uint8_t... bitmask> inline constexpr uint32_t SetBits17(void) { return (bitmask | ...); //     ... | bit } }; #undef BITMASK #undef MASKWIDTH 

Ahora resultó:

 ldr r2, [pc, #372] ; (0x800060c <main()+396>) ldr r0, [r2, #24] orr.w r0, r0, #28 ; APB2ENR |= Gport::A | Gport::B | GPort::C str r0, [r3, #24] 

Y el código de clase se ha vuelto más simple.

Conclusión: C ++ 17 nos permite usar las plantillas con un número variable de parámetros para obtener el mismo conjunto mínimo de instrucciones (incluso cuando la optimización está desactivada) que se obtiene cuando se utiliza el trabajo clásico con el microcontrolador a través de definiciones de registro, pero al mismo tiempo obtenemos todas las ventajas de una fuerte escritura de C ++, verificaciones durante la compilación, reutilizado a través de la estructura de las clases base del código, etc.

Aquí hay algo como esto escrito en C ++

 Rcc.PortOn<Port::A, Port::B, Port::C>(); 

Y el texto clásico sobre registros:

 RCC->APB2 |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN; 

despliegue en un conjunto óptimo de instrucciones. Aquí está el código generado por GCC (optimización desactivada -Og):

  ldr r2, [pc, #372] ; (0x800060c <main()+396>) [  RCC] ldr r0, [r2, #0] ; r0 = RCC->APB2 // [  APB2] orr.w r0, r0, #160 ; r0 |= 0x10100000 str r0, [r2, #0] ; RCC->APB2 = r0 

Ahora debe continuar trabajando y escribir la clase del puerto de entrada-salida. Trabajar con bits de puerto de E / S es complicado por el hecho de que se asignan cuatro bits para la configuración de un tramo de puerto y, por lo tanto, se requieren 64 bits de configuración en un puerto de 16 bits, que se dividen en dos registros CRL y CRH de 32 bits. Además, el ancho de la máscara de bits se convierte en más de 1. Pero aquí, desplazarse por C ++ 17 muestra sus capacidades.

imagen

A continuación, se escribirá la clase TGPIO, así como las clases para trabajar con otros periféricos, un puerto serie, I2C, SPI, DAP, temporizadores y mucho más, que generalmente está presente en los microcontroladores ARM Cortex y luego será posible parpadear con dichos LED.

Pero más sobre eso en la siguiente nota. Fuentes del proyecto en github .

Artículos de Internet utilizados para escribir notas.


Plantillas con un número variable de argumentos en C ++ 11 .
Innovaciones en las plantillas .
Innovación del lenguaje C ++ 17. Parte 1. Convolución y derivación .
Lista de enlaces a la documentación para microcontroladores STM .
Macros de Parámetros Variables

Artículos sobre Khabr que me llevaron a escribir esta nota


Semáforo en Attiny13 .

Julian Assange arrestado por la policía del Reino Unido
El espacio como un recuerdo vago

Escrito 12/04/2019 - ¡Feliz Día de la Cosmonáutica!

PS
STM32F103c8t6 en Stm CubeMx Imagen STM32F103c8t6 de CubeMX.

Como punto de partida, se utiliza el texto creado por la extensión Eclips para trabajar con los microcontroladores GNU MCU Eclipse ARM Embedded y STM CubeMX , es decir, hay archivos de funciones estándar C ++, _start () y _init (), las definiciones de vectores de interrupción se toman de Eclipse MCU ARM Embedded, y el registro central de Cortex M3 y los archivos de trabajo son de un proyecto realizado por CubeMX.


PPS
En KDPV se representa la depuración con el controlador STM32F103c8t6. No todos tienen un tablero de este tipo, pero no es difícil comprarlo, sin embargo, esto está más allá del alcance de este artículo.

Source: https://habr.com/ru/post/448288/


All Articles