Buscando la forma más eficiente de trabajar con SD

Hola a todos. En este hilo estábamos discutiendo sobre la velocidad de lectura/escritura de una tarjeta SD con Arduino.

Abro uno nuevo porque en el otro siento que nos estamos desviando del tema.

Además me gustaría abrir una discusión como esta, ya que tengo un programa que requiere la búsqueda de una palabra digitada por comunicación serial. Esta búsqueda se hace en un archivo que contiene una lista muy larga de palabras; entonces lo que necesito es encontrar la manera más rápida de ejecutar eso.

Por último, había leído algo de que la librería no actualiza bytes "físicos" por separado, sino en "bloques" de 512 bytes (por "sectores").
Entonces, es más eficiente esperar que un búfer de ese tamaño se llene antes de escribir lo datos? Y si fuera así, el ATmega328P no sería la mejor opción; porque 512 bytes representan 1/4 de la memoria RAM :o Y dependiendo del programa, eso es mucho.
Ese "búfer" hay que crearlo manualmente, o se crea implícitamente en cada objeto File?

Agradezco de antemano sus respuestas, opiniones, comentarios, etc... :slight_smile:

Pillo sitio que me interesa este tema.

:slight_smile:

Efectivamente, cada lectura o escritura física en la SD supone hacerlo con un bloque de 512 bytes. Por lo tanto, la librería SD (o más bien SDFat, que es sobre la que se apoya SD) dispone de un buffer de ese tamaño para efectuar la lectura o escritura de un bloque.

Como dice Lucario, eso supone una parte importante de la memoria disponible en un 328P, por lo que encima la librería sólo utiliza uno (tal vez uno de los puntos por los que en el due funciona más rápida sea porque puede permitirse utilizar varios buffers). Por ello la librería SDFat inicialmente sólo soportaba un archivo abierto (creo que actualmente se pueden tener varios, pero compartiendo el mismo buffer de escritura).

Cuando escribimos en SD realmente estamos enviando información al buffer y actualizando el objeto File (para indicar por ejemplo el nuevo tamaño de archivo). Este proceso, al hacerse exclusivamente en la RAM, apenas consume tiempo, salvo que se realice un flush o actualización del archivo en la SD. Este se puede realizar por múltiples razones:

  • La instrucción de escritura lleva implícito un flush (creo que print lo lleva, pero write no).
  • La escritura excede el ámbito del bloque actual.
  • Hacemos un flush manual o un close.

Cada vez que se produce un flush, se realizan las siguientes tareas:

  • Se envía el bloque completo de 512 bytes a la SD para su escritura.
  • Como sólo tenemos ese buffer y necesitamos actualizar la información del fichero, leemos al mismo 512 bytes correspondientes al bloque donde se halla la cabecera del mismo.
  • Actualizamos la información de la cabecera y escribimos de nuevo el bloque completo.
  • Si vamos a escribir más, primeramente hay que volver a cargar el bloque actual al buffer.

Quiere decir que la escritura física de un solo byte conlleva dos lecturas y dos escrituras de 512 bytes, es decir, una transferencia de 2 Kb a través del puerto SPI. Por ello decía en el otro hilo que dependiendo de la frecuencia con la que debamos actualizar los datos, la SD puede resultar lenta. Lo óptimo entonces, para escritura en un archivo secuencial con la librería SD sería evitar los flush innecesarios. Ciertamente, la velocidad podría ser más alta si tuviéramos dos bloques de buffer (uno para los datos y otro para la cabecera), ya que evitaríamos muchas relecturas. No sé si esto está previsto de alguna forma en las nuevas versiones de la librería (aunque haya que pagar el costo de otros 512 bytes) o habría que hacer una modificación profunda. Creo que lo que sí existe por ahí para escribir a mayor velocidad (por ejemplo para volcar sonido) es la escritura RAW. Creo que consiste básicamente en ir escribiendo directamente bloques de datos contiguos, en lugar de ir actualizando cabeceras tras cada escritura.

¡Guau! :o

¡Muchas gracias por esa explicación tan elaborada! ¡He aprendido bastante! :slight_smile:

Entonces tu dices que es más eficiente escribir con write que con print? Bueno, si es así tampoco es algo del otro mundo. Sería como reinventar print pero sin flush:

unsigned int miPrint(char *str, File archivo) {
  unsigned int longitud = strlen(str);
  for (unsigned int i = 0; i < longitud; i++) {
    archivo.write(str[i]);
  }
  return longitud;
}

Propongo un código así, porque ni en la documentación de la librería SD encuentro alguna función que escriba directamente desde un "array" (y que no sea print o println) :confused:

Por otro lado, que hay sobre la lectura?
Ahí la "transacción SPI" se hace por un único byte o aún se hace por bloques?
Y me imagino que al ser lectura, la "transacción SPI" sólo se hace una vez... verdad?

También se podría sustituir directamente por la otra versión de write:

unsigned int miPrint(char *str, File archivo) {
  unsigned int longitud = strlen(str);
  archivo.write(str, longitud);
  return longitud;
}

En cuanto a la lectura, también se realiza por bloques, por lo que la lectura de un solo byte supone cargar el bloque entero; pero si vas a leer secuencialmente, sólo se cargará un nuevo bloque cuando el nuevo dato a leer no se encuentre en el bloque cargado actualmente.
Hay dos cosas que hacen que el trabajo con sd sea bastante más lento en arduino que en un ordenador, por ejemplo:

  • Que el modo SPI en las SD no es el más rápido, precisamente, de los que incorporan.
  • Que el trabajo óptimo con dispositivos de almacenamiento requiere de ingentes cantidades de memoria si queremos evitar transferir repetidamente los mismos datos.

Un inciso a todo esto.
He leído que también influye la calidad del hardware del lector. Es así?
Porque resulta que la semana pasada realizando pruebas, los lectores que tengo (dos del mismo tipo) he tenido problemas a la hora de lectura. He probado un lector que tenía en un TFT y de lujo.

He pedido 3 lectores diferentes y estoy a la espera de llegada para realizar mas pruebas.


Este ultimo, tengo la versión 1.0 y es el que me da problemas. A ver si esta nueva versión lo soluciona.

Efectivamente, uno de los hándicap que tienen que librar los arduinos de 5v es que la Sd trabaja a 3V3, y en muchas ocasiones lo único que se utiliza es un divisor resistivo para alimentarla y para las señales de salida (SS, SCK y MOSI). La entrada hacia el arduino (MISO) llega a 3.3 desde la tarjeta al arduino, y "aproximadamente" se debería interpretar como HIGH dicho voltaje, pero el hecho es que dependiendo de módulos, unos tratan mejor que otros el cambio de niveles de la señal, resultando en mejor transferencia y más velocidad.
Por ello los micros que trabajan nativamente a 3.3 parten con una buena ventaja respecto de los que deben adaptar niveles.
EDITO:
De hecho, yo también estoy teniendo problemas con módulos de este tipo. No sé si tiene que ver el cableado o que directamente los módulos son malos, o que debería utilizar algún tipo de filtrado, pero si quiero evitar dejar la SD hecha unos zorros debo trabajar a la mitad de la velocidad normal de comunicación (osea en SPI_QUARTER_SPEED).

noter:
También se podría sustituir directamente por la otra versión de write:

unsigned int miPrint(char *str, File archivo) {

unsigned int longitud = strlen(str);
 archivo.write(str, longitud);
 return longitud;
}

Sí sere...
Sí estaba documentada una función así, y yo diciendo que no había :sweat_smile: :roll_eyes:

En cuanto a la lectura, también se realiza por bloques, por lo que la lectura de un solo byte supone cargar el bloque entero; pero si vas a leer secuencialmente, sólo se cargará un nuevo bloque cuando el nuevo dato a leer no se encuentre en el bloque cargado actualmente.
Hay dos cosas que hacen que el trabajo con sd sea bastante más lento en arduino que en un ordenador, por ejemplo:

  • Que el modo SPI en las SD no es el más rápido, precisamente, de los que incorporan.
  • Que el trabajo óptimo con dispositivos de almacenamiento requiere de ingentes cantidades de memoria si queremos evitar transferir repetidamente los mismos datos.

Más dudas:

  • Hay un búfer para lectura, y otro para escritura; o cómo es?
  • Si la memoria es una limitante, entonces que pasa si definimos SPI_FULL_SPEED? Mejorará algo?

El post al que hacía referencia antes es muy antiguo, por lo que puede que la cosa haya cambiado. En dicho post, el autor de SDFat (librería base de SD) comenta lo siguiente:

SD cards can only be read or written in 512 byte blocks and SdFat has a single 512 byte buffer.

Es decir que sólo tenemos un buffer "pluriempleado" para el intercambio en una dirección u otra.

A flush causes the data block to be written, the directory block to be read, updated and written. The next write requires the data block to be read so it can be updated. This is 2048 bytes of I/O.

Una escritura en tarjeta, aunque sólo sea de un byte, requiere haber leido previamente el bloque de datos entero, modificar en el buffer el byte, escribir el bloque de datos, leer el bloque de directorio, actualizar la información de la cabecera de fichero y volver a escribir el bloque de cabecera. Total 2kb de transferencia. Si en lugar de sólo un buffer utilizáramos uno para bloque de datos y otro para bloque de cabecera, reduciríamos las transferencias a la mitad.

En el caso de que pudiéramos trabajar a SPI_FULL_SPEED (en la librería creo que está definida por defecto la velocidad SPI_HALF_SPEED), la comunicación Arduino/SD en ambas direcciones se vería duplicada, con lo que prácticamente duplicaríamos la velocidad, al pasar a trabajar de 4Mhz a 8Mhz.

Acabo de ejecutar este código:

#include <SD.h>
const char texto[] = "Hola mundo de Arduino, aqui probando la velocidad de mi SD....\r\n";
const byte longitud = 64; // Longitud del texto, calculada manualmente
const unsigned int repeticiones = 65534U;
File archivo;

void setup() {
  Serial.begin(9600);
  if (!SD.begin(4/*, SPI_FULL_SPEED*/)) {
    Serial.println(F("Fallo en la SD..."));
    while (1);
  }
  if (SD.exists("Prueba.txt")) {
    SD.remove("Prueba.txt");
  }
  archivo = SD.open("Prueba.txt", FILE_WRITE);
  if (!archivo) {
    Serial.println(F("Fallo en el archivo..."));
    while (1);
  }
  Serial.println(F("Iniciando prueba de velocidad con \"miPrint\"..."));
  unsigned long tAnt = millis();
  for (unsigned int r = 0; r < repeticiones; r++) {
    miPrint(texto);
  }
  archivo.close(); // En teoría, el archivo resultante debe tener un tamaño de 4194176 bytes.
  unsigned long tPos = millis();
  Serial.print(F("Archivo terminado en "));
  Serial.print(tPos - tAnt);
  Serial.println(F(" milisegundos."));
  Serial.println();

  SD.remove("Prueba.txt");
  archivo = SD.open("Prueba.txt", FILE_WRITE);
  if (!archivo) {
    Serial.println(F("Fallo en el archivo..."));
    while (1);
  }
  Serial.println(F("Iniciando prueba de velocidad con \"print\"..."));
  tAnt = millis();
  for (unsigned int r = 0; r < repeticiones; r++) {
    archivo.print(texto);
  }
  archivo.close(); // En teoría, el archivo resultante debe tener un tamaño de 4194176 bytes.
  tPos = millis();
  Serial.print(F("Archivo terminado en "));
  Serial.print(tPos - tAnt);
  Serial.println(F(" milisegundos."));
  Serial.println();
  Serial.println(F("Eso es todo amigos..."));
}

void loop() {
  // Nada por aquí

}

void miPrint(const char *str) {
  // unsigned int longitud = strlen(str);
  archivo.write(str, longitud);
  // return longitud;
}

En resumen: escribe 65534 veces, un texto de una longitud de 64 caracteres. Primero con miPrint y luego con la función print que viene en el objeto File. El archivo resultante tiene un tamaño de 4194176 bytes (aprox. 4 MB). Y este fue el resultado:

Iniciando prueba de velocidad con "miPrint"...
Archivo terminado en 71415 milisegundos.

Iniciando prueba de velocidad con "print"...
Archivo terminado en 111039 milisegundos.

Eso es todo amigos...

La velocidad promedio de la primera, ha sido de 57 KB/s.
La velocidad promedio de la segunda, ha sido de 36 KB/s.

Juzguen ustedes mismos...

En el próximo post, haré la misma prueba pero con la librería SdFat y SPI_FULL_SPEED

PD: el programa fue ejecutado en un Arduino Nano (ATmega328P @16 MHz)

Lucario448:
En el próximo post, haré la misma prueba pero con la librería SdFat y SPI_FULL_SPEED

Bueno pues... lo prometido es deuda. El código usado es el siguiente:

#include <SdFat.h>
const char texto[] = "Hola mundo de Arduino, aqui probando la velocidad de mi SD....\r\n";
const byte longitud = 64; // Longitud del texto, calculada manualmente
const unsigned int repeticiones = 65534U;
File archivo;
SdFat sd;

void setup() {
  Serial.begin(9600);
  if (!sd.begin(4, SPI_FULL_SPEED)) {
    Serial.println(F("Fallo en la SD..."));
    while (1);
  }
  if (sd.exists("Prueba.txt")) {
    sd.remove("Prueba.txt");
  }
  archivo = sd.open("Prueba.txt", FILE_WRITE);
  if (!archivo) {
    Serial.println(F("Fallo en el archivo..."));
    while (1);
  }
  Serial.println(F("Iniciando prueba de velocidad con \"miPrint\"..."));
  unsigned long tAnt = millis();
  for (unsigned int r = 0; r < repeticiones; r++) {
    miPrint(texto);
  }
  archivo.close(); // En teoría, el archivo resultante debe tener un tamaño de 4194176 bytes.
  unsigned long tPos = millis();
  Serial.print(F("Archivo terminado en "));
  Serial.print(tPos - tAnt);
  Serial.println(F(" milisegundos."));
  Serial.println();

  sd.remove("Prueba.txt");
  archivo = sd.open("Prueba.txt", FILE_WRITE);
  if (!archivo) {
    Serial.println(F("Fallo en el archivo..."));
    while (1);
  }
  Serial.println(F("Iniciando prueba de velocidad con \"print\"..."));
  tAnt = millis();
  for (unsigned int r = 0; r < repeticiones; r++) {
    archivo.print(texto);
  }
  archivo.close(); // En teoría, el archivo resultante debe tener un tamaño de 4194176 bytes.
  tPos = millis();
  Serial.print(F("Archivo terminado en "));
  Serial.print(tPos - tAnt);
  Serial.println(F(" milisegundos."));
  Serial.println();
  Serial.println(F("Eso es todo amigos..."));
}

void loop() {
  // Nada por aquí

}

void miPrint(const char *str) {
  // unsigned int longitud = strlen(str);
  archivo.write(str, longitud);
  // return longitud;
}

Y el resultado es el siguiente:

Iniciando prueba de velocidad con "miPrint"...
Archivo terminado en 92301 milisegundos.

Iniciando prueba de velocidad con "print"...
Archivo terminado en 92604 milisegundos.

Eso es todo amigos...

La velocidad promedio ha sido de 44 KB/s en ambos casos.

De ahí podremos deducir que esta librería ha optimizado la función print.

Pero... si en teoría definí SPI_FULL_SPEED, por qué el desempeño de miPrint ha empeorado más bien? Qué ha pasado?

PD: esta es la librería que he usado...

Hola muchachos, despues de leer éste foro y el de inglés de arriba a abajo, no logro encontrarle la forma de grabar en una SD (con DUE) una velocidad de datos de 50Ks/s, veo que algunos ejemplos ésto es posible, pero no logro dar con la tecla... estoy "jugando" con 6 entradas analógicas para guardar sus datos en la memoria a la máxima velocidad posible, ya que quiero medir corrientes/tesiones armónicas en un sistema trifásico...