La complejidad de la programación integrada.
Es el primer paso para ver los problemas integrados de la programación de máquinas de PC; aprender a usar ideas de programación integradas, ese es el segundo paso; usar las ideas de PC e ideas integradas juntas para aplicar a proyectos reales, que Este es el tercer paso. Muchos amigos han pasado de la programación de PC a la programación integrada. En China, pocos amigos de la programación integrada se han graduado de las especialidades en informática de Zhenger Bajing, y todos se graduaron de las especialidades relacionadas con la automatización y la electrónica. Estos zapatos para niños tienen una rica experiencia práctica, pero carecen de conocimientos teóricos; una gran parte de los zapatos para niños graduados de la carrera de informática se utilizan para aplicaciones de nivel superior que son independientes del sistema operativo, como juegos en línea y páginas web. Además, no está muy dispuesto a participar en la industria integrada, después de todo, este camino no es fácil de seguir. Tienen un gran conocimiento teórico, pero carecen de conocimientos relacionados, como los circuitos. El aprendizaje en el incrustado necesita aprender un conocimiento específico, lo cual es más difícil de alcanzar.
Aunque no se han realizado encuestas de la industria, por lo que he visto y reclutado, los ingenieros que trabajan en la industria integrada carecen de conocimientos teóricos o experiencia práctica. Muy pocos son ambos. La razón sigue siendo el problema de la educación universitaria en China. Este tema no se discutirá aquí para evitar la guerra de palabras. Quiero enumerar algunos ejemplos en mi práctica. Despierta la atención de todos sobre algunos problemas al hacer proyectos integrados.
primera pregunta:
Los colegas desarrollaron un controlador de puerto serie bajo uC / OS-II. Tanto el controlador como la interfaz encontraron problemas durante la prueba. Se desarrolla un programa de comunicación en la aplicación. El controlador del puerto serie proporciona una función para consultar los caracteres del búfer del controlador: GetRxBuffCharNum (). La capa superior necesita aceptar un cierto número de caracteres para analizar el paquete. El código escrito por un colega se expresa en pseudocódigo de la siguiente manera:
bExit = FALSE;
hacer {
if (GetRxBuffCharNum ()> = 30)
bExit = ReadRxBuff (buff, GetRxBuffCharNum ());
} while (! bExit);
Este código juzga que hay más de 30 caracteres en el búfer actual y lee todos los caracteres en el búfer hasta que la lectura sea exitosa. La lógica es clara y el pensamiento es claro. Pero este código no funciona correctamente. Si está en una PC, no debe haber problemas y el trabajo es anormal. Pero es realmente desconocido en el incrustado. Los colegas están muy deprimidos, no sé por qué. Ven y pídeme que resuelva el problema. Cuando vi el código en ese momento, le pregunté, ¿cómo se implementa GetRxBuffCharNum ()? Abrelo:
sin firmar GetRxBuffCharNum (nulo)
{
cpu_register reg;
número sin signo;
reg = interrupt_disable ();
num = gRxBuffCharNum;
interrupt_enable (reg);
retorno (num);
}
Obviamente, porque en el bucle, hay una región crítica global entre interruput_disable () e interrupt_enable () para garantizar la integridad de gRxBufCharNum. Sin embargo, debido a que en el bucle externo do {} while (), la CPU frecuentemente cierra la interrupción y la abre, esta vez es muy corta. De hecho, la CPU puede no responder normalmente a las interrupciones UART. Por supuesto, esto está relacionado con la velocidad en baudios de uart, el tamaño del búfer de hardware y la velocidad de la CPU. La velocidad en baudios que usamos es muy alta, aproximadamente 3Mbps. La señal de inicio uart y la señal de parada ocupan un bit. Un byte requiere 10 ciclos. La velocidad de transmisión de 3Mbps requiere aproximadamente 3.3us para transferir un byte. ¿Cuántas instrucciones de CPU puede ejecutar 3.3us? Un ARM de 100MHz puede ejecutar alrededor de 150 instrucciones. Como resultado, ¿cuánto tiempo lleva apagarse? En general, ARM necesita más de 4 instrucciones para desactivar la interrupción, y hay más de 4 instrucciones para activar el ARM. El código que recibe la interrupción uart es en realidad más de 20 instrucciones. Por lo tanto, de esta manera, puede haber un error en la pérdida de datos de comunicación, que se refleja en el nivel del sistema, es decir, la comunicación es inestable.
Modificar este código es realmente muy simple, la forma más fácil es modificarlo desde arriba. cual es:
bExit = FALSE;
hacer {
DelayUs (20); // Delay 20us, generalmente usa instrucción de bucle vacío
num = GetRxBuffCharNum ();
si (num> = 30)
bExit = ReadRxBuff (buff, num);
} while (! bExit);
De esta manera, la CPU tiene tiempo para ejecutar el código interrumpido, evitando así la interrupción de la ejecución del código de interrupción causada por paradas frecuentes y la pérdida de información generada. En sistemas embebidos, la mayoría de las aplicaciones RTOS son sin controladores seriales. Al diseñar el código usted mismo, la combinación del código y el núcleo no se considera completamente. Causar problemas profundos en el código. La razón por la que RTOS se llama RTOS se debe a la respuesta rápida a los eventos; la respuesta rápida de los eventos depende de la velocidad de respuesta de la CPU a las interrupciones. El controlador está altamente integrado con el kernel en un sistema como Linux, y se ejecuta en modo kernel juntos. Aunque RTOS no puede copiar la estructura de Linux, tiene cierta importancia de referencia.
Como puede ver en el ejemplo anterior, Embedded requiere que los desarrolladores comprendan todos los aspectos del código.
El segundo ejemplo:
Un colega maneja un chip 14094 serie-paralelo. La señal en serie se simula usando IO porque no hay hardware dedicado. Los colegas escribieron un controlador a mano, y resultó que todavía era un problema después de 3 o 4 días de depuración. Ya no podía verlo, así que fui y lo miré. Las señales paralelas que se controlan a veces son normales y otras anormales. Miré el código, el uso de pseudocódigo es probablemente:
para (i = 0; i <8; i ++)
{
SetData ((datos >> i) & 0x1);
SetClockHigh ();
para (j = 0; j <5; j ++);
SetClockLow ();
}
Envíe 8 bits de datos de bit0 a bit7 en cada nivel alto a su vez. Debería ser normal. ¿No puede ver dónde está el problema? Lo pensé detenidamente, leí la hoja de datos 14094 y lo entendí. Resulta que 14094 requiere que el nivel alto del reloj dure 10 ns y el nivel bajo dure 10 ns. Este código genera un retraso de alto nivel, pero no genera un retraso de bajo nivel. Si la interrupción se inserta entre niveles bajos, entonces este código está bien. Sin embargo, si la CPU se ejecuta sin interrupción a un nivel bajo, no funcionará correctamente. Entonces son buenos y malos momentos.
La modificación también es relativamente simple:
para (i = 0; i <8; i ++)
{
SetData ((datos >> i) & 0x1);
SetClockHigh ();
para (j = 0; j <5; j ++);
SetClockLow ();
para (j = 0; j <5; j ++);
}
Esto es completamente normal. Pero este sigue siendo un código que no se puede portar muy bien, porque la optimización del compilador puede causar la pérdida de estos dos bucles de retraso. Si se pierde, no se puede garantizar el requisito de que el nivel alto y el nivel bajo duren 10 ns, y no funcionará correctamente. Por lo tanto, para un código verdaderamente portátil, este bucle debe convertirse en un DelayNs de nanosegundos (10);
Al igual que Linux, al encender, primero mida cuánto tiempo lleva ejecutar la instrucción nop y cuántas instrucciones nop se ejecutan durante 10ns. Simplemente ejecute un cierto comando nop. Use el compilador para evitar instrucciones de compilación optimizadas o palabras clave especiales para evitar que el compilador optimice el bucle de retraso. Como en el CCG
__volatile__ __asm __ ("nop;");
A partir de este ejemplo, está claro que escribir un buen código requiere mucho conocimiento para respaldarlo. Qué dices
Los sistemas integrados a menudo no tienen soporte para el sistema operativo, o debido al soporte del sistema operativo, pero debido a varias limitaciones, la funcionalidad proporcionada por el sistema operativo es lamentable. Por lo tanto, una gran cantidad de código no puede ejecutarse de manera salvaje como la programación de PC. Hoy hablaré sobre el problema de la asignación de memoria. La fragmentación de la memoria puede ser familiar para todos. Sin embargo, en los sistemas embebidos, lo más temido es la fragmentación de la memoria, que también es la principal causa de muerte de la estabilidad del sistema. Una vez hice un proyecto, hay muchos malloc y gratuitos en el sistema, con diferentes tamaños, que van desde más de 60 bytes a 64 KB. Use un RTOS como soporte. En ese momento, tenía dos opciones, una era usar malloc y estar libre de la biblioteca del sistema C, y la otra era usar la asignación de memoria fija proporcionada por el sistema operativo. El diseño de nuestro sistema requiere un funcionamiento estable durante más de 3 meses. De hecho, se redujo durante unos 6 días consecutivos. Se han sospechado varios problemas. La decisión final es que la asignación de memoria es realmente mucho tiempo. Después de una gran cantidad de asignación de memoria, la memoria del sistema se fragmenta y no puede ser continua. Aunque hay un gran espacio, es imposible asignar espacio continuo. Cuando hay un gran espacio para aplicar, solo se puede hacer por el tiempo de inactividad. Para que el sistema cumpla con los requisitos de diseño originales, simulamos todo el hardware en la PC, ejecutamos el código incrustado en la PC, volvimos a cargar malloc y gratis, e hicimos un programa estadístico complejo. Comportamiento de la memoria estadística del sistema. Después de correr durante varios días, los datos se extrajeron y analizaron. Aunque la memoria aplicada es de 5 tipos de flores, todavía hay algunas reglas. Clasificamos las de menos de 100 bytes, 512B y 1KB. Clase, 2 KB se clasifica en una clase, 64 KB se clasifica en una clase. Cuente el número de cada categoría y agregue un margen del 30% sobre la base original. Hacer una aplicación de memoria fija aumenta en gran medida el tiempo para que el sistema funcione de manera estable y continua. Este es el caso de los métodos integrados, que no temen a los métodos primitivos, o temen al rendimiento insatisfactorio.
Problema de desbordamiento de memoria, problema de desbordamiento de memoria El sistema integrado es más terrible que el sistema de PC. A menudo se desborda sin previo aviso. Es difícil pensar, especialmente para los principiantes en C / C ++, que no están familiarizados con los punteros y no pueden verificarlos. Dado que el sistema de PC tiene una MMU, y la memoria cruza severamente el límite, está protegida por la MMU y no tendrá graves consecuencias de desastre. Los sistemas integrados a menudo no tienen MMU, que son muy diferentes. El código del sistema está roto y pueden ejecutarse. Solo Dios y esa CPU saben lo que es. Echemos un vistazo a este código:
char * strcpy (char * dest, const char * src)
{
afirmar (dest! = NULL && src! = NULL);
while (* src! = '�')
{
* dest ++ = * src ++;
}
* dest = '�';
retorno (dest);
}
Este código es un código para copiar una cadena de caracteres. Es básicamente suficiente para escribirlo en una PC. Pero el incrustado debe tener cuidado con una cosa, es decir, src realmente termina con ''. Si no fuera por las palabras, sería una tragedia. Cuándo terminará, jeje, solo el viejo hombre de Dios lo sabe. Si este código tiene la suerte de ejecutarse, se estima que el programa puede ejecutarse normalmente. Porque el área de memoria señalada por dest está casi destruida. Para ser compatible con las bibliotecas estándar de C / C ++, realmente no hay una buena manera, por lo que este problema solo puede dejarse al programador para que lo verifique.
idéntico,
memcpy (dest, src, n);
El mismo problema con la copia de memoria, tenga cuidado de pasar un valor negativo a n. Esta es la cantidad de bytes que se copian y los valores negativos se ven obligados a convertirse en positivos. Se convierte en un gran número positivo, causando que se destruya toda la memoria después de dest ...
El puntero de memoria en el incrustado debe verificarse estrictamente antes de que pueda usarse, y el tamaño de la memoria debe depurarse estrictamente. De lo contrario, la tragedia es difícil de evitar. Como un puntero de función, aunque se asigna un NULL, 0 en el incrustado. Si es ARM, ni siquiera hay un error anormal, y se reinicia directamente, porque incluso si se llama al puntero de función, el código comienza a ejecutarse desde 0. Y 0 es la posición del primer código que ARM ejecuta después del encendido. Esto es especialmente cierto en ARM7. Esta tragedia es mucho más trágica que la de la PC, y MMU debe dar un error de instrucción indefinido. Despertar la atención de los programadores. En embebido, todo queda para que los programadores lo encuentren.
El desbordamiento de memoria ocurre en cualquier momento accidental, ¿cuánto montón asigna a todo el sistema front-end (o sistema operativo)? ¿Qué tan grande es la pila? En circunstancias normales, ¿cuál es la profundidad de la llamada al sistema (máxima) y cuánta pila está ocupada? No basta con mirar la función del programa, y estos parámetros deben contarse. De lo contrario, siempre que haya un desbordamiento. Es fatal para el sistema. El sistema integrado requiere que el sistema funcione de forma continua durante mucho tiempo y tiene requisitos estrictos de estabilidad y confiabilidad. Lleva algún tiempo moler estos sistemas con cuidado.
La depuración de sistemas embebidos es a menudo muy complicada, los métodos disponibles no son tantos como la programación de PC y el costo de desarrollo es mucho mayor que el de los sistemas de PC. Los principales medios para depurar sistemas integrados son solo el seguimiento de un solo paso representado por JTAG, el método de eliminación de carpetas printf, etc.
No todos estos dos métodos de depuración pueden resolver el problema en el sistema integrado. Jtag requiere que un depurador tenga un dispositivo de depuración (que puede ser costoso), conectado al sistema de destino. Utilice software como GDB Client para iniciar sesión en el dispositivo de depuración y rastrear el programa en ejecución. Para ser honesto, este método es el último método de depuración para embebido, y también es un mejor método de depuración. Sin embargo, todavía hay algunas deficiencias. Cuando hay demasiados puntos de interrupción, se supera el límite de hardware. Algunas CPU de gama baja no admiten más puntos de interrupción. JTAG necesita usar simulación de software o trampas de software (interrupciones suaves o excepciones) Lograr un punto de ruptura. El mecanismo es más complicado: para decirlo simplemente: 1. No se puede depurar durante mucho tiempo, lo que no es estable, 2. Puede afectar el comportamiento del programa en el momento de la ejecución, que se ve afectado por el tiempo. Después de conectar el sistema JTAG, los puntos de interrupción implementados por el hardware no afectarán la velocidad del sistema, pero los puntos de interrupción implementados por el software deben sacrificar algo de rendimiento. La fiabilidad debe verse comprometida