¿Cómo hago TDD en dispositivos integrados?

17

No soy nuevo en programación e incluso he trabajado con algunos C y ASM de bajo nivel en AVR, pero realmente no puedo entender un proyecto C incrustado a mayor escala.

Siendo degenerado por la filosofía de Ruby de TDD / BDD, no puedo entender cómo la gente escribe y prueba códigos como este. No digo que sea un código incorrecto, simplemente no entiendo cómo puede funcionar.

Quería profundizar en la programación de bajo nivel, pero realmente no tengo idea de cómo abordar esto, ya que parece una mentalidad completamente diferente a la que estoy acostumbrado. No tengo problemas para comprender la aritmética del puntero, o cómo funciona la asignación de memoria, pero cuando veo cuán complejo se ve el código C / C ++ en comparación con Ruby, parece increíblemente difícil.

Como ya me ordené una placa Arduino, me encantaría profundizar más en un nivel C bajo y realmente entender cómo hacer las cosas correctamente, pero parece que ninguna de las reglas de los lenguajes de alto nivel se aplica.

¿Es posible hacer TDD en dispositivos integrados o al desarrollar controladores o cosas como el gestor de arranque personalizado, etc.?

Jakub Arnold
fuente
3
Hola Darth, realmente no podemos ayudarte a superar tu miedo a C, pero la pregunta sobre TDD en dispositivos integrados es sobre el tema aquí: he revisado tu pregunta para incluirla en su lugar.

Respuestas:

18

En primer lugar, debe saber que intentar comprender el código que no escribió es 5 veces más difícil que escribirlo usted mismo. Puede aprender C leyendo el código de producción, pero tomará mucho más tiempo que aprender haciendo.

Siendo degenerado por la filosofía de Ruby de TDD / BDD, no puedo entender cómo la gente escribe y prueba códigos como este. No digo que sea un código incorrecto, simplemente no entiendo cómo puede funcionar.

Es una habilidad; te vuelves mejor en eso. La mayoría de los programadores de C no entienden cómo las personas usan Ruby, pero eso no significa que no puedan.

¿Es posible hacer TDD en dispositivos integrados o al desarrollar controladores o cosas como el gestor de arranque personalizado, etc.?

Bueno, hay libros sobre el tema:

ingrese la descripción de la imagen aquí Si un abejorro puede hacerlo, ¡tú también puedes!

Tenga en cuenta que la aplicación de prácticas de otros idiomas generalmente no funciona. Sin embargo, TDD es bastante universal.

Pubby
fuente
2
Cada TDD que he visto para mis sistemas integrados solo encontró errores en los sistemas que tenían errores fáciles de resolver que habría encontrado fácilmente por mi cuenta. Nunca habrían encontrado con lo que necesito ayuda, las interacciones dependientes del tiempo con otros chips y las interacciones de interrupción.
Kortuk
3
Esto depende del tipo de sistema en el que esté trabajando. Descubrí que usar TDD para probar el software, junto con una buena abstracción de hardware, en realidad me permite simular esas interacciones dependientes del tiempo con mucha más facilidad. El otro beneficio que la gente suele mirar es que las pruebas, que se automatizan, pueden ejecutarse en cualquier momento y no requieren que alguien se siente en el dispositivo con un analizador lógico para asegurarse de que el software funciona. TDD me ha ahorrado semanas de depuración solo en mi proyecto actual. A menudo, son los errores que creemos que son fáciles de detectar que causan errores que no esperaríamos.
Nick Pascucci
Además, permite el desarrollo y las pruebas fuera del objetivo.
cp.engr
¿Puedo seguir este libro para comprender TDD para C no incorporado? Para cualquier espacio de usuario de programación C?
sobreexchange
15

Una gran variedad de respuestas aquí ... principalmente abordando el problema de varias maneras.

He estado escribiendo software y firmware integrados de bajo nivel durante más de 25 años en una variedad de lenguajes, principalmente C (pero con desviaciones en Ada, Occam2, PL / M y una variedad de ensambladores en el camino).

