Translate

lunes, 8 de abril de 2013

INTERFAZ SPI con memoria flash (M25P80)


Las memorias flash es un tipo de memoria electrónica no volátil, derivada de las EEPROM, que que puede ser borrada y reprogramada electrónicamente (http://en.wikipedia.org/wiki/Flash_memory). El tipo más común de memoria flash es el tipo NAND que es la base de las tarjetas de memoria,  USB oendrives y discos de estado sólido. Al contrario que una memoria EEPROM las memorias flash de tipo NAND pueden ser escritas o borradas en bloques más pequeños que todo el dispositivo (aunque mayores que los bytes individuales).  En todos estos casos la memoria va acompañada de un controlador que hace invisible el manejo a bajo nivel de la memoria. El usuario se reduce a escribir/leer sectores lógicos. En esta entrada vamos a escribir algunas rutinas para conectar una memoria flash (M25P80, 1 Mbyte) con un PIC usando una comunicación SPI. Al contrario que en los casos citados antes vamos a manejar directamente los sectores físicos de la memoria, por lo que además de los aspectos de la comunicación SPI aprenderemos también algunas peculiaridades de este tipo de memorias. 

Para nuestro desarrollo usaremos una breakboard (de MikroElektronica, figura adjunta, unos $10) que nos evita tener que soldar y nos da los pines correctamente espaciados y etiquetados.  Podemos ver la alimentación (3.3V) y GND, así como las conexiones SPI (~CS, SCK, MISO, MOSI). El único pin (que no usaremos) no identificable es ~HOLD (que permite dejar en suspenso una comunicación. El IC tiene un pin adicional ~WP (Write Protection) que permite implementar una protección hardware de los datos. En esta tarjeta está atado a 3.3V por lo que la tarjeta está "desprotegida" a nivel hardware (aunque es posible implementar protección parcial o total de datos a través del software). 

Código asociado a esta entrada: spi_flask.c



-------------------------------------------------------------------------------------------------------------------------------


Hardware:

Un problema con el podemos encontrarnos habitualmente es la diferencia de niveles entre los 5V de muchos microcontroladores y los 3.3V presentes en muchos periféricos (como por ejemplo esta memoria flash).

Si estamos usando un micro a 3.3V es simplemente una cuestión de conectar las líneas directamente (MOSI de SDO de micro a SDI de memoria) , MISO de SDO de tarjeta a SDI de micro, SCK con SCK) y usar la misma alimentación (3,3V) para ambos. Si el micro va a 5V (como es nuestro caso), precisaremos:

a)      Un regulador a 3.3V para la alimentación de la memoria
b)      Algún dispositivo/electrónica de cambio de niveles.

En el caso SPI, al contrario que en otros sistemas (como por ejemplo I2C) hay una solución cutre al problema del cambio de niveles, debido a que las líneas son unidireccionales. Sabemos a priori que MOSI, CS y SCL (micro -> tarjeta) son de entrada (desde el punto de vista del periférico) y MISO es de salida (tarjeta -> micro).

En las líneas de entrada podemos disponer de simples divisores de voltaje (1/3, 2/3) que reduzcan los 5V a 2*5/3 =3.3V. La línea de salida de la tarjeta (SDO) podemos conectarla directamente a la entrada SDI del PIC (cruzando los dedos para que el nivel alto de la tarjeta supere el umbral de entrada del pin del PIC).

En la figura se adjunta esquema del montaje usado:




Las resistencias R1 (2K2)  y R2 (3K3) configuran un divisor de tensión para pasar de 5V a 3.3V.  Las resistencias R3 (pull-ups de 10K) mantienen a nivel alto (5V o 3.3V) las líneas de datos en ausencia de actuación por parte del procesador o la memoria.


Software: configuración del puerto SPI:

Obviamente el PIC será master en la comunicación SPI. Las especificaciones indican que la memoria soporta hasta un máximo de 25 MHz de reloj, por lo que podemos usar sin problemas la frecuencia máxima Fosc/4.

En cuanto al modo SPI a usar, la siguiente gráfica del datasheet nos da la información necesaria (C es clock, D es SDI, datos de entrada en tarjeta,  y Q es SDO, datos de salida hacia el PIC):




