sábado, marzo 12, 2016

Buenas Maneras (II): Errores y excepciones

En C++, en JAVA y en C# (y supongo que en la mayoría de los lenguajes de alto nivel), existe una herramienta muy potente para la gestión de errores dentro de un programa: las Excepciones.

A decir verdad en C++ no pasan más que por ser un retorno de un int por una vía distinta a la habitual, sin embargo sigue siendo un camino a tener en cuenta. Hay que tener en cuenta que las excepciones no son necesariamente errores en una función, sino situaciones excepcionales y distintas al flujo normal del programa (la propia definición de la palabra lo dice).

He observado que algunos programadores utilizan las excepciones para devolver valores o ejecutar un código dentro del flujo normal del programa, y esto a mi modo de ver es un error muy grande.

Para hacerlo más claro voy a explicar un ejemplo:
Un programa que comunica el PC con un microcontrolador vía serie. Al enviar el PC una orden, el micro responde que la operación no se ha podido realizar de forma correcta. Algunos programadores utilizan una excepción para informar al usuario, lo cual es un error. En este caso deberíamos tener en cuenta los posibles casos que el micro pueda informar y tratarlos adecuadamente.
Debemos dejar las excepciones para resolver problemas que nos surjan fuera del flujo normal, e intentar dejar las respuestas de las diferentes partes del programa al margen de las excepciones.

Otro ejemplo un poco menos claro:
En el mismo programa de antes se debe tener en cuenta que el micro pueda estar desconectado u ocupado realizando alguna otra tarea. Esto generalmente provocará un TIMEOUT en la llamada al puerto serie. Muchos programadores utilizan una excepción para informar al usuario que el puerto no responde (en la propia excepción). Si bien el TIMEOUT es una excepción en sí misma, no se debe utilizar para informar al usuario, sino que, lo que se debería hacer es recoger la excepción y tratarla en el flujo normal del programa (reintentos de enviar el comando, informar al usuario, etc.).

En definitiva para una función cualquiera la cabecera debería seguir la siguente pauta:

tRespuesta Funcion(tParametros) throw tExcepcion

Donde tRespuesta definirá las respuestas normales de la función y tExcepcion definirá los errores y excepciones que no hayan podido ser tratados dentro de la función o que deban ser informados a otras funciones.

En C el tema cambia, ya que no existen las excepciones de forma nativa, así que u optamos por utilizar librerías externas que implementan macros para realizar esta función, o directamente nos montamos nosotros el sistema.

En mi caso he decidido hacer esto último de una forma muy sencilla: todas mis funciones devuelven un entero:
int Funcion(tParametros)

de tal forma que si el entero es un número negativo la función está devolviendo un código de error. Si la función devuelve 0 (OK) es un funcionamiento normal y dejo los números positivos para la devolución de otros resultados (por ejemplo números de bytes leidos de un puerto, número de estado de una máquina de estados, etc.)

De esta forma siempre puedo hacer el siguiente código:

