-
Notifications
You must be signed in to change notification settings - Fork 14
CT.5: SPI esclavo
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
- 2019-Junio-13: Version inicial del cuaderno técnico
- 2024-Abril-08: Ejemplos adaptados a la nueva toolchain: apio-0.9.3. Eliminado el error en la verificación. Probados con icestudio 0.11.3wip (futura versión 0.12). Los pantallazos de los ejemplos no se han actualizado todavía
- Fecha: 2019/Junio/14
Haz click en la imagen para ver el vídeo en Youtube
Haz click en la imagen para ver el vídeo en Youtube
- Colection-Jedi-v1.6.0.zip: Colección para este cuaderno técnico. Descargar e instalar
- Introducción
- Arduino maestro, FPGA esclava
- El bloque SPI esclavo
- Ejemplo 1: Mostrando en los LEDs el dato recibido
- Ejemplo 2: Capturando los datos en un registro
- Ejemplo 3: Envío de datos al maestro
- Ejemplo 4: Envío y recepción
- Especificaciones del módulo SPI esclavo
- Midiendo el bus SPI
- Implementando comandos
-
Registros mapeados
- Ejemplo 11-1: Registro de LEDs
- El bloque SPI-cmd-regs
- Ejemplo 11-2: Registro de LEDs con bloque SPI-cmd-reg
- Bloque Reg-addr
- Ejemplo 11-3: Registro de LEDs con bloque Reg-addr
- Ejemplo 12: Registro de LEDs por puerto serie
- Ejemplo 13: Dos registros mapeados, de lectura/escritura
- Ejemplo 14: Cuatro registros mapeados
- Ejemplo 15: Cuatro registros mapeados. Puerto serie
- Mini-controlador VGA por SPI
- Conexión de dos esclavos
- Esclavo SPI en la placa Icezum Alhambra
- Conclusiones
- Descargas
- Autor
- Licencia
- Créditos y agradecimientos
- Enlaces
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
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!
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
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!
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
#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
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
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
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
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
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)
#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! :-)
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
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
#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
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
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:
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
#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)
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)
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
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
#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 :-)
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...
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
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 :-)
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
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
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
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
//-- 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
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!!
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
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
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 |
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
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()
//-- 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
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
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
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!
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
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
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
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)
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
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
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:
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! 😀
(14-SPI-cuatro-registros-mapeados.ice)
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:
(14-SPI-cuatro-registros-mapeados.ino)
//-- 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!
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
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
(15-Serial-cuatro-registros-mapeados.ice)
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
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
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
(17-SPI-esclavo-A.ice) (17-SPI-esclavo-B.ice)
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
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
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
(18-SPI-Icezum-Alhambra-cuatro-registros-mapeados.ice)
Y en este vídeo lo vemos en acción:
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
Todos los ejemplos se pueden descargar de este repositorio
- Juan González-Gómez (Obijuan)
-
Dibujo bloques del SPI: en:User:Cburnett - Trabajo propio. Este gráfico vectorial, sin especificar según el W3C, fue creado con Inkscape., CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=1476502
-
Dibujo Maestro SPI con varios esclavos: en:User:Cburnett - Own workThis W3C-unspecified vector image was created with Inkscape., CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=1476503
- Unidad de PWM de frecuencia aproximada
- VGA Retro: Puesta en marcha. MonsterLED
- Pines de Entrada/Salida
- Control de LEDs
- SPI esclavo
- SPI Maestro
- Display SPI de 4 dígitos de 7 segmentos
- Entrada y Salida de Bits con Componentes Virtuales
- Memorias
- Entradas y pulsadores
- Señales del sistema. Medición con el LEDOscopio
- Controlador LCD 16x2
- Señales periódicas y temporización
- Buses: Medio compartido
- Memoria Flash SPI
- Conexión de LEDs en la Alhambra II. Placa AP‐LED8‐THT
- Periféricos PMOD
- Fundamentos. Sistema Unario
- Autómatas
- Pantallas de vídeo. Fundamentos. Display de 1x4 LEDs
- Pantallas de vídeo. Fundamentos. Matriz de 4x4 LEDs