Gestión de interrupciones en microcontroladores RISC-V 
La gestión de las interrupciones es un tema planteado de forma muy básica en la arquitectura original RISC-V. En controlador básico de interrupciones que se describe en los documentos oficiales de la arquitectura está más orientado a la captura de eventos entre diferentes niveles de ejecución que a la captura de eventos externos, algo que, en el caso de los microcontroladores, se echa de menos. Abordaremos el uso del este controlador básico de interrupciones y el uso del CLIC, un controlador de interrupciones más avanzado, que complementa al primero y que está disponible en casi todos los núcleos RISC-V.

Controlador básico de interrupciones

El control de interrupciones que se describe en el volumen 2 de la especificación es un control de interrupciones muy básico orientado más a la captura de eventos entre diferentes niveles de privilegio (captura desde el nivel supervisor de eventos producidos en el nivel usuario, o captura desde el nivel máquina de eventos producidos en el nivel supervisor) que a la captura de eventos externos. De hecho, en el controlador de interrupciones "oficial" sólo tenemos como "interrupciones" (tal y como las entendemos casi todos los que venimos del mundo de lo microcontroladores) dos tipos: las de timer y las etiquetadas genéricamente como "externas" (estas ultimas, dependientes de la implementación que haga cada fabricante).

Como se puede ver, se trata de un mecanismo de interrupción muy pobre y que, aunque muy bien pensado para entornos multiusuario, es insuficiente para entornos embebidos, en los que todo el código se ejecutará casi siempre con los niveles de privilegio M (de máquina) y apenas se usan otros niveles de privilegio. Sin embargo, en caso de que sólo necesitemos una interrupción de timer, sería la mejor opción, ya que está presente en todas las implementaciones de RISC-V (forma parte de la especificación estándar y siempre existirá).

Los CSR

Muchos de los parámetros de funcionamiento en los procesadores RISC-V se configuran mediante CSR (Control and Status Registers), dichos registros son registros internos del procesador que son accesibles mediantes instrucciones de esamblador especiales de transferencias de valores desde/hasta registros normales (CSRR, CSRW, etc.) o mediante valores inmediatos (CSRWI, etc.). Existen una serie de CSRs estándar y, además, cada fabricante puede incluir los suyos propios, siempre y cuando se mantengan los del estándar de RISC-V.

...
// poner a 1 el bit 3 del CSR "msi"
asm volatile (
    "csrsi mstatus, 8"
);
...
// escribir 0x08000000 en el CSR número 0x307 (cuando no podemos usar el nombre)
asm volatile (
    "csrw 0x307, %[reg]" : : [reg] "r" (0x08000000)
);
...

En este ejemplo de código el ensamblador que viene con las binutils sí reconoce el símbolo "mstatus" como CSR (al ser un CSR estándar presente en todos los núcleos RV32I), sin embargo el CSR "mtvt" es un CSR definido para el controlador de interrupciones avanzado (CLIC) y, como no está definido como símbolo en las "binutils", debemos poner su valor (0x307) si queremos acceder a él desde ensamblador.

Ejemplo 1: Blinker usando el mecanismo básico de interrupciones

Como dijimos antes, el mecanismo básico de interrupciones permite el uso de una interrupción de timer así como de una interrupción externa. En este caso usaremos este mecanismo básico de interrupciones para configurar la interrupción de timer con el objetivo de hacer parpadear un led.



Lo primero que hacemos es configurar en el CSR llamado "mtvec" el vector de interrupción, que podrá ser vectorizado o no, es decir, podremos indicar un puntero a una función o la dirección de memoria de una tabla donde estén indicadas, a su vez, las direcciones de salto de cada uno de las interrupciones que se definan (al estilo ARM Cortex-M). La forma en la que el RISC-V debe interpretar este valor (si como puntero a función o como puntero a tabla) se debe indicar en los dos bits menos significativos de este CSR. Como vamos a usar el mecanismo básico de interrupciones, la cantidad de interrupciones que se pueden generar es muy escaso y, por tanto, he optado por indicar directamente un puntero a una función (modo directo, mode = 00): Nótese que los bits 0 y 1 de la dirección de memoria de la función deben estar a 0 (puesto que estos bits en el CSR de mtvec se usan para indicar el modo), lo que obliga a que el handle de interrupción esté en una dirección de memoria múltiplo de 4 (alineada a 32 bits).

A continuación configuramos el CSR "mie" (Machine Interrupt Enable), donde decimos, para el nivel de privilegio M (el de máquina, con el que arranca el RISC-V por defecto) qué interrupciones se habilitan (activamos el bit correspondiente al timer):



En este CSR ponemos a 1 el bit 7 (MTIE = Machine Timer Interrupt Enable), mientras que en el CSR "mstatus" debemos habilitar las interrupciones de nivel de máquina poniendo a 1 el bit 3.



Con estos pasos ya tenemos configurado el controlador básico de interrupciones para que se dispare la interrupción de timer. El timer es muy parecido al SysTick de los procesadores ARM Cortex-M: un contador que se incrementa a la misma velocidad que el reloj del sistema y que, en el caso del RISC-V, se configura con dos registros mapeados en memoria:

- MTIME: Un registro de 64 bits que se va incrementando de forma indefinida y que cuando se desborda vuelve a cero (de lectura y escritura).

- MTIMECMP: Un registro de 64 bits que sólo se escribe desde software (aunque también es de lectura y escritura) y que, cuando vale igual que mtime, se produce la interrupción de timer en caso de estar ésta configurada.

Las direcciones de memoria donde están mapeados estos dos registros de 64 bits no están definidas por el estándar, son de libre elección por parte del fabricante. En el caso del GD32VF103, se alojan en 0xD1000000 y 0xD1000008 respectivamente.

#include <stdint.h>

using namespace std;

#define  RCU_APB2EN  *((volatile uint32_t *) 0x40021018)
#define  GPIOC_CTL1  *((volatile uint32_t *) 0x40011004)
#define  GPIOC_OCTL  *((volatile uint32_t *) 0x4001100C)
#define  MTIME       *((volatile uint64_t *) 0xD1000000)
#define  MTIMECMP    *((volatile uint64_t *) 0xD1000008)

#define  MTIME_INTERRUPT_PERIOD  12000000    // 24 MHz, so 12000000 generates an interrupt period of exactly half a second

void interruptHandler() __attribute__ ((interrupt, section(".interrupt_handler")));

void interruptHandler() {
    MTIME = 0;
    MTIMECMP = MTIME_INTERRUPT_PERIOD;
    GPIOC_OCTL = GPIOC_OCTL ^ (((uint32_t) 1) << 13);
}

int main() {
    // enable clock on port C
    RCU_APB2EN = RCU_APB2EN | (((uint32_t) 1) << 4);
    // PC13 pin is output, low speed, push-pull
    GPIOC_CTL1 = 0x44244444;
    // basic (non vectored) interrupt handler (to force non vectored, set 0 to lower two bits of mtvec, so force 4 byte aligned on linker script for interrupt handler)
    asm volatile (
        "csrw mtvec, %[reg]" : : [reg] "r" ((uint32_t) interruptHandler)
    );
    // machine interrupt enable
    asm volatile (
        "csrw mie, %[reg]" : : [reg] "r" ((uint32_t) 0x80)
    );
    asm volatile (
        "csrsi mstatus, 8"
    );
    // configure interrupt period
    MTIME = 0;
    MTIMECMP = MTIME_INTERRUPT_PERIOD;
    // sleep
    while (true)
        asm volatile ("wfi");
}

Paso a comentar las partes más relevantes del código:

1. Definimos la función "interruptHandler" (aunque da igual el nombre que le pongamos), le ponemos los atributos de compilador "interrupt" y "section(".interrupt_handler")" (el nombre de la sección también da igual, lo importante es que no esté en la sección ".text" que es donde se pone por defecto todo el código). En la función reiniciamos los registro MTIME y MTIMECMP y cambiamos de estado el led. No es necesario marcar ningún flag para indicar que la interrupción está atendida ya que, según la documentación del estándar del RISC-V, al escribir en la dirección de memoria de MTIMECMP el procesador ya considera atendida la interrupción de timer.

2. En la función main habilitamos el reloj para la circuitería de GPIO, configuramos el puerto PC13 como salida push-pull a baja velocidad y configuramos la interrupción de timer: Escribimos en "mtvec" la dirección de memoria del handler que hemos definido antes (como estará alineado a 32 bits, sus dos bits menos significativos ya valdrán 0, por lo que, de paso, también seleccionamos que queremos un tratamiento de interrupciones no vectorizado, sino definiendo una única función para atender todas las interrupciones), habilitamos los bits de "mie" y "mstatus" que vimos antes y configuramos los registros MTIME y MTIMECMP para que el tiempo entre interrupciones sea de medio segundo (sabiendo que el reloj del timer va a 96 MHz / 4 = 24 MHz).

3. Entramos en un bucle infinito con la instrucción "wfi" para que el procesador permanezca dormido y en modo de bajo consumo mientras no esté atendiendo interrupciones.

Para que este código funcione correctamente es necesario, además de "startup.cc", que se encarga de inicializar todo el subsistema de relojes para que el núcleo vaya a 96 MHz, que en el linker script estén correctamente indicadas las secciones, sobre todo la sección que hemos llamado ".interrupt_handler", ya que debemos forzar a que dicha sección esté alineada a 32 bits (para que su dirección de memoria sea múltiplo de 4 y así los dos bits menos significativos de dicha dirección de memoria valgan 0).

SECTIONS {
    . = 0x08000000 ;
    .text : {
        startup.o (.startup0)
    }
    . = 0x08000200 ;
    .text : {
        _linker_code = . ;
        startup.o (.startup1)
        *(.text)
        *(.text.*)
        *(.rodata*)
        *(.gnu.linkonce.t*)
        *(.gnu.linkonce.r*)
    }
    . = ALIGN(4);     /* to force lower 2 bits of address to 0 (mtvec.mode = 0 to select non vectored interrupt handler) */
    .interrupt_handler : {
        *(.interrupt_handler)
    }
...
}


Ejemplo 2: Blinker usando el CLIC (Core Local Interrupt Controller)

El CLIC (Core Local Interrupt Controller) es aún una propuesta de estándar, que se encuentra documentada en el GitHub de RISC-V International, pero que ya está siendo implementada por múltiples fabricantes, entre ellos GigaDevice (en su GD32VF103), SiFive (en todos sus procesadores) y muchos otros. Es una propuesta de controlador de interrupciones compatible con el sistema básico de interrupciones, pero que permite hasta 4096 interrupciones externas, configurables por niveles, prioridad, etc. Al ser una propuesta de la propia RISC-V International, lo cierto es que se ha convertido en estándar "de facto" y sólo cabe esperar que se acabe poniendo de forma "bonita" en un documento de especificación, en lugar de como está ahora. El CLIC define una serie de CSRs nuevos además de un conjunto de registros mapeados en memoria para el manejo "ampliado" de interrupciones.

En este caso, aunque el CLIC puede usarse con interrupciones no vectorizadas, es recomendable configurarlo para usarlas vectorizadas. En un CSR denominado "mclicbase" se encuentra la dirección de memoria base a partir de la que se localiza el resto de registros mapeados en memoria del CLIC. En el caso del GD32VF103 he optado por poner a fuego dicho valor, ya que es conocido (está indicado en la documentación del fabricante) y de esta forma también simplificamos el código generado (aunque sea menos portable). A partir de la dirección base del CLIC se obtiene el resto de registros mapeados en memoria.

#define  CLIC_BASE                    0xD2000000
#define  CLIC_IP(source)              *(volatile uint8_t *)(CLIC_BASE + 0x00001000 + ((source) * 4))
#define  CLIC_IE(source)              *(volatile uint8_t *)(CLIC_BASE + 0x00001001 + ((source) * 4))
#define  CLIC_ATTR(source)            *(volatile uint8_t *)(CLIC_BASE + 0x00001002 + ((source) * 4))
#define  CLIC_CTL(source)             *(volatile uint8_t *)(CLIC_BASE + 0x00001003 + ((source) * 4))
#define  CLIC_CFG                     *(volatile uint8_t *)(CLIC_BASE + 0x00000000)
#define  CLIC_MTH                     *(volatile uint8_t *)(CLIC_BASE + 0x0000000B)
#define  CLIC_IP_IE_ATTR_CTL(source)  *(volatile uint32_t *)(CLIC_BASE + 0x00001000 + ((source) * 4))

Para configurar las interrupciones vectorizadas con el CLIC primero preparamos la tabla de interrupciones que, en el caso del GD32VF103, debe estar a partir de la dirección 0x00000000 (limitaciones del propio fabricante) y que nosotros situaremos a partir de la dirección 0x08000000, ya que ambas zonas de memoria son alias una de la otra y es a partir de 0x08000000 donde se encuentra la memoria flash de programa. Recordemos que en el caso del GD32VF103, la dirección de memoria 0x08000000 es la dirección de reset y que en esa posición lo que tenemos es una instrucción "jump" hacia 0x08000200 que es donde empieza la ejecución del código. Es en este "hueco" entre 0x08000000 y 0x08000200 es donde se aloja la tabla de vectores de interrupción. Como sólo vamos a configurar uno de los vectores de interrupción (el del timer, que se corresponde con la posición 7), hacemos que el linker script situe ahí (0x08000000 + (7 * 4) = 0x0800001C) la dirección de memoria de una sección que llamaremos ".clic_int_tmr" (el nombre no es relevante).

SECTIONS {
    . = 0x08000000 ;
    .text : {
        startup.o (.startup0)
    }
    . = 0x0800001C ;
    .clic_int_tmr_vector : {
        LONG(CLIC_INT_TMR_ADDRESS);
    }
    . = 0x08000200 ;
    .text : {
        _linker_code = . ;
        startup.o (.startup1)
        *(.text)
        *(.text.*)
        *(.rodata*)
        *(.gnu.linkonce.t*)
        *(.gnu.linkonce.r*)
    }
    . = ALIGN(4);
    CLIC_INT_TMR_ADDRESS = . ;
    .clic_int_tmr : {
        *(.clic_int_tmr)
    }
    ...
}

A continuación, en "main.cc" definimos nuestra función de manejo de las interrupciones:

void clicIntTmr() __attribute__ ((interrupt, section(".clic_int_tmr")));

void clicIntTmr() {
    MTIME = 0;
    MTIMECMP = MTIME_1_SEC;
    GPIOC_OCTL = GPIOC_OCTL ^ (((uint32_t) 1) << 13);   // toggle led
    CLIC_IP(7) = 0;    // not pending
}

También aplicamos a esta función el atributo "interrupt" e indicamos que dicha función debe estar en la sección ".clic_int_tmr" (el nombre que elegimos en el linker script). La única diferencia notable con respecto al caso del mecanismo básico de interrupciones es que aquí nos aseguramos de borrar de forma explícita el flag "interrupt pending" (IP) antes de regresar de la interrupción.

La preparación del CLIC es ligeramente más compleja que en el caso del mecanismo básico de interrupciones:

int main() {
    // enable clock on port C
    RCU_APB2EN = RCU_APB2EN | (((uint32_t) 1) << 4);
    // PC13 pin is output, low speed, push-pull
    GPIOC_CTL1 = 0x44244444;
    // clear CLIC config register and set vectored interrupts
    CLIC_CFG = 0;
    CLIC_MTH = 0;   // specific register for GD32VF103 ECLIC
    for (uint16_t i = 0; i < 4096; i++)
        CLIC_IP_IE_ATTR_CTL(i) = 0;
    // use CLIC vectored interrupt handler (put 1 on mode (lower two) bits of mtvec)
    asm volatile (
        "csrw mtvec, %[reg]" : : [reg] "r" (0x00000003)
    );
    // machine interrupt enable
    asm volatile (
        "csrsi mstatus, 8"
    );
    // set CLIC interrupt vector table (mtvt = 0x307)
    asm volatile (
        "csrw 0x307, %[reg]" : : [reg] "r" (0x08000000)
    );
    CLIC_ATTR(7) = 0b11000001;    // machine mode, level triggered, vectored
    CLIC_IE(7) = 1;               // enable interrupt 7
    CLIC_IP(7) = 0;               // not pending
    CLIC_CTL(7) = 0;              // priority
    // configure interrupt period
    MTIME = 0;
    MTIMECMP = MTIME_1_SEC;
    while (true)
        asm volatile ("wfi");
}

Los pasos son los siguientes:

1. Inicializamos el CLIC según indica la documentación: borrando CLIC_CFG, CLIC_MTH y poniendo a cero los 4096 registros de configuración mapeados en memoria.

2. Indicamos en el CSR "mtvec" que vamos a usar el CLIC en lugar del mecanismo básico de interrupciones poniendo a 1 los dos bits menos significativos de "mtvec". Recordemos que en el caso del mecanismo básico de interrupciones, estos dos bits estaban a 0.

3. Indicamos en el CSR "mstatus" que se habilitan las interrupciones a nivel de máquina (M). Igual que como hacíamos con el mecanismo básico de interrupciones.

4. Escribimos en un CSR nuevo definido para CLIC y llamado "mtvt" la dirección de memoria de la tabla de vectores de interrupción, que debe estar alineada a 64 bytes (en nuestro caso la dirección de memoria 0x08000000 cumple con este requisito). Nótese que el CSR "mtvt" no se reconoce por parte de las binutils de GNU, por lo que en el ensamblador generado hemos tenido que usar el valor numérico que corresponde con dicho CSR (0x307).

5. Configuramos en los registros del CLIC mapeados en memoria (en la posición correspondiente a la interrupción 7 de los 4096 registros), la prioridad, el tipo de interrupción y la propia habilitación de la misma (CLIC_ATTR, CLIC_IE, CLIC_IP y CLIC_CTL).

6. El resto de operaciones son las mismas que en caso del mecanismo básico de interrupciones de timer: inicializar MTIME y MTIMECMP y entrar en el bucle infinito en el que dormimos al procesador con la instrucción "wfi" cuando no está atendiendo interrupciones.

El resultado es el mismo: un led que parpadea, pero utilizando las dos aproximaciones distintas al mecanismo de interrupciones que permite hoy día la arquitectura RISC-V.

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

[ 2 comentarios ] ( 5385 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 736 )
Blinker básico para RISC-V en C++ 
Partiendo del compilador cruzado GCC para RISC-V descrito en el post anterior, el primer objetivo será hacer una prueba de concepto con un procesador RISC-V real. En este caso he optado por un GD32VF103, un microcontrolador de GigaDevice, con 128 Kb de memoria flash de programa, 32 Kb de SRAM, procesador RISC-V con arquitectura básica RV32IMAC y periféricos muy parecidos a los del STM32F103.

Secuencia de arranque

Los procesadores RISC-V, al contrario que los ARM, no poseen, en su configuración base, un vector de interrupciones, es una extensión estándar pero no forma parte del núcleo en sí. Por tanto, el concepto que existe en los ARM de una tabla de interrupciones en la que el "reset" es una interrupción más, no existe en RISC-V: En los RISC-V existe el "RESET_VECTOR", que es la dirección de memoria desde donde arranca el procesador cuando se reinicia o se enciende. Dicha dirección suele ser 0x00000000 pero puede ser libremente definida por el fabricante, en el caso que nos ocupa, el GD32VF103 incluye un bootloader en la dirección 0x00000000, mientras que en la dirección 0x08000000 se encuentra la memoria flash de programa (los 128 Kb). Cuando el micro arranca en modo bootloader (determinado por un pin), arranca desde la dirección 0x00000000 (RESET_VECTOR = 0x00000000), ejecuta el bootloader y éste se encarga de poner el micro en modo DFU a través del USB, mientras que cuando arranca en modo normal (sin bootloader), el micro arranca desde la dirección 0x08000000 (RESET_VECTOR = 0x08000000), donde se supone que debe estar el programa principal.

Algo parecido ocurre con el puntero de pila: Al contrario que los Cortex-M de ARM, en los RISC-V no existe una tabla que indique el valor que debe tener, en el momento del arranque, el registro de pila (SP), por lo que dicho registro debe ser cargado a mano mediante una instrucción en ensamblador. Por ejemplo, una secuencia de arranque mínima podría ser:

reset_vector:
    la sp, 0x20005000   ; la SRAM acaba en 0x20005000 y la pila "crece" hacia abajo
    call main           ; llamamos a la función main

Nótese que no se está teniendo el cuenta la inicialización de variables globales (incluidos los constructores de objetos globales), pero es un punto de partida.

Linker script y arranque

Basándonos en un post anterior hacemos un linker script básico y un código de arranque e inicialización para variables globales y constructores. En este caso, al principio del linker script dividimos el código generado en dos zonas:

- Una al principio, en 0x08000000, donde sólo alojaremos una instrucción de salto.

- Otra a partir de 0x08000200 donde alojaremos la inicialización y el resto del código (incluida la función main).

Esto lo hacemos así porque en el caso del GD32VF103 sí que existe, para la extensión de las interrupciones de RISC-V, una tabla de interrupciones que se extiende desde 0x0800000C hasta 0x080001FC, por lo que, para poder en un futuro usar ese vector de interrupciones, haremos que el grueso del código de aloje a partir de 0x08000200 y en 0x08000000 lo que ponemos es un salto incondicional a 0x08000200.

SECTIONS {
. = 0x08000000 ;
.text : {
startup.o (.startup0)
}
. = 0x08000200 ;
.text : {
_linker_code = . ;
startup.o (.startup1)
*(.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__ = . ;
}
...

Como se puede ver, se definen dos secciones al principio del linker script:

- Una a partir de la dirección 0x08000000 y en la que se alojará el código de las funciones etiquetadas con la sección ".startup0" del fichero startup.o.

- Otra a partir de la dirección 0x08000200 y en la que se alojará primero el código de las funciones que se encuentren etiquetadas con la sección ".startup1" seguido por el resto del código del programa.

A continuación se ubican las secciones estándar ".preinit_array", ".init_array", ".ctors" encargadas de alojar las llamadas a los constructores y las funciones de inicialización de variables globales, y las secciones estándar ".fini_array" y ".dtors", encargadas de alojar las llamadas a los destructores y las funciones de destrucción de variables globales (esta parte del código no se debería ejecutar jamás en un sistema embebido, se incluye por elegancia).

Variables globales en RAM y copia de datos de Flash a RAM

...
flash_sdata = . ;
. = 0x20000000 ;
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 = . ;
end = . ;
}

En esta parte del linker script se indica que la dirección virtual de los datos es 0x20000000 (esta es la dirección que se usará desde el código de programa para acceder a los datos en RAM), pero sin embargo dichos datos están inicialmente alojados en la memoria flash (a continuación de la sección ".dtors"), por lo que es necesario un copiado de dichas variables globales inicializadas desde la memoria flash hasta la RAM antes de que se ejecute la función "main".

Código de arranque

En el fichero startup.cc se definen dos funciones _startup_0 y _startup_1 y se etiquetan como de las secciones ".startup0" y ".startup1", respectivamente. La función _startup_0 está definida con el atributo "naked" (para que el compilador no genere código de preámbulo ni postámbulo) y contiene una única instrucción de salto:

void _startup_0() __attribute__((section(".startup0"), naked));

void _startup_0() {
    asm volatile (
        "j %0" : : "i" (_startup_1)
    );
}

Esta es la función que se aloja en la dirección 0x08000000 y que, como se vio antes, es la encargada de saltar a _startup_1. La función _startup_1, que también está definida como "naked" pero etiquetada en la sección ".startup1" (por lo que se aloja a partir de la dirección 0x08000200) alberga el resto de código de inicialización:

void _startup_1() __attribute__((section(".startup1"), naked));

void _startup_1() {
    asm volatile (
        "la sp, 0x20005000"  // point SP to the end of SRAM
    );
    _initClock();
    _initDataRAM();
    _initBssRAM();
    _callConstructors();
    _callInitArray();
    main();
    _callFiniArray();
    _callDestructors();
    while (true)
        ;
}

Y realiza lo siguiente:

1. Inicializa el puntero de pila para que apunte al final de la RAM (el puntero de pila se decrementa cuando se hace "push" y se incrementa cuando se hace "pop").

2. Inicializa los PLLs del microcontrolador para configurar el reloj de cristal (en mi caso he dejado esta función vacía por lo que el microcontrolador queda funcionando con el reloj RC interno que es más lento pero suficiente para nuestro blinker).

3. Inicializa la RAM que se copia desde la flash (ver aquí los detalles de este proceso).

4. Inicializa la RAM que debe estar a cero (BSS).

5. Invoca las funciones de inicialización y los constructores.

6. Finalmente incova a la función "main".

Al final se coloca una especie de código "de cortesía" para que, en el caso de que la función "main" regrese (cosa que no debería ocurrir), se invoquen los destructores y el micro se quede "colgado" (bucle infinito).

Prueba de concepto

Se ha optado por hacer un simple blinker basado en esperas estándar de ciclos de procesador, en lugar de un timer, ya que, como se comentó antes, el núcleo básico de un RISC-V no incluye interrupciones y creo que es mejor abordarlas en una siguiente entrega:

#include 

using namespace std;

#define  RCU_APB2EN  *((uint32_t *) 0x40021018)
#define  GPIOC_CTL1  *((uint32_t *) 0x40011004)
#define  GPIOC_OCTL  *((uint32_t *) 0x4001100C)

int main() {
    // enable clock on port C
    RCU_APB2EN |= ((uint32_t) 1) << 4;
    // PC13 pin is output, low speed, push-pull
    GPIOC_CTL1 = 0x44244444;
    while (true) {
        for (uint32_t i = 0; i < 200000; i++)
            ;
        GPIOC_OCTL ^= (((uint32_t) 1) << 13);
    }
}

La placa en la que está alojada el GD32VF103 es una Longan Nano que posee un led tricolor con la componente roja conectada al pin PC13 del micro. Para hacer parpadear este led rojo debemos:

1. Habilitar el reloj interno de la circuitería GPIO del micro.

2. Configurar el pin PC13 como GPIO de salida en push-pull.

3. Modificar el bit 13 del registro asociado de forma periódica para hacer que el led parpadee.

Los registros son una copia casi exacta de los del STM32F103 por lo que si estás acostumbrado a ese micro de ST, adaptarte al GD32VF103 será muy sencillo.

Bootloader

Cuando arranca el microcontrolador GD32VF103, si el pin BOOT0 se encuentra a nivel 1, no se arranca desde la dirección 0x08000000, sino que se ejecuta un "bootloader" interno que configura el microcontrolador como un dispositivo USB con interfaz estándar DFU (Device Firmware Upgrade). Se trata de un estándar conocido y para el que existen herramientas como dfu-util (que, en el caso de Linux, sólo requiere de la librería "libusb"). Con el microcontrolador en modo DFU y el comando "dfu-util" podemos "tostar" nuestro ejecutable "main.bin" de forma muy sencilla:

dfu-util --dfuse-address 0x08000000 -D main.bin

En la sección soft está todo el código disponible.



[ añadir comentario ] ( 1006 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 726 )
Compilar la toolchain de GNU para RISC-V (bare metal) 
Los procesadores RISC-V empiezan a hacerse poco a poco un huequito en nuestros corazones. A continuación pongo las instrucciones para compilar la toolchain cruzada de GNU (GCC) para RISC-V con newlib.

Se trata de una toolchain para sistemas "bare metal", sin sistema operativo, por lo que no tiene soporte para multihilos ni para librerías dinámicas.

binutils 2.36

mkdir -p /opt/baremetalriscv/src
mkdir -p /opt/baremetalriscv/build
cd /opt/baremetalriscv/src
wget https://ftp.gnu.org/gnu/binutils/binutils-2.36.tar.bz2
tar xf binutils-2.36.tar.bz2
cd ../build
mkdir binutils-2.36
cd binutils-2.36/
../../src/binutils-2.36/configure --prefix=/opt/baremetalriscv --target=riscv32-none-elf --disable-nls
make
make install

gcc 11.1.0 (stage 1)

cd /opt/baremetalriscv/src
wget https://ftp.gnu.org/gnu/gcc/gcc-11.1.0/gcc-11.1.0.tar.gz
wget https://ftp.gnu.org/gnu/gmp/gmp-6.2.1.tar.bz2
wget https://ftp.gnu.org/gnu/mpc/mpc-1.2.1.tar.gz
wget https://ftp.gnu.org/gnu/mpfr/mpfr-4.1.0.tar.gz
tar xf gcc-11.1.0.tar.gz
tar xf gmp-6.2.1.tar.bz2
tar xf mpc-1.2.1.tar.gz
tar xf mpfr-4.1.0.tar.gz
mv gmp-6.2.1 gcc-11.1.0/gmp
mv mpc-1.2.1 gcc-11.1.0/mpc
mv mpfr-4.1.0 gcc-11.1.0/mpfr
cd ../build/
mkdir gcc-11.1.0-stage-1
cd gcc-11.1.0-stage-1/
export PATH=/opt/baremetalriscv/bin:${PATH}
../../src/gcc-11.1.0/configure --prefix=/opt/baremetalriscv --target=riscv32-none-elf --enable-languages=c --without-headers --disable-nls --disable-threads --disable-shared --disable-libssp --with-newlib
make all-gcc all-target-libgcc
make install-gcc install-target-libgcc

newlib

cd /opt/baremetalriscv/src
git clone git://github.com/riscv/riscv-newlib.git
cd ../build
mkdir newlib
cd newlib
../../src/riscv-newlib/configure --prefix=/opt/baremetalriscv --target=riscv32-none-elf
make
make install

gcc 11.1.0 (stage 2)

cd /opt/baremetalriscv/build
mkdir gcc-11.1.0-stage-2
cd gcc-11.1.0-stage-2/
../../src/gcc-11.1.0/configure --prefix=/opt/baremetalriscv --target=riscv32-none-elf --enable-languages=c,c++ --disable-nls --disable-threads --disable-shared --disable-libssp --with-newlib --with-headers=../../src/riscv-newlib/newlib/libc/include
make
make install


El compilador de C++ de GCC 11.1 compila por defecto en modo C++17 y soporta prácticamente todo el estándar C++20.

ACTUALIZACIÓN: El proceso de compilación descrito sirve también para gcc-12.2, binutils-2.40 y el repositorio de newlib oficial (git://sourceware.org/git/newlib-cygwin.git) puesto que el proyecto riscv-newlib ya se ha integrado en el proyecto oficial de newlib.

[ añadir comentario ] ( 572 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3 / 676 )
Estrella de Navidad con CPLD 
El proyecto navideño de este año es una sencilla estrella de navidad que puede colocarse tanto en el árbol como en el belén y que requiere de muy poca circuitería.

Idea

El objetivo era no recurrir a la típica luz parpadeante sino darle un efecto de movimiento, que parezca que la estrella se mueva por el firmamento. Para simular este efecto lo más sencillo es alinear varias luces y hacer que la luz se mueva rápidamente, con un brillo creciente y en una dirección concreta, lo que da el efecto de que la luz un efecto meteorito.

Diseño eléctrico

A nivel electrónico el concepto es muy sencillo: 8 leds de alta luminosidad protegidos con resistencias y conectados a 8 salidas de un CPLD.

Uno de los leds hace de estrella principal mientras que los 7 leds restantes servirán para hacer el efecto de la estela de la estrella principal. La secuencia de iluminación será la siguiente:

- E0 iluminado al 5%, resto apagado

- E1 iluminado al 10%, resto apagado

- E2 iluminado al 15%, resto apagado

- E3 iluminado al 20%, resto apagado

- E4 iluminado al 30%, resto apagado

- E5 iluminado al 40%, resto apagado

- E6 iluminado al 50%, resto apagado

- EP iluminado al 100% durante varias unidades de tiempo más, resto apagado

- Todo apagado durante varias unidades de tiempo

Diseño lógico y funcionamiento

A continuación una propuesta de diagrama de bloques sencillo:

Al circuito combinacional A tiene como entradas el valor del registro contador de 22 bits y el valor del registro de desplazamiento de 20 bits y como salida la entrada de selección del multiplexor del registro de desplazamiento de 20 bits:

registro contadorreg. desplaz.mux
x0valor 1
0xsalida desplazador izquierda
≠0≠0reg. desplaz. (mantener)


El registro contador se utiliza con dos propósitos:

- Como medida de unidad de tiempo: Cada vez que se desborda, se desplaza el registro de desplazamiento. A 50 MHz de frecuencia de reloj, tenemos una frecuencia de desplazamiento de ${50000000 \over {2^{22}}} = 11.92093 \: Hz$, es decir ${1 \over 11.92093} = 0.08389 \: seg$, aproximadamente una décima de segundo como unidad de tiempo.

- Como registro contador para el PWM de los leds de la estela: Como es un contador estándar de desbordamiento, se pueden usar los 10 bits menos significativos para generar una señal PWM, esto nos da una frecuencia de señal PWM de ${50000000 \over {2^{10}}} \approx 49 \: KHz$ que es una buena frecuencia para un led.

Al iniciarse el CPLD todos los bits del registro de desplazamiento estarán a 0, lo que provocará que el circuito combinacional A emita un 1 para que, en el primer ciclo de reloj, se cargue un 1 en el registro de desplazamiento. A partir de aquí el circuito combinacional A mantendrá el multiplexor en modo "copia" (manteniendo el valor del registro de desplazamiento) y sólo mandará a desplazar cuando el registro contador se desborde. El registro de desplazamiento tendrá un bit 1 moviéndose de izquierda a derecha a razón de un salto cada décima de segundo, cuando el bit llega al extremo izquierdo aparece de nuevo en el extremo derecho del registro (formalmente deberíamos llamar al registro de desplazamiento, registro de "rotación").

Los 5 bits menos significativos del registro de desplazamiento no se conectan a nada, lo que significa que durante unas 5 décimas de segundo (medio segundo aproximadamente) ninguna de las luces se enciende, cuando el 1 pasa al bit 5 del registro de desplazamiento, se pone a 1 la entrada inferior de la puerta AND que gobierna el led E0, que hace las veces de enable para la salida PWM con menor ciclo de trabajo (menor luminosidad). Cuando el 1 pasa al bit 6 del registro de desplazamiento, se pone a 1 la entrada inferior de la puerta AND que gobierna el led E1, que hace de enable para la salida PWM con ciclo de trabajo ligeramente superior (un poco más de limunosidad que el anterior) y así sucesivamente. A medida que el 1 va desplazándose a la izquierda (un salto por cada décima de segundo aproximadamente) se van iluminando los leds E0 a E6 de forma consecutiva y con ciclos de trabajo PWM crecientes (es decir, cantidad de luz creciente). Los últimos 8 bits del registro de desplazamiento (bits 19 al 12) están conectados a una puerta OR que gobierna la luz EP (estrella principal), esto hace que la estrella principal esté encendida unas 8 décimas de segundo, y luego vuelta a empezar.

Implementación

A continuación el código VHDL:
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity MaxIIBethlehemStar is
    port (
        ClkIn       : in std_logic;
        BoardLedOut : out std_logic;
        StarOut     : out std_logic;
        Wake1Out    : out std_logic;
        Wake2Out    : out std_logic;
        Wake3Out    : out std_logic;
        Wake4Out    : out std_logic;
        Wake5Out    : out std_logic;
        Wake6Out    : out std_logic;
        Wake7Out    : out std_logic
    );
end entity;

architecture A of MaxIIBethlehemStar is
    constant Intensity1PWM : integer := 100;   -- over 1024
    constant Intensity2PWM : integer := 83;
    constant Intensity3PWM : integer := 66;
    constant Intensity4PWM : integer := 50;
    constant Intensity5PWM : integer := 35;
    constant Intensity6PWM : integer := 20;
    constant Intensity7PWM : integer := 10;
    signal TimerDBus : std_logic_vector(21 downto 0);
    signal TimerQBus : std_logic_vector(21 downto 0);
    signal ShiftDBus : std_logic_vector(19 downto 0);
    signal ShiftQBus : std_logic_vector(19 downto 0);
    signal Intensity1 : std_logic;
    signal Intensity2 : std_logic;
    signal Intensity3 : std_logic;
    signal Intensity4 : std_logic;
    signal Intensity5 : std_logic;
    signal Intensity6 : std_logic;
    signal Intensity7 : std_logic;
begin
    -- intensity signals (using timer lower 10 bits as PWM counter)
    Intensity1 <= '0' when (unsigned(TimerQBus(9 downto 0)) > to_unsigned(Intensity1PWM, 10)) else
                  '1';
    Intensity2 <= '0' when (unsigned(TimerQBus(9 downto 0)) > to_unsigned(Intensity2PWM, 10)) else
                  '1';
    Intensity3 <= '0' when (unsigned(TimerQBus(9 downto 0)) > to_unsigned(Intensity3PWM, 10)) else
                  '1';
    Intensity4 <= '0' when (unsigned(TimerQBus(9 downto 0)) > to_unsigned(Intensity4PWM, 10)) else
                  '1';
    Intensity5 <= '0' when (unsigned(TimerQBus(9 downto 0)) > to_unsigned(Intensity5PWM, 10)) else
                  '1';
    Intensity6 <= '0' when (unsigned(TimerQBus(9 downto 0)) > to_unsigned(Intensity6PWM, 10)) else
                  '1';
    Intensity7 <= '0' when (unsigned(TimerQBus(9 downto 0)) > to_unsigned(Intensity7PWM, 10)) else
                  '1';

    -- timer counter
    process (ClkIn)
    begin
        if (ClkIn'event and (ClkIn = '1')) then
            TimerQBus <= TimerDBus;
        end if;
    end process;

    TimerDBus <= std_logic_vector(unsigned(TimerQBus) + to_unsigned(1, 22));

    -- shift register
    process (ClkIn)
    begin
        if (ClkIn'event and (ClkIn = '1')) then
            ShiftQBus <= ShiftDBus;
        end if;
    end process;

    ShiftDBus <= std_logic_vector(to_unsigned(1, 20)) when (unsigned(ShiftQBus) = 0) else
                 ShiftQBus(18 downto 0) & ShiftQBus(19) when (unsigned(TimerQBus) = 0) else
                     ShiftQBus;

    -- outputs
    StarOut <= ShiftQBus(19) or ShiftQBus(18) or ShiftQBus(17) or ShiftQBus(16) or ShiftQBus(15) or ShiftQBus(14) or ShiftQBus(13) or ShiftQBus(12);
    Wake1Out <= Intensity1 and ShiftQBus(11);
    Wake2Out <= Intensity2 and ShiftQBus(10);
    Wake3Out <= Intensity3 and ShiftQBus(9);
    Wake4Out <= Intensity4 and ShiftQBus(8);
    Wake5Out <= Intensity5 and ShiftQBus(7);
    Wake6Out <= Intensity6 and ShiftQBus(6);
    Wake7Out <= Intensity7 and ShiftQBus(5);

    -- debug
    BoardLedOut <= (ShiftQBus(19) or ShiftQBus(18) or ShiftQBus(17) or ShiftQBus(16) or ShiftQBus(15) or ShiftQBus(14) or ShiftQBus(13) or ShiftQBus(12)) or
                   (Intensity1 and ShiftQBus(11)) or
                   (Intensity2 and ShiftQBus(10)) or
                   (Intensity3 and ShiftQBus(9)) or
                   (Intensity4 and ShiftQBus(8)) or
                   (Intensity5 and ShiftQBus(7)) or
                   (Intensity6 and ShiftQBus(6)) or
                   (Intensity7 and ShiftQBus(5));
end architecture;

El montaje se ha implementado sobre un CPLD MAX II de Altera, usando leds blancos de alta luminosidad y montándolos luego sobre una base de cartón duro con la típica forma de estrella de navidad.





El código fuente está disponible en la sección soft. ¡Feliz Navidad y feliz 2021!

[ añadir comentario ] ( 801 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 2.9 / 692 )
Implementación sobre una FPGA de un transmisor de radio en la banda comercial de FM 
En anteriores entradas se realizó la implementación, tanto de un transmisor FM como de un conversor analógico digital delta-sigma, sobre FPGA y como proyectos separados. A lo largo de esta entrada se aborda el desarrollo de un transmisor FM en la banda de la radio comercial para transmitir música, combinando ambos proyectos en uno solo.

Conversión analógica digital delta-sigma

Como se puede comprobar en el post anterior relacionado, la conversión analógica digital de tipo delta-sigma tiene la ventaja de requerir muy pocos componentes externos: un comparador analógico (para lo que puede usarse un amplificador operacional normal), un condensador y una resistencia; y permite implementar un ADC de precisión arbitraria (la precisión sólo está limitada por los recursos disponibles en la FPGA).



Recordemos que la conversión analógico digital de tipo delta-sigma lo que hace es, mediante un biestable de tipo D, hacer que la salida que carga el condensador C a través de la resistencia R "siga" a la entrada analógica:


- Si la entrada + del comparador está por encima de la entrada -, el comparador emite un 1, que es cargado en el siguiente ciclo de reloj por el biestable de entrada, haciendo que la salida que va a la resistencia R tome el valor de 1 y trate de "acercar" el valor de la entrada - del comparador al valor de la entrada + del comparador.

- Si la entrada + del comparador está por debajo de la entrada -, el comparador emite un 0, que es cargado en el siguiente ciclo de reloj por el biestable de entrada, haciendo que la salida que va a la resistencia R tome el valor de 0 y trate de "acercar" (esta vez hacia "abajo") el valor de la entrada - del comparador al valor de la entrada + del comparador.


Como se puede ver, este comportamiento hace que para valores próximos a 0 voltios en la entrada + del comparador, el biestable de entrada emite muchos más 0s que 1s, mientras que para valores próximos a 3.3 voltios en la entrada + del comparador, el biestable de entrada emite muchos más 1s que 0s. También se puede ver que para valores próximos a Vcc / 2 = 1.65 voltios, el biestable emitirá una cantidad aproximadamente igual de 0s que de 1s.

Si lo que hacemos ahora es asociar al valor 0 de la salida del biestable, el valor numérico -1, y, al valor 1 de la salida del biestable, el valor numérico +1, lo que tenemos es un circuito digital que emitirá:


- Más +1 que -1 para valores próximos a 3.3 voltios.

- Más -1 que +1 para valores próximos a 0 voltios.

- Una cantidad aproximadamente igual de -1 y +1 para valores próximos a 1.65 voltios.


Si vamos acumulando estos -1 y +1 sobre un acumulador que se pone a cero cada cierto tiempo, lo que tendremos es que en dicho acumulador tendremos un valor proporcional al voltaje de entrada del ADC:


- Valores próximos a 3.3 voltios, al generar más +1 que -1, dan como resultado un valor de conversión muy alto y positivo.

- Valores próximos a 0 voltios, al generar más -1 que +1, dan como resultado un valor de conversión muy bajo y negativo.

- Valores próximos a 1.65 voltios, al generar una cantidad aproximadamente similar de -1 y de +1, dan como resultado un valor de conversión próximo a 0.


Debido a esta necesidad de contar -1s y +1s necesitamos sobremuestreo: Si queremos obtener una resolución de 16 bits, eso significa que el valor máximo de conversión tendrá que ser -32767 mientras que el valor mínimo de conversión tendrá que ser -32768. Si asumimos una escala simétrica tendremos un valor entre -32767 y +32767, eso significa que debemos hacer 32767 sumas (de -1s y +1s) antes de leer el valor de conversión. Dichas sumas las tenemos que hacer en un registro, que llamaremos "acumulador de conversión".

Para el caso de 16 bits necesitaríamos un contador de 15 bits que, en el momento de que valga 0 haga que el "acumulador de conversión" copie su valor en el registro de salida del ADC y se ponga a cero, y, durante los 32767 pulsos restantes (hasta el siguiente desbordamiento), se sumen los -1s y +1s que van entrando.

Así vemos que, por ejemplo, para 12 MHz y 16 bits de resolución, como necesitaríamos un contador de 15 bits, al final la frecuencia de muestreo podrá ser, como máximo de:

$$ {12000000 \over {2^{15}}} = 366.21 \: Hz $$

Por ejemplo, si queremos hacer una conversión en calidad CD necesitaríamos un reloj de sistema (sobremuestreo) de:

$$ {44100 \times {2^{15}}} = 1445068800 \: Hz $$

En el caso que nos ocupa se ha decidido implementar el transmisor en una FPGA MAX10 de las que viene en una placa MAX1000 de Arrow, a 12 MHz, cuyo reloj puede ser subido mediante PLLs hasta unos 400 MHz. Debido a esta limitación se ha decidido subir el reloj a 300 MHz y, para trabajar con esa frecuencia de sobremuestreo, se han tenido que bajar un poco las especificaciones del ADC, usando un contador de 13 bits y un acumulador de conversión de 14 bits. De esta forma tenemos una frecuencia de muestreo de:

$$ {300000000 \over {2^{13}}} = 36621.09375 \: Hz $$

Por lo que el ancho de banda es de unos 18 KHz (buen ancho de banda para música) y la resolución de conversión es de 14 bits (valores de conversión entre -8191 y +8191). No es calidad CD pero tampoco está mal.

Consideraciones entorno a la entrada de sonido analógica

Hay que tener en cuenta que las señales de sonido tal cual salen de un amplificador, son señales simétricas (con semiciclos positivos y negativos) mientras que nuestro ADC mide voltajes entre 0 y 3.3 voltios (no mide voltajes negativos). Es necesario, por tanto, acondicional la señal de sonido de entrada para que quede "desplazada" hacia arriba y un valor de 0 voltios de entrada se traduzca en 1.65 voltios a la entrada + del comparador. Es por esto por lo que se coloca el divisor de tensión entre 3.3 y 0 voltios a la entrada de audio, que desplaza el "0" de la señal de sonido hasta los 1.65 voltios.



El nivel de señal que entrega la salida de auriculares de un ordenador es suficiente para la entrada del ADC y no requiere amplificación adicional, al menos para esta prueba.

Generador de señal de antena

La frecuencia de transmisión elegida es 87.5 MHz (el extremo inferior de la banda de radiodifusión de sonido). La señal de antena que se quiere generar debe estar centrada, por tanto, en dicha frecuencia y dicha frecuencia deberá variarse en un rango máximo de +-75 KHz (estándar de radiodifusión). Para la generación de frecuencias arbitrarias que sean inferiores a la frecuencia de reloj de un sistema digital lo lógico es utilizar un acumulador de fase.

Un acumulador de fase no es más que un registro que se incrementa en un valor constante (no tiene por qué incrementarse de 1 en 1) con desbordamiento. Por ejemplo, si a partir de un reloj de 300 MHz queremos generar un reloj de 75 MHz lo que podemos hacer es incrementar un registro de 2 bits de 1 en 1 a 300 MHz:

...
Pulso: 00 --> 01
Pulso: 01 --> 10
Pulso: 10 --> 11
Pulso: 11 --> 00
Pulso: 00 --> 01
Pulso: 01 --> 10
Pulso: 10 --> 11
Pulso: 11 --> 00
...

En este ejemplo se puede ver que si la frecuencia de pulso es de 300 MHz, la frecuencia del bit 0 será de ${300 \over 2} = 150 \: MHz$ mientras que la frecuencia del bit 1 será de ${150 \over 2} = 75 \: MHz$. Un acumulador de fase con incrementos potencia de 2 es, formalmente, un divisor de frecuencia. Si se aplica este mismo principio para registros con mayor cantidad de bits y usando incrementos arbitrarios, conseguimos frecuencias diferentes.

Asumamos que la señal de salida de nuestro "oscilador" será siempre el bit más significativo de un registro de 16 bits y nuestro reloj va a 300 MHz. Eso significará que, incrementando el registro de 1 en 1, el bit más significativo cambiará a razón de:

$$ {300000000 \over {2^N}} = 4577 \: Hz $$

Si en lugar de incrementar el registro de 1 en 1, lo incrementamos de 2 en 2, la cantidad de pulsos que tarde en desbordarse el registro será menor, por tanto, la frecuencia del bit más significativo será mayor, es decir, la frecuencia del bit más significativo es proporcional al valor de incremento del registro. El máximo valor de incremento será $2^{N-1}$ para N bits (es decir un 1 seguido de N-1 ceros) que hará que el registro se comporte de la siguiente manera:

...
Pulso: 0000000000000000 --> 1000000000000000
Pulso: 1000000000000000 --> 0000000000000000
Pulso: 0000000000000000 --> 1000000000000000
Pulso: 1000000000000000 --> 0000000000000000
...

En este caso extremo tenemos que la frecuencia del bit más significativo es de 150 MHz (para una frecuencia de pulso de 300 MHz). De forma genérica tenemos que el valor de incremento para una frecuencia dada puede calcularse de la siguiente forma:

$$ I = {{f_{deseada}} \over 300000000} \times 2^{N} $$

En nuestro caso, si queremos emitir a 87.5 MHz hay que usar un valor de N=54 bits para que el valor de incremento no sea fraccionario:

$$ I_{central} = {87500000 \over 300000000} \times 2^{54} = 5254199565265579 $$

Ese valor es el que habría que usar como valor de incremento en cada pulso de reloj para que en el bit más significativo del registro de 54 bits (bit 53) tengamos una señal a 87.5 MHz. Nótese que para valores de incremento que no sean potencias de 2, obtendremos señales no cuadradas o de fase algo irregular (en el anterior post dedicado a la transmisión FM se profundiza en este tema), pero para el caso que nos ocupa, la "calidad" de la señal resultante no es significativa, lo importante es que la frecuencia fundamental sea la correcta. Para hacer modulación en frecuencia hemos de modificar esta frecuencia en el rango de -75 KHz y +75 KHz, lo que nos genera los siguiente incrementos mínimos y máximos:

$$ I_{min} = {(87500000 - 75000) \over 300000000} \times 2^{54} = 5249695965638208 $$

$$ I_{max} = {(87500000 + 75000) \over 300000000} \times 2^{54} = 5258703164892949 $$

Si calculamos la diferencia entre los incrementos y la dividimos entre dos nos dará el valor 4503599627370.5 que es la amplitud máxima que deberá tener la señal del ADC para que modifique el valor del incremento y que este, a su vez, genere una variación máxima de +- 75 KHz en la frecuencia portadora.

Como la salida del ADC da valores entre -8191 y +8191 a este valor hay que multiplicarle el valor ${4503599627370.5 \over 8191} = 549822930$ para que valores próximos a -8191 en la salida del ADC generen una portadora de 87.5 MHz - 75 KHz = 87.425 MHz y valores próximos a +8191 en la salida del ADC generen una portadora de 87.5 MHz + 75 KHz = 87.575 MHz. El valor del incremento del acumulador de fase que genera la señal de la antena será el siguiente:

$$ I = I_{central} + (ADC \times 549822930) = 5254199565265579 + (ADC \times 549822930) $$

A continuación puede verse el código fuente completo (cabe en un único fichero VHDL).

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;


entity MAX10FMTransmitter is
    port (
        Clk              : in std_logic;
        AnalogComparator : in std_logic;
        PWM              : out std_logic;
        Antenna          : out std_logic;
        Led              : out std_logic_vector(7 downto 0)
    );
end entity;

architecture A of MAX10FMTransmitter is
    component PLL
        port (
            inclk0 : in std_logic := '0';
            c0        : out std_logic 
        );
    end component;
    signal Clk300 : std_logic;
    signal ADCFollowerD : std_logic;
    signal ADCFollowerQ : std_logic;
    signal ADCScalerD : std_logic_vector(12 downto 0);
    signal ADCScalerQ : std_logic_vector(12 downto 0);
    signal ADCAccumulatorD : std_logic_vector(13 downto 0);
    signal ADCAccumulatorQ : std_logic_vector(13 downto 0);   -- signed value: -8191 to +8191
    signal ADCOutputD : std_logic_vector(13 downto 0);
    signal ADCOutputQ : std_logic_vector(13 downto 0);
    signal PhaseAccumulatorD : std_logic_vector(53 downto 0);
    signal PhaseAccumulatorQ : std_logic_vector(53 downto 0);
    signal PhaseAccumulatorIncD : std_logic_vector(53 downto 0);
    signal PhaseAccumulatorIncQ : std_logic_vector(53 downto 0);
    -- increment for phase accumulator to transmit at 87.5 MHz: (87500000 / 300000000) * (2^54) = 5254199565265579
    --constant TxCentralFrequencyInc : integer := 5254199565265579;   -- must fit in 54 bits
    constant TxCentralFrequencyIncL : integer := 2863311531;   -- 32 bits
    constant TxCentralFrequencyIncH : integer := 1223338;      -- 22 bits
    constant TXCentralFrequencyInc : signed(53 downto 0) := to_signed(TxCentralFrequencyIncH, 22) & to_signed(TxCentralFrequencyIncL, 32);
    -- gain for ADC output 
    constant ADCGain : integer := 549822930;   -- ADCGain needs 30 bits
begin
    -- PLL to obtain 300 MHz from external 12 MHz
    P : PLL port map (
        inclk0 => Clk,
        c0 => Clk300
    );

    -- ADC
    process (Clk300)
    begin
        if (Clk300'event and (Clk300 = '1')) then
            ADCFollowerQ <= ADCFollowerD;
        end if;
    end process;
    
    ADCFollowerD <= AnalogComparator;
    PWM <= ADCFollowerQ;
    
    process (Clk300)
    begin
        if (Clk300'event and (Clk300 = '1')) then
            ADCScalerQ <= ADCScalerD;
        end if;
    end process;
    
    ADCScalerD <= std_logic_vector(unsigned(ADCScalerQ) + 1);

    process (Clk300)
    begin
        if (Clk300'event and (Clk300 = '1')) then
            ADCAccumulatorQ <= ADCAccumulatorD;
        end if;
    end process;

    ADCAccumulatorD <= std_logic_vector(to_signed(0, 14)) when (unsigned(ADCScalerQ) = 0) else
                       std_logic_vector(signed(ADCAccumulatorQ) + to_signed(1, 14)) when (ADCFollowerQ = '1') else
                       std_logic_vector(signed(ADCAccumulatorQ) - to_signed(1, 14)) when (ADCFollowerQ = '0') else
                       ADCAccumulatorQ;
    
    process (Clk300)
    begin
        if (Clk300'event and (Clk300 = '1')) then
            ADCOutputQ <= ADCOutputD;
        end if;
    end process;
    
    ADCOutputD <= ADCAccumulatorQ when (unsigned(ADCScalerQ) = 0) else
                  ADCOutputQ;
    Led <= ADCOutputQ(13 downto 6);
    
    -- phase accumulator
    process (Clk300)
    begin
        if (Clk300'event and (Clk300 = '1')) then
            PhaseAccumulatorQ <= PhaseAccumulatorD;
        end if;
    end process;
        
    PhaseAccumulatorD <= std_logic_vector(unsigned(PhaseAccumulatorQ) + unsigned(PhaseAccumulatorIncQ));
    Antenna <= PhaseAccumulatorQ(53);
    
    -- phase accumulator increment control (output frequency control)
    process (Clk300)
    begin
        if (Clk300'event and (Clk300 = '1')) then
            PhaseAccumulatorIncQ <= PhaseAccumulatorIncD;
        end if;
    end process;
    
    -- 14 bits * 40 bits = 54 bits
    PhaseAccumulatorIncD <= std_logic_vector((signed(ADCAccumulatorQ) * to_signed(ADCGain, 40)) + TxCentralFrequencyInc) when (unsigned(ADCScalerQ) = 0) else
                            PhaseAccumulatorIncQ;
end architecture;


El bit 53 del registro acumulador de fase se saca por un pin de la FPGA y en dicho pin se puede colocar un simple trozo de cable. No es necesario hacer ningún circuito que acondicione la señal de salida.



Si a corta distancia del circuito ponemos un receptor de radio FM comercial sintonizado a 87.5 MHz podremos escuchar la señal que está leyendo el ADC de la FPGA y que está siendo transmitida en FM.



Código fuente disponible en la sección soft.

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

<< | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Siguiente> >>