CT.15: Memoria flash SPI - Obijuan/Cuadernos-tecnicos-FPGAs-libres GitHub Wiki
Descripción
Experimentos y puesta en marcha de la Memoria flash serie por SPI de la placa Alhambra II. Creación del controlador de bajo nivel desde cero
- Icestudio: Todos los ejemplos se han probado con Icestudio 0.11.1w. Usa esta versión o superior
- Colecciones:
- iceFlash: Bloques para acceder a la memoria flash serie en modo de lectura
- Ejemplos: Todos los ejemplos de este cuaderno técnico están accesibles en su repositorio en github
Historial
- 2023-Dic-28: Version inicial del cuaderno técnico
- 2024-Mayo-24: Ejemplos adaptados a la nueva toolchain: apio-0.9.4. Eliminado el error en la verificación. Probados con icestudio 0.12. Los pantallazos de los ejemplos no se han actualizado todavía
Contenido
- Introducción
- Nivel 0: SPI
- Nivel 1: SPI en BUS
- Nivel 2: Inicialización de la Flash
- Nivel 3: Lectura de la flash
- Mapa de la memoria Flash
- Primeros 8 bytes del bitstream
- Comandos de la memoria flash
- Ejemplo 11: Lectura del primer byte de identificación
- Ejemplo 12: Mostrar el byte de identificación en los LEDs
- Bloque Flash-Readid-bus
- Ejemplo 14: Lectura de un byte de la memoria Flash
- Bloque Flash-read8-bus
- Bloque Flash-read16-bus
- Bloque Flash-read32-bus
- Acceso a la Flash desde el PC
- Nivel 4: Controladores finales
- Ejemplos finales
- Conclusiones
Introducción
Las memorias Flash nos permiten almacenar tanto datos como programas para usarlas en nuestros circuitos. Si estamos diseñando nuestro propio procesador, en la flash guardamos el código máquina del programa a ejecutar, así como datos de sólo lectura, como por ejemplo sprites para mostrar en una pantalla gráfica, mensajes de texto a imprimir en la consola de salida, valores de configuración, etc.
Se usan como MEMORIAS ROM en nuestros circuitos, con la ventaja de que podemos cargarlas con nuevos datos fácilmente, antes de que arranque el circuito
El principal inconveniente es que tienen muchos pines. Así por ejemplo, una memoria flash de 4MB como la mostrada en la figura anterior, necesita al menos 31 pines: 22 para las direcciones, 8 para los datos y 1 para activar la lectura
Por esta razón se utilizan las memorias flash serie, que se comunican en serie (en vez de en paralelo), lo que requiere muchos menos pines. Una de estas memorias es la flash N25Q032A que utiliza el bus SPI (comunicación serie síncrona). ¡¡¡Pasamos de 31 a 4 pines!!!. Sólo necesitamos 4 pines disponibles en la FPGA para acceder a esta memoria
Placas de desarrollo con FPGA como la Alhambra-II, Icestick, Icebreaker... incluyen una memoria flash serie conectada por SPI a la FPGA. En ella es donde se almacena el Bitstream para configurar la FPGA al arrancar. Pero queda mucho espacio libre para situar nuestros datos
En este cuaderno técnicos crearemos un controlador básico desde cero, partiendo de la hoja de datos, para leer datos de la memoria Flash. Nuestro primer paso será bajarnos esta hoja de datos (y de otras memorias flash compatibles)
- Hoja de datos memoria N25Q032A13ESE40F: N25Q032A13ESE40F.pdf
- Hoja de datos memoria S25FL064L: S25FL064L.pdf
El diseño lo descomponemos en 4 Niveles. En cada uno se resuelve un problema y se implementan una serie de bloques de Icestudio:
- Nivel 0: Acceso al medio físico. Transmisor SPI de un byte (sys-spi-tx)
- Nivel 1: SPI en BUS. Transmisión de varios bytes, de manera independiente
- Nivel 2: Temporización inicial y configuración de la flash
- Nivel 3: Controladores de comandos de la flash: readid, read8, read16, read32. Flash_spi_controller
- Nivel 4: Controladores finales de acceso a la flash: flash_read, flash_read16, flash_read32
Nivel 0: SPI
Empezamos por el nivel físico: los pines de conexión entre la FPGA y la memoria flash, así como la temporización de los bits
Conexión física entre la FPGA y la memoria Flash
Desarrollaremos el controlador en la placa Alhambra-II que trae el chip N25Q032A (memoria flash) en el propio PCB, y conectada directamente a la FPGA
La conexión entre la FPGA y la memoria se realiza a través del bus SPI. La FPGA es el master y la memoria el esclavo. Los 4 hilos son:
- Mosi: Envío de datos desde la FPGA a la memoria flash (FPGA-> Memoria)
- Miso: Lectura de datos de la memoria (Memoria -> FPGA)
- cs: Chip Select. Activo a nivel bajo. Cuando cs=0, se accede a la memoria
- sck: Señal de reloj. Generada por la FPGA
La frecuencia máxima de funcionamiento de la flash es de 108MHZ, según se especific en su hoja de datos. Como el reloj del sistema de la Alhambra-II es de 12MHZ esta será la frecuencia que utilizaremos. Esto simplifica el controlador y su uso en nuestros circuitos
La flash funciona en tres modos: Normal, dual y cuádruple (Quad). El que utilizaremos será el normal, que es el SPI estándar, en el que se envía 1 bit en cada flanco de subida del reloj. Por tanto, son necesarios 8 ciclos de reloj para enviar un byte
En el modo dual se envían 2 bits en cada flanco de reloj (un byte tarda 4 ciclos) y en el modo cuádrupe se envían 4 bits en cada flanco de reloj (2 ciclos por byte)
La memoria flash funciona por comandos. La FPGA envía un comando a través de MOSI (bit a bit) y recibe la respuesta a través de MISO
Cronograma
En la página 16 de la hoja de datos 2 encontramos la información del cronograma para enviar un comando de la FPGA a la memoria flash:

Las siglas SDR significan Single Data Rate, o lo que es lo mismo, el modo normal en el que transmiten todos los bits en serie por un único hilo. La polaridad del reloj puede ser 0 ó 1: significa que cuando no se usa, el reloj puede estar a 0, o bien a 1. Lo importante es que en ambos casos, el bit lo lee la eeprom en flanco de subida
Además observamos las siguientes cosas:
- La transacción comienza siempre cuando cs se pone a
0 - El primer bit enviado desde la FPGA es el de mayor peso (MSb)
- La flash lee los bits en flanco de subida
- La flash devuelve los bits en flanco de bajada
- En nuestro controlador usaremos POLARIDAD=1. Cuando cs se activa, el reloj estará a 1. Cuando cs se desactiva, el reloj también pasará a 1
- El estado del reloj cuando CS NO está activa ES INDIFERENTE (puede estar en cualquier estado)
Todas estas ideas las recopilamos en este nuevo cronograma, una reconstrucción del anterior
Envío de un comando
En la página 19 de la hoja de datos 2 encontramos el cronograma para el envío de un comando de 8 bits, que no recibe ningún dato de vuelta de la flash
Para iniciar el envío del comando, la señal CS se pone a 0. A continuación se envían los bits, comenzando por el de mayor peso (bit 7). La flash los lee en el flanco de subida del reloj. Cuando se ha enviado el último (bit 0) se pone la señal CS de nuevo a 1 para indicar que hemos terminado
Decimos que esta transmisión de la FPGA a la Flash sólo tiene fase de commando. Un ejemplo es el comando de WAKEUP (0xAB) que saca a la flash del modo sleep (de bajo consumo) y que se debe enviar antes de empezar a trabajar con ella
Comando y respuesta
En este cronograma se muestra el envío de un comando y el byte que devuelve como respuesta a ese comando. Tras recibir el último bit del comando, la flash comienza a transmitir la respuesta en la línea MISO.
La FPGA captura los datos recibidos también el flanco de subida. Un ejemplo sería el comando READID que lo envía la FPGA y la flash devuelve el identificdor (En realidad este identificador es de 32 bits, pero en el cronograma se ha puesto un ejemplo de comando recibido de 1 byte)
Comando, parámetro y respuesta
Algunos comandos requieren de argumentos adiciones, como por ejemplo el comando READ que se le pasa la dirección a leer. En este ejemplo el parámetro es de 1 byte y la respueta también, pero pueden ser de diferentes tamaños
Bloque Sys-SPI-tx
Comenzamos la implementación de nuestro controlador por el bloque Sys-SPI-tx: Un transmisor de 1 byte por el SPI a la velocidad del reloj del sistema, que en la Alhambra-II es de 12MHZ
Ejemplo 1: Transmisión de 1 byte por SPI a 12MHZ
Comenzamos transmitiendo un byte por el SPI a la máxima velocidad (reloj del sistema). Para comprobar su funcionamiento utilizaremos el analizador lógico donde capturaremos 6 señales de interés:
- Las 4 señales del SPI:
sck,ss,mosi,miso - Señales de control:
startydone
La señal MISO no la estamos utilizando pero la dejamos para reservar ese canal del analizar (y no tener que estar cambiando cables para probar los diferentes ejemplos). Por ello la conectamos directamente a 1
Al apretar el pulsador SW1 se transmite el byte 0xAA por el SPI. Este es el circuito:
En las siguientes secciones lo explicamos con más detalle. Ahora de momento vamos a probarlo. Este es el montaje:

Los canales del 0 al 5 del analizador se conectan a los pines D0-D5 de la Alhambra-II, respectivamente
Realizamos las mediciones. Fijamos la frecuencia de muestreo a 24MHZ y tomamos 100 muestras. Configuramos el disparador con el flanco de subida de la señal start

Para validar su funcionamiento, en vez de comprobarlo señal por señal, activamos el decodificador del protocolo SPI de la herramienta PulseView. Lo configuramos para que la polaridad del reloj sea 1 (CPOL=1). En la parte inferior vemos lo que se ha decodificado. Por MISO se recibe 0xFF, ya que la hemos dejado a 1, y por MOSI el valor transmitido 0xAA
Funcionamiento del Ejemplo 1
El circuito está divido en diferentes partes, que las estudiamos por separado
Registro de desplazamiento
La parte principal es el registro de desplazamiento a la izquierda de 8-bits. Es el que se encarga de la transmisión propiamente dicha

Este registro carga el valor de 8 bits a transmitir por el SPI, y lo desplaza a la izquierda en cada ciclo reloj, realizando la transmisión. Funciona a la velocidad del sistema: 12Mhz
Comienza a funcionar cuando llega un tic por start. El valor que se carga es el que llega por data, que es 0xAA en este ejemplo. Una vez cargado, el desplazamiento se hace automáticamente, en cada ciclo de reloj. El registro se rellena con un 1 en cada desplazamiento a la izquierda. El bit de mayor peso (b7) es el que se envía primero (especificación de la memoria flash)
El funcionamiento se muestra en esta figura.
Transcurridos 7 ciclos de reloj desde la carga, el bit de menor peso (b0) sale por MOSI. En el siguiente ciclo la transmisión ha terminado, y el registro vuelve a tener el valor inicial de reposo (0xFF)
Estado del transmisor
El transmisor se encuentra en dos estados fundamentales: Apagado (en reposo) o Encendido (En funcionamiento), determinados por un biestable RS, que inicialmente está a 0 (Apagado)

Cada vez que llega un tic por la señal 'start' el biestable se pone a 1 para indicar que el transmisor está encendido. Se apaga al activarse la señal de done
Generación de Slave Select (SS)
La señal de busy, que indica el estado del transmisor, se usa para generar la señal ss. Basta con invertirla

La señal SS, por tanto, estará a 1 (desactivada) cuando el transmisor está apagado. Al empezar a transmitir, el transmisor se enciende y la señal SS se pone a 0, habilitando la memoria flash para recibir datos por el SPI
Unidad de Control
La unidad de control implementa los estados internos del transmisor y genera la señal DONE para indicar que la transmisión debe finalizar

La representación de los 8 estados internos del transmisor se realiza mediante 8 biestables D del sistema conectados en serie. Se han separado en dos grupos de 1 y 7 bits respectivamente. El primer grupo representa el estado de carga: el transmisor carga el valor a enviar (con lo que el bit 7 sale por MOSI). El segundo grupo indica la fase de desplazamiento
Cuando Start se activa, se captura en el primer biestable en el siguiente ciclo. A continuación se desplaza hacia la derecha en cada ciclo de reloj. Cuando llega al bit de la derecha (el de menor peso), se indica que el bit 0 del dato (b0) ya está en la línea MOSI, por lo que ya se puede terminar la transmisión. Es decir, que el bit 0 del registro de desplazamiento de la unidad de control es la señal de DONE
Este es el tablero de juego:
Inicialmente todos los biestables están a 0. En la siguiente figura se muestra la evolución del estado. En cada estado sale un bit diferente del dato por el MOSI del SPI (que se muestra en verde)
Creación del bloque Sys-SPI-tx
Ahora que ya sabemos cómo transmitir un byte por el SPI, y lo hemos comprobado con el analizador, procedemos a encapsularlo dentro del bloque Sys-SPI-tx:
Las señales del SPI: Sck, ss y Mosi se han agrupado en un BUS para que sea más fácil el uso del bloque
La implementación es la siguiente:

La señal de reloj del SPI (SCK) ahora sólo se activa cuando hay transmisión. Cuando el transmisor está en reposo se pone a 1 (Polaridad 1)
Ejemplo 2: Probando el bloque: Transmisión de 1 byte
Este ejemplo es similar al ejemplo 1: Se transmite el byte 0xAA al apretar el pulsador SW1, pero ahora está mucho más simplificado, porque toda la complejidad se ha llevado al bloque Sys-SPI-tx
Este es el resultado en el analizador lógico. Se observa que ahora la señal de reloj sólo funciona durante la transmisión y que efectivamente el resto del tiempo está a 1

Transmisión continua
El transmisor del SPI envía un byte cada vez, pero para acceder a la memoria flash tenemos que enviar tramas más largas: comando + parámetros. Nos tenemos que asegurar que este transmisor es capaz de enviar más de un byte, y en el proceso la señal SS NO se puede desactivar y además el reloj tiene que estar funcionando
Para comprobarlo utilizamos el ejemplo 3
Ejemplo 3: Transmisión continua
Este ejemplo es igual al anterior, pero ahora la señal done se realimenta a la entrada del bloque Sys-SPI-tx de forma que está todo el rato transmitiendo. Inicialmente el sistema está en reposo. Al apretar el pulsador sw1 se transmite el primer byte y a partir de ahí continua transmitiendo indefinidamente
(03-SPI-byte-block-tx-continua.ice)

En la captura del osciloscopio podemos apreciar que efectivamente la señal SS permanece activa durante la transmisión de todos los bytes

Para tomar una nueva lectura hay que resetear la placa, y volver a apretar SW1
Nivel 1: SPI en Bus
En este nivel enviamos bytes por el SPI, pero a través de un BusF. Esto nos permite simplificar y ordenar los circuitos. Con este bus, circuitos independientes pueden acceder al SPI
El controlador de nivel 0 del SPI utiliza estas 4 señales:
- Data: Dato de 8 bits a transmitir por el SPI
- Start: Señal de control para indicar el comienzo de la transmisión
- Busy: Estado del transmisor spi (transmitiendo dato o en reposo)
- Done: Envío del dato completado
Vamos a agrupar estas señales en un BUS de 11 bits. Las señales Data y Start van desde los circuitos hacia el transmisor del SPI, mientras que Busy y done van del transmisor SPI hacia el resto de circuito. Son señales de realimentación. Por ello utilizaremos un Bus Unidireccional con Feeback (BusF). Puedes encontrar más información en el cuaderno técnico CT14: Buses: Medio compartido
Estructura del bus
Este bus de 11 bits lo organizamos de la siguiente manera (realmente el cómo se ordenen los cables da igual, lo importante es definir un orden y crear los componentes para acceder a ellos)
- Busy: Bit 10
- Done: Bit 9
- Start: Bit 8
- Dato: Bits 7-0
Para acceder a estas señales creamos los bloques Split y Join correspondientes, que se encuentran en la colección iceFlash
Bloque de acceso al bus: access-2-9
Para acceder al Bus del SPI utilizamos el componente access-2-9 de la carpeta BUSF de la colección iceBus. El Bus del SPI es de 11-bits, pero los 2 de mayor peso (busy y done) son las señales de realimentación. El resto de señales (start y data) son las que hay que inyectar en el bus (9 bits)
Cuando se activa oe, el sub-bus de 9 bits con las señales start y data se inyectan en el Bus para acceder al transmisor del SPI
Esta es su implementación:

El bus de entarda (bus_i) se divide en sus dos componentes: La parte de Feedback (f) y el bus con los datos y start. La parte de feedback se lleva tal cual al bus de salida (bus_o). El sub-bus de datos se conecta a un elemento de acceso por donde entran los nuevos datos. Finalmente se agrupan ambas partes para volver a formar el bus de salida de 11 bits
Bloque Sys-SPI-tx-BUS
Ampliamos el bloque sys-SPI-tx para permitir su conexión en un BUSF
Aunque las señales de busy y done van por el bus de salida, pero también se han sacado directamente del bloque para facilitar las pruebas
Ejemplo 4: Transmisión de 2 bytes independientes
Como ejemplo de manejo del bloque sys-SPI-tx-bus vamos a enviar los bytes 0x01 y 0x02 de forma independiente al apretar los pulsadores SW1 y SW2 respectivamente
Hay dos circuitos independientes, conectados al SPI a través de sub-buses de 9 bits. Cada uno de ellos vuelca al bus del SPI su valor, al apretar los pulsadores correspondientes
Esto es lo que se lee en el analizador al apretar el pulsador SW1:

Al apretar el pulsador SW2 comprobamos que se lee byte 0x02

Escritura de bytes
Para transmitir fácilmente bytes por el nuevo transmisor SPI de BUS, utilizamos los bloques SPI-write-byte y SPI-write-k-byte. Estos bloques son la clave: con ellos podemos enviar datos muy fácilmente, y construir los comandos para el control de la flash en los niveles superiores
Bloque SPI-write-byte
Este bloque realiza la escritura de un byte que se le pasa como entrada
Su implementación se muestra en esta figura:

El circuito se activa al recibir un tic por write. El dato y la señal de write se inyectan en el bus para realizar la transmisión. El estado del circuito se cambia a ocupado (y la señal busy se activa)
Cuando la transmisión termina, el transmisor del SPI activa done que llega por el Bus al bloque, y se saca por la salida done. Cuando el bloque está en reposo la señal done se ignora
De esta forma cada bloque spi-write-byte sabe de manera independiente cuándo se ha realizado la transmisión y los podemos conectar fácilmente en cadena para enviar varios bytes, como veremos en los ejemplos
Bloque SPI-write-k-byte
Este bloque es igual que el SPI-write-byte pero el byte a enviar se pasa como parámetro en vez de como una entrada. Esto simplifica algunos circuitos
La implementación se realiza mediante un bloque k para definir la constante conectada a la entrada del bloque SPI-write-byte:

Ejemplo 5: Write-bytes: Transmisión de dos bytes independientes
Este es el mismo ejemplo que el 4: se transmiten los bytes 0x01 y 0x02 al apretar los botones SW1 y SW2 respectivamente, pero usando los bloques SPI-write-byte
Vemos que ahora se simplifica la conexión de los circuitos al SPI. En el analizador lógico se obtiene exactamente lo mismo que el ejemplo 4
Ejemplo 6: Transmisión de dos bytes consecutivos
En este ejemplo se transmiten 2 bytes consecutivamente, como parte de la misma trama SPI. Al apretar el pulsador SW1 se envía el valor 0x01 y a continuación el 0x02
(06-write-two-consecutive-bytes.ice)

Esta es la ventaja de utilizar los bloques spi-write-byte. Para enviar dos bytes consecutivos basta con conectar el done del primero con el write del segundo, de forma que cuando se termina de enviar el primero, se comienza automáticamente con el segundo
Así nos ahorramos la necesidad de colocar un controlador específico
Esto es lo que vemos en el analizador lógico:

La señal ss permanece a nivel bajo (activa) durante el envío de los 2 bytes. En realidad estamos enviando un único comando formado por 2 bytes (0x01 y0x02)
Ejemplo 7: Transmisión de dos bytes independientes
Si ahora queremos enviar dos tramas independientes, una llevando el byte 0x01 y la otra el byte 0x02, hay que realizar una espera al terminar la primera transmisión. De esta forma la señal ss se pone a uno para terminar la trama 1. Tras esta pausa se realiza el envío del segundo byte
Para realizar esto basta con conectar un biestable D del sistema entre los dos bloques SPI-write-byte, que realiza una pausa de 1 ciclo
(07-write-two-independent-bytes.ice)

Ahora en el analizador lógico vemos perfectamente las dos tramas independientes:

Nivel 2: Inicialización de la Flash
Antes de leer la memoria flash es necesario inicializarla. Para ello basta con enviar el comando WAKEUP (0xAB) al arrancar el circuito. Esta es la misión del controlador de este nivel
En la tarjeta Alhambra-II, la flash entra en en modo de bajo consumo cada vez que se enciende o resetea la placa. Por ello, antes de usar la memoria flash es necesario activarla con el comando WAKEUP
Ejemplo 8: Retraso de 64 ciclos
Una vez reseteada la memoria flash, hay que esperar un tiempo hasta que se activa y comienza a recibir comandos. El tiempo máximo es de unos 5µs aproximadamente (64 ciclos del reloj a 12Mhz). Una vez transcurridos esos 64 ciclos, ya se pueden enviar comandos a la flash con normalidad.
Por tanto, necesitamos un circuito que realice esta pausa. Esto se puede realizar de muchas formas. Una es como la indicada en este ejemplo: Un contador del sistema de 6 bits, que cuenta desde 0 hasta 63
Cuando el contador alcanza la cuenta máxima (63) se captura un 1 en el biestable y se reinicia el contador. De esta forma el contador se queda parado en la cuenta inicial 0
En el analizador lógico se comprueba que el tic de inicialización efectivamente se activa transcurridos los 64 ciclos

Ejemplo 9: Inicialización de la flash
Este circuito realiza la inicialización completa de la flash, y la deja lista para usar. Esto lo hace de forma automática, cuando el circuito arranca en el ciclo 0
Lo comprobamos en el analizador:

Bloque flash-controller0
Lo encapsulamos todo en el primer controlador de la flash: el bloque flash-controller0. Este bloque envía el comando WAKEUP a la flash (transcurridos 64 ciclos) y queda a la espera de recibir comandos a escribir a través del bus
Esta es la implementación

Ejemplo 10: Bloque flash-controller0: Inicialización de la flash
Este es el mismo ejemplo que el 9, pero usando el nuevo controlador flash-controller0. Al arrancar el circuito el controllador se inicializa automáticamente, y queda listo para recibir comandos del usuario. Ahora todo queda mucho más simplificado
En el analizador lógico se obtiene lo mismo que en el ejemplo 9
Ya tenemos nuestro primer controlador, listo para enviar comandos de la flash en el próximo nivel
Nivel 3: Lectura de la Flash
En este nivel construiremos los controladores que nos permitirán leer bytes, medias palabras o palabras de la memoria flash. Como siempre, los iremos construyendo poco a poco, a partir del controlador del nivel 2
Mapa de memoria
El tamaño de la memoria flash es de 4Mbytes. Sus direcciones son de 22bits (casi 3 bytes), y van desde la 0x00_00_00 hasta la 0x3F_FF_FF. El bitstream está almacenado a partir de la dirección 0, y tiene un tamaño de 32KB para la Icezum Alhambra y Icestick, y de 136KB para la Alhambra II. Eso nos da una idea de la cantidad de espacio disponible que queda para usar en nuestros circuitos. ¡¡Unos 3.8Mbytes libres!!!
Tomaremos esta dirección como referencia: 0x04_00_00. Las posiciones de esta dirección en adelante las tenemos disponibles (en realidad desde un poco antes, pero por comidad usaremos esta dirección como límite)
Primeros 8 bytes del Bitstream
El bitstream de configuración de la FPGA se almacena en la memoria flash, comenzando en la dirección 0x00_00_00. Para la tarjeta Alhambra-II observamos que los primeros 8 bytes de este bitstream tienen este valor: FF 00 00 FF 7E AA 99 7E
El bitstream que se genera desde icestudio y se graba en la flash se encuentra dentro de la carpeta ice-build (en nuestro directorio de trabajo) y tiene el nombre de hardware.bin. Si volcamos las primeras posiciones de este fichero comprobamos que efectivamente aparece el valor antes mencionado

Este es el comando linux utilizado para ver el fichero hardware.bin:
hd hardware.bin | head
En esta tabla se muestran los mismos valores asociados a su dirección correspondiente:
| Direccion flash | Byte |
|---|---|
| 00_00_00 | FF |
| 00_00_01 | 00 |
| 00_00_02 | 00 |
| 00_00_03 | FF |
| 00_00_04 | 7E |
| 00_00_05 | AA |
| 00_00_06 | 99 |
| 00_00_07 | 7E |
Nos resultará muy útil para utilizarla como valores de control para saber si la lectura la estamos realizando correctamente. Así, por ejemplo, sabemos que en la dirección 0x000004 debe estar el byte 0x7E (que podremos enviar a los LEDs para comprobar si es correcto o no)
Comandos de la memoria flash
La memoria SPI se controla mediante comandos. En la hoja de datos están los detalles de todos. En este cuaderno técnico sólo usaremos 3 para las pruebas: Activar (WAKEUP), leer el identificador (READID) y leer de una dirección(READ). Cada comando tiene su código, sus parámetros y los bytes para las respuestas
| Nombre | Código comando | Parámetros | Bytes de respuesta | Descripción |
|---|---|---|---|---|
| WAKEUP | 0xAB | Ninguno | Ninguno | Activar la flash. Es necesario ejecutarlo después de haber hecho un reset o el encendido de la placa. De lo contrario no se podrá leer nada |
| READID | 0x9F | Ninguno | 4 ó más | Devolver el identificador del chip de la flash |
| READ | 0x03 | Dirección (3 bytes): A2 A1 A0 | 1 ó más | Devolver los bytes que se encuentran en la dirección A2_A1_A0 |
En esta tabla se muestra un ejemplo de uso de cada comando. Son los que usaremos en las pruebas
| Comando | Ejemplo | Descripción |
|---|---|---|
| WAKEUP | 0xAB | Sólo con enviarlo la memoria se activa |
| READID | 0x9F 0xFF 0xFF 0xFF | Se reciben 4 bytes. El primero es basura. Los 3 restantes contienen la identificación. Ej: ID válido: EF 40 16 |
| READ | 0x03 0x04 0x00 0x00 0xFF | Lectura del byte que se encuentra en la dirección 0x040000 |
| READ | 0x03 0x00 0x00 0x04 0xFF | Lectura del byte de la dirección 0x04 |
Ejemplo 11: Lectura del primer byte de identificación
En este ejemplo enviamos el comando READID seguido de un byte cualquiera (0xFF: dummy) para obtener el primer byte de la identificación, que se corresponde con el código del fabricante de la flash. El objetivo es ver en el analizador Lógico este código del fabricante, para comprobar que la comunicación con la flash funciona en el sentido flash->FPGA
El comando READID se envía cada vez que se apreita el pulsador SW1:
(11-read-id-manufacturer-analizador.ice)

Este es el resultado en el analizador lógico:

El código del fabricante leído es 0xEF. El identificador completo se puede ver en la ventana Command output que aparece en la opción View/Command output de Icestudio, tras realizar la carga en la FPGA
En la placa que estoy usando el identificador es: EF 40 16 00

Ejemplo 12: Mostrar el byte de identificación en los LEDs
La lectura del byte de identificación se hace a través de la entrada serie MOSI. Esta señal se introduce por un registro de desplazamiento de 8 bits para convertirlo de serie a paralelo. Cuando se activa la señal manufac se captura el último bit. Un ciclo después se captura el valor leído en un registro de 8 bits
Este registro de 8 bits almacena el código del fabricante (0xEF) y lo muestra en los LEDs que tiene conectados a su salida
(12-read-id-manufacturer-LEDs.ice)

La lectura ahora se hace automáticamente al arrancar el circuito. Por ello debemos ver en los LEDs de la Alhambra-II el código 0xEF
El analizador se ha configurado para empezar a capturar cuando la señal ss se activa (flanco de bajada). Por ello la captura comienza con el comando de WAKEUP. Este es el resultado en el analizador lógico

Bloque Flash-readid-bus
En este bloque se ha encapsulado todo lo necesario para leer 3 bytes (24 bits) del identificador de la flash
Esta es la implementación:

Ahora hay 3 registros de 8 bits, cada uno para capturar el byte del identificador correspondiente, recibido a través del conversor serie-paralelo. En el Bus del SPI en total hay que escribir 4 bytes: uno con el código del comando READID (0x9F) y los otros tres para generar el reloj para la recepción de los 3 bytes del identificador
Estos tres bytes se agrupan en un bus de 24 bits y se devuelven por la salida id
Ejemplo 13: Mostrar el identificador de la flash en los LEDs
Este es un ejemplo de uso del bloque Flash-readid-bus. Al arrancar el circuito se leen 3 bytes del identificador de la flash, y se muestran en los LEDs. Por defecto se muestra primero el byte de menor peso. Con el pulsador SW1 se cambia al siguiente
Los valores leídos, para esta flash, son: primero 0x16, luego 0x40 al apretar el pulsador y finalmente 0xEF al volver a apretarlo. Si se aprieta de nuevo se vuelve al byte 0
Esto es lo que aparece en el analizador lógico

Ejemplo 14: Lectura de un byte de la memoria flash
En este ejemplo se lee el byte situado en la dirección 0x00_00_04 de la flash, y se muestra en los LEDs. El proceso de lectura es similar al de la lectura del identificador, pero ahora hay que enviar el comando READ seguido de los 3 bytes de la direccion. En total necesitamos 5 transacciones con el SPI: Envío del comando READ, envío de los 3 bytes de la dirección y envío de un valor dummy para generar el reloj para la lectura
Al apretar el pulsador SW1 comprobamos que efectivamente sale el valor 0x7E por los LEDs, que en binario es: 01111110
Y este es el resultado en el analizador lógico:

Bloque Flash-read8-bus
Este es el bloque para leer un byte de una posición de memoria en la flash. Se ha encapulado en un único bloque el envío del comando, la dirección, el dummy byte, así como la lectura del byte:
Esta es la implementación del bloque:

Ejemplo 15: Flash-read8-bus: Volcado manual de la flash en los LEDs
Ejemplo de uso del bloque Flash-read8-bus. Mostrar en los LEDs los bytes que hay en la memoria a partir de la dirección 0. Cada vez que se aprieta el pulsador se pasa a la siguiente dirección
El patrón de salida en los LEDs es el indicado en la sección Primeros 8 bytes del Bitstream: FF,00,00,FF,7E,AA,99,7E...
Bloque Flash-read16-bus
Este bloque realiza la lectura de una media palabra (16-bits) de la memoria flash, asumiendo una ordenación little endian: es decir, que el byte de menor peso está en la dirección de lectura, y en la siguiente dirección está el byte de mayor peso
Esta es la implementación:

Es similar a la del bloque Flash-read8-bus, pero enviando un byte adicional de dummy y leyendo un segundo valor por el pin miso
Ejemplo 16: Lectura de una media palabra (16-bits)
Este circuito lee la media palabra que se encuentra situada en la dirección 0x04, que se corresponde con el Bitstream. Sabemos que la palabra ahí almacenada es: 0xAA7E, con ordenación little-endian. Es decir, que en la dirección 0x04 se encuentra el byte 7E y en la dirección 0x5 el byte AA
Al apretar el pulsador sw1 se realiza la lectura y se muestra en los LEDs
Con el pulsador sw2 se cambia el byte a visualizar en los leds (inicialmente el byte bajo, y al apretar se cambia al byte alto)
En la Alhambra-II Veremos el valor 0x7E. Al apretar el pulsador sw2 aparece 0xAA
Esto es lo que se muestra en el analizador lógico:

Bloque Flash-read32-bus
Este bloque realiza la lectura de una palabra (32-bits) de la memoria flash, asumiendo una ordenación little endian: es decir, que el byte de menor peso está en la dirección de lectura, y el de mayor peso está 3 bytes más arriba
Esta es la implementación:

Ejemplo 17: Lectura de una palabra (32-bits)
Este circuito lee la palabra que se encuentra situada en la dirección 0x04, que se corresponde con el Bitstream. Sabemos que la palabra ahí almacenada es: 0x7E99AA7E, con ordenación little-endian. Es decir, que en la dirección 0x04 se encuentra el byte 7E, en 0x05 AA, en 0x06 99 y en 0x07 7E
Al apretar el pulsador sw1 se realiza la lectura y se muestra en los LEDs
Con el pulsador sw2 se cambia el byte a visualizar en los leds
En la Alhambra-II Veremos el valor 0x7E. Al apretar el pulsador sw2 sucesivamente vemos AA, 99 y 7E
Esto es lo que se muestra en el analizador lógico:

Acceso a la Flash desde el PC
La memoria flash de la placa Alhambra II y compatibles (icebreaker, icestick...) se puede leer y grabar utilizando la herramienta iceprog, que está también integrada en apio. Se tiene acceso a esta herramienta con el comando apio raw "iceprog"
Volcado de la Flash a un fichero Binario
La lectura de la flash se realiza utilizando el parámetro -o para especificar la dirección de acceso y -R para el número de bytes a leer. Así por ejemplo, para volcar los 8 primeros bytes a partir de la dirección 0x000000 utilizamos este comando: apio raw "iceprog -o 0x0 -R 8 dump.bin":
obijuan@Hoth:~
$ apio raw "iceprog -o 0x0 -R 8 dump.bin"
init..
cdone: high
reset..
cdone: low
flash ID: 0xEF 0x40 0x16 0x00
reading..
done.
cdone: high
Bye.
obijuan@Hoth:~
$
Esto genera el fichero binario dump.bin de un tamaño de 8 bytes (que es lo que hemos volcado)
obijuan@Hoth:~
$ ls -l dump.bin
-rw-rw-r-- 1 obijuan obijuan 8 dic 21 18:59 dump.bin
Visualización de ficheros binarios
Para visualizar ficheros binarios hay que utilizar un editor/visualizador hexadecimal. En Linux tenemos el comando hd en la línea de comandos:
$ hd dump.bin
00000000 ff 00 00 ff 7e aa 99 7e |....~..~|
00000008
En linux también se puede utilizar el programa Ghex. Esta es la pinta que tiene al abrir el fichero anterior dump.bin:

El Visual Code dispone de la extensión Hex Editor que permite visualizar/editar ficheros binarios

También existen aplicaciones web on-line, como por ejemplo https://hexed.it/

Creando un fichero binario de prueba
Renombramos el fichero dump.bin a testFlash.bin y lo editamos para poner estos bytes: 81 42 24 18 08 04 02 01

Este será el fichero que utilizaremos para hacer pruebas También se puede descargar desde aquí: testFlash.bin
Grabando un fichero en la flash
Para grabar un fichero binario hay que especificar la dirección de comienzo con el parámetro -o y el nombre del fichero binario. Como ejemplo vamos a grabar el fichero testFlash.bin en la dirección 0x040000. Usamos el comando: apio raw "iceprog -o 0x040000 testFlash.bin":
obijuan@Hoth:~
$ apio raw "iceprog -o 0x040000 testFlash.bin"
init..
cdone: high
reset..
cdone: low
flash ID: 0xEF 0x40 0x16 0x00
file size: 8
erase 64kB sector at 0x040000..
programming..
done.
reading..
VERIFY OK
cdone: high
Bye.
obijuan@Hoth:~
$
Nivel 4: Controladores finales
En este nivel se encuentran los controladores finales para acceder a la memoria flash en lectura, de forma fácil y directa
Hay 4 controladores independientes, según la operación que se quiera realizar
- Flash-readid: Lectura del identificador de la flash. Este bloque es útil para hacer pruebas
- Flash-read8: Lectura de un byte
- Flash-read16: Lectura de una media palabra (16-bits)
- Flash-read32: Lectura de una palabra (32-bits)

Flash-readid
Este controlador lee los 3 bytes del identificador de la flash. Su única utilidad es hacer pruebas de funcionamiento
Esta es la implementación:

Ejemplo 18: Lectura del identificador en los LEDs
Este ejemplo es igual que el 13: Al arrancar el circuito se leen los 3 bytes del identificador de la memoria flash y se muestra en los LEDs. Con el pulsador SW1 se selecciona el byte que se quiere ver. Estos bytes son: EF 40 16
Esta es la medida con el analizador lógico:

Flash-Read8
Este controlador se usa para leer un byte de la memoria flash. El bloque es similar para la lectura de medias palabras (16-bits) y palabras (32-bits). La única diferencia es el tamaño del pin dat
Esta es la implementación:

Ejemplo 19: Lectura de un byte de la dirección 0x04
Al arrancar el circuito se lee el byte de la direción 0x000004 (que es 7E) y se muesta en los LEDs
Este es la medida con el analizador lógico:

Flash-read16
Este controlador se usa para leer una media-palabra (16-bits) de la memoria flash
Esta es la implementación:

Ejemplo 20: Lectura de una media palabra de la dirección 0x04
Al arrancar el circuito se lee la media palabra de la direción 0x000004 (que es AA7E) y se muesta en los LEDs
Este es la medida con el analizador lógico:

Flash-read32
Este controlador se usa para leer una palabra (32-bits) de la memoria flash
Esta es la implementación:

Ejemplo 21: Lectura de una palabra de la dirección 0x04
Al arrancar el circuito se lee la palabra de la direción 0x000004 (que es 7E99AA7E) y se muesta en los LEDs
Este es la medida con el analizador lógico:

Ejemplos finales
Ya tenemos creados nuestros controladores FINALES. Ahora toda la complejidad está dentro de ellos, y nos resultará muy fácil incluirlos en nuestros propios circuitos. A partir de ahora la memoria flash se comporta como una memoria normal, a la que accedemos indicando la dirección y la orden de lectura
Para probar estos ejemplos previamente debemos grabar en la dirección 0x040000 el fichero Testflash.bin que contiene estos 8 bytes: 81 42 24 18 08 04 02 01 (Ver apartado Grabando un fichero en la flash)
Ejemplo 22: Volcado de la memoria en los LEDs
En este ejemplo navegamos por la memoria flash utilizando las teclas sw1 y sw2. La dirección inicial es la 0x040000 donde previamente hemos grabado el fichero Testflash.bin con la secuencia 81 42 24 18 08 04 02 01
La dirección inicial de acceso (0x040000) se almacena en un registro de 24 bits con incremento y decremento. Es un registro especial que se incrementa en 1 cuando llega un tic por su entrada inc, y se decrementa también en 1 al recibir un tic por dec
Al arrancar el circuito se accede a la dirección 0x040000 para mostrar este primer valor. En los LEDs veremos el valor 0x81 (10000001)
En este vídeo de youtube lo vemos en acción:
Ejemplo 23: Navegación de 16 bits
Este ejemplo es el mismo que el 22, pero ahora se leen medias palabras (16-bits). Se comienza en la dirección 0x040000. Con el pulsador sw2 se avanza a la siguiente palabra, que está en la dirección 0x040002, la siguiente en la 0x040004 etc...
Con un pulsador externo conectado al pin D7 se selecciona qué byte observar (byte alto o byte bajo)
Las medias palabras almacenas se muestran en esta tabla:
| Direccion | Media palabra (16-bits) |
|---|---|
| 0x040000 | 0x4281 |
| 0x040002 | 0x1824 |
| 0x040004 | 0x0408 |
| 0x040006 | 0x0102 |
| 0x040008 | 0xFFFF |
La dirección inicial de acceso (0x040000) se almacena en un registro de 24 bits con incremento y decremento. Es un registro especial que se incrementa o decrementa en 2 unidades, según la orden (tic) recibida por sus entradas inc/dec
Al arrancar el circuito se accede a la dirección 0x040000 para mostrar este primer valor. En los LEDs veremos el valor 0x81 (10000001) (Que es el byte de menor peso de la primera media palabra)
En este vídeo de youtube lo vemos en acción:
Ejemplo 24: Navegación de 32 bits
Este ejemplo es el mismo que el 23, pero ahora se leen palabras (32-bits). Se comienza en la dirección 0x040000. Con el pulsador sw2 se avanza a la siguiente palabra, que está en la dirección 0x040004, la siguiente en la 0x040008 etc...
Con un pulsador externo conectado al pin D7 se selecciona qué byte observar (byte0 (menor peso), byte1, byte2 ó byte3 (mayor peso)
Las palabras almacenas se muestran en esta tabla:
| Direccion | Media palabra (16-bits) |
|---|---|
| 0x040000 | 0x18244281 |
| 0x040004 | 0x01020408 |
| 0x040008 | 0xFFFFFFFF |
La dirección inicial de acceso (0x040000) se almacena en un registro de 24 bits con incremento y decremento. Es un registro especial que se incrementa o decrementa en 4 unidades, según la orden (tic) recibida por sus entradas inc/dec
Al arrancar el circuito se accede a la dirección 0x040000 para mostrar este primer valor. En los LEDs veremos el valor 0x81 (10000001) (Que es el byte de menor peso de la primera palabra)
En este vídeo de youtube lo vemos en acción:
Ejemplo 25: Secuencia en los LEDs
En este último ejemplo se recorren las 8 posiciones de memoria flash: desde la dirección 04_00_00 hasta las 04_00_07 automáticamente, mostrando su contenido en los LEDs cada medio segundo. El resultado es que se muestra una secuencia en los LEDs. Esta secuencia depende de los valores almacenados en estas 8 posiciones
La secuencia que se muestra en los LEDs es: 81 42 24 18 08 04 02 01
En esta animación se muestra el funcionamiento:

Conclusiones
Ya disponemos de un controlador para leer información de la flash. Además, por el camino hemos aprendido cómo hacer controladores de este tipo desde 0. Estos componentes nos van a ser muy útiles para nuestros sistemas basados en microprocesadores, y también para almacenar datos para mostrar en las pantallas gráficas
Enlaces
- Log de la Memoria Flash: experimentos iniciales
- Apartado Memoria Flash SPI dentro del Cuaderno técnico CT6: SPI Maestro






















