Instalación y configuración del compilador C++ de GNU para la placa de desarrollo Teensy3 
Teensy3 es una pequeña placa de desarrollo que se integra perfectamente en el entorno de desarrollo de Arduino y que incluye un potente procesador ARM Cortex-M4 a 96 MHz con 256Kb de Flash, 64Kb de RAM, DAC, ADC, CAN y otros periféricos. A lo largo de este artículo se explicará paso a paso cómo construir y configurar desde cero la toolchain de GNU para programar el Teensy3 en C++.



Compilar la toolchain de GNU

En mi caso he utilizado una placa Teensy 3.1. El corazón de esta placa es el microcontrolador de 32 bits MK20DX256VLH7, un ARM Cortex-M4 a 96MHz. Lo primero que hay que hacer es compilar la toolchain en sí para el target arm-none-eabi. Uno de los mejores tutoriales de compilación dde la toolchain de GNU para ARM es el publicado aquí. En mi caso he utilizado las binutils 2.24, el gcc 4.9.0 y la última versión de newlib disponible por CVS.

El tutorial anterior utiliza una versión de gcc antigua y no echa en falta ninguna librería, sin embargo, si queremos compilar el gcc 4.9.0 hemos de incluir las librerías GMP, MPFR y MPC. En este enlace se indica cómo incluir las librerías en el código fuente del gcc 4.9.0 para que se compilen junto con el gcc.

Una vez compilada la toolchain tendremos el compilador de C++ y el resto de herramientas (ensamblador, enlazador, etc.) disponibles en la carpeta de prefijo de instalación que hayamos utilizado. En mi caso he usado /opt/teensy.

El microcontrolador MK20DX256

Este microcontrolador está basado en un núcleo ARM Cortex-M4. La secuencia de arranque de estos procesadores se describe aquí. De forma resumida, en la direción de memoria 0x00000000 se encuentra el valor que se le asigna al puntero de pila en el arranque, mientras que en la dirección 0x00000004 se encuentra la dirección de memoria donde se ejecuta la primera instrucción, hay más vectores, aunque estos dos son los más importantes. Como se puede ver, no se trata de instrucciones, sino de una tabla de punteros.

Los 256Kb de Flash comienzan a partir de la dirección 0x00000000 mientras que los 64Kb de RAM van desde 0x1FFF8000 hasta 0x20008000. La pila del Cortex-M4 puede configurarse de cualquier forma (ascendente, descendente, etc.). Sin embargo las instrucciones “push” y “pop” por defecto trabajan con una pila descendente (mas información aquí). De esta forma nuestro mapa de memoria queda como sigue:

offset      tamaño        uso
0x00000000 4 bytes puntero de pila en el arranque
0x00000004 4 bytes puntero a la primera instrucción tras el reset
0x00000400 16 bytes palabras de configuración del microcontrolador
0x00000410 261104 bytes código
0x1FFF8000 32768 bytes variables globales
0x20000000 32768 bytes pila y variables locales


Al ser una pila descendente, el puntero de pila se inicializa con 0x20007FFC, que es la última palabra de 32 bits direccionable en la RAM. Con este mapa de memoria elaboramos el siguiente linker script:

SECTIONS {
	. = 0x00000000 ;
	.cortex_m4_vectors : {
		LONG(0x20007FFC);
		LONG(0x00000411);
	}
	. = 0x00000400 ;
	.flash_configuration : {
		LONG(0xFFFFFFFF);
		LONG(0xFFFFFFFF);
		LONG(0xFFFFFFFF);
		LONG(0xFFFFFFFE);
	}
	.text : {
		_linker_code = . ;
		init.o (.text)
		*(.text)
		*(.text.*)
		*(.rodata*)
		*(.gnu.linkonce.t*)
		*(.gnu.linkonce.r*)
	}
	.preinit_array : {
		__preinit_array_start = . ;
		*(.preinit_array)
		__preinit_array_end = . ;
	}
	.init_array : {
		__init_array_start = . ;
		*(.init_array)
		__init_array_end = . ;
	}
	.fini_array : {
		__fini_array_start = . ;
		*(.fini_array)
		__fini_array_end = . ;
	}
	.ctors : {
		__CTOR_LIST__ = . ;
		LONG((__CTOR_END__ - __CTOR_LIST__) / 4 - 2)
		*(.ctors)
		LONG(0)
		__CTOR_END__ = . ;
	}
	.dtors : {
		__DTOR_LIST__ = . ;
		LONG((__DTOR_END__ - __DTOR_LIST__) / 4 - 2)
		*(.dtors)
		LONG(0)
		__DTOR_END__ = . ;
	}
	flash_sdata = . ;
	. = 0x1FFF8000 ;
	ram_sdata = . ;
	.data : AT (flash_sdata) {
		_linker_data = . ;
		*(.data)
		*(.data.*)
		*(.gnu.linkonce.d*)
	}
	ram_edata = . ;
	data_size = ram_edata - ram_sdata;
	ram_sbssdata = . ;
	.bss : AT (LOADADDR(.data) + SIZEOF(.data)) {
		_linker_bss = . ;
		*(.bss)
		*(.bss.*)
		*(.gnu.linkonce.b.*)
		*(.COMMON)
	}
	ram_ebssdata = . ;
	bssdata_size = ram_ebssdata - ram_sbssdata;
	_linker_end = . ;
}


El linker script tiene las secciones habituales: .text, .init_array, .fini_array, .ctors, .dtors, .data, .bss, etc. más dos secciones adicionales: .cortex_m4_vectors y .flash_configuration.

Dirección de carga y dirección virtual en el linker script

Cuando se escribe un linker script para un microcontrolador o para un sistema basado en memoria Flash hay que tener en cuenta que las secciones .data y .bss (datos inicializados y sin inicializar, respectivamente) se cargan inicialmente en Flash y que, en el momento de la ejecución, dichas secciones deben copiarse a RAM. El linker script permite definir direcciones de carga y direcciones virtuales para cada sección de forma independiente con lo que es posible hacer de forma sencilla esta funcionalidad:

flash_sdata = . ;
. = 0x1FFF8000 ;
ram_sdata = . ;
.data : AT (flash_sdata) {
*(.data)
}
ram_edata = . ;
data_size = ram_edata - ram_sdata ;


Antes de definir la sección .data guardamos en la variable “flash_sdata” la dirección virtual actual, definimos la dirección virtual actual como 0x1FFF8000 (inicio de la parte de la RAM que hemos decidido que aloje las variables globales) y guardamos en “ram_sdata” esta dirección virtual. A continuación definimos la sección .data con el atributo “AT (flash_sdata)” para indicarle al linker que queremos que se cargue en la dirección “flash_sdata”, que es una dirección dentro de la memoria Flash, aunque la dirección virtual será 0x1FFF8000. Tras la definición de la sección .data generamos símbolos para calcular el tamaño de la sección. Con la sección .bss (datos globales sin inicializar) hacemos lo mismo.

Código de inicialización

El linker script nos ha permitido definir las secciones .data y .bss con direcciones virtuales y de carga diferentes, sin embargo la copia de los datos habrá que hacerla a mano antes de la ejecución de la función “main”.

Este fichero contiene el código de inicialización en ensamblador que vamos a utilizar.

	.syntax unified
	.thumb    
	.thumb_func
	.global _startup
	.section .text

_init:
    mov     r0,#0
    mov     r1,#0
    mov     r2,#0
    mov     r3,#0
    mov     r4,#0
    mov     r5,#0
    mov     r6,#0
    mov     r7,#0
    mov     r8,#0
    mov     r9,#0
    mov     r10,#0
    mov     r11,#0
    mov     r12,#0

/* disable interrupts */
    CPSID i

/* unlock_watchdog */
    ldr r6, = 0x4005200e /* WDOG_UNLOCK doc: K20P64M50SF0RM.pdf ( Page: 423 ) */
    ldr r0, = 0xc520
    strh r0, [r6]
    ldr r0, = 0xd928
    strh r0, [r6]

/* disable_watchdog */
    ldr r6, = 0x40052000 /* WDOG_STCTRLH doc: K20P64M50SF0RM.pdf ( Page: 418 ) */
    ldr r0, = 0x01d2
    strh r0, [r6]

/* enable interrupts */
    CPSIE i

	/* copy data section from flash to ram */
	ldr r0, = flash_sdata
	ldr r1, = ram_sdata
	ldr r2, = data_size
	cmp r2, #0
	beq skip_copy
copy:
	ldrb r4, [r0], #1
	strb r4, [r1], #1
	subs r2, r2, #1
	bne copy
skip_copy:

	/* fill bssdata section with zeros */
	ldr r1, = ram_sbssdata
	ldr r2, = bssdata_size
	cmp r2, #0
	beq skip_fill
	mov r4, #0
fill:
	strb r4, [r1], #1
	subs r2, r2, #1
	bne fill
skip_fill:

	b _startup

    .end


Como se puede ver: Inicializamos los registros del procesador, deshabilitamos las interrupciones, desactivamos el watchdog del MK20 (hay que hacerlo dentro de los primeros 256 ciclos del procesador tras el reset), copiamos los datos de la sección .data desde la Flash a la RAM y rellenamos con ceros la sección .bss. Cuando terminamos de rellenar con ceros la sección .bss saltamos a la función “_startup”.

La función "_startup" la definimos en un fichero de C++:

#include <stdint.h>

using namespace std;

extern "C" {
	extern void (*__CTOR_LIST__)();
	extern void (*__DTOR_LIST__)();
	extern void (*__init_array_start)();
	extern void (*__init_array_end)();
	extern void (*__fini_array_start)();
	extern void (*__fini_array_end)();
}

void callInitArray() {
	void (**f)() = &__init_array_start;
	while (f != &__init_array_end) {
		(*f)();
		f++;
	}
}

void callConstructors() {
	void (**constructor)() = &__CTOR_LIST__;
	uint32_t total = *(uint32_t *) constructor;
	constructor++;
	while (total) {
		(*constructor)();
		total--;
		constructor++;
	}
}

void callDestructors() {
	void (**destructor)() = &__DTOR_LIST__;
	uint32_t total = *(uint32_t *) destructor;
	destructor++;
	while (total) {
		(*destructor)();
		total--;
		destructor++;
	}
}

void callFiniArray() {
	void (**f)() = &__fini_array_start;
	while (f != &__fini_array_end) {
		(*f)();
		f++;
	}
}

extern int main();

void _startup() asm("_startup");

void _startup() {
	callConstructors();
	callInitArray();
	main();
	callFiniArray();
	callDestructors();
	while (true)
		;
}

extern "C" void __cxa_pure_virtual() {}
void *__dso_handle = 0;
extern "C" void __cxa_atexit() {}


La función “_startup” se encarga de invocar a los constructores globales y a la función “main”. El fichero Makefile que se utiliza es el siguiente:

BIN_PREFIX=/opt/teensy/bin/
CXX=${BIN_PREFIX}arm-none-eabi-g++
OBJCOPY=${BIN_PREFIX}arm-none-eabi-objcopy
CXX_FLAGS=-mtune=cortex-m4 -mthumb -fno-exceptions -fno-rtti -nostdlib -nodefaultlibs -nostartfiles

main.hex: main.elf
	$(OBJCOPY) -O ihex -R .eeprom main.elf main.hex

main.elf: main.o init.o startup.o
	$(CXX) $(CXX_FLAGS) -Wl,-Tteensy31.ld -o main.elf main.o init.o startup.o

main.o: main.cc
	$(CXX) $(CXX_FLAGS) -c -o $@ $<

startup.o: startup.cc
	$(CXX) $(CXX_FLAGS) -c -o $@ $<

init.o: init.s
	$(CXX) $(CXX_FLAGS) -c -o $@ $<

clean:
	rm -f *.elf *.o *.hex *.bin


Prueba de concepto

En este caso simplemente he implementado la típica funcionalidad de led parpadeante (para variar xD)

#include <stdint.h>

using namespace std;

#define  GPIO_ENABLE      (1 << 8)
#define  PULL_UP_ENABLE   (1 << 1)
#define  PULL_UP_SELECT   (1 << 0)
#define  DRIVE_STR        (1 << 6)
#define  PORT_CTRL_FLAGS  (DRIVE_STR | GPIO_ENABLE | PULL_UP_ENABLE | PULL_UP_SELECT)

#define  PORTC_PCR5    *((uint32_t *) 0x4004B014)
#define  GPIOC_PDDR    *((uint32_t *) 0x400FF094)
#define  GPIOC_PDOR    *((uint32_t *) 0x400FF080)
#define  SIM_SCGC5     *((uint32_t *) 0x40048038)
#define  WDOG_UNLOCK   *((uint32_t *) 0x4005200E)
#define  WDOG_STCTRLH  *((uint32_t *) 0x40052000)

#define  CLOCKS_ACTIVE_TO_ALL_GPIO  0x00043F82
#define  WATCHDOG_UNLOCK_VALUE_1    0xC520      
#define  WATCHDOG_UNLOCK_VALUE_2    0xD928      
#define  WATCHDOG_DISABLE_VALUE     0x01D2      

class Clase {
	public:
		uint32_t n;
		uint32_t v1, v2;
		Clase(uint32_t x1, uint32_t x2, uint32_t aux);
};

Clase::Clase(uint32_t x1, uint32_t x2, uint32_t aux) {
	this->v1 = x1;
	this->v2 = x2;
	this->n = aux;
}

Clase objeto(1, 2, 100000);
uint32_t a = 50000;
//uint32_t a = 100000;

int main() {
	// configure PTC5
	SIM_SCGC5 = CLOCKS_ACTIVE_TO_ALL_GPIO;
	PORTC_PCR5 = PORT_CTRL_FLAGS;
	// PORTC is output
	GPIOC_PDDR = (uint32_t) 0xFFFFFFFF;
	while (1) {
		volatile uint32_t i;
		// PORTC to 1
		GPIOC_PDOR = (uint32_t) 0xFFFFFFFF;
		for (i = 0; i < objeto.n; i++)
			;
		// PORTC to 0
		GPIOC_PDOR = (uint32_t) 0x00000000;
		for (i = 0; i < objeto.n; i++)
			;
	}
}


El código fuente puede descargarse de la sección soft.


ACTUALIZACIÓN: En la sección "Compilar la toolchain de GNU" se incluye un enlace a una web que ya no se encuentra disponible (http://kunen.org/uC/gnu_tool.html). En dicha web se indicaban los pasos pormenorizados para compilar toda la toolchain de GNU. Afortunadamente he podido rescatar gracias al histórico de mi ordenador los pasos que seguí yo guiándome por esa web y los he recompilado es este post.

ACTUALIZACIÓN: La versión anterior no rellenaba con ceros la sección .bss de la RAM de datos globales. Se ha incluido la corrección en la descripción del texto, en el código mostrado en el post y en el fichero tar.gz de la sección soft.

[ añadir comentario ] ( 1886 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 1827 )

<< <Anterior | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |