Salida de audio de alta calidad con la placa Teensy 
La placa Teensy 3.1 (ARM Cortex M4) dispone de un puerto I2S para la transferencia de audio digital. Si combinamos esta salida con un buen DAC de alta fidelidad el resultado es espectacular :-)

El DAC de Texas Instruments PCM5102 es un DAC que soporta el estándar I2S de transferencia de audio digital y el estándar “left justified” (variante del I2S). Existen muchos otros DACs de audio en el mercado con soporte para estos formatos, sin embargo los más usados son el ES9023 y derivados, de ESS, y el PCM5102 y derivados, de Texas Instruments. En mi caso, adquirí una placa con un integrado PCM5102A y la circuitería mínima (componentes pasivos, espadines para conectar alimentación y las tres líneas del protocolo I2S y dos conectores RCA hembra de salida, uno para cada canal).



Unos 14€ por AliExpress (gastos de envío incluidos), aunque ahora creo que está incluso más barato.

I2S

El protocolo I2S es un protocolo muy sencillo de transferencia de audio digital. Aunque por su nombre puede parecer que es un protocolo derivado o parecido al protocolo I2C, lo cierto es que sólo se parecen en el nombre y, para nuestro alivio, es bastante más sencillo que el I2C.


(imagen extraida de Wikimedia, realizada por el usuario Wdwd y con licencia Creative Commons Attribution 3.0 Unported)

El protocolo, como se puede ver en el diagrama, solo necesita de tres hilos: uno para datos, otro para el reloj y otro para seleccionar la palabra o el frame (ponemos esta señal a 0 para enviar la muestra del canal izquierdo y a 1 para enviar la muestra del canal derecho).

Al tratarse de un protocolo de transferencia serie, si queremos emitir audio con calidad CD (16 bits a 44100 Hz estéreo) hace falta generar un reloj de:
$$44100 \times 16 \times 2 = 1411200 \thinspace Hz$$
Como se puede ver, si se quiere trabajar con frecuencias de muestreo lo suficientemente altas como para asegurar una mínima calidad de audio, es necesario hardware dedicado: generar esas señales por software es muy ineficiente. En nuestro caso el microcontrolador MK20 de Freescale (ARM Cortex-M4) que viene en la placa Teensy sí que viene equipado con un interface I2S totalmente programable.

El interface I2S en el microcontrolador MK20

El interface I2S tiene dos modos: directo y mediante DMA. En esta primera aproximación he implementado el modo directo (sin DMA). Es el modo que más CPU consume pero también es el más sencillo. Los pasos para configurar la interface de salida I2S en el MK20 son, grosso modo, los siguientes:

1. Configurar el multiplexor de pines para asignar las tres señales a pines reales.

2. Configurar el los divisores de frecuencia para obtener el “bit clock” de I2S a partir del reloj del sistema.

3. Configurar el tamaño de palabra (16 bits estéreo en nuestro caso).

4. Colgar de la IRQ 35 la función encargada de escribir las muestras en el registro de datos I2S.

5. Habilitar la IRQ 35 (vector de interrupción 16 + 35 = 51 del ARM Cortex-M4).

Configurar el multiplexor de pines es muy sencillo. En este caso he optado por usar la configuración “ALT6” para los pines PORTA.12, PORTA.13 y PORTC.3 que les dan la funcionalidad TX, FS (frame select, el equivalente a "word select") y BCLK (bit clock) respectivamente.



Para configurar el BCLK se dispone de un divisor de frecuencia fraccionario y de un divisor de frecuencia entero. Si quisiéramos usar una frecuencia de muestreo de 48KHz haríamos los siguiente:

1. Establecemos como fuente de reloj, el reloj del núcleo (SYSCLK) que, en nuestro caso, va a 96 MHz.

2. El divisor de frecuencia fraccionario lo configuramos con el valor: 16 / 125 (96 * 16 / 125 = 12.288 MHz).

3. El divisor de frecuencia entero lo configuramos a continuación con el valor 8: 12.288 / 8 = 1.536 MHz).

En este caso: 48 KHz * 2 * 16 = 1.536 MHz.

El resto de pasos es mejor verlos en el código:

