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

ANSI C

Contenido

LENGUAJE C ANSI

Ventajas

Desventajas

Los tipos de datos

MODIFICADORES ESTABLECIDOS POR LA NORMA ANSI

CUALIFICADORES ANSI

OPERADORES

DIVISIONES ENTERAS VS REALES

OPERADORES DE MANEJO DE BITS

OPERADORES LÓGICOS Y RELACIONALES

FUNCIONES

ESTRUCTURAS DE CONTROL

PUNTEROS A MEMORIA

DIRECTIVAS

ENSAMBLADOR EMPOTRADO

ESTRUCTURAS

UNIONES

MACROS

OPTIMIZACIÓN DEL CÓDIGO C

Programación orientada a objetos

¿Cómo definimos un objeto?

Recomendaciones generales de optimización en C

 

 LENGUAJE C ANSI

Ventajas:

  • Desarrollo de aplicaciones más rápido
  • Programación más cómoda, disponibilidad de funciones de manejo de módulos internos
  • Mantenimiento menos costoso

Desventajas:

  • Código menos eficiente
  • Mayor ocupación de memoria

Cosas que se requieren en un lenguaje de programación para microcontroladores:

  • Acceso directo a la memoria y al hardware, programación de periféricos
  • Llamar a rutinas en ensamblador o insertar código máquina
  • Conexión directa con las interrupciones
  • Generación de código eficiente en ocupación de memoria y velocidad de ejecución

Entonces, el objetivo de éste tema es aprender a hacer un código más eficiente y algunos trucos que servirán para tener acceso directo a funciones, memoria, periféricos y nuevos tipos de variable.

Los tipos de datos

Sus tamaños y valores, ya conocidos por típicos, son los siguientes:

Tipo Tamaño (bits) Valor mínimo decimal Valor máximo decimal
Char 8 -128 128
Unsigned char 8 0 255
Int 16 -32768 32767
Unsigned int 16 0 65535
Short long 24 -8388608 8388607
Unsigned short long 24 0 16777215
Long 32 -2147483648 2147483647
Unsigned long 32 0 4292967295

 

Y con flotantes:

Tipo Tamaño (bits) Exponente mínimo Exponente máximo
Float 32 -126 128
Double 32 -126 128

 

Una variable contiene un dato que puede modificarse, la declaración incluye:

  • Nombre: máximo 8 caracteres, iniciando con letra y que no sea palabra reservada
  • Tipo
  • Ambito: global, local o externa

 

 

MODIFICADORES ESTABLECIDOS POR LA NORMA ANSI

Se escriben delante del tipo

  1. Auto:
    - Las variables declaradas fuera de las funciones son globales
    - Las declaradas en funciones son locales. El ámbito local tiene prioridad.
    - Si no se inicializa el valor es indefinido
    - Las globales se comportan como estáticas
  2. Extern:
    - Indica que la variable declarada pertenece a otro módulo, por lo que no es necesario reservarle memoria
    - Dentro de un mismo fichero fuente sirve para acceder a una variable aunque todavía no se haya llegado a su declaración
    - Se autoinicializan a cero
  3. Register:
    - Se debe guardar en uno de los registros del procesador
    - Si no es posible, se comporta como Auto
    - Se usa para optimizar el tiempo de ejecución de algunas funciones
  4. Static:
    - Variables locales a una función que retienen su valor en llamadas sucesivas a dicha función
    - Hay que poner static delante de la declaración
    - Se inicializan a cero
    - Ahorra el número de instrucciones para acceder a las variables
  5. Typedef:
    - Asigna un nuevo nombre a un tipo de datos definido por el programador

 

CUALIFICADORES ANSI

  • Const: el contenido de la variable es fijo
  • Volatile: el contenido de la variable puede cambiar
  • RAM: la variable se situa en la memoria de datos
  • ROM: la variable está en la memoria de programa, y se puede asignar cerca o lejos

Ejemplos:

  • Variable en memoria de datos: char data;
  • Variable en código cercano: rom near char data;
  • Variable en código lejano: rom far char data;

CASTING: mecanismo usado para cambiar el tipo de expresiones y variables a = (int)b;

 

 

OPERADORES

Operador Operación aritmética
+ Suma
- Resta
* Multiplicación
/ División
% Módulo
++ Incremento
-- Decremento
= Asignación
Formas reducidas Equivalencia
a+= b a = a+b
A*=b A = a*b

 

DIVISIONES ENTERAS VS REALES

A veces puede ocurrir que una división de números enteros con resultado real se guarda en entero, esto se puede evitar añadiendo .0 a uno de los números, por ejemplo:

  • 4 / 3 = 1 porque el resultado se da en entero
  • 0 / 3 = 1.333 ya está el resultado en real
  • 4 / 3.0 = 1.333
  • 0 / 3.0 = 1.333

OPERADORES DE MANEJO DE BITS

Operador Operación con bits
& AND
| OR
^ XOR
~ NOT
>> Desplazamiento a derecha
<< Desplazamiento a izquierda

 

OPERADORES LÓGICOS Y RELACIONALES

Operador Operación lógica o relacional
== Igual
!= Distinto
> Mayor
>= Mayor o igual
< Menor
<= Menor o igual
&& And
|| Or
! Not

 

FUNCIONES

  • Asociadas a eventos: se incluye el código que se ejecuta cuando se produzca un evento, por ejemplo una interrupción
  • De propósito general: se usan cuando una parte del código se repite varias veces en el programa, haciendo una llamada a la subrutina o función

 

Se componen de tipo, nombre, paso de parámetros (opcional) y un conjunto de instrucciones

Para la devolución de una variable al programa principal, en lenguaje C no hay paso por referencia, se pasa por valor la dirección de la variable a modificar:

Int reiniciar(int *a, int b){ … }

Así, la variable que pasemos como a se modificará tras ejecutar la función pero la variable que pasemos por b se quedará igual. La llamada a la función se tendría que hacer:

Reiniciar(&x, y);

Cuando un array se pasa como argumento a una función, la última de las dimensiones no se define:

Void calcula(int v[], int m[4][]){…}

 

ESTRUCTURAS DE CONTROL

  • Repetitivas: while, do-while, for
  • Selectivas: if, if-else, switch
  • Bifurcación de control: break, continue, goto, return, exit

Una forma simple de hacer un if en una sola línea es:

Y = (a>9 ? 100:200);

Equivale a if(a>9) Y = 100; else Y = 200;

Sentencia break: interrumpe la ejecución de un bucle while, do-while o for

Sentencia continue: se utiliza en los bucles para pasar a la siguiente repetición saltándose lo que falte para el final

Sentencia goto: transfiere el control a la sentencia etiquetada por el identificador. NUNCA debe usarse en C.

PUNTEROS A MEMORIA

Es una variable que contiene la dirección a una zona de memoria donde reside un tipo de dato. Los punteros a memoria de datos ocupan 16 bits.

Char car; //Variable tipo char

Char pcar; //Puntero a una variable tipo char

Car = 0xAA;
pcar = &car; //& me da  la dirección de ‘car’

Hay una forma optimizada de acceso a los datos que es:

#define CAR(*(char *)0x701)
CAR = 0xAA;

Pero entonces el compilador no chequea posibles conflictos

Como habíamos visto antes, algunas constantes se pueden almacenar en la ROM cerca o lejos, y entonces el puntero tiene un tamaño de 16 bits para cerca y 24 bits para lejos

Se pueden hacer punteros a funciones para ejecutar una función distinta según conveniencia, declarando en primer lugar un puntero, después asignando el nombre de la función al puntero y ejecutando el puntero:

Void (*fp)(); //Declara el puntero a una función void

fp = función_A //Asigna el nombre de la función que se va a ejecutar

(*fp)(); //Ejecuta la función asignada

…

Void función_A(void){…} //La función debe estar declarada y desarrollada

Con esto, también se puede pasar una función como argumento de otra función

Char (*f)(int, int); //f es un puntero a una función que devuelve un char y recibe dos enteros como argumento

A un puntero a función como el que acabamos de declarar se le puede asignar como valor cualquier identificador de una función que tenga los mismos argumentos y resultado

DIRECTIVAS

Se escribe antes de la declaración de una variable

  • #pragma varlocate ‘banco’ ‘nombre-variable’
    Esta directiva le dice al compilador, en el momento del linkado, dónde puede encontrar la variable especificada, optimizando el cambio de banco.
    Nosotros debemos especificar en los ficheros fuentes qué variables están afectadas
  • Linker script: es un fichero que proporciona diversa información al linker
    • Define posiciones en memoria de las secciones
    • Define el tamaño y situación de la pila (stack) software
    • Indica la situación de las palabras de configuración

ENSAMBLADOR EMPOTRADO

A veces es necesario incluir algo de ensamblador en medio de un código en C o llamar a una librería en ensamblador

Cómo se llama a variables de C desde ensamblador:

  • Las variables se declaran globales en C
  • Las variables se declaran extern en ensamblador
  • Se utilizan directamente

Cómo se llama a variables de ASM desde C

  • Las variables se declaran globales en ASM
  • Las variables se declaran extern en C
  • Se utilizan directamente

Para llamar a funciones se lleva a cabo el mismo procedimiento, pero la devolución de los valores tiene que ser siguiendo la norma del C18. La función se llama como si se tratase de una en C.

ESTRUCTURAS

Es una estructura de datos donde podemos clasificar las variables como si se tratase de un vector pero pudiendo hacer llamada a una variable concreta de una estructura concreta. Pongamos por ejemplo la lista de propiedades de un objeto, vamos a ver, en el caso de una persona, qué se le puede asignar:

Struct datos

{

Int  peso, altura;

Char nombre[];

} persona;

Entonces podemos llamar a las variables como:

Persona.peso;

Persona.altura;

O asignar sus valores

Persona.nombre = “Pedro”;

Las estructuras de datos son tipos complejos y no deben ser pasados como argumentos ni devueltos por funciones, en su lugar se usan punteros a dichas estructuras.

UNIONES

Un union es igual que una estructura pero todos los campos comparten la misma memoria. Es decir, si modificas una de las variables estarás modificando un trozo del resto.

struct

MACROS

Es un identificador equivalente a una expresión, sentencia o grupo de sentencias

Por ejemplo podemos declarar en macro una función que contiene un if y devuelve el valor del parámetro con valor máximo:

#define máximo (a,b) ((a>b) ? a:b)

En palabras, si a es mayor que b, devuelve a, y si no, devuelve b

Luego se puede llamar como cualquier función: MAX = máximo(x,y);

OPTIMIZACIÓN DEL CÓDIGO C

El objetivo es usar estructuras de datos para aumentar la eficiencia en la programación, es decir, usar objetos. Un objeto es la abstracción de una entidad del mundo real, por lo que se acerca la solución a la semántica de los seres humanos, modelando como objetos las entidades que aparecen en el programa.

Programación orientada a objetos:

  • Busca sintetizar las partes bien definidas de una aplicación mediante la utilización de objetos
  • Es fácil dividir el trabajo
  • Facilidad de prueba de módulos independientes
  • Los objetos pueden ser reutilizables en diferentes sistemas y proyectos
  • Un mismo problema puede requerir definir unos objetos u otros en función del nivel de detalle que se le quiera dar.

¿Cómo definimos un objeto?

Antes hemos visto las clases:

  • Una clase es una abstracción que define propiedades (atributos) y comportamientos (métodos) para un grupo de objetos, y tiene sentido en el diseño.
  • La clase es la plantilla
  • Ejemplo: idea de coche, sabemos que tiene ruedas, un color, una potencia de motor…

Ahora el objeto:

  • Es una realización concreta de una clase
  • Es una instancia (copia) en tiempo de ejecución de una clase
  • Ejemplo: definimos que es un coche de carreras, el número ruedas, el color, motor… etc

¿Qué es un método de una clase? Son las funciones que acceden a los datos atributo de la clase, y llevan a cabo operaciones que cambian su estado. Es el conjunto de mensajes a los que un objeto puede responder. Si quiero saber o modificar el número de ruedas del coche x de la clase “coche”, debo llamar a una función método que devuelve o modifica éste número.

Recomendaciones generales de optimización en C

  • Usar lazos simples
  • Evitar llamadas a subrutinas en lazos
  • Usar subrutinas inline: el compilador inserta la función directamente donde se encuentra la llamada
  • Evitar divisiones o modularizaciones de las operaciones
  • Usar & y shift cuando sea posible
  • Usar la regla 90/10: el 10% de las líneas de código deben ocupar el 90% del tiempo de la carga de la CPU
  • Como objetivo, obtener un código que se ejecuta de forma más eficiente:
    • En tiempo de ejecución
    • En espacio de memoria utilizado
  • Algoritmo adecuado: robusto y eficiente, pensar qué está haciendo realmente el código, familiarizarse con el cuerpo del programa para usar los algoritmos más apropiados.
  • Estructura de datos apropiada: optimizar el uso del espacio interno del código de los programas y la legibilidad de los mismos
  • Reutilización de código: librerías ya optimizadas
  • Elección del tipo variable a utilizar
  • Claridad en el código: programas fáciles de leer y entender para cualquier programador
  • Eliminación de código no utilizado, que nunca se ejecuta
  • Evitar en lo posible las variables globales
  • Mantener las variables lo más locales que se pueda, y mejorar su propagación
  • Cuando se necesite acceder al valor de una variable de una función, se pasará como argumento
  • Utilizar los comentarios para dejar clara la codificación
  • Utilizar MACROs para aclarar las construcciones confusas
  • Insertar líneas en blanco en la codificación para agrupar ideas del código
  • Importante declarar variables volatile cuando:
    • La localización de memoria pueda ser modificada por algo que no sea el compilador
    • El orden de las operaciones no debe ser cambiado por el compilador