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.?
fuente
Respuestas:
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.
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.
Bueno, hay libros sobre el tema:
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.
fuente
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í:
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.
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
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".
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".
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.
fuente
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.
fuente
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.
fuente
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.
fuente
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.
fuente
¿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.
fuente
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.
fuente