Skip to content

Peripherals: SD Cards, SPIFFS, littleFS, EEPROM, and IO channels.

Stefan Lenz edited this page Jan 19, 2023 · 7 revisions

Mac, Linux, Windows, and MSDOS

On a Mac, Linux, Windows and MSDOS the commands LOAD and SAVE work on the filesystem. They store a file and load it. All files are always stored as ASCII files. Loading is essentially like typing in lines by hand. If you have an existing program in memory the loaded one is merged line by line. You need to clear the memory using "NEW" if a new program is to be loaded. If LOAD is called in a program, it clears the old program, loads the new one and then continues execution at the first line of the new program.

All the file I/O commands like OPEN, CLOSE, and DELETE work on the filesystem.

There is no support for directories or directory changing. BASIC always stays in the directory it has been started in.

Internal EEPROM file storage

If you compile with ARDUINOEEPROM the EEPROM can be used as a file storage. SAVE "!" deposits your program in the EEPROM if there is enough space. LOAD "!" reloads it.

With the command SET 1,1 you can activate EEPROM autorun. SET 1,0 switches off autorun. SET 1, 255 removes the program flag. The EEPROM cannot be loaded any more. The information needed for this logic is stored in the header bytes of the EEPROM. Currently the header size is 3. Don't change these bytes with PEEK and POKE unless you understand what you are doing.

If a program in the EEPROM is marked for autorun with SET 1,1 the Arduino starts the program without showing a command prompt after reset. Autorun of an EEPROM program is done directly from the EEPROM. There is no LOAD to memory needed. An EEPROM autorun program has the entire RAM for variables. An arduino in EEPROM autorun mode still listens to the serial port. A running program can be interrupted by typing in the break character # as first character after connecting the Arduino to the computer.

If the interpreter is compiled with the flag ARDUINOPGMEEPROM set, EEPROM is directly used as program storage. All the RAM is free for variables. Editing programs can be slow with this setting.

On Arduinos without mass storage, load and save can be used without a parameter. LOAD and SAVE are always available as commands even is the interpreter is compiled with a minimal configuration.

EEPROM dummy

POSIX like OSes like Mac, Linux, and Windows/MINGW have an EEPROM dummy. It simulates a 1024 bytes EEPROM like on an Arduino UNO. This is controlled by the parameter EEPROMSIZE in basic.h. EPS8266 and ESP32 also have an EEPROM dummy. Maximum size would be 4096 bytes but it is set to 1024 bytes like the POSIX EEPROM dummy.

ESP8266 and ESP32 controllers also offer an EEPROM support although they have no build in EEPROM. The standard ESP EEPROM emulation is used for it.

The EEPROM dummy is written to flash or disk after and END, STOP, SLEEP or CALL 0 statement.

External EEPROMS as a simple file storage

For systems with little flash and memory and use case where no file systems are needed, an external EEPROM can be used as replacement of the internal EEPROM. #define ARDUINOI2CEEPROM in the hardware section and set the I2CEEPROMSIZE and EEPROMI2CADDR to the right parameters. If the size is not set, an autodetect tries to find the size of the EEPROM. 4k and 32k are supported..

After start of the BASIC interpreter the EEPROM size should be visible in the startup prompt. A 24C256 standard EEPROM shows as 32767 bytes free. This EEPROM can be used as a normal EEPROM like the internal ones described above. SAVE saves programs to it. SET 1,1 sets the autorun flags. After the next reboot the Arduino runs the program in the EEPROM.

Autorunable EEPROM modules can be used to extend the program storage of small Arduinos. You can prepare an EEPROM module on an Arduino with enough memory like a MEGA256 with 8 kB of memory. You can save it on this system, set it to autorun with SET 1,1 and then remove the module. If you plug in into a smaller Arduino, like a UNO, the program will run there. The entire RAM is available for variables.

EEPROM modules are interchangeable between different platforms as long as number_t and address_t have the same size. The EEPROM stores all numbers in tokenised form and the representation of them depends on these types.