#include "error.h"
return = foo(parameters);
if(return < OK) { // Tratamiento de los errores }

He incluso tratar cada uno de los posibles errores por separado con un switch-case-default.

Evidentemente reconocer los posibles problemas dentro de la función (punteros no válidos, indices fuera de rango, etc.) corre a cargo mío, pero es un sistema que me funciona bastante bien y muy simple y rápido de implementar.

Sólo hay que tener en cuenta que los valores a devolver por la función siempre tienen que ser enteros positivos y que si se necesitaran otro tipo de valores se deberá pasar un parámetro por referencia para guardarlos allí.

Un apunte, algunos microcontroladores tienen implementado una serie de traps, es decir funciones a las que se llaman cuando hacemos una operación prohibida (como una división por cero), estas traps ya hacen de excepciones (aunque en realidad son errores graves) por lo que se debería no intentar que saltasen nunca ya que hacerlo debería significar hacer un reset del programa.


S2

Ranganok Schahzaman

sábado, marzo 05, 2016

Entendiendo... los Osciloscopios (III): cuantificación

Una vez que hemos determinado el muestreo, el convertidor analógico-digital (ADC) tiene que cuantificar el valor de la señal recibida (dado que ha de pasarlo a información digital), y como no tenemos una memoria infinita truncará la resolución con la que se adquiere la medida.

Dependiendo del número de bits que resuelva el ADC se tendrán más o menos niveles. Por ejemplo para un ADC de 8 bits se tienen 256 niveles posibles de tensión, para uno de 10 bits 1024 niveles, etc. (a razón de 2^n niveles). Es decir, si ponemos una señal rampa (continúa) a la entrada del osciloscopio, el ADC lo digitalizará de la siguiente forma:

Cuantificacion Midthread
Niveles de cuantificación con 4 bits
Los escalones serán más finos cuantos más niveles (más bits de información por muestra) tengamos. Con cada bit extra se dobla el número de niveles, por lo tanto queda claro que nos interesan un ADC del máximo número de bits posibles.:
2-bit resolution analog comparison3-bit resolution analog comparison
Cuantificación de una
señal senoidal con  2 bits
Cuantificación de una
señal senoidal con  3 bits
Además hay que tener en cuenta que, aunque aquí se muestra todos los niveles de cuantificación disponibles para la señal, en un osciloscopio la cuantificación se realiza para el fondo de escala (es decir para lo que se muestra en pantalla), por lo que para la señal los bits disponibles siempre serán menos.

Aquí nos encontramos con el problema de siempre: no lo podemos tener todo, los ADC's tardan un tiempo en cuantificar la señal, y este tiempo augmenta según augmenta el número de bits, por lo que tener un ADC de 6GS/s y 24bits será tremendamente complicado (y muy muy caro). Así que debemos preguntarnos que cuál es la resolución y la velocidad que necesitamos para nuestra aplicación -evidentemente 6GS/s y 24bits servirán para practicamente todas las aplicaciones que necesitemos, sin embargo ¿necesitamos tantos datos?-.

Por un lado ¿cómo de rápidos serán los cambios? No es lo mismo leer una señal térmica (que toma algunos segundos en variar) que una "glich" en la señal, por lo que necesitamos un ancho de banda suficiente para detectar la señal (ya hemos hablado de esto y como mínimo necesitamos el doble del ancho de banda de la señal).

Además de eso, de que magnitud es el cambio para que sea relevante respecto a nuestra señal principal:
  • En una señal de 5V con 8 bits tendremos 256 niveles, es decir que cada bit corresponderá a unos 20mV (5V/256 = 19.53mV) de resolución
  • Si utilizamos 10bits tendremos 1024 niveles que cada bit corresponderá a unos 5mV (5V/1024 = 4.88mV)
  • Para 12bits, es decir 4096 niveles, la resolución será de aproximadamente 1mV (5V/4096=1.22mV), etc.
¿Realmente necesitamos ver la variación de 1mV sobre una señal de 5V (un 0,02% de la señal)?

Por lo que debemos tener una idea de la variación necesaria de la señal para que esta sea suficientemente importante para ser registrada, sobretodo sabiendo que si necesitamos mayor resolución en un punto aplicaremos una ganancia a la señal analógica antes del ADC, o lo que es lo mismo cambiaremos el fondo de escala a un valor menor (en vez de 5V aplicaremos 2V, 1V, 500mV, etc. en las ecuaciones anteriores).

Por otro lador  en un osciloscopio nos limita la resolución de la pantalla, por lo que si la pantalla es Full HD, es decir, con 1080 pixeles de altura, ¿qué sentido tiene tener 12bits (4096 niveles)? Seremos incapaces de representarlos en la propia pantalla del osciloscopio, y eso sin contar que muchos osciloscopios todavía utilizan pantallas con resolución VGA (o XVGA) de 480 pixeles de altura.

Lo más sencillo es sacrificar la velocidad y tener ADCs de más bits (podemos tener 24bits y 110kS/s muy baratos), o, por el contrario sacrificar los bits y tener major velocidad (8bits y 250MS/s bastante asequibles). Por ejemplo, los osciloscopios modernos de bajo coste (<500€) suelen tener 8bits y 1GS/s de muestreo en total (para los 2 o 4 canales que tienen). Incluso los de marca de coste asequible aumentan la velocidad sin cambiar el número de bits (8bits y 2GS/s). Es decir, los fabricantes en general están de acuerdo que, en un osciloscopio es más importante la velocidad de muestreo que el número de bits aplicados a cuantificar la señal.

S2

Ranganok Schahzaman




sábado, febrero 20, 2016

Buenas Maneras (I): Llaves e identaciones

Inauguro una pequeña guía de estilo de como me gusta encontrar un código cuando trabajo en él (llamadme maniático si queréis).

A la hora de diseñar e implementar un nuevo firmware (o programa) no tengo mucho problema, ya que lo hago a mi gusto, sin embargo a la hora de mantener un programa o firmware realizado por otra persona es donde me suelo encontrar los problemas... Depende de lo hijop#t@ "limpio" que sea el programador anterior, que el código este comentado, indentado correctamente y bien diseñado.

Existen muchas guías de estilo (de hecho todos los proyectos grandes tienen la suya propia), pero yo me he hecho la mía a lo largo de los años y he notado que los errores se reducen y me es más sencillo leer y entender código antiguo.

Para no liarnos escojamos un lenguaje de programación de base: C++. Escojo este porque permitirá hacer comentarios tanto para lenguajes de bajo nivel (C), como para alto nivel (Java o C#).

Hoy vamos ha hacer algunos comentarios del estilo básico a la hora de escribir un programa (lo que llamaríamos la caligrafía).

Antes de empezar

Antes de empezar hay que tener en cuenta ciertas cosas:
  1. La guía de estilo debe ser clara en todas las situaciones. Yo recomiendo partir siempre de alguna guía ya establecida en un proyecto grande, de esta forma habrá muchos temas ya contemplados y que ni siquiera se nos habrían ocurrido en el primer momento (a título personal yo uso la guía de ANSI de base).
  2. La guía de estilo debe ser la misma para todos los participantes de un proyecto. No es nada agradable ver distintas partes del proyecto formateadas de forma diferente, además es confuso y puede llevar a errores.
  3. La guía de estilo debe ser estable en el tiempo de vida de un proyecto. Las guías de estilo no son inmutables, todo el mundo incorpora prácticas nuevas y mejores en su programación, sin embargo en un mismo proyecto es importante mantener la misma guía de estilo a lo largo de todo el tiempo de desarrollo para no crear confusión a la hora de leer el código. Aquí hay dos opciones, o se mantiene el estilo anterior o se cambia en todo el código. Hacer una u otra cosa dependerá de muchas cosas pero si se va a cambiar todo el código hay que tener en cuenta que haremos una pequeña ruptura con el código anterior (sobretodo si utilizamos un control de versiones), y hay que hacerlo de forma muy controlada.
  4. Se debe automatizar el proceso. Independientemente que interioricemos el proceso de escritura es importante usar un IDE que puedas automatizar el proceso de corrección de estilo (la mayoría de los IDEs modernos ya tienen esta opción incluida), de esta forma será mucho más sencillo. Sin embargo, en ningún caso implica que debamos dejarlo todo al IDE, en multitud de ocasiones no tendremos la oportunidad de realizar la automatización, por lo que será bueno que nos acostumbremos a escribir el código según la guía que hallamos elegido.

Llaves e identaciones

Primera regla de las identaciones usad siempre tabulaciones y no espacios esto incluye a no convertir las tabulaciones en espacios. A parte de ser una manía mía, implica desplazarse por el código más rápido y evita desajustes en el código cuando se tocan accidentalmente las teclas Supr o Retroceso. Sólo recomiendo usar los espacios para casos extremos en los que no haya más remedio (cuando no se puedan usar tabulaciones como el caso de esta bitácora). Otra cosa, las identaciones que ocupen el tamaño de 4 espacios (esto generalmente se puede utilizar Veamos ejemplos incorrectos, a mi modo de ver, de usar las identaciones:

¡¡¡ HORROR !!!, esta forma de programar hace que el código sea cada vez más ilegible (sobretodo cuando llevas 10000 líneas de forma similar). La solución correcta sería:

Sí, ya se que queda más largo el código, pero queda más limpio y te deja sitio para poner los comentarios que hagan falta. Por cierto poner !i o poner i==0 no es indiferente, la primera se deberá utilizar cuando sea una variable de verdadero o falso (booleano) y la segunda cuando sea una variable numérica. Otro:

 Esto queda feo con ganas, a parte que (personalmente) le tengo una manía horrible a poner la llaves no de forma identada (que empiece el bloque y lo acaben en la misma columna), y sobretodo cuando en vez de a=0 hay 100 líneas de código te puedes perder. Quedaría así:

o en este caso que tenemos una sóla línea:

fácil limpio y corto. Sin embargo es recomendable utilizar la primera opción para que las llaves limiten el bloque (menos propenso a errores). Otro ejemplo con la instrucción switch, partirmos de:

Cuando tenemos que hay mucho código dentro de los case queda muy sucio y poco comprensible. Una buena forma de no perdernos sería:

Así tenemos alineado el código y dentro de unas llaves, por lo que será más legible y más fácil de seguir. Siguiendo con el switch es bueno que la variable a sea de tipo enum (no siempre se puede), de esta forma podremos hacer un código más descriptivo, y por lo tanto más sencillo de leer.


Más adelante seguiremos avanzando con este y otros temas que nos permitirán aumentar la productividad a la hora de escribir y corregir código.

S2

Ranganok Schahzaman