Capítulo 1. Introducción Introducción

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Los sistemas embebidos son cosas distintas para personas distintas. Para alguien que ha trabajado en servidores, una aplicación desarrollada para un teléfono es un sistema embebido. A alguien que ha escrito código para diminutos microprocesadores de 8 bits, cualquier cosa con un sistema operativo no le parece muy embebido. Suelo decir a la gente no técnica que los sistemas integrados son cosas como microondas y automóviles que ejecutan software pero no son ordenadores. (La mayoría de la gente reconoce un ordenador como un dispositivo de propósito general.) Quizá una forma fácil de definir el término sin regatear en tecnología sea:

Un sistema embebido es un sistema informático construido específicamente para su aplicación.

Dado que su misión es más limitada que la de un ordenador de uso general, un sistema embebido tiene menos soporte para cosas que no están relacionadas con la realización del trabajo. El hardware suele tener limitaciones. Por ejemplo, considera una CPU que funcione más despacio para ahorrar batería, un sistema que utilice menos memoria para que pueda fabricarse más barato, y procesadores que sólo vienen en determinadas velocidades o admiten un subconjunto de periféricos.

El hardware no es la única parte del sistema con restricciones. En algunos sistemas, el software debe actuar de forma determinista (exactamente igual cada vez) o en tiempo real (reaccionando siempre a un evento con la suficiente rapidez). Algunos sistemas requieren que el software sea tolerante a fallos, con una degradación elegante ante los errores. Por ejemplo, considera un sistema en el que el mantenimiento de un software defectuoso o un hardware averiado puede ser inviable (como un satélite o una etiqueta de seguimiento en una ballena). Otros sistemas requieren que el software deje de funcionar a la primera señal de problemas, a menudo proporcionando mensajes de error claros (por ejemplo, un monitor cardíaco no debe fallar silenciosamente).

Este breve capítulo repasa la visión de alto nivel de los sistemas embebidos. Siendo realistas, podrías leer el artículo de Wikipedia, pero ésta es una forma de que nos conozcamos. Lamentablemente, este capítulo habla sobre todo de lo difícil que es desarrollar sistemas embebidos. Entre los diferentes compiladores, depuradores y limitaciones de recursos, la forma en que diseñamos e implementamos el código es diferente a la de otras variedades de software. Algunos podrían llamar a este campo un poco atrasado, pero eso no es cierto; en su mayor parte, nos centramos en resolver problemas diferentes. Y, sin embargo, hay algunas técnicas de ingeniería de software que son útiles pero que se pasan por alto (pero eso es para el resto del libro).

Una de las mejores cosas de los sistemas embebidos ha sido el movimiento maker. A todo el mundo le encantan las luces brillantes, así que la gente se interesa por hacer carrera en el software de bajo nivel. Si ése eres tú, bienvenido. Pero admito que espero gente que tenga experiencia con hardware o software y necesite saber cómo hacer bien y eficazmente la pieza entre ambos.

Al final de cada capítulo, tengo una pregunta de entrevista vagamente relacionada con el material. Una de las actividades para subir de nivel en mi carrera fue aprender a entrevistar a otras personas para puestos de mi equipo. Muy decepcionada porque no había un recurso sobre cómo hacerlo, he incluido mis preguntas favoritas para entrevistas y lo que busco como entrevistadora. Son un poco extrañas, pero espero que las disfrutes tanto como yo.

Admito que espero que disfrutes tanto como yo del desarrollo de sistemas embebidos. Hay retos, pero esa es la parte divertida.

Desarrollo de sistemas integrados

Los sistemas embebidos son especiales y ofrecen retos únicos a los desarrolladores. La mayoría de los ingenieros de software embebido desarrollan un conjunto de herramientas para hacer frente a las limitaciones. Antes de empezar a construir el tuyo, veamos las dificultades asociadas al desarrollo de un sistema embebido. Una vez que te hayas familiarizado con las posibles limitaciones de tu sistema embebido, partiremos de algunos principios que nos guiarán hacia mejores soluciones.

Compiladores y lenguajes

Los sistemas integrados utilizan compiladores cruzados. Aunque un compilador cruzado se ejecuta en tu ordenador de sobremesa o portátil, crea código que no lo hace. La imagen compilada cruzada se ejecuta en tu sistema embebido de destino. Dado que el código debe ejecutarse en tu procesador embebido, el proveedor del sistema de destino suele vender un compilador cruzado o proporciona una lista de compiladores cruzados disponibles entre los que elegir. Muchos procesadores grandes utilizan los compiladores cruzados de la familia de herramientas GNU, como GCC.

