Programación del microcontrolador LPC810 en C++ desde cero 
El LPC810 es un microcontrolador con núcleo ARM Cortex-M0+ en encapsulado DIP8 y con reloj interno. Es bastante limitado (4Kb de memoria flash y 1Kb de memoria RAM) pero el encapsulado DIP8 y el reloj interno permiten montar proyectos sencillos en protoboard, lo que le da un valor educativo muy alto. Un micro ideal para introducirse en los 32 bits cuando se viene del mundo de los 8 bits.

Características generales

El LPC810 es un ARM Cortex-M0+ en encapsulado DIP8 que incluye un multiplicador rápido de 1 ciclo, 4 Kb de memoria flash, 1 Kb de memoria RAM, un reloj interno a 12 MHz overclockeable hasta 30 MHz, bootloader por puerto serie, I2C, SPI e interface de depuración compatible JTAG. El bootloader no ocupa espacio en los 4Kb de memoria flash sino que reside una zona de memoria aparte lo que facilita mucho la programación y la configuración del linker script y del compilador.

Núcleo ARM Cortex-M0+

Todos los ARM Cortex-M mapean, tras el reset, la tabla de vectores en la posición 0x00000000 con el siguiente contenido:

dirección     tamaño   contenido
0x00000000 4 puntero de pila
0x00000004 4 puntero a la primera instrucción a ejecutar
...
0x0000003C 4 puntero al manejador del SysTick (timer)
...

Hay más vectores en la tabla. Sólo he indicado los más relevantes para nuestro ejemplo y por ahora nos centraremos en los dos primeros vectores (puntero de pila y vector de reset), que son los más importantes.

Bootloader

Cuando el chip LPC810 se reinicia o se enciende se ejecuta el código del bootloader. Dicho código comprueba, entre otras cosas, si el pin ISP (pin 5) del chip está a masa, si lo está, entra en modo programación (es el modo usado para tostar el integrado), en caso contrario comprueba si el código almacenado en la memoria flash es "correcto". La forma de ver si el código cargado en la memoria flash es correcto es sumando las 8 primeras palabras de 32 bits (que coincide con los 8 primeros vectores de la tabla de vectores que se vieron antes), si el resultado es 0, se considera que el código el válido y se arranca, en caso contrario se entra en modo programación (como si el pin ISP hubiese estado a masa).



El programa que se ha utilizado para tostar el LPC810 es el lpc21isp (open source) y éste ya se encarga de calcular el valor que debe tener la posición de memoria correspondiente al vector de interrupción 7 para que la suma de los 8 primeros vectores valga 0. Ni en el linker script ni en el código fuente hay que preocuparse por este valor.

Fichero de startup y linker script

En anteriores posts en los que se abordaba la programación de otro microcontrolador de la familia ARM Cortex-M (el K20 de Freescale, de la placa Teensy), utilizaba una combinación de código ensamblador y de código C para realizar la secuencia de arranque. En este caso se ha optado por abordar el código de arranque (startup) sólo en lenguaje C/C++ para facilitar la claridad. Recordemos, antes de seguir, que el formato de fichero objeto (.o) y el formato ejecutable (.elf) de salida del gcc organizan su contenido por secciones. Las secciones básicas de cualquier fichero objeto o ejecutable ELF son las siguientes:

- “.text”, que es la sección que incluye el código.
- “.data”, que es la sección que incluye las variables globales inicializadas.
- “.bss”, que es la sección que incluye las variables globales sin inicializar (realmente sí se inicializan, pero a cero).

La secuencia de arranque que se sigue normalmente en cualquier sistema embebido para los programas hechos con gcc la podemos resumir como sigue:

1. Se copia la parte de la flash que incluye las variables globales en RAM (sección “.data” de los ficheros objeto).

2. Se inicializa a cero la parte de la RAM en la que van alojadas las variables sin inicializar (sección “.bss” de los ficheros objeto).

3. Se recorre la lista de punteros a funciones acabada en 0 alojada en la sección “.ctors”. Cada entrada es una función que hay que invocar.

4. Se recorrer la lista de punteros a funciones alojada en la sección “.init_array”. Cada entrada es una función que hay que invocar.

5. Se invoca a la función “main”.

6. Al terminar “main” (cosa que no suele ocurrir en un microcontrolador) se recorre la lista de punteros a funciones alojada en la sección “.fini_array”. Cada entrada es una función que hay que invocar.

7. Por último se recorre la lista de punteros a funciones (acabada en 0) y alojada en la sección “.dtors”.

8. En este instante se supone que se regresa al sistema operativo, pero como somos un sistema embebido nos “colgamos” (while (true) ; )

Se puede observar que las secciones “.ctors” y “.init_array” sirven para lo mismo, igual que las secciones “.dtors” y “.fini_array”. Hace algunos años gcc utilizaba las secciones “.ctors” y “.dtors” pero en las ultimas versiones esta usando las secciones “.init_array” y “.fini_array” (dejando las correspondientes “.ctors” y “.dtors” vacias) por compatibilidad y homogeneidad con la librería de C de GNU (glibc).

Siguiendo los pasos descritos, podemos escribir nuestro fichero de arranque “startup.cc”. Dentro de este código fuente, la función “void _startup()” es el punto de entrada:

void _startup() __attribute__((section(".startup"), naked));

void _startup() {
	_initDataRAM();
	_initBssRAM();
	_initClock();
	_callConstructors();
	_callInitArray();
	main();
	_callFiniArray();
	_callDestructors();
	while (true)
		;
}


Como se puede apreciar la función “void _startup()” tiene definidos los atributos.

- section(“.startup”)
- naked

Los atributos son una característica exclusiva de gcc (no forman parte del estándar del lenguaje C) y permiten controlar de forma fina la generación del código por parte del compilador gcc. En este caso se le dice al compilador que el código de la función “void _startup()” no lo coloque en la sección estándar “.text” (que es, por defecto, donde va el código), sino que lo coloque en una sección aparte y que llame a dicha sección “.startup”. El atributo “naked” le indica al compilador que no genere código preámbulo ni postámbulo para la función (preparación de la pila para variables locales, etc.), en otras palabras: le estamos diciendo al compilador que se limite a generer el código del cuerpo de la función, que el resto será responsabilidad nuestra.

¿Qué sentido tienen estos atributos? El poner el código de esta función en una sección aparte llamada “.startup” nos permite forzar en el linker script a que el código de esta función se sitúe al principio del vector de reset, mientras que el atributo naked nos permite reducir al mínimo ese código (no necesitamos código preámbulo ni postámbulo puesto que esa función no es llamada por nadie ni termina nunca).

SECTIONS {
	. = 0x00000000 ;
	.cortex_m0plus_vectors : {
		LONG(0x10000400);
		LONG(0x000000C1);
	}
	. = 0x0000003C ;
	.cotex_m0plus_vector_systick : {
		LONG(SYSTICK_ADDRESS + 1);
	}
	. = 0x000000C0 ;
	.text : {
		_linker_code = . ;
		startup.o (.startup)
		*(.text)
		*(.text.*)
		*(.rodata*)
		*(.gnu.linkonce.t*)
		*(.gnu.linkonce.r*)
	}
	SYSTICK_ADDRESS = . ;
	.systick : {
		*(.systick)
	}

	...

}

Como se puede ver para la tabla de vectores sólo hace falta definir los dos primeros valores (puntero de pila y vector de reset). El vector de reset está fijado a 0xC1 ya que el código de startup empieza en la posición de memoria 0xC0 (justo después de la tabla de vectores en los LPC810). En la tabla de vectores se ponen las direcciones con el bit 0 a 1 ya que se trata de un ARM Cortex-M y sólo soporta el juego de instrucciones Thumb (16 bits por instrucción).

La función “_startup” ademas de los pasos descritos (“.data”, “.bss”, “.init_array”, etc.) se encarga también de configurar el reloj del sistema en el LPC810 para que vaya a la máxima frecuencia permitida. En el arranque, el LPC810 utiliza su reloj interno RC, que va a 12 MHz. Estos 12 MHz pueden aumentarse configurando la PLL hasta llegar a los 30 MHz

Inicialización de variables globales

La función “void _startup()” invoca a varias funciones antes y después de invocar la función “main()”. La función “void _initDataRAM()” inicializa las variables globales inicializadas (que no están a cero) copiando una parte de la memoria flash hacia la RAM:

extern "C" {
	extern unsigned char flash_sdata;
	extern unsigned char ram_sdata;
	extern unsigned char ram_edata;
}

void _initDataRAM() {
	// init .data section (global variables) with flash data
	unsigned char *from = &flash_sdata;
	unsigned char *to = &ram_sdata;
	while (to != &ram_edata) {
		*to = *from;
		from++;
		to++;
	}
}


Mientras que la función “void _initBssRAM()” llena de ceros la zona de la memoria RAM indicada por la sección “.bss” (variables sin inicializar).

extern "C" {
	extern unsigned char ram_sbssdata;
	extern unsigned char ram_ebssdata;
}

void _initBssRAM() {
	// init .bss section with zeros
	unsigned char *p = &ram_sbssdata;
	while (p != &ram_ebssdata) {
		*p = 0;
		p++;
	}
}


A continuación las funciones “void _callConstructors()” y “void _callInitArray()” son las encargadas de llevar a cabo las inicializaciones “complejas”, invocando una o a una cada una de las funciones incluidas en la lista correspondiente (“.ctors” y “.init_array”). Estas llamadas se encargan de hacer estas inicializaciones (por ejemplo, cuando declaramos un objeto global, es preciso llamar a su contructor antes de que se ejecute la funcion “main”).

Prueba de concepto: parpadeo

Como prueba de concepto se ha desarrollado un pequeño programa muy sencillo que hace parpadear un led conectado a uno de los pines del integrado. Lo único que necesitamos es una toolchain con el target “arm-none-eabi” configurado (aquí los pasos para compilar una desde cero). Para controlar el parpadeo se ha usado la interrupción SysTick, esta interrupción está disponible en todos los ARM Cortex-M y consiste en un timer con un contador descendente de 24 bits que, cuando llega a 0, dispara dicha interrupción SysTick y vuelve a cargarse con el valor inicial.



1. En el linker script “lpc810.ld” incluimos una sección especial, que denominamos “.systick” y hacemos que la entrada 15 de la tabla de vectores (dirección de memoria 0x0000003C) apunte a la dirección de la memoria flash donde residirá la sección “.systick”.

SECTIONS {
	. = 0x00000000 ;
	.cortex_m0plus_vectors : {
		LONG(0x10000400);
		LONG(0x000000C1);
	}
	. = 0x0000003C ;
	.cotex_m0plus_vector_systick : {
		LONG(SYSTICK_ADDRESS + 1);
	}
	. = 0x000000C0 ;
	.text : {
		_linker_code = . ;
		startup.o (.startup)
		*(.text)
		*(.text.*)
		*(.rodata*)
		*(.gnu.linkonce.t*)
		*(.gnu.linkonce.r*)
	}
	SYSTICK_ADDRESS = . ;
	.systick : {
		*(.systick)
	}

	...

}

2. En el código fuente de nuestro programa definimos una función “systick” (aunque podemos ponerle el nombre que queramos) y mediante los atributos de GCC, le decimos al compilador que debe ir alojada en la sección “.systick”.

void systick()  __attribute__ ((section(".systick")));

Esto hace que nuestra función “systick” quede alojada en la seccion “.systick” de la memoria de programa. El vector de interrupción 15 apuntará, por tanto, a esta función “systick”.

3. En el cuerpo de la función “void systick()” simplemente escribimos un 1 en el bit 2 del registro “NOT0”. Escribir un 1 hace que el bit PIN0_2 cambie de estado.

void systick() {
	// change PIO0_2
	NOT0 = (1 << 2);
}

4. En la función “int main()” configuramos el PIN0_2 como pin de salida GPIO, configuramos el SysTick para que utilice reloj del sistema / 2 como fuente de reloj y en el registro de cuenta metemos el valor 15000000 (el reloj del sistema va a 30 MHz, por lo que si contamos hasta 15000000 usando un reloj de 30/2 = 15 MHz, tendremos una interrupción por segundo).

// PIN0_2 is output
PINENABLE0 = 0xFFFFFFBF;
DIR0 = (1 << 2);
// enable systick for interval = 1 second at 30 MHz
SYST_RVR = 15000000ULL;
SYST_CVR = 0;
SYST_CSR |= 0x03;   // clock source for systick is system clock / 2 = 15 MHz

5. A partir de este instante la funcion “systick” será invocada internamente una vez por segundo, provocando el parpadeo. Lo lógico ahora es “no hacer nada”, aunque hay varias formas de no hacer nada. Se puede siemplemente hacer un blucle infinito de toda la vida:

while (true)
        ;

Aunque en este caso lo más adecuado sería poner el procesador en algún modo de baja potencia para que esté medio dormido entre invocación e invocacion de la interrupción “systick”. Un término medio entre ambas aproximaciones es usar la instrucción “WFI” (Wait For Interrupt”) que hace que parte del procesador se pare (no avanza ni siquiera el contador de programa) hasta que se dispare alguna interrupción.

while (true) {
	// WFI (wait for interrupt) instruction, enters low power mode
	asm volatile ("wfi");
}



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

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

| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |