Cinco años de uso de C ++ para proyectos de microcontroladores en producción

En este artículo, le contaré cómo transferí las empresas en las que trabajé durante más de cinco años desde la gestión de proyectos para microcontroladores a C a C ++ y lo que surgió (spoiler: todo está mal).

Un poco sobre mi


Comencé a escribir con microcontroladores C, teniendo solo experiencia escolar con Pascal, luego estudié ensamblador y pasé unos 3 años estudiando varias arquitecturas de microcontroladores y sus periféricos. Luego estaba la experiencia del trabajo real en C # y C ++ con su estudio paralelo, que tomó varios años. Después de este período, nuevamente y durante mucho tiempo volví a programar microcontroladores, que ya tenían la base teórica necesaria para trabajar en proyectos reales.

Primer año


No tenía nada en contra del estilo de procedimiento de C, pero la empresa que comenzó mi práctica real en proyectos reales usó "programación en C en un estilo orientado a objetos". Se veía algo así.

typedef const struct _uart_init { USART_TypeDef *USARTx; uint32_t baudrate; ... } uart_cfg_t; int uart_init (uart_cfg_t *cfg); int uart_start_tx (int fd, void *d, uint16_t l); int uart_tx (int fd, void *d, uint16_t l, uint32_t timeout); 

Este enfoque tenía las siguientes ventajas:

  1. el código continuó siendo el código C. De esto se deducen las siguientes ventajas:
    • es más fácil controlar los "objetos", porque es fácil rastrear quién y dónde causa qué y en qué secuencia (con la excepción de interrupciones, pero no en este artículo);
    • para almacenar el "puntero al objeto" es suficiente recordar el fd devuelto;
    • si se eliminó el "objeto", cuando intente usarlo, recibirá un error correspondiente en el valor de retorno de la función;
  2. La abstracción de tales objetos sobre el HAL utilizado allí hizo posible escribir objetos personalizables para la tarea desde su propia estructura de inicialización (y en caso de falta de funcionalidad HAL, uno podría ocultar el acceso a los registros dentro de los "objetos").

Contras:

  1. si alguien eliminó el "objeto", y luego creó uno nuevo de un tipo diferente, puede suceder que el nuevo obtenga el fd del anterior y no se determinará el comportamiento adicional. Este comportamiento podría cambiarse fácilmente a costa de un pequeño consumo de memoria para una lista vinculada en lugar de usar una matriz con un valor clave (la matriz para cada índice fd almacenó un puntero a la estructura del objeto).
  2. era imposible marcar estáticamente la memoria bajo "objetos globales". Como en la mayoría de las aplicaciones los "objetos" se crearon una vez y no se eliminaron más, parecía una "muleta". Aquí, al crear un objeto, sería posible pasar un puntero a su estructura interna, que se asignó estáticamente durante el diseño, pero esto confundiría aún más el código de inicialización y rompería la encapsulación.

Cuando se les preguntó por qué no se seleccionó C ++ al construir toda la infraestructura, respondieron algo como lo siguiente: "Bueno, C ++ conlleva costos adicionales fuertes, costos de memoria no controlados, así como un archivo de firmware ejecutable voluminoso". Quizás tenían razón. De hecho, en el momento del comienzo del diseño, solo había GCC 3.0.5, que no brillaba con especial amabilidad con C ++ (todavía tenemos que trabajar con él para escribir programas bajo QNX6). No había constexpr y C ++ 11/14, lo que le permitía crear objetos globales, que en esencia eran datos en el área .data, calculados en la etapa de compilación y métodos para ellos.

A la pregunta, ¿por qué no escribir en los registros? Obtuve una respuesta clara de que el uso de "objetos" le permite configurar el mismo tipo de aplicación "en un día".

Al darme cuenta de todo esto y al darme cuenta de que ahora C ++ no es lo mismo que con GCC 3.0.5, comencé a reescribir la parte principal de la funcionalidad en C ++. Para comenzar, trabaje con los periféricos de hardware del microcontrolador y luego con los periféricos de los dispositivos externos. De hecho, esto era solo un shell más conveniente sobre lo que estaba disponible en ese momento.

Año dos y tres


Reescribí todo lo que necesitaba para mis proyectos en C ++ y continué escribiendo nuevos módulos de inmediato en C ++. Sin embargo, estos eran solo shells sobre C. Después de darme cuenta de que no estaba usando C ++ lo suficiente, comencé a usar sus fortalezas: plantillas, clases de solo encabezado, constexpr y más. Todo iba bien.

Cuarto y quinto año


  • todos los objetos son globales e incluyen enlaces entre sí en la etapa de compilación (de acuerdo con la arquitectura del proyecto);
  • todos los objetos tienen memoria asignada en la etapa de diseño;
  • por objeto de clase para cada pin;
  • un objeto que encapsula todos los pines para inicializarlos con un método;
  • un objeto de control RCC que encapsula todos los objetos que están en los buses de hardware;
  • El proyecto convertidor CAN <-> RS485 según el protocolo del cliente contiene 60 objetos;
  • en caso de que algo esté a ese nivel en el nivel HAL o de clase de algún objeto, no solo debe "solucionar el problema", sino también pensar cómo solucionarlo para que esta solución funcione en todas las configuraciones posibles de este módulo ;
  • las plantillas utilizadas y constexpr no se pueden calcular antes de ver el mapa, los archivos asm y bin del firmware final (o iniciar la depuración en el microcontrolador);
  • en caso de un error en la plantilla, se envía un mensaje con una longitud de un tercio de la configuración del proyecto desde GCC. Leer y comprender algo de esto es un logro aparte.

Resumen


Ahora entiendo lo siguiente:
  1. El uso de "constructores de módulos universales" solo complica innecesariamente el programa. Es mucho más fácil ajustar los registros de configuración para un nuevo proyecto que profundizar en las relaciones entre los objetos, y luego también en la biblioteca HAL;
  2. no tenga miedo de usar C ++ por miedo a que "engulle mucha memoria" o "esté menos optimizado que C". No lo es. Debe temer que el uso de objetos y muchas capas de abstracción haga que el código sea ilegible, y la depuración será una hazaña heroica;
  3. si no usa nada "complicado", como plantillas, herencia y otros atractivos encantos de C ++, ¿por qué usar C ++? ¿Solo por el bien de los objetos? ¿Vale la pena? ¿Y por el bien de los objetos globales estáticos sin usar nuevo / eliminar prohibido en algunos proyectos?

En resumen, podemos decir que la aparente simplicidad de usar C ++ resultó ser solo una excusa para aumentar repetidamente la complejidad del proyecto sin ninguna ganancia en velocidad o memoria.

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


All Articles