bool i2sInit() {
	// configure i/o pins
	// (PTA12 = TX, PTA13 = FS, PTC3 = BCLK) --> ALT6
	PORTA_PCR12 = ((uint32_t) 6) << 8;
	PORTA_PCR13 = ((uint32_t) 6) << 8;
	PORTC_PCR3 = ((uint32_t) 6) << 8;
	// enable system clock for i2s module
	SIM_SCGC6 |= ((uint32_t) 1) << 15;
	// select input clock 0 and output enable
	I2S0_MCR = ((uint32_t) 1) << 30;
#if (I2S_SAMPLE_RATE == 48000)
	// divide to get the 12.2880 MHz from 96MHz (96 * (16/125))
	I2S0_MDR = (((uint32_t) 15) << 12) | ((uint32_t) 124);
#elif (I2S_SAMPLE_RATE == 44100)
	// divide to get the 11.2896 MHz from 96MHz (96 * (2/17))
	I2S0_MDR = (((uint32_t) 1) << 12) | ((uint32_t) 16);
#elif (I2S_SAMPLE_RATE == 32050)
	// divide to get the 8.2051 MHz from 96MHz (96 * (10/117))
	I2S0_MDR = (((uint32_t) 9) << 12) | ((uint32_t) 116);
#else
#error "I2S_SAMPLE_RATE must be 48000, 44100 or 32050"
#endif
	// re-enable system clock to the i2s module
	SIM_SCGC6 |= ((uint32_t) 1) << 15;
	// disable tx (TE=0) while configuring
	I2S0_TCSR &= ~(((uint32_t) 1) << 31);
	// transmitter remains enabled until (and TE set) the end of the current frame
	for (int i = 0; (i < 1000) && (I2S0_TCSR & (((uint32_t) 1) << 31)); i++)
		;
	if (I2S0_TCSR & (((uint32_t) 1) << 31))
		return false;
	// no word mask
	I2S0_TMR = 0;
	// set FIFO watermark
	I2S0_TCR1 = ((uint32_t) (I2S_FRAME_SIZE - 1));
	// use asynchronous mode (SYNC=0), BCLK polatiry active low (BCP=0), select master clock 1 (MSEL=1), bit clock divide (DIV=3), BCLK internally generated
	I2S0_TCR2 = (((uint32_t) 1) << 25) | (((uint32_t) 1) << 26) | ((uint32_t) 3) | (((uint32_t) 1) << 24);
	// transmit data channel is enabled (TCE=1)
	I2S0_TCR3 = (((uint32_t) 1) << 16);
	// frame size (FRSZ), bits per frame sync (SYWD), MSB (MF=1), I2S standard (not "left justified") (FSE=1), frame sync in master mode (FSD)
	I2S0_TCR4 = (((uint32_t) (I2S_FRAME_SIZE - 1)) << 16) | (((uint32_t) (I2S_IO_BIT_DEPTH - 1)) << 8) | (((uint32_t) 1) << 4) | (((uint32_t) 1) << 3) | ((uint32_t) 1);
	// bits per word for first word in each frane (W0W), bits per word for rest of words in each frame (WNW), bit index for first bit tx (MSB, 15-th for 16 bit)
	//I2S0_TCR5 = (((uint32_t) (I2S_IO_BIT_DEPTH - 1)) << 16) | (((uint32_t) (I2S_IO_BIT_DEPTH - 1)) << 24) | (((uint32_t) 15) << 8);
	I2S0_TCR5 = (((uint32_t) (I2S_IO_BIT_DEPTH - 1)) << 16) | (((uint32_t) (I2S_IO_BIT_DEPTH - 1)) << 24) | (((uint32_t) (I2S_IO_BIT_DEPTH - 1)) << 8);
	return true;
}


void i2sStart() {
	wavePtr = (int16_t *) &_binary_drum_loop_16_raw_start;
	NVIC_ENABLE_IRQ(IRQ_I2S0_TX);
	// tx enable (TE=1), bit clock enable (BCE=1), FIFO request interrupt enable, FIFO reset
	I2S0_TCSR |= (((uint32_t) 1) << 31) | (((uint32_t) 1) << 28) | (((uint32_t) 1) << 8) | (((uint32_t) 1) << 25);
}


void i2sStop() {
	NVIC_DISABLE_IRQ(IRQ_I2S0_TX);
}

Además de lo dicho, es necesario colgar de la IRQ 35 una rutina que será invocada tantas veces por segundo como indique la frecuencia de muestreo y que será la encargada de escribir en el registro de salida I2S las muestras de audio que se van a emitir por la interface I2S. Definimos la rutina de la siguiente manera dentro del codigo C++:

extern char _binary_drum_loop_16_raw_start;
extern char _binary_drum_loop_16_raw_end;
volatile char *p;


void i2sTx()  __attribute__ ((section(".i2s_tx")));


volatile int16_t *wavePtr;


void i2sTx() {
	// if FRF=0, return
	if (!(I2S0_TCSR & (((uint32_t) 1) << 16)))
		return;
	// write left and right sample
	I2S0_TDR0 = (uint32_t) *wavePtr;
	I2S0_TDR0 = (uint32_t) *wavePtr;
	wavePtr++;
	if (wavePtr >= ((int16_t *) &_binary_drum_loop_16_raw_end))
		wavePtr = (int16_t *) &_binary_drum_loop_16_raw_start;
	// if underrun, clear underrun
	if (I2S0_TCSR & (((uint32_t) 1) << 18))
		I2S0_TCSR |= (((uint32_t) 1) << 18);
	// if frame sync error, clear frame sync error flag
	if (I2S0_TCSR & (((uint32_t) 1) << 19))
		I2S0_TCSR |= (((uint32_t) 1) << 19);
}

Y en el linker script de nuestro proyecto incluimos una seccion especial a la que llamaremos “.cortex_m4_vector_i2s_tx” y que ubicamos en la direccion de memoria 0x000000CC (la correspondiente a la IRQ 35). En esta sección ponemos la dirección de memoria de nuestra rutina de servicio de interrupción (la encargada de escribir las muestras), es decir metemos la dirección de memoria I2S_TX_ADDRESS + 1 (recordar que al tratarse de un Cortex-M, el reportorio de instrucciones es siempre el reportorio “thumb” y, por lo tanto, los destinos de salto para subrutinas y para codigo siempre deben tener su bit 0 a 1).
SECTIONS {
	. = 0x00000000 ;
	.cortex_m4_vectors : {
		LONG(0x20007FFC);
		LONG(0x00000411);
	}
	. = 0x000000CC ;
	.cortex_m4_vector_i2s_tx : {
		LONG(I2S_TX_ADDRESS + 1);
	}
	. = 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*)
	}
	I2S_TX_ADDRESS = . ;
	.i2s_tx : {
		*(.i2s_tx)
	}
	.preinit_array : {
		__preinit_array_start = . ;
		*(.preinit_array)
		__preinit_array_end = . ;
	}

	...resto del linker script...

Audio de ejemplo

Se ha partido de un sample de dominio público consistente en dos golpes de bombo y caja con charles en medio, típicos del estilo de música house. La muestra se emite en 16 bits con una frecuencia de muestreo de 32050 Hz (Se ha usado este frecuencia por razones de espacio en la memoria flash: es una frecuencia que permite reproducir a una calidad buena manteniendo un tamaño lo suficientemente limitado como para caber en la memoria flash del microcontrolador).



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

[ añadir comentario ] ( 1931 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 13775 )
Síntesis musical mediante modelado analógico en el Teensy 
Aprovechando el post anterior en el que se usaba el DAC de la placa Teensy 3.1 para generar un bucle sonido, he ido un poco más allá y he implementado un pequeño sintetizador monofónico de modelado analógico. La secuenciación es por ahora interna (en una siguiente iteración, se le incorporará una entrada MIDI) e incluye un oscilador de onda en diente de sierra o cuadrada, un filtro de estado variable configurable como paso bajo, paso banda, paso alto y elimina-banda, una envolvente para la frecuencia de corte del filtro y una envolvente para la amplitud.

Punto de partida

En este post anterior se diseñó e implementó un reproductor de sonido para el Teensy que almacenaba un bucle en la memoria flash del microcontrolador. Se utilizó el DAC de 12 bits que viene de serie con el microcontrolador MK20 del Teensy y para el envío de muestras a dicho DAC se usó la interrupción periódica Systick, que traen de serie todos los microcontroladores ARM Cortex-M, ajustada a la frecuencia de muestreo.

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

void systick() {
    DACDAT = next sample
}

int main() {
    // configure DAC
    SIM_SCGC2 |= (1 << 12);    // enable DAC clock generator
    DAC0_C1 = 0x00;            // disable DAC DMA
    DAC0_C0 = 0xC0;            // enable DAC for VREF2 (3.3v)
    // configure SYSTICK
    SYST_RVR = F_CPU / SAMPLE_RATE;
    SYST_CVR = 0;
    SYST_CSR |= 0x07;
    while (1)
        ;
}

Objetivo

El objetivo planteado en este caso era implementar un pequeño sintetizador monofónico partiendo de ese mismo modelo (interrupción Systick + DAC). Los bloques planteados para el mini sintetizador son los siguientes:


El oscilador

Se ha planteado un oscilador muy sencillo basado en tabla de ondas. Un oscilador basado en tabla de ondas consiste en uno o varios arrays con los valores de la onda que queremos generar, en cada array se guarda un único ciclo de onda y el oscilador lo que hace para emitir tonos a diferente frecuencia es recorrer dicha tabla a diferentes velocidades dando la vuelta cuando llega al final:

El tamaño de la tabla de ondas vendrá determinado por la resolución (calidad) que queramos darle y por la frecuencia de muestreo. Imaginemos que tenemos un array con 100 valores de una onda con forma de diente de sierra:
// 100 enteros con signo entre -50 y 49
[-50, -49, -48, -47, ... , 47, 48, 49]

Si la frecuencia de muestreo es de 44100 Hz (la frecuencia de muestreo estándar de calidad CD) y queremos reproducir un tono de 440 Hz (nota LA de la cuarta octava del piano) a partir de esta tabla de ondas el oscilador tendrá que usar un incremento de:

$$\Delta t = {440 \over {44100 \over 100}} = 0.9977324263$$

Esto es, el oscilador tomará la primera muestra de la posición 0 de la tabla, la siguiente muestra la tomará de la posición 0.9977324263, la siguiente de la posición 1.9954648526, y así sucesivamente. Como obviamente se trata de una tabla con posiciones enteras, en cada iteración se coge la muestra más próxima o se interpola.

En este caso se ha optado por usar la muestra en la posición de la parte entera del índice. No es la mejor forma de hacerlo pero sí la más rápida:

t índice (parte entera de t)
0 0
0.9977324263 0
1.9954648526 1
2.9931972789 2
3.9909297052 3
...
99.77324263 99
100.7709750563
como la tabla mide 100, en este momento se vuelve a empezar, manteniendo la parte fraccionaria
0.7709750563 0
1.7687074826 1
2.7664399089 2
...

Debido a que estamos haciendo la implementación en un procesador sin unidad de coma flotante, se realizan todos los cálculos usando aritmética de punto fijo. El formato elegido es el Q16.16 (16 bits enteros + 16 bits fraccionarios = 32 bits que pueden alojarse en un tipo int32_t). Se definen, además, varias macros para facilitar la comprensión del código:

typedef int32_t fixedpoint_t;

#define  __FP_INTEGER_BITS     16
#define  __FP_FRACTIONAL_BITS  16
#define  __TO_FP(a)              (((int32_t) (a)) << __FP_FRACTIONAL_BITS)
#define  __FP_1                  (((int32_t) 1) << __FP_FRACTIONAL_BITS)
#define  __FP_ADD(a, b)          (((int32_t) (a)) + ((int32_t) (b)))
#define  __FP_SUB(a, b)          (((int32_t) (a)) - ((int32_t) (b)))
#define  __FP_MUL(a, b)          ((int32_t) ((((int64_t) (a)) * ((int64_t) (b))) >> __FP_FRACTIONAL_BITS))
#define  __FP_DIV(a, b)          ((int32_t) ((((int64_t) (a)) << __FP_FRACTIONAL_BITS) / ((int64_t) (b))))

El código del método getNextSample del oscilador (el que se invoca para calcular cada muestra) queda, por tanto, como sigue:

fixedpoint_t Oscillator::getNextSample() {
    if (this->status == STATUS_STOPPED)
        return 0;
    else if (this->status == STATUS_STARTED) {
        fixedpoint_t v = ((fixedpoint_t) Wavetable::VALUES[this->t >> __FP_FRACTIONAL_BITS]);
        if (this->patch->waveform == OscillatorPatch::WAVEFORM_SQUARE)
            v = (v > 0) ? __TO_FP(1) : __TO_FP(-1);
        this->t = __FP_ADD(this->t, this->inc);
        if (this->t >= Wavetable::SIZE_FP)
            this->t = __FP_SUB(this->t, Wavetable::SIZE_FP);
        return v;
    }
    else
        return 0;
}

Como se puede ver, el atributo t del objeto es el que se va incrementando y a la hora de determinar qué valor devuelve el método getNextSample() se usa como índice de la tabla simplemente la parte entera de t. Esta decisión no es gratuita e implica que hay que tratar de que los incrementos siempre sean mayores o iguales a 1 para que no se produzcan "escalones" en la señal de salida debido a que se repitan muestras de la tabla: en el ejemplo anterior la primera muestra (t = 0) y la segunda (t = 0.9977324263) serán la misma ya que la parte entera de ambos valores es 0. Para evitar que se produzcan estos escalones se ha optado por incrementar el tamaño de la tabla de ondas.

Si partimos de la base de que la frecuencia del tono es directamente proporcional al incremento de t, se puede buscar un tamaño de tabla tal que, para la frecuencia más baja que se quiera reproducir, se obtenga un incremento de t igual a 1. En efecto, si consideramos que no vamos a reproducir tonos por debajo de los 20 Hz (límite inferior del umbral de audición humano), definiendo las tablas de ondas con un tamaño de
$${44100 \over 20} = 2205 \thinspace muestras$$
Para cualquier tono que queramos reproducir, tendremos siempre un incremento de t mayor o igual a 1. Por otro lado podemos simplificar la ecuación del cálculo del incremento de t:
$$\Delta t = {f_{tono} \over 20}$$
Debido a que el propio cálculo del incremento de t implica una división y las divisiones consumen gran cantidad de recursos en procesadores sin unidad de división (como es el caso del ARM Cortex-M) se ha optado por meter en una tabla los diferentes valores del incremento de t. El índice de dicha tabla es el índice de la nota MIDI (entre 0 y 127).

const fixedpoint_t Wavetable::MIDI_FREQ_INC[128] = {
    0, 28384, 30071, 31859, 33754, 35761, 37887, 40140, 42527, 45056 ... 34563955, 36619234, 38796727, 41103701
};

Con esta tabla precalculada sintonizar el oscilador solo requiere una indexación y una asignación:

void Oscillator::noteOn(uint8_t midiKey, uint8_t midiVelocity) {
    this->inc = Wavetable::MIDI_FREQ_INC[midiKey];
    this->status = STATUS_STARTED;
}


Generador de envolvente

Se utilizan dos generadores de envolvente independientes. Uno que modula la frecuencia de corte del filtro y otro que modula la amplitud del sonido final, antes de escribirlo en el DAC. El tipo de envolvente más común y el que se ha utilizado en este caso es el tipo ADSR (Attack-Decay-Sustain-Release). Cada envolvente de este tipo posee tres valores característicos: el tiempo de ataque (A), el tiempo de caída (D), el nivel de sostenido (S) y el tiempo de liberación (R). Se puede ver un generador de envolvente como una generador de una señal muy lenta que varía entre 0 y 1.

Si se define un nivel de sostenido igual a 0, tenemos una envolvente de tipo AD (Attack-Decay).

Aunque lo más común es definir los parámetro A, D y R en unidades de tiempo (milisegundos, microsegundos), en este caso se ha optado por indicar dichos valores en forma de incrementos, de esta forma no es necesario realizar ninguna multiplicación ni división por cada muestra que se calcula.

fixedpoint_t EnvelopeGenerator::getNextSample() {
	uint8_t localStatus = this->status;
	fixedpoint_t ret = 0;
	do {
		this->status = localStatus;
		if (localStatus == STATUS_STOP)
			ret = 0;
		else if (localStatus == STATUS_ATTACK) {
			ret = this->lastSample + this->patch->attackInc;
			if (ret >= __TO_FP(1)) {
				ret = __TO_FP(1);
				localStatus = STATUS_DECAY;
			}
		}
		else if (localStatus == STATUS_DECAY) {
			ret = this->lastSample - this->patch->decayInc;
			if (ret <= this->patch->sustainLevel) {
				ret = this->patch->sustainLevel;
				if (ret == 0)
					localStatus = STATUS_STOP;
				else
					localStatus = STATUS_SUSTAIN;
			}
		}
		else if (localStatus == STATUS_SUSTAIN) {
			ret = this->patch->sustainLevel;
			if (this->noteOffReceived) {
				this->noteOffReceived = false;
				localStatus = STATUS_RELEASE;
			}
		}
		else if (localStatus == STATUS_RELEASE) {
			ret = this->lastSample - this->patch->releaseInc;
			if (ret <= 0) {
				ret = 0;
				localStatus = STATUS_STOP;
			}
		}
	} while (localStatus != this->status);
	this->lastSample = ret;
	return __FP_MUL(ret, this->amplitude);
}

En la fase de ataque (A) se va incrementando la señal de salida desde 0 hasta 1 en pasos attackInc, en la fase de caída (D) se va decrementando la señal de salida desde 1 hasta el nivel de sostenido en pasos decayInc. Si el nivel de sostenido es 0 la envolvente para al terminar la fase de caída (D), en caso contrario mantiene el nivel de sostenido hasta que se invoca el método noteOff. En ese momento se inicia la fase de liberación (R) decrementando la señal de salida desde el nivel de sostenido hasta 0 en pasos releaseInc.

Filtro

A la hora de implementar un filtro digital existen diferentes aproximaciones: discretización de filtros analógicos conocidos, diseño digital directo usando diagrama de polos y ceros, etc. En este caso se ha optado por una conocida implementación publicada en el libro “Musical Applications of Microprocessors” de Hal Chamberlin. Se trata de una implementación en digital de un filtro de estado variable que permite extraer señales paso bajo, paso banda, paso alto y elimina banda utilizando muy pocos cálculos.

Dicho filtro viene caracterizado por el siguiente sistema de ecuaciones en diferencias:
$$pasoAlto[n] = entrada - ({r \times pasoBanda[n-1]}) - pasoBajo[n]$$
$$pasoBanda[n] = ({f \times pasoAlto[n]}) + pasoBanda[n - 1]$$
$$pasoBajo[n] = ({f \times pasoBanda[n - 1]}) + pasoBajo[n - 1]$$
Siendo:
$$f = 2\sin\left({\pi F_c \over F_s}\right)$$
$$r = {1 \over Q}$$
Siendo $F_c$ la frecuencia de corte del filtro, $F_s$ la frecuencia de muestreo y $Q$ la Q del filtro (la resonancia).

Si se reordenan las ecuaciones en diferencias:
$$pasoBajo[n] = ({f \times pasoBanda[n - 1]}) + pasoBajo[n - 1]$$
$$pasoAlto[n] = entrada - ({r \times pasoBanda[n - 1]}) - pasoBajo[n]$$
$$pasoBanda[n] = ({f \times pasoAlto[n]}) + pasoBanda[n - 1]$$
Podemos olvidarnos de los índices:
pasoBajo += f * pasoBanda
pasoAlto = entrada - (r * pasoBanda) - pasoBajo
pasoBanda += f * pasoAlto

Como se puede apreciar es preciso mantener en memoria al menos las variables “pasoBajo” y “pasoBanda” entre que se procesa una muestra y la siguiente (se trata de un filtro digital de segundo orden).

fixedpoint_t StateVariableFilter::getNextSample(fixedpoint_t input) {
    this->lowPass = __FP_ADD(this->lowPass, __FP_MUL(this->cutoff, this->bandPass));
    fixedpoint_t highPass = __FP_SUB(__FP_SUB(input, this->lowPass), __FP_MUL(this->resonance, this->bandPass));
    this->bandPass = __FP_ADD(this->bandPass, __FP_MUL(this->cutoff, highPass));
    if (this->mode == MODE_LOWPASS)
        return this->lowPass;
    else if (this->mode == MODE_BANDPASS)
        return this->bandPass;
    else if (this->mode == MODE_HIGHPASS)
        return highPass;
    else if (this->mode == MODE_NOTCH)
        return __FP_ADD(highPass, this->lowPass);
    return 0;
}


Voz

En la clase Voice juntamos los elementos que se han definido hasta ahora. Esta clase implementa también la interface Generator:


void Voice::noteOn(uint8_t midiKey, uint8_t midiVelocity) {
    this->oscillator.noteOn(midiKey, midiVelocity);
    this->ampEnv.noteOn(midiKey, midiVelocity);
    this->filterEnv.noteOn(midiKey, midiVelocity);
}

void Voice::noteOff(uint8_t midiKey) {
    this->oscillator.noteOff(midiKey);
    this->ampEnv.noteOff(midiKey);
    this->filterEnv.noteOff(midiKey);
}

fixedpoint_t Voice::getNextSample() {
    fixedpoint_t o = this->oscillator.getNextSample();
    fixedpoint_t env = __FP_MUL(this->filterEnv.getNextSample(), this->filterEnvMod);
    fixedpoint_t cutoff = __FP_ADD(env, this->cutoff);
    if (cutoff < 0)
        cutoff = 0;
    else if (cutoff > __TO_FP(1))
        cutoff = __TO_FP(1);
    this->filter.cutoff = cutoff;
    fixedpoint_t aux = this->filter.getNextSample(o);
    fixedpoint_t e = this->ampEnv.getNextSample();
    return __FP_MUL(aux, e);
}

Ahora cada objeto Voice es un sintetizador monofónico. Si en un futuro se quisiese implementar un sintetizador polifónico simplemente habría que instanciar tantos objetos Voice como voces de polifonía se quisieran.

Secuenciador

Aunque en el diagrama inicial no aparece, es fundamental implementar un secuenciador si se quiere probar el sintetizador y no podemos o no queremos pelearnos aún con la implementación de una entrada MIDI. El secuenciador se encarga de disparar notas en determinados instantes de tiempo, en otras palabras, es el objeto que “toca” el instrumento.

En este caso se ha optado por la implementación de un sencillo secuenciador de 16 pasos equidistantes en semicorcheas. 16 semicorcheas forman un compás de 4 por 4 por lo que toca un único compás. El secuenciador vuelve al empezar cuando llega al final: después de tocar la nota de la semicorchea 15, empieza de nuevo en la semicorchea 0.

void Sequencer::setBPM(uint16_t bpm) {
    uint32_t stepsPerMinute = bpm * 4;               // semicorcheas (pasos de secuenciador) por minuto
    this->n = (Generator::SAMPLES_MS * ((uint32_t) 60000)) / stepsPerMinute;  // muestras por semicorchea
}

Como el método run se invoca cada vez que se va a generar una muestra (SAMPLE_RATE veces por segundo) se disparará una nota cada vez que un contador interno llegue a n.

void Sequencer::run() {
    if (this->status == STATUS_PLAY) {
        if (this->t == 0) {
            uint8_t note = this->midiNote[this->nextNoteIndex];
            if ((note > 0) && (this->generator != NULL))
                this->generator->noteOn(note, 100);
                    this->nextNoteIndex++;
            if (this->nextNoteIndex == SEQUENCE_SIZE)
                this->nextNoteIndex = 0;
        }
        this->t++;
        if (this->t == this->n)
            this->t = 0;
    }
}

Nótese que el secuenciador no envía eventos de tipo “noteOff”. Esto está hecho así adrede para este caso concreto por simplicidad, y porque las envolventes que se usan tienen siempre el nivel de sostenido a 0 (el sonido se acaba extinguiendo aunque el secuenciador no envíe eventos “noteOff”). Como la clase Voice implementa la interface Generator, le podemos decir al secuenciador que mande los disparos de nota (“noteOn”) al objeto de tipo Voice:

Voice v;
Sequencer seq;
...
seq.setBPM(120);
seq.setGenerator(v);

De esta forma ya tenemos adecuadamente inicializado el secuenciador. Ahora sólo falta meter las notas MIDI que queramos que toque. Un valor de nota igual a 0 indica al secuenciador que no queremos disparar ninguna nota en esa semicorchea:

seq.midiNote[0] = 36;   // Do 1
seq.midiNote[1] = 24;   // Do 0
seq.midiNote[2] = 0;
seq.midiNote[3] = 36;   // Do 1
seq.midiNote[4] = 39;   // Re# 1
seq.midiNote[5] = 0;
seq.midiNote[6] = 0;
seq.midiNote[7] = 39;   // Re# 1
seq.midiNote[8] = 36;   // Do 1
seq.midiNote[9] = 24;   // Do 0
seq.midiNote[10] = 0;
seq.midiNote[11] = 36;  // Do 1
seq.midiNote[12] = 39;  // Re# 1
seq.midiNote[13] = 0;
seq.midiNote[14] = 0;
seq.midiNote[15] = 43;  // Sol 1
seq.start();    // cambiamos el estado interno del secuenciador a STATUS_PLAY

En este caso se ha metido una sencilla secuencia típica de música electrónica, en la escala de Do menor.

Juntándolo todo

El secuenciador invoca al método “noteOn” del objeto Voice cada vez que hay una nota nueva que tocar y el valor devuelto por el método “getNextSample” del objeto de tipo Voice es el que se manda al DAC. La señal debe ser adaptada de fixedpoint_t a entero sin signo de 12 bits (0 - 4095):

void systick() {
    seq.run();
    fixedpoint_t aux = v.getNextSample() >> 1;  // evitar clipping
    uint16_t out;
    if (aux >= __TO_FP(1))
        out = 4095;
    else if (aux <= __TO_FP(-1))
        out = 0;
    else
        out = (uint16_t) (((aux + 32768) >> 4) & 0x00000FFF);
    DACDAT = out;
}


Resultados

En la implementación final realizada se ha optado por utilizar una frecuencia de muestreo de 32 KHz. Usar esta frecuencia de muestreo permite generar tonos más precisos ya que 96 MHz no es divisible entre 44.1 KHz pero sí lo es entre 32 KHz (la frecuencia de muestreo es más precisa a 32 KHz que a 44.1 KHz).

A continuación un vídeo en el que puede verse y oirse el invento. No se oye muy alto porque tuve que poner el volumen bajo (era tarde cuando grabé) y encima pasó un camión en ese momento por la calle ¬¬



Todo el código fuente está disponible en la sección soft.


[ añadir comentario ] ( 1566 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |  enlace relacionado  |   ( 3 / 1903 )
Reproducir audio a través del DAC del Teensy 
El procesador ARM Cortex-M4 (Un MK20DX256 de Freescale) incluido en la placa de desarrollo Teensy 3.1 viene equipado con una salida analógica (DAC, no PWM) de 12 bits de resolución con la que es posible generar audio con una calidad razonable y sin apenas hardware externo.

Punto de partida

Se parte del compilador gcc, las binutils y la newlib compilados para el target “arm-none-eabi” detallados en este post y del trabajo realizado anteriormente en este otro post.

DAC

El DAC del microcontrolador Freescale MK20DX256 tiene una resolución de 12 bits (sin signo) y puede ser utilizado tanto de forma sencilla como mediante DMA. En este caso se va a optar por un uso sencillo sin DMA: La escritura de los datos de las muestras la hará el propio código del programa.

...
#define  SIM_SCGC2    *((uint32_t *) 0x4004802C)
#define  DACDAT       *((uint16_t *) 0x400CC000)
#define  DAC0_C0      *((uint8_t *)  0x400CC021)
#define  DAC0_C1      *((uint8_t *)  0x400CC022)
...
SIM_SCGC2 |= (1 << 12);   // habilitar el generador de reloj para el DAC
DAC0_C1 = 0x00;           // deshabilitar el modo DMA para el DAC
DAC0_C0 = 0xC0;           // habilitar el DAC para VREF2 (3.3v)
// a partir de ahora ya se puede escribir en el DAC (DACDAT)
...

Hay que tener en cuenta que el registro DACDAT es un registro de 12 bits sin signo (unsigned).

Systick

La interrupción “systick” es una de las interrupciones estándar del núcleo ARM Cortex-M4 (de hecho está presente en todos los procesadores ARM Cortex). Se trata de una interrupción que se dispara cuando un contador de 24 bits llega a cero, dicho contador está gobernado por el reloj del núcleo (cuidado, suele ser diferente al reloj del bus) y carece de divisores (es muy simple).

El vector de la interrupción se encuentra en la dirección de memoria 0x0000003C. En esta dirección de memoria debe alojarse la dirección de memoria donde se encuentre la función que se ejecutará cada vez que el systick llegue a cero y vuelva a cargarse (una indirección).

Para implementar esta funcionalidad con el GCC se modifica el linker script (teensy31.ld) para incluir el nuevo vector de interrupción:

...
. = 0x00000000 ;
.cortex_m4_vectors : {
    LONG(0x20007FFC);
    LONG(0x00000411);
}
. = 0x0000003C ;
.cortex_m4_vector_systick : {
    LONG(SYSTICK_ADDRESS + 1);
}
. = 0x00000400 ;
.flash_configuration : {
    LONG(0xFFFFFFFF);
    LONG(0xFFFFFFFF);
    LONG(0xFFFFFFFF);
    LONG(0xFFFFFFFE);
}
...

Y para incluir una nueva sección dentro de la memoria de programa con una dirección de memoria prefijada:

...
SYSTICK_ADDRESS = . ;
.systick : {
    *(.systick)
}
...

Nótese que la dirección de memoria almacenada en 0x0000003C es la siguiente dirección impar después de SYSTICK_ADDRESS. Esto tiene una explicación y es muy sencilla:

Los procesadores ARM soportan dos repertorios de instrucciones: un repertorio muy amplio y potente en el que cada instrucción ocupa 32 bits (modo “arm”) y otro repertorio más reducido en el que cada instrucción ocupa 16 bits (modo “thumb”). El primero es más potente pero ocupa más, mientras que el segundo en menos potente pero ocupa mucho menos. Lo que se puede hacer en modo “arm” se puede hacer también en modo “thumb” aunque es posible que para hacer lo que hace una instrucción “arm” sean necesarias dos o tres instrucciones “thumb”.

La forma en que un procesador ARM sabe si una instrucción a la que apunta el PC forma parte de un repertorio de instrucciones u otro es mediante el bit 0 del PC. Si el bit 0 vale 0, se trata de una instrucción “arm”, mientras que si el bit vale 1 se trata de una instrucción “thumb” (nótese que sea cual sea el modo, todas las instrucciones se encuentran, como mínimo, en direcciones pares, en las direcciones impares nunca hay instrucciones).

Por otro lado según la especificación ARM, las excepciones (interrupciones) se deben ejecutar siempre en modo “thumb”. De todas formas en este caso no tenemos elección ya que la serie Cortex-M de ARM sólo soporta el repertorio de instrucciones “thumb” (http://infocenter.arm.com/help/index.js ... BIBGJ.html).

Con la nueva sección de código llamada “.systick” en el código fuente puede ahora definirse la función que va a manejar la interrupción:

...
#define  SYST_CSR  *((uint32_t *) 0xE000E010)
#define  SYST_RVR  *((uint32_t *) 0xE000E014)
#define  SYST_CVR  *((uint32_t *) 0xE000E018)
#define  SAMPLE_RATE  44100

// indicamos al compilador que queremos alojar el cuerpo de esta función en la sección “.systick”
void systick()  __attribute__ ((section(".systick")));

void systick() {
    // TODO
}
...

...
// configuramos el systick para que se ejecute SAMPLE_RATE veces por segundo
SYST_RVR = F_CPU / SAMPLE_RATE;
SYST_CVR = 0;
SYST_CSR |= 0x07;
...

F_CPU es la velocidad en Hz del núcleo (en este caso 96 MHz = 96000000 Hz) y hacemos que el systick se ejecute 44100 veces por segundo (la frecuencia de muestreo del sonido a reproducir).

No se debe utilizar el atributo “interrupt” al declarar la función “systick” ya que en ese caso el compilador intenta compilarla en modo “arm” en lugar de “thumb”.

Sonido

Partiendo de uno de los sonidos (un bucle de bateria) publicado con licencia Creative Commons Attribution-ShareAlike por el usuario de Soundcloud “Phantom Hack3r” (AKA Loop Studio, https://soundcloud.com/phantom-hack3r) se ha editado, se ha dejado sólo con un único compás (el inicial) y se ha exportado a WAV (“drum_loop_1.wav”).

A continuación, usando la herramienta de línea de comandos, “sox” se exporta a su vez este fichero WAV a un formato crudo de 8 bits, mono y sin signo:
sox drum_loop_1.wav -u -b 8 -c 1 -r 44100 drum_loop_1.raw
Luego el fichero drum_loop_1.raw se convierte a un fichero objeto para meterlo como si fuese código dentro del microcontrolador:
/opt/teensy/bin/arm-none-eabi-objcopy --input binary --output elf32-littlearm --binary-architecture arm --rename-section .data=.text drum_loop_1.raw drum_loop_1.o
La opción “--rename-section .data=.text” es muy importante ya que marca los datos generados para que se alojen en la sección “.text” del fichero de salida. Esta sección es la sección que será alojada en la memoria flash del Teensy.

Ahora en “drum_loop_1.o” hay definidas dos variables “_binary_drum_loop_1_raw_start” y “_binary_drum_loop_1_raw_end” cuya dirección de memoria es el inicio y el final respectivamente de los datos crudos convertidos (“drum_loop_1.raw”).

Circuito de salida

A la hora de conectar la salida analógica del DAC a unos altavoces hay que hacerlo siempre a través de un amplificador ya que la corriente máxima que soporta la salida DAC es muy baja. De entre todas las opciones de amplificación, la más sencilla es, sin duda, el uso de unos altavoces amplificados de PC (solución sugerida por el propio creador del Teensy, Paul Stoffregen, aquí).



Se trata de un sencillo condensador electrolítico (para el desacoplo de continua) entre la salida del DAC y la entrada del amplificador de altavoces.

Resultado final

El código fuente final de main.cc es el siguiente:

#include <stdint.h>

using namespace std;

#define  SYST_CSR  *((uint32_t *) 0xE000E010)
#define  SYST_RVR  *((uint32_t *) 0xE000E014)
#define  SYST_CVR  *((uint32_t *) 0xE000E018)

#define  SIM_SCGC2    *((uint32_t *) 0x4004802C)
#define  DACDAT       *((uint16_t *) 0x400CC000)
#define  DAC0_C0      *((uint8_t *)  0x400CC021)
#define  DAC0_C1      *((uint8_t *)  0x400CC022)
#define  SAMPLE_RATE  44100

extern char _binary_drum_loop_1_raw_start;
extern char _binary_drum_loop_1_raw_end;
volatile char *p;

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

void systick() {
    DACDAT = ((uint16_t) *p) << 4;
    p++;
    if (p == &_binary_drum_loop_1_raw_end)
        p = &_binary_drum_loop_1_raw_start;
}

int main() {
    // configure DAC
    SIM_SCGC2 |= (1 << 12);    // enable DAC clock generator
    DAC0_C1 = 0x00;            // disable DAC DMA
    DAC0_C0 = 0xC0;            // enable DAC for VREF2 (3.3v)
    // configure SYSTICK
    p = &_binary_drum_loop_1_raw_start;
    SYST_RVR = F_CPU / SAMPLE_RATE;
    SYST_CVR = 0;
    SYST_CSR |= 0x07;
    while (1)
        ;
}

A continuación un vídeo donde puede verse (y oírse) el montaje en funcionamiento.



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

[ añadir comentario ] ( 1680 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 14429 )
Compilar la toolchain de GNU para Teensy 
En vista de la repentina desaparición del contenido de la web http://kunen.org/uC/gnu_tool.html (“Building the GNU ARM Toolchain for Bare Metal”) que se usó como referencia en este post y en la que se explicaba con gran detalle y facilidad cómo compilar la toolchain de GNU para el target arm-none-eabi, he decidido poner aquí los pasos que di yo (basados en los ahora desaparecidos) para compilar dicha toolchain (yo lo hice para las versiones binutils-2.25, gcc-5.1.0 y snapshot 20150423 de newlib).

Me he basado en los pasos descritos en la web desaparecida y en la web http://wiki.osdev.org/GCC_Cross-Compiler#GCC.

Hay que asegurarse de que se encuentran instalados los paquetes: textinfo, zlib-devel, flex, bison y el resto de herramientas de compilación (gcc, autoconf, etc.). Es recomendable hacer todos los pasos como usuario root.

Descarga
mkdir -p /opt/teensy/src
cd /opt/teensy/src

binutils 2.25
wget ftp://ftp.gnu.org/gnu/binutils/binutils-2.25.tar.bz2
tar xjf binutils-2.25.tar.bz2

gcc 5.1.0
wget ftp://ftp.gnu.org/gnu/gcc/gcc-5.1.0/gcc-5.1.0.tar.bz2
wget https://gmplib.org/download/gmp/gmp-6.0.0a.tar.bz2
wget http://www.mpfr.org/mpfr-current/mpfr-3.1.2.tar.bz2
wget ftp://ftp.gnu.org/gnu/mpc/mpc-1.0.3.tar.gz
wget http://isl.gforge.inria.fr/isl-0.14.tar.bz2
tar xjf gcc-5.1.0.tar.bz2
tar xjf gmp-6.0.0a.tar.bz2
tar xjf isl-0.14.tar.bz2
tar xzf mpc-1.0.3.tar.gz
tar xjf mpfr-3.1.2.tar.bz2
mv gmp-6.0.0 gcc-5.1.0/gmp
mv isl-0.14 gcc-5.1.0/isl
mv mpc-1.0.3 gcc-5.1.0/mpc
mv mpfr-3.1.2 gcc-5.1.0/mpfr

newlib
wget ftp://sourceware.org/pub/newlib/newlib-2.2.0.20150423.tar.gz
tar xzf newlib-2.2.0.20150423.tar.gz

Compilación

binutils 2.25
mkdir -p /opt/teensy/build/binutils-2.25
cd /opt/teensy/build/binutils-2.25
../../src/binutils-2.25/configure --target=arm-none-eabi --prefix=/opt/teensy --enable-interwork --enable-multilib
make
make install
export PATH="/opt/teensy/bin:$PATH"

gcc 5.1.0 (inicial)
mkdir -p /opt/teensy/build/gcc-5.1.0
cd /opt/teensy/build/gcc-5.1.0
../../src/gcc-5.1.0/configure --target=arm-none-eabi --prefix=/opt/teensy --enable-interwork --enable-multilib --enable-languages="c,c++" --with-newlib --with-headers=../../src/newlib-2.2.0.20150423/newlib/libc/include --with-system-zlib
make all-gcc
make install-gcc

newlib
mkdir -p /opt/teensy/build/newlib
cd /opt/teensy/build/newlib
../../src/newlib-2.2.0.20150423/configure --target=arm-none-eabi --prefix=/opt/teensy --enable-interwork --enable-multilib
make
make install

gcc 5.1.0 (final)
cd /opt/teensy/build/gcc-5.1.0
make all
make install

Tras completar todo el proceso, el compilador, las binutils y la newlib para ARM quedan instaladas en la carpeta /opt/teensy.

[ añadir comentario ] ( 1255 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 2377 )
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 )

<< | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |