Programar como un electrónico

Programando hoy en día las llamadas librerías nos quitan un montón de trabajo, el mejor ejemplo de esto es programar en el Arduino IDE. En él ni siquiera te tienes que preocupar de importar las librerías más usadas, y puedes cambiar de placa sin tener que cambiar ni un poquito del código que has escrito, es casi magia.

Si no sabes de qué te estoy hablando, no pasa nada, se explicará según avance este hilo.

Pero hubo un tiempo, no muy lejano, en el que las cosas eran más complejas y se programaban con ensamblador. Este es el lenguaje de programación más puro (por describirlo de alguna forma) al que te vas a enfrentar cuando programes algo, aunque estoy seguro de que no lo tendrás que usar porque, como digo, es cosa del pasado. Sin embargo está bien, estuvo bien por mi parte aprender eso, porque da una visión mucho más cercana de lo que es en realidad un sistema de este tipo, un microcontrolador, un procesador, su memoria, por dónde van los datos... Que parece innecesario, pero luego hasta los ordenadores se manejan con esto en el fondo. Y eso es lo que quiero transmitir.

Como decía, el ensamblador es el lenguaje de programación más básico, lo que le transmites al programador con lo que escribes no es más que mover unos bits aquí y allá, hacer operaciones lógicas básicas, todo funciona leyendo y escribiendo registros.

¿Qué es un registro?

Bueno, no me voy a poner ahora a explicar qué tiene un ordenador por dentro (RAM, CPU, almacenamiento, buses de datos), pero sí extender esto a los microcontroladores.

En primer lugar, el programa se escribe en la memoria permanente del micro, ahora por lo general son memorias tipo FLASH, ahí se almacena en unos y ceros todo el programa y es el único lugar donde se mantienen los datos inalterados aunque se apague y se encienda. A no ser que el micro tenga otra memoria para datos del usuario, por ejemplo una EEPROM, que ahí también son permanentes, hasta que los borres.

En los micros la CPU es un concepto un poco abstracto, ya que está desperdigada por el resto de componentes. Se encarga de leer la memoria de programa, mover los datos y llevarlos a la unidad aritmético-lógica (ALU), el lugar donde se realizan operaciones matemáticas o lógicas muy simples, como sumar, multiplicar o hacer AND u OR.

Hay memoria RAM, está por ahí para guardar las variables mientras corre el programa, esas variables que se borran tras el apagado.

Hay vida más allá de estos tres componentes, hay lo que se llama registros. Un registro es como otra memoria que se borra al apagar el micro, y está hecha de la misma forma, de una serie de bits. Además tiene una dirección, a la que la CPU puede ir para leer o escribir los datos. Es igual que una memoria simple, pero los registros tienen funciones especiales. Vamos a ver un ejemplo.

El micro está leyendo el programa en la FLASH, y el programa le dice que escriba dos variables en la RAM, que son dos números en binario 110 (6) y 010 (2), y quiere que esas dos variables se sumen. El proceso no es nada simple.

En primer lugar va a coger uno de esos números y lo va a poner en un registro auxiliar (el registro W por ejemplo en el PIC), para ello tiene que pasar por la ALU sin realizar ninguna operación.

En el siguiente paso va a coger el otro número y lo va a poner en la ALU a la vez que el primer número, que estaba en el registro auxiliar, también entra en la ALU por el otro lado. La ALU tiene la instrucción de sumar y obtiene 1000 (8) que se guarda en, ojo cuidado, el registro auxiliar. Una vez terminada la operación hay un registro de estado (STATUS en el PIC), que tiene un bit que se pone a 1 cuando la ALU ha terminado. Se mira este bit y como ya lo hemos visto, hay que decirle que se ponga a 0 para que avise la próxima vez.

Ya sabemos que el resultado está preparado, ahora se puede mover a la RAM para guardar el resultado o incluso enviarlo a un registro de los periféricos, por ejemplo un pin de salida digital, si se envía al registro del puerto digital (p.ej. PORTB), esto haría que (con otras configuraciones previas) el pin correspondiente al bit número 4 de ese registro se pusiese en 1, y encendería... No sé, digamos que se enciende el LED, que siempre queda bonito.

Espero que con esto más o menos ya tengas una idea de qué es un registro. Es eso, un lugar en las direcciones de memoria del micro donde cambiamos bits para que ocurran ciertas cosas. Ya estamos un poquito más cerca de entender qué hace un código en C.