La memoria admite dos polaridades de reloj (CPOL=CKP=0/1) como se observa en las dos trazas superiores y muestrea los datos que le llegan (D) en las transiciones de subida. Por lo tanto el micro debe usar el criterio LowToHigh (L2H=1, colocar datos en transición de subida del reloj).  Recordando que CKE = CPOL xor L2H las posibilidades son:

CPOL=CKP=1     L2H=1       CKE = 1 xor 1 = 0   -> CPHA = 1
CPOL=CKP=0     L2H=1       CKE = 0 xor 1 = 1   -> CPHA = 0

Luego los dos modos SPI admitidos son (0,0) y (1,1) expresados en formato (CPOL,CPHA), como por otra parte se nos indica amablemente en la parte superior izquierda de la gráfica.

El otro parámetro a determinar es SMP, relacionado con el muestreo de los bits entrantes por el PIC. Para ello nos fijamos en los datos que manda la memoria (Q).  Si elegimos modo SPI(0,0) (traza superior) vemos que el momento adecuado para muestrear a Q en el centro del periodo de reloj, por lo que haremos SMP=0. Si por el contrario escogemos usar modo SPI(1,1) (2ª traza) el momento adecuado es al final del periodo (SMP=1).

Usando las rutinas anteriores, una posible inicialización del puerto SPI sería:

void FLASH_spi_init(byte clock)
{
 M25P80_CS_dir=0; deselect_M25P80; // Configure CS line as output and set it low.
 spi_master(clock);  // Configure SPI as master and set clock
 spi_mode(0,0,0);    // SPI mode (0,0). Sample at mid period
 spi_enable();       // Enable SPI
}

Previamente debemos definir (#defines) dos variables M25P80_CS y M25P80_CS_dir  para informar al resto de las rutinas de que pin estamos usando como ChipSelect (CS) y su correspondiente bit de dirección TRIS.

// CS line
#define M25P80_CS       LATCbits.LATC2
#define M25P80_CS_dir   TRISCbits.TRISC2
#define select_M25P80   M25P80_CS=0
#define deselect_M25P80 M25P80_CS=1

Descripción de la memoria:

La capacidad de esta memoria flash es de 1Mbyte, organizada en páginas de 256 bytes. Cada 256 páginas forman un sector (65536 bytes). Hay un total de 16 sectores, sumando 16x65536 = 2^20 = 1 Mbyte. Precisaremos por lo tanto  20 bits para la dirección de cualquier byte de la memoria.

Además de la zona de datos hay un registro de status de 8 bits que podemos consultar y modificar. Los principales bits de este registro son:

·    BP 0:2  estos tres bits permiten proteger toda (111), nada (000) o partes (XXX) de la memoria imposibilitando su cambio o borrado.

·    WEL (Write Enable Latch): es imprescindible poner a 1 este bit previo a toda operación que suponga reprogramar o borrar datos. Actúa como un seguro.

·    WIP (Write in Progress) se pone a 1 mientras la memoria está ocupada en un ciclo de programación y borrado. Sirve para que el micro sepa cuando la memoria está lista para aceptar nuevos comando.


Software: Envío de órdenes a la memoria:

La estructura de un intercambio SPI micro/memoria responde a la siguiente secuencia:

1)      El micro envía una instrucción (1 byte)
2)      Opcionalmente si la instrucción lo requiere se manda una dirección (3 bytes = 24 bits)
3)      Se envían o reciben datos según el tipo de instrucción.

Todo intercambio es precedido por una selección del dispositivo (CS=0) y terminado con CS=1.

Solo el paso 1 es obligatorio. Los pasos 2/3 son opcionales y dependen del tipo de instrucción. Algunas instrucciones precisan todos los pasos (1-2-3) como por ejemplo la lectura de una página de datos.
Otras son de tipo 1-3, como por ejemplo la lectura del registro de status. Finalmente otras son del tipo 1, instrucciones que no precisan argumentos y no devuelven ningún dato, como poner a 1 el bit WEL.

La imagen adjunta muestra la tabla de instrucciones, indicando las que precisan una dirección como argumento y cuales esperan recibir o devuelven bytes:



Usando la información que conocemos sobre el modo SPI, el protocolo de intercambio de comandos y la tabla de instrucciones dada, podemos escribir un serie de funciones que implementen las principales funcionalidades de la memoria flash.

Iremos comentando algunas de las peculiaridades del código según lo vayamos viendo. En primer lugar definiremos los comandos de la tabla anterior:

#define CMD_WREN        0x06  //Write enable
#define CMD_WRDI        0x04  //Write disable
#define CMD_RDSR        0x05  // Read status
#define CMD_WRSR        0x01  // Write status
#define CMD_READ        0x03  // Read Data
#define CMD_FAST_READ   0x0B  // Fast Read data (>20 Mhz)
#define CMD_PP          0x02  // Page program
#define ERASE_SECTOR    0xD8  // Erase sector (1)
#define CMD_ERASE_ALL   0xC7  // Erase all    (1)
#define CMD_DPD         0xB9  // Enter Deep Power Down
#define CMD_REL         0xAB  // Release from DPW or read Elec signature



Ahora escribiremos algunas rutinas que leen o escriben el registro de estado, o algunos de sus bits, como WEL o los bits de protección:

// Functions that Read/Modify Status register or its bits

void write_enable(void)  {select_M25P80; spi_tx(CMD_WREN); deselect_M25P80;} // Sets WEL=1
void write_disable(void) {select_M25P80; spi_tx(CMD_WRDI); deselect_M25P80;} // Clears WEL

byte read_status(void
{
 byte res;
 select_M25P80; spi_tx(CMD_RDSR); res=spi_rx(); deselect_M25P80;
 return res;
}

void write_status(byte stat)
{
 write_enable();
 select_M25P80; spi_tx(CMD_WRSR); spi_tx(stat); deselect_M25P80;
}

byte set_protection(byte bp)
{
 byte stat;
 stat=read_status();   // Gets current status
 stat = stat & 0b11100011; // Erase BP bits
 stat = stat | (bp<<2);    // Set BP bits
 write_status(stat);
 return stat;
}

byte get_protection(void)
{
 byte stat;
 stat=read_status();
 stat = (stat>>2) & 0x07; // Extracts BP bits
 return(stat);
}
////////////////////////////////////////////////////////////////////////

Notad que siempre que vamos a hacer una escritura es preciso hacer WEL=1 usando la función write_enable(). No es necesario hacer WEL=0 ya que de eso se encarga el hardware tras ejecutar el correspondiente comando de escritura.

Un comentario sobre la función read_status(). En la tabla vemos que para la orden RDST se indica que el número de bytes de datos (en este caso recibidos) va de 1 a infinito. Lo que quiere decir esto es que esta orden puede usarse para leer ininterrumpidamente los contenidos del registro. En nuestro función, al levantar la línea CS tras recibir el primer byte interrumpimos dicha comunicación, pero si mantenemos CS=0, los contenidos del registro seguirán llegando (siempre que nosotros como master los "pidamos" mandando 8 pulsos de reloj). Esta funcionalidad puede usarse para por ejemplo comprobar si una operación de escritura/borrado ha terminado y la memoria vuelve a estar "libre".

La misma estrategia se usa en otras órdenes (aquella que indican #bytes de 1 a …).  Por ejemplo, en una orden de escritura o de lectura no hay que indicar previamente el número de bytes a grabar o leer. Simplemente se interrumpe la comunicación (~CS=1) cuando el número adecuado de bytes han sido enviados/recibidos.

Veamos unas funciones para la lectura/programación de una página de datos completa. Ambas funciones reciben un número de página (0 a 4095) y un puntero a un buffer de memoria y mueven 256 bytes de un lugar a otro.

  
// Reads a page # npage (256 bytes) and places its contents in buf[]
void read_page(uint16 npage,char* buf)
{
 uint16 k;

 select_M25P80;
 spi_tx(CMD_READ);
 spi_tx(npage>>8); spi_tx(npage&0xFF); spi_tx(0);  // Start address of page
 for(k=0;k<256;k++) buf[k]=spi_rx();
 deselect_M25P80;
}

// Program (1 -> 0) the contents of buf[] to page # npage
void write_page(uint16 npage,char* buf)
{
 uint16 k;

 write_enable();
 select_M25P80;
 spi_tx(CMD_PP);
 spi_tx(npage>>8); spi_tx(npage&0xFF); spi_tx(0);  // Start address of page
 for(k=0;k<256;k++) spi_tx(buf[k]);
 deselect_M25P80;
}


De nuevo, en la función write_page() hemos de usar write_enable() para poner WEL=1, antes del comando de escritura (CMD_PP). De lo contrario dicho comando no se ejecutaría.

En las funciones anteriores usamos por primera vez comandos (CMD_READ y CMD_PP) que han de ser seguidos de una dirección (inicio página a leer/escribir). La dirección serán 3 bytes, aunque dada la capacidad de la memoria sólo los 20 bits menos significativos tienen validez (2^20 = 1024x1024 = 1 Mbyte). Dado que una página tiene 256 bytes y hay 256 páginas en un sector, los tres bytes enviados corresponden al sector (1er byte), página (2do byte) y offset dentro de página (3er byte).  En los casos anteriores la dirección enviada era la de un principio de la página, por lo que el tercer byte de offset es siempre 0.

No estamos restringidos a lecturas/escrituras por bloques de 256 bytes. Ya hemos dicho que el número de bytes a mandar/recibir no es parte de la instrucción. En el caso anterior nos deteníamos tras transferir 256 bytes simplemente poniendo CS=1. Podemos por lo tanto hacer una lectura/escritura de un número arbitrario de bytes sin más que mantener CS=0 mientras queramos seguir manteniendo la transferencia. La dirección de comienzo tampoco tiene por que ser el inicio de una página (puede ser un offset distinto de 0). Con cada lectura/escritura la dirección se incrementa automáticamente.  

Sin embargo, hay una diferencia fundamental entre lecturas y escrituras en este modo. En modo lectura, si la dirección llega a una frontera de página la atraviesa, por lo que en principio podríamos hacer una lectura de toda la memoria con una sola instrucción CMD_READ. Sin embargo en escritura, si el puntero llega al final de la página no pasa a la página siguiente sino que retrocede al principio de la página en la que estamos. Por lo tanto no tiene sentido tratar de programar más de 256 bytes. Se puede hacer, pero a partir de 256 empezaríamos a machacar los primeros datos enviados. En ese caso, sólo los últimos 256 valores enviados serían programados.

Las siguientes rutinas implementan una lectura/programación de n bytes a partir de una dirección cualquiera:


// Reads n bytes starting @ address add and places them in buffer buf[]
void read_bytes(unsigned long add, unsigned long n, char* buf)
{
 unsigned long k;
 unsigned char sector,page,offset;

 // Gets sector, page and offset within the page from the original address
 offset = add&0xFF; add>>=8;
 page = add&0xFF; add>>=8;
 sector=add&0xFF;

 select_M25P80;
 spi_tx(CMD_READ);
 spi_tx(sector); spi_tx(page); spi_tx(offset);  // 20 bit address
 for(k=0;k<n;k++) buf[k]=spi_rx();
 deselect_M25P80;
}

// Programs n bytes (up to 256) in buf[] to address add.
void write_bytes(unsigned long add, unsigned int n, char* buf)
{
 unsigned int k;
 unsigned char sector,page,offset;

 // Gets sector, page and offset within the page from the original address
 offset = add&0xFF; add>>=8;
 page = add&0xFF; add>>=8;
 sector=add&0xFF;

 write_enable();
 select_M25P80;
 spi_tx(CMD_PP);
 spi_tx(sector); spi_tx(page); spi_tx(offset);  // 20 bit address
 for(k=0;k<n;k++) spi_tx(buf[k]);
 deselect_M25P80;
}

Consideremos ahora el borrado (erase) de datos. En las memorias flash un borrado supone poner los bits a 1 y típicamente el bloque de borrado es bastante mayor que el bloque de lectura. Así mientras podemos programar bytes individuales o por páginas de 256 bytes, sólo podemos borrar por sectores (256 páginas = 65536 bytes).

Existe una orden para borrar un sector en particular y otra para borrar todo el dispositivo. Ambas ordenes precisan poner el bit WEL a 1 con write_enable(). Sin embargo, por seguridad, en este caso no se ha incluido dicho comando dentro de las funciones. Es responsabilidad del usuario llamar a write_enable() antes de intentar una operación de borrado.

// Erases (sets to 1) all bits in sector # s
void sector_erase(unsigned char s)
{
 select_M25P80;
 spi_tx(ERASE_SECTOR);
 spi_tx(s); spi_tx(0); spi_tx(0);  // Start address of sector to be erased 
 deselect_M25P80;
}

// Erase all sectors (final state of the memmory 0xFF)
void erase_all(void)     {select_M25P80; spi_tx(CMD_ERASE_ALL); deselect_M25P80;}

Ya sólo quedan un par de instrucciones de la tabla que no hemos usado. El comando CMD_DPD pone al dispositivo en un modo de bajo consumo y el comando CMD_REL lo despierta

// Send flash memory to DeepPowerDown (CMD_DPD)
void power_down(void) {select_M25P80; spi_tx(CMD_DPD); deselect_M25P80;}
// Releases (CMD_REL) memory from DeepPowerDown state.
void power_up(void)  {select_M25P80; spi_tx(CMD_REL); deselect_M25P80; //delay_us(3);
}

Si se usa el comando (CMD_REL)  cuando el dispositivo está ya despierto, sirve también para obtener la firma electrónica del dispositivo (0x13), y es una forma de comprobar si la comunicación es correcta:

// Gets electronic signature by sendind REL when not in DeepPowerDown mode
byte get_signature(void)
{
 byte k;
 select_M25P80;
 spi_tx(CMD_REL); for(k=0;k<3;k++) spi_tx(0xAA); // Three dummy bytes
 k=spi_rx();
 deselect_M25P80;
 return k;
}


Con esto tenemos todas las funciones necesarias para poder usar la memoria. 


Velocidad de escritura/lectura

Vamos a escribir un sencillo programa que lea o escriba sucesivas páginas de la memoria (256 bytes). Lo primero que tenemos que hacer es reservar una zona de 2456 bytes que sirva como buffer de los datos.


byte page_data[256];

void main()
{
  uint16 page;

  // TEst speed //////////////////////////////////
  TRISB=0; PORTB=0;   TRISD=0; PORTD=0;
  FLASH_spi_init(0);

  page=0;
  while(1)
   {
    PORTDbits.RD1=1;
    read_page(page,page_data); //write_page(page,page_data);
    PORTDbits.RD1=0;
    Delay1KTCYx(20);
    PORTB=page; page++; page&=4095;
   }

 }

El programa simplemente inicializa el módulo SPI y entra en un bucle donde se leen (o escriben) sucesivamente las 4096 páginas del dispositivo. En PORTB nos da un feedback del incremento de páginas, mientras que en PORTD.RD1 podemos ver el tiempo dedicado a la tarea de lectura de una página. Tras cada página hacemos un delay de  20,000 instrucciones, que a 8Mz de reloj (2 MHz instrucción)  corresponden a 10 msec. En las siguientes imágenes podemos ver los tiempos dedicados a lectura (izquierda) y escritura (derecha) de una página:




Como se ve ambos tiempos son muy similares, unos 8.4 msec por página. Notad que dichos tiempos pueden reducirse sin más que usar un reloj más rápido. Ahora estamos usando un reloj de 2 MHz para el módulo SPI, muy alejado de los 25 MHz a los que puede llegar nuestro dispositivo. 

Como siempre, si no disponemos de un osciloscopio podemos medir el voltaje en RD1 y recordando que V_RD1/Vcc deber ser aproximadamente T / (T+10), podemos estimar el tiempo empleado (en msec). En mi caso he medido 4.86V en Vcc y 2.19 en RD1 por lo que:

                             T/(T+10) = 2.19/4.86 = 0.45, 

de lo que se deduce que T = 8.2 msec, muy próximo a lo observado con el osciloscopio.



Interfaz serie para probar las rutinas de la librería

Finalmente, para verificar las rutinas anteriores y explorar algunas de las peculiaridades de una memoria flash he escrito un programa que implementa una (muy sencilla) interfaz serie.  Tras arrancar, el programa manda un mensaje al puerto serie (19200 bauds) para indicar que está listo y presenta un prompt (>) al usuario. El usuario puede introducir una serie de instrucciones (algunas con un argumento) de la siguiente lista. Dichas funciones esencialmente llaman a las funciones que hemos visto.  Como antes, el programa reserva un área de datos de 256 bytes para el intercambio de datos:

        
         COMANDO              FUNCION USADA                 DESCRIPCION

>        init  arg            FLASH_spi_init(arg)     Inicializa modulo SPI
     wren                write_enable();             WEL=1
>        wrdi                 write_disable();            WEL=0
>        rsta                  read_status();              Devuelve registro de status.
>        wsta arg          write_status(arg);        Escribe registro de status
>        prot                 get_protection();          Obtiene 3 bits de protección del registro status
>        sprot arg         set_protection(arg);     Escribe los 3 bits de protección en registro status
>        rpag arg          read_page(arg,data)    Lee contenidos de una pagina  y muestra los 16                                                                                    
                                                                    primeros bytes  leidos por el puerto serie
>        wpag arg         write_page(arg,data)   Muestra los 16 primeros bytes de la zona de datos en
                                                                    pantalla y escribe los 256 bytes en la pagina dada.
>        erase               erase_all()                   Borra TODA la memoria (poner WEL=1  antes)
>        esec arg          sector_erase(arg)       Borra sector dado (hay que poner WEL=1  antes)
>        sleep               power_down();             Modo de bajo consumo
>        wake               power_up();                 Salimos del modo de bajo consumo.
>        sign                 get_signature()           Obtiene  y muestra la firma del dispositivo
>        data                        ---                         Muestra los 16 primeros bytes del area de datos
>        sdata   arg              ---                         Pone los bytes en el area de datos = arg   y muestra 
                                                                   los 16 primeros bytes. No escribe en el dispositivo.               
>        rdat     arg              ---                         Llena de números aleatorios la zona de datos y vuelca
                                                                   los 16 primeros datos.  No escribe en dispositivo.

Los tres últimos comandos manejan simplemente el área de datos y no interaccionan con la memoria, por lo que no usan ninguna de las funciones desarrolladas.

En nuestra primera interacción, para comprobar que las conexiones son correctas y hemos entendido el protocolo vamos a dar una serie de ordenes muy sencillas:

a) Inicializar el módulo SPI (siempre necesario).
b) Leer firma del dispositivo (13h)
c) Leer registro de status (instrucción RDSR): el manual indica que por defecto debe ser 0x00
d) Poner a 1 el bit WEL (instrucción WREN) y ver si ha cambiado el registro (debería ser 0x02).
e) Volver a poner a 0 el bit WEL (instrucción WRDI). Comprobar el cambio con RDSR.

El resultado de dichos comandos es el siguiente

M25P80 session
init 0 -> COD=0,ARG=0
sign   -> COD=14,ARG=0  Signatute =19
rsta   -> COD=3,ARG=0  Status =0
wren   -> COD=1,ARG=0
rsta   -> COD=3,ARG=0  Status =2
wrdis  -> COD=2,ARG=0
rsta   -> COD=3,ARG=0  Status =0

Pudiendose comprobar que los resultados son los esperados. El dispositivo devuelve 19 (0x13) como firma y al poner WEL=1 (2º LSbit del registro), el registro pasa a valer 2.

Veamos algunos ejemplos de lectura/escritura/borrado. Empezaremos borrando el primer sector (esec 0) y leyendo la primera página (rpag 0). El resultado deberían ser todos 0xFF. A continuación ponemos el área de memoria a 15 (sdata 15) y hacemos una orden de escritura a la página 0 (wpag 0). Una lectura de la misma página (rpag 0) debe darnos un montón de 0x0F's:

wren -> COD=1,ARG=0
esec 0 -> COD=8,ARG=0
rpag 0 -> COD=5,ARG=0 Data read from page:
FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
sdata 15 -> COD=10,ARG=15 Data in buffer:
0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
wpag 0 -> COD=6,ARG=0 To be written to page:
0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
rpag 0 -> COD=5,ARG=0 Data read from page:
0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
rpag 1 -> COD=5,ARG=1 Data read from page:
FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF

Vemos que una lectura de la página 1 (rpag 1) mantiene los datos originales (0xFF). Hasta ahora todo correcto. Repitamos ahora la operación de escritura (wpag 0) pero usando ahora un conjunto de bytes aleatorios:

rdat 37 -> COD=11,ARG=37 Data in buffer:
AA 33 90 D1 46 7F 4C BD 22 0B 48 E9 3E D7 84 55
wpag 0 -> COD=6,ARG=0 To be written to page:
AA 33 90 D1 46 7F 4C BD 22 0B 48 E9 3E D7 84 55
rpag 0 -> COD=5,ARG=0 Data read from page:
0A 03 00 01 06 0F 0C 0D 02 0B 08 09 0E 07 04 05

Vemos que algo ha ido mal. Los datos del buffer (AA, 33, 90, etc. ) no se han escrito correctamente. Cuando leemos los datos de la página 0 vemos que sólo los 4 bits menos significativos (A, 3, 0, etc. ) se han grabado. En los cuatro bits más significativos se mantiene el valor 0 que teníamos. ¿Qué esta pasando?

Lo que ocurre es que durante este tutorial todavía no hemos mencionado algunas de las propiedades peculiares de las memorias flash. La escritura de bytes individuales en una memoria flash sólo puede cambiar los bits del estado 1 al 0. De hecho, se prefiere hablar de programar (pasar de 1 a 0) datos en vez de escribir datos (que se entendería como cambiar arbitrariamente un bit). Por lo tanto en una memoria flash una operación de borrado pone los bits a 1, mientras que una de programación pone los 1 a 0 (pero no puede pasar de 0 a 1). Podría parecer que la combinación de ambas permitiría escribir cualquier dato, pero el problema es que la operación de borrado (erase) solo es aplicable a bloques de datos, no a bytes individuales.

Si intentamos ahora escribir una serie de datos aleatorios a la página 1 (que conservaba sus valores 0xFF iniciales) veremos que funciona correctamente.

rdat 13 -> COD=11,ARG=13 Data in buffer:
B2 DB 58 39 CE A7 94 A5 2A B3 10 51 C6 FF CC 3D
wpag 1 -> COD=6,ARG=1 To be written to page:
B2 DB 58 39 CE A7 94 A5 2A B3 10 51 C6 FF CC 3D
rpag 1 -> COD=5,ARG=1 Data read from page:
B2 DB 58 39 CE A7 94 A5 2A B3 10 51 C6 FF CC 3D

Hay situaciones donde este tipo de memoria no plantearía problemas. Pensemos en un datalogger que cada cierto tiempo guarda unos datos. Empezaríamos borrando todo el dispositivo y el programa podría empezar a escribir sus datos desde el inicio. Como los contenidos son inicialmente 0xFF, la operación de programar puede escribir cualquier dato. Al llegar al final de la memoria haríamos un comando de borrar el sector 0 y volveríamos a escribir sobre él. De esta forma la memoria siempre conservaría el último Mbyte de datos.

Sin embargo, en una situación normal (pensar en un sistema de archivos) necesitaremos modificar ciertos bytes en una página/sector dados. Esto no se puede hacer directamente, ya que si intentamos programar los nuevos datos, lo que quedaría en la memoria sería el AND entre los nuevos datos y los antiguos. En la práctica esto supone que en los dispositivos con memorias flash (memorias USB, tarjetas de memoria SD, CF, etc.) el acceso a los datos es siempre por bloques, no por bytes individuales.

Consideremos la memoria M25P80 que nos ocupa y en los pasos necesarios para modificar unos bytes dentro de una cierta página. Lo primero sería leer la página completa en un buffer intermedio, modificar los bytes necesarios y volver a escribir dicha página. Pero dicha página no puede escribirse en su mismo lugar. Deberíamos borrar (poner a 0xFF) el contenido de la página antes de volver a programarla. Pero en nuestro caso no podemos borrar sólo una página. Tendríamos  que borrar todo el sector, lo que es inadmisible, ya que un sector contiene otras 255 páginas que no deseamos perder. La única solución es escribir la página modificada en otro sector que previamente se ha
borrado y vamos usando para estos menesteres.

Como  se ve, la simple modificación de 1 byte provoca que toda la página se mueva físicamente. Por supuesto alguien tiene que llevar la pista a todos estos cambios. Es por esto por lo que las memorias flash requieren unos controladores especiales que hagan trasparentes todas estas complicaciones de cara al usuario final, que maneja sectores lógicos. Es el controlador el que sabe la relación sectores lógicos – sectores físicos.

La cosa se complica porque las memorias flash tienen un número de ciclos borrado/programación. Aunque en las memorias actuales dicho número es muy alto (del orden de 100,000 o 1,000,000) el software residente en el controlador debe implementar estrategias de wear-leveling, asegurándose de que todos los sectores acumulan el mismo número de ciclos de borrado. De esta forma se maximizaría la vida útil de la memoria.


Como se ve no es trivial el manejo de este tipo de memorias en aplicaciones generales. En ese caso es mucho mejor usar, por ejemplo, tarjetas de memoria (SD, CF). Con este tipo de dispositivos el PIC no tiene que lidiar con los sectores físicos de la memoria flash, limitándose a escribir/leer de sectores lógicos y dejando al controlador de la tarjeta SD que se ocupe de las peculiaridades de la memoria.  



Bits de protección

Finalmente un comentario sobre los bits de protección del registro de status. Dichos bits permiten proteger (al ponerse a 1) diversas sectores de la memoria, impidiendo su programación o borrado. Si por ejemplo partimos de la página 0 recién borrada (valores 0xFF) y ponemos los bits de protección a 7 (sprot 7) toda la memoria se encuentra protegida, por lo que la consiguiente orden de escritura no tiene efecto

rpag 0 -> COD=5,ARG=0 Data read from page:
FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
sprot 7 -> COD=15,ARG=7
prot -> COD=16,ARG=0  Protection bits =7
sdata 19 -> COD=10,ARG=19 Data in buffer:
13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13
wpag 0 -> COD=6,ARG=0 To be written to page:
13 13 13 13 13 13 13 13 13 13 13 13 13 13 13 13
rpag 0 -> COD=5,ARG=0 Data read from page:
FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF

Por el contrario un valor de 100 (4) en los bits de protección solo protege los 8 sectores finales (páginas de la 2048 a 4095) por lo que ahora si podemos escribir en la página 0:

sprot 4 -> COD=15,ARG=4
prot -> COD=16,ARG=0  Protection bits =4
rdata 57 -> COD=11,ARG=57 Data in buffer:
CE A7 94 A5 2A B3 10 51 C6 FF CC 3D A2 8B C8 69
wpag 0 -> COD=6,ARG=0 To be written to page:
CE A7 94 A5 2A B3 10 51 C6 FF CC 3D A2 8B C8 69
rpag 0 -> COD=5,ARG=0 Data read from page:
CE A7 94 A5 2A B3 10 51 C6 FF CC 3D A2 8B C8 69

Pero si intentamos escribir en la página 3000 (sector 11) la protección nos lo impide:

wpag 3000 -> COD=6,ARG=3000 To be written to page:
CE A7 94 A5 2A B3 10 51 C6 FF CC 3D A2 8B C8 69
rpag 3000 -> COD=5,ARG=3000 Data read from page:
FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF












4 comentarios:

  1. Muy interesante..me agrado
    un saludo

    ResponderEliminar
  2. Gracias. Información muy útil y muy bien explicado.

    ResponderEliminar
  3. Hola muy interesante tu pagina, mira tengo un proyecto donde comunico un sensor HMC5883L (q trabaja a 3.3v y soporta 3.6) con un pic 16F877a,el sensor lo alimento con 3.3v,la comunicación entre ambos es por I2C, y las enradas y salidas del sensor solo soportan los 3.6V, me arriesgue y descubri q pueden ocmunicarse con el PIC sin ninguna interfaz y funciona bien,pero temo q pueda llegar a quemar el sensor (esta caro), se te ocurre alguna interfaz para conectar la patilla SDA??,A mi se me ocurria un multiplexor CMOS, pero desconosco alguno q tenga en sus salidas 3.3v, y tengo la duda de si puede servir un multiplexor como una linea de comnicacion two-way. Agradesco si me pudieras ayudar ayudar :D

    ResponderEliminar
  4. Si ya escribí toda la memoria y quiero modificar algunos bytes no se puede pues debo borrar toda la memoria.
    ¿Existe alguna solución a esto?
    Siempre hablando de micros con RAM inferior al tamaño de pagina mínima que se puede borrar, usualmente 4K.

    ResponderEliminar