Tecnología > Electricidad y Electrónica

TUTORIAL: ATtiny 84 con avr-gcc.

(1/3) > >>

Manuel:
Vamos a ver cómo programar un ATtiny84 de forma fácil y autónoma, usando avr-gcc, en varios episodios que iré indexando.


* Montaje básico.
miniFAQ:

¿Por qué ATtiny84?
Es un chip barato (menos de 1,50 euros en formato DIP14), el más alto de su clase con 8K de flash y 512 bits de SRAM y EEPROM. Está disponible en formato DIP (hobbyist-friendly). Puede funcionar hasta a 20Mhz de frecuencia. Tiene un ADC de 7 canales, con 10 bits de resolución. Ponerlo a funcionar en una protoboard requiere una cantidad mínima de componentes. A ese precio, puede sustituir a varios chips lógicos, y salir más barato y ser más versátil. Además, es fácil portar estas ideas a muchos otros chips ATtiny.

¿Por qué avr-gcc?
Es gratis, rápido y fácil. Dispone de gran cantidad de herramientas auxiliares para desensamblar y analizar programas. Si quieres ensamblar, puedes hacerlo inline con gran facilidad.

¿Vas a usar algún firmware?
Ninguno. La idea es emplear código que se sostenga por si mismo, para tener acceso completo al aparato con un mínimo de derroche en espacio.

Manuel:
Para resumir todas las ideas, veamos cómo hacer un blink mínimo en un ATtiny84 funcionando a 20Mhz.

Lo primero, monta el chip en una protoboard, llevando el pin1 a 5V, el pin14 a tierra, los pin2 y pin3 a tierra con sendos condensadores de 22pF, y conectados entre si con un cristal a 20Mhz (o la frecuencia que quieras, por encima de 3Mhz). Para hacer el blink, conecta un LED al pin8, y llévalo a tierra con una resistencia razonable (200 Ohm o así.) Una imagen aquí.

Ahora, programa los fuses del microcontrolador para que funcione bien a esta frecuencia de reloj. Esto se logra con avrdude, usando el comando:


--- Código: ---avrdude -c arduinoISP -p t84 -U lfuse:w:0xff:m -U hfuse:w:0xdc:m -U efuse:w:0xff:m
--- Fin del código ---

Es posible que tengas que cambiar el tipo de programador, por ejemplo, -c usbasp si usas el usbasp, etc. Esta elección de fuses prepara el micro para funcionar con el cristal, y activa del brown-out detector a un nivel razonable.

Ahora vamos a por el programa del blink en si mismo:


--- Código: (C) ---#include <inttypes.h>
#include <avr/io.h>
#include <util/delay.h>
#include <avr/eeprom.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include <string.h>
 
int main ()
{
DDRA |= (1 << 5);      // Pin 5 como salida.
    for (;;)
    {
        PORTA |= (1 << 5);      // Enciende pin 8.
         _delay_ms(250);
PORTA &= ~(1 << 5);   // Apaga pin 8.
         _delay_ms(250);
    }
}
--- Fin del código ---

La parte de main() en este código es bastante clara, si uno se lee la documentación de ATtiny84: se está configurando el bit 5 del puerto A, que corresponde al pin 8 del chip. Se usan los registros DDRA (Data Direction puerto A) para configurarlo como salida, y el registro PORTA para encender y apagar el LED. La macro _delay_ms está definida en el fichero de cabecera util/delay.h y es precisa y fácil de usar.

Una cosa que llama mucho la atención al pasar del ensamblador al C es que en este programa no se hace mención de los vectores de interrupción. ¡Y al menos necesitamos el vector de reset! ¿Cómo resuelve avr-gcc este problema? Parece que tenemos dos problemas:


* Queremos que nuestro código main() y demás subrutinas no sobreescriban los vectores de interrupción, sino que empiecen más adelante que ellos.
* Queremos almacenar en los dos primeros bytes de la flash un salto relativo a main().
Las versiones modernas de avr-gcc se ocupan directamente de esto, como veremos en la parte siguiente, sobre desensamblado de nuestro programa. Por ahora, podemos confiar en que gcc se encargará adecuadamente del trabajo sucio.