Direcciones

Ahora hablemos de las direcciones, hemos dicho que los registros son lugares en la memoria y que la CPU puede dirigirse a ellos mediante su dirección. Esta viene dada en las hojas de características del micro, es decir, te lo chiva el fabricante, y en numeración hexadecimal, lo que no lo hace muy amigable pero sí algo más compacto. Y tu se lo transmites al programa previamente, aunque aún no lo sepas.

Continuando con el ejemplo, en ensamblador tu no le dices al programa "manda lo que hay en el registro auxiliar al puerto b", lo que dices realmente es "mueve lo que hay en el registro auxiliar al registro con dirección 0x06". En este caso no tienes que decir la dirección del registro auxiliar porque ya está implícita en la función de ensamblador, MOVWF. Al fin y al cabo casi todo lo que se mueve pasa por ese registro, así se escribe menos. Vamos a escribir esta línea en ensamblador:

MOVWF 0x06

Nota: no todo lo que se mueve en todos los micros pasa por ese registro, pero sí la absoluta mayoría. Los más actuales tienen un módulo llamado DMA (direct memory access) que evita precisamente eso, pasando directamente de un registro a otro o a la memoria, con lo que la CPU puede usar el registro auxiliar para otra cosa mientras tanto.

Librerías

Pero menudo lío, ¿no? Pues llegan al rescate las librerías. ¿Qué hacen las librerías? Predefinir las cosas para que no lo tengas que hacer tu, y con esto me refiero a que, con la tecnología tan avanzada que tenemos, podemos hacer lo siguiente:

MOVWF portb

¿Qué acaba de hacer esta librería? Asignar un nombre bonito a una dirección de memoria, para que no tengamos que recordar los numeritos, ha hecho algo tan simple como esto:

portb equ 0x06

Hagamos otro ejemplo, esta vez vamos a poner a 1 un pin del puerto b directamente, pero sin alterar el estado del resto de los pines como hemos hecho (ups) accidentalmente antes. Para ello el ensamblador tiene que hacer:

  • Leer el puerto b y llevarlo al registro auxiliar W
  • Operar con un número binario cuyo resultado cambie el bit exacto que queremos sin alterar el resto
  • Limpiar el bit de STATUS que indica que ha terminado la operación
  • Llevar el resultado del registro W al registro del puerto b

¿Qué has hecho tu en un código C con librerías? Si estamos en Arduino IDE por ejemplo:

digitalWrite(pin4, HIGH)

Pero la ejecución es exactamente la misma, y esto ya viene hecho de antes por otra persona entonces no tienes que preocuparte.

"Bueno no es para tanto"

Pues no, no es para tanto si hablamos de un micro de 8 bits como es el PIC o los más comunes Atmel de Arduino, en estos la dirección de un registro se puede describir mediante un número hexadecimal de 2 dígitos, por ejemplo el 0x06 que hemos visto antes.

PERO. Luego vienen los micros de 32 bits. Los ordenadores son de entre 32 y 64 bits, recordemos. En estos casos la dirección del registro se tiene que escribir con 8 dígitos. 0x00000000. Ya va empeorando el asunto, pero esto no es nada.

En un micro de arquitectura ARM, la configuración de cada uno de los pines alcanza tal grado de complejidad (resistencias pullup/down, multiplexores, funciones analógicas, pwm, comunicación, velocidad, etc etc etc) que los registros empiezan a tener registros dentro. Y sin embargo, gracias a las librerías, logramos compactarlo todo a una serie de instrucciones muy simples y fáciles de recordar, por ejemplo lo que expliqué aquí.

¿Pero entonces se usa para algo el ensamblador? No, el ensamblador se ha sustituído completamente por las librerías, pero sí que usamos de vez en cuando un sucedáneo. Ya no nos gusta escribir la función movwf, sino que podemos hacer algo como:

portb = 0x08

Y en ocasiones, se da que no existen librerías para lo que quieres o simplemente lo vas a hacer más rápido tu directamente que ponerte a buscarlo, por lo que sí que puede pasar que escribas direcciones a mano...

0x06 = 0x08

Como esto cambiaba todo el puerto voy a hacer algo que sólo cambia el pin 4:

0x06.4 = 1

¿Entonces qué es eso de programar como un electrónico?