Los compiladores de software embebido a menudo sólo admiten C, o C y C++. Además, algunos compiladores de C++ embebido sólo implementan un subconjunto del lenguaje (suelen faltar la herencia múltiple, las excepciones y las plantillas). Hay una creciente popularidad de otros lenguajes, pero C y C++ siguen siendo los más extendidos.

Independientemente del lenguaje que necesites utilizar en tu software, puedes practicar el diseño orientado a objetos. Los principios de diseño de encapsulación, modularidad y abstracción de datos pueden aplicarse a cualquier aplicación en casi cualquier lenguaje. El objetivo es que el diseño sea robusto, mantenible y flexible. Deberíamos aprovechar toda la ayuda que podamos obtener del campo de la orientación a objetos.

En su conjunto, un sistema embebido puede considerarse equivalente a un objeto, sobre todo uno que funciona en un sistema mayor (como un mando a distancia que habla con un televisor inteligente, un sistema de control distribuido en una fábrica o un sensor de implementación del airbag en un coche). A un nivel superior, todo está inherentemente orientado a objetos, y es lógico extender esto al software embebido.

Por otra parte, no recomiendo una adhesión estricta a todos los principios de diseño orientado a objetos. Los sistemas embebidos se ven arrastrados en demasiadas direcciones como para poder establecer tal mandamiento. Una vez que reconozcas las compensaciones, podrás equilibrar los objetivos de diseño del software y los objetivos de diseño del sistema.

La mayoría de los ejemplos de este libro están en C o C++. Creo que el lenguaje es menos importante que los conceptos, así que aunque no estés familiarizado con la sintaxis, mira el código. Este libro no te enseñará ningún lenguaje de programación (excepto algo de lenguaje ensamblador), pero los buenos principios de diseño trascienden el lenguaje.

Depurando

Si tuvieras que depurar un programa que se ejecuta en un ordenador, podrías compilarlo y depurarlo en ese ordenador. El sistema tendría recursos suficientes para ejecutar el programa y soportar su depuración al mismo tiempo. De hecho, el hardware no sabría que estás depurando una aplicación, ya que todo se hace en software.

Los sistemas empotrados no son así. Además de un compilador cruzado, necesitarás un depurador cruzado. El depurador se instala en tu ordenador y se comunica con el procesador de destino a través de una interfaz de procesador especial (ver Figura 1-1). La interfaz está dedicada a permitir que otra persona espíe al procesador mientras funciona. Esta interfaz suele denominarse JTAG (pronunciado "jay-tag"), independientemente de si realmente implementa ese estándar tan extendido.

El procesador debe gastar parte de sus recursos para soportar la interfaz de depuración, permitiendo que el depurador lo detenga mientras se ejecuta y proporcionando el tipo normal de información de depuración. Soportar las operaciones de depuración añade costes al procesador. Para mantener los costes bajos, algunos procesadores admiten un subconjunto limitado de funciones. Por ejemplo, añadir un punto de interrupción hace que el procesador modifique el código cargado en memoria para decir "detente aquí". Sin embargo, si tu código se está ejecutando en la memoria flash (o en cualquier otro tipo de memoria de sólo lectura), en lugar de modificar el código, el procesador tiene que establecer un registro interno (punto de interrupción del hardware) y compararlo en cada ciclo de ejecución con la dirección del código que se está ejecutando, deteniéndose cuando coincidan. Esto puede cambiar la temporización del código, dando lugar a molestos fallos que sólo se producen cuando estás (o quizá no estés) depurando. Los registros internos también consumen recursos, por lo que a menudo sólo hay un número limitado de puntos de interrupción de hardware disponibles (con frecuencia sólo hay dos).

Figura 1-1. Ordenador y procesador de destino

En resumen, los procesadores admiten la depuración, pero no siempre tanta depuración como estás acostumbrado si vienes del mundo del software no embebido.

El dispositivo que se comunica entre tu PC y el procesador embebido suele denominarse depurador de hardware, programador, sonda de depuración, emulador en circuito (ICE) o adaptador JTAG. Pueden referirse (de forma algo incorrecta) a la misma cosa, o pueden ser varios dispositivos. El depurador es específico del procesador (o familia de procesadores), así que no puedes coger el depurador que conseguiste para un proyecto y asumir que funcionará en otro. Los costes del depurador se acumulan, sobre todo si reúnes suficientes o si tienes un gran equipo trabajando en tu sistema.

Para evitar comprar un depurador o lidiar con las limitaciones del procesador, muchos sistemas embebidos están diseñados para que su depuración se realice principalmente a través de printf, o algún tipo de registro más ligero, a un puerto de comunicación que de otro modo no se utilizaría. Aunque es increíblemente útil, esto también puede cambiar la sincronización del sistema, dejando posiblemente que algunos fallos se revelen sólo después de desactivar la salida de depuración.

Escribir software para un sistema embebido puede ser complicado, ya que tienes que equilibrar las necesidades del sistema y las limitaciones del hardware. Ahora tendrás que añadir otro elemento a tu lista de tareas: hacer que el software sea depurable en un entorno algo hostil, algo de lo que hablaremos más en el Capítulo 2.

Limitaciones de recursos

Un sistema embebido se diseña para realizar una tarea específica, eliminando los recursos que no necesita para cumplir su misión. Los recursos considerados son los siguientes

  • Memoria (RAM)

  • Espacio de código (ROM o flash)

  • Ciclos o velocidad del procesador

  • Consumo de energía (que se traduce en duración de la batería)

  • Periféricos del procesador

Hasta cierto punto, son intercambiables. Por ejemplo, puedes intercambiar espacio de código por ciclos de procesador, escribiendo partes de tu código para que ocupen más espacio pero se ejecuten más rápidamente. O puedes reducir la velocidad del procesador para disminuir el consumo de energía. Si no tienes una interfaz periférica concreta, podrías crearla en software con líneas de E/S y ciclos de procesador. Sin embargo, incluso compensando, sólo tienes un suministro limitado de cada recurso. El reto de la limitación de recursos es uno de los más acuciantes para los sistemas embebidos.

Otro conjunto de retos proviene del trabajo con el hardware. La carga añadida de la depuración cruzada puede ser frustrante. Durante la puesta a punto de la placa, la incertidumbre de si un fallo está en el hardware o en el software puede dificultar la resolución de los problemas. A diferencia de tu ordenador, el software que escribas puede ser capaz de causar daños reales al hardware. Sobre todo, tienes que conocer el hardware y lo que es capaz de hacer. Ese conocimiento podría no ser aplicable al siguiente sistema en el que trabajes. Tendrás que aprender rápidamente.

Una vez finalizados el desarrollo y las pruebas, se fabrica el sistema, que es algo que la mayoría de los ingenieros de software puro nunca tienen que considerar. Sin embargo, crear un sistema que pueda fabricarse por un coste razonable es un objetivo que tanto los ingenieros de software embebido como los ingenieros de hardware tienen que tener en cuenta. Apoyar la fabricación es una forma de asegurarte de que el sistema que has creado se reproduce con gran fidelidad.

Tras la fabricación, las unidades van al campo. Con los productos de consumo, eso significa que van a millones de hogares donde cualquier fallo que hayas creado lo disfrutan muchos. Con productos médicos, de aviación u otros productos críticos, tus fallos pueden ser catastróficos (por eso tienes que hacer tanto papeleo). Con equipos científicos o de monitoreo, el campo podría ser un lugar donde la unidad no pueda recuperarse nunca (o recuperarse sólo con grandes riesgos y gastos; considera los dispositivos en las calderas de los volcanes), así que más vale que funcione. La vida que va a llevar tu sistema después de abandonarte es algo que debes tener en cuenta al diseñar el software.

Después de que hayas averiguado todas estas cuestiones y determinado cómo abordarlas para tu sistema, aún queda el mayor reto, común a todas las ramas de la ingeniería: el cambio. No sólo cambian los objetivos del producto, sino también las necesidades del proyecto a lo largo de su vida. Al principio, tal vez quieras piratear algo sólo para probarlo. A medida que te tomas más en serio y comprendes mejor (y defines) los objetivos del producto y el hardware que utilizas, empiezas a construir más infraestructura para que el software sea depurable, robusto y flexible. En un entorno con recursos limitados, tendrás que determinar cuánta infraestructura puedes permitirte en términos de tiempo de desarrollo, RAM, espacio de código y ciclos de procesador. Lo que empezaste a construir inicialmente no es lo que tendrás al final del desarrollo. Y el desarrollo rara vez se completa.

