반응형

SPIFFS는 SPI 연결을 이용하는 FLASH 저장 장치의 파일 시스템을 정의하고 데이터가 저장된 파일의 생성 및 수정 그리고 읽기를 하는 프로그램이다. 

 

SPIFFS는 EEPROM과 다르게 인덱스를 기준으로 저장하지 않고 파일 단위로 데이터를 구분하여 저장할 수 있다. 파일 단위로 저장할 수 있게되면서 스트링 형식의 데이터를 처리하는데 용이하고, 그 스트링은 JSON 형식 또는 배열 형식이 될 수 있다. 이 얘기는 JSON 형식일 경우 파일 내에 변수 인자가 포함 된다는 것이고, 배열 형식일 경우 EEPROM처럼 인덱스를 이용하여 데이터의 처리가 가능하다는 것이다. 이는 독립된 EEPROM 여러개를 사용하는 것과 비슷하다. 따라서 EPROM을 대체할 수 있으며, 데이터 초기값을 포함한 파일(예: config.txt)을 컴파일 전에 미리 올려 놓을 수 있어서 EEPROM처럼 MCU 셋업시 초기값을 써주어야 하는 과정을 생략할 수 있는 장점이 있다.  단점은 EEPROM과 마찬 가지로 데이터 쓰기시 상당한 시간이 소요된다. 상당한 시간이라고 했지만 보통은 불편함 없이 사용할 수 있다. 다만, 타이머에 의한 인터럽트가 빠른 주기로 실행되고 있을 때에는(예: LED 디스플레이 출력용 데이터 전송 등) 타이머 인터럽트가 실행되는 시간 간격보다 더 많은 시간이 소요되어 타이머 인터럽트의 실행을 방해하고 오류를 발생 시킬 수 있다.

 

이미 파일 형태로 저장된 데이터를 스케치 컴파일전에 SPIFFS파일 시스템에 업로드 시키기 위해서는 아두이노 IDE에 플러그인 형태로 설치되는 프로그램을 사용하여야 한다. 플러그인 설치에 관해서는 이글의 말미에 언급하겠다.

 

목표: SPIFFS를 이용하여 다음과 같은 작업을 시행해 보자.

1. SPIFFS 파일 시스템 포맷하기

2. 스트링 데이터 저장, 읽기, 수정하기

3. Stream 함수 테스트

4. 와이파이 환경설정 정보를 저장하고 읽기

5. 폰트파일을 모듈에 업로드 하고 그 폰트파일을 이용하여 시리얼 모니터에 문자 이미지를 출력하기

 

아래는 ESP8266의 시스템 구조이다.

ESP8266의 파일 시스템은 저장 장치 FLASH(NodeMcu의 경우 4M)를 각 사용 영역별로 분할 하게 되는데 상기 그림에서 볼 수 있듯이 Sketch, OTA update, File system, EEPROM, WiFi config(SDK)로 나뉘며 WiFi config를 제외한 나머지 항목들을 아두이노 IDE의 툴 -> Flash Size 항목(ESP32의 경우 Partition Scheme)에서 그 크기들에 대한 설정을 할 수 있다.  

 

Sketch 영역은 스케치가 업로드되는 영역이고 OTA update는 무선으로 스케치를 변경하기 위해 할당된 영역이며 File system이 다루고자 하는 SPIFFS 라이브러리에 의해 생성된 파일들이 저장되는 영역이다. WiFi config 영역은 WiFi 환경에 관한 내용들 SSID, Password등 WiFi 연결에 관한 설정을 저장하는 항목인데 보안상 이유 때문에 일반적으로 직접 접근하여 수정할수 없고 Wifi 라이브러리에 의해 관리되는 영역이다. 이러한 이유로 와이파이 관련 데이터가 변경될 경우 이전 데이터가 남아 있어 WiFi 연결이 지연되거나 오류(Stack Error)를 유발하는 경우가 있었다. SSID와 Password를 변경한 스케치를 반복적으로 업로드 하던중 간혹 발생했었다. 이런 오류가 발생하면 ESP8266의 전체 시스템을 공장 초기화 하고 스케치를 다시 업로드 하는게 쉬운 해결 방법이다.  공장 초기화 방법은 이전글 Esp8266 NodeMcu 및 ESP32 Dev Module, stack 오류 을 참조하기 바란다.

 

아두이노 IDE에서 SPIFFS 파일시스템 설정 하기

SPIFFS 파일시스템을 사용하기 위해서는 아두이노 IDE의 툴 -> 모듈 항목에서 SPIFFS 파일시스템에서 사용할 용량을 설정해주어야 한다.

ESP8266 NodeMcu 파일시스템 설정(SPIFFS 사용)
ESP32 파일시스템 설정(SPIFFS 사용)

1. SPIFFS 파일 시스템 포맷하기

SPIFFS 파일시스템내 파일을 모두 삭제하고자 할 때 사용하면된다.  스케치를 업로드 했는데 SPIFFS 파일시스템을 마운트하는데 실패하는 경우가 발생할 수 있다. 그럴때는 상기의 파티션 설정에서 SPIFF 파일 시스템을 사용하도록 설정했는지 확인해보거나 SPIFFS 파일 시스템을 포맷해 보기 바란다.

SPIFFS_format.ino
0.00MB
#include "FS.h"  // ESP8266 SPIFFS 라이브러리
//#include "SPIFFS.h"  // ESP32 SPIFFS 라이브러리

void setup() {
  Serial.begin(115200);
  Serial.println();
  SPIFFS.begin();
  SPIFFS.format();
  Serial.println("Format complete!");
}

void loop() {

}

 

 

2. 스트링 데이터 저장, 읽기, 수정하기

SPIFF 라이브러리 역시 ESP8266과 ESP32 라이브러리 코드가 비슷하다. ESP8266 SPIFF 코드를을 기반으로 ESP32용 코드가 만들어 졌으리라 생각한다. 따라서 우선 ESP8266 SPIFF 코드들을 살펴보고 ESP32 코드의 다른점들을 비교해 보겠다. 

 

우선, 예제로서 ESP32의 SPIFFS 예제를 가져와 ESP8266에서 사용할 수 있도록 코드를 수정해 주었다. ESP32의 SPIFFS 예제는 파일 생성 / 삭제, 스트링 쓰기, 추가하기와 쓰기 읽기 성능 테스트(벤치 마크)를 하도록 코딩되어 있다. 성능 테스트 기능이 있으므로 ESP8266의 CPU Frequency를 변경해가며 테스트도 해보자.

 

SPIFFS 라이브러리 명칭

#include "FS.h"        // ESP8266
#include "SPIFFS.h"   // ESP32

 

아래 스케치는 ESP32용 SPIFFS 테스트 코드를 ESP8266에 맞게 수정해 준 것이다.

ESP8266_example_SPIFFS_Test.ino
0.00MB
#include "FS.h"

void setup(){
  Serial.begin(115200);
  Serial.println();
  if (!SPIFFS.begin()) {
    Serial.println("Failed to mount file system");
    return;
  }
  SPIFFS.format();
  FSInfo fsInfo;
  SPIFFS.info(fsInfo);
  Serial.print("totalBytes: "); Serial.println(fsInfo.totalBytes);
  Serial.print("usedBytes: "); Serial.println(fsInfo.usedBytes);
  listDir("/");
  writeFile("/hello.txt", "Hello ");
  readFile("/hello.txt");
  appendFile("/hello.txt", "World!\r\n");
  readFile("/hello.txt");
  renameFile("/hello.txt", "/foo.txt");
  readFile("/foo.txt");
  deleteFile("/foo.txt");
  testFileIO("/test.txt");
  deleteFile("/test.txt");
  Serial.println( "Test complete" );
}

void loop(){

}

void listDir(const char * dirname){
  Serial.printf("Listing directory: %s\r\n", dirname);
  Dir dir = SPIFFS.openDir(dirname);
  while (dir.next()) {
    Serial.print("File Name: "); Serial.print(dir.fileName());
    if(dir.fileSize()) {
      File f = dir.openFile("r");
      Serial.print(", Size: "); Serial.println(f.size());
    }
  }
}

void readFile(const char * path){
  Serial.printf("Reading file: %s\r\n", path);
  File file = SPIFFS.open(path, "r");
  if(!file || file.isDirectory()){
    Serial.println("- failed to open file for reading");
    return;
  }
  Serial.println("read from file:");
  while(file.available()){
    Serial.write(file.read());
  }
}

void writeFile(const char * path, const char * message){
  Serial.printf("Writing file: %s\r\n", path);
  File file = SPIFFS.open(path, "w");
  if(!file){
    Serial.println("failed to open file for writing");
    return;
  }
  if(file.print(message)){
    Serial.println("file written");
  } else {
    Serial.println("frite failed");
  }
}

void appendFile(const char * path, const char * message){
  Serial.printf("Appending to file: %s\r\n", path);
  File file = SPIFFS.open(path, "a");
  if(!file){
    Serial.println("failed to open file for appending");
      return;
  }
  if(file.print(message)){
    Serial.println("message appended");
  } else {
    Serial.println("append failed");
  }
}

void renameFile(const char * path1, const char * path2){
  Serial.printf("Renaming file %s to %s\r\n", path1, path2);
  if (SPIFFS.rename(path1, path2)) {
    Serial.println("file renamed");
  } else {
    Serial.println("rename failed");
  }
}

void deleteFile(const char * path){
  Serial.printf("Deleting file: %s\r\n", path);
  if(SPIFFS.remove(path)){
    Serial.println("file deleted");
  } else {
    Serial.println("delete failed");
  }
}

void testFileIO(const char * path){
  Serial.printf("Testing file I/O with %s\r\n", path);
  static uint8_t buf[512];
  size_t len = 0;
  File file = SPIFFS.open(path, "w");
  if(!file){
    Serial.println("failed to open file for writing");
    return;
  }
  size_t i;
  Serial.print("writing" );
  uint32_t start = millis();
  for(i=0; i<1024; i++){      // ESP8266 - 1024
    if ((i & 0x001F) == 0x001F){
      Serial.print(".");
    }
    file.write(buf, 512);
  }
  Serial.println("");
  uint32_t end = millis() - start;
  Serial.printf("%u bytes written in %u ms\r\n", 1024 * 512, end);
  file.close();
  file = SPIFFS.open(path, "r");
  start = millis();
  end = start;
  i = 0;
  if(file && !file.isDirectory()){
    len = file.size();
    size_t flen = len;
    start = millis();
    Serial.print("reading" );
    while(len){
      size_t toRead = len;
      if(toRead > 512){
        toRead = 512;
      }
      file.read(buf, toRead);
      if ((i++ & 0x001F) == 0x001F){
        Serial.print(".");
      }
      len -= toRead;
    }
    Serial.println("");
    end = millis() - start;
    Serial.printf("%u bytes read in %u ms\r\n", flen, end);
    file.close();
  } else {
    Serial.println("failed to open file for reading");
  }
}

 

전체적인 테스트 흐름을 살펴보자. 

1. setup() 함수에 진입하면 SPIFFS 파일 시스템을 포맷한다. 

2. 루트 디렉토리의 파일 리스트를 읽어온다. (포맷했으므로 파일리스트는 출력되지 않는다.)

3. hello.txt 파일을 생성하고 스트링 "Hello"를 저장한다.

4. 저장한 hello.txt의 데이터를 읽어온다. 

5. hello.txt 파일에 스트링 "World!\r\n"를 추가한다. 

6. 데이터가 추가된 hello.txt 파일의 데이터를 읽어온다. 

7. hello.txt 파일 이름을 foo.txt 파일로 변경한다. 

8. 변경한 foo.txt 파일의 데이터를 읽어온다. 

9. foo.txt 파일을 삭제한다. 

10. test.txt 파일을 생성하고 쓰기 및 읽기 성능테스트를 한다.

11. test.txt 파일을 지운다. 

 

아래는 시리얼 모니터에 출력된 내용이다.  (CPU Frequency: 160Mhz )

Writing file: /hello.txt 
file written 
Reading file: /hello.txt 
read from file: 
Hello Appending to file: /hello.txt 
message appended 
Reading file: /hello.txt 
read from file: 
Hello World! 
Renaming file /hello.txt to /foo.txt 
file renamed 
Reading file: /foo.txt 
read from file: 
Hello World! 
Deleting file: /foo.txt 
file deleted 
Testing file I/O with /test.txt 
writing................................ 
524288 bytes written in 6431 ms 
reading................................ 
524288 bytes read in 173 ms 
Deleting file: /test.txt 
file deleted 
Test complete

 

 

상기 예제에 사용된 SPIFFS 제어용 코드들에 대해 살펴보자. 

 

SPIFFS.begin(); // SPIFFS 파일 시스템을 마운트, 성공: true, 실패: false 반환

setup() 함수에서 플래그 리턴값을 사용하여 SPIFFS 파일 시스템 시작에 대한 메시지를 출력한다. 

 

SPIFFS.format(); // 파일 시스템을 포맷, 포맷 성공: true 반환 

setup() 함수 초기 진입시 테스트를 위해 SPIFFS 파일시스템의 파일들을 모두 지운다. 

 

SPIFFS.info();  // FSInfo 구조체에 파일 시스템 정보를 채운다. - 성공하면 true 반환 

FSInfo fsInfo;       // 파일시스템 스트럭쳐 객체 선언 
SPIFFS.info(fsInfo); // 파일시트템 정보 값 확인 및 저장 
Serial.print("totalBytes: "); Serial.println(fsInfo.totalBytes); // 스트럭쳐 변수에 접근 원하는 값을 불러온다. 
Serial.print("usedBytes: "); Serial.println(fsInfo.usedBytes);   // 스트럭쳐 변수에 접근 원하는 값을 불러온다. 

 

Filesystem information structure 
struct FSInfo { 
    size_t totalBytes; 
    size_t usedBytes; 
    size_t blockSize; 
    size_t pageSize; 
    size_t maxOpenFiles; 
    size_t maxPathLength; 
};

 

SPIFFS.openDir(path); // 절대 경로가 지정된 디렉토리를 연다. Dir 객체를 리턴 

SPIFFS 디렉토리 객체(Dir)를 선언하고 사용해야 한다. (예: Dir test = SPIFFS.openDir(path);

 

listDir() 사용자 함수에서 루트('/') 디렉토리에 있는 파일 리스트를 확인하고 출력하도록 사용하였다. 초기 진입시 SPIFFS 파일 시스템을 포맷하므로 파일 리스트는 출력되지 않는다. 

 

SPIFFS.open(path, mode);  // 파일 열기(경로, 액세스 모드), 파일이 열리면 true 반환

SPIFFS 파일 객체(File)를 선언하고 사용해야 한다. (예: File test = SPIFFS.open("/test.txt", "r"); 

 

path: '/'로 시작하는 절대 경로(예: "/dir/filename.txt")이며 절대 경로의 길이는 '/', 이름, ',' 및 확장자 포함하는 최대 31문자까지 사용할 수 있다. 
mode(액세스 모드):  표현 "r", "w", "a", "r+", "w+", "a+"
r     // 읽기전용  
w    // 쓰기- 파일이 없으면 파일을 만들고, 있으면 파일크기를 0으로 만든후 처음 부터 쓰기시작  
a    // 쓰기(추가) - 파일이 없으면 파일을 만들고, 있으면 파일의 맨 마지막부터 쓰기시작  
r +  // 먼저 읽고 쓰기 - 파일의 처음부터 쓰기 시작 
w + // 먼저 읽고 쓰기 - 파일이 없으면 파일을 만들고, 있으면 파일크기를 0으로 만든후 처음 부터 쓰기시작  
a + // 먼저 읽고 쓰기 - 파일이 없으면 파일을 만들고, 있으면 파일의 맨 처음부터 읽고, 맨 마지막부터 쓰기시작 

모드 a(추가)를 제외한 쓰기는 쓸때마다 파일 크기를 0으로 만든다음(모두 지운다음) 다시 쓴다는데 주의를 해야한다. EEPROM 처럼 특정인덱스의 값만 바꾸는 것이 아니다. 먼저 읽고 쓰기는 이러한 특성에 기인해 필요한 기능이다. 저장된 데이터의 일부만을 수정하고자 한다면 필요에 따라서 데이터를 읽어 분리 시킨다음 수정할 부분을 수정한후 다시 합쳐서 저장하는 개념이다(거의 사용하지 않는다). 

readFile(), writeFile(), appendFile(), testFileIO() 사용자 함수에서 사용되었다. 

 

불리언 리턴값을 이용한 파일 열기의 확인 

File f = SPIFFS.open("/f.txt", "w"); 
  if (!f) {                                       // 파일 오픈 확인 
     Serial.println("file open failed"); 
  } 

 

SPIFFS.rename(pathFrom, pathTo); // (대상파일 경로 및 이름, 변경할 파일 경로 및 이름) 파일의 이름이 성공적으로 변경되면 true를 반환 

renameFile() 사용자 함수에서 사용되었다. 

 

SPIFFS.remove(path); // 절대 경로가 지정된 파일을 삭제한다. 파일이 삭제 된 경우 true를 반환 

deleteFile() 사용자 함수에서 삭제와 동시에 반환되는 리턴값을 이용하여 메시지를 출력 하고있다.  

 

 

File object 
SPIFFS.open() 함수, dir.openFile()함수는 파일 객체에서 스트림을 사용할 수 있도록 readBytes, findUntil, parseInt(), parseFloat(), println 및 기타 모든 스트림 방법(시리얼 통신 함수)을 지원 한다.

예: String line = f.readStringUntil('\n');  // 객체.readStringUntil('\n'); 스트링 읽기

*" indexOf();" 와 같은 스트링 클래스 함수는 사용할 수 없다.

 

시리얼 통신에 사용하는 스트림 함수와 스트링 클래스 함수에 대한 자세한 사항은 이전글  아두이노 - 시리얼통신 주요함수와 예제, String class를 참조하기 바란다.          

 

file.seek(offset, mode); // 이 함수는 fseek C 함수 처럼 동작 

SeekSet: 시작부터 offset 바이트만큼 이동. 
SeekCur: 현재의 위치에서 offset 바이트 만큼 이동. 
SeekEnd: 파일의 끝에서 부터 offset 바이트만큼 이동.

예: f.seek(16, SeekSet); // "0100 0011 1100 0010 0101 0110 1001 1010" 데이터가 있다면 커서위치 16에 커서를 위치한다.
    for (int i = 0; i < 8; i++) val = f.read(); // 커서위치 16에서 8개 데이터 0101 0110을 읽어온다. 

커서 위치에 관한 설명은 3.Stream 함수 테스트에 부연되어 있다.

 

file.position(); // 파일 내의 현재의 커서 위치를 바이트로 돌려준다.

 

file.size(); // 파일 크기를 바이트 단위로 반환 한다.

 

file.name(); // 파일 이름을 const char*로 반환 한다.

예: String name = file.name(); // 경로포함 이름이 저장된다. /hello.txt

 

file.fullName() // 디렉토리 내의 파일이름을 디렉토리 경로포함 한 이름으로 const char* 자료형으로 반환한다.

Dir d = SPIFFS.openDir("testdir/");       // 디렉토리 파일명: testdir/file1일때
File f = d.openFile("r");                 // f.name(): "file1", f.fullName() == "testdir/file1" 

 

file.close(); // 파일 닫기 - 파일 사용 종료

 

SPIFFS.end(); // SPIFFS 파일 시스템을 마운트 해제 - OTA를 사용하여 SPIFFS를 업데이트하기 전에 해제 해야함.


SPIFFS.exists(path); // 주어진 경로에 파일이 있으면 true 반환

 

Directory object (Dir):  Dir 객체의 목적은 파일 이름을 사용하지 않고 디렉터리 내의 파일로 이동하는 것

dir.next()  // 파일 위치를 다음으로 이동시킨다. fileName(), fileSize(), openFile() 함수 실행전 선행되어야 한다. 
dir.fileSize()  // 디렉토리내 현재위치의 파일사이즈를 반환한다. 

 

Dir dir = SPIFFS.openDir("/data"); 
while (dir.next()) { 
    Serial.print(dir.fileName()); 
    if(dir.fileSize()) { 
        File f = dir.openFile("r"); 
          Serial.println(f.size()); 
    } 
}

 

상기에 설명되지 않은 내용은 아래 사이트에서 추가로 확인해 볼 수 있다.

https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html

 

 

ESP8266와 ESP32의 SPIFFS  라이브러리 차이점

ESP32_example_SPIFFS_Test.ino
0.00MB

SPIFFS 파일시스템을 시작할 때 SPIFFS 마운트 실패시 SPIFFS 파일시스템을 포맷할 수 있는 옵션이 추가 되었다. 

#define FORMAT_SPIFFS_IF_FAILED true 

if(!SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)){ 
  Serial.println("SPIFFS Mount Failed"); 
  return; 
}

 

디렉토리 열기 명령어가 변경되었다.

SPIFFS.openDir(dirname)  // ESP8266 
SPIFFS.open(dirname);    // ESP32

 

디렉토리 다음파일 이동 명령어가 변경되었다. 

객체.next();           // ESP8266 
객체.openNextFile();   // ESP32

 

ESP8266은 디렉토리 함수 실행 결과를 저장하기 위해 확장자 "Dir"을 별도로 사용하지만 ESP32는 "File" 확장자로 디렉토리관련 명령과 파일 관련 명령을 처리할 수 있다.  
ESP8266  과 ESP32의 void listDir(const char * dirname) 함수를 비교해 보면 알 수 있다.

// ESP8266 
void listDir(const char * dirname){ 
  Serial.printf("Listing directory: %s\r\n", dirname); 
  Dir dir = SPIFFS.openDir(dirname); 
  while (dir.next()) { 
    Serial.println(dir.fileName()); 
    if(dir.fileSize()) { 
      File f = dir.openFile("r"); 
      Serial.println(f.size()); 
    } 
  } 


// ESP32 
void listDir(const char * dirname){ 
  Serial.printf("Listing directory: %s\r\n", dirname); 
  File root = SPIFFS.open(dirname); // ESP8266은 확장자 "Dir"과 "File"로 구분해서 사용, ESP32는 "File"로 통합 
  File file = root.openNextFile(); 
  while(file){ // 다음 파일이 있으면(디렉토리 또는 파일) 
    if(file.isDirectory()){ // 다음 파일이 디렉토리 이면 
      Serial.print("  DIR : "); Serial.println(file.name()); // 디렉토리 이름 출력 
    } else {                // 파일이면 
      Serial.print("  FILE: "); Serial.print(file.name());   // 파일이름 
      Serial.print("\tSIZE: "); Serial.println(file.size()); // 파일 크기 
    } 
    file = root.openNextFile(); 
  } 
}

 

파일을 읽을 때 옵션 "r"을 생략할 수 있게 되었다. 옵션을 그대로 사용해도 된다.

SPIFFS.open(path, "r");  // ESP8266 
SPIFFS.open(path);       // ESP32

 

 

openNextFile() 함수 이용하여 열기

File root = SPIFFS.open("/");      // 파일 객체 디렉토리 열기
File file1 = root.openNextFile();   // 파일 명이 아닌 openNextFile() 함수를 이용 파일 열기

File file2 = root.openNextFile();   // 다음 파일 열기
file1.close(); 
root.rewindDirectory();              // 디렉토리 내 파일 위치 포인터 초기화 - 맨 처음으로 이동
file1 = root.openNextFile();        // 디렉토리 내 첫번째 파일 열기

 

디렉토리.rewindDirectory(); // 파일 객체로 선언된 디렉토리를 열고 "디렉토리.openNextFile();" 함수를 이용하여 파일을 열었을 경우 디렉토리 내 파일의 위치를 지정하는 포인터를 초기화하여 디렉토리 내 맨 처음 위치로 포인터를 이동시킨다. 

 

3. Stream 함수 테스트

Stream 함수란 시리얼 통신에 사용하는 함수들로써 시간의 경과에 따라 들어오는 연속되는 데이터의 흐름을 처리하는 함수이다. SPIFFS 라이브러리는 이러한 Stream 함수를 지원한다고 한다.  아래 스케치를 통해 Stream 함수를 활용하는 예제들을 살펴보자.   

ESP8266_SPIFFS_Stream_fn_test.ino
0.00MB
ESP32_SPIFFS_Stream_fn_test.ino
0.00MB
#include "FS.h"     // ESP8266

unsigned long int atime;        // 시작 시간, 밀리 초

void listDir(const char * dirname){
  Serial.printf("Listing directory: %s\r\n", dirname);
  Dir dir = SPIFFS.openDir(dirname);
  while (dir.next()) {
    Serial.print("File Name: "); Serial.print(dir.fileName());
    if(dir.fileSize()) {
      File f = dir.openFile("r");
      Serial.print(", Size: "); Serial.println(f.size());
    }
  }
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  if (!SPIFFS.begin()) {
    Serial.println("Failed to mount file system");
    return;
  }
  FSInfo fsInfo;
  SPIFFS.info(fsInfo);
  Serial.print("totalBytes: "); Serial.println(fsInfo.totalBytes);
  Serial.print("usedBytes: "); Serial.println(fsInfo.usedBytes);
  listDir("/");
  String temp = "test, 53.3, teH_Sst\"2, 5.2, 85"; // 저장될 문자열 "test, 53.3, teH_Sst"2, 5.2, 85"
  atime = micros();
  File f =  SPIFFS.open("/Stream_test.txt", "w");  
  f.println(temp);  // println사용 - '\n'문자 포함 쓰기
  f.close();
  atime = micros() - atime;
  Serial.print("Wirting time: "); Serial.print(atime); Serial.println(" micro sec");
  f = SPIFFS.open("/Stream_test.txt", "r");
  Serial.println(f.readStringUntil('\n')); // '\n'문자 까지 읽기
  f.close();
  f = SPIFFS.open("/Stream_test.txt", "r");
  Serial.println(f.parseFloat());   // 스트림 함수 테스트
  Serial.println(f.parseInt());
  Serial.println(f.parseFloat());
  Serial.println(f.parseInt());
  f.close(); 
  f = SPIFFS.open("/Stream_test.txt", "r");
  uint16_t position_t;
  f.find("Sst\"");  // 문자열 Sst" 검색 
  position_t = f.position();
  Serial.print("Sst\" position: "); Serial.println(position_t); // Sst" position
  Serial.println(char(f.read())); // f.read()는 byte로 읽는다 -> DEC 50: - CHAR 2
  f.close(); 
  f = SPIFFS.open("/Stream_test.txt", "r");
  f.find("H");  // "" 문자 또는 문자열("H_S") 검색 
  position_t = f.position();
  Serial.print("H position: "); Serial.println(position_t); // " position
  char tempA[4] = { '\0', }; // 문자열로 출력하기 위해 '\0'으로 초기화, 문자열 크기: 배열크기 -1 
  f.readBytes(tempA, sizeof(tempA)-1);  // 배열 크기 -1 보다 작게
  Serial.println(tempA);
  f.find('"');  // 따옴표를 검색할 경우에는 ''사용 문자 검색 사용 나머지는 "" 사용
  position_t = f.position();
  Serial.print("\" position: "); Serial.println(position_t); // '"' position
  char tempB[7] = { '\0', }; // 문자열로 출력하기 위해 '\0'으로 초기화, 문자열 크기: 배열크기 -1 
  f.readBytes(tempB, sizeof(tempB)-1);  // 배열 크기 -1
  Serial.println(tempB);
  f.close();
  f = SPIFFS.open("/Stream_test.txt", "r"); // "test, 53.3, teH_Sst\"2, 5.2, 85"
  byte len = f.size();
  Serial.println(len);
  while (position_t < len) {
    f.findUntil("e", ",");  // 문자 'e'를  ','를 기준으로 검색한다. 
    position_t = f.position();
    Serial.print("Cursor position: "); Serial.println(position_t); // 검색중 커서의 위치를 출력한다.
  }
  f.close();
  Serial.println("test finished"); 
}

void loop() {
  if(Serial.available() > 0){
    String temp = Serial.readStringUntil('\n');
    if (temp == "1") {      // 시리얼 모니터에 1을 입력하면 포맷
      Serial.println("File System Format....");
      if(SPIFFS.format())  Serial.println("File System Formated");  //Format File System
      else   Serial.println("File System Formatting Error");
    }
    else if (temp == "2") { // 2를 입력하면 파일 리스트 출력
      listDir("/");
    }
  }
}

 

테스트에 사용할 문자열은 아래와 같이 ','로 구분되어 있으며 소수와 정수를 포함하고 있다. 콤마에 의해 구분되어 있으므로 parseInt()와 parseFloat()를 사용하여 수소 및 정수를 추출 할 수 있게 된다.

     

문자열 : test, 53.3, teH_Sst"2, 5.2, 85

 

스트림 함수와 커서의 위치

SPIFFS 파일시스템 라이브러리에서 스트림 함수는 커서의 위치를 기준으로 처리되는데 첫번째 문자의 앞이 커서의 0번째 위치이며 문자나 문자열을 검색할 경우 검색한 문자나 문자열 다음에 커서가 위치한다.  

            t e s t ,   5 3 . 3 ,   t e H _ S s t " 2 ,   5 . 2 ,   8 5
커서위치:: 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0

 

만약 f.find("Sst\"");  함수를 사용하여 문자열 Sst" 을 검색하면 커서의 위치는 " 다음인 20에 위치한다. 

f.position() 함수는 현재 커서의 위치를 반환한다.

f.read() 함수는 현재 위치에서 1byte를 읽고 byte값을 반환한다. 

f.findUntil("e", ",") 함수는 ','를 기준으로 문자 'e'를  검색한다. 이때 커서의 위치는 문자 'e' 와 ',' 다음에 위치하게 된다. 

Cursor position: 2 
Cursor position: 5 
Cursor position: 11 
Cursor position: 14 
Cursor position: 22 
Cursor position: 27 
Cursor position: 32

 

 

 

4. 와이파이 환경설정 정보를 저장하고 읽기

 

SPIFF 파일 시스템을 이용하여 와이파이 환경설정을 config.txt 파일에 저장하고  설정 값이 변경될 경우 그 값을 config.txt 파일에 저장하고 와이파이 연결시 변경된 값을 읽어와 연결하도록 해보자.

SPIFFS_config.ino
0.00MB
#include "FS.h" // ESP8266
//#include "SPIFFS.h"  // ESP32 SPIFSPIFFS 라이브러리

char ssid[21]          = "YOUR_SSID";
char pass[21]          = "YOUR_PASS";
float timeZone       = 9;
uint8_t summerTime     = 0; // 3600

String Name(String a) {  
  String temp = "\"{v}\":";
  temp.replace("{v}", a);
  return temp;
}

String strVal(String a) {  
  String temp = "\"{v}\",";
  temp.replace("{v}", a);
  return temp;
}

String intNum(int a) {  
  String temp = "{v},";
  temp.replace("{v}", String(a));
  return temp;
}

String floatNum(float a) {  
  String temp = "{v},";
  temp.replace("{v}", String(a));
  return temp;
}

void stringTo(String ssidTemp, String passTemp) { // 스트링 SSID / PASS 배열에 저장
  for (int i = 0; i < ssidTemp.length(); i++) ssid[i] = ssidTemp[i];
  ssid[ssidTemp.length()] = '\0';
  for (int i = 0; i < passTemp.length(); i++) pass[i] = passTemp[i];
  pass[passTemp.length()] = '\0';
}

/* 데이터 저장 형식 - JSON 형식
   첫 번째, "스트링"처럼 큰 따옴표 기호(")로 묶인 스트링은 변수 명이고 고유하다.   
   두 번째, 변수에 대한 값은 연이어 나오는':'문자 다음에 위치한다. 
   세 번째, 값이 스트링이면 큰 따옴표(")로 묶이고, 값이 숫자(소수/정수)이면 큰 따옴표(")가 없다 
   네 번째, 값 다음에 반드시 콤마(,)가 있다. */

bool saveConfig() { // "SSID":"YOUR_SSID","PASS":"YOUR_PASS","ZONE":9.00,"SUMMER":0,
  String value;
  value = Name("SSID") + strVal(ssid);
  value += Name("PASS") + strVal(pass);
  value += Name("ZONE") + floatNum(timeZone);
  value += Name("SUMMER") + intNum(summerTime);
  File configFile = SPIFFS.open("/config.txt", "w");
  if (!configFile) {
    Serial.println("Failed to open config file for writing");
    return false;
  }
  configFile.println(value); // SPIFF config.txt에 데이터 저장, '\n'포함
  configFile.close();
  return true;
}

String json_parser(String s, String a) { 
  String val;
  if (s.indexOf(a) != -1) {
    int st_index = s.indexOf(a);
    int val_index = s.indexOf(':', st_index);
    if (s.charAt(val_index + 1) == '"') {     // 값이 스트링 형식이면
      int ed_index = s.indexOf('"', val_index + 2);
      val = s.substring(val_index + 2, ed_index);
    }
    else {                                   // 값이 스트링 형식이 아니면
      int ed_index = s.indexOf(',', val_index + 1);
      val = s.substring(val_index + 1, ed_index);
    }
  } 
  else {
    Serial.print(a); Serial.println(F(" is not available"));
  }
  return val;
}

bool loadConfig() {
  File configFile = SPIFFS.open("/config.txt", "r");
  if (!configFile) {
    Serial.println("Failed to open config file");
    return false;
  }
  String line = configFile.readStringUntil('\n');
  configFile.close();
  String ssidTemp = json_parser(line, "SSID");
  String passTemp = json_parser(line, "PASS");
  stringTo(ssidTemp, passTemp);                // String을 배열에 저장
  String temp = json_parser(line, "ZONE");
  timeZone = temp.toFloat();                   // 스트링을 float로 변환
  temp = json_parser(line, "SUMMER");
  summerTime = temp.toInt();                   // 스트링을 int로 변환
  return true;
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("Mounting FS...");
  if (!SPIFFS.begin()) {
    Serial.println("Failed to mount file system");
    return;
  }
  if (SPIFFS.exists("/config.txt")) loadConfig();
  else {
    saveConfig();
    loadConfig();
  }
  Serial.println(ssid);
  Serial.println(pass);
  Serial.println(timeZone);
  Serial.println(summerTime);
}

void loop() {
  if(Serial.available() > 0){
    String temp = Serial.readStringUntil('\n');
    if (temp == "1") {
      Serial.println("File System Format....");
      if(SPIFFS.format())  Serial.println("File System Formated");  //Format File System
      else   Serial.println("File System Formatting Error");
    }
    else if (temp.startsWith("id:")) {
      Serial.println("SPIFF test");
      temp.remove(0, 3);
      int index = temp.indexOf(",");
      String ssidTemp = temp.substring(0, index);
      temp.remove(0, index+1);
      String passTemp = temp;
      stringTo(ssidTemp, passTemp);
      timeZone = -1.5;
      summerTime = 30;
      saveConfig();
      loadConfig();
      Serial.println(ssid);
      Serial.println(pass);
      Serial.println(timeZone);
      Serial.println(summerTime);
    }
  }
}

상기 스케치를 업로드 하고 시리얼 모니터의 전송옵션이 "새 줄"인 상태에서 입력창에 

"id:YOUR_NEW_SSID,YOUR_NEW_PASS"라고 입력후 엔터를 치면  아이디 및 비밀번호가 변경된 것을 확인 할 수 있다.

작동 흐름은 다음과 같다. 

setup() 함수에서 SPIFFS 파일시스템을 마운트하고 config.txt 파일이 있는지를 검사한다. 파일이 없으면 

saveConfig() 함수를 실행시켜 config.txt 파일을 생성하고 그 파일에 해당 초기값을 저장한다. 만약 파일이 있다면 loadConfig() 함수를 실행시켜 config.txt 파일에 저장된 값을 초기값이 저장된 변수에 덮어 씌우기를 한다. 모듈이 리부팅 되기전에 변수의 값이 변경되어 config.txt에 변경된 값이 저장되어 있다면 변수값 덮어 씌우기를 통해 변경된 값이 반영되게 된다. 

if (SPIFFS.exists("/config.txt")) loadConfig(); 
  else saveConfig();

 

saveConfig() 함수에서 변수값을 저장할 때에는 아래 함수들을 사용하여 JSON 형식으로 저장되도록 하였다.

String Name(String a) {      // 문자열을 "문자열":로 변환
  String temp = "\"{v}\":";
  temp.replace("{v}", a);
  return temp;
}

String strVal(String a) {    // 문자열을 "문자열",로 변환
  String temp = "\"{v}\",";
  temp.replace("{v}", a);
  return temp;
}

String intNum(int a) {       // 정수를 정수문자열,로 변환
  String temp = "{v},";
  temp.replace("{v}", String(a));
  return temp;
}

String floatNum(float a) {  // 소수를 소수문자열,로 변환
  String temp = "{v},";
  temp.replace("{v}", String(a));
  return temp;
}

변환 결과 : "SSID":"YOUR_SSID","PASS":"YOUR_PASS","ZONE":9.00,"SUMMER":0,

 

아래 함수는 시리얼 모니터를 를 통해 수신된 스트링 형식의 새로운 아이디와 비밀번호를 배열 ssid와 pass에 저장해주는 코드이다. 

void stringTo(String ssidTemp, String passTemp) { // 스트링 SSID / PASS 배열에 저장
  for (int i = 0; i < ssidTemp.length(); i++) ssid[i] = ssidTemp[i];
  ssid[ssidTemp.length()] = '\0';
  for (int i = 0; i < passTemp.length(); i++) pass[i] = passTemp[i];
  pass[passTemp.length()] = '\0';
}

 

loadConfig() 와 관련된 코드에 대한 설명은 이전 글 아두이노 - JSON 형식 데이터를 parsing하는 방법 및 코드 에서 확인할 수 있다.

 

5. 예제용 폰트파일을 모듈에 업로드하고 그 폰트파일을 이용하여 시리얼 모니터에 문자 이미지를 출력하기

폰트파일의 문자를 시리얼 모니터로 출력한 이미지

폰트파일을 모듈에 업로드 하기 위해서는 아두이노 IDE 플러그인 형태의 프로그램을 다운로드 받고 설치해야만 한다. 

 

 

ESP8266 / ESP32 SPIFFS 플러그인 다운로드 및 설치

 

1. ESP8266 

https://github.com/esp8266/arduino-esp8266fs-plugin

상기의 페이지로 들어간다. SPIFFS 설치 설명 부분의 "releases page"를 클릭하면 플러그인 다운로드 페이지로 이동한다.

플러그인 다운로드 페이지에서 아래의 ESP8266FS-0.4.0.zip 파일을 다운로드한다.

다운로드한 zip 파일의 압축을 풀면 아래와 같은 폴더가 생성된다.

"ESP8266FS" 폴더에서 마우스 우측 버튼을 클릭한 뒤 나오는 메뉴창에서 복사를 클릭한다. 

 

아두이노 IDE 설치 폴더로 이동한다. "C" 드라이브에 설치했다면 "C:\Program Files (x86)\Arduino" 경로로 들어가면 되고 Arduino 폴더 안에 아래 그림처럼 "tools" 폴더가 없다면 "tools" 폴더를 만들어 주어야 한다.

"tools" 폴더 안에 복사한 "ESP8266FS" 폴더를 붙여 넣기 하면 된다.

ESP8266 SPIFFS 플러그인이 정상적으로 설치가 되었다면 아두이노 IDE에서 "툴" 항목을 선택했을 때 아래와 같이 "ESP8266 Sketch Data Upload"라는 항목이 생성된 것을 확인할 수 있다.

 

2. ESP32 SPIFFS 최신 플러그인 다운로드 및 설치하기

 

https://github.com/me-no-dev/arduino-esp32fs-plugin

상기의 페이지로 들어간다. SPIFFS 설치 설명 부분의 "releases page"를 클릭하면 플러그인 다운로드 페이지로 이동한다.

플러그인 다운로드 페이지에서 아래의 ESP32FS-1.0.zip 파일을 다운로드한다.

다운받은 zip파일의 압축을 풀면 아래와 같은 폴더가 생성된다.

"ESP32FS" 폴더에서 마우스 우측 버튼을 클릭한 뒤 나오는 메뉴창에서 복사를 클릭 한다.

"ESP32FS" 폴더를 복사한다.

 

아두이노 IDE 프로그램이 설치된 폴더로 이동한다. "C" 드라이브에 설치했다면 "C:\Program Files (x86)\Arduino" 경로로 들어가면 되고 Arduino 폴더 안에 아래 그림처럼 "tools" 폴더가 없다면 "tools" 폴더를 만들어 주어야 한다.

"tools" 폴더안에 복사한 "ESP32FS" 폴더를 붙여넣기 하면 된다.

아두이노 IDE가 실행되고 있다면 종료하고 재실행 해준다. 

ESP8266 SPIFFS 플러그인이 정상적으로 설치가 되었다면 아두이노 IDE에서 "툴" 항목을 선택했을때 아래와 같이 "ESP32 Sketch Data Upload" 라는 항목이 생성된 것을 확인 할 수 있다.

상기처럼 아두이노 IDE 프로그램이 설치된 폴더에 플러그인을 설치하지 않고 스케치가 자동으로 저장되는 폴더에 플러그인을 설치할 수도 있다. 

 

아두이노 IDE에서 파일 -> 환경설정으로 들어가면 표시되는 

스케치북 위치:

C:\Users\Administrator\Documents\Arduino

 

폴더안에 이전 방법과 동일하게 tools 폴더를 생성하고 플러그인 프로그램을 붙여넣기 하고 아두이노를 종료한뒤 재실행하면된다. 

 

최종 주소는 아래와 같다. 

C:\Users\Administrator\Documents\Arduino\tools\플러그인 폴더(ESP8266FS)

 

둘중 한가지 방법을 이용하면 된다. 

SPIFFS 파일시스템에 파일을 업로드 할 수 있도록 하는 플러그인의 설치가 완료 되었다. 

 

 

파일 업로드하기

 

스케치에서 사용할 파일을 SPIFFS 파일 시스템에 업로드 하기위해서는 스케치 파일이 저장되어 있는 폴더안에 "data"폴더를 생성하고 업로드하고자 하는 파일을 data 폴더안에 위치시킨다음 툴 -> ESP8266 Sketch Data Upload를 클릭하면 된다. 이 때 시리얼 모니터 창이 열려있다면 닫아주어야 한다. 

 

data 폴더의 위치가 중요하다. 현재 열려있는 스케치의 폴더안에 data 폴더가 반드시 있어야하고 그 data 폴더안에 업로드 하고자 하는 파일이 위치해야만 한다. 

 

아래 파일은 스케치 폴더안에 테스트용 폰트파일이 포함된 data 폴더를 갖고 있는 예제 파일이다.

ESP8266_SPIFFS_Upload_Test.zip
0.00MB
ESP32_SPIFFS_Upload_Test.zip
0.00MB
#include "FS.h" // ESP8266

void listDir(const char * dirname){
  Serial.printf("Listing directory: %s\r\n", dirname);
  Dir dir = SPIFFS.openDir(dirname);
  while (dir.next()) {
    Serial.print("File Name: "); Serial.print(dir.fileName());
    if(dir.fileSize()) {
      File f = dir.openFile("r");
      Serial.print(", Size: "); Serial.println(f.size());
    }
  }
}

void setup(){
  Serial.begin(115200);
  Serial.println();
  if (!SPIFFS.begin()) {
    Serial.println("Failed to mount file system");
    return;
  }
  FSInfo fsInfo;
  SPIFFS.info(fsInfo);
  Serial.print("totalBytes: "); Serial.println(fsInfo.totalBytes);
  Serial.print("usedBytes: "); Serial.println(fsInfo.usedBytes);
  listDir("/");
}

void loop(){
  if(Serial.available() > 0){
    String temp = Serial.readStringUntil('\n');
    if (temp == "1") {
      Serial.println("File System Format....");
      if(SPIFFS.format())  Serial.println("File System Formated");  //Format File System
      else   Serial.println("File System Formatting Error");
    }
    else if (temp == "2") {
      listDir("/");
    }
  }
}

스케치를 업로드하고 시리얼 모니터에서 '1'을 입력하여 SPIFFS 파일시스템을 포맷 시킨다.  다음 시리얼 모니터를 닫고 아두이노 IDE의 툴 -> "ESP8266 Sketch Data Upload"항목을 클릭한다. 그러면 파일 업로드가 시작된다. 파일 업로드가 완료되면 시리얼 모니터를 켜고 '2'를 입력하여 파일 목록을 확인해 본다. 

 

시리얼 모니터에 문자 이미지를 출력하는 것은 도트 매트릭스에 문자를 출력하는 것과 같은 코드를 이용한다. 단지 차이점은 출력 방식이 LED를 켜고 끄는것과 시리얼 모니터에 0과 1의 상태를 출력하는 차이가 있다.

이전 글 아두이노 - 도트 매트릭스 제어하기, dot matrix 의 코드를 가져오고 2차원 배열 형식의 비트맵 데이터를 사용자 폰트 형식으로 변경해 주었다. 이글에서 설명되지 않은 부분들은 이전 글을 참조하기 바란다. 

 

이전 글에서 사용한 비트맵 이미지 데이터
#define SPACE { \ 
    {0, 0, 0, 0, 0, 0, 0, 0}, \ 
    {0, 0, 0, 0, 0, 0, 0, 0}, \ 
    {0, 0, 0, 0, 0, 0, 0, 0}, \ 
    {0, 0, 0, 0, 0, 0, 0, 0}, \ 
    {0, 0, 0, 0, 0, 0, 0, 0}, \ 
    {0, 0, 0, 0, 0, 0, 0, 0}, \ 
    {0, 0, 0, 0, 0, 0, 0, 0}, \ 
    {0, 0, 0, 0, 0, 0, 0, 0} \ 

#define H { \ 
    {0, 1, 0, 0, 0, 0, 1, 0}, \ 
    {0, 1, 0, 0, 0, 0, 1, 0}, \ 
    {0, 1, 0, 0, 0, 0, 1, 0}, \ 
    {0, 1, 1, 1, 1, 1, 1, 0}, \ 
    {0, 1, 0, 0, 0, 0, 1, 0}, \ 
    {0, 1, 0, 0, 0, 0, 1, 0}, \ 
    {0, 1, 0, 0, 0, 0, 1, 0}, \ 
    {0, 1, 0, 0, 0, 0, 1, 0}  \ 

#define E  { \ 
    {0, 1, 1, 1, 1, 1, 1, 0}, \ 
    {0, 1, 0, 0, 0, 0, 0, 0}, \ 
    {0, 1, 0, 0, 0, 0, 0, 0}, \ 
    {0, 1, 1, 1, 1, 1, 1, 0}, \ 
    {0, 1, 0, 0, 0, 0, 0, 0}, \ 
    {0, 1, 0, 0, 0, 0, 0, 0}, \ 
    {0, 1, 0, 0, 0, 0, 0, 0}, \ 
    {0, 1, 1, 1, 1, 1, 1, 0}  \ 

#define L { \ 
    {0, 1, 0, 0, 0, 0, 0, 0}, \ 
    {0, 1, 0, 0, 0, 0, 0, 0}, \ 
    {0, 1, 0, 0, 0, 0, 0, 0}, \ 
    {0, 1, 0, 0, 0, 0, 0, 0}, \ 
    {0, 1, 0, 0, 0, 0, 0, 0}, \ 
    {0, 1, 0, 0, 0, 0, 0, 0}, \ 
    {0, 1, 0, 0, 0, 0, 0, 0}, \ 
    {0, 1, 1, 1, 1, 1, 1, 0}  \ 

#define O { \ 
    {0, 0, 0, 1, 1, 0, 0, 0}, \ 
    {0, 0, 1, 0, 0, 1, 0, 0}, \ 
    {0, 1, 0, 0, 0, 0, 1, 0}, \ 
    {0, 1, 0, 0, 0, 0, 1, 0}, \ 
    {0, 1, 0, 0, 0, 0, 1, 0}, \ 
    {0, 1, 0, 0, 0, 0, 1, 0}, \ 
    {0, 0, 1, 0, 0, 1, 0, 0}, \ 
    {0, 0, 0, 1, 1, 0, 0, 0}  \ 

#define W { \ 
    {1, 0, 0, 0, 0, 0, 0, 1},\ 
    {1, 0, 0, 0, 0, 0, 1, 0},\ 
    {1, 0, 0, 0, 0, 0, 1, 0},\ 
    {0, 1, 0, 1, 0, 0, 1, 0},\ 
    {0, 1, 0, 1, 0, 1, 0, 0},\ 
    {0, 1, 0, 1, 0, 1, 0, 0},\ 
    {0, 0, 1, 1, 0, 1, 0, 0},\ 
    {0, 0, 0, 1, 1, 0, 0, 0},\ 

#define R { \ 
    {1, 1, 1, 1, 1, 0, 0, 0},\ 
    {1, 0, 0, 0, 1, 0, 0, 0},\ 
    {1, 0, 0, 0, 1, 0, 0, 0},\ 
    {1, 1, 1, 1, 1, 0, 0, 0},\ 
    {1, 0, 1, 0, 0, 0, 0, 0},\ 
    {1, 0, 0, 1, 0, 0, 0, 0},\ 
    {1, 0, 0, 0, 1, 0, 0, 0},\ 
    {1, 0, 0, 0, 0, 1, 0, 0},\ 

#define D { \ 
    {1, 1, 1, 1, 1, 0, 0, 0},\ 
    {1, 1, 0, 0, 1, 1, 0, 0},\ 
    {1, 1, 0, 0, 0, 1, 1, 0},\ 
    {1, 1, 0, 0, 0, 1, 1, 0},\ 
    {1, 1, 0, 0, 0, 1, 1, 0},\ 
    {1, 1, 0, 0, 0, 1, 1, 0},\ 
    {1, 1, 0, 0, 1, 1, 0, 0},\ 
    {1, 1, 1, 1, 1, 0, 0, 0},\ 
}
이 글에서 사용할 변환된 폰트 데이터

00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000000
H
01000010
01000010
01000010
01111110
01000010
01000010
01000010
01000010
E
01111110
01000000
01000000
01111110
01000000
01000000
01000000
01111110
L
01000000
01000000
01000000
01000000
01000000
01000000
01000000
01111110
O
00011000
00100100
01000010
01000010
01000010
01000010
00100100
00011000
W
01000001
01000001
01000010
00101010
00101010
00101010
00101010
00011100
R
01111100
01000100
01000100
01111100
01010000
01001000
01000100
01000010
D
01111000
01001100
01000110
01000110
01000110
01000110
01001100
01111000
















변환된 폰트의 구성은 표현하는 문자가 있어 문자로 검색할 수 있도록 하였고 해당 문자의 바로 다음줄에 8bits x 8bytes 형식의 이진수 형식의 스트링이 여백없는 줄바꿈으로 이루어져 있다. 

SPIFFS 라이브러리 함수를 이용하여 변환된 폰트에서 출력하고자 하는 문자를 검색하고 그 문자의 표현에 해당하는 이진수 형식의 스트링을 64개 읽어서 시리얼 모니터에 출력해주면 되는 것이다. 

 

아래는 시리얼 모니터에 문자를 출력하기 위한 SPIFFS 라이브러리 관련 핵심 코드 설명이다. 

String message = "HELLO WORLD"; // 출력할 문자열 

f = SPIFFS.open("/font.txt", "r"); // 폰트파일을 읽기로 열기 
f.seek(0, SeekSet);                // 커서의 위치를 0으로 설정 
f.find(message[1]);                // 문자'E' 검색 - 검색되면 커서는 문자'E' 다음에 위치한다. 
f.seek(2, SeekCur);                // 현재 위치에서 2바이트 이동(줄바꿈 또는 새줄), +2: 1310(캐리지리턴, 라인피드) 
* 새 줄 또는 줄바꿈으로 구성된 폰트 파일에서 새 줄 또는 줄바꿈은 폰트 텍스트 파일에서 보이지는 않지만 char 형식의 문자가 2바이트(DEC 13, 10) 차지하게 된다.  
  
if (f.read() == '0') Serial.print("  "); // 2바이트 오프셋한 위치에서 부터 1바이트씩 읽고 문자가 '0'이면 "  " 출력
else  Serial.print("@@");                // 1이면 "@@" 출력

 

ESP8266_SPIFFS_bin_font_test.zip
0.00MB
ESP32_SPIFFS_bin_font_test.zip
0.00MB
#include "FS.h" // ESP8266

File f; 

String message = "HELLO WORLD";

void setonoff() { 
  byte numPatterns = message.length();
  f = SPIFFS.open("/font.txt", "r");
  for (byte i = 0; i < numPatterns; i++) { 
    f.seek(0, SeekSet);
    f.find(message[i]);
    f.seek(2, SeekCur); // 현재 위치에서 2바이트 이동 +2: 1310(캐리지리턴, 라인피드 스트링값)
    for (int i = 0; i < 64; i++) {
        if (f.read() == '0') Serial.print("  ");
        else  Serial.print("@@");
        if ((i+1) % 8 == 0) {
          Serial.println();   // 8개 열 출력되면 줄바꿈
          f.seek(2, SeekCur); // +2: 1310(캐리지리턴, 라인피드 스트링값)
        }
        if (i == 63) Serial.println(); // 문자 완료되면 한줄 추가
    }
    delay(1000); // 단어간 구분
  }
  f.close();
}

void setup() {
  Serial.begin(115200);
  if (!SPIFFS.begin()) {
    Serial.println("Failed to mount file system");
    return;
  }
  FSInfo fsInfo;
  SPIFFS.info(fsInfo);
  Serial.print("totalBytes: "); Serial.println(fsInfo.totalBytes);
  Serial.print("usedBytes: "); Serial.println(fsInfo.usedBytes);
  listDir("/");
  Serial.println(); 
  setonoff();
}

void listDir(const char * dirname){
  Serial.printf("Listing directory: %s\r\n", dirname);
  Dir dir = SPIFFS.openDir(dirname);
  while (dir.next()) {
    Serial.print("File Name: "); Serial.print(dir.fileName());
    if(dir.fileSize()) {
      File f = dir.openFile("r");
      Serial.print(", Size: "); Serial.println(f.size());
    }
  }
}

void loop() {
  if(Serial.available() > 0){
    String temp = Serial.readStringUntil('\n');
    if (temp == "1") {
      Serial.println("File System Format....");
      if(SPIFFS.format())  Serial.println("File System Formated");  //Format File System
      else   Serial.println("File System Formatting Error");
    }
    else if (temp == "2") {
      listDir("/");
    }
  }
}

만약 폰트파일을 업로드 시키지 않았다면 우선 시리얼 모니터를 닫고 data 폴더 안에 있는 폰트 파일을 모듈에 업로드를 시켜야 한다. 

상기 스케치를 업로드 하면 시리얼 모니터에 문자열 이미지가 출력된다.

이렇듯 사용자가 폰트를 직접 만들어서 사용할 수 있지만 전체 폰트를 만들려면 많은 시간이 소요되게 된다. 인터넷 상에서 구할 수 있는 폰트를 이용하여 문자를 출력해보도록 하자. 하지만 이러한 폰트들을 이용하기 위해서는 앞서 만들었던 폰트의 규칙 "검색 문자를 제시하고 해당 문자 표현에 맞는 이진수 스트링을 줄바꿈을 이용해서 구성한다."라는 것은 폰트를 만든 사람은 알고 있지만 별다른 설명이 없는 한 규칙을 알기 위해서는 폰트를 만드는 것 만큼 시간이 소요될 수 있다. 일반적인 폰트의 구성을 이해하기 위해 상기의 폰트를 일반적인 폰트의 구성 형식으로 변환시켜 보자. 

일반적인 폰트 파일은 검색을 위한 문자가 없고 줄바꿈을 사용하지도 않는다. 또한 이진수 스트링을 사용하지 않고 8비트에 해당하는 2진수 스트링을 1바이트 char 문자로 표현하고 있다.

 

 

아래는 문자 'H'의 폰트 파일 변환 표이다

2진수 스트링 ||   ASCII CODE 
01000010       :: DEC 66   :: B :: HEX 42
01000010       :: DEC 66   :: B :: HEX 42
01000010       :: DEC 66   :: B :: HEX 42
01111110       :: DEC 126 :: ~ :: HEX 7E
01000010       :: DEC 66   :: B :: HEX 42
01000010       :: DEC 66   :: B :: HEX 42
01000010       :: DEC 66   :: B :: HEX 42
01000010       :: DEC 66   :: B :: HEX 42

문자 'H'의 8bytes 폰트 데이터 : "66666612666666666" 또는 "BBB~BBBB" 또는 "4242427E42424242" 

 

이제 문자의 시작위치를 어떻게 결정하는지를 알아야 한다. 크게 영문 폰트와 한글 폰트로 구분할 수 있는데 영문 폰트는 ASCII 코드의 DEC 값과 폰트의 시작위치가 일치 되도록 구성되어 있으며 한글 폰트 같은 경우 배열을 이용하여 해당 문자의 시작위치를 찾아 갈수 있도록 하고 있다. 새로 생성할 폰트는 ASCII 코드의 구성과 일치하지 않기에 한글 폰트 구성과 같이 배열을 이용하여 문자의 시작위치를 정의하고 줄바꿈 없는 연속되는 1바이트 폰트 데이터를 이용하여 시리얼 모니터에 문자를 출력해 보자. 

 

새로 생성할 폰트의 문자 구성은 "SPACE,H,E,L,O,W,R,L,D" 이다. 이 문자열을 배열 또는 스틸링 변수로 초기화를 해준다. 

String font_order = " HELOWRLD"; 
char font_order[10] = " HELOWRLD";

 

이 배열 또는 스트링에서 SPACE의 인덱스 0이 폰트에서의 SPACE의 시작 위치가 되고, 이 때 폰트 상에는 첫번째 1바이트 문자부터 8번째 문자까지가 SPACE를 표한하는 폰트 데이터가 된다. 이렇게 8개마다 각 문자의 폰트 데이터가 순서대로 위치하게 되므로 출력하고자 검색한 문자의 "인덱스 x 8"이 폰트에서의 해당 문자 출력 위치가 되게 된다. 

 

아래 코드는 앞선 스케치의 시리얼 모니터 문자 출력 코드를 이용하여 폰트(font8.txt)를 생성하는 코드이다. 

 

 

 

void str_bin() { 
  byte order = font_order.length();
  char charVal[8];
  for (byte j = 0; j < order; j++) { 
    f = SPIFFS.open("/font.txt", "r");
    f.seek(0, SeekSet);
    f.find(font_order[j]);
    f.seek(2, SeekCur); // +2: 1310(캐리지리턴, 라인피드 스트링값)
    for (int k = 0; k < 8; k++) {
      uint8_t Dec = 0;
      for (int i = 0; i < 8; i++) {
        if (f.read() != '0') Dec = Dec | (0b00000001 << 7 - i);
      }
      charVal[k] = char(Dec);
      f.seek(2, SeekCur); // +2: 1310(캐리지리턴, 라인피드 스트링값)
    }
    f.close();
    if (j == 0) { 
      f = SPIFFS.open("/font8.txt", "w"); // 첫번째 폰트 문자 8바이트 쓰기 
      for (int i = 0; i < 8; i++) f.print(charVal[i]);
      f.close();
    }
    else {
      f = SPIFFS.open("/font8.txt", "a"); // 두번째 문자부터는 추가
      for (int i = 0; i < 8; i++) f.print(charVal[i]);
      f.close();
    }
  }
  f = SPIFFS.open("/font8.txt", "r");  // 저장된 폰트 데이터 확인
  for (int i = 0; i < f.size(); i++) Serial.print(f.read()); // 폰트 DEC값 시리얼 모니터 출력
  Serial.println();
  f.seek(0, SeekSet);
  for (int i = 0; i < f.size(); i++) Serial.print(char(f.read())); // 폰트 문자값 시리얼 모니터 출력
  Serial.println();
  f.seek(0, SeekSet);
  for (int i = 0; i < f.size(); i++) {
    Serial.print(f.read(), HEX); // 문자
    Serial.print(" ");
  }
  Serial.println();
  Serial.println("made a new font");
  f.close();
}

 

아래는 상기 코드에 의해 생성된 폰트 파일의 데이터 이다. 

DEC:   
00000000666666126666666661266464126646464126646464646464641262436666666663624656566424242422812468681248072686664646464646464126120767070707076120
char:
        BBB~BBBB~@@~@@@~@@@@@@@~$BBBB$AAB****|DD|PHDB@@@@@@@~xLFFFFLx
HEX:
0 0 0 0 0 0 0 0 42 42 42 7E 42 42 42 42 7E 40 40 7E 40 40 40 7E 40 40 40 40 40 40 40 7E 18 24 42 42 42 42 24 18 41 41 42 2A 2A 2A 2A 1C 7C 44 44 7C 50 48 44 42 40 40 40 40 40 40 40 7E 78 4C 46 46 46 46 4C 78 
font8.txt
0.00MB

폰트의 구성 규칙을 알고 있으니 그에 맞게 출력 코드를 적용시켜주면 된다. 

void font8_display() { 
  byte numPatterns = message.length();
  f = SPIFFS.open("/font8.txt", "r");
  for (byte i = 0; i < numPatterns; i++) { // 1초후 단어간 이동, 시간 초기화
    int cPosition = 0;  // 출력할 문자 위치 확인 f.find(message[i]); 대체 코드
    while (cPosition < font_order.length()) {
      if (font_order[cPosition] == message[i]) break;
      cPosition++;
    }
    int fPosition = cPosition * 8; // 폰트내 문자 시작 위치 지정 한개 폰트는 8byte
    f.seek(0, SeekSet);
    f.seek(fPosition, SeekSet);
    for (int i = 0; i < 8; i++) {
      byte bin = f.read();
      for (int j = 0; j < 8; j++) { // 비트마스크 연산  
        if (byte(bin >> (7 - j) & 0x01) == 0) Serial.print("  ");
        else  Serial.print("@@");
      }
      Serial.println();   // 8개 비트 출력되면 줄바꿈
    }
    Serial.println(); // 한 문자 완료되면 한줄 추가
    delay(1000); // 단어간 구분 - 껏다 킴
  }
  f.close();
}
ESP8266_SPIFFS_8byte_font_test.zip
0.00MB
ESP32_SPIFFS_8byte_font_test.zip
0.00MB
#include "FS.h" // ESP8266

File f; 

String font_order = " HELOWRLD";
String message = "HELLO WORLD";

void str_bin() { 
  byte order = font_order.length();
  char charVal[8];
  for (byte j = 0; j < order; j++) { 
    f = SPIFFS.open("/font.txt", "r");
    f.seek(0, SeekSet);
    f.find(font_order[j]);
    f.seek(2, SeekCur); // +2: 1310(캐리지리턴, 라인피드 스트링값)
    for (int k = 0; k < 8; k++) {
      uint8_t Dec = 0;
      for (int i = 0; i < 8; i++) {
        if (f.read() != '0') Dec = Dec | (0b00000001 << 7 - i);
      }
      charVal[k] = char(Dec);
      f.seek(2, SeekCur); // +2: 1310(캐리지리턴, 라인피드 스트링값)
    }
    f.close();
    if (j == 0) { 
      f = SPIFFS.open("/font8.txt", "w"); // 첫번째 폰트 문자 8바이트 쓰기 
      for (int i = 0; i < 8; i++) f.print(charVal[i]);
      f.close();
    }
    else {
      f = SPIFFS.open("/font8.txt", "a"); // 두번째 문자부터는 추가
      for (int i = 0; i < 8; i++) f.print(charVal[i]);
      f.close();
    }
  }
  f = SPIFFS.open("/font8.txt", "r");  // 저장된 폰트 데이터 확인
  for (int i = 0; i < f.size(); i++) Serial.print(f.read()); // DEC값
  Serial.println();
  f.seek(0, SeekSet);
  for (int i = 0; i < f.size(); i++) Serial.print(char(f.read())); // 문자
  Serial.println();
  Serial.println("made a new font");
  f.close();
}

void font8_display() { 
  byte numPatterns = message.length();
  f = SPIFFS.open("/font8.txt", "r");
  for (byte i = 0; i < numPatterns; i++) { // 1초후 단어간 이동, 시간 초기화
    int cPosition = 0;  // 출력할 문자 위치 확인 f.find(message[i]); 대체 코드
    while (cPosition < font_order.length()) {
      if (font_order[cPosition] == message[i]) break;
      cPosition++;
    }
    int fPosition = cPosition * 8; // 폰트내 문자 시작 위치 지정 한개 폰트는 8byte
    f.seek(0, SeekSet);
    f.seek(fPosition, SeekSet);
    for (int i = 0; i < 8; i++) {
      byte bin = f.read();
      for (int j = 0; j < 8; j++) { // 비트마스크 연산  
        if (byte(bin >> (7 - j) & 0x01) == 0) Serial.print("  ");
        else  Serial.print("@@");
      }
      Serial.println();   // 8개 비트 출력되면 줄바꿈
    }
    Serial.println(); // 한 문자 완료되면 한줄 추가
    delay(1000); // 단어간 구분 - 껏다 킴
  }
  f.close();
}


void setup() {
  Serial.begin(115200);
  if (!SPIFFS.begin()) {
    Serial.println("Failed to mount file system");
    return;
  }
  FSInfo fsInfo;
  SPIFFS.info(fsInfo);
  Serial.print("totalBytes: "); Serial.println(fsInfo.totalBytes);
  Serial.print("usedBytes: "); Serial.println(fsInfo.usedBytes);
  listDir("/");
  str_bin();
  font8_display();
}

void listDir(const char * dirname){
  Serial.printf("Listing directory: %s\r\n", dirname);
  Dir dir = SPIFFS.openDir(dirname);
  while (dir.next()) {
    Serial.print("File Name: "); Serial.print(dir.fileName());
    if(dir.fileSize()) {
      File f = dir.openFile("r");
      Serial.print(", Size: "); Serial.println(f.size());
    }
  }
}

void loop() {
  if(Serial.available() > 0){
    String temp = Serial.readStringUntil('\n');
    if (temp == "1") {
      Serial.println("File System Format....");
      if(SPIFFS.format())  Serial.println("File System Formated");  //Format File System
      else   Serial.println("File System Formatting Error");
    }
    else if (temp == "2") {
      listDir("/");
    }
  }
}

 

이제 폰트 데이터가 8bits x 16byte 로 구성된 영문 폰트를 이용하여 문자를 출력해 보자.

사용하는 영문 폰트는 인터넷상에서 구할 수 있는 폰트이다.  

 

 

영문 폰트의 규칙을 살펴보면 아래와 같다. 

- 1바이트(8bit) 16개로 한개의 문자를 표현한다. (데이터를 확인할 때 DEC값 보다는 HEX값으로 확인을 한다.)

- 영문의 경우 ASCII 코드 순서대로 폰트 데이터가 위치되어 있다. 

ASCII 코드의 DEC 0은 null('\0')이다. 0번은 null 이므로 헥사값 0x00인 16개 바이트를 사용하여 폰트로 표현하고 있다. 이는 SPACE(ASCII 코드 DEC 32)와 같다.  

ASCII 코드의 DEC 1은 start of heading이라는 제어코드이고 DEC 0번의 16개의 헥사값에 이어서 16개의 바이트로 표현된다(17번 ~ 32번 헥사값). 0부터 시작하는 인덱스로 표현하자면 16번째 인덱스가 시작번호가 되며 제어문자의 경우 알수 없는 문자가 표현되고 있다. 이렇게 아스키 코드표의 DEC 번호에 맞춰 구성된 것이 8bits x 16byte 영문 폰트이고 ASCII 코드 번호 x 16이 표현하고자 하는 글자의 폰트 시작위치가 되게 된다. 

예를 들어 '!'의 아스키코드 번호는 33이다. 33 x 16 = 528이 폰트의 시작위치이고  여기서 16개의 1바이트 헥사값을 가져오면 아래와 같다. 

시작번호 528에서부터 16개 바이트 헥사값: 00 00 00 1B 3C 3C 3C 3C 1B 1B 00 1B 1B 1B 00 00 

헥사값을 바이너리로 변환하면 아래와 같이 느낌표가 표현된다. 

00000000 
00000000 
00000000 
00011000 
00111100 
00111100 
00111100 
00111100 
00011000 
00011000 
00000000 
00011000 
00011000 
00011000 
00000000 
00000000

 

data 폴더내에 첨부된 영문폰트를 모듈에 업로드 시킨 뒤에 스케치를 업로드한다.

ESP8266_SPIFFS_16byte_font_test.zip
0.00MB
ESP32_SPIFFS_16byte_font_test.zip
0.00MB
#include "FS.h" // ESP8266

File f; 

/*-8bits x 16 byte font-*/
//1 "@@" 또는 "  "는 1bit              
//2                 
//3                 
//4   @@@@@@@@@@    
//5   @@@@@@@@@@@@  
//6   @@@@    @@@@@@
//7   @@@@      @@@@
//8   @@@@      @@@@
//9   @@@@      @@@@
//10  @@@@      @@@@
//11  @@@@    @@@@@@
//12  @@@@@@@@@@@@  
//13  @@@@@@@@@@    
//14                
//15                
//16                

String message = "HELLO WORLD";

void font16_display() { 
  byte numPatterns = message.length();
  f = SPIFFS.open("/Efont16.eng", "r"); // 16 byte 폰트 - 아스키코드 순서와 같음
  for (byte i = 0; i < numPatterns; i++) { // 1초후 단어간 이동, 시간 초기화
    int cPosition = message[i];     // 아스키값이 출력 문자 위치
    int fPosition = cPosition * 16; // 폰트내 문자 시작 위치 지정 한개 폰트는 16byte
    f.seek(0, SeekSet);
    f.seek(fPosition, SeekSet);
    for (int i = 0; i < 16; i++) {
      byte bin = f.read();  // 1byte 씩 읽는다.
      for (int j = 0; j < 8; j++) { // 비트마스크 연산  
        if (byte(bin >> (7 - j) & 0x01) == 0) Serial.print("  ");
        else  Serial.print("@@");
      }
      Serial.println();   // 8개 비트 출력되면 줄바꿈
    }
    Serial.println(); // 여백이 있어서 한줄 추가 필요없음
    delay(1000); // 단어간 구분 - 껏다 킴
  }
  f.close();
}

void setup() {
  Serial.begin(115200);
  if (!SPIFFS.begin()) {
    Serial.println("Failed to mount file system");
    return;
  }
  FSInfo fsInfo;
  SPIFFS.info(fsInfo);
  Serial.print("totalBytes: "); Serial.println(fsInfo.totalBytes);
  Serial.print("usedBytes: "); Serial.println(fsInfo.usedBytes);
  listDir("/");
  font16_display();
}

void listDir(const char * dirname){
  Serial.printf("Listing directory: %s\r\n", dirname);
  Dir dir = SPIFFS.openDir(dirname);
  while (dir.next()) {
    Serial.print("File Name: "); Serial.print(dir.fileName());
    if(dir.fileSize()) {
      File f = dir.openFile("r");
      Serial.print(", Size: "); Serial.println(f.size());
    }
  }
}

void loop() {
  if(Serial.available() > 0){
    String temp = Serial.readStringUntil('\n');
    if (temp == "1") {
      Serial.println("File System Format....");
      if(SPIFFS.format())  Serial.println("File System Formated");  //Format File System
      else   Serial.println("File System Formatting Error");
    }
    else if (temp == "2") {
      listDir("/");
    }
  }
}

출력되는 스트링을 변경하고자 한다면 스트링 변수 String message = "HELLO WORLD"; 에서 초기화 스트링을 다른 영문 스트링으로 변경해 주면 된다. 

 

 

+ Recent posts