La programación que se hace en los ordenadores es muchísimas veces independiente del hardware, es decir, vas a hacerlo funcionar en cualquier ordenador, y no importan sus registros, direcciones, ni su configuración porque eso ya lo han implementado previamente los fabricantes de cada uno de los componentes (drivers) y se ha agrupado todo bajo el sistema operativo para hacerlo funcionar sin que te enteres.

En electrónica programamos microcontroladores muy dependientes del hardware y sus periféricos. Cuando hacemos una placa con un micro, le añadimos una señal de reloj, esta señal tiene que ser tratada previamente para que llegue a todos los periféricos correctamente, y esto se hace a nivel de registros (pero con librerías). Nosotros somos el fabricante. Cuando añadimos las entradas y salidas, configuramos cada uno de los pines para que haga exactamente su tarea. Se trata de saber dónde están y a dónde van los bits que quieres manejar, más que poner una función y dejar que el sistema operativo se preocupe por tí.

Estos métodos tienen ciertas ventajas, un mayor control sobre lo que estás haciendo y muchas veces, en vez de usar una librería que lo hace todo, puedes encender únicamente los periféricos que necesitas, con lo que ahorras energía. Otra ventaja es que durante el diseño tienes libertad de colocar las cosas como mejor te convenga, sin tener que seguir los esquemas del fabricante.

Espero que os haya parecido interesante

Arduino serial plotter

Como ya avisé el otro día por twitter, Arduino ha introducido en la nueva versión del IDE (1.6.6) una gráfica de los datos numéricos que entran por el puerto serie. Ahora os enseñaré cómo usarlo.

Materiales

En primer lugar, necesitáis la nueva versión de Arduino IDE 1.6.6, que se puede descargar desde aquí: su página oficial.

Lo siguiente que necesitáis es una placa Arduino que se pueda conectar a un puerto serie virtual (mediante USB), con el ordenador. Si no me equivoco, todas tienen esta funcionalidad, pero las más simples son las que llevan un conector USB integrado y nos ahorran cables y adaptadores.

¿Qué datos enviaremos por el puerto serie? Usaremos un pin del puerto analógico (A0) para leer un sensor, o un valor analógico, de 10 bits. El valor irá de 0 a 1024, en números enteros. Para saber qué lectura de tensión hay, se hace un cálculo simple.

En lugar de un sensor, yo he usado un potenciómetro como divisor de tensión para la primera prueba. Luego lo he reemplazado por un condensador de gran capacidad que conectaba y desconectaba a mano para ver su carga y descarga, en paralelo a una resistencia.

Y éso es todo, la conexión del circuito es así:

Programación

Quizás os pensáis que hay que escribir alguna función especial, pero no. El serial plotter coge absolutamente todo lo que le entra por el puerto serie, entonces lo único que hay que hacer es enviar el dato y ya se visualiza. El código es el siguiente:

/*  Graph
created 2006
by David A. Mellis
modified 9 Apr 2012
by Tom Igoe and Scott Fitzgerald

This example code is in the public domain.

http://www.arduino.cc/en/Tutorial/Graph
*/

void setup() {
// initialize the serial communication:
Serial.begin(9600);
}

void loop() {
// send the value of analog input 0:
Serial.println(analogRead(A0));
// wait a bit for the analog-to-digital converter
// to stabilize after the last reading:
delay(2);
}

 

Aquí tengo los resultados que he obtenido de la carga y descarga de un condensador:

plotter

¿Pero en serio es útil?

Bueno, la cuestión es que podemos visualizar instantáneamente lo que hay en la entrada analógica, pero no está muy bien preparada gráficamente. Para empezar no hay un eje temporal, y la resolución que muestra el eje de amplitud, como veis, desde 0 a 5 voltios apenas pasando por 2.5, es mínima. Esto continua ocurriendo incluso con números grandes. Ni siquiera hay una cuadrícula o rejilla que ayude a medir a ojo.

Los nuevos datos que van apareciendo creo que tardan en desaparecer por la izquierda unos dos segundos, sin duda no es una gráfica para frecuencias "altas", ni variaciones pequeñas.

Por otro lado, se trata de un código en processing, es decir, podemos pasar del plotter que nos trae el IDE de arduino y programarnos el nuestro propio... Esto mejoraría mucho la interfaz y las posibilidades.

En definitiva, está curioso para visualizar rápidamente una señal lo suficientemente lenta y con amplitud adecuada, pero no podemos aproximar su valor muy bien, y de medir gráficamente la frecuencia ni hablar, porque no hay eje de tiempos (ni de número de muestra).

ARM: Interrupciones

En ocasiones, en vez de esperar a que llegue el turno del if que muestrea el estado del botón que estamos pulsando para que ocurran cosas, nos interesa que el microcontrolador esté atento al botón y actúe inmediatamente cuando se pulsa.

Recuerdo el concepto de interrupción en la parte de PICs:

La interrupción se definiría como decirle al PIC “deja absolutamente todo lo que estés haciendo y atiende a la función que he escrito para cuando ésto ocurra”

Un ARM se comporta casi de la misma forma, lo que se añade de nuevo es una lista de prioridades para las distintas interrupciones, debido a que éstos microcontroladores son capaces de llevar muchas más, y están diseñados de forma que pueden atender a todas, si no en el acto, un poco después al finalizar con la anterior.

Por ejemplo, la placa STM32F4Discovery soporta 240 fuentes de interrupción distintas y 256  niveles de prioridad.

Las interrupciones de las que hablaremos ahora son las exteriores, dadas por los pines (EXTI0,1,2,...)

Los ARM disponen de un controlador de interrupciones llamado Nested Vectored Interrupt Controller (NVIC), que compara la prioridad de la interrupción que acaba de saltar con la prioridad de la que se está ejecutando.

CMSIS utiliza unos números (IRQn) para identificar las interrupciones, siendo IRQn = 0 la primera y los valores negativos "excepciones del núcleo del procesador". Por ejemplo el timer principal es el -1 y la línea 0 de interrupciones externas  el 6.

Una vez definida una interrupción, solo hay que ir a la librería stm32f4xx_it.c y añadir la función handler (ISR) correspondiente a dicha interrupción.

Veamos un ejemplo en el que se configura el pin PA0 (botón de usuario) para encender/apagar un LED mediante interrupción:

void main(){
   LED_Init(LED3) //Configura el pin del LED como salida, no voy a entrar en detalle
   EXTILine0_Config() //Configura el pin PA0 como entrada y su interrupción

   while(1); //Esperar a que llegue la interrupción
}

void EXTILine0_Config(){

   //Se declaran las estructuras
   GPIO_InitTypeDef GPIO_InitStructure;
   EXTI_InitTypeDef EXTI_InitStructure;
   NVIC_InitTypeDef NVIC_InitStructure;
 
   //Se le da reloj a GPIOA
   RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
   //Y también a SYSCFG con APB2
   RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);

   //Se configura el pin PA0 como entrada
   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
   GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
   GPIO_Init(GPIOA, &GPIO_InitStructure);

   //Se conecta el pin PA0 a la línea 0 de EXTI
   SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);

   //Se configura la línea 0
   EXTI_InitStructure.EXTI_Line = EXTI_Line0; //Nos referimos a la línea 0
   EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //Va a generar interrupción
   EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; //Con el flanco de subida del botón
   EXTI_InitStructure.EXTI_LineCmd = EXTI_LineCmd_ENABLE; //Habilitamos la línea
   EXTI_Init(&EXTI_InitStructure); //Función de configuración con los parámetros dados

   //Ahora se habilita la interrupción y se le da el nivel de prioridad más bajo
   NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;  //A qué EXTI nos referimos
   NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x01; //Define la prioridad
   //con la que se ejecuta esta interrupción si ya hay otra en marcha, dejando la
   //otra para otro momento si es necesario. A menor número mayor prioridad
   NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01;
   //Si hay 2 interrupciones con la misma PreemptionPriority se define aquí la
   //prioridad que tiene ésta sobre la otra, el número más bajo es el más urgente
   NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //Habilita
   NVIC_Init(&NVIC_InitStructure); //Configura con los parámetros dados
}

A continuación, nos vamos a la librería stm32f4xx_it.c  y abrimos una nueva función que será el handler:

void EXTI0_IRQHandler(){
   if(EXTI_GetITStatus(EXTI_Line0) != RESET){   //Esto es como una bandera
   //Si no está en RESET significa que se ha activado la interrupción
      LED_Toggle(LED3) //Cambia el estado del LED al contrario del que estaba
      EXTI_ClearITPendingBit(EXTI_Line0) //Pone la bandera a RESET
   }
}