Por tanto, vamos a compilar el código fuente y subirlo. Para compilar,


--- Código: ---avr-gcc -c main.c -o main.o -Os -mmcu=attiny84 -D__AVR_ATtiny84__ -DF_CPU=20000000L
--- Fin del código ---

La mayor parte de esto no es muy misteriosa. Las dos definiciones podrían haberse incluido en el fichero fuente:


--- Código: (C) ---#define __AVR_ATtiny84__              // Modelo de mcu.
#define F_CPU= 20000000L         // Velocidad del reloj.
--- Fin del código ---
Observa que si tu reloj va a otra frecuencia, debes cambiarlo aquí. Por ejemplo, a 8Mhz deberías usar F_CPU=8000000L.

La optimación -Os es fundamental para que algunos de los ficheros de cabecera funcionen, así que no es conveniente saltársela.

Una vez que tenemos el fichero main.o generado por el compilador, podemos generar un fichero .elf con el programa:


--- Código: ---avr-gcc main.o -o main.elf -Os -mmcu=attiny84 -D__AVR_ATtiny84__ -DF_CPU=20000000L
--- Fin del código ---

Las flags del compilador son idénticas, pero esta vez sólo se hace el linkado.

Ahora extraemos el fichero .eep, que contiene la información a subir a la eeprom: en nuestro caso, no hay ninguna que subir:


--- Código: ---avr-objcopy -j .eeprom --set-section-flags=.eeprom='alloc,load' --change-section-lma .eeprom=0 -O ihex main.elf main.eep
--- Fin del código ---

Y la parte a subir a la flash de nuestro programa, que en este caso es todo:


--- Código: ---avr-objcopy -O ihex -R .eeprom main.elf main.hex
--- Fin del código ---

El fichero main.hex es el que subiremos usando avrdude. Antes de hacerlo, de todos modos, es buena idea saber cuánto estamos usando de memoria, por si nos hemos pasado. Para ello, ejecutamos:


--- Código: ---avr-size --mcu=attiny84 -C --format=avr main.elf
--- Fin del código ---

La salida es el clásico mensaje al que estamos aconstumbrados en el software de Arduino:


--- Código: ---AVR Memory Usage
----------------
Device: attiny84

Program:    96  bytes (1.2% Full)
(.text + .data + .bootloader)

Data:          0 bytes (0.0% Full)
(.data + .bss + .noinit)

--- Fin del código ---

Por supuesto, nuestro minimalista blink apenas ocupa memoria. Para subir el programa a la flash, invocamos a avrudude:


--- Código: ---avrdude -c arduinoISP -p t84 -B 9500 -e -U flash:w:"main.hex"
--- Fin del código ---

La opción -B 9500 ralentiza el envío, porque en mi caso lo hago a través de cables largos, y a grandes velocidades hay errores de transmisión. Si tu programamdor va rápido, puedes ahorrarte esta línea.

Y con esto tu attiny está listo para correr el programa. El proceso puede parecer muy largo, pero creando una macro, o un makefile (más adelante publicaré el mío), todo el procedimiento es muy rápido y adaptable a otros proyectos.

Carlos:
Te recomiendo estas instrucciones para hacer más legible el código:


--- Código: ---#define SET_BIT(byte, bit)  byte |= (1<<(bit))
#define CLEAR_BIT(byte, bit)  byte &= ~(1<<(bit))

   SET_BIT(DDRA, 5);     // Pin 5 como salida.

--- Fin del código ---

Además a mí me gusta crear un archivo de cabecera en todas las placas que hago para traducir los pines.
Dejo aquí un pequeño ejemplo:


--- Código: ---
typedef struct {
   unsigned char b0:1;
   unsigned char b1:1;
   unsigned char b2:1;
   unsigned char b3:1;
   unsigned char b4:1;
   unsigned char b5:1;
   unsigned char b6:1;
   unsigned char b7:1;
} bits;

// Define input and output pins
#define PORT_A (* (volatile bits *) &PORTA)
#define PIN_A  (* (volatile bits *) &PINA)
#define DDR_A  (* (volatile bits *) &DDRA)

