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.

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).
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
Muy interesante..me agrado
ResponderEliminarun saludo
Gracias. Información muy útil y muy bien explicado.
ResponderEliminarHola 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
ResponderEliminarSi ya escribí toda la memoria y quiero modificar algunos bytes no se puede pues debo borrar toda la memoria.
ResponderEliminar¿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.