Crear un sistema especialmente diseñado para una aplicación tiene un desafortunado efecto secundario: el sistema podría no soportar los cambios a medida que la aplicación evoluciona. La ingeniería de sistemas embebidos no sólo tiene que ver con las restricciones estrictas y la vida útil final del sistema. El objetivo es averiguar cuáles de esas limitaciones serán un problema más adelante en el desarrollo del producto. Tendrás que predecir el curso probable de los cambios e intentar diseñar un software lo bastante flexible para adaptarse a cualquier camino que tome la aplicación. Saca tu bola de cristal.

Principios para afrontar esos retos

Los sistemas integrados pueden parecer un rompecabezas, con piezas que se entrelazan (y que sólo encajan de una manera). A veces puedes forzar las piezas para que encajen, pero la imagen resultante puede no ser la que aparece en la caja. Sin embargo, deberíamos desechar la idea del resultado final como una única versión de código enviada al final del proyecto.

En lugar de eso, imagina que el puzzle tiene una dimensión temporal que varía a lo largo de toda su vida: concepción, creación de prototipos, puesta en placa, depuración, pruebas, lanzamiento, mantenimiento y repetición. La flexibilidad no se refiere sólo a lo que el código puede hacer en este momento, sino también a cómo puede afrontar su vida útil. Nuestro objetivo es ser lo suficientemente flexibles como para cumplir los objetivos del producto y, al mismo tiempo, hacer frente a las limitaciones de recursos y otros problemas inherentes a los sistemas embebidos.

Hay algunos principios excelentes que podemos tomar del diseño de software para hacer que el sistema sea más flexible. Con la modularidad, separamos la funcionalidad en subsistemas y ocultamos los datos que utiliza cada subsistema. Con la encapsulación, creamos interfaces entre los subsistemas para que no sepan mucho unos de otros. Una vez que tenemos subsistemas poco acoplados (u objetos, si lo prefieres), podemos cambiar un área del software con la seguridad de que no afectará a otra área. Esto nos permite desmontar nuestro sistema y volver a montarlo de forma diferente cuando lo necesitemos.

Reconocer dónde dividir un sistema en partes requiere práctica. Una buena regla general es considerar qué partes pueden cambiar independientemente. En los sistemas embebidos, esto se ve favorecido por la presencia de objetos físicos que puedes tener en cuenta. Si un sensor X habla a través de un canal de comunicación Y, son cosas separadas y buenas candidatas para ser subsistemas separados (y módulos de código).

Si dividimos las cosas en objetos, podemos hacer pruebas con ellos. He tenido la suerte de contar con excelentes equipos de control de calidad en algunos proyectos. En otros, no he tenido a nadie que se interpusiera entre mi código y las personas que iban a utilizar el sistema. He descubierto que los errores detectados antes de la publicación del software son como regalos. Cuanto antes se detecten los errores, menos costará solucionarlos y mejor para todos.

No tienes que esperar a que alguien te haga regalos. Las pruebas y la calidad van de la mano. Cuando pienses en cómo escribir un trozo de código, dedica algo de tiempo a considerar cómo lo probarás. Escribir código de prueba para tu sistema lo mejorará, proporcionará cierta documentación para tu código y hará que otras personas piensen que escribes un gran software.

Documentar tu código es otra forma de reducir los errores. Puede ser difícil saber el nivel de detalle al comentar tu código:

i++; // increment the index

No, así no. Las líneas como ésa rara vez necesitan comentarios. El objetivo es escribir el comentario para alguien como tú, que esté mirando el código un año después de cuando lo escribiste. Para entonces, tú-futuro probablemente estarás trabajando en algo diferente y habrás olvidado exactamente qué solución creativa se te ocurrió a ti-futuro. Futuro-tú probablemente ni siquiera recuerde haber escrito este código, así que ayúdate con un poco de orientación. En general, sin embargo, asume que el lector tendrá tu cerebro y tus conocimientos generales, así que documenta lo que hace el código, no cómo lo hace.

Por último, con los sistemas de recursos limitados, existe la tentación de optimizar tu código pronto y a menudo. Lucha contra esa tentación. Implementa las funciones, haz que funcionen, pruébalas y luego hazlas más pequeñas o más rápidas según sea necesario.

Sólo tienes una cantidad limitada de tiempo: céntrate en dónde puedes obtener mejores resultados buscando los mayores consumidores de recursos después de tener un subsistema que funcione. No te sirve de nada optimizar una función para que sea más rápida si se ejecuta raramente y queda empequeñecida por el tiempo empleado en otra función que se ejecuta con frecuencia. Sin duda, hacer frente a las limitaciones del sistema requerirá cierta optimización. Sólo tienes que asegurarte de que entiendes dónde se utilizan tus recursos antes de empezar a afinar.

Debemos olvidarnos de las pequeñas eficiencias, digamos un 97% de las veces: la optimización prematura es la raíz de todos los males.

Donald Knuth

Prototipos y Maker Boards

"Pero espera", dices, "ya tengo un sistema que funciona construido con un Arduino o una Raspberry Pi Pico. Sólo tengo que averiguar cómo enviarlo".

Lo entiendo. El sistema hace casi todo lo que quieres que haga. El proyecto parece casi hecho. Las placas de desarrollo estándar son increíbles, sobre todo las "maker-friendly". Facilitan la creación de prototipos. Sin embargo, el prototipo no es el producto.

Hay muchas tareas que a menudo se olvidan en esta fase. ¿Cómo se actualizará el firmware? ¿Necesita el sistema dormir para reducir el consumo de energía? ¿Necesitamos un perro guardián en caso de error catastrófico? ¿Cuánto espacio y cuántos ciclos de procesamiento necesitamos guardar para futuras correcciones de errores y mejoras? ¿Cómo fabricamos muchos dispositivos en lugar de una unidad hecha a mano? ¿Cómo comprobaremos la seguridad? ¿Comandos de usuario inexplicables? ¿Casos de esquina? ¿Hardware roto?

Dejando a un lado el software, cuando las placas existentes en el mercado no satisfacen tus necesidades, a menudo son necesarias placas personalizadas. Las placas de desarrollo pueden ser demasiado delicadas, conectadas por cables frágiles. Pueden ser demasiado caras para tu mercado objetivo o consumir demasiada energía. Pueden tener el tamaño o la forma equivocados. Puede que no aguanten las condiciones ambientales previstas (como fluctuaciones de temperatura en un coche, mojarse o ir al espacio).

Cualquiera que sea la razón para conseguir una placa personalizada, normalmente significa eliminar el hardware de programación (y depuración) que forma parte de las placas de desarrollo de los procesadores.

El hardware personalizado también te sacará de algunos de los entornos de desarrollo y marcos de software simplificados. Utilizando las capas de abstracción de hardware del proveedor del microprocesador con un compilador tradicional, puedes llegar a tamaños de código más pequeños (a menudo también más rápidos). La ausencia de nada entre tú y el procesador te permite crear un manejo determinista y en tiempo real. También puedes utilizar y configurar un RTOS (sistema operativo en tiempo real). Puedes comprender más fácilmente la licencia del código que utilizas en las bibliotecas.

Añadir un programador/depurador externo te permite depurar más allá de printf, permitiéndote ver el interior de tu código. Esto parece bastante mágico después de hacerlo por las malas durante tanto tiempo.

Aun así, hay un abismo entre el prototipo y el dispositivo de envío, entre el soporte de una unidad en tu mesa y mil o un millón sobre el terreno. No te dejes engañar y creas que el proyecto está completo porque todas las funciones han funcionado por fin (una vez, en condiciones perfectas).

Las placas de desarrollo y los entornos de desarrollo simplificados son estupendos para prototipos y para ayudar a seleccionar un procesador. Pero llegará un momento en que el dispositivo tenga que ser más pequeño, más rápido y/o más barato. En ese momento, entrarán en juego las limitaciones de recursos, y necesitarás un libro como éste para ayudarte.

Otras lecturas

Hay muchas referencias excelentes sobre patrones de diseño. Éstas son mis favoritas:

  • El primero es Patrones de diseño: Elements of Reusable Object-Oriented Software, de Erich Gamma y otros (Addison-Wesley). Publicado originalmente en 1995, este libro desencadenó la revolución de los patrones de diseño de software. Debido a sus cuatro colaboradores, a menudo se conoce como el libro de la "Banda de los Cuatro" (o un patrón de diseño estándar puede señalarse como un patrón GoF).

  • El segundo es Head First Design Patterns, de Eric T. Freeman y otros (O'Reilly). Este libro es mucho más legible que el libro original de GoF. Los ejemplos concretos me resultaron mucho más fáciles de recordar.

Para obtener más información sobre cómo pasar del prototipo al envío de unidades, te recomiendo el libro de Alan Cohen Prototype to Product: A Practical Guide for Getting to Market (O'Reilly).

Get Creación de sistemas empotrados, 2ª edición now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.