Antes de empezar, que quede claro que esto no es un manual de programación en C. Son trucos y explicaciones para que resulte más sencillo programar en bajo nivel mediante el lenguaje C.
El tipo de datos int
El lenguaje especifica que el int tiene que ser del tamaño natural del sistema, esto quiere decir que si usas un micro de 8bits el int debe ser de 8 bits, si el micro es de 16, pues el int lo mismo, de 16bits...
El problema que esto genera es que si necesitamos cambiar el programa para usarlo con otro micro, puede que no sea compatible.
Para evitar incompatibilidades, lo suyo es crear una librería de tipos en los que definimos los que usamos y, si necesitamos cambiar de micro, modificamos esta librería y listo.
Por ejemplo:
#ifndef TIPOS_H
#define TIPOS_H
typedef unsigned char uint8;
typedef unsigned short int uint16;
typedef unsigned long int uint32;
typedef signed char int8;
typedef signed short int int16;
typedef signed long int int32;
#endif
Así con modificar la librería tipos.h en cada compilador no tendremos incompatiblidades.
Manipulación de Bits
Los registros de los micros, así como sus periféricos, suelen trabajar con bytes, pero para configurar algo solemos necesitar trabajar con un bit sólo. Para ello usaremos los siguientes operadores:
- << y >>: rota x bits a la derecha o a la izquierda según indican "las flechas". Se usaría así: temp <<4; rotaríamos 4 bits a la izquierda la variable temp. Hay que tener en cuenta, que los bits de más peso pasarán a ser los de menos peso, en el ejemplo estamos cambiando los 4 bits de más peso por los 4 de menos peso.
- &: operador and, realiza la operación lógica and. Si dos bits son 1 el resultado es 1, en caso contrario 0.
- |: operador or, realiza la operación lógica or. Si los dos bits son 0, el resultado es 0 en caso contrario 1.
- ~: operador not, realiza la operación lógica de negación. Cambia el valor de 0 a 1 y de 1 a 0. Para introducirlo, hay que pulsar la tecla "alt", la secuencia de números 126 y soltamos "alt".
- ^: operador xor, realiza la operación lógica XOR. Si los bits son distintos el resultado es 1, en caso contrario es 0.
Gracias a estos operandos y a las máscaras, podemos trabajar con los bits.
Las máscaras son definiones nemónicas que crearemos para simplificar el código. Por ejemplo, si el bit 3 del registro STATUS es WAKE, crearemos una máscara #define WAKE_STATUS 0x04.
Así, si queremos activar el bit WAKE, haremos una operación OR entre STATUS y WAKE_STATUS: STATUS |= WAKE_STATUS.
Si queremos comprobar el estado del bit haremos: if (STATUS & WAKE_STATUS). Darse cuenta de que sólo lleva un &, con dos estaríamos poniendo 2 condiciones (indicaríamos que tiene que estar activo STATUS y WAKE_STATUS), al poner sólo uno estamos indicando que es el operando and.
Si queremos poner a 0 el bit haremos un and con un not: STATUS &= ~(WAKE_STATUS).
Y si queremos cambiar su valor, haremos un XOR WAKE_STATUS ^= WAKE_STATUS.
También se pueden definir estructuras para tratar con bits.
typedef struct{
uint8 digito0 :4,
digito1 :4;
}BCD2;
En este caso se define una estructura para una número en Binario Codificado en decimal (BCD). Este sistema se utiliza para almacenar números del 0-9 en un rango de 4 bits, así por ejemplo el número 12d=0001 0010; el primer dígito del número es un grupo de 4bits y el segundo otro.
Esta codificación se utiliza en los displays de 7 segmentos para poder sacar cada dígito por un display.
Volviendo al ejemplo, si queremos modificar los datos de la estructura:
BCD2 valor = {1, 2};
Definimos una variable tipo BCD2 de nombre valor y la inicializamos con el dato 12.
Este tipo de estructuras es muy útil para acceder a registros del microcontolador.
Si una estructura la llamamos con "nombre del registro"_bits y cada variable de la estructura son los datos del registro; inicializamos un puntero con la dirección de memoria del registro y lo nombramos con "nombre de registro". Si cargamos el valor de "nombre del registro"_bits en la variable "nombre del registro" podremos acceder al bit o bits que necesitamos.
typedef struct{
unit8 PB7:1,
PB6 :1,
PB5 :1,
PB4 :1,
PB3 :1,
PB2 :1,
PB1 :1,
PB0 :1;
}PORTB_bits
#define PORTB (*(volatile uint8 *)0x40) //suponiendo que la dirección de PORTB es 40h
PORTB_bits.PB6=1;
PORTB=PORTB_bits;
En vez de estructuras, se pueden usar uniones; en el fondo es lo mismo.
La diferencia entre una estructura y una unión es que cada variable interna de una estructura tiene asignada una zona de memoria y para la unión se asigna una zona para toda la unión.
Siguiendo con el ejemplo anterior:
typedef union{
unit8 PB7,
unit8 PB6 ;
unit8 PB5 ;
unit8 PB4 ;
unit8 PB3 ;
unit8 PB2 ;
unit8 PB1 ;
unit8 PB0 ;
}PORTB_bits
Atentos a la diferencia en la definición. En las estructuras sólo se pone punto y coma en la última definición, en las uniones en todos las variables.