#define PORT_B (* (volatile bits *) &PORTB)
#define PIN_B  (* (volatile bits *) &PINB)
#define DDR_B  (* (volatile bits *) &DDRB)

#define PORT_C (* (volatile bits *) &PORTC)
#define PIN_C  (* (volatile bits *) &PINC)
#define DDR_C  (* (volatile bits *) &DDRC)

#define PORT_D (* (volatile bits *) &PORTD)
#define PIN_D  (* (volatile bits *) &PIND)
#define DDR_D  (* (volatile bits *) &DDRD)

// Define standar Input/Output values
#define DDR_OUTPUT  1
#define DDR_INPUT   0

#define PORT_HIGH   1
#define PORT_LOW    0

#define PIN_HIGH    1
#define PIN_LOW     0

// Output leds
#define DDR_D1    DDR_C.b7
#define DDR_D2    DDR_C.b0
#define DDR_D3    DDR_D.b6
#define DDR_D4    DDR_B.b0
#define DDR_D5    DDR_A.b0
#define DDR_D6    DDR_A.b1
#define DDR_D7    DDR_D.b5
#define DDR_D8    DDR_D.b7

#define PORT_D1   PORT_C.b7
#define PORT_D2   PORT_C.b0
#define PORT_D3   PORT_D.b6
#define PORT_D4   PORT_B.b0
#define PORT_D5   PORT_A.b0
#define PORT_D6   PORT_A.b1
#define PORT_D7   PORT_D.b5
#define PORT_D8   PORT_D.b7

--- Fin del código ---

Un saludo.

Carlos:
Manuel, ¿sabes cómo se puede ver la lista de instrucciones en ensamblador después de compilar el programa c?

Manuel:
Vamos a desensamblar el programa compilado en el capítulo anterior, para hacernos una idea de cómo trata el compilador nuestros programas. La manera de lograrlo es:


--- Código: ---avr-objdump build/main.elf --architecture=avr:25 -S
--- Fin del código ---

Conviene usar el fichero .elf generado anteriormente, para tener una versión definitiva de lo que se va a subir: la información proveniente del fichero objeto puede ser menos reveladora. El ATtiny84 tiene arquitectura avr25, y el modificador -S pide desensamblarlo todo. El resultado es:


--- Código: ---build/main.elf:     file format elf32-avr


Disassembly of section .text:

00000000 <__vectors>:
   0: 10 c0        rjmp .+32      ; 0x22 <__ctors_end>
   2: 17 c0        rjmp .+46      ; 0x32 <__bad_interrupt>
   4: 16 c0        rjmp .+44      ; 0x32 <__bad_interrupt>
   6: 15 c0        rjmp .+42      ; 0x32 <__bad_interrupt>
   8: 14 c0        rjmp .+40      ; 0x32 <__bad_interrupt>
   a: 13 c0        rjmp .+38      ; 0x32 <__bad_interrupt>
   c: 12 c0        rjmp .+36      ; 0x32 <__bad_interrupt>
   e: 11 c0        rjmp .+34      ; 0x32 <__bad_interrupt>
  10: 10 c0        rjmp .+32      ; 0x32 <__bad_interrupt>
  12: 0f c0        rjmp .+30      ; 0x32 <__bad_interrupt>
  14: 0e c0        rjmp .+28      ; 0x32 <__bad_interrupt>
  16: 0d c0        rjmp .+26      ; 0x32 <__bad_interrupt>
  18: 0c c0        rjmp .+24      ; 0x32 <__bad_interrupt>
  1a: 0b c0        rjmp .+22      ; 0x32 <__bad_interrupt>
  1c: 0a c0        rjmp .+20      ; 0x32 <__bad_interrupt>
  1e: 09 c0        rjmp .+18      ; 0x32 <__bad_interrupt>
  20: 08 c0        rjmp .+16      ; 0x32 <__bad_interrupt>

00000022 <__ctors_end>:
  22: 11 24        eor r1, r1
  24: 1f be        out 0x3f, r1 ; 63
  26: cf e5        ldi r28, 0x5F ; 95
  28: d2 e0        ldi r29, 0x02 ; 2
  2a: de bf        out 0x3e, r29 ; 62
  2c: cd bf        out 0x3d, r28 ; 61
  2e: 02 d0        rcall .+4      ; 0x34 <main>
  30: 15 c0        rjmp .+42      ; 0x5c <_exit>

00000032 <__bad_interrupt>:
  32: e6 cf        rjmp .-52      ; 0x0 <__vectors>

00000034 <main>:
  34: d5 9a        sbi 0x1a, 5 ; 26
  36: 24 ef        ldi r18, 0xF4 ; 244
  38: 31 e0        ldi r19, 0x01 ; 1
  3a: dd 9a        sbi 0x1b, 5 ; 27
  3c: 84 ec        ldi r24, 0xC4 ; 196
  3e: 99 e0        ldi r25, 0x09 ; 9
  40: f9 01        movw r30, r18
  42: 31 97        sbiw r30, 0x01 ; 1
  44: f1 f7        brne .-4      ; 0x42 <__SREG__+0x3>
  46: 01 97        sbiw r24, 0x01 ; 1
  48: d9 f7        brne .-10      ; 0x40 <__SREG__+0x1>
  4a: dd 98        cbi 0x1b, 5 ; 27
  4c: 84 ec        ldi r24, 0xC4 ; 196
  4e: 99 e0        ldi r25, 0x09 ; 9
  50: f9 01        movw r30, r18
  52: 31 97        sbiw r30, 0x01 ; 1
  54: f1 f7        brne .-4      ; 0x52 <__SREG__+0x13>
  56: 01 97        sbiw r24, 0x01 ; 1
  58: d9 f7        brne .-10      ; 0x50 <__SREG__+0x11>
  5a: ef cf        rjmp .-34      ; 0x3a <main+0x6>

0000005c <_exit>:
  5c: f8 94        cli

0000005e <__stop_program>:
  5e: ff cf        rjmp .-2      ; 0x5e <__stop_program>

--- Fin del código ---

Lo más importante es comprobar que, en efecto, avr-gcc se ocupa de gestionar automáticamente los vectores de interrupción, y hacer que el vector 0 apunte  a main(). Los demás vectores apuntan a <__bad_interrupt> (0x32), que directamente lleva a un reset. En otro episodio veremos cómo usar la macro ISR() del fichero avr/interrupt.h para alterar otros vectores, y cómo esto se refleja de manera clara en el ensamblador.

Veamos algunos detalles interesantes del desensamblado:

Después del reset, se salta a <__ctors_end> (0x22), claramente abreviatura de "vectors end". Allí se pone a cero el registro r1 (en C ese registro siempre está a cero),  y a continuación se anula el registro de estado (out 0x3f, r1). A continuación se inicializan los registros de pila (0x3e y 0x3d) en el valor 0x25f = 607. Este valor parece misterioso hasta que se consulta el mapa de la memoria RAM del ATtiny (hoja de datos, Sec. 5.1.1, pág. 16): la memoria comienza con 0x60 registros de entrada-salida, y luego los 512 bytes (0x200) de SRAM  lo que, empezando en cero, da el top de la RAM en 0x260 - 1 = 0x25f. La pila es inicializada en el top de la RAM y, naturalmente, crece hacia abajo.

Inicializada la pila, es seguro hacer un call a main() y comenzar las rutinas en C.

La función main() no tiene gran interés. Es un enorme bucle con los retardos _delay_ms() preprogramados en forma de bucles internos. Todo está bastante bien y resulta adecuadamente compacto.

Observa que, en caso de que main() no fuera un bucle infinito, finalmente retornaría a <__ctor_end>, el cual llamaría a <exit> y entraría en un bucle infinito. Todo es seguro y está bien pensado.

Así pues, esta es la anatomía de nuestro programa blink. Es bastante tranquilizador ver lo bien que hace su trabajo el compilador.

Navegación

[0] Índice de Mensajes

[#] Página Siguiente

Ir a la versión completa