Protocolo SPI
Ahora que hemos visto en qué consiste el formato de programación ICSP, vamos a aplicar el protocolo SPI que está relacionado con este modelo.
Al igual que hicimos con el protocolo I2C, tenemos que conectar correctamente los siguientes pines a nuestros componentes.
- SCLK (Clock): Es el pulso que marca la sincronización. Con cada pulso de este reloj, se lee o se envía un bit.
- MOSI (Master Output Slave Input): Salida de datos del Master y entrada de datos al Esclavo.
- MISO (Master Input Slave Output): Salida de datos del Esclavo y entrada al Master.
- SS/Select: Para seleccionar un Esclavo, o para que el Master le diga al Esclavo que se active.
Protocolo SPI
Serial Peripheral Interface es un protocolo síncrono, que nos proporciona mayor velocidad de transmisión que con el protocolo I2C.
Al igual que la comunicación por I2C, también nos permite conectar varios dispositivos esclavos, pero en este caso no hace falta relacionar una dirección de acceso a cada componente, sino que utiliza un pin adicional que selecciona el componente activo en toda la linea denominado como SS (Slave Select) o CS( Chip Select ).
El resto de pines MOSI, MISO, SCLK deben compartirse entre todos y el SS (Slave Select) es independiente para cada componente conectado a la linea.
Existen dos modos de conexión. El más utilizado para conectar es el primer modelo, de manera que todos los componentes se ejecuten de forma independiente con su linea SS activa.
El segundo modelo, es útil para mejorar el acceso en cadena con un solo pin SS de control. Para más información se puede acceder al siguiente enlace.
Una desventaja de este protocolo es que solo funciona a distancias cortas.
Componentes SPI – Lector tarjeta SD
Para practicar con la programación por SPI, vamos a usar cualquiera de los componentes que podemos reconocer si encontramos los pines MOSI, MISO y SCLK para conectar.
Entre los componentes más utilizados por SPI con Arduino podemos encontrar
- Lector de tarjetas SD
- Pantallas TFT
- Lector tarjetas NFC
Para programar nuestra tarjeta SD, seguramente encontremos distintos modelos de lector de tarjetas SD, pero si seguimos las instrucciones de nuestro anterior tutorial.
- SD-MOSI
- SD-MISO
- SD-SCK
- 5V
- GND
- CS (Chip Select) –> 3
Todos los componentes que se conectan por ICSP, se han de conectar a través de sus correpondientes pines.
Otra posibilidad es la de conectar los pines ICSP que se corresponden con los pines digitales 10 a 13. Aunque en el caso de necesitar pines para conectar a otros componentes este modelo de conexión sería contraproducente.
- SD-MOSI –> 11
- SD-MISO –> 12
- SD-SCK –> 13
- 5V
- GND
- CS (Chip Select) –> 3
Las tarjetas SD nos proporcionan mucha libertad para por ejemplo no sobrecargar la memoria de nuestra placa pudiendo obtener información de forma externa a la placa, procesarla y ejecutarla. Vamos a realizar los siguientes ejemplos de uso.
Unas notas previas a tener en cuenta para programar la tarjeta SD son variadas y las podemos encontrar en el siguiente enlace.
- Detector de tarjetas
- Rutas y ficheros
- Leer directorios
- Leer nombre de ficheros
- Crear ficheros
- Eliminar Directorios
- Leer datos
- Escribir datos
Detector de tarjetas
Para realizar ejercicios con la tarjeta SD, no nos hará falta instalar ninguna librería adicional, ya que Arduino tiene instaladas por defecto las librerías necesarias para ello.
Lo primero que tenemos que tener en cuenta, es cargar la librería SD y especificar en qué pin hemos conectado la tarjeta SD. La instruccion para detectar que tenemos conectada la tarjeta es el método SD.begin().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #include <SD.h> const byte SDpin = 3; unsigned long auxtimer; void setup() { Serial.begin( 9600 ); pinMode(SDpin, OUTPUT); SDClass SD; if ( SD.begin( SDpin ) ){ Serial.println( "SD SUCCESS" ); } else { Serial.println( "SD Initialization failed!" ); } } void loop() { if ( millis() - auxtimer > 500){ auxtimer = millis(); if ( SD.begin( SDpin ) ){ Serial.println( "SD SUCCESS" ); } else { Serial.println( "SD Initialization failed!" ); } }; } |
Lo primero que vamos a poder comprobar en este ejemplo es que la placa detecta casi instantaneamente que está la tarjeta conectada, pero cuando quitamos la tarjeta, la comprobación se retrasa más que el medio segundo definido, por lo que la comprobación no es instantanea. En este caso hay que tener cuidado del evento entre conexión y desconexión de la propia tarjeta.
IMPORTANTE: Si no hacemos un control de evento temporal como el que hemos realizado y hacemos comprobación de conexión de la SD de forma continua en bucle no tendremos un formato de detección en el lector consistente.
Rutas y ficheros
Una vez que hemos accedido a la tarjeta, el siguiente paso es leer los archivos que existen en ella. Uno de los aspectos a tener más en cuenta dentro de nuestro acceso es saber diferenciar entre un fichero o un directorio, ya que existirán métodos diferentes para procesar uno u otro.
Lo primero que vamos a realizar es leer por el puerto serie la información de nuestro directorio raiz. Para ello, vamos a utilizar el método SD.open( filename ) para abrir los directorios y acceder a la información contenida en ellos.
Cada vez que ejecutamos el método open, abriremos un directorio o un fichero y lo podemos guardar dentro de un objeto de la clase File. Si lo que devuelve es un booleano con false, eso quiere decir que la ruta especificada como nombre de fichero no existe, por lo que no se puede acceder a ella y no devuelve nada.
Una vez que tengamos la información, deberemos reconocer si el objeto es un fichero o un directorio con el método isDirectory que devuelve un booleano true si reconoce que es un directorio y podemos obtener tanto su nombre con el método name() y el tamaño que ocupa con el método size().
Para hacer referencia al directorio raiz, tendremos que escribir la ruta definida como «/».
SD.open(«/»);
Por último para hacer un repaso de los ficheros y directorios que nos podemos encontrar en una carpeta, haremos uso del método openNextFile() . Este método establecera el cursos en la dirección de la SD que se encuentra en la memoria para hacer la lectura.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | #include <SD.h> const byte SDpin = 3; void setup() { Serial.begin( 9600 ); pinMode(SDpin, OUTPUT); SDClass SD; if ( SD.begin( SDpin ) ){ Serial.println( "SD SUCCESS" ); File root = SD.open( "/" ); if (root.isDirectory()) { while ( true ) { File entry = root.openNextFile(); Serial.println( entry.name() ); if (! entry) { // no more files break ; } Serial.print( "\t\t" ); Serial.println(entry.size(), DEC); delay(100); } } } else { Serial.println( "SD Initialization failed!" ); } Serial.println( "SD Done" ); } void loop() { } |
Podremos comprobar que el tamaño de los nombre se encuentra limitado y no los imprime completamente.
Este hecho se debe a la siguiente cuestión documentada en el siguiente enlace entre otras cuestiones.
Para solucionar este caso hay que buscar el modelo LFN (Long File Name ) de una nueva librería para el manejo de tarjetas SD denominada SDFat y que podemos encontrar en el gestor de librerías.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | #include "SdFat.h" const byte SDpin = 3; SdFat sd; SdFile dirFile; void setup() { Serial.begin( 9600 ); pinMode(SDpin, OUTPUT); if ( !sd.begin( SDpin, SD_SCK_MHZ(50)) ) { sd.initErrorHalt(); } else { Serial.println( "SD SUCCESS" ); } if (!dirFile.open( "/" , O_READ)) { sd.errorHalt( "open root failed" ); } else { SdFile entry; while ( entry.openNext(&dirFile, O_READ) ) { entry.printName(&Serial); Serial.println(); entry.close(); } } Serial.println(); Serial.println( "SD Done" ); } void loop() { } |
De esta manera podremos leer cual es el fichero o directorio que queremos acceder o leer sin restricciones de tamaño en el nombre.
Leer datos
Una vez que conocemos la manera de acceder a un fichero, ahora desarrollaremos el programa para leer su contenido.
En este ejemplo, vamos a guardar un fichero de texto llamado «test.txt» dentro de el directorio raiz de nuestra tarjeta. El programa va a funcionar atendiendo al monitor serie en el que introduciremos el nombre del fichero y éste se abrirá y leeremos su contenido desde el mismo monitor.
IMPORTANTE: Hay que seleccionar el modo «Sin Ajuste de linea» al abrir el monitor serie, para que no introduzca caracteres de salto de carro y fin de linea al introducir el nombre, si no leeriamos «test.txt\CR\LF»
Para revisar el concepto de fin de carro y salto de linea , vamos a leer esos caracteres dentro de nuestro programa y los vamos a contar para hacer evidencia de la existencia de estos caracteres invisibles.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | #include <SD.h> const byte SDpin = 3; void setup() { Serial.begin( 9600 ); pinMode(SDpin, OUTPUT); if ( SD.begin( SDpin ) ){ Serial.println( "SD SUCCESS" ); String filecharname = "test.txt" ; if (SD.exists( filecharname )){ Serial.println( " Setup test.txt Exists" ); } } else { Serial.println( "SD Initialization failed!" ); } } void loop() { if (Serial.available() ){ String filestr = Serial.readString(); int maxLength = filestr.length(); if ( SD.exists( filestr )){ Serial.print( "File " );Serial.println( filestr ); File entry = SD.open( filestr ); if (entry){ Serial.println( " Exists" ); Serial.println(); int countCR = 0; int countLF = 0; while (entry.available()) { //Serial.print("\t"); //peek() lee el caracter disponible pero no desplaza el puntero //En cambio read() si que desplaza el puntero Serial.print( ( char )entry.peek() ); //Los valores de CR y LF no aparecen escritos en el monitor serie, pero existen en el fichero. //Hay diferencia al leerlo como un caracter, a leerlo como un dato no declarado. Por defecto nos aparecera su valor numerico. int data = entry.read(); //Cada linea en el fichero termina con un retorno de carro CR y un fin de linea LF. //El numero 13 corresponde con el retorno de carro, tambien se representa con "\r". if (data== 13){ countCR++; } //El numero 10 corresponde con el fin de linea, tambien se representa con "\n" //A partir de este momento, el proximo valor leido es el dato de comienzo de la siguiente linea if (data== 10){ countLF++; } } entry.close(); Serial.println();Serial.println(); Serial.print( "LF: " );Serial.println(countLF); Serial.print( "CR: " );Serial.println(countCR); } else { Serial.println( "File Not Exists" ); } } else { Serial.println( "String File Not Exists" ); } } } |
Escribir datos
Ahora vamos a desarrollar un programa para escribir datos dentro de un fichero. En este caso, tendremos dos posibilidades. Una es introducir información dentro de un fichero ya existente; y el segundo formato es crear un fichero nuevo y empezar a introducir información desde el principio.
Para ello es necesario tener en cuenta un concepto de permisos asociados a cada fichero para establecer permisos de escritura o solo lectura.
En principio no tendremos nunca problemas para leer un fichero, pero sí que podremos especificar si queremos proteger un fichero contra escritura para que no se sobreescriban los datos. Para hacer esto haremos uso de la función SD.open().
En este ejercicio reaprovecharemos el ejemplo anterior para leer un fichero si existe en la tarjeta y si no existe, creará uno nuevo y escribiremos datos dentro del mismo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | #include <SD.h> const byte SDpin = 3; void setup() { Serial.begin( 9600 ); pinMode(SDpin, OUTPUT); if ( SD.begin( SDpin ) ){ Serial.println( "SD SUCCESS" ); } else { Serial.println( "SD Initialization failed!" ); } } void loop() { if (Serial.available() ){ String filestr = Serial.readString(); if ( SD.exists( filestr )){ Serial.print( "File " );Serial.println( filestr ); File entry = SD.open( filestr ); if (entry){ Serial.println( " Exists" ); Serial.println(); while (entry.available()) { Serial.print( ( char )entry.read() ); } Serial.print( "Write what you want to store in SD File:" ); while ( !Serial.available() ){ } String serialData = Serial.readString(); entry.println(serialData); entry.close(); } else { Serial.println( "File Not Exists" ); } } else { File entry = SD.open( filestr, FILE_WRITE); Serial.println(); Serial.print( "Write what you want to store in SD File:" ); Serial.println(filestr); while ( !Serial.available() ){ } String serialData = Serial.readString(); entry.println(serialData); Serial.println( "Data stores: Now, you can read your new File" ); entry.close(); } } } |
IMPORTANTE: Los ficheros en caso de no existir, se crean directamente con la misma función SD.open. En el caso de querer eliminar un archivo tendremos que usar la función SD.remove();
Crear y eliminar ficheros
Como ya hemos visto en el programa anterior para crear ficheros usaremos el método SD.open. y para eliminarlos SD.remove();
Para indicar si queremos eliminar un fichero vamos a establecer un formato de uso en el que usaremos un menú de opciones numeradas para desarrollar cada acción.
Creación y eliminación de directorios
Hasta aquí hemos accedido a los ficheros que se encuentran en la tarjeta SD dentro del directorio raiz, pero si quisieramos acceder a otros ficheros tendriamos que especificarlos añadiendo la ruta con el símbolo «\».
Para poder crear directorios usaremos el método SD.mkdir() y para eliminarlo usaremos el método SD.rmdir().
En todo este proceso es conveniente que se pueda revisar la existencia de un fichero o directorio concreto.
Problemas más comunes en tarjetas SD
Si la tarjeta SD no se puede leer con Arduino, es posible que nuestra tajeta SD no esté formateada correctamente para su correcta lectura.
Una vez completado este tutorial, puedes acceder al siguiente nivel.
Los ejercicios de código, proyectos y recursos que desarrollaremos durante el curso se pueden consultar a través de nuestro Github.
Documentacion ZaragozaMakerSpace Github