programación - programacion embebida en c pdf
Patrones de acoplamiento suelto para la programación de sistemas embebidos (6)
¡Será mejor que estés muy seguro de que lo que quieres es un diseño fijo! ¡Derribarlo y ponerlo en uno dinámico puede ser muy complicado!
Sugiero que los problemas que cualquier marco incrustado está tratando de administrar son:
Cálculo de las compensaciones a los datos
Debería ser posible crear una estructura única para toda la memoria, pero esto * así * no se siente como la forma correcta de hacerlo. Normalmente no se pide a los compiladores de C que trabajen con estructuras de varios megabytes y tengo la sensación de que esto no es muy portátil entre compiladores.
Si no se usan estructuras, entonces se necesitan cinco conjuntos de definiciones, basadas en lo que esencialmente es un esquema de datos:
- tamaños de tipos simples
- Compensaciones de campos dentro de los tipos de grupo.
- tamaños de tipos de grupo
- Compensaciones de grupos en carreras de grupos.
- tamaño de las carreras de grupos
- (posiblemente también direcciones absolutas de ejecuciones de grupos si el rendimiento lo exige)
Estas definiciones tienen un árbol de dependencia similar a un árbol, que se complica rápidamente en C sin formato porque los tipos generalmente tienen que ser empaquetados / alineados, por ejemplo, en grupos de 4 bytes para un rendimiento óptimo. Las definiciones completamente expandidas pueden terminar rápidamente más complejas de lo que algunos compiladores están felices de procesar.
La forma más fácil de administrar estos problemas en proyectos de C sin procesar es calcular las compensaciones con un programa en C que sea una herramienta de "compilación" ejecutable del proyecto e importarlas en el proyecto como un archivo .h que contenga números explícitos de opffset. Con este enfoque, una sola macro que tome una dirección base y los índices relevantes debería estar disponible durante la compilación principal para acceder a cada hoja de la estructura de datos.
Evitar la corrupción de punteros de función y maximizar la productividad de depuración
Si los punteros de función se almacenan en el objeto, son más vulnerables a la corrupción de datos que conduce a errores misteriosos. La mejor manera (para esos rangos de memoria que contienen diferentes tipos de objetos de vez en cuando) es almacenar un código vtable en el objeto que es un índice de búsqueda en un conjunto de conjuntos de punteros de función.
Los vtables se pueden calcular y generar de nuevo como un archivo .h con # define por un programa generador C que es una "herramienta de compilación" ejecutable.
Se requiere una macro de constructor especial para escribir en el id vtable apropiado para inicializar el uso de un objeto.
Ambos problemas ya se han resuelto de manera efectiva, por ejemplo, mediante el preprocesador objetivo-C (que generará C), pero puede hacerlo desde cero si desea quedarse con un conjunto muy pequeño de herramientas.
Asignación de bloques de memoria a recursos / tareas en la estructura de memoria estática
Si necesita soportar subprocesos múltiples, asociar tareas dinámicas de corta duración con índices particulares en la estructura de árbol (lo más cercano a asignar un objeto en el programa de procedimiento / OO equivalente) quizás se realice mejor mediante el bloqueo de prueba de un índice arbitrario, usando por ejemplo, un incremento atómico (desde cero con == 1 cheque) o exclusión mutua, luego verificando si el bloque está disponible, y si es así, marcándolo como usado, luego desbloqueando el bloque.
Si no se requiere soporte de subprocesos múltiples, esto no es necesario; Sugiero escribir un marco personalizado para administrar dichos procesos de asignación de recursos que se pueden ejecutar en modo de subproceso múltiple o de un solo subproceso, para permitir que el resto de la base de código no se preocupe por este tema y para permitir un rendimiento más rápido en un solo Sistemas de rosca.
¿Dónde puedo encontrar algunas pautas o ejemplos buenos y probados sobre cómo escribir código extensible, modular y mal acoplado en C (si es posible)?
El fondo de nuestro problema es que estamos manteniendo un gran proyecto de código heredado en C simple para un microcontrolador de bajo costo con recursos informáticos y de memoria limitados. Debido al hecho de que el sistema debe ser extremadamente confiable y la memoria es bastante limitada, una de las primeras restricciones es no usar la asignación dinámica de memoria . Todas las estructuras están mapeadas estáticamente.
Así que estamos buscando formas de hacer que este código sea más fácil de mantener y más modular. No estamos interesados en los estándares de codificación, sino en sugerencias de diseño. Tenemos buenas convenciones de codificación (nomenclatura, código de organización, SVN), por lo que no es un problema.
Por lo que he visto en la web (puedo estar equivocado), parece que la mayoría de los programadores que programan exclusivamente en C simple o en ensamblador, al menos en la comunidad de UC / Embedded, se abstienen de usar algo más que la simple programación de procedimientos.
Por ejemplo, podríamos obtener la mayoría de los beneficios de la POO y el desacoplamiento en C simple usando las funciones de devolución de llamada, las estructuras que contienen punteros de función y cosas similares (no requeriría una asignación dinámica, solo pasar los punteros a las estructuras), pero nos gustaría ver Si hay algunos métodos probados ya alrededor.
¿Conoce estos recursos o tiene sugerencias similares además de "por qué no cambia a C ++ u otro lenguaje de programación"?
[Editar]
Muchas gracias por todas las respuestas, todavía no he tenido tiempo de examinarlas. La plataforma es de 16 bits (XC166 o similar) uC, hw desnudo (sin RTOS).
Es posible que desee echar un vistazo al estándar de algoritmo xDAIS. Fue diseñado para aplicaciones DSP, pero las ideas también pueden ajustarse a diseños integrados de bajos recursos.
http://en.wikipedia.org/wiki/XDAIS_algorithms
En pocas palabras: xDAIS es una convención de interfaz de estilo OOP no muy diferente de COM para el lenguaje C. Tiene un conjunto fijo de interfaces que un módulo puede implementar a través de una estructura de punteros de función.
Las interfaces están estrictamente definidas, por lo que es muy fácil intercambiar componentes, apilarlos juntos para crear una funcionalidad de mayor nivel y así sucesivamente. Las interfaces (y un verificador de códigos) también aseguran que todos los componentes permanezcan separados. Si se usa el verificador de códigos, es imposible escribir un componente que llame directamente a las funciones privadas de otros componentes.
La asignación de memoria generalmente se realiza en el momento de la inicialización y bajo el control del diseñador del sistema (es parte de la interfaz principal que todos los componentes deben implementar).
Las estrategias de asignación estática y dinámica son posibles. Incluso puede volverse dinámico sin el riesgo de fragmentación de la memoria porque todos los componentes deben poder reubicarse en diferentes direcciones de memoria.
El estándar xDAIS define un mecanismo de estilo OOP muy magro para la herencia. Esto es muy útil para fines de depuración y registro. ¿Tener un algoritmo que haga cosas graciosas? Solo agregue un envoltorio simple de un solo archivo alrededor de un algoritmo existente y registre todas las llamadas a un UART o algo así. Debido a la interfaz estrictamente definida, no hay conjeturas sobre cómo funciona un módulo y cómo se pasan los parámetros.
He usado xDAIS en el pasado y funciona bien. Se tarda un poco en acostumbrarse, pero los beneficios de la arquitectura plug-and-play y la facilidad de depuración superan el esfuerzo inicial.
Intentaré comenzar una respuesta aquí. Si se me ocurre algo más, volveré aquí porque este problema es interesante. También supervisaré esta pregunta para otras respuestas.
Lógica separada y ejecución:
Los sistemas integrados pueden beneficiarse del mismo tipo de separación de lógica y E / S que las aplicaciones de grandes empresas.
Si, por ejemplo, está codificando para un dispositivo integrado que lee valores, los interpreta y modifica algo según estas lecturas, es posible que desee separar la parte "lógica" completamente de la parte en la que realmente se comunica con la red, el hardware, El usuario o cualquier entidad externa.
Cuando puede describir las "reglas" completamente en algún tipo de estructura de memoria o código C, sin vincular a nada que no sean las rutinas de paso de mensajes o similares, tiene lo que trato de describir. En resumen, reducir los efectos secundarios hace que el código sea más modular.
No sé si está utilizando subprocesos o no, pero de cualquier modo, proto threads ofrece una abstracción similar, menos potente que los subprocesos, pero también es mucho menos probable que confunda al programador.
Al crecer en Amiga, me cuesta mucho olvidarlo. Un sistema operativo capaz de ROM, pero fácilmente extendido en RAM por bibliotecas cargables. El uso intensivo de pases de puntero se realizó tanto para código estricto como para mensajes rápidos.
Al leer otra respuesta de Nils Pipenbrinck, su sugerencia de usar http://en.wikipedia.org/wiki/XDAIS_algorithms parece ser una buena (pero no solo la única) forma de implementar esto. Si la mayoría de su código está usando una convención de mensajes como esta, es probable que su código sea modular y mantenible.
También activaría el paso del preprocesador o precompilador que se ejecuta en el código antes de compilar para el objetivo, pero luego nos desplazamos a un área gris ... esto es casi como cambiar de idioma, y el requisito era C.
Lo que mencionas sobre la imitación de la POO es una buena práctica. Más que conocer un estándar, puedo darte una idea de cómo lo hago. En realidad lo estoy usando cuidando algunos detalles:
- Ocultando las estructuras internas del módulo.
- Exponer los tamaños de la estructura a la aplicación para permitir la asignación estática.
- Exponer una interfaz de devolución de llamada para pedir prestada la funcionalidad de otros módulos.
- Manteniendo cada módulo compilable por si mismo.
- Por último, pero no menos importante: mantenga el código fácil de leer, fácil de usar y fácil de modificar.
@ my_module.c
typedef struct _s_class
{
uint32_t an_attribute;
void (*required_behavior)(uint32_t);
} class_t;
void obj_init(void * obj, void(*req_beh_callback)(uint32_t))
{
((class_t*)obj)->an_attribute = 0;
((class_t*)obj)->required_behavior = req_beh_callback;
}
void obj_method1(void* obj)
{
((class_t*)obj)->an_attribute++;
required_behavior(((class_t*)obj)->an_attribute);
}
size_t get_object_size()
{
return sizeof(class_t);
}
@ my_module.h
void obj_init(void * obj, void(*req_beh_callback)(uint32_t));
void obj_method1(void* obj);
size_t get_object_size();
// run get_object_size() once to get the number
// that goes in this macro. may differ between CPU
// architectures.
#define OBJECT_SIZE 4
@ my_application.c
#include "my_module.h"
uint8_t my_object[OBJECT_SIZE]; // static allocation :)
void callback_for_obj(uint32_t i)
{
... do stuff ...
}
int main()
{
obj_init(my_object, callback_for_obj);
obj_method1(my_object);
return 0;
}
¡Avíseme si tiene alguna sugerencia o pregunta, ya que también me ayudan a aprender más!
No estamos utilizando muchos dispositivos pequeños, pero tenemos algunos con limitaciones de memoria. Estamos asignando buffers estáticos, pero encontramos que a veces la asignación de memoria dinámica en realidad ayuda a reducir el uso de la memoria. Controlamos estrictamente el tamaño del montón y la política de asignación y tenemos que verificar y manejar las condiciones de la memoria, no como errores sino como un funcionamiento normal. Por ejemplo, nos hemos quedado sin memoria, por lo que enviamos los datos que tenemos, borramos los búferes y reanudamos las operaciones donde lo dejamos.
¿Por qué no cambiamos a C ++? Me encantaría. No cambiamos principalmente por estas razones:
- Nuestros monos codificados no lo asimilarían, y son reacios a aprender.
- Las bibliotecas de C ++ son significativamente más grandes (aunque es posible que podamos solucionar este problema).
- Para aquellos dispositivos RTOS realmente pequeños no suele ser necesario. Para dispositivos más grandes, donde estamos ejecutando un Linux incorporado, sería bueno.
Estamos en una situación similar. Para abordar estas inquietudes, hemos implementado un sistema de compilación que admite múltiples implementaciones de interfaces deseadas (cuya implementación es una función del objetivo de compilación), y evitamos el uso de características de API que no están incluidas en las envolturas portátiles. La definición del contenedor reside en un archivo .h que #include
el archivo de encabezado específico de la implementación. La siguiente maqueta demuestra cómo podemos manejar una interfaz de semáforo:
#ifndef __SCHEDULER_H
#define __SCHEDULER_H
/*! /addtogroup semaphore Routines for working with semaphores.
* @{
*/
/* impl/impl_scheduler.h gets copied into place before any file using
* this interface gets compiled. */
#include "impl/impl_scheduler.h"
/* semaphore operation return values */
typedef enum _semaphoreErr_e
{
SEMAPHORE_OK = impl_SEMAPHORE_OK,
SEMAPHORE_TIMEOUT = impl_SEMAPHORE_TIMEOUT
} semaphoreErr_e;
/*! public data type - clients always use the semaphore_t type. */
typedef impl_semaphore_t semaphore_t;
/*! create a semaphore. */
inline semaphore_t *semaphoreCreate(int InitialValue) {
return impl_semaphoreCreate(InitialValue);
}
/*! block on a semaphore. */
inline semaphoreErr_e semaphorePend(semaphore_t *Sem, int Timeout) {
return impl_semaphorePend(Sem, Timeout);
}
/*! Allow another client to take a semaphore. */
inline void semaphorePost(semaphore_t *Sem) {
impl_semaphorePost(Sem);
}
/*! @} */
#endif
La API pública está documentada para su uso y la implementación está oculta hasta el momento de la compilación. El uso de estos envoltorios tampoco debe imponer ninguna sobrecarga (aunque podría depender de su compilador). Sin embargo, hay un montón de mecanografía puramente mecánica involucrada.