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 ] ( 1904 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 1864 )
Vúmetro LCD 
La posibilidad de redefinir una parte del juego de caracteres en los displays LCD alfanuméricos, en combinación con el uso de una de las entradas analógicas del AVR y un pequeño circuito analógico, nos va a permitir la implementación de un sencillo vúmetro en el Arduino.

Un vúmetro no es más que un medidor gráfico de intensidad de señal de audio. En este caso vamos a abordar una implementación muy sencilla y lineal (no logarítmica).



Entrada analógica

La entrada analógica del microcontrolador AVR en el Arduino permite medir entre 0 y 5 voltios con una precisión de 10 bits, de 0 a 1023. 0 se corresponde con 0 voltios y 1023 se corresponde con 5 voltios. Para obtener la intensidad de señal en una de las entradas analógicas, realizamos los siguientes pasos:

1. Amplificación
2. Detección de envolvente

La amplificación se ha implementado construyendo una sencilla etapa estándar de amplificación basada en un transistor NPN con configuración de emisor común:



La señal de entrada es una señal de audio en alterna (en este caso la he conectado a la salida de auriculares de mi móvil). Dicha señal, al pasar por el circuito de amplificación se invierte en fase (no nos importa) y, lo más importante, se amplifica. La señal de salida es también en alterna (el condensador de salida elimina la componente de continua) y pasa la siguiente parte del circuito: el detector de envolvente.



Un detector de envolvente es un circuito que, a partir de una señal de entrada, determina su envolvente:



(imagen extraida de Wikimedia Commons)

El circuito detector de envolvente es muy sencillo: La señal alterna, al hacerla pasar por un diodo, se rectifica y sólo deja pasar los semiciclos positivos. En cada uno de los semiciclos positivos de la señal se carga el condensador y, durante las pausas entre semiciclos, en ausencia de corriente que pase por el diodo, el condensador se descarga lentamente a través de la resistencia, así hasta el siguiente ciclo, que se repite el proceso. El resultado en la salida es una señal que “sigue” de forma aproximada a los picos de la señal de audio que hay en la entrada.

Hay que elegir correctamente los valores del condensador y la resistencia: Un valor de condensador muy bajo hará que se descargue rápidamente mientras que un valor de condensador muy alto hará que tarde excesivamente en cargarse. En el caso de la resistencia de descarga del condensador un valor muy alto hará que el condensador apenas se descargue en las pausas entre ciclos (lo que puede provocar que los ciclos se “sumen” a medida que llegan) y un valor muy bajo hará que el condensador se descargue muy rápido, perdiendo el efecto de seguimiento de envolvente.

Display LCD

En post anteriores de este blog publiqué varias clases C++ para la gestión de displays LCD. En este caso he reutilizado la clase “Lcd20x4” de proyectos anteriores, redefiniendo de forma estática 5 de los caracteres definibles por el usuario.

Para implementar una barra horizontal hay que tener en cuenta que tenemos 20 columnas y que cada carácter tiene 5 columnas de puntos: En total tenemos 100 pixels en horizontal para un display LCD de 20x4. Si definimos los caracteres de la siguiente forma:
* . . . .
* . . . .
* . . . .
* . . . . --> 1
* . . . .
* . . . .
* . . . .

* * . . .
* * . . .
* * . . .
* * . . . --> 2
* * . . .
* * . . .
* * . . .

...

* * * * *
* * * * *
* * * * *
* * * * * --> 5
* * * * *
* * * * *
* * * * *

Utilizando este método de visualización, la barra LCD podrá adoptar valores entre 0 y 100. El pseudocódigo para visualizar un valor “v” será:

procedimiento mostrar_valor(v)     // 0 <= v <= 100
numCaracteresTotalmenteLlenos := parte entera de (v / 5)
valorCaracterParcial := (v mod 5)
ir_coordenada(0, 0)
para x := 1 hasta numCaracteresTotalmenteLlenos hacer
escribir_carácter(5)
fin para
escribir_carácter(valorCaracterParcial)
para x := (numCaracteresTotalmenteLlenos + 2) hasta 20 hacer
escribir_carácter(0)
fin para
fin procedimiento