Después de un largo período de reflexión y prueba y error, me he decidido por un método que obtiene resultados con bastante rapidez y es bastante fácil de crear envolturas y arneses de prueba (¡donde AGREGAN VALOR!)

El método es más o menos así:

  1. Escriba un controlador o una unidad de código de abstracción de hardware para cada periférico principal que desee usar. También escriba uno para inicializar el procesador y configurar todo (esto hace que el entorno sea amigable). Por lo general, en pequeños procesadores integrados, su AVR es un ejemplo, puede haber entre 10 y 20 unidades, todas pequeñas. Estas pueden ser unidades para inicialización, conversión A / D a memorias intermedias de memoria sin escala, salida bit a bit, entrada de botón pulsador (sin rebote solo muestreado), controladores de modulación de ancho de pulso, UART / controladores seriales simples que interrumpen el uso y pequeñas memorias intermedias de E / S. Puede haber algunos más, por ejemplo, controladores I2C o SPI para EEPROM, EPROM u otros dispositivos I2C / SPI.

  2. Para cada una de las unidades de abstracción de hardware (HAL) / controlador, entonces escribo un programa de prueba. Esto se basa en un puerto serie (UART) y un procesador init, por lo que el primer programa de prueba usa esas 2 unidades solamente y solo realiza algunas entradas y salidas básicas. Esto me permite probar que puedo iniciar el procesador y que tengo soporte básico de depuración de E / S en serie funcionando. Una vez que eso funciona (y solo entonces), desarrollo los otros programas de prueba HAL, construyéndolos sobre las unidades UART e INIT buenas conocidas. Por lo tanto, podría tener programas de prueba para leer las entradas bit a bit y mostrarlas en una forma agradable (hexadecimal, decimal, lo que sea) en mi terminal de depuración en serie. Luego puedo pasar a cosas más grandes y complejas, como los programas de prueba EEPROM o EPROM: hago que la mayoría de estos menús funcionen para poder seleccionar una prueba para ejecutar, ejecutarla y ver el resultado. No puedo ESCRIBIRLO, pero generalmente no lo hago

  3. Una vez que tengo todo mi HAL funcionando, entonces encuentro la manera de obtener un tic del temporizador regular. Esto suele ser a una velocidad entre 4 y 20 ms. Esto debe ser regular, generado en una interrupción. El rollover / overflow de los contadores suele ser cómo se puede hacer esto. El manejador de interrupciones INCREMENTA un "semáforo" de tamaño de byte. En este punto, también puede jugar con la administración de energía si es necesario. La idea del semáforo es que si su valor es> 0, debe ejecutar el "bucle principal".

  4. El EJECUTIVO ejecuta el bucle principal. Prácticamente solo espera que ese semáforo se convierta en no 0 (abstraigo este detalle). En este punto, puede jugar con contadores para contar estos ticks (porque conoce la tasa de ticks) y así puede establecer indicadores que muestren si el tick ejecutivo actual es para un intervalo de 1 segundo, 1 minuto y otros intervalos comunes que podría querer usar. Una vez que el ejecutivo sabe que el semáforo es> 0, ejecuta una sola pasada a través de cada función de "actualización" de los procesos de "aplicación".

  5. Los procesos de la aplicación se sientan uno al lado del otro y se ejecutan regularmente mediante una marca de "actualización". Esta es solo una función convocada por el ejecutivo. Esta es efectivamente la multitarea de los hombres pobres con un RTOS local muy simple que se basa en todas las aplicaciones que ingresan, realizan un pequeño trabajo y salen. Las aplicaciones necesitan mantener sus propias variables de estado y no pueden hacer cálculos de ejecución prolongada porque no hay un sistema operativo preventivo para forzar la imparcialidad. OBVIAMENTE, el tiempo de ejecución de las aplicaciones (acumulativamente) debe ser menor que el período de tick principal.

El enfoque anterior se amplía fácilmente para que pueda agregar cosas como pilas de comunicación que se ejecutan de forma asincrónica y luego se pueden enviar mensajes de comunicación a las aplicaciones (agrega una nueva función a cada uno que es el "rx_message_handler" y escribe un despachador de mensajes que figura a qué aplicación enviar).

Este enfoque funciona para casi cualquier sistema de comunicación que desee nombrar: puede (y lo ha hecho) funcionar para muchos sistemas patentados, sistemas de comunicaciones de estándares abiertos, incluso funciona para pilas TCP / IP.

También tiene la ventaja de estar construido en piezas modulares con interfaces bien definidas. Puede introducir y extraer piezas en cualquier momento, sustituir diferentes piezas. En cada punto del camino, puede agregar arneses de prueba o controladores que se basan en las partes conocidas de la capa inferior (las cosas a continuación). He descubierto que aproximadamente del 30% al 50% de un diseño puede beneficiarse al agregar pruebas unitarias especialmente escritas que generalmente se agregan con bastante facilidad.

Llevé todo esto un paso más allá (una idea que descubrí de otra persona que ha hecho esto) y reemplacé la capa HAL con un equivalente para PC. Entonces, por ejemplo, puede usar C / C ++ y winforms o similar en una PC y al escribir el código CUIDADOSAMENTE puede emular cada interfaz (por ejemplo, EEPROM = un archivo de disco leído en la memoria de la PC) y luego ejecutar la aplicación integrada completa en una PC. La capacidad de utilizar un entorno de depuración amigable puede ahorrar una gran cantidad de tiempo y esfuerzo. Solo los proyectos realmente grandes pueden justificar esta cantidad de esfuerzo.

La descripción anterior es algo que no es exclusivo de cómo hago las cosas en plataformas integradas: me he encontrado con numerosas organizaciones comerciales que hacen cosas similares. La forma en que se hace suele ser muy diferente en la implementación, pero los principios son con frecuencia muy similares.

Espero que lo anterior dé un poco de sabor ... este enfoque funciona para pequeños sistemas integrados que se ejecutan en unos pocos KB con gestión agresiva de la batería hasta monstruos de 100K o más líneas de origen que funcionan con alimentación permanente. Si ejecuta "incrustado" en un sistema operativo grande como Windows CE, etc., todo lo anterior es completamente irrelevante. Pero eso no es una programación REAL REAL, de todos modos.

rápidamente_ahora
fuente
2
La mayoría de los periféricos de hardware no se pueden probar a través de un UART, porque a menudo está interesado principalmente en las características de temporización. Si desea verificar una frecuencia de muestreo ADC, un ciclo de trabajo PWM, el comportamiento de algún otro periférico en serie (SPI, CAN, etc.), o simplemente el tiempo de ejecución de alguna parte de su programa, entonces no puede hacerlo a través de un UART Cualquier prueba seria de firmware integrado incluye un osciloscopio: no puede programar sistemas integrados sin uno.
1
Oh si, absolutamente. Solo olvidé mencionar eso. Pero una vez que tenga su UART en funcionamiento, es muy fácil configurar pruebas o casos de prueba (que es de lo que se trataba la pregunta), estimular las cosas, permitir la entrada del usuario, obtener resultados y mostrar de manera amigable. Esto + tu CRO hace la vida muy fácil.
rapid_now
2

El código que tiene una larga historia de desarrollo incremental y optimizaciones para múltiples plataformas, como los ejemplos que eligió, generalmente es más difícil de leer.

Lo que pasa con C es que en realidad es capaz de abarcar plataformas en una amplia gama de riqueza de API y rendimiento de hardware (y falta de ella). MacVim funcionó de manera receptiva en máquinas con más de 1000 veces menos memoria y rendimiento de procesador que un teléfono inteligente típico de hoy. ¿Puede su código Ruby? Esa es una de las razones por las que podría parecer más simple que los ejemplos de C maduros que elegiste.

hotpaw2
fuente
2

Estoy en la posición inversa de haber pasado la mayor parte de los últimos 9 años como programador en C, y recientemente trabajando en algunos front-end de Ruby on Rails.

En lo que trabajo en C es en su mayoría sistemas personalizados de tamaño mediano para controlar almacenes automatizados (costo típico de unos cientos de miles de libras, hasta un par de millones). La funcionalidad de ejemplo es una base de datos personalizada en memoria, que interactúa con la maquinaria con algunos requisitos de tiempo de respuesta cortos y una gestión de nivel superior del flujo de trabajo del almacén.

En primer lugar, puedo decir que no hacemos TDD. He intentado en varias ocasiones presentar pruebas unitarias, pero en C es más problemático de lo que vale, al menos cuando se desarrolla software personalizado. Pero diría que TDD es mucho menos necesario en C que Ruby. Principalmente, eso es solo porque C se compila, y si se compila sin advertencias, ya ha realizado una cantidad de pruebas bastante similar a las pruebas de andamiaje autogeneradas rspec en Rails. Rubí sin pruebas unitarias no es factible.

Pero lo que diría es que C no tiene que ser tan difícil como algunas personas lo hacen. Gran parte de la biblioteca estándar de C es un desastre de nombres de funciones incomprensibles y muchos programas de C siguen esta convención. Me alegra decir que no, y de hecho tenemos muchos contenedores para la funcionalidad de la biblioteca estándar (ST_Copy en lugar de strncpy, ST_PatternMatch en lugar de regcomp / regexec, CHARSET_Convert en lugar de iconv_open / iconv / iconv_close, etc.). Nuestro código C interno me lee mejor que la mayoría de las otras cosas que he leído.

Pero cuando dice que las reglas de otros idiomas de nivel superior no parecen aplicarse, no estoy de acuerdo. Una gran cantidad de buen código C 'se siente' orientado a objetos. A menudo ve un patrón de inicialización de un identificador en un recurso, llamando a algunas funciones que pasan el identificador como argumento y, finalmente, liberando el recurso. De hecho, los principios de diseño de la programación orientada a objetos provienen en gran medida de las cosas buenas que la gente hacía en lenguajes de procedimiento.

Los momentos en que C se vuelve realmente complicado son a menudo cuando se hacen cosas como los controladores de dispositivos y los núcleos del sistema operativo que son básicamente de muy bajo nivel. Cuando escribe un sistema de nivel superior, también puede usar las funciones de nivel superior de C y evitar la complejidad de bajo nivel.

Una cosa muy interesante por la que querrás echar un vistazo es el código fuente C para Ruby. En los documentos de la API de Ruby (http://www.ruby-doc.org/core-1.9.3/) puede hacer clic y ver el código fuente de los distintos métodos. Lo interesante es que este código se ve bastante agradable y elegante, no se ve tan complejo como podría imaginarse.

asc99c
fuente
" ... también puedes usar las funciones de nivel superior de C ... ", ¿cómo las hay? ;-)
alk
¡Quiero decir un nivel más alto que la manipulación de bits y la magia de puntero a puntero que sueles ver en el código de tipo de controlador de dispositivo! Y si no le preocupa la sobrecarga de un par de llamadas a funciones, puede hacer que el código C tenga un nivel bastante alto.
asc99c
" ... puedes hacer un código C que realmente se vea razonablemente de alto nivel ", absolutamente, estoy totalmente de acuerdo con eso. Pero aunque " ... las características de nivel superior ... " no son de C, pero en tu cabeza, ¿no?
alk
2

Lo que he hecho es separar el código dependiente del dispositivo del código independiente del dispositivo, luego probar el código independiente del dispositivo. Con buena modularidad y disciplina, terminará con una base de código en su mayoría bien probada.

Paul Nathan
fuente
2

No hay razón por la que no puedas. El problema es que puede que no haya buenos marcos de prueba de unidades "estándar" como los que tiene en otros tipos de desarrollo. Está bien. Simplemente significa que tiene que adoptar un enfoque de "rodar su propio" para las pruebas.

Por ejemplo, es posible que deba programar la instrumentación para producir "entradas falsas" para sus convertidores A / D o tal vez deba generar una secuencia de "datos falsos" para que responda su dispositivo integrado.

Si encuentra resistencia al uso de la palabra "TDD", llámelo "DVT" (prueba de verificación de diseño) que hará que los EE se sientan más cómodos con la idea.

Angelo
fuente
0

¿Es posible hacer TDD en dispositivos integrados o al desarrollar controladores o cosas como el gestor de arranque personalizado, etc.?

Hace algún tiempo necesitaba escribir un gestor de arranque de primer nivel para una CPU ARM. En realidad, hay uno de los chicos que venden esta CPU. Y utilizamos un esquema donde su gestor de arranque arranca nuestro gestor de arranque. Pero esto fue lento, ya que necesitábamos flashear dos archivos en NOR flash en lugar de uno, necesitábamos construir el tamaño de nuestro gestor de arranque en el primer gestor de arranque, y reconstruirlo cada vez que cambiamos nuestro gestor de arranque y así sucesivamente.

Así que decidí integrar las funciones de su gestor de arranque en las nuestras. Como era un código comercial, tenía que asegurarme de que todo funcionara como se esperaba. Así que modifiqué QEMU para emular bloques de IP de esa CPU (no todos, solo aquellos que tocan el gestor de arranque), y agregué código a QEMU para "imprimir" todos los registros de lectura / escritura que controlan cosas como el controlador PLL, UART, SRAM y pronto. Luego actualicé nuestro gestor de arranque para admitir esta CPU, y después de eso comparé la salida que le da a nuestro gestor de arranque y su emulador, esto me ayuda a detectar varios errores. Fue escrito en parte en el ensamblador ARM, en parte en C. También después de que QEMU modificado me ayudó a detectar un error, que no pude detectar utilizando JTAG y una CPU ARM real.

Entonces, incluso con C y ensamblador puede usar pruebas.

Evgeniy
fuente
-2

Sí, es posible hacer TDD en software embebido. Las personas que dicen que no es posible, que no son relevantes o que no son aplicables no son correctas. Se puede obtener un gran valor de TDD en embebido como con cualquier software.

Sin embargo, la mejor manera de hacerlo es no ejecutar sus pruebas en el destino, sino abstraer sus dependencias de hardware y compilar y ejecutar en su PC host.

Cuando esté haciendo TDD, creará y ejecutará muchas pruebas. Necesita un software que lo ayude a hacer esto. Desea un marco de prueba que haga que sea rápido y fácil hacerlo, con descubrimiento automático de pruebas y generación de simulacros.

La mejor opción para C en este momento es Ceedling. Aquí hay una publicación sobre lo que escribí al respecto:

http://www.electronvector.com/blog/try-embedded-test-driven-development-right-now-with-ceedling

¡Y está construido en Ruby! Sin embargo , no necesitas saber ningún Ruby para usarlo.

Cherno
fuente
Se espera que las respuestas se mantengan solas. Obligar a los lectores a acceder a recursos externos para descubrir sustancias está mal visto en Stack Exchange ("lea el artículo o consulte Ceedling"). Considere la posibilidad de editar para que se ajuste a las normas de calidad del sitio
mosquito
¿Ceedling tiene algún mecanismo para soportar eventos asincrónicos? Uno de los aspectos más desafiantes de las aplicaciones integradas en tiempo real es que se ocupan de recibir entradas de sistemas muy complejos que son difíciles de modelar ...
Jay Elston
@ Jay No tiene nada específicamente para apoyar eso. Sin embargo, he tenido éxito probando ese tipo de cosas con burlas y configurando una arquitectura para soportarlo. Por ejemplo, recientemente trabajé en un proyecto donde los eventos controlados por interrupciones se pusieron en una cola y luego se consumieron en una máquina de estado "controlador de eventos". Esto era esencialmente una función que se llamaba cada vez que ocurría un evento. Al probar esa función, podría burlarme de la llamada a la función que sacó eventos de la cola, y así podría simular cualquier evento que ocurra en el sistema. La prueba de manejo también ayuda aquí.
cherno