Desarrollo de una miniconsola de videojuegos portátil (1): diseño hardware 
A lo largo de 4 entradas consecutivas en el blog iré detallando todo el desarrollo y la implementación de una miniconsola de videojuegos portátil que he desarrollado para mi hijo. La idea era hacer una consola al estilo "maquinita" o "game & watch" pero algo más elaborada, alimentada con batería recargable y con algunos juegos prefijados. En esta entrada me centraré en el diseño hardware y el desarrollo de las librerías básicas para acceso al hardware.

Características principales

- Microcontrolador GD32VF103: núcleo RISC-V de 32 bits a 96 MHz, con 256 Kb de Flash y 32 Kb de SRAM.

- Pantalla: Módulo GMG12864 basado en el controlador de display ST7565 de 128x64 pixels en blanco y negro (sin escalas de grises, cada pixel encendido o apagado).

- Botonera: Cruceta (arriba, abajo, izquierda y derecha) más dos botones adicionales (A y B) de funcionalidad personalizable.

- Alimentación: Batería de una celda de LiPo o LiIon de 1200 mAh (3.7 voltios) para unas 6 horas de juego continuado. Recargable mediante módulo de controlador de carga con conector USB-C y con interruptor de encendido.



A continuación una foto del frontal de la consola (encendida aunque aún sin caja).



Y de la parte trasera, donde se puede ver todo el trabajo de soldadura (a muchos técnicos en electrónica seguramente les sangrarán los ojos, pero bueno, hice lo que pude, se me da mejor programar que soldar).



Pantalla

Aprovechando que el microcontrolador tiene una potencia razonable se opta por un modelo de pantalla con una capa de abstracción basada en framebuffer, de tal manera que los juegos de la consola escribirán en un framebuffer de lineal de $128 \times 64 = 8192$ bytes. Para encender o apagar el pixel (x, y) se escribirá un 1 o un 0, respectivamente, en el offset $\left( y \times 128 \right) + x$ del framebuffer:

frameBuffer[(y * 128) + x] = 1;     // encender pixel (x, y)
frameBuffer[(y * 128) + x] = 0;     // apagar pixel (x, y)

Habrá una clase encargada de traducir la información del framebuffer en transferencias SPI al módulo GMG12864 para que se pinte de forma adecuada la pantalla. Esta abstracción nos permite adaptarnos a pantallas futuras y no depender sólo de esa pantalla en concreto, además de que facilita el desarrollo y las pruebas como veremos más adelante.

Botonera

La botonera se implementa con 6 botones mecánicos con el común a masa. Las 6 entradas GPIO en el microcontrolador se configuraon como GPIOs en pullup y así nos ahorramos tener que poner resistencias de pull-up por fuera. Se opta por no poner circuitería antirrebote en los botones para abaratar costes: el antirrebote se realizará por software, mediante una máquina de estados que, con temporizadores, evitará que se produzcan rebotes en la acción de las teclas.

Alimentación

La alimentación es muy sencilla, se utiliza un módulo TP4056 para una celda LiPo o LiIon de 3.7 voltios que ya viene con conector USB-C para carga y salida estabilizada que puede ir directa a la entrada de 5 voltios del módulo del microcontrolador. Toda la consola requiere 3.3 voltios para funcionar pero, como el convertidor de voltaje de la placa del microcontrolador tiene un dropout muy bajo, se pueden meter los 3.7 voltios de salida de la controladora de carga por la entrada de 5 voltios de la placa del microcontrolador. El interruptor de alimentación se coloca en serie con la alimentación que llega al microcontrolador y a la pantalla de tal manera que, aunque el interruptor de la consola esté apagado, su batería se podrá cargar con un cargador estándar USB-C.

Entorno de desarrollo y clases básicas

Como otros desarrollos "grandes" que he hecho siempre intento que el proceso de desarrollo y de depuración sean lo más eficientes posibles y para ello trato siempre de aprovechar el uso de clases abstractas para abstraer el código del hardware específico o la plataforma en la que estoy trabajando. Por ejemplo, para el manejo de la pantalla habrá una clase "Display" que albergará el framebuffer y algunas funciones miembro auxiliares, a continuación se crea una carpeta "gd32vf103" donde irá la implementación específica para el microcontrolador y la pantalla utilizadas "SPIGMG12864Display" que heredará de "Display". Se crea también una carpeta "linux" donde va la implementación específica para Xlib ("XlibDisplay").

A continuación los diagramas de clases de las clases principales del código fuente:



En azul las clases específicas del microcontrolador, en verde las clases específicas de linux y en blanco las clases comunes.




En estos diagramas de clases se pueden ver las clases básicas que constituyen el "framework" de la miniconsola. El elemento central para entender cómo funciona el flujo del software es la clase "Screen" que representa una pantalla (título, menú, un juego en sí, etc.) y la clase "ScreenManager", encargada de ir cambiando de pantallas en función de las necesidades del flujo del programa.

Cada Screen debe implementar las funciones miembro:

void Screen::onLoad(InterScreenData *dataFromPreviousScreen): Esta función se invoca cuando se carga una pantalla, se supone que a partir de este momento el framebuffer es "suyo" por lo que lo lógico en esta función miembro es que se inicialicen variables, se borre el framebuffer, se pinten las partes fijas del mismo, se inicialice la mecánica de esta pantalla, etc.

Screen *Screen::onUpdate(): Esta función se ejecuta cada 20 ms por parte del timer del sistema para que se implementen la mecánica de la pantalla (menú, juego, etc.). Si devuelve *this o nullptr significará que no hay que cambiar de pantalla, en caso contrario significa que queremos cambiarnos a la pantalla correspondiente.

InterScreenData *Screen::onUnload(): Esta función miembro se ejecuta en caso de que la última llamada a "onUpdate()" haya devuelto una "Screen *" válida (no nullptr) y diferente a la actual. La idea es poner aquí código de "terminación" de nuestra pantalla. Un objeto de clase "Screen" puede ser cargado ("onLoad") y descargado ("onUnload") varias veces entre su construcción y su destrucción.

La clase "ScreenManager" heredará de la clase "Task" para implementar la función miembro run, donde se realizará la mecanica del onLoad/onUpdate/onUnload indicada:
class InterScreenData {
}; 
    
class Screen;

class Screen {
    public:
        virtual void onLoad(InterScreenData *dataFromPreviousScreen) = 0;
        virtual Screen *onUpdate() = 0;
        virtual InterScreenData *onUnload() = 0;
};

class ScreenManager : public Task {
    public:
        Screen *currentScreen;
        ScreenManager(Screen &initialScreen, InterScreenData *initialInterScreenData);
        virtual void run();
};

ScreenManager::ScreenManager(Screen &initialScreen, InterScreenData *initialInterScreenData) : currentScreen(&initialScreen) {
    this->currentScreen->onLoad(initialInterScreenData);
}   

void ScreenManager::run() {
    Screen *nextScreen = this->currentScreen->onUpdate();
    if ((nextScreen != this->currentScreen) && (nextScreen != nullptr)) {
        InterScreenData *isd = this->currentScreen->onUnload();
        nextScreen->onLoad(isd);
        this->currentScreen = nextScreen;
    }
}

Como se puede ver es una mecánica muy sencilla. A partir de la clase "Screen" se crean todas las pantallas de la aplicación, por ejemplo:

- SplashScreen: Pantalla de bienvenida con una imagen de fondo y un texto de copyright que espera a que pulses un botón para pasar a la siguiente pantalla.

- MenuScreen: Pantalla de menú que permite, a su vez, especializarse para crear diferentes menus (como MainMenuScreen).

- ...

Cualquier clase que herede de Screen y que implemente los tres métodos especificados será otro tipo de pantalla con la funcionalidad que queramos.

Esta forma de programar la aplicación es muy escalable y permite crear fácilmente flujos de código muy elaborados:
int main() {
    // init hardware
    interruptInit();
    RGBLed::init();
    GPIOButtons buttons;
    Random random(buttons);
    SPIGMG12864Display display;
    display.blank();
    // create screens
    InitialSplashScreen initialSplashScreen(display, buttons);
    MainMenuScreen mainMenuScreen(display, buttons);
    TetrisMainScreen tetrisMainScreen(display, buttons, random);
    TanksMainScreen tanksMainScreen(display, buttons, random);
    SnakeMainScreen snakeMainScreen(display, buttons, random);
    SnoopyMainScreen snoopyMainScreen(display, buttons, random);
    // link screens
    initialSplashScreen.setNextScreen(mainMenuScreen);
    mainMenuScreen.setBackScreen(initialSplashScreen);
    mainMenuScreen.setTetrisScreen(tetrisMainScreen);
    mainMenuScreen.setTanksScreen(tanksMainScreen);
    mainMenuScreen.setSnakeScreen(snakeMainScreen);
    mainMenuScreen.setSnoopyScreen(snoopyMainScreen);
    // main loop
    ScreenManager m(initialSplashScreen, nullptr);
    MyListener myListener(display, buttons, m);
    Timer::init(myListener, 20_msForTimer);
    while (true)
        asm volatile ("wfi");
}

No se usan variables globales (no son necesarias):

1. Se inicializa el hardware: el controlador de interrupciones, la pantalla, la botonera y el generador de números pseudoaleatorios.

2. Se construyen todas las pantallas: A todas les pasamos el objeto display y el objeto buttons (a algunas de ellas se les pasa el generador del números pseudoaleatorios).

3. Se enlazan las pantallas: Cada objeto de clase Screen debe tener los punteros a las pantallas hacia las que puede irse a partir de él. Por ejemplo, la pantalla initialSplashScreen debe saber que debe ir a la pantalla mainMenuScreen cuando pulsen un botón. De la misma forma la pantalla de menú, que debe saber a qué pantalla se salta con cada opción.

4. Justo antes del bucle principal: Se le indica al ScreenManager cual es pantalla inicial (la que debe aparecer en el arranque).

5. Bucle principal: En este caso, para ahorrar energía, no se hace el típico bucle "while (true)" sino que se programa el timer del sistema para dispararse cada 20 milisegundos y en la función miembro "timerExpired" del objeto escuchador del timer, se invoca la máquina el "run" de los botones y el "run" del ScreenManager, que es la función miembro encargada de gestionar las pantallas (llamar a onLoad/onUpdate/onUnload de las pantallas). Haciendo el bucle principal podemos utilizar la instrucción ensamblador "wfi" (wait for interrupt) para que, entre iteraciones, el procesador pueda dormirse y así evitar que se consuma mucha batería.



Siguiente entrega

En la siguiente entrega se analizará el diseño y la implementación del Tetris (uno de los cuatro juegos que incluye la miniconsola).



Todo el código y los diseños están en la sección soft.

[ añadir comentario ] ( 353 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente  |   ( 3 / 327 )
Programación bare metal del SoC Allwinner D1 
Al igual que se hizo en una anterior entrega con el SoC Allwinner H5, un ARM Cortex-A53 (64 bits), esta vez toca hacer prueba de concepto de bare metal con el SoC Allwinner D1, uno de los más utilizados en placas tipo SBC basadas en RISC-V. El D1 es un RISC-V de 64 bits que tiene un mecanismo de arranque muy parecido al usado por los otros SoCs de Allwinner. Programar a nivel bare metal un SoC de estas características tiene poco sentido práctico más allá de la experimentación, de hecho para aplicaciones serias siempre es recomendable utilizar un RTOS o un Linux, pero pelearse con las interioridades de un SoC a nivel bare metal permite aprender mucho sobre estos chips y sobre las secuencias de arranque de los procesadores en general.

MangoPi

En este caso, se utilizará una placa Mango Pi, con un tamaño muy parecido a la RPi Zero, pero que utiliza un SoC D1, aunque la prueba descrita en este post se podrá realizar en cualquier SBC que utilice un SoC D1.

Secuencia de arranque del D1

Se recomienda echar un vistazo al post anterior donde se aborda el mismo objetivo, pero con el SoC Allwinner H5 (un ARM Cortex-A53). Al igual que el SoC H5 y otros SoCs de Allwinner, lo que hace el procesador cuando arranca es, básicamente:

1.- Copiar el contenido de los primeros 32 Kbytes que empiezan en el sector 16 de la tarjeta de memoria en una zona de la memoria estática interna (más info).

2.- Comprobar el checksum del código, calculado en la cabecera de esos 32 Kbytes (más info).

3.- Si el checksum es correcto, ejecuta la primera instrucción de esos 32 Kbytes (que suele ser una instrucción de salto "jal", por lo que el código de arranque en sí normalmente se coloca justo después de la cabecera de esos 32 Kbytes).

La secuencia es igual a la utilizada en los SoC ARM de Allwinner (como el H5) con la única diferencia de que la instrucción de salto (los 4 primeros bytes de ese bloque) se codifican como la instrucción "jal" de RISC-V (es la misma instrucción tanto para RV32I como para RV64I).

A continuación puede verse el código máquina correspondiente a la instrucción "jal" (Jump And Link) de RISC-V que se coloca en los 4 primeros bytes de la cabecera:
bit                                                 bit
31 ......................... 12 +--rd---+ 6 5 4 3 2 1 0
     imm[20|10:1|11|19:12]      0 0 0 0 0 1 1 0 1 1 1 1
                                \-------/ \-----------/
                                    |           |
                                    |           +-------- opcode
                                    +-------------------- reg destino
                                                     (en r0 se guarda la
                                                       dirección de la
                                                     siguiente instrucción
                                                            a "jal")

"imm" es el offset al cual debe saltarse (en complemento a 2, puede ser un valor negativo). Nótese que el valor de "imm" es de 21 bits pero se descarta el bit menos significativo, puesto que el código en RISC-V siempre debe alojarse en direcciones pares de memoria.

Parchear la herramienta mksunxiboot

La herramienta mksunxiboot se encarga de calcular la cabecera con el correspondiente checksum para que el SoC arranque correctamente el código que queramos. Es un código en C muy sencillo que compilamos y ejecutamos en el ordenador, recibe como entrada un fichero ".bin" con el código de arranque y genera un ".bin" con el código de arranque precedido con la cabecera necesaria (con el checksum dentro) para que el SoC reconozca como "correcto" el código y lo ejecute.

La herramienta original está diseñada para SoCs ARM así que para adecuarla al D1 sólo hace falta cambiar la instrucción de salto para que, en lugar de ser una instrucción de salto ARM, sea una instrucción de salto RISC-V.

En "mksunxiboot.c" se sustituye el siguiente código:
    img.header.jump_instruction =	/* b instruction */
        0xEA000000 | /* jump to the first instruction after the header */
        (
            (sizeof(boot_file_head_t) / sizeof(int) - 2)
            & 0x00FFFFFF
        );

Por este otro:
    u32 code_offset = sizeof(boot_file_head_t);
    img.header.jump_instruction =	/* risc-v "jal" instruction */
        (((code_offset >> 20) & 1) << 31) |
        (((code_offset >> 1) & 0x3FF) << 21) |
        (((code_offset >> 11) & 1) << 20) |
        (((code_offset >> 12) & 0xFF) << 12) |
        0x00000006F;

Y ya está, compilando con:
gcc -o mksunxiboot mksunxiboot.c

Tenemos nuestra herramienta preparada para "firmar" nuestro código RISC-V para el D1.

SPL

En terminología Allwinner, el SPL (o Second Program Loader) es como se llama al código que se ejecuta justo después de la ROM del D1 (el que se le pasa a "mksunxiboot" para que lo firme) y que es el código que se pone en la tarjeta de memoria justo a continuación de la cabecera que calcula "mksunxiboot".

    ; sector 16 de la tarjeta de memoria
    jal @inicioCódigoSPL     ; primeros 4 bytes de la cabecera
    ... resto de la cabecera calculada por la utilidad "mksunxiboot" ...
@inicioCódigoSPL:
    ... nuestro código de arranque o SPL ...


Haremos un SPL extremadamente sencillo que se limite a hacer parpadear un pin GPIO al que se le conecta un LED.

#include <stdint.h>

void spl() __attribute__ ((naked, section(".spl")));

#define  WAIT       40000000ULL
#define  GPIO_BASE  0x02000000
#define  PC_CFG     *((uint32_t *) (GPIO_BASE + 0x0060))
#define  PC_DAT     *((uint32_t *) (GPIO_BASE + 0x0070))
#define  PC_PULL0   *((uint32_t *) (GPIO_BASE + 0x0084))

void spl() {
    PC_CFG = (PC_CFG & 0xFFFFFF0F) | 0x00000010;       // PC1 = output
    PC_PULL0 = (PC_PULL0 & 0xFFFFFFF3) | 0x00000008;   // PC1 = pull-down
    while (true) {
        PC_DAT |= 0x00000002;     // PC1 = 1
        for (uint64_t n = 0; n < WAIT; n++)
            ;
        PC_DAT &= 0xFFFFFFFD;     // PC1 = 0
        for (uint64_t n = 0; n < WAIT; n++)
            ;
    }
}

Como se puede comprobar, el código no hace uso de variables globales ni de llamadas a funciones o librerías, para que sea autocontenido y no genere dependencias externas. Algunos aspectos importantes:

- El nombre de la función es irrelevante, no tiene por qué ser "main". De hecho el código no se enlazará, sólo se compilará.

- Mediante atributos, indicamos al compilador que el código de la función debe alojarse en una sección llamada ".spl" (nombre arbitrario) y que debe ser "naked" (el compilador no generará código de preámbulo o postámbulo, ya que es una función a la que se llega con un salto, no con una llamada).

- El código simplemente inicializa la línea GPIO correspondiente al pin PC1 (el pin número 22 del conector de 40 pines de la Mango Pi), como un GPIO de salida y a continuación se queda en un bucle infinito emitiendo un 1 y un 0 alternativamente, para que pueda parpadear un led conectado a PC1.

Una vez compilado (generado el fichero "spl.o"), se utiliza la utilidad "objcopy" para extraer el código de la sección ".spl" y meterlo en un fichero binario crudo "spl.bin". A continuación, este fichero "spl.bin" es pasado por la utilidad "mksunxiboot" que modificamos anteriormente, para que genere otro "spl_with_signature.bin" que estará firmado y, por tanto, podrá ya ser ejecutado por el D1.
riscv64-none-elf-g++ -mtune=thead-c906 -fno-exceptions -fno-rtti -nostartfiles -c -o spl.o spl.cc
riscv64-none-elf-objcopy -O binary -j .spl spl.o spl.bin
./mksunxiboot spl.bin spl_with_signature.bin

El último paso consiste en grabar este fichero "spl_with_signature.bin" directamente en el sector 16 de una tarjeta de memoria (nótese que cada sector en una tarjeta de memoria mide 512 bytes, por tanto el sector 16 es el offset 8192 = 1024 * 8 a nivel de byte):
dd if=spl_with_signature.bin of=/dev/sdX bs=1024 seek=8

Si colocamos un LED entre PC1 y masa (usar siempre una resistencia en serie) y arrancamos el D1 con la tarjeta de memoria que acabamos de tostar, podremos ver nuestro blinker bare metal en acción.



Todo el código en la sección soft.

[ añadir comentario ] ( 930 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente  |   ( 2.8 / 438 )
Luces de Belén mediante un generador de números verdaderamente aleatorios 
Para la generación de números aleatorios en circuitos digitales existen, principalmente, dos opciones: por un lado el uso de LFSRs (registros de desplazamiento con realimentación lineal) de ciclo maximal, que son circuitos deterministas con los que se obtienen números pseudoaleatorios, y, por otro lado, el uso de algún parámetro físico que realmente tenga un comportamiento ruidoso y nos permita extraer números de él, algo que no sea determinista. La primera opción es la que he utilizado siempre hasta ahora pero con la excusa del montaje del Belén de este año, se propone la implementación de un generador de números "verdaderamente" aleatorios (TRNG) para generar el brillo del firmamento del Belén. El resultado visual es el de siempre, la diferencia es que la secuencia de brillos es realmente aleatoria.

Objetivo

La idea es la misma de años anteriores: generar destellos aleatorios en un firmamento artificial hecho de Leds blancos para el montaje del Belén. Debido a que la secuencia de destellos no tiene por qué ser "realmente" aleatoria, hasta ahora nos bastaba con un generador de números pseudoaleatorios implementado con un LFSR de ciclo maximal (un puñado de biestables y una función de transición basada en dos o tres puertas lógicas, poco más).

Los LFSRs son, por definición, deterministas, no existe ninguna "magia" en ellos, lo que sucede es que la secuencia de números que generan es lo suficientemente "extraña" y aparentemente no determinista, como para que, a ojos de un observador humano, parezca que realmente se están generando números totalmente aleatorios. En este post se hace una introducción y una explicación muy completa sobre el fundamento, uso e implementación de LFSRs en circuitos digitales.

Buscar una fuente de ruido

En nuestro caso, si queremos generar números verdaderamente aleatorios, debemos buscar una fuente de ruido físico que podamos traducir en una secuencia de números (o, al menos, en una secuencia aleatoria de 0s y 1s). Una primera aproximación electrónica podría ser implementar un ADC y leer mediante ese ADC alguna fuente de ruido electrónico analógico.

Una buena opción en este sentido es aprovechar la característica de ruido de que se produce de forma natural en los transistores de unión cuando se polariza la unión base-emisor de forma inversa, dejando el colector al aire.



Esta características de los transistores de unión es bien conocida y muy usada en generadores de ruido analógicos, ya que el ruido que genera se asemeja mucho al ruido blanco y no requiere una excesiva calibración.

En el caso de que necesitemos generar números aleatorios de esta manera debemos colocar un ADC para leer el ruido que genere el transistor y ya tendríamos nuestro TRNG. Esta aproximación es correcta pero engorrosa y costosa por las siguientes razones:

- Requiere de circuitería analógica: no siempre podremos disponer de ella y es algo más delicada siempre a la hora de diseñarla, calibrarla y testearla.

- Requiere de un ADC: Los ADCs son recursos caros, incluso aunque los implementemos en forma de delta-sigma, consumen recursos y siempre requieren de una parte de electrónica analógica que, como se comentó antes, requiere más calibración, testeo y cuidado en el diseño.

La pregunta que surge es ¿Habría posibilidad de encontrar una fuente de ruido dentro de un circuito digital de tal forma que nos permita implementar un TRNG directamente en una FPGA o en un ASIC? Lo cierto es que sí y no es nada complicado.

Oscilador en anillo

Un oscilador en anillo es un tipo especial de oscilador digital formado por un bucle cerrado de una cantidad impar de inversores.



Como se puede comprobar, al haber una cantidad impar de inversores, el circuito será aestable y la oscilación que se genera (en cualquiera de sus inversores) tendrá una frecuencia inversamente proporcional a la suma de los tiempos de propagación de todas las puertas inversoras. Hasta aquí parece un oscilador más, pero lo cierto es que, al ser un oscilador no sintonizado (no se utiliza un cristal de cuarzo para mantener una frecuencia estable, como los osciladores "reales" que se utilizan habitualmente en los circuitos digitales) la frecuencia y ciclo de trabajo dependerá del tiempo de propagación de las puertas lógicas y este tiempo de propagación nunca es fijo, dependerá de:

- La tecnología de fabricación.

- La distancia en el sustrato que haya entre la salida de una puerta y la entrada de la puerta siguiente.

- Las impurezas en el sustrato (siempre las hay).

- La temperatura.

- Radiación electromagnética externa que afecte al circuito.



Por eso los fabricantes de circuitos digitales nunca dan un tiempo exacto de propagación de puerta, dan un rango (para un rango determinado de temperatura y de condiciones determinadas). Es este jittering (desplazamiento de fase y/o de frecuencia) que se produce en estos osciladores lo que podemos aprovechar como fuente de ruido.

Extraer el ruido de jittering

Si definimos dos o más osciladores en anillo "teóricamente" iguales (por ejemplo con la misma cantidad de puertas y aunque compartan sustrato, en la misma FPGA o ASIC), lo cierto es que acabarán desfasándose unos con otros de forma aleatoria debido a las razones anteriormente citadas. En el paper An embedded true random number generator for FPGAs (Kohlbrenner P. y Gaj K., 2004) se introduce esta técnica y se plantea un circuito auxiliar muestrador que permite obtener una secuencia de bits muy próxima a una distribución uniforme (ruido blanco). Yo he simplificado mucho dicho muestreador: no genera una secuencia de distribución uniforme pero para el caso que nos ocupa cumple su cometido correctamente ya que lo único que hago es muestrear la función XOR entre la salida de ambos osciladores en anillo y meter el bit resultante en un registro de desplazamiento.



El código VHDL asociado sería el siguiente:

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

entity Max1000TRNG is
    port  (
        ClkIn : in std_logic;
        Led1Out : out std_logic;
        Led2Out : out std_logic;
        Led3Out : out std_logic;
        Led4Out : out std_logic;
        Led5Out : out std_logic;
        Led6Out : out std_logic;
        Led7Out : out std_logic;
        Led8Out : out std_logic;
        Star1Out : out std_logic;
        Star2Out : out std_logic;
        Star3Out : out std_logic;
        Star4Out : out std_logic;
        Star5Out : out std_logic;
        Star6Out : out std_logic;
        Star7Out : out std_logic;
        Star8Out : out std_logic
    );
end entity;

architecture A of Max1000TRNG is
    signal CounterD : std_logic_vector(23 downto 0);
    signal CounterQ : std_logic_vector(23 downto 0);
    signal FreeOsc1A : std_logic;
    signal FreeOsc1B : std_logic;
    signal FreeOsc1C : std_logic;
    signal FreeOsc1D : std_logic;
    signal FreeOsc1E : std_logic;
    signal FreeOsc2A : std_logic;
    signal FreeOsc2B : std_logic;
    signal FreeOsc2C : std_logic;
    signal FreeOsc2D : std_logic;
    signal FreeOsc2E : std_logic;
    attribute keep : boolean;
    attribute keep of FreeOsc1A : signal is true;
    attribute keep of FreeOsc1B : signal is true;
    attribute keep of FreeOsc1C : signal is true;
    attribute keep of FreeOsc1D : signal is true;
    attribute keep of FreeOsc1E : signal is true;
    attribute keep of FreeOsc2A : signal is true;
    attribute keep of FreeOsc2B : signal is true;
    attribute keep of FreeOsc2C : signal is true;
    attribute keep of FreeOsc2D : signal is true;
    attribute keep of FreeOsc2E : signal is true;
    signal RandomD : std_logic_vector(7 downto 0);
    signal RandomQ : std_logic_vector(7 downto 0);
    signal LatchD : std_logic_vector(7 downto 0);
    signal LatchQ : std_logic_vector(7 downto 0);
begin
    process (ClkIn)
    begin
        if (ClkIn'event and (ClkIn = '1')) then
            CounterQ <= CounterD;
            RandomQ <= RandomD;
            LatchQ <= LatchD;
        end if;
    end process;

    -- free oscillator 1
    FreeOsc1A <= not(FreeOsc1E);
    FreeOsc1B <= not(FreeOsc1A);
    FreeOsc1C <= not(FreeOsc1B);
    FreeOsc1D <= not(FreeOsc1C);
    FreeOsc1E <= not(FreeOsc1D);
    -- free oscillator 2
    FreeOsc2A <= not(FreeOsc2E);
    FreeOsc2B <= not(FreeOsc2A);
    FreeOsc2C <= not(FreeOsc2B);
    FreeOsc2D <= not(FreeOsc2C);
    FreeOsc2E <= not(FreeOsc2D);
    -- random bit generated from jittering between free oscillators
    RandomD <= RandomQ(6 downto 0) & (FreeOsc1A xor FreeOsc2A);
    -- latch the random number every time counter overflows
    CounterD <= std_logic_vector(unsigned(CounterQ) + 1);
    LatchD <= RandomQ when (CounterQ = std_logic_vector(to_unsigned(0, 24))) else
              LatchQ;
    Led1Out <= LatchQ(0);
    Led2Out <= LatchQ(1);
    Led3Out <= LatchQ(2);
    Led4Out <= LatchQ(3);
    Led5Out <= LatchQ(4);
    Led6Out <= LatchQ(5);
    Led7Out <= LatchQ(6);
    Led8Out <= LatchQ(7);
    Star1Out <= LatchQ(0);
    Star2Out <= LatchQ(1);
    Star3Out <= LatchQ(2);
    Star4Out <= LatchQ(3);
    Star5Out <= LatchQ(4);
    Star6Out <= LatchQ(5);
    Star7Out <= LatchQ(6);
    Star8Out <= LatchQ(7);
end architecture;

Nótese el uso de la palabra reservada "attribute" para definir un atributo booleano llamado "keep" asociado a las señales de los osciladores en anillo. Este atributo se utiliza para indicarle al entorno de desarrollo que no simplifique la función booleana y que mantenga ("keep") las señales indicadas, aunque al entorno le parezcan "desechables". Mediante este truco obligamos al entorno de desarrollo a implementar los osciladores en anillo con el número exacto de puertas que necesitamos.



Como siempre, el código fuente puede descargarse de la sección soft.

¡Feliz Navidad!

[ añadir comentario ] ( 2616 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente  |   ( 3 / 481 )
Implementación de un dispositivo USB sobre el microcontrolador CH552 
El CH552 es un microcontrolador de gama baja, basado en arquitectura de 8 bits, compatible con el 8051, con 1Kb de RAM y 16 Kb de memoria de programa. Incluye, además de los periféricos estándar del 8051 (timers, UART, etc.), un dispositivo USB full-speed. A lo largo de este post detallaremos cómo programar este microcontrolador con un ejemplo de implementación de un sencillo dispositivo USB.

CH552



Este microcontrolador está basado en un núcleo compatible MCS51 (8051) y, además de los periféricos estándar del 8051, dispone de una interfaz USB full-speed (12 Mbps) que sirve, además. como interfaz para tostar el integrado. Para desarrollar este pequeño proyecto utilizaremos una de las muchas placas disponibles en AliExpress que contienen este microcontrolador. Son placas muy baratas (muchas de menos de 2 ¤) y, para nuestro caso, sólo será necesaria una que incluya, como mínimo:

- El puerto USB (para conectar al ordenador).

- Un botón de bootloader (manteniendo pulsado este botón mientras se conecta el CH551 por USB al ordenador, hace que el micro entre en modo de programación y podamos tostarlo desde el ordenador).

- Un led conectado a cualquiera de sus pines GPIO.

El CH552 incluye dentro ya un reloj a 24 MHz calibrado y los PLLs para llegar a los 48 MHz que son necesarios para el subsistema USB del microcontrolador.

Compilador

Desgraciadamente no hay implementación oficial del backend del GCC (y, por tanto del g++) para la arquitectura 8051 por lo que nos limitaremos a programarlo en C usando el compilador SDCC. El SDCC podemos instalarlo de repositorio o compilarlo de forma sencilla:

$ ./configure --prefix=/opt/sdcc --disable-pic14-port --disable-pic16-port
$ make
$ make install

De esta forma tendremos en /opt/sdcc un compilador SDCC totalmente funcional. El SDCC soporta de forma nativa la arquitectura 8051 por lo que apenas serán necesarios algunos retoques en la línea de comandos para compilar nuestros programas para CH552.

Particularidades de la arquitectura MCS51

La arquitectura MCS51 (que es como se llama a la arquitectura compartida por todos los microcontroladores 8051 y compatibles, como el CH552) es una arquitectura un tanto peculiar. Se puede resumir en los siguientes puntos:

1. Arquitectura de tipo harvard a nivel funcional (instrucciones y datos en memorias separadas, aunque sólo a nivel funcional, internamente es von newmann).

2. Dos espacios de memoria RAM:

2.1. La IRAM que es siempre de 256 bytes divididos en 128 bytes de datos y 128 bytes de registros de función especial (SFR) para acceder a los periféricos.

2.2. La XRAM que es opcional y puede ser de hasta 64 KBytes. El acceso a la XRAM sólo se puede realizar mediante la instrucción "movx".

Es una arquitectura muy limitada pero que aún se sigue usando en microcontroladores de gama baja como el CH552.

Primera prueba de concepto: un blinker

A todos los efectos, el CH552 es un 8051 y, por tanto comparte con él todos los registros estándar de la familia MCS51, incluidos los registros de configuración de timer. Los registros relacionados con la configuración del reloj y GPIO sí son específicos del CH552. El proceso para hacer parpadear un led se puede resumir en:

* En la rutina de interrupción del timer 0:

- Incluimos la lógica de parpadeo del led.

* En función main():

- Configuramos el PLL del CH552 para que vaya a la frecuencia que necesitamos. Al arrancar, el CH552 va a 6 MHz pero podemos subir la frecuencia hasta 24 MHz como máximo.

- Configuramos el pin GPIO donde está conectado el led como pin de salida.

- Configuramos el timer 0 (puede ser cualquiera, yo elegí el 0).
Habilitamos las interrupciones.

A continuación puede verse el código fuente de lo que sería un sencillo blinker basado en el timer 0 del 8051.

#include <stdint.h>

__sfr __at (0xB0) P3;          // led at P3.2   (https://github.com/wuxx/nanoCH55x/blob/master/hardware/nanoCH55x-v1.1.pdf)
__sfr __at (0x96) P3_MOD_OC;
__sfr __at (0x97) P3_DIR_PU;
__sfr __at (0xB9) CLOCK_CFG;
__sfr __at (0x88) TCON;
__sfr __at (0x89) TMOD;
__sfr __at (0x8A) TL0;
__sfr __at (0x8C) TH0;
__sfr __at (0xA8) IE;
__sfr __at (0xA1) SAFE_MOD;
__sfr __at (0xC9) T2MOD;

uint16_t x;

void timer0Init() {
    const uint16_t TIMER_0_INIT_VALUE = 65535 - 2000;
    T2MOD &= 0xEF;                 // T0_CLK=0 --> freq = Fsys / 12 = 24 MHz / 2 = 2 MHz
    TMOD = (TMOD & 0xF0) | 0x01;   // mode 1 (16 bit timer), T0_GATE=0, T0_CT=0
    TH0 = (uint8_t) (TIMER_0_INIT_VALUE >> 8);
    TL0 = (uint8_t) (TIMER_0_INIT_VALUE & 0x00FF);
    TCON = (TCON & 0xCF) | 0x10;   // clear timer flag (TF0=0) and start timer (TR0=1)
}

void timerIsr() __interrupt (1) {   // Timer 0 ISR
    x++;
    if (x == 1000) {   // at 1 KHz, so each 1000 calls we toggle led to toggle it each second
        P3 ^= 0x04;    // switch P3.2
        x = 0;
    }
    timer0Init();
}

int main() {
    // set Fsys = 24 MHz
    SAFE_MOD = 0x55;
    SAFE_MOD = 0xAA;
    CLOCK_CFG = (CLOCK_CFG & 0xF8) | 0x06;
    SAFE_MOD = 0;
    // configure P3.2 as push-pull output pin
    P3_MOD_OC &= 0xFB;
    P3_DIR_PU |= 0x04;
    // configure timer 0 at Fsys / 12 = 2 MHz: T0_CT=0, T0_CLK=0, T0_GATE=0
    timer0Init();
    // enable timer 0 interrupts
    IE |= 0x80;   // global
    IE |= 0x02;   // timer 0
    x = 0;
    // sleep
    while (1)
        ;
}

Configuramos el timer 0 en modo 1 (timer de 16 bits sin recarga automática, esto es, hay que recargaro cada vez que queramos esperar un intervalo de tiempo). La función "timer0Init" se encarga de inicializar el timer 0 que, en el caso del CH552 se incrementa con una frecuencia de ${F_{sys} \over 12} = {24 \over 12} = 2\ MHz$. Si queremos que el timer 0 se desborde una vez por segundo, habría que cargar el valor 65535 - 2000000 en el contador, pero eso no es posible, pues se trata de un contador de 16 bits y, por tanto los valores de carga deben estar entre 0 y 65535. En este caso lo que se ha hecho es buscar una frecuencia de desbordamiento de 1 KHz, para lo cual se carga el timer 0 con el valor 65535 - 2000. Como se incrementa a 2 MHz, después de 2000 incrementos el contador se desbordará, con lo que tendremos un tiempo de desbordamiento de 1 / 1000 segundos. Si cada ${1 \over 1000}$ segundos se dispara la interrupción de timer 0 y en esa interrupción, a su vez, cambiamos el valor del bit del led cada 1000 llamadas (la variable x que se usa como contador en el código), conseguiremos que el led cambie de estado una vez por segundo.

# compilamos
$ /opt/sdcc/bin/sdcc -mmcs51 -c -o main.rel main.c

# enlazamos
$ /opt/sdcc/bin/sdcc -mmcs51 --xram-loc 0 --xram-size 1024 --code-loc 0 --code-size 14336 -o main.ihx main.rel

# convertimos el fichero hex a binario (objcopy es utilidad estándar en Linux)
$ objcopy -Iihex -Obinary main.ihx main.bin

# tostamos el binario en el CH552 (previamente insertado mientras pulsamos el botón de bootloader)
$ ch55xtool -f main.bin

El parámetro "-mmcs51" sirve para indicar que la arquitectura destino es 8051 y los parámetros adicionales que se pasan en la fase de enlazado sirven para indicar al compilador SDCC el tamaño y la disposición de la memoria en el CH552. "objcopy" es una utilidad estándar en sistemas Linux mientras que "ch55xtool" es una utilidad de Python (se instala con el "pip") encargada de tostar el integrado.

Una vez tostado el CH552 lo podemos desconectar del conector USB y volver a conectarlo (esta vez sin pulsar el botón de bootloader) para comprobar que el led cambia de estado una vez por segundo.

Protocolo USB

En este post, donde se realiza una implementación similar para un microcontrolador AVR (ATMega32u4), se explica de forma resumida el protocolo USB, al igual que en varios documentos disponibles de forma abierta en la web:

https://www.usbmadesimple.co.uk

https://www.beyondlogic.org/usbnutshell

https://www.engineersgarage.com/signal-and-encoding-of-usb-system-part-5-6

https://www.keil.com/pack/doc/mw/USB/html/_u_s_b__endpoints.html

En este caso se han implementado los mensajes más usuales y mínimos del protocolo USB:

- GET_DESCRIPTOR (para dispositivo y configuración)

- SET_ADDRESS

- SET_CONFIGURATION

- GET_STATUS

Para que un dispositivo sea 100% compatible USB debe implementar también otros mensajes adicionales, pero para que un Linux reconozca un dispositivo, es suficiente con que dicho dispositivo responda a los mensajes enumerados.

El objetivo será que el CH552 sea detectado como un dispositivo de caracteres básico (estilo puerto serie) por parte del kernel de Linux, que el kernel cree un dispositivo en /dev/loQueSea y que se pueda encender y apagar el led de la placa CH552 mediante comandos "echo" enviados a dicho dispositivo:

$ echo 0 > /dev/loquesea     # apagar el led
$ echo 1 > /dev/loquesea     # encender el led


Dispositivo USB

Definiremos un dispositivo USB muy básico con un descriptor de dispositivo, otro de configuración y dos endpoints: uno de entrada y otro de salida, ambos de tipo "bulk". El descriptor del dispositivo será el siguiente:

__code const deviceDescriptor myDeviceDescriptor = {
    18,       // bLength
    0x01,     // bDescriptorType   (device)
    0x0110,   // bcdUSB  (USB 1.1)
    0x00,     // bDeviceClass
    0x00,     // bDeviceSubClass
    0x00,     // bDeviceProtocol
    64,       // bMaxPacketSize
    0xF055,   // idVendor
    0x0001,   // idProduct
    0x0000,   // bcdDevice
    0x00,     // iManufacturer
    0x00,     // iProduct
    0x00,     // iSerialNumber
    0x01      // bNumConfigurations
};


Mientras que el descriptor de configuración completo (incluyendo los descriptores de interfaz y endpoints) será el siguiente:

__code const myConfigurationDescriptorType myConfigurationDescriptor = {
    {   // configuration descriptor
        9,       // bLength
        0x02,    // bDescriptorType
        sizeof(configurationDescriptor) + sizeof(interfaceDescriptor) + sizeof(endpointDescriptor) + sizeof(endpointDescriptor),   // wTotalLength
        1,       // bNumInterfaces
        1,       // bConfigurationValue
        0,       // iConfiguration
        0x80,    // bmAttributes
        5        // bMaxPower  (5 * 2 = 10 mA)
    },
    {   // interface descriptor
        9,       // bLength;
        0x04,    // bDescriptorType
        0,       // bInterfaceNumber
        0,       // bAlternateSetting
        2,       // bNumEndpoints
        0xFF,    // bInterfaceClass (vendor defined)
        0xFF,    // bInterfaceSubClass (vendor defined)
        0xFF,    // bInterfaceProtocol (vendor defined)
        0        // iInterface
    },
    {   // endpoint descriptor (ep1 in)
        7,       // bLength
        0x05,    // bDescriptorType
        0x01,    // bEndpointAddress
        0x02,    // bmAttributes
        64,      // wMaxPacketSize
        10       // bInterval
    },
    {   // endpoint descriptor (ep1 out)
        7,       // bLength
        0x05,    // bDescriptorType
        0x81,    // bEndpointAddress
        0x02,    // bmAttributes
        64,      // wMaxPacketSize
        10       // bInterval
    }
};


Usaremos los primeros 64 bytes de la XRAM para las transferencias del endpoint 0 (el endpoint de control) y los 64 bytes siguientes para las transferencias del endpoint 1, que será bidireccional y definimos todos los endpoints con un tamaño máximo de transferencia de 64 bytes. Nótese que, aunque haya dos descriptores de endpoint en el descriptor de configuración, estos dos descriptores hacen referencia a un mismo endpoint 1, pero en sentidos opuestos. En este caso tenemos dos descriptores y un único endpoint que es bidireccional.

La secuencia de acciones mínima que hacen los sistemas operativos cuando detectan que hay un dispositivo nuevo conectado al bus USB es la siguiente:

1. El host envía a la dirección 0 del bus una petición de GET_DESCRIPTOR(dispositivo) para que el dispositivo responda con su descriptor de dispositivo.

2. Una vez el host recibe el descriptor de dispositivo, comprueba que es válido y, si lo es, le asigna una dirección en el bus enviando una petición SET_ADDRESS al dispositivo para hacerle saber cual será su dirección en el bus en las siguientes peticiones.

3. A continuación el host vuelve a enviar una petición GET_DESCRIPTOR(dispositivo) pero esta vez a la nueva dirección de bus para comprobar que el dispositivo está ahí.

4. El host envía una petición GET_DESCRIPTOR(configuración) por cada configuración que haya (están indicadas en el descriptor del dispositivo y normalmente sólo hay una configuración, como en nuestro caso).

5. El host envía una petición SET_CONFIGURATION para indicar qué configuración se va a usar (por si hay más de una).

6. El host envía una petición GET_STATUS para preguntarle al dispositivo si está operativo.

7. A partir de este instante, el resto de endpoints deberán estar operativos para recibir tokens IN del host (enviar datos al host) o recibir tokens OUT del host (recibir datos del host).

Para configurar inicialmente el módulo USB del CH552:

- Deshabilitamos las interrupciones USB.

- Configuramos los endpoints: dirección de memoria en la XRAM donde se reciben y transmiten los paquetes y configuración del tipo de endpoint.

- Forzamos a que la dirección del dispositivo es la 0.

- Habilitamos la capa física (resistencias pull-up).

- Habilitamos el DMA que se encargará de escribir/leer en la XRAM cada paquetes que llegue/salga.

- Habilitamos las interrupciones: como mínimo para los eventos de reset USB y para los eventos de transferencia USB.

void usbInit() {
    IE_EX &= ~0x04;      // disable usb interrupts
    USB_CTRL = 0;        // device mode
    // endpoint 0 configuration
    UEP0_DMA_L = 0x00;   // configure ep0 buffer address at XRAM 0x0000
    UEP0_DMA_H = 0x00;
    UEP0_CTRL = 0x02;    // ep0 OUT transfer return ACK, IN transfer return NAK
    // endpoint 1 configuration
    UEP1_DMA_L = 0x40;   // configure ep1 buffer address at XRAM 0x0040
    UEP1_DMA_H = 0x00;
    UEP1_CTRL = 0x00;    // ep1 OUT transfer return ACK, OUT transfer return ACK
    UEP1_T_LEN = 0;      // ep1 IN transfers always tx 0 bytes
    UEP4_1_MOD = 0xC0;   // enable ep1 IN (tx) and OUT (rx)
    // rest of usb device configuration
    USB_DEV_AD = 0x00;   // initial device address = 0
    UDEV_CTRL = 0x80;    // disable internal DP/DM pull-down
    USB_CTRL = 0x29;     // UC_DEV_PU_EN=1, UC_INT_BUSY=1, UC_DMA_EN=1 (0b00101001)
    UDEV_CTRL |= 0x01 ;  // UD_PORT_EN=1 (0b00000001)
    USB_INT_FG = 0xFF;   // clear interrupt flags
    USB_INT_EN = 0x03;   // enable usb reset and transfer interrupts
    IE_EX |= 0x04;       // enable usb interrupts
    IE |= 0x80;          // enable global interrupts
    usbStatus = USB_STATUS_WAIT_SETUP;
}


A partir de este momento la rutina de interrupción del vector número 8 (el asociado a USB) se disparará cada vez que haya un reset en el bus USB o cada vez que haya una transferencia en cualquiera de los endpoints que hemos configurado:

Si es un evento de reset USB:
  Se pone la dirección del dispositivo a 0.
  Se configuran de nuevo los endpoints, por si acaso.
Si es un evento de transferencia USB terminada:
  Si es un token de SETUP y es el endpoint 0:
    Leemos el paquete de SETUP que estará en el buffer de recepción del endpoint 0.
    Si es una petición de tipo GET_DESCRIPTOR:
      Ponemos el descriptor que nos solicita el host en el buffer del endpoint 0.
      Nos preparamos para recibir un token IN.
    Si es una petición de tipo SET_ADDRESS:
      Guardamos en una variable la nueva dirección que tenemos que usar.
      Preparamos un paquete de respuesta de 0 bytes para el próximo token IN que llegue.
      Nos preparamos para recibir un token IN.
    Si es una petición de tipo SET_CONFIGURATION:
      Preparar próxima transferencia con token IN de 0 bytes a modo de ACK.
    Si es una petición de tipo GET_STATUS:
      Preparar próxima transferencia con token IN con 2 bytes a 0 para indicar status ok.
  Si es un token IN y es el endpoint 0:
    Si mandé un descriptor:
      Preparar próxima transferencia de tipo OUT a modo de ack desde el host.
    Si justo antes llegó un SET_ADDRESS:
      Ya se puede configurar la dirección USB del dispositivo con la que se almacenó en una variable.
      Preparar endpoint 0 de nuevo para que llegue otro token SETUP.
    Si justo antes llegó un SET_CONFIGURATION o un GET_STATUS:
      Preparar endpoint 0 de nuevo para que llegue otro token SETUP.
  Si es un token OUT y es el endpoint 0:
    Preparar endpoint 0 de nuevo para que llegue otro token SETUP.
  Si es un token IN y es el endpoint 1:
    Se habrán transferido 0 bytes desde el CH552 al host (PC). Nada que hacer.
  Si es un token OUT y es el endpoint 1:
    Encender o apagar el led en función del primer byte del buffer del endpoint 1.


Aquí puede verse el código fuente en C de este pseudocódigo:

void usbIsr() __interrupt (8) {    // USB ISR
    if (USB_INT_FG & 0x01) {
        // usb reset interrupt
        UEP0_CTRL = 0x02;    // ep0 OUT transfer return ACK expecting DATA0, IN transfer return NAK
        USB_DEV_AD = 0x00;   // initial device address = 0
        usbStatus = USB_STATUS_WAIT_SETUP;
    }
    else if (USB_INT_FG & 0x02) {
        // usb transfer interrupt
        uint8_t token = (USB_INT_ST >> 4) & 0x03;    // 0=out, 1=sof, 2=in, 3=setup
        uint8_t endpoint = (USB_INT_ST & 0x0F);
        if ((token == 3) && (endpoint == 0) && (USB_RX_LEN == 8)) {
            // SETUP token
            #define P ((setupPacket *) ep0Buffer)
            if ((P->bmRequestType == 0x80) && (P->bRequest == 0x06)) {
                // GET_DESCRIPTOR
                uint8_t descriptorType = P->wValue >> 8;
                uint8_t descriptorIndex = P->wValue & 0x00FF;
                if (descriptorType == 0x01) {
                    // GET_DESCRIPTOR (device)
                    descriptorPtr = (uint8_t *) &myDeviceDescriptor;
                    descriptorPtrToSend = (P->wLength < sizeof(deviceDescriptor)) ? P->wLength : sizeof(deviceDescriptor);
                    descriptorPtrNextToSend = descriptorPtrToSend;
                    memcpy(ep0Buffer, descriptorPtr, descriptorPtrNextToSend);
                    UEP0_T_LEN = descriptorPtrNextToSend;
                    UEP0_CTRL = 0xC0;       // UEP_R_TOG=1, UEP_T_TOG=1, UEP_R_RES=ack, UEP_T_RES=ack
                    usbStatus = USB_STATUS_WAIT_IN_AFTER_GET_DESCRIPTOR;
                }
                else if (descriptorType == 0x02) {
                    // GET_DESCRIPTOR (configuration)
                    descriptorPtr = (uint8_t *) &myConfigurationDescriptor;
                    descriptorPtrToSend = (P->wLength < sizeof(myConfigurationDescriptorType)) ? P->wLength : sizeof(myConfigurationDescriptorType);
                    descriptorPtrNextToSend = descriptorPtrToSend;
                    memcpy(ep0Buffer, descriptorPtr, descriptorPtrNextToSend);
                    UEP0_T_LEN = descriptorPtrNextToSend;
                    UEP0_CTRL = 0xC0;       // UEP_R_TOG=1, UEP_T_TOG=1, UEP_R_RES=ack, UEP_T_RES=ack
                    usbStatus = USB_STATUS_WAIT_IN_AFTER_GET_DESCRIPTOR;
                }
                else {
                    //
                }
            }
            else if ((P->bmRequestType == 0x00) && (P->bRequest == 0x05)) {
                // SET_ADDRESS
                addressToUse = (P->wValue & 0x00FF);
                // response with a DATA1 with 0 length
                UEP0_T_LEN = 0;
                UEP0_CTRL = 0x40;       // next IN transfer (response) with DATA1
                usbStatus = USB_STATUS_WAIT_IN_AFTER_SET_ADDRESS;
            }
            else if ((P->bmRequestType == 0x00) && (P->bRequest == 0x09)) {
                // SET_CONFIGURATION
                // response with a DATA1 with 0 length
                UEP0_T_LEN = 0;
                UEP0_CTRL = 0x40;       // next IN transfer (response) with DATA1
                usbStatus = USB_STATUS_WAIT_IN_AFTER_SET_CONFIGURATION;
            }
            else if (((P->bmRequestType & 0x80) == 0x80) && (P->bRequest == 0x00)) {
                // GET_STATUS
                ep0Buffer[0] = ep0Buffer[1] = 0;   // always return status ok
                UEP0_T_LEN = 2;                    // status word = 2 bytes
                UEP0_CTRL = 0xC0;       // UEP_R_TOG=1, UEP_T_TOG=1, UEP_R_RES=ack, UEP_T_RES=ack
                usbStatus = USB_STATUS_WAIT_IN_AFTER_GET_STATUS;
            }
        }
        else if ((token == 2) && (endpoint == 0)) {
            // IN token
            if (usbStatus == USB_STATUS_WAIT_IN_AFTER_GET_DESCRIPTOR) {
                descriptorPtrToSend -= descriptorPtrNextToSend;
                descriptorPtr += descriptorPtrNextToSend;
                if (descriptorPtrToSend > 0) {
                    descriptorPtrNextToSend = (descriptorPtrToSend > 8) ? 8 : descriptorPtrToSend;
                    memcpy(ep0Buffer, descriptorPtr, descriptorPtrNextToSend);
                    UEP0_T_LEN = descriptorPtrNextToSend;
                    UEP0_CTRL ^= 0x40;           // manual toggle UEP_T_TOG
                }
                else {
                    UEP0_CTRL = 0x80;        // expect OUT transfer with DATA 1 (status stage)
                    usbStatus = USB_STATUS_WAIT_OUT_AFTER_IN_AFTER_GET_DESCRIPTOR;
                }
            }
            else if (usbStatus == USB_STATUS_WAIT_IN_AFTER_SET_ADDRESS) {
                USB_DEV_AD = (USB_DEV_AD & 0x80) | addressToUse;
                UEP0_CTRL = 0x02;    // ep0 OUT transfer return ACK, IN transfer return NAK
                usbStatus = USB_STATUS_WAIT_SETUP;
            }
            else if (usbStatus == USB_STATUS_WAIT_IN_AFTER_SET_CONFIGURATION) {
                P3 |= 0x04;          // switch on led when device configured
                UEP0_CTRL = 0x02;    // ep0 OUT transfer return ACK, IN transfer return NAK
                usbStatus = USB_STATUS_WAIT_SETUP;
            }
            else if (usbStatus == USB_STATUS_WAIT_IN_AFTER_GET_STATUS) {
                UEP0_CTRL = 0x02;    // ep0 OUT transfer return ACK, IN transfer return NAK
                usbStatus = USB_STATUS_WAIT_SETUP;
            }
        }
        else if ((token == 0) && (endpoint == 0)) {
            // OUT token
            UEP0_CTRL = 0x02;    // ep0 OUT transfer return ACK, expect DATA0 (SETUP), IN transfer return NAK
            usbStatus = USB_STATUS_WAIT_SETUP;
        }
        // data in & out at endpoint 1
        else if ((token == 2) && (endpoint == 1)) {
            // IN token:  CH552 --> USB host (always tx 0 bytes)
            UEP1_CTRL ^= 0x40;           // manual toggle UEP_T_TOG
        }
        else if ((token == 0) && (endpoint == 1)) {
            // OUT token: USB host --> CH552 (if first byte is '0' then switch off led, else if '1' then switch on len)
            if (ep1Buffer[0] == '1')
                P3 |= 0x04;
            else if (ep1Buffer[0] == '0')
                P3 &= ~0x04;
            UEP1_CTRL ^= 0x80;           // manual toggle UEP_R_TOG
        }
    }
    USB_INT_FG = 0xFF;
}


Nótese que algunos detalles como el cambio entre paquetes de tipo DATA0 y DATA1 no están indicados en el pseudocódigo para facilitar la comprensión.

De esta forma la función main queda muy sencilla:

int main() {
    // set Fsys = 24 MHz
    SAFE_MOD = 0x55;
    SAFE_MOD = 0xAA;
    CLOCK_CFG = (CLOCK_CFG & 0xF8) | 0x06;
    SAFE_MOD = 0;
    // configure P3.2 as push-pull output pin
    P3_MOD_OC &= 0xFB;
    P3_DIR_PU |= 0x04;
    P3 = 0;    // turn off led
    // configure usb device
    usbInit();
    // sleep
    while (1)
        ;
}


El programa se compila y se tosta exactamente de la misma manera que se vió en la prueba de concepto (no tiene dependencias externas).

Funcionamiento

Una vez que el CH552 está programado lo desconectamos del conector USB que se usó para tostarlo, a continuación podemos irnos a una consola de root y hacer un

$ journalctl -f


Para empezar a trazar todos los eventos del kernel. Si en ese momento enchufamos el CH552 por USB (sin pulsar el botón de bootloader) podremos ver un mensaje de log del kernel parecido a este:

...
feb 02 18:57:33 avelino kernel: usb 3-2: new full-speed USB device number 2 using xhci_hcd
feb 02 18:57:33 avelino kernel: usb 3-2: New USB device found, idVendor=f055, idProduct=0001, bcdDevice= 0.00
feb 03 18:57:33 avelino kernel: usb 3-2: New USB device strings: Mfr=0, Product=0, SerialNumber=0
...


Lo que significará que el Kernel ha reconocido correctamente el dispositivo y lo ha enumerado. De hecho podemos hacer:

$ lsusb
...
Bus 003 Device 002: ID f055:0001  
...


Y lo veremos junto con el resto de dispositivos USB que tengamos conectados en nuestro ordenador. Si hacemos un "lsusb -v -d f055:0001" nos dará incluso la información de los descriptores de dispositivo y de configuración.

Hasta aquí bien, pero aún no tenemos un driver de dispositivos en el kernel que sea capaz de manejar nuestro nuevo dispositivo inventado y que nos permita encender y apagar el led desde un programa de usuario (haciendo "echo 1 > /dev/loquesea" por ejemplo). En este sentido podemos usar el módulo de kernel "usbserial". Si, cuando cargamos este módulo en el kernel, le pasamos los parámetros "vendor=VENDORID product=PRODUCTID" (en nuestro caso "vendor=0xf055 product=0x0001") haremos que, si se detecta un dispositivo con esos vendor y product el módulo "usbserial" creará un /dev/ttyUSBn por cada par de endpoints IN/OUT que tenga el dispositivo. Por tanto en nuestro caso nos bastará con hacer un:

$ modprobe usbserial vendor=0xf055 product=0x0001


Después de enchufar nuestro nuevo dispositivo. Haciendo esto, el módulo "usbserial" creará un dispositivo en /dev/ttyUSB0 vinculado a los dos enpoints (ep1 IN y ep1 OUT indicados en el descriptor de configuración) de tal manera que escribiendo en /dev/ttyUSB0 se enviarán tokens OUT al endpoint 1 del CH552 para escribir datos en él, mientras que leyendo de /dev/ttyUSB0 se enviarán tokens IN al endpoint 1 del CH552 para leer datos de él. Tras esto ya podremos apagar y encender el led el CH552 con los comandos:

$ echo 1 > /dev/ttyUSB0     # encender
$ echo 0 > /dev/ttyUSB0     # apagar





ACTUALIZACIÓN:


Implementación de un dispositivo de tipo HID

La solución descrita hasta ahora requiere, para su uso, que podamos insertar un driver en el kernel ("usbserial" o similar) que conozca la configuración de nuestro dispositivo o, en su defecto, utilizar "libusb" para que las aplicaciones de usuario puedan enviar y recibir datos del dispositivo. Existe, sin embargo, una alternativa que permitiría el uso del dispositivo desde el espacio de usuario sin necesidad de la instalación de ningún driver: configurar nuestro dispositivo como un dispositivo HID.

HID (Human Interface Device) son una clase de dispositivos dentro del estándar USB utilizados por teclados, ratones, joysticks, touch pads, etc. Un HID USB se diferencia con el dispositivo "plano" que teníamos hasta ahora en los siguientes aspectos:

- El descriptor de configuración indica en el cambo bInterfaceClass el valor 0x03 para indicar que es HID.

- Dentro de la estructura del descriptor de configuración se mete un descriptor HID entre el descriptor de interfaz y el primer descriptor de endpoint.

- Debe incluir al menos un descriptor de endpoint IN de tipo interrupt. En nuestro caso pondremos dos endpoints de tipo interrupt: uno IN y otro OUT (para encender y apagar el led).

- Debemos definir un descriptor "HID report". Dicho descriptor permite definir el formato de los datos que se enviarán y/o recibirán por los endpoints (por ejemplo, los teclados definen un descriptor de HID report donde indican el formato de los bytes que se envían al ordenador con cada pulsación).

- Debemos hacer que el dispositivo responda adecuadamente a la petición GET_DESCRIPTOR(hid report), que es una petición que envía en ordenador, cuando ve que el dispositivo es HID, con el propósito de que el propio dispositivo envíe su descriptor de "HID report". Nótese que una cosa es el descriptor HID (que se incluye dentro del descriptor de configuración) y otra cosa es el descriptor de HID report, que no se incluye en el descriptor de configuración, ya que el ordenador lo solicita mediante la petición GET_DESCRIPTOR(hid report) específica.

La clase de dispositivos HID de USB está muy bien explicada en la documentación oficial, aunque, como el resto de documentos, es algo densa y aquí tratamos de explicarlo de forma sencilla para el caso que nos ocupa. A continuación definimos el nuevo descriptor de configuración para nuestro dispositivo USB HID (se ha resaltado el descriptor HID):

__code const myConfigurationDescriptorType myConfigurationDescriptor = {
    {   // configuration descriptor
        9,       // bLength
        0x02,    // bDescriptorType
        sizeof(configurationDescriptor) + sizeof(interfaceDescriptor) + sizeof(hidDescriptor) + sizeof(endpointDescriptor) + sizeof(endpointDescriptor),   // wTotalLength
        1,       // bNumInterfaces
        1,       // bConfigurationValue
        0,       // iConfiguration
        0x80,    // bmAttributes
        5        // bMaxPower  (5 * 2 = 10 mA)
    },
    {   // interface descriptor
        9,       // bLength
        0x04,    // bDescriptorType
        0,       // bInterfaceNumber
        0,       // bAlternateSetting
        2,       // bNumEndpoints
        0x03,    // bInterfaceClass (HID)
        0x00,    // bInterfaceSubClass (none)
        0x00,    // bInterfaceProtocol (none)
        0        // iInterface
    },
    {   // hid descriptor
        9,       // bLength
        0x21,    // bDescriptorType
        0x0101,  // bcdHID
        0,       // bCountryCode
        1,       // bNumDescriptors
        0x22,    // bReportDescriptorType (report)
        sizeof(hidReportDescriptor)   // wReportDescriptorLength
    },
    {   // endpoint descriptor (ep1 in)
        7,       // bLength
        0x05,    // bDescriptorType
        0x81,    // bEndpointAddress
        0x03,    // bmAttributes (interrupt endpoint)
        64,      // wMaxPacketSize
        10       // bInterval (max. ms between IN tokens)
    },
    {   // endpoint descriptor (ep1 out)
        7,       // bLength
        0x05,    // bDescriptorType
        0x01,    // bEndpointAddress
        0x03,    // bmAttributes (interrupt endpoint)
        64,      // wMaxPacketSize
        10       // bInterval (max. ms between OUT tokens)
    }
};


Como se puede ver se ha cambiado el campo bInterfaceClass para indicar que es un dispositivo HID, se ha incluido el descriptor HID y se han redefinido los endpoints para que sean ambos de tipo interrupt. El campo bReportDescriptorType del descriptor HID indica que hay definido, además, un descriptor de "HID report" con una longitud de wReportDescriptorLength bytes. Este descriptor de "HID report" será solicitado con posterioridad por el ordenador a través de una petición GET_DESCRIPTOR(hid report):

__code const uint8_t hidReportDescriptor[] = {     // hid report descriptor for a raw 64 byte rx/tx pipe
    0x06, 0xAB, 0xFF,    // usage_page 0xFFAB
    0x0A, 0x00, 0x02,    // usage 0x0200
    0xA1, 0x01,          // collection 0x01
    0x75, 0x08,          // report size = 8 bits
    0x15, 0x00,          // logical minimum = 0
    0x26, 0xFF, 0x00,    // logical maximum = 255
    0x95, 64,            // report count = 64 bytes
    0x09, 0x01,          // usage
    0x81, 0x02,          // Input (array)
    0x95, 64,            // report count = 64 bytes
    0x09, 0x02,          // usage
    0x91, 0x02,          // Output (array)
    0xC0                 // end collection
};


Los descriptores de HID report son complejos de diseñar y para tal menester se encuentran disponibles aplicaciones de ayuda (por ejemplo, la utilidad oficial de la organización USB Implementers Forum, que se puede ejecutar en Linux con wine) y varios tutoriales (recomiendo este). Para nuestro caso particular sólo necesitamos definir un HID "vendor specific" que permita enviar y recibir paquetes crudos de 64 bytes cada uno.

Una vez que el host (ordenador) detecta que es un dispositivo USB HID y obtiene sus descriptores, empieza a enviar (con la periodicidad en milisegundos indicada en los campos bInterval de los diferentes endpoints de tipo interrupt) tokens IN y OUT para recibir y enviar (respectivamente) datos desde y hasta el dispositivo.

A pesar de que, a priori puede resultar más engorrosa esta nueva "capa" HID sobre la primera aproximación, el definir un dispositivo como HID tienen un gran ventaja: Es posible enviar y recibir paquetes HID (que se envían y reciben por los endpoints interrupt) sin necesidad de ningún driver específico, ni en Windows ni en Linux (por ejemplo, en Linux el kernel crea un dispositivo /dev/hidrawN, cuando un proceso escribe en ese dispositivo, se envían tokens OUT por el endpoint OUT interrupt para escribir los datos, mientras que cuando un proceso intenta leer de ese dispositivo, se envían tokens IN por el endpoint IN interrupt para tratar de leer datos).

Prueba del HID

Cuando conectamos el CH552 configurado como USB HID vemos que el kernel lo ha detectado como un dispositivo USB HID:

...
feb 05 13:53:18 avelino kernel: usb 3-2: new full-speed USB device number 18 using xhci_hcd
feb 05 13:53:18 avelino kernel: usb 3-2: New USB device found, idVendor=f055, idProduct=0001, bcdDevice= 0.00
feb 05 13:53:18 avelino kernel: usb 3-2: New USB device strings: Mfr=0, Product=0, SerialNumber=0
feb 05 13:53:23 avelino kernel: hid-generic 0003:F055:0001.0008: hiddev0,hidraw3: USB HID v1.01 Device [HID f055:0001] on usb-0000:00:14.0-2/input0
...


Creándose automáticamente el dispositivo (en este caso el "/dev/hidraw3") que puede ser escrito desde la shell para encender y apagar el led, sin necesidad de ningún driver:

$ echo 1 > /dev/hidraw3    # encender
$ echo 0 > /dev/hidraw3    # apagar


Todo el software puede descargarse de la sección soft.

[ 1 comentario ] ( 1710 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente  |   ( 3 / 829 )
Módulo de movimiento lento para el belén con motor paso a paso en C++ 
Es Navidad y toca montaje electrónico para el Belén. Este año trataremos de hacer un generador de movimiento lento que permita simular el paso de los tres reyes magos y hacer que estos transiten desde un extremo del belén hasta el pesebre moviéndose de forma lenta y autónoma a lo largo de los días.

Objetivo

La idea es que los tres reyes magos (tres figuras que van a lomos de sus respectivos camellos) se vayan desplazando lentamente desde un extremo del montaje del belén hasta el otro extremo (donde está situado el pesebre) a lo largo de los días y de forma imperceptible. De esta forma cada día estarán más cerca de su "destino".



Descripción funcional

Se plantea un montaje basado en un microcontrolador (en este caso un RISC-V), tres botones ("modo", "avance" y "retroceso"), un display de 7 segmentos y un motor paso a paso de 4 fases con reductora (4076 pasos por vuelta). El módulo funcionará en 3 modos entre los que se alternará pulsando uno de los botones:

- Modo de movimiento libre del motor paso a paso: en este modo se muestra la letra "c" en el display de 7 segmentos y usando los dos botones restantes se podrá mover rápidamente (aprox cinco vueltas por minuto) el motor paso a paso.

- Modo de configuración de días: En este modo se configurará la cantidad de días (en el display de 7 segmentos) que se desea que dure el movimiento completo del motor paso a paso.

- Modo de reposo (display de 7 segmentos apagado): Al entrar en este modo, el microcontrolador calcula, a partir de los pasos avanzados (o retrocedidos) en el modo 1 y a partir de los días configurados en el modo 2, la velocidad (lenta) a la que tendrá que moverse el motor paso a paso para que en el transcurso de esos días el motor vuelva a la posición de encendido. El montaje se quedaría en este modo hasta que el motor paso a paso llegue a su posición inicial (la que tenía cuando se encendió).

Cuando el montaje se enciende, el modo en el que arranca es el modo 1.

Un caso de uso típico sería el siguiente. El motor paso a paso se alojará en el pesebre, un hilo tendrá un extremo conectado al eje del motor (en el que se enrollará como si fuese el carrete de una caña de pescar) y el otro extremo de ese hilo estará pegado a una cartulina donde estarán alojados los reyes magos:

1. Encendemos el montaje (modo 1) y hacemos girar el motor paso a paso de tal forma que la cartulina con los reyes magos quede en lo que será el final de su recorrido (pegados al pesebre y con el hilo lo más enrollado posible al eje del motor).

2. Apagamos el módulo.

3. Volvemos a encender el módulo para que quede registrada la posición actual del motor como posición "reposo" o de "destino".

4. Con el módulo recién encendido (de nuevo modo 1), volvemos a mover el motor de forma rápida pero en el sentido contrario al dado en el paso 1, para que vaya soltando hilo mientras vamos tirando de los reyes magos (para mantener el hilo algo tenso) hasta que hayamos colocado los reyes magos en su posición inicial. Al estar en modo 1, el microcontrolador habrá estado contabilizando la cantidad de pasos que hemos tenido que hacer mover al motor para que los reyes magos lleguen a su posición inicial.

5. Pulsamos el botón de configuración para pasar al modo 2 y usamos los otros dos botones para definir la cantidad de días que queremos que tarden los reyes magos en llegar al pesebre.

6. Pulsamos de nuevo el botón de configuración para pasar el modo 3 (se apagará el display de 7 segmentos, aunque el led azul sigue parpadeando para que sepamos que el montaje sigue funcionando) y lo dejamos encendido así durante todos los días que dure el montaje. En este modo el motor paso a paso se irá moviendo poco a poco de tal manera que cuando haya pasado la cantidad de días configurada en el modo 2, los reyes magos hayan llegado al pesebre.

7. No se debe apagar el montaje ya que no se almacena el estado en ningún tipo de memoria no volátil. Si hay un corte de corriente en el montaje, hay que volver a configurarlo empezando por el paso 1.

Circuito



Como se puede ver se trata de un montaje muy sencillo que hace uso sólo de las características GPIO del microcontrolador y, por tanto, aunque está hecho con un RISC-V, el montaje es fácilmente portable a cualquier otro microcontrolador con la suficiente cantidad de pines GPIO: 3 de entrada y 11 de salida (7 para el display y 4 para el motor paso a paso).

- El teclado consta de 3 botones ("modo", "-" y "+") con circuito antirrebote básico.

- El display de 7 segmentos es uno sencillo de cátodo común.

- El motor paso a paso es un 28BYJ-48 de 4 fases, a 5 voltios y controlado por un chip ULN2003. El motor tiene una reductora que hace que requiera 4076 pasos para completar una vuelta.

Diseño del software

El software está desarrollado en C++ y el diagrama de clases es el siguiente:



Las clases están diseñadas para ser lo más independientes posible del hardware final en el que se ejecuten. De cara a la implementación específica para el GD32VF103, se definieron las siguientes clases:



"Heartbeat" es una clase que se encarga de hacer perpadear el led azul de la placa del GD32VF103 mientras que las clases My... son las encargadas de configurar y leer o escribir en los GPIO correspondientes a cada funcionalidad (teclado, 7 segmentos y motor paso a paso, respectivamente).

Por ejemplo, la clase "Stepper" es la encargada de mover el motor paso a paso. La velocidad está dada por el valor de la variable miembro "ticksPerStep", que indica la cantidad de veces que debe ejecutarse "run()" para avanzar en un paso el motor. Teniendo el cuenta que cada "run()" se ejecutan cada 10 milisegundos (100 veces por segundo), se puede calcular el valor de "ticksPerStep" a partir de la velocidad que queramos imprimirle al motor:

#include "Stepper.H"

using namespace avelino;
using namespace std;

void Stepper::init() {
    this->gpioConfigure();
    this->gpioWrite(0);
}

void Stepper::start() {
    this->doStart = true;
}

void Stepper::stop() {
    this->doStart = false;
}

void Stepper::run() {
    Status localStatus = this->status;
    do {
        this->status = localStatus;
        if (localStatus == Status::STOPPED) {
            if (this->doStart) {
                this->gpioWrite(this->lastMask);
                this->timer = this->ticksPerStep;
                //cout << "Stepper::run: STOPPED --> RUNNING_1, timer=" << this->timer << endl;
                localStatus = Status::RUNNING;
            }
        }
        else if (localStatus == Status::RUNNING) {
            this->timer--;
            int32_t threshold = (int32_t) this->ticksPerStep - (int32_t) this->getTicksPerPulse();
            if (this->timer < threshold)
                this->gpioWrite(0);
            if (this->timer <= 0) {
                this->lastMask = this->getNextMask(this->lastMask, this->wise);
                this->gpioWrite(this->lastMask);
                if (this->listener != NULL)
                    this->listener->stepDone(*this);
                this->timer = this->ticksPerStep;
                //cout << "Stepper::run: RUNNING_1 --> RUNNING_2, timer=" << this->timer << endl;
            }
            if (!this->doStart) {
                this->gpioWrite(0);
                //cout << "Stepper::run: RUNNING_1 --> STOPPED" << endl;
                localStatus = Status::STOPPED;
            }
        }
    } while (localStatus != this->status);
}

Como se puede ver, la clase "Stepper" se encarga de la gestión de los avances en los pasos del motor, mientras que la clase "MyStepper" se encarga de implementar las operaciones de bajo nivel relacionadas con el motor:

#include "MyStepper.H"

using namespace avelino;
using namespace std;

#define  RCU_APB2EN   *((uint32_t *) 0x40021018)
#define  GPIOA_CTL0   *((uint32_t *) 0x40010800)
#define  GPIOA_OCTL   *((uint32_t *) 0x4001080C)
#define  GPIOB_CTL0   *((uint32_t *) 0x40010C00)
#define  GPIOB_OCTL   *((uint32_t *) 0x40010C0C)

void MyStepper::gpioConfigure() const {
    // enable clock on ports A and B
    RCU_APB2EN = RCU_APB2EN | (((uint32_t) 3) << 2);
    // configure A5, A6 and A7 as push/pull output
    GPIOA_CTL0 = (GPIOA_CTL0 & 0x000FFFFF) | 0x22200000;
    // configure B0 as push/pull output
    GPIOB_CTL0 = (GPIOB_CTL0 & 0xFFFFFFF0) | 0x00000002;
}

void MyStepper::gpioWrite(uint32_t mask) const {
    if (mask & 1)
        GPIOA_OCTL |= (((uint32_t) 1) << 5);
    else
        GPIOA_OCTL &= ~(((uint32_t) 1) << 5);
    if (mask & 2)
        GPIOA_OCTL |= (((uint32_t) 1) << 6);
    else
        GPIOA_OCTL &= ~(((uint32_t) 1) << 6);
    if (mask & 4)
        GPIOA_OCTL |= (((uint32_t) 1) << 7);
    else
        GPIOA_OCTL &= ~(((uint32_t) 1) << 7);
    if (mask & 8)
        GPIOB_OCTL |= ((uint32_t) 1);
    else
        GPIOB_OCTL &= ~((uint32_t) 1);
}

uint32_t MyStepper::getNextMask(uint32_t prevMask, Wise wise) const {
    if (wise == Stepper::Wise::CLOCK)
        return ((prevMask << 1) | ((prevMask >> 3) & 1)) & 0x0F;
    else
        return ((prevMask >> 1) | ((prevMask & 1) << 3)) & 0x0F;
}

const uint32_t MyStepper::getStepsPerRevolution() const {
    return 4076;
}

const uint32_t MyStepper::getTicksPerPulse() const {
    return 20;   // 20 * 10 = 200 ms
}

Las tareas de MyStepper se limitan a:

- Calcular una máscara de paso a partir de la anterior.

- Configurar las salidas GPIO.

- Emitir una máscara a las salidas GPIO.

De esta manera se mantiene separado el código C++ independiente del hardware del código C++ dependiente del hardware. De hecho la subcarpeta "linux" implementa versiones simuladas de Stepper, Keypad y SevenSegments, que permiten depurar en un terminal de Linux el funcionamiento del módulo antes de tostarlo en el microcontrolador.

Los tres diferentes modos de funcionamiento del módulo están representados por clases que heredan de la clase "State": una clase por cada modo. La clase "ConfigureStepsState" es la clase cuyo "run()" se ejecuta mientras estamos en el modo 1, la clase "ConfigureDaysState" es la clase cuyo "run()" se ejecuta mientras estamos en el modo 2, mientras que la clase "IdleState" es la clase cuyo "run()" se ejecuta mientras estamos en el modo 3 o modo de reposo. La clase "StateManager" se encarga de gestionar la "salida" de un modo y la "entrada" en el siguiente modo.

#ifndef  __STATE_H__
#define  __STATE_H__

#include "Task.H"

extern "C++" {
    namespace avelino {
        using namespace std;

        template <typename T>
        class State : public Task {
            protected:
                T *data;
            public:
                virtual void onLoad(T &data) = 0;
                virtual void run() = 0;
                virtual State<T> &getNextState() = 0;
                virtual T &onUnload() = 0;
        };

        template <typename T>
        class StateManager : public Task {
            protected:
                State<T> *currentState;
            public:
                StateManager(State<T> &initialState, T &initialData) : currentState(&initialState) {
                    this->currentState->onLoad(initialData);
                };
                virtual void run() {
                    if (this->currentState != NULL) {
                        this->currentState->run();
                        State<T> &nextState = this->currentState->getNextState();
                        if (this->currentState != &nextState) {
                            T &data = this->currentState->onUnload();
                            this->currentState = &nextState;
                            this->currentState->onLoad(data);
                        }
                    }
                };
        };
    }
}

#endif  // __STATE_H__

Al final, lo que se hace en "main.cc" es configurar el CLIC (Core Local Interrupt Controller) del RISC-V para que genere una interrupción de timer cada 10 milisegundos y en cada callback del timer se ejecuta el run de los objetos:

- keypad (teclado)

- sevenSegments (7 segmentos)

- stepper (motor paso a paso)

- heartbeat (el led que parpadea)

- stateManager (encargado, a su vez de ejecutar el "run()" del "ConfigureStepsState", del "ConfigureDaysState" o del "IdleState", en función del modo en el que nos encontremos).

#include "Task.H"
#include "Timer.H"
#include "MyStepper.H"
#include "MyKeypad.H"
#include "MySevenSegments.H"
#include "SharedData.H"
#include "State.H"
#include "MyIdleState.H"
#include "MyConfigureStepsState.H"
#include "ConfigureDaysState.H"
#include "interrupt.H"
#include "Heartbeat.H"

using namespace avelino;
using namespace std;

class MyTimerListener : public TimerListener {
    public:
        Task **tasks;
        uint16_t numTasks;
        MyTimerListener(Task **tasks, uint16_t numTasks) : tasks(tasks), numTasks(numTasks) { };
        virtual void timerExpired();
};

void MyTimerListener::timerExpired() {
    Task **t = this->tasks;
    for (uint16_t i = 0; i < this->numTasks; i++, t++)
        (*t)->run();
}

int main() {
    interruptInit();
    MyKeypad keypad;
    keypad.init();
    MySevenSegments sevenSegments;
    sevenSegments.init();
    MyStepper stepper;
    stepper.init();
    Heartbeat heartbeat;
    // states
    SharedData data;
    MyConfigureStepsState configureStepsState(keypad, sevenSegments, stepper);
    ConfigureDaysState configureDaysState(keypad, sevenSegments, stepper);
    MyIdleState idleState(keypad, sevenSegments, stepper);
    // link states
    configureStepsState.setNextState(configureDaysState);
    configureDaysState.setNextState(idleState);
    idleState.setNextState(configureStepsState);
    StateManager<SharedData> stateManager(configureStepsState, data);
    // round robin tasks
    const int NUM_TASKS = 5;
    Task *tasks[NUM_TASKS] = { &keypad, &sevenSegments, &stepper, &heartbeat, &stateManager };
    MyTimerListener myTimerListener(tasks, NUM_TASKS);
    Timer timer;
    timer.init(myTimerListener, 10_msForTimer);
    while (true)
        asm volatile ("wfi");
    return 0;
}

Inicializamos todos los objetos y creamos un sencillo gestor de tareas de tipo round-robin para invocar las funciones miembro "run()" de los objetos de tipo "Task".

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







¡Feliz Navidad! :-)

[ 2 comentarios ] ( 25629 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente  |   ( 3 / 829 )

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