Como se puede ver, para cada valor “v” que se quiera visualizar en la barra, se escribe toda una fila horizontal del display LCD. La visualización puede optimizarse si partimos de la base de que la diferencia entre un valor “v” enviado en un instante “t” y el valor “v” enviado al display en un instante “t + d” no va a variar mucho si “d” es lo suficientemente pequeño. En nuestro caso vamos a hacer un muestreo de la entrada analógica cada 50ms por lo que la “v” no va a variar excesivamente entre un instante de muestreo y el siguiente.

Si asumimos que la “v” no va a variar mucho podemos enviar al LCD sólo los cambios:

barraLogica[20]
barraFisica[20]

procedimiento mostrar_valor(v)
numCaracteresTotalmenteLlenos := parte entera de (v / 5)
valorCaracterParcial := (v mod 5)
j := 0
para x := 1 hasta numCaracteresTotalmenteLlenos hacer
barraLogica[j] := 5
j := j + 1
fin para
barraLogica[j] := valorCaracterParcial
j := j + 1
para x := (numCaracteresTotalmenteLlenos + 2) hasta 20 hacer
barraLogica[j] := 0
j := j + 1
fin para
fin procedimiento

procedimiento actualiza
dentroCambio := NO
inicioCambio := -1
para j := 0 hasta 19 hacer
si (dentroCambio) entonces
si (barraFisica[j] = barraLogica[j]) entonces
escribir_barra_fisica(inicioCambio, j)
dentroCambio := NO
fin si
en otro caso
si (barraFisica[j] <> barraLogica[j]) entonces
inicioCambio := j
dentroCambio := SI
fin si
fin si
barraFisica[j] := barraLogica[j]
fin para
si (dentroCambio) entonces
escribir_barra_fisica(inicioCambio, 19)
fin si
fin procedimiento

El procedimiento “actualiza” se ejecutará de forma periódica y, como se puede ver, sólo envía al display LCD los caracteres que cambien. Esta forma de refresco del display LCD es más eficiente y nos permite tasas de muestreo mayores.

En el siguiente vídeo puede verse el circuito en acción:



El código en C++ puede descargarse de la sección soft.

[ añadir comentario ] ( 1843 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 14939 )
Control de display LCD con soporte para caracteres en español 
El control de displays LCD alfanuméricos desde microcontroladores es un tópico ampliamente abordado en muchas webs y tutoriales. El juego de caracteres utilizado por este tipo de displays es de tipo ASCII con algunos símbolos adicionales, sobre todo asiáticos, y se echan en falta varios de los símbolos propios del español (tildes, diéresis, etc.). A lo largo de este post se plantea un sencillo algoritmo de gestión del display LCD que permite la utilización de texto en español de forma transparente al usuario.

Características del display

Los displays LCD alfanuméricos estándar almacenan los mapas de bits (los dibujos) de su juego de caracteres en una ROM interna, con la excepción de los 8 primeros caracteres (del 0 al 7). Los mapas de bits de estos 8 primeros caracteres se almacenan en una parte de la RAM del LCD denominada CGRAM (Character Generator RAM). Es esta parte de la RAM la que hay que utilizar para representar los caracteres extendidos no ASCII del español en nuestro display.

El mapa de bits de cada carácter está formado por una matriz de 5x7 puntos que se almacena en la CGRAM como 8 bytes consecutivos: 1 byte por cada línea horizontal (de la cual sólo son significativos los 5 bits más bajos) y un 1 byte adicional para la línea de cursor que siempre se pone a 0 para displays de 5x7. Como son configurables sólo los 8 primeros caracteres del juego de caracteres, esto nos da 8 * 8 = 64 bytes para la CGRAM.
        . . . x .       0 0 0 1 0
. . x . . 0 0 1 0 0
. x x x . 0 1 1 1 0
á --> . . . . x --> 0 0 0 0 1
. x x x x 0 1 1 1 1
x . . . x 1 0 0 0 1
. x x x x 0 1 1 1 1
. . . . . 0 0 0 0 0

Gestión de los caracteres

En español tenemos 16 caracteres que se salen de la simbología ASCII:
á é í ó ú ü ñ Á É Í Ó Ú Ü Ñ ¿ ¡

Como no es posible cargar en la CGRAM los mapas de bits de estos 16 caracteres de forma simultánea, es necesario implementar algún tipo de gestión a nivel software que cargue en CGRAM sólo los caracteres que necesitamos en cada momento.

Para la gestión de los caracteres se han utilizado las siguientes estructuras de datos (en pseudocódigo):
EntradaTabla {
caracterLatin1
mapaDeBits
caracterLcd
caracterLcdDefecto
vecesUsado
marcaCarga
}

EntradaTabla tabla[16]
Cola caracteresLcdDisponibles

Cada entrada de la tabla de caracteres especiales incluye el carácter “ISO-8859-1” o “latin1” correspondiente y el mapa de bits que lo dibuja en el display. caracterLcd es el carácter del LCD (1 al 7, no vamos a usar la entrada 0 por si acaso) que está mapeando a este carácter especial. caracterLcdDefecto es el carácter de la ROM que mapeará a este carácter latin1 en caso de que no esté disponible ningún hueco en la CGRAM (por ejemplo ‘á’ tiene como carácter por defecto ‘a’).

vecesUsado indica cuántas veces está siendo usado esa entrada de la tabla de caracteres extendidos: cada vez que se utiliza un carácter especial, se incrementa este contador de la entrada correspondiente y cada vez que se deja de utilizar en alguna parte de la pantalla (se borra o se sustituye por otro), se decrementa este contador de la entrada correspondiente. Cuando un contador llega a 0, el hueco que ocupaba esa entrada en la CGRAM es marcado como vacío (metido en la cola de caracteres LCD disponibles.

marcaCarga indica cuando una entrada de la tabla debe ser cargada en la CGRAM. La carga de los bitmaps de las entradas marcadas se realiza a posteiori para no influir en la escritura de los caracteres. Primero se escribe en la DDRAM (la memoria de pantalla) y al final, si es necesario enviar bitmaps de caracteres no ASCII, se escribe en la CGRAM.
inicializar(EntradaTabla e)
e.caracterLcd = 0
e.vecesUsado = 0
e.marcaCarga = NO
fin

inicializarCola
meter(caracteresLcdDisponibles, 1)
meter(caracteresLcdDisponibles, 2)
meter(caracteresLcdDisponibles, 3)
meter(caracteresLcdDisponibles, 4)
meter(caracteresLcdDisponibles, 5)
meter(caracteresLcdDisponibles, 6)
meter(caracteresLcdDisponibles, 7)
fin

buscarEntrada(c) {
Devuelve la entrada en “tabla” que cumpla “caracterLatin1 == c”, o NULL en caso de no haber ninguna entrada.
fin

reemplazarCaracter(nuevo, viejo)
EntradaTabla e = buscarEntrada(viejo)
si (e != NULL) {
e.vecesUsado = e.vecesUsado - 1
si (e.vecesUsado == 0) {
meter(caracteresLcdDisponibles, e.caracterLcd)
e.caracterLcd = 0
fin si
fin si
e = buscarEntrada(nuevo);
si (e == NULL)
devolver nuevo
en otro caso
si (e.caracterLcd > 0)
e.vecesUsado = e.vecesUsado + 1
en otro caso
si (caracteresLcdDisponibles está vacía)
devolver e.caracterLcdDefecto
e.caracterLcd = sacar(caracteresLcdDisponibles)
e.vecesUsado = 1
e.marcaCarga = SI
fin si
devolver e.caracterLcd
fin si
fin

procesar
para todos los segmentos de texto que haya que cambiar en el display
para todos los caracteres del segmento de texto
nuevo = nuevo carácter
viejo = actual carácter
v = reemplazarCaracter(nuevo, viejo)
emitir(v)
fin para
fin para
para todas las entradas e de la tabla con marcaCarga = SI hacer
cargar e.mapaDeBits en la CGRAM correspondiente al carácter e.caracterLcd
e.marcaCarga = NO
fin para
fin

Ejemplo de traza

Imaginemos que tenemos la pantalla en blanco (recién inicializada): Todas las entradas de la tabla las tenemos inicializadas (caracterLcd=0, vecesUsado=0, marcaCarga=NO) y la cola “caracteresLcdDisponibles” inicializada con los 7 huecos.

Para escribir la palabra “Máquina”, el proceso irá llamando a “reemplazarCaracter” por cada nueva letra:
    reemplazarCaracter(‘M’, ‘’)
No hay entrada en la tabla para el carácter ‘’
No hay entrada en la tabla para el carácter ‘M’
devuelve ‘M’
reemplazarCaracter(‘á’, ‘’)
No hay entrada en la tabla para el carácter ‘’
Hay una entrada e para el carácter ‘á’
como e.caracterLcd = 0, entonces
la cola de caracteres disponibles no está vacía
e.caracterLcd = 1
e.vecesUsado = 1
e.marcarCarga = SI
devuelve e.caracterLcd (1)
reemplazarCaracter(‘q’, ‘’)
No hay entrada en la tabla para el carácter ‘’
No hay entrada en la tabla para el carácter ‘q’
devuelve ‘q’
reemplazarCaracter(‘u’, ‘’)
No hay entrada en la tabla para el carácter ‘’
No hay entrada en la tabla para el carácter ‘u’
devuelve ‘u’
...

Al final del proceso de escritura de la cadena ‘Máquina’ tenemos que la cola de caracteres disponibles está así: [2, 3, 4, 5, 6, 7] y que la entrada de la tabla de caracteres correspondiente a la letra ‘á’ tiene caracterLcd=1, vecesUsado=1, el resto de entradas siguen como al principio.



De esta forma se va alojando espacio en la CGRAM del display LCD en función de los caracteres especiales que necesitamos en todo momento. Nótese que, en caso de que ya no nos queden huecos libres (la cola de huecos esté vacía), devolvemos el carácter por defecto.

Políticas alternativas de sustitución de caracteres

Una política alternativa podría ser establecer una prioridad por cada carácter de la tabla: en caso de vaciado de la cola de huecos, se sacrifica el carácter con menor prioridad de los que estén siendo usados en ese momento. Un criterio de prioridad podría ser en función del valor de “vecesUsado”. De esta forma se sacrificarían los caracteres menos usados.

Nótese que esta política de “sacrificado” de caracteres menos prioritarios obligaría a refrescar los caracteres a sacrificar y cambiarlos por los caracteres por defecto correspondientes. En todas las posiciones donde se encuentren.

En esta implementación no se lleva a cabo ninguna política de sacrificado de caracteres. Cuando la cola de caracteres disponibles se acaba, se imprime el carácter por defecto.

Implementación

Este algoritmo de gestión de caracteres extendidos para displays LCD se ha implementado sobre un Arduino Leonardo en C++.



Se ha optado por una anchura de bus de 4 bits para minimizar el número de cables. El código fuente puede descargarse de la sección soft.

[ añadir comentario ] ( 1904 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 1960 )
Aproximación de Padé para el cálculo eficiente de la función exponencial 
Implementar la función exponencial en un sistema embebido con poca RAM, poca memoria de programa y sin coprocesador matemático pasa, normalmente, por intentar evitar el uso de la librería matemática de C. La sobrecarga que produciría el utilizar la función “exp” de dicha librería unida a la sobrecarga propia de la manipulación de datos en coma flotante por software desaconsejan totalmente el uso de dicha librería en sistema embebidos pequeños. Analizaremos diferentes aproximaciones polinomiales a la función exponencial y el uso de aritmética de punto fijo para realizar dicho cálculo.

Analizaremos dos aproximaciones polinómicas: La serie de Taylor y la aproximación de Padé (esta última se trata realmente de una aproximación racional).

Serie de Taylor

Lo primero que se le suele venir a uno a la cabeza cuando piensa en aproximaciones polinómicas suele ser la serie de Taylor, dicha serie es muy sencilla de calcular y genera una buena aproximación en el entorno de un punto. En este caso se ha optado por aproximar la función exponencial en el entorno de x=0:
$$e^{x} \simeq 1 + {x \over 1!} + {x^2 \over 2!} + {x^3 \over 3!} + ...$$
Esto nos da una muy buena aproximación, aunque para conseguir un error aceptable, es necesario calcular la serie de Taylor para un orden relativamente alto. El error con respecto a la función “exp” de la librería matemática de C comienza a ser asumible a partir del orden 6 trabajando en coma flotante.

Aproximación de Padé

La aproximación de Padé de orden (m, n) es la función racional:
$$R(x) = {p_0 + p_{1}x + p_{2}x^2 + ... + p_{m}x^m \over 1 + q_{1}x+ q_{2}x^2 + ... + q_{n}x^n}$$
Que cumple que:
$$f(0) = R(0)$$
$$f'(0) = R'(0)$$
$$f''(0) = R''(0)$$
$$...$$
$$f^{(m+n)}(0) = R^{(m+n)}(0)$$
El cálculo de los coeficientes de Padé no es trivial y existen varias técnicas para obtenerlos, como el algoritmo Epsilon de Wynn o el algoritmo euclídeo extendido para el cálculo del máximo común divisor. Por suerte, para la función exponencial, podemos consultar las tablas de Padé que ya se encuentran en internet calculadas para diferentes órdenes (valores de m y de n):

Aquí una tabla publicada en wikipedia.

Se ha optado en este caso por utilizar la aproximación de Padé de orden [3 / 3] (m = n = 3).

A continuación puede verse una implementación en C de ambas aproximaciones.

double taylor_exp(double x) {
	double ret = 0;
	int i;
	double num = 1;
	double den = 1;
	for (i = 0; i <= 6; i++) {
		ret += num / den;
		num *= x;
		den *= (i + 1);
	}
	return ret;
}

double pade_exp(double x) {
	double x2 = x * x;
	double x3 = x2 * x;
	double num = 1 + (x / 2) + (x2 / 10) + (x3 / 120);
	double den = 1 - (x / 2) + (x2 / 10) - (x3 / 120);
	return num / den;
}

Como puede apreciarse, la serie de Taylor es de orden 6, mientras que la aproximación de Padé que se ha implementado es la de orden [3 / 3]. A continuación se reproduce la salida de una prueba de ambas funciones comparándolas con la función “exp” de la librería matemática:
# ./taylor_vs_pade_float
exp(0.250000):
exp() function : 1.2840254167
6th order taylor : 1.2840254042
3rd order pade : 1.2840254175

Como puede verse, la aproximación de Padé consigue un error comparable al de la serie de Taylor con muchas menos operaciones.

Utilizar aritmética de punto fijo

Ahora que están ambos algoritmos implementados en coma flotante, pasaremos el cálculo a aritmética de punto fijo en formato Q16.16 (más info sobre la notación Q). En el formato Q16.16 tenemos 16 bits para la parte entera y 16 bits para la parte fraccionaria, en total 32 bits.

typedef int32_t fixedpoint_t;

#define  FP_NEG(x)           (-(x))
#define  FP_ADD(x, y)        ((x) + (y))
#define  FP_SUB(x, y)        ((x) - (y))
#define  FP_MUL(x, y)        ((int32_t) (((int64_t) (x)) * ((int64_t) (y)) >> 16))
#define  FP_DIV(x, y)        ((int32_t) ((((int64_t) (x)) << 16) / ((int64_t) (y))))
#define  TO_FP(x)            ((int32_t) ((x) << 16))
#define  FROM_FP(x)          ((x) >> 16)
#define  FP_FRACTIONAL_BITS  16

Las funciones anteriores puede ser ahora reescritas para utilizar el formato Q16.16:

fixedpoint_t taylor_exp(fixedpoint_t x) {
	fixedpoint_t ret = 0;
	int i;
	fixedpoint_t num = TO_FP(1);
	fixedpoint_t den = TO_FP(1);
	for (i = 0; i <= 6; i++) {
		ret = FP_ADD(ret, FP_DIV(num, den));
		num = FP_MUL(num, x);
		den = FP_MUL(den, FP_ADD(TO_FP(i), TO_FP(1)));
	}
	return ret;
}

fixedpoint_t pade_exp(fixedpoint_t x) {
	fixedpoint_t x2 = FP_MUL(x, x);
	fixedpoint_t x3 = FP_MUL(x2, x);
	fixedpoint_t num = FP_ADD(TO_FP(1), FP_ADD(FP_DIV(x, TO_FP(2)), FP_ADD(FP_DIV(x2, TO_FP(10)), FP_DIV(x3, TO_FP(120)))));
	fixedpoint_t den = FP_ADD(FP_SUB(FP_DIV(x2, TO_FP(10)), FP_DIV(x3, TO_FP(120))), FP_SUB(TO_FP(1), FP_DIV(x, TO_FP(2))));
	return FP_DIV(num, den);
}

En este caso, realizando la misma prueba obtenemos resultados algo peores (debido a la pérdida de precisión inherente al uso del punto fijo) y, aunque para las dos aproximaciones obtenemos el mismo valor, la aproximación de Padé requiere menor cantidad de operaciones que la serie de Taylor.
# ./taylor_vs_pade_fixed
exp(0.250000):
exp() function : 1.2840254167
6th order taylor : 1.2839965820
3rd order pade : 1.2839965820

La aproximación de Padé, como ha podido verse, da mejores resultados que las series de Taylor como aproximación a la función exponencial, tanto desde el punto de vista de la eficiencia como de la precisión.

[ añadir comentario ] ( 3200 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 1922 )
Multitarea en sistemas embebidos pequeños 
La multitarea es la capacidad que tienen los sistemas de realizar varias tareas o procesos de forma simultánea en el tiempo. En el ámbito de los sistemas grandes o de sistemas embebidos avanzados esta capacidad viene dada normalmente por un pequeño sistema operativo encargado de gestionar dicha multitarea y de crear una capa de abstracción entre los procesos y el hardware (Linux embebido, eCos, Windows CE, etc.). En sistemas embebidos pequeños con procesadores más modestos (ya sea por anchura de bus, por RAM o por ambas razones) la cosa cambia: No tenemos sistema operativo y la multitarea debe ser gestionada por el propio software.

Vamos a asumir que todas las tareas que queremos planificar son igual de prioritarias y, por lo tanto, utilizaremos una planificación estilo round-robin (la más simple).

Multitarea apropiativa.

En la multitarea apropiativa, el proceso principal (voy a resistirme a utilizar el concepto de “sistema operativo”) se encarga de distribuir el tiempo de CPU entre las diferentes tareas. Si una tarea se cuelga o comienza a consumir muchos recursos, el resto de tareas no tiene por qué verse afectado ya que el software principal le quita el control de la CPU a las tareas sin importarle lo que estén haciendo en ese momento.

Es el tipo de multitarea ideal para evitar cuelgues completos del sistema y al mismo tiempo es el que más restricciones técnicas tiene.

- El sistema debe permitir “proteger” ciertas funciones o instrucciones para que no puedan ser ejecutadas por las tareas.
- Normalmente es necesario realizar manipulaciones de la pila del sistema.
- Se suelen utilizar timers para controlar el tiempo de CPU que tiene cada tarea.

Un esqueleto muy básico de lo que sería implementar multitarea apropiativa en un sistema embebido pequeño sería el siguiente:

class Tarea {
    public:
        bool enEjecucion;
        Estado estado;
        Tarea *siguiente;
        Tarea() { this->enEjecucion = false; };
        virtual void run() = 0;
};

Tarea *tareaInicial;
Tarea *tareaActual = NULL;

void leerEstadoActual(Tarea *t) {
    // se lee de la pila del sistema el estado de los registros en el momento de la interrupción y se guarda en t->estado
}

void escribirEstadoActual(Tarea *t) {
    if (!t->enEjecucion) {
        // hay que inicializar t->estado para que cada tarea tenga su propia pila y para que el contador de programa apunte a la primera instrucción de t->run
        t->enEjecucion = true;
    }
    // se sobreescribe en la pila del sistema el estado de los registros guardado en t->estado
}

void isr() {
    if (tareaActual != NULL) {
        leerEstadoActual(tareaActual);
        tareaActual = tareaActual->siguiente;
    }
    else
        tareaActual = tareaInicial;
    // este "if" sobra si las tareas se meten en una lista circular
    if (tareaActual == NULL)
        tareaActual = tareaInicial;
    escribirEstadoActual(tareaActual);
}

int main() {
    configurarISRTimer(isr);
    configurarYLanzarTimer();
    while (true)
        ;
    return 0;
}


En los comentarios de leerEstadoActual y escribirEstadoActual puede apreciarse el nivel de complejidad que requiere este tipo de multitarea: Es necesario conocer bien cómo funciona el mecanismo de interrupciones en el procesador en el que estamos así como controlar bien el layout de la memoria para cada tarea (pila, código, datos, etc.). En un sistema embebido normalmente no tenemos instrucciones protegidas con lo que nada impedirá que una de las tareas se “apropie” del ISR del timer que estamos utilizando para planificar y se nos fastidie el invento. Algo parecido ocurriría si alguna de las tareas decide deshabilitar la interrupción del timer: Se hará con el control completo de la CPU.

Multitarea cooperativa.

La multitarea cooperativa consiste en que cada tarea cede voluntariamente el procesador a otras tareas. Es el tipo de multitarea más sencillo de implementar, también puede ser más problemático ya que, si una tarea ocupa demasiado tiempo de procesador (ya sea por un error, por una condición mal evaluada o por una mala praxis de programación) el resto de tareas se ejecutarán, en el mejor de los casos, de forma menos eficiente y en el peor de los casos dejarán de ejecutarse. Sin embargo en los sistema embebidos pequeños normalmente podemos controlar bien todas las tareas que están en ejecución, y, si realizamos un buen diseño, podemos conseguir muy buenos resultados.

Una multitarea cooperativa con planificación simple estilo round-robin quedaría como sigue:

class Tarea {
    public:
        virtual void init() = 0;
        virtual void run() = 0;
};

class Tarea1 : public Tarea {
    ...
};

class Tarea2 : public Tarea {
    ...
};

Tarea1 t1;
Tarea2 t2;

int main() {
    t1.init();
    t2.init();
    while (true) {
        t1.run();
        t2.run();
    }
}


O, de forma más genérica, utilizando una lista simplemente enlazada de tareas:

class Tarea {
    public:
        Tarea *siguiente;
        virtual void init() = 0;
        virtual void run() = 0;
};

...

int main() {
    Tarea *t = TAREA_INICIAL;
    while (t != NULL) {
        t->init();
        t = t->siguiente;
    }
    while (true) {
        t = TAREA_INICIAL;
        while (t != NULL) {
            t->run();
            t = t->siguiente;
        }
    }
}


Como se puede ver no accedemos ni a la pila del sistema ni a características que requieran un conocimiento profundo de la arquitectura o del procesador que estamos utilizando. Esto hace que el código resultante sea más fácilmente portable a otros sistemas. Como contrapartida tenemos que asegurarnos de que ningún método “run” estrangule mucho al procesador.

Donde más podemos estrangular al resto de tareas es:
- En tiempos de espera para entrada/salida.
- En algoritmos pesados.

Desbloquear tareas de entrada y salida.

Las tareas que realizan entrada/salida suelen requerir de tiempos de espera para completarse. Pensemos en una comunicación serie a 9600 bps. Si tenemos que crear una clase UART con el método write una primera aproximación sería la siguiente:

void UART::write(uint8_t *data, uint16_t size) {
    for (uint16_t j = 0; j < size; j++) {
        while (!BUFFER_UART_TX_VACIO)
            ;
        BUFFER_UART = data[j];
    }
    while (!BUFFER_UART_TX_VACIO)
        ;
}


Llamar al método UART::write() desde el método run de alguna de las tareas estrangulará, claramente al resto de tareas ya que la tarea desde la que se llame no regresará del run hasta que, como mínimo, se envíe el buffer completo de bytes. Hay que tener en cuenta lo siguiente: Si queremos transmitir 64 bytes y nuestra UART manda un bit de start y otro de stop, realmente está enviando 10 bits por cada byte, lo que hacen un total 640 bits. A 9600 bps, 640 bits tardan 0.067 segundos. Es poco en términos humanos, pero si pensamos que un procesador RISC a 16 MHz (un PIC o un AVR de gama media) ejecuta 16 millones de instrucciones por segundo (una instrucción cada 0.0000000625 segundos), esperar 0.067 segundos supone un estrangulamiento del resto de tareas.

¿Qué hacemos en estos casos? Muy sencillo, utilizar un modelo de estados. Podemos crear una tarea adicional encargada de gestionar la transmisión UART y definirla así:

class UART : public Tarea {
    protected:
        uint8_t estado;
        uint8_t *bufferEnvio;
        uint16_t indiceBufferEnvio;
        uint16_t tamBufferEnvio;
    public:
        UART();
        void init();
        void run();
        bool escribir(uint8_t *buffer, uint16_t tam);
};

#define  ESTADO_UART_OCIOSO                   0
#define  ESTADO_UART_ESPERAR_BUFFER_TX_VACIO  1
#define  ESTADO_UART_ESCRIBIR_BYTE            2

UART::UART() {
    this->estado = ESTADO_UART_OCIOSO;
    this->bufferEnvio = NULL;
}

void UART::init() {
    this->bufferEnvio = NULL;
    this->estado = ESTADO_UART_OCIOSO;
}

void UART::run() {
    if (this->estado == ESTADO_UART_OCIOSO) {
        if (this->bufferEnvio != NULL)
            this->estado = ESTADO_UART_ESPERAR_BUFFER_TX_VACIO;
    }
    else if (this->estado == ESTADO_UART_ESPERAR_BUFFER_TX_VACIO) {
        if (BUFFER_UART_TX_VACIO)
            this->estado = ESTADO_UART_ESCRIBIR_BYTE;
    }
    else if (this->estado == ESTADO_UART_ESCRIBIR_BYTE) {
        if (this->indiceBufferEnvio == this->tamBufferEnvio) {
            this->estado = ESTADO_UART_OCIOSO;
            this->bufferEnvio = NULL;
        }
        else {
            BUFFER_UART = this->bufferEnvio[this->indiceBufferEnvio];
            this->indiceBufferEnvio++;
            this->estado = ESTADO_UART_ESPERAR_BUFFER_TX_VACIO;
        }
    }
}

bool UART::escribir(uint8_t *buffer, uint16_t tam) {
    if (this->estado == ESTADO_UART_OCIOSO) {
        this->tamBufferEnvio = tam;
        this->indiceBufferEnvio = 0;
        this->bufferEnvio = buffer;
        return true;
    }
    else
        return false;
}


Ahora incluimos nuestra tarea UART (que hemos heredado de la clase Tarea) como una tarea más a ser planificada: Invocamos su método “init” antes de comenzar y luego, en el bucle infinito invocamos su método “run” junto con el resto de métodos “run” del resto de tareas:

UART uart;

int main() {
    t1.init();
    t2.init();
    uart.init();
    while (true) {
        t1.run();
        t2.run();
        uart.run();
    }
}


A priori puede resultar un poco lioso desbloquear las tareas bloqueantes de esta forma, mediante máquinas de estados que generan más código, pero a la larga mejora el rendimiento general de la aplicación y es más sencillo adaptar el código para que funcione mediante interrupciones: con muy poco esfuerzo se puede adaptar el método “run” para convertirlo en una rutina de interrupción (ISR) que atienda la tranmisión UART.

Espero que este post haya servido de ayuda. Los últimos códigos fuente en C++ que he publicado para Arduino (el último de las luces del belén y el del tres en raya) utilizan multitarea cooperativa.

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

<< <Anterior | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |