Skip to content

CT.5: SPI esclavo

Juan Gonzalez-Gomez edited this page Jun 13, 2019 · 294 revisions

Descripción

Aprenderemos a hacer periféricos por el bus SPI, para que un maestro acceda a ellos. La comunicación maestro - periférico se implementa mediante comandos. Típicamente el esclavo tiene una serie de registros, mapeados en direcciones de memoria, a los que el maestro accede para leerlos o escribirlos

Colección

(En construcción...)

  • Colection-Jedi-v1.6.0.zip: Colección para este cuaderno técnico. Descargar e instalar

Contenido

Introducción

Los circuitos digitales se intercambian información a través de buses de comunicaciones. Hay muchos. Uno de ellos es el bus SPI. Un microcontrolador, como por ejemplo arduino, puede leer datos de un sensor mediante el bus SPI

El circuito principal, el que lleva la voz cantante en la comunicación se nomina el Maestro. Típicamente es un microprocesador (Ej. Arduino). El otro circuito se denomina esclavo, y su misión es responder a los comandos que le envía el maestro

Los datos se transmiten en serie, bit a bit, uno por cada periodo del reloj SCLK del maestro. Según cómo se haya configurado, las acciones de lectura y escritura de los bits se harán bien en los flancos de subida o de bajada

Por MOSI se transmiten los datos del maestro al esclavo, y por MISO del esclavo al maestro. La información fluye en ambos sentidos a la vez (full-dúplex). El esclavo sólo funciona cuando se activa la señal SS, en caso contrario no responderá a ninguna petición

Arduino maestro, FPGA esclava

Nos centraremos en cómo hacer circuitos esclavos en la FPGA, que se comunican con un maestro. En los ejemplos utilizaremos un Arduino UNO como Maestro. Con Arduino trabajaremos en software, mientras que con la FPGA en hardware

Utilizamos la unidad de SPI hardware que incorpora Arduino, por lo que los pines que usaremos para las señales son: D13-SCLK, D12-MISO, D11-MOSI, D10-SS. Los conectamos a los pines del mismo nombre en la Alhmbra-II. El conexionado queda así:

¡Ya lo tenemos todo listo para empezar con los experimentos y las pruebas!

El bloque SPI-esclavo

Para implementar fácilmente nuestros circuitos esclavos por el SPI usaremos el bloque SPI-esclavo, dispoinible en la colección Jedi 1.6.0 en el menú Varios/SPI/SPI-slave/SPI-slave-unit. Las patas MISO, MOSI, SCLK y SS se conectan directamente a los pines SPI correspondientes

Los intercambios de información se hacen siempre en grupos de 8 bits, lo denominamos una transacción. En cada transacción llega un dato del maestro y se envía a la vez otro hacia el maestro. Por tanto, en toda transacción hay siempre un dato que llega y otro que sale

Por rcv se recibe un tic cada vez que se ha completado una transacción, y por tanto se habrá recibido un dato nuevo y se habrá enviado otro. El esclavo emite un tic por load para cargar el dato que quiere enviar al maestro. Pero hasta que el maestro no lo indique no se envía nada

Ejemplo 1: Mostrando en los LEDs el dato recibido

En este primer ejemplo haremos un circuito esclavo en la FPGA que simplemente muestra por los LEDs el dato recibido a través del SPI. En el arduino se ejecutará un programa que envía dos valores, cada medio segundo. El escenario es el siguiente

El circuito es muy sencillo. Sólo hay que colocar el bloque SPI-esclavo y conectar directamente las señales del SPI a los pines correspondientes. El dato de salida se lleva directamente a los LEDs de la Alhambra II. ¡Ya tenemos nuestro primer periférico SPI hecho! ¡Así de fácil!

(01-SPI-LEDs.ice)

En el Arduino cargamos este código. El SPI se configura a su máxima velocidad: 4 Mhz. Los pines SCLK y MOSI los controla el hardware spi de Arduino, mientras que SS lo establecemos nosotros en el programa. La función write_LEDs() activa SS, envía el valor y desactiva SS

(01-SPI-LEDs.ino)

#include <SPI.h>

//-- Pin usado para la seleccion del esclavo
#define SS 10

void setup() {

  //-- Inicializar SPI
  SPI.begin();
  SPI.beginTransaction (SPISettings (4000000, MSBFIRST, SPI_MODE0));

}

//-- Enviar un valor por el SPI para
//-- sacarlo por los LEDs de la FPGA
void write_LEDs(uint8_t value)
{
  digitalWrite(SS, LOW);
  SPI.transfer(value); 
  digitalWrite(SS, HIGH);
}

void loop() {

  //-- Sacar valor 0xAA por los LEDs
  write_LEDs(0xAA);
  delay(500);

  //-- Sacar valor 0x55 por los LEDs
  write_LEDs(0x55);
  delay(500);
}

El programa principal simplemente envía 0xAA, espera medio segundo, envía 0x55 y espera otro medio segundo. Esta se repite para obtener la secuencia en los LEDs. En este vídeo lo vemos en acción. Si quitamos el cable SCLK se dejan de transmitir los datos y la secuencia para

Click to see the youtube video

También, al apretar el reset de Arduino, la secuencia se para, porque se dejan de enviar los valores. Al apretar el reset de la FPGA, todo se apaga porque el hardware desaparece. Al terminar se vuelve a configurar la FPGA, aparece el circuito del SPI y la secuencia continúa

Ejemplo 2: Capturando los datos en un registro

Si necesitamos guardar el dato recibido en un registro, usamos la señal rcv. En este ejemplo se captura el dato y se muestra en los LEDs, pero pasándolo por el bloque brillo-gradual para generar una transición suave

(02-SPI-LEDs-regs.ice)

Actualizamos el hardware, pero usamos el mismo software anterior para Arduino. En este vídeo se muestra el resultado. El registros que hemos usado no es necesario realmente. Si lo quitamos sigue funcionando igual. Es sólo un ejemplo de cómo capturar lo recibido

Click to see the youtube video

Ejemplo 3: Envío de datos al maestro

Para enviar datos desde el esclavo al maestro, primero hay que cargarlos en el transmisor, y se enviarán en la siguiente transacción que inicie el maestro. Como ejemplo leeremos los dos pulsadores de la Alhambra II, SW1 y SW2 y los mostraremos en dos LEDs conectados al Arduino

Sabemos que comienza una transacción nueva cuando la señal SS pasa de 0 a 1 (es activa a nivel bajo). Ese es el evento que usaremos para capturar el estado de los pulsadores. El byte a transmitir contiene el estado de SW1 y SW2 en los bits 0 y 1 respectivamente, y el resto a 0

(03-SPI-Pulsadores.ice)

El programa de arduino está constantemente leyendo los pulsadores a través del SPI. Si alguno está apretado, se enciende su LED correspondiente. Se usa función leer_pulsadores(). Activa el esclavo y realiza una transacción de envío y recepción (a la vez). Se envía el valor 0, que el esclavo ignora (es basura)

(03-SPI-pulsadores.ino)

#include <SPI.h>

//-- Pin usado para la seleccion del esclavo
#define SS 10

//-- Pin de los LEDs
#define LED1 7
#define LED2 6

void setup() 
{
  //-- Configurar los LEDs
  pinMode(LED1, OUTPUT);
  pinMode(LED2, OUTPUT);

  //-- Inicializar SPI
  SPI.begin();
  SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0));
}

//-- Leer los pulsadores de la FPGA,
//-- a través del SPI
uint8_t leer_pulsadores()
{
  //-- Activar el esclavo
  digitalWrite(SS, LOW);

  //-- Leer el valor. A la vez hay que enviar
  //-- otro dato. Mandamos un 0 (basura)
  uint8_t value = SPI.transfer(0x00); 

  //-- Desactivar el esclavo
  digitalWrite(SS, HIGH);

  return value;
}

void loop() 
{

  //-- Leer los pulsadores
  uint8_t estado = leer_pulsadores();

  //-- Encender el LED1 si el pulsador SW1 está apretado
  digitalWrite(LED1, estado & 0x01);

  //-- Encender el LED2 si el puslador SW2 está apretado
  digitalWrite(LED2, estado & 0x02);
}

Cargamos el programa en el Arduino, y el circuito en la FPGA. En este vídeo se muestra en funcionamiento. Apretando cada pulsador se enciende el LED correspondiente. ¡Nuestro periférico de lectura de pulsadores por SPI funciona! :-)

Click to see the youtube video

Ejemplo 4: Envío y recepción

En este ejemplo se muestran transmisiones y recepciones. Todo lo que envía el maestro se saca por los LEDs. Cada dato recibido se incrementa en 1 y se devuelve como respuesta en la siguiente transacción

(04-SPI-send-receive.ice)

El programa de arduino genera una secuencia de dos estados, enviando los valores 0xF0 y 0x0F, que se ven en los LEDs. Los valores recibidos se imprimen en el terminal serie de Arduino para comprobar que se han incrementado

(04-SPI-send-receive.ino)

#include <SPI.h>

//-- Pin usado para la seleccion del esclavo
#define SS 10

void setup() 
{
  //-- Inicializar SPI
  SPI.begin();
  SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0));

  //-- Debug
  Serial.begin(9600);
}

//-- Realizar una transaccion. Se envia un dato
//-- y se devuelve lo recibido
uint8_t spi_transaction(uint8_t tx_value)
{
  //-- Activar el esclavo
  digitalWrite(SS, LOW);

  uint8_t rx_value = SPI.transfer(tx_value); 

  //-- Desactivar el esclavo
  digitalWrite(SS, HIGH);

  return rx_value;
}

void loop() 
{

  //-- Enviar el valor 0xF0
  uint8_t rx1 = spi_transaction(0xF0);
  Serial.print("Send: 0xF0. Received: ");
  Serial.println(rx1, HEX);
  delay(500);

  //-- Enviar el valor 0x0F
  uint8_t rx2 = spi_transaction(0x0F);
  Serial.print("Send: 0x0F. Received: ");
  Serial.println(rx2, HEX);
  delay(500);
}

En el terminal de arduino vemos los valores. El primer valor recibido es 0, porque es el de la primera transacción en la que todavía no se ha cargado ningún valor. En la siguiente se recibe 0xF1 porque previamente se había enviado 0xF0. En la siguiente 0x10, que es 0x0F + 1

En este vídeo vemos la secuencia en los LEDs, y los datos recibidos en el terminal de Arduino. Ya sabemos cómo hacer periféricos que envían y reciben. Estamos listos para pasar al siguiente nivel

Click to see the youtube video

Especificaciones del módulo SPI esclavo

El bloque SPI esclavo que estamos usando en los ejemplos de este cuaderno técnico tiene las siguientes especificaciones técnicas:

Parámetro Valor Descripción
Fmax 2Mhz recuencia máxima reloj (SCLK)
CPOL 0 Polaridad del reloj. El reloj permanece a 0 cuando está en reposo
CPHA 0 Fase del reloj. Captura en el primer flanco
Bits 8 Números de bits transferidos en cada sentido de la transacción
Ordenación BMS El primer bit transmitido es el de mayor peso
Modo SPI Arduino SPI_MODE0 Configuración del SPI de arduino para la comunicación con este bloque

Este es su cronograma. Tanto el maestro como el esclavo capturan los datos en el flanco de subida, y los depositan en el de bajada

Midiendo el bus SPI

Vamos a realizar mediciones para comprobar que nuestro SPI se comporta adecuadamente. Usamos el mismo circuito del ejemplo 4, al que simplemente le hemos añadido la documentación sobre los canales a los hemos conectado cada uno de los pines. Es el ejemplo 5:

(05-SPI-medicion.ice)

Este es el escenario. El bus SPI va del Arduino a la FPGA usando cables macho-macho, y al analizador mediante cables hembra-hembra. En los canales del 0 al 3 se muestran las 4 señales del SPI

En el arduino cargamos un programa que envía la secuencia 0xAA y 0x55 por el SPI, sin pausas y sin usar el puerto serie. Los valores 0xAA y 0x55 se usan mucho en hardware porque tiene los bits alternados y permiten ver mejor las señales

(05-SPI-medicion.ino)

#include <SPI.h>

//-- Pin usado para la seleccion del esclavo
#define SS 10

void setup() 
{
  //-- Inicializar SPI
  SPI.begin();
  SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0));
}

//-- Realizar una transaccion. Se envia un dato
//-- y se devuelve lo recibido
uint8_t spi_transaction(uint8_t tx_value)
{
  //-- Activar el esclavo
  digitalWrite(SS, LOW);

  uint8_t rx_value = SPI.transfer(tx_value); 

  //-- Desactivar el esclavo
  digitalWrite(SS, HIGH);

  return rx_value;
}

void loop() 
{

  //-- Enviar el valor 0x55
  uint8_t rx1 = spi_transaction(0x55);
  //delay(500);

  //-- Enviar el valor 0xAA
  uint8_t rx2 = spi_transaction(0xAA);
  //delay(500);
}

Realizamos las mediciones con el Pulse-View. Los valores que se envían por MOSI son 0x55 y 0xAA, alternativamente. Y los recibidos por MISO son los mismos pero incrementados en una unidad: 0x56 y 0xAB, y recibidos en la transacción siguiente

.

Aquí se muestra una transacción más de cerca. Los marcadores A y B están en los dos primeros flancos de subida, donde se hacen las capturas. Se comprueba que se cumple el cronograma anterior. Las señales MOSI y MISO inferiores muestran los bits ya capturados

Y en esta otra captura se muestra la temporización de la señal de reloj, que efectivamente es de 2Mhz (500ns)

Implementando comandos

Con el SPI ya tenemos solucionado el problema del intercambio de bytes entre dos circuitos, en ambas direcciones. El siguiente paso es definir comandos para controlar nuestros periféricos. Tendremos que definir una sintáxis para ellos

Un forma típica es usar comandos de 2 bytes, donde el primero contiene el código de comando, y el segundo el valor. Así por ejemplo, para sacar un valor por los LEDs podemos definir el comando WRITE_LEDS así:

Comando Código comando Descripción
WRITE_LEDS val 0x40 Sacar el número val por los LEDs

Por ejemplo, para sacar el valor 0xAA por los LEDs, tendríamos que enviar los siguientes bytes: 0x40 y 0xAA. El primero le indica al periférico la acción a realizar (activar los leds) y el segundo el valor de los LEDs. El código 0x40 lo hemos definido nosotros (los diseñadores)

Ejemplo 6-1: Implementación del comando WRITE_LEDS

En este ejemplo se implementa el comando WRITE_LEDs. Un biestable RS es el encargado de notificar la llegada del comando, comparando el dato recibido con el código 0x40. Este biestable se inicializa a 0 con cada comando terminado, cuando SS se pone a 1

(06-1-SPI-WRITE-LEDs.ice)

Todo lo recibido se ignora, hasta que se detecta el comando. Entonces el biestable habilita la puerta AND, de forma que el siguiente tic llega hasta el registro, capturándose el valor y sacándose por los LEDs. El maestro debe activar SS, envíar los dos bytes y desativar SS

Esto es lo que hace la función write_LEDs(). El programa principal saca una secuencia de 3 estados, para comprobar que el comando funciona correctamente. En este ejemplo no se prueba, pero cualquier otro comando diferente se ignora

(06-SPI-WRITE-LEDs.ino)

#include <SPI.h>

//-- Pin usado para la seleccion del esclavo
#define SS 10

//-- Comando WRITE_LEDS
#define WLEDS 0x40

void setup() {

  //-- Inicializar SPI
  SPI.begin();
  SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0));

}

//-- Enviar un valor por el SPI para
//-- sacarlo por los LEDs de la FPGA
void write_LEDs(uint8_t value)
{
  digitalWrite(SS, LOW);

  //-- Enviar el codigo de comando
  SPI.transfer(WLEDS);

  //-- Enviar el valor para los LEDs
  SPI.transfer(value);
   
  digitalWrite(SS, HIGH);
}

void loop() {

  //-- Sacar valor 0xAA por los LEDs
  write_LEDs(0xAA);
  delay(500);

  //-- Sacar valor 0x55 por los LEDs
  write_LEDs(0x55);
  delay(500);

  //-- Sacar 0x0F por los LEDs
  write_LEDs(0x0F);
  delay(500);
}

Cargamos el circuito y el programa de Arduino. En este vídeo los vemos en acción. No es nada espectacular, pero tenemos implementado un periférico por el SPI que responde a nuestro primer comando de 2 bytes :-)

Click to see the youtube video

Bloque sintáctico cmd8

Los comandos con dos parámetros: código de comando y valor son muy comunes. Para hacer más fácil y rápida su implementación podemos usar el bloque cmd8, disponible en el menú Varios/Syntax/cmd8. Por su entrada se van recibiendo los datos del spi

Si se recibe el comando especificado, el bloque devuelve el valor que llega tras ese comando, y emite un tic de datos. Para reconocer el siguiente comando se debe inicializar. Se hace con la señal SS del SPI

Se trata de un bloque sintáctico, para detectar el patrón comando-valor. Aunque lo estamos usando con el SPI, se trata de un bloque genérico y sirve para reconocer comandos que lleguen por cualquier periférico: bus i2c, spi, serie, etc...

Ejemplo 6-2: Comando WRITE_LEDS con bloque cmd8

Rehacemos el ejemplo 6-1, pero usaando el nuevo bloque cmd8. Ahora queda mucho más compacto y fácil de entender y modifcar. Y lo más importante, es mucho más fácil añadir nuevos comandos. El funcionamiento es exactamente igual que el del ejemplo anterior

(06-2-SPI-WRITE-LEDs.ice)

Ejemplo 7: Comandos WRITE_LEDs y Brillo-LEDs

En este ejemplo implementamos un comando más, para controlar el brillo de los LEDs: BRILLO_LEDS, cuyo código de comando es 0x50. Así, nuestro nuevo periférico tiene 2 comandos: escribir un valor en los LEDs (WRITE_LEDs) y controlar su brillo (BRILLO_LEDs)

Comando Código comando Descripción
WRITE_LEDS val 0x40 Sacar el número val por los LEDs
BRILLO_LEDS val 0x50 Establecer el brillo de los LEDs

Los dos comandos se implementan muy fácilmente con el bloque sintáctico cmd8. Usamos unos para cada comando, y colocamos un registro para almacenar el valor recibido. Un registro para almacenar el nivel de brillo y otro para el dato a mostrar en los LEDs

(07-2-SPI-WRITE-Brillo-LEDs.ice)

Gracias a las etiquetas, el diseño es muy legible y nos permite separarlo en tres zonas: la parte de comunicación SPI, otra para el comando WRITE_LEDs y otra para el comando BRILLO_LEDs. También permite que sea muy fácil llevar comandos de un periférico a otro mediante copiar y pegar :-)

En el programa de Arduino creamos la función brillo_LEDs() para establecer el nivel de brillo usando el nuevo comando. Este es el código. La función write_LEDs() no cambia: es la misma que ya teníamos implementada. La mecánica es la misma: bajar SS, enviar el código de comando, el valor para el brillo y subir SS

(07-SPI-WRITE-Brillo-LEDs.ino)

//-- Comando BRILLO_LEDS  
#define BLEDS 0x50

void brillo_LEDs(uint8_t value)
{
  digitalWrite(SS, LOW);

  //-- Enviar el codigo de comando
  SPI.transfer(BLEDS);

  //-- Enviar el valor del brillo
  SPI.transfer(value);
   
  digitalWrite(SS, HIGH);
}

Como ejemplo de uso, en el programa principal establecemos diferentes valores para los leds, con diferentes intensidades, cada medio segundo, generando así una secuencia simple

void loop() 
{

  write_LEDs(0xFF);
  brillo_LEDs(255);
  delay(500);

  //-- Sacar valor 0xAA por los LEDs
  write_LEDs(0x3F);
  brillo_LEDs(100);
  delay(500);

  //-- Sacar valor 0x55 por los LEDs
  write_LEDs(0x0F);
  brillo_LEDs(20);
  delay(500);

  //-- Sacar 0x0F por los LEDs
  write_LEDs(0x03);
  brillo_LEDs(5);
  delay(500);
}

Cargamos el programa en el Arduino y el circuito en la FPGA. En este vídeo vemos el resultado. Nuestro periférico por SPI ya tiene 2 comandos :-)

Click to see the youtube video

Ejemplo 8: LEDs pulsantes

En este ejemplo modificaremos el programa de arduino para implementar LEDs pulsantes, que se encienden y apagan progresivamente. En el setup() se escribe 0xFF en los LEDs y en el bucle principal se modifica el brillo, del mínimo al máximo y vice versa

(08-SPI-Leds-pulsantes.ino)

void loop() 
{

  int brillo;

  //-- Encendido progresivo
  for (brillo=0; brillo <= 255; brillo++) {
    brillo_LEDs(brillo);
    delay(2);
  }

  //-- Apagado progresivo
  for (brillo=255; brillo >= 0; brillo--) {
    brillo_LEDs(brillo);
    delay(2);
  }

  //-- Permanecer un tiempo con los LEDs apagados
  delay(400);

}

En este vídeo lo vemos en acción. Cambiando el tiempo de los diferentes delays se consigue modificar el tiempo de encendido/apagado, así como el tiempo en el que los LEDs están totalmente apagados

Click to see the youtube video

Ejemplo 9: Comando READ_BUTTONS

Implementaremos un comando más: READ_BUTTONs, para leer los dos pulsadores de la Alhambra II. Para este comando usaremos el código 0x60. Al recibir este comando, el esclavo nos devuelve el estado de ambos pulsadores en la siguiente transacción. Tabla resumen de los 3 comandos:

Comando Código comando Descripción
WRITE_LEDS val 0x40 Sacar el número val por los LEDs
BRILLO_LEDS val 0x50 Establecer el brillo de los LEDs
READ_BUTTONS 0x60 Lectura de los pulsadores

El comando READ_BUTTONS NO tiene argumentos adicionales. Por ello NO usamos el bloque sintáctico cmd8, sino que en cuanto se recibe el código 0x60 se carga el valor de los pulsadores en el registro de transmisión del SPI, para su envío en la siguiente transacción

(09-SPI-READ_buttons.ice)

Desde el Arduino, hacemos la lectura de los pulsadores usando la función read_buttons(). Se activa el esclavo, se envía el código 0x60 y luego el código basura 0x00 para que haya una segunda transacción. El esclavo devuelve el estado de los pulsadores en esta segunda transacción

(09-SPI-Read-buttons.ino)

//-- Comando READ_BUTTONS
#define RBUTT 0x60

uint8_t read_buttons()
{
  digitalWrite(SS, LOW);

  //-- Enviar el codigo de comando
  SPI.transfer(RBUTT);

  //-- Leer el estado de los pulsadores
  //-- Se envía el byte "basura" 0x00
  uint8_t value = SPI.transfer(0x00);
   
  digitalWrite(SS, HIGH);

  return value;
}

Para probar el funcionamiento de todos los comandos hacemos un programa en Arduino que genera una secuencia en los LEDs, de dos estados. Con los pulsadores SW1 y SW2 subimos y bajamos el brillo de los LEDs. Conectamos dos LEDs en el Arduino para mostrar el estado de los pulsadores. Este es el programa principal:

void loop() 
{
  
  uint8_t buttons;
  uint8_t brillo = 100;

  unsigned long tiempo1 = millis();
  unsigned long tiempo2;
  uint8_t valor_leds = 0x55;

  //-- Valor inicial a mostrar en los LEDs
  write_LEDs(valor_leds);

  //-- Bucle principal
  while(1) {

    //-- Establecer brillo actual
    brillo_LEDs(brillo);

    //-- Leer botones
    buttons = read_buttons();

    //-- Pulsador SW1 apretado
    if (buttons & 0x01) {

      //-- Encender LED1 (de arduino)
      digitalWrite(LED1, HIGH);
      
      //-- Incrementar brillo si no hemos llegado al tope 
      if (brillo < 255)
        brillo++;
    }
    else
      //-- Apagar LED1
      digitalWrite(LED1, LOW);

    //-- Pulsador SW2 apretado
    if (buttons & 0x02) {

       //-- Encender LED2 (de arduino)
       digitalWrite(LED2, HIGH);
      
      //-- Decrementar brillo si no hemos llegado a 0 
      if (brillo > 0)
        brillo--;
    }
    else
      //-- Apagar LED2
      digitalWrite(LED2, LOW);

    //-- Cada 300ms actualizar la secuencia
    tiempo2 = millis();
    if (tiempo2 > tiempo1 + 300) {
      valor_leds = ~valor_leds;
      write_LEDs(valor_leds);
      tiempo1 = tiempo2;
    }
    delay(5);
  }
}

En este vídeo se ven las pruebas. Comienza con la secuencia en los LEDs. Al pretar SW2, el brillo disminuye. Al apretar SW1, aumenta. Además, se encienden los LEDs en el Arduino, para tener feedback visual de las pulsaciones

Click to see the youtube video

Lo que más me gusta de estos experimentos es que estamos trabajando en ambos mundos: Hardware y Software. Este pantallazo de mi portátil de trabajo me encanta. En la derecha tenemos el diseño del hardware. En la izquierda su programación

En la derecha usamos un pensamiento espacial y paralelo, donde hay movimiento físico de bits. En la izquierda un pensamiento algorítmico secuencial: se ejecuta una instrucción tras otra. En la derecha tenemos FPGAs, en la izquierda Arduino (procesador)

Con un click de ratón aquí, actualizamos el hardware. Con otro click allá, cambiamos su programación. No dejo de asombrarme de lo potente y apasionante que es esto. ¡Estoy enganchadísimo! ¡¡Sigamos pues!!

Ejemplo 10: Comando READ_ID

Implementaremos un comando más: READ_ID que nos devolverá el identificador del periférico. Un valor constante, por ejemplo 0xE3, que identifique a este esclavo. Así aprenderemos cómo enviar al maestro información proveniente de varias fuentes

Mediante un codificador de 2 a 1, obtenemos una señal que nos indica cuál de los dos comandos READ se ha recibido, lo que nos permite seleccionar el byte a enviar al maestro, mediante un multiplexor 2 a 1. Por load llevamos el tic de carga al SPI

(10-SPI-READ_ID.ice)

El resto del circuito es igual que en el ejemplo anterior. Eso es lo bueno de usar etiquetas, que podemos tener sub-circuitos dentro de los circuitos para organizarlo mejor. Este es el circuito completo

El programa de arduino lo podemos mejorar. Creamos la función cmd() para enviar un comando genérico. Pasamos como parámetro el código del comando y el valor. Devuelve lo leido por el SPI, si el comando era de lectura. Los comandos los implementamos a partir de esta función

(10-SPI-Read-id/10-SPI-Read-id.ino)

//-- Comando generico
uint8_t cmd(uint8_t cod, uint8_t val)
{
  digitalWrite(SS, LOW);

  //-- Enviar el codigo de comando
  SPI.transfer(cod);

  //-- Enviar el valor de su parametro
  uint8_t ret = SPI.transfer(val);
   
  digitalWrite(SS, HIGH);

  return ret;
}

//-- Enviar un valor por el SPI para
//-- sacarlo por los LEDs de la FPGA
void write_LEDs(uint8_t value)
{
  cmd(WLEDS, value);
}

void brillo_LEDs(uint8_t value)
{
  cmd(BLEDS, value);
}

uint8_t read_buttons()
{
  return cmd(RBUTT, 0x00);
}

uint8_t read_id()
{
  return cmd(RID, 0x00);
}

Modificamos el ejemplo anterior (ejemplo 9) para que cada 2 segundos lea el identificador del esclavo y lo imprima por la consola serie. Al final del bucle principal añadimos:

//-- Cada 2 segundos se lee el identificador del chip
    if (tiempo2 > tiempo_rid + 2000) {
      Serial.print("Identificador: ");
      uint8_t id = read_id();
      Serial.println(id, HEX);
      tiempo_rid = tiempo2;
    }

Probamos el nuevo ejemplo. Además de ponder cambiar el brillo de los LEDs con los pulsadores mientras hay una secuencia, veremos en la consola serie el identificador el esclavo que hemos asignado. Aparece la información cada 2 segundos

Registros mapeados

Muchos periféricos por SP tienen registros accesibles mediante una dirección. Se denominan registros mapeados. Para acceder a ellos se usa un registro especial que contiene la dirección del registro al que se quiere acceder. Es el registro de dirección (o puntero de registros)

Sólo se usa 3 comandos: uno para establecer el valor del registro de dirección, lo que selecciona el registro a usar. Otro para escribir en el registro activo y otro para leer de él. Los denominamos SAP (Set address Pointer), WR (Write in register) y RD (Read from register)

Cada comando tiene su propio código, que dependen del diseñador. Los códigos que vamos a usar son los mostrados en esta tabla, que coinciden con los usados por el chip CAP1188, un lector de sensores capacitivos de MicroChip

Comando Abrev. Código Descripción
SET ADDRES POINTER val SAP 0x7D Establecer el valor del registro de dirección
WRITE REGISTER val WR 0x7E Escribir en el registro apuntado por el registro de dirección
READ REGISTER RD 0x7F Leer el registro apuntado por el registro de dirección

Ejemplo 11-1: Registro de LEDs

Empezaremos por un ejemplo de un periférico SPI en el que sólo tenemos un registro mapeado: el registro de LEDs, en la dirección 10h. Al escribir en él se cambian los LEDs. Su lectura nos devuelve los valores actuales que se están mostrando

Esta es la implementación de los 3 comandos, que ya conocemos. Sólo cambian los código usados, que son otros. Sólo hay un registro, el de dirección, donde se almacena la dirección del registro a leer o escribir. El comando de escritura emite el tic wr y el de lectura el tic rd

(11-SPI-REG-LEDs.ice)

Con el tic de wr se guarda el valor en el registro previamente seleccionado con el comando SAP, y con el tic de rd se carga el valor a enviar al maestro, que se transmisitrá en la siguiente transacción. Gracias a las etiquetas el diseño queda muy compacto y modular

El registro LEDs está en la dirección 10h (lo hemos decidido nosotros) y es de lectura y escritura. El valor que tiene inicialmente es 0, pero se puede establecer cualquier otro. En esta tabla se resume

Dir. R/W Nombre Función Valor por defecto
10h R/W LEDs Valor mostrado en los LEDs 00h

Con un comparador generamos leds_cs, para indicar que se accede a la dirección del registro LEDs. Al llegar el tic de wr se guarda el valor en el registro, que se saca por los LEDs. Esta es su implementación

Para la lectura se usa un multiplexor 2-1 y la señal leds_cs para seleccionar qué llevar al bloque SPI. Si se ha seleccionado el regisro LEDs, se lleva su valor, o de lo contrario un 0. Por tanto, si leemos de cualquier otro registro que no sea LEDs, obtendremos un 0

El circuito completo es el siguiente, con todas las partes juntas

En el programa de Arduino creamos las funciones para invocar los tres comandos: SAP_cmd(), WR_cmd() y RD_cmd()

(11-SPI-Reg-LEDs.ino)

//-- Codigo de los comandos
#define SAP 0x7D  //-- Comando SET ADDRESS POINTER
#define WR  0x7E  //-- Comando de escritura en registro
#define RD  0x7F  //-- Comando de lectura en registro

void SAP_cmd(uint8_t value)
{
  cmd(SAP, value);
}

void WR_cmd(uint8_t value)
{
  cmd(WR, value);
}

uint8_t RD_cmd()
{
  return cmd(RD, 0x00);
}

Y a partir de ellas escribimos las dos funciones que nos dan el interfaz final: write_reg(), para escribir en un registro situado en una dirección, y read_reg() para leer el registro de una dirección. En este ejemplo sólo tenemos el registro LEDs en la dirección 10h

//-- Direcciones de los registros
#define LEDS_REG    0x10  //-- Registro de LEDs

//-- Escritura en un registro mapeado
void write_reg(uint8_t reg, uint8_t val)
{
  SAP_cmd(reg);
  WR_cmd(val);
}

//-- Lectura de un registro mapeado
uint8_t read_reg(uint8_t reg)
{
  SAP_cmd(reg);
  return RD_cmd();
}

El programa principal genera una secuencia de movimiento de los LEDs de 2 estados. Cada vez que se escribe un valor, se vuelve a leer y se muestra en la consola serie, para comprobar que la lectura funciona correctamente

uint8_t leds;
uint8_t valor_leds = 0xC3;

void loop() 
{

  //-- Escribir un valor en los LEDs
  write_reg(LEDS_REG, valor_leds);

  //-- Leer el valor escrito
  leds = read_reg(LEDS_REG);
  Serial.print("LEDs: ");
  Serial.println(leds, HEX);
  delay(500);

  //-- Cambiar la secuencia de los LEDs
  valor_leds = ~valor_leds; 
  
}

Cargamos el circuito en la FPGA y el programa en el Arduino. En la consola serie veremos el valor leído de los LEDs, cada medio segundo

Y en este vídeo vemos la secuencia en acción

Click to see the youtube video

El Bloque SPI-cmd-regs

Trabajar con regitros mapeados es lo más habitual, por eso se ha creado el bloque SPI-reg-cmd, accesible desde el menú Varios/SPI/SPI-slave, que implementa los comandos SAP, WR y RD necesarios para seleccionar el registro a usar, escribir o leer

Recibe por su entrada los datos que llegan del SPI y nos devuelve a la salida la dirección del registro seleccionado, el valor a escribir en caso de escritura y los tics de lectura y escritura necesarios para realizar estas dos operaciones

Aunque lo estamos usando con el SPI, se trata de un bloque genérico que nos permite implementar el acceso a registros mapeados para cualquier otro Bus: I2C, Serie, etc... Pero de momento nos centramos en el SPI, que es nuestro primer Bus

Ejemplo 11-2: Registro de LEDs con bloque SPI-cmd-reg

Rehacemos el ejemplo 11-1 pero utilizando este nuevo bloque SPI-cmd-reg. El funcionamiento es exactamente igual, no hay nada nuevo, pero ahora el circuito queda mucho más compacto y legible. Los datos que llegan del SPI se procesan en este bloque, que accede al registro de LEDs

(11-2-SPI-REG-LEDs.ice)

Este es el poder del diseño jerárquico: crear bloque más potentes a partir de otros más simples, que nos permitan ocultar la complejidad e ir creciendo en funcionalidad. Poco a poco haremos bloques más avanzados, hasta que lleguemos al procesador. Sigamos de momento con el SPI

Dentro de poco, gracias al trabajo de Carlos Venegas podremos trabajar en icestudio con diseños jerárquicos de forma más avanzado: navengado y editando bloques a cualquier nivel. Esto se empieza a poner cada vez más emocionante. ¡Gracias Carlos!

Bloque Reg-addr

Los registros mapeados en una dirección son también muy comunes, por lo que resulta muy útil tener un bloque específico para implementarlos: el bloque reg-addr, disponible en el menú Varios/Registros/08-bits/reg-addr. Los parámetros son la dirección de mapeado y su valor por defecto

Cuando recibe un tic de escritura, almacena el valor de su entrada de datos (bus de datos) si la dirección en el bus de direcciones coincide con la que tienen configurada. Cuando esto ocurre, además se activa su salida cs (chip select) para indicar que se ha seleccionado

Ejemplo 11-3: Registro de LEDs con bloque reg-addr

Modificamos el ejemplo 11-2 para implementar el registro de LEDs usando el bloque reg-addr. El esquema es mucho más claro. El bloque SPI se conecta con el de comandos para procesar lo recibido, y envíar la información por el bus de direcciones, bus de datos y el de control

(11-3-SPI-REG-LEDs.ice)

El registro de LEDs se conecta a estos buses para que se puedan realizar escrituras y lecturas. Su salida se conecta al bus de datos de salida (dataout) mediante un multiplexor. Este bus es el que llega al bloque SPI para enviar las lecturas al maestro

Ejemplo 12: Registro de LEDs por puerto serie

Aunque este cuaderno técnico es sobre el SPI, vamos a hacer un ejemplo de acceso al registro de LEDs mapeado en memoria pero a través del puerto serie. Los comandos los enviamos desde un terminal serie en el PC. Podemos usar el propio terminal de Arduino

Esto nos servirá para comprobar que los bloques de comandos y mapeo de registros son genéricos. Aunque cambiemos el nivel físico (del SPI al puerto serie), el resto del circuito se comporta igual. También nos servirá para prototipar más rápido

Este es el mismo circuito del ejemplo 11-3, pero para el puerto serie. Para hacer más fácil su uso hemos cambiado los códigos de los comandos por caracteres ASCII que se puedan teclear fácilmente desde cualquier termianl serie. La dirección del registro de LEDs es "1" (0x31)

(12-serial-reg-LEDs.ice)

Cualquier carácter enviado se ignora salvo que sea "S" (SAP), "W" (WR) o "R" (RD). La lectura genérica devuelve el carácter "-" salvo que hayamos seleccionado el registro de LEDs previamente con el comando "S1", en cuyo caso nos devuelve su valor

Abrimos el terminal de arduino, a la velocidad de 115200 baudios para hacer pruebas. Si enviamos el comando de lectura "R" nos devolverá el carácter "-". Podemos enviar varias Rs para hacer varias lecturas

Si enviamos la cadena "S1WU", se selecciona el registro (dirección "1") y se encenderán los LEDs con el valor 0x55 (0x55 es el valor ASCII de la 'U'). Ahora al leer nos devuelve lo que habíamos escrito antes: la "U". Si enviamos "W*" saldrá el valor 0x2A por los LEDs, y leeremos *

La Alhambra II está conectada directamente al PC. En esta foto se muestran los LEDs después de enviar el comando de escritura "WU"

Y en este vídeo de muestra el funcionamiento

Click to see the youtube video

Ejemplo 13: Dos registros mapeados, de lectura/escritura

Añadimos un registro adicional para controlar el brillo de los LEDs, mapeado en la dirección 0x11. En nuestro periférico tenemos ahora dos registros de lectura/escritura accesibles a través de los comandos SAP, WR y RD

Dir. R/W Nombre Función Valor por defecto
10h R/W LEDs Valor mostrado en los LEDs 00h
11h R/W BRILLO Nivel de brillo de los LEDs 255

El registro de brillo se añade muy fácilmente con el bloque reg-addr. Especificamos su dirección (0x11) y su valor por defecto (255) en los parámetros. Para la escritura sólo hay que conectarlo al bus de direcciones, de datos y al tic de escritura

Para la lectura necesitamos un circuito adicional. Con un codificador de 2 a 1 determinamos qué registro está seleccionado: el de brillo o el de leds. Si no está seleccionado ninguno, la salida zero del codificador estará a 1. Con un multiplexor 2 a 1 obtenemos el dato del registro seleccionado

Con un segundo multiplexor 2-1 devolvemos bien el valor del registro o bien 0x00 si no hay seleccionado ninguna. Es nuestro valor de lectura por defecto. La salida final va por el bus de datos de salida (dataout) hacia el bloque del spi para enviarlo al maestro

Usamos el bloque brillo8 para implementar el control de brillo de los LEDs. Conectamos sus entradas a los dos registros con el valor de los leds y con el nivel de brillo

Esta es la pinta que tiene el circuito final, con todas sus partes. Añadir un registro adicional ha sido fácil, ¿no? :-)

(13-SPI-dos-registros-mapeados.ice)

Para probarlo hacemos un programa en arduino que muestre en los LEDs la misma secuencia del ejemplo 11, pero alternando también el brillo. No hay que añadir ninguna función más, sólo la definición del registro de brillo y modificar el bucle principal

(13-SPI-dos-registros-mapeados.ino)

#define BRILLO_REG  0x11  //-- Registro de brillo
uint8_t leds;
uint8_t valor_leds = 0xC3;
uint8_t brillo;
uint8_t brillo_leds = 255;
uint8_t i = 0;

void loop() 
{

  //-- Escribir un valor en los LEDs
  write_reg(LEDS_REG, valor_leds);

  //-- Escribir el nivel de brillo 
  write_reg(BRILLO_REG, brillo_leds);

  //-- Leer los valores escritos
  leds = read_reg(LEDS_REG);
  brillo = read_reg(BRILLO_REG);
  Serial.print("LEDs: ");
  Serial.print(leds, HEX);
  Serial.print(", Brillo: ");
  Serial.println(brillo);
  delay(500);

  //-- Cambiar la secuencia de los LEDs
  valor_leds = ~valor_leds; 

  //-- Alternar el brillo
  i = (i + 1)%2;
  if (i==0)
    brillo_leds = 255; //-- Brillo máximo
  else
    brillo_leds = 20;  //-- Brillo bajo
}

Cargamos el circuito en la FPGA y el programa en Arduino. En este vídeo se muestra la secuencia en acción. El brillo es máximo cuando se encienden los leds exteriores, y mínimo cuando lo hacen los interiores. Nuestros dos registros mapeados funcionan correctamente en escritura

Click to see the youtube video

Desde el terminal de Arduino comprobamos que los valores leídos son los correctos. Se leen los dos registros cada medio segundo. Lo que se escribe es 0xC3 en LEDs con brillo de 255 y luego 0x3C en LEDs con brillo 20. Comprobamos la lectura:

Tras unos segundos, este es el aspecto que presentará el terminal de arduino:

Ejemplo 14: Cuatro registros mapeados

Como último ejemplo de registros mapeados, añadiremos dos más, pero de sólo lectura. Uno contendrá el identificador del periférico, definido por nosotros, y otro el estado de los pulsadores SW1 y SW2 de la Alhambra II. Estos son los 4 registros disponibles:

Dir. R/W Nombre Función Valor por defecto
10h R/W LEDs Valor mostrado en los LEDs 00h
11h R/W BRILLO Nivel de brillo de los LEDs 255
12h R PULSADORES Estado de los pulsadores SW1 y SW2 00h
FDh R ID Código de identificación del periférico 50h

Los dos nuevos registros son de sólo lectura, por ello no usamos bloques reg-addr. El registro ID debe devolver una constante, que no se puede modificar. Sólo necesitamos usar un comparador para detectar su dirección y colocar la constante con su valor

La estructura del registro de pulsadores es similar. Con un comparador detectamos los accesos a este registros. A partir de los bits de los pulsadores construimos un número con todo ceros en los bits más significativos y el estado de los pulsadores en los dos de menor peso

Para realizar la lectura de cualquier registro necesitamos un multiplexor 4:1 y un codificador 4:1. Con el multiplexor seleccionamos el dato del registro activo. El codificador nos indica qué registro es el activo. Con un mux 2:1 devolvermos el valor del registro seleccionado ó 0x00 por defecto

Este es el circuito completo. ¡Me encanta!😀 Y con un sólo click lo tenemos sintetizado y cargado en nuestra FPGA. Me sigue pareciendo magia... ¡Y todo con herramientas libres! ¡Sólo con herramientas libres! ¡Herramientas del patrimonio tecnógico de la humanidad! 😀

Para probarlo modificamos el programa de arduino anterior para imprimir en la consola el estado de los pulsadores y el identificador del periférico. Usamos estas definiciones para acceder a los 4 registros:

//-- Direcciones de los registros
#define LEDS_REG    0x10  //-- Registro de LEDs
#define BRILLO_REG  0x11  //-- Registro de brillo
#define BUTTONS_REG 0x12  //-- Registro de pulsadores
#define ID_REG      0xFD  //-- Registro de Identificacion

Este es el bucle principal. El estado de los pulsadores se muestra en la consola y en dos leds conectados al propio arduino

uint8_t butt;
uint8_t id;
uint8_t leds;
uint8_t valor_leds = 0xC3;
uint8_t brillo;
uint8_t brillo_leds = 255;
uint8_t i = 0;

void loop() 
{

  //-- Escribir un valor en los LEDs
  write_reg(LEDS_REG, valor_leds);

  //-- Escribir el nivel de brillo 
  write_reg(BRILLO_REG, brillo_leds);

  //-- Leer los valores escritos
  leds = read_reg(LEDS_REG);
  brillo = read_reg(BRILLO_REG);

  //-- Leer el identificador
  id = read_reg(ID_REG);

  //-- Leer los pulsaddores
  butt = read_reg(BUTTONS_REG);

  //-- Encender los leds de arduino
  if (butt & 0x01) digitalWrite(LED1, HIGH);
  else digitalWrite(LED1, LOW);
  
  if (butt & 0x02) digitalWrite(LED2, HIGH);
  else digitalWrite(LED2, LOW);


  //-- Mostrar las lecturas en la consola
  Serial.print("ID: ");
  Serial.print(id, HEX);
  Serial.print(", LEDs: ");
  Serial.print(leds, HEX);
  Serial.print(", Brillo: ");
  Serial.print(brillo);
  Serial.print(", Botones: ");
  Serial.println(butt, HEX);
  delay(500);

  //-- Cambiar la secuencia de los LEDs
  valor_leds = ~valor_leds; 

  //-- Alternar el brillo
  i = (i + 1)%2;
  if (i==0)
    brillo_leds = 255; //-- Brillo máximo
  else
    brillo_leds = 20;  //-- Brillo bajo
}

Lo cargamos y lo probamos. El funcionamiento es el mismo que el ejemplo anterior, pero ahora, además, se leen los pulsadores cada medio segundo y se muestra su estado en los LEDs. ¡Nuestros registros mapeados funcionan!

Click to see the youtube video

Y en la consola de arduino comprobamos que todos los valores leídos son correctos. El identificador es 0x50 y el estado de los pulsadores varía según los apretamos

Este es un pantallazo de la consola de arduino, en un instante concreto, para comprobar de forma estática que los valores son correctos

Ejemplo 15: Cuatro registros mapeados. Puerto serie

Si queremos que este mismo ejemplo funcione sobre el puerto serie sólo hay que cambiar el bloque SPI por el transmisor y receptor serie, como hicimos en el ejemplo 12. Para facilitar las pruebas cambiamos también los códigos de los comandos, para que sean caracteres ASCII

También cambiamos las direcciones de los registros, para facilitar las pruebas desde el terminal serie, y los valores devueltos (El registro de identificación contiene "A", y el de pulsadores "0" inicialmente)

  • Comandos:
Comando Abrev. Código Descripción
SET ADDRES POINTER val SAP "S" Establecer el valor del registro de dirección
WRITE REGISTER val WR "W" Escribir en el registro apuntado por el registro de dirección
READ REGISTER RD "R" Leer el registro apuntado por el registro de dirección
  • Registros:
Dir. R/W Nombre Función Valor por defecto
"1" R/W LEDs Valor mostrado en los LEDs 00h
"2" R/W BRILLO Nivel de brillo de los LEDs 255
"3" R PULSADORES Estado de los pulsadores SW1 y SW2 "0"
"I" R ID Código de identificación del periférico "A"

Este es el circuito completo, con los valores cambiados

Lo probamos con cualquier terminal de comunicaciones, a 115200 baudios. Por ejemplo con el script communicator. En rojo vemos los comandos enviados, y en negro lo recibido. Con SIR leemos el registro de identifición, que es "A". Con S3R los pulsadores

Mini-controlador VGA por SPI

En el cuaderno técnico 2 aprendimos cómo poner en marcha la pantalla VGA, con una resolucón de 256x240 y color verde. Hicimos un componente, llamado VGALEDs para controlar la mitad derecha e izquierda de la pantalla como si fuesen "píxeles gordos"

Mapearemos este componente como el registro VGALEDs, y lo haremos accesible a través del SPI para poder manejarlo desde el Arduino. ¡Es nuestro primer mini-controlador VGA por SPI!. El circuito es prácticamente igual al ejemplo 11-3

El registro VGALEDs lo hemos mapeado en la dirección 0x10 y accedemos a él usando los comandos que ya conocemos: SAP, WR y RD. Los dos bits de menor peso son lo que se llevan al componente VGALEDs que controla la VGA. Además, se sacan por los LEDs 1 y 0 para ver su estado

Este es el escenario. La Alhambra-II está conectada al monitor VGA a través de la placa AP-VGA. El Arduino Uno está conectado a la Alhambra-II, por los cables del SPI

Este es el código completo que se ejecuta en el Arduino. El bucle principal simplemente escribe los valores 0x01 y su negado para hacer que las dos mitades de la pantalla se enciendan alternativamente, cambiando cada medio segundo

#include <SPI.h>

//-- Pin usado para la seleccion del esclavo
#define SS 10

//-- Codigo de los comandos
#define SAP 0x7D  //-- Comando SET ADDRESS POINTER
#define WR  0x7E  //-- Comando de escritura en registro
#define RD  0x7F  //-- Comando de lectura en registro

//-- Direcciones de los registros
#define VGALEDS_REG    0x10  //-- Registro VGALEDs

void setup() {

  //-- Inicializar SPI
  SPI.begin();
  SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0));
}

//-- Comando generico
uint8_t cmd(uint8_t cod, uint8_t val)
{
  digitalWrite(SS, LOW);

  //-- Enviar el codigo de comando
  SPI.transfer(cod);

  //-- Enviar el valor de su parametro
  uint8_t ret = SPI.transfer(val);
   
  digitalWrite(SS, HIGH);

  return ret;
}


void SAP_cmd(uint8_t value)
{
  cmd(SAP, value);
}

void WR_cmd(uint8_t value)
{
  cmd(WR, value);
}

uint8_t RD_cmd()
{
  return cmd(RD, 0x00);
}

//-- Escritura en un registro mapeado
void write_reg(uint8_t reg, uint8_t val)
{
  SAP_cmd(reg);
  WR_cmd(val);
}

//-- Lectura de un registro mapeado
uint8_t read_reg(uint8_t reg)
{
  SAP_cmd(reg);
  return RD_cmd();
}

uint8_t vgaleds = 0x01;

void loop() 
{

  //-- Escribir un valor en el registro VGALEDs
  write_reg(VGALEDS_REG, vgaleds);

  //-- Esperar
  delay(500);

  //-- Cambiar de estado los bits de VGALEDs
  vgaleds = ~vgaleds;
}

Cargamos el circuito en la FPGA y el programa en el Arduino. En esta foto se muestra uno de los estados, cuando la mitad izquierda de la pantalla está encendida

y en este vídeo vemos la animación en acción. Además de activarse la dos mitades alternativamente, el registro VGALEDs también se muestra en los LEDs 0 y 1, que también se están alternando

Click to see the youtube video

Conexión de dos esclavos

El Bus SPI permite la comunicación entre un maestro y varios esclavos. La señal de reloj (SCLK) y la de datos (MOSI) llega a todos los esclavos. La salidas de los esclavos están unidas y llegan al maestro por MISO

El maestro tiene un cable propio para activar cada esclavo (ss). Así, si hay 3 esclavos, habrá tres señales ss para activar cada uno de ellos. Sólo puede estar activado un esclavo a la vez. Haremos un ejemplo práctico de conexión de dos esclavos a Arduino

Este es el esquema de que usaremos en los ejemplos: Un arduino UNO conectado mediante el bus SPI a dos FPGAs esclavas. El maestro debe gestionar dos señales de selección de esclavo: ss1 y ss2

En la Alhambra II los pines Dx están duplicados: hay uno hembra y otro macho. Esto simplifica mucho las conexiones. Desde el Arduino a la FPGA 1 usaremos cables macho-macho. Y para llevar los cables a la FPGA 2 usamos hembra-hembra, desde la FPGA1

Los cables amarillos son macho-macho, y son los de selección. Van desde el Arduino a la FPGA1 y a la FPGA2. Para alimentar la FPGA2 podemos tirar directamente el cable de GND y el de VCC desde la FPGA1

Este es el escenario real para hacer las pruebas: El Arduino UNO conectado por SPI a las dos placas Alhambra-II. Para las señales SS1 y SS2 de selección de los esclavos se usan los pines D10 y D9 de Arduino, respectivamente

Haremos un ejemplo sencillo de prueba. Usamos el circuito del ejemplo 14, que tiene los 4 registros mapeados. Cambiaremos la identificación: el esclavo 1 tendrá el identificador 0xAA y el esclavo 2 el 0xBB. Cargamos el hardware en ambas placas, por separado

En el software de arduino tenemos que definir los pines de selección: SS1 y SS2. Usaremos los pines 10 y 9. Los configuramos para salida y los dejamos inicialmente a 1 para que no haya ningún esclavo seleccionado

//-- Pin de los LEDs
#define LED1 7
#define LED2 6

void setup() 
{

  //-- Pines de seleccion de esclavos: son de salida
  pinMode(SS1, OUTPUT);
  pinMode(SS2, OUTPUT);

  //-- Inicialmente no hay esclavos seleccionados
  digitalWrite(SS1, HIGH);
  digitalWrite(SS2, HIGH);

  //.... otras configuraciones
}

Todas las funciones de acceso al SPI hay que modificarlas para aceptar un nuevo parámetro: el pin de selección del esclavo. Esto nos permitirá indicar con qué esclavo nos queremos comunicar

//-- Comando generico
uint8_t cmd(uint8_t ss, uint8_t cod, uint8_t val)
{
  digitalWrite(ss, LOW);

  //-- Enviar el codigo de comando
  SPI.transfer(cod);

  //-- Enviar el valor de su parametro
  uint8_t ret = SPI.transfer(val);
   
  digitalWrite(ss, HIGH);
  return ret;
}

void SAP_cmd(uint8_t ss, uint8_t value)
{
  cmd(ss, SAP, value);
}

void WR_cmd(uint8_t ss, uint8_t value)
{
  cmd(ss, WR, value);
}

uint8_t RD_cmd(uint8_t ss)
{
  return cmd(ss, RD, 0x00);
}

//-- Escritura en un registro mapeado
void write_reg(uint8_t ss, uint8_t reg, uint8_t val)
{
  SAP_cmd(ss, reg);
  WR_cmd(ss, val);
}

//-- Lectura de un registro mapeado
uint8_t read_reg(uint8_t ss, uint8_t reg)
{
  SAP_cmd(ss, reg);
  return RD_cmd(ss);
}

Y por último, el programa principal sacará un secuencia de dos estados por cada esclavo, además de leer sus identificadores y sus pulsadores SW1. Estas operaciones se realizan cada medio segundo, para hacerlo sencillo. En la consola serie se imprimen los resultados

uint8_t id_a;
uint8_t id_b;
uint8_t butt_a;
uint8_t butt_b;

void loop() 
{

  //-- Leer el identificador
  id_a = read_reg(SS1, ID_REG);
  id_b = read_reg(SS2, ID_REG);

  //-- Leer pulsadores
  butt_a = read_reg(SS1, BUTTONS_REG);
  butt_b = read_reg(SS2, BUTTONS_REG);

  //-- Escribir un valor en los LEDs
  write_reg(SS1, LEDS_REG, id_a);
  write_reg(SS2, LEDS_REG, id_b);

  //-- Encender los leds de arduino
  if (butt_a & 0x01) digitalWrite(LED1, HIGH);
  else digitalWrite(LED1, LOW);

  if (butt_b & 0x01) digitalWrite(LED2, HIGH);
  else digitalWrite(LED2, LOW);

  //-- Mostrar las lecturas en la consola
  Serial.print("ID_A: ");
  Serial.println(id_a, HEX);
  Serial.print("ID_B: ");
  Serial.println(id_b, HEX);
  delay(500);

  //-- Escribir un valor en los LEDs
  write_reg(SS1, LEDS_REG, ~id_a);
  write_reg(SS2, LEDS_REG, ~id_b);
  delay(500);
}

La secuencia en los LEDs se genera enviando su identificador y su negado. De esta forma cada esclavo mostrará una secuencia de 2 estados diferente. Cargamos el programa en el Arduino. En este vídeo lo vemos en acción

Click to see the youtube video

El refresco de los pulsadores se hace cada medio segundo, por eso hay un poco de retraso desde que se pulsa hasta que el arduino lo muestras. Se ha hecho así por simplicidad. En la consola serie podemos ver los valores leidos de cada esclavo

Este es un pantallazo estático del terminal de arduino, para ver mejor lo que se recibe al ejecutar el ejemplo

Esclavo SPI en la placa Icezum Alhambra

Aunque los ejemplos de este cuaderno técnico están hecho para la placa Alhambra-II, también son válidos para la Icezum Alhambra (y para el resto de placas ice40, por supuesto). Este es el escenario en el que se está probando el ejemplo 14

Y en este vídeo lo vemos en acción:

Click to see the youtube video

Conclusiones

El bus SPI nos permite crear nuestros propios periféricos hardware, para acceder a ellos desde cualquier maestro: arduino, Raspberry, micro-bbc, fpga... Hemos visto cómo con los bloques creados en la colección Jedi 1.6.0 es muy fácil su implementación

También es muy útil para hacer prototipos. Si NO tenemos pines suficiente para un proyecto, basta con llevarnos algunas de sus partes a otra FPGA externa, y conectarnos por SPI. Por ejemplo para tener un controlador de teclado, de ratón, de VGA, etc

Hasta ahora sólo teníamos la posiblidad de usar los chips que nos dan los fabricantes, adaptándonos a ellos. Ahora podemos crear nuestro propio hardware, para que se adapte perfectamente a nuestro proyecto. ¡Tenemos el control hasta el último bit!

Desde el punto de vista educativo y formativo, podemos crear proyectos en los que es posible entender y modificar todos los detalles hasta el nivel que queramos. Sin que haya cajas negras o partes que no sabemos qué pasa en su interior

Descargas

Todos los ejemplos se pueden descargar de este repositorio

Autor

Licencia

Créditos y agradecimientos

Enlaces

Clone this wiki locally