The compiler macro ARDUINOI2CEEPROM_BUFFERED controls if the I2C EEPROM is buffered or not. A buffered EEPROM needs the EFS library explained below while the support for unbuffered EEPROMS is built-in in BASIC. Unbuffered EEPROMS write every byte into the EEPROM directly. This is helpful if the Arduino runs on battery and there is risk of power failure.

External EEPROMS as a file system.

Storing data on the internal EEPROM works for small systems but the size of the EEPROM is limited and the storage cannot be replaced like for SD cards. For smaller microcontrollers with limited EEPROM space like the Nano Every a different solution is needed. SD card file systems use a lot of memory as internal buffers are needed to store the 512 byte block sizes of the SD card.

BASIC supports my EEPROM filesystem EEPROM Filesystem. Compile the code with #define ARDUINOEFS in the hardware section. Make sure that you set the I2C address and the EEPROM size in the respective definitions. The EEPROM FS partitions an external EEPROM in chunks of equal size called slots. Programs and files can be stored in these slots.

I wrote a tutorial on EEPROM use in general at Arduino.cc with the focus on EFS. Some info on schematics is also there.

SD cards

If you compile with ARDUINOSD the SD library is loaded. This costs 12 kB of PROGMEM. LOAD and SAVE work like on Mac and Linux. In addition to this basic Apple DOS style commands CATALOG and DELETE is available as file command. CATALOG lists the files on disk. It stops and waits for a key to be pressed if used with an LCD display. DELETE erases a file. No safety net here. The file is gone for good after DELETE.

SD card support needs a lot of buffer space. If there are instabilities with SD cards, consider reducing the BASIC memory parameter by hand. Some SD cards don't work with Arduinos. One has to try different model to find an optimal solution.

ESPSPIFFS

On ESP SPIFFS is supported for the internal filesystem. All filesystem commands work on the internal flash after compiling the interpreter with the flags #define ESPSPIFFS.

The FDISK command has to be used on a new microcontroller to format the flash. FDISK erases the flash and creates a new filesystem.

The use of ESPSPIFFS has to be configured at upload of BASIC in the Arduino IDE.

RP2040LITTLEFS

Support for LittleFS on RP2040 is added using the LittleFS POSIX API. All BASIC file commands supported.

Autoexec on Arduino and ESP

Any system with a file storage has autorun now. The file autoexec.bas is automatically executed after start of the system. An autoexec program can be interrupted with '#'.

File I/O

Basic file I/O is implemented. OPEN "filename", 0 opens a file for reading, OPEN "filename", 1 opens for writing and OPEN "filename", 2 for append. CLOSE 0, CLOSE 1 and CLOSE 2 closes the respective file. To write to a file the modifier &16 has to be set in a print command. PRINT &16, "hello world" writes to a file. INPUT &16, A$ reads from a file. There can be only one file open for read and one file for write/append at the same time. Opening an new file flushes the file and closes it.

File I/O status can be checked by reading the special variable @S. If @S is 0 the operation was successful, any other value indicates an exception. @S has to be reset by the program. It stays different from 0 after an exception until @S=0 is set. Typical conditions are EOF which leads to @S=1.

Only one file can be open for read and for write/append at the same time.

I/O channels

The modifier &16 in the PRINT and INPUT command addresses the output channels.

BASIC has output stream numbers to steer the input and output of characters. Currently implemented channels are

&1: primary serial line

&2: keyboard and display

&4: secondary serial line (for printers)

&7: I2C wire communication

&8: RF2401 radio communication

&9: MQTT communication via Wifi

&16: File I/O

The I/O channels are the BASIC version of the stream class. For all channels the function AVAIL() in BASIC corresponds to the available() function in C++.

Please consult the manual for details in I/O channels in BASIC: https://github.com/slviajero/tinybasic/blob/main/MANUAL.md

Adding a file system driver to BASIC

BASIC is built with a generic file system driver interface. The BASIC interpreter basic.c (or its .ino equivalent) does not know anything about the concrete implementation of the filesystem. This is done completely in hardware-arduino.h. Any device that has handle file names and character streams can be integrated as a filesystem. This is done in the following steps.

Define a name and a macro for the filesystem.

In hardware-arduino.h the various file systems are identified with a macro like ARDUINOSD or ESPSPIFFS. If you want to integrate your own filesystem, give it a name first. Let's assume it to be ARDUINOMYFS for the rest of this text.

Add this macro in the first few lines of hardware-arduino.h close to the ARDUINOSD definition. You would add a line

#define ARDUINOMYFS

Further down in the code around line 500 there is a section of dependencies. This is were NEEDSWIRE appears for the first time. If your filesystem needs I2C or SPI, add a section that looks like

#ifdef ARDUINOMYFS

#define NEEDSWIRE

#endif

or

#ifdef ARDUINOMYFS

#define ARDUINOSPI

#endif

this makes sure that the drivers for I2S and/or SPI are loaded and initialised.

Further down, around line 700 there is a section where libraries are loaded. Look for the ARDUINOSD section and use it as a template for the library loading of your filesystem. Typically, your code section would look like

#ifdef ARDUINOMYFS

#define FILESYSTEMDRIVER

#include <MyFsDriver.h>

#endif

Load all your libraries here and define the FILESYSTEMDRIVER macro. This makes sure BASIC knows about the file system driver.

Build the file access functions

To load and save programs and to access files, the following methods and variables need to be implemented:

FILE ifile;

FILE ofile;

void fsbegin(char);

int fsstat(char);

void filewrite(char);

char fileread();

char ifileopen(const char*);

void ifileclose();

char ofileopen(char*, const char*);

void ofileclose();

int fileavailable();

This happens around line 3700 in hardware-arduino.h. Again, best use ARDUINOSD or ESPSPIFFS as an example.

BASIC assumes that there is one file descriptor for read ifile and one for write ofile. It also assumes that all filenames are without a path prefix. Patch prefixes are handled by the mkfilenname() and emrootfsprefix() function. The essentially add a necessary prefix or remove it. The internal data structure of BASIC limit the file name to 31 bytes.

fsbegin() starts the filesystem. It is called once at the start of BASIC. CALL 1 in BASIC can be used to call this method again. This may be helpful if there is a medium change like an EEPROM, a tape or a disk.

On error, all of these function have to set ert to 1. On end of file, they need to set ert=-1. fileread() should return -1 on end of file.

Once you have implemented all methods, your BASIC interpreter can open and close files.

Build the directory access functions

Directory access is a separate set of function. They are not needed to for file access but need to be implemented for the CATALOG, DELETE, and FDISK commands. These functions are

void rootopen();

int rootnextfile();

int rootisfile();

const char* rootfilename();

int rootfilesize();

void rootfileclose();

void rootclose();

void removefile(char*);

void formatdisk(short);

rootopen() is called to open the directory BASIC operates on. rootnextfile() must iterate through the directory. rootisfile() must return true if the object in the directory is a file. rootfilename() must return a pointer to the name of the file currently under inverstigation in the directory. rootfilesize() must return its size. rootfileclose() closes the active file. rootclose() closes the directory structure.

The complete sequence of directory access in BASIC is

rootopen();

while (rootnextfile()) {

	if (rootisfile()) {

		name=rootfilename();

		if (*name != '_' && *name !='.' && streq(name, filename)){

			outscf(name, 14); outspc();

			if (rootfilesize()>0) outnumber(rootfilesize()); 

			outcr();

			if ( dspwaitonscroll() == 27 ) break;

		}

	}

	rootfileclose();

}

rootclose();

The root directory is opened, and BASIC iterates through it. The name the the size is printed. Some platforms require the file to be closed. At the end of the iteration the root file system is closed. Some platforms require this.

Deleting a file is done by removefile(). It requires the file name without a prefix.

FDISK formats the file system. It receives one numerical argument. The interpretation of the numerical argument is platform dependent.