User-Configurable Settings in a Teensy World

Meshtron

Member
Hey guys, I'm working on an old project idea again and planning to build my components around Teensy as the "brains" since it incorporates all the hardware I need plus the (very nice) ability to encrypt source code. I know this question is very broad, apologies for that but we're early in the process so very few options are totally off the table.

Broadly speaking, I have two types of components: fancy switches and fancy load drivers. Each of these, and the ways they interact (as in mapping a fancy switch to a specific driver output) are all software-configurable. I'm trying to think through my options on how to get an interface in front of a user that lets them configure these options and save that configuration data to EEPROM. There is also likely to be a requirement to be able to flash updated firmware to the devices, though I may be able to avoid that.

I think these are my options, but curious if anyone here has done something (one of these or something different) that worked well for them, or alternatively if you've done something and had horrible experience, I'd love to hear more about it.

Option 1: Bluetooth
Concept: Use a simple phone app to connect to device(s), view current configuration settings, make changes
Advantages: simple, portable, graphical UI, no special user hardware required
Disadvantages: have to write both Android and iOS apps, have to add hardware to Teensy, user Bluetooth problems can become (in their mind) my technical problems, not sure if I could do firmware flash this way

Option 2: SD Card
Concept: Use website to offer user UI for generating configuration settings, save output to SD card, insert SD card, detect new settings, flash to EEPROM. Could also have SD card inserted with no new configuration file used for Teensy to write current config to SD card as a starting point
Advantages: good software support for testing if present/files exist/etc., can maintain encrypted firmware updates
Disadvantages: need micro-SD writing hardware on a computer, physical access can be problematic to get card in/out, computer file system compatibility concerns

Option 3: USB Drive
Concept: Enable USB Host on Teensy and watch for USB drive inserted. Can use similar config file concept as SD card (read/write)
Advantages: easy physical access, ubiquitous hardware, flexibility of read/write options over time
Disadvantages: hardware modification(s) to Teensy, eliminates any SD card availability, not sure if I can flash firmware successfully from USB drive

Option 4: Physical Screen
Concept: Attach a small screen and (if not touch screen) buttons to work through config menus
Advantages: Can see and change config in real-time without moving storage between hardware/PC, easy to make configuration changes any time (even when not near a computer), easy to keep hardware config menu and capabilities in sync (all one firmware)
Disadvantages: Physical space constraints for screen, incremental cost in components and connectivity, adds large new hardware failure point
 
Option 5: Use MTP
Using the MTP software, you can make your Teensy provide MTP access to the files on your Teensy when you attach the Teensy to the computer via USB.

Using your system file explorer, you can copy files to/from the various file systems mounted on your Teensy. On the Teensy 4.1 you can use:
  • The built-in SD card;
  • The volatile PSram chip(s) soldered to the underside of the Teensy 4.1 (these get reset every power cycle or boot);
  • The NOR or NAND flash chip (permanent memory) soldered to the underside of the Teensy 4.1; (and)
  • Using the upper flash memory of the Teensy not used for the program.

NOR flash is typically for the smaller flash chips (such as the 16 megabyte/128 megabit chips), while NAND flash is for the larger flash chips up to 256 megabyte/2 gigabyte chips). Be sure to check your flash chip for compatibility, as not all flash chips are supported.

The Teensy 4.0 doesn't have the options for soldering PSram or flash chips to the Teensy, but it does have the upper flash memory (though it is smaller than the Teensy 4.1).

I believe the repository for MTP_t4 is at:

Here is a sample test program that I have to test the various MTP storage types under Fedora 36 Linux:

Code:
// From /shared/arduino/git/MTP_t4.git/trunk/examples/mtp-basic/mtp-basic.ino

#include "SPI.h"
#include "SD.h"
#include "MTP.h"

#if defined(ARDUINO_TEENSY41)
// The Teensy 4.1 has two sets of solder pads underneath the Teensy that you
// can solder either 2 PSram chips, 1 flash memory chip, or 1 flash memory chip
// and a PSram chip.  The smaller flash chips tend to use NOR flash while the
// larger flash chips tend to use NAND flash.
#define USE_LFS_RAM			1	// T4.1 PSRAM (or RAM)
#define USE_LFS_QSPI_NOR		1	// T4.1 QSPI NOR flash (16MB)
#define USE_LFS_QSPI_NAND		1	// T4.1 QSPI NAND flash (128MB)

#else
#define USE_LFS_RAM			0	// T4.1 PSRAM (or RAM)
#define USE_LFS_QSPI_NOR		0	// T4.1 QSPI NOR flash (16MB)
#define USE_LFS_QSPI_NAND		0	// T4.1 QSPI NAND flash (128MB)
#endif

#if defined(ARDUINO_TEENSY40) || defined(ARDUINO_TEENSY41)
// Teensy 4.0 and 4.1 can use the upper flash memory as a file system via
// LittleFS.  The top bytes (64K for Teensy 4.0 and 256K for Teensy 4.1) of
// this memory is reserved for EEPROM emulation and the LED blink restore
// program.  On the Teensy 4.0 you have 1 megabyte - 64K of flash for a
// filesystem.  On the Teensy 4.1, you have 7 megabytes - 256K of flash for the
// filesystem.
#define USE_LFS_PROGM			1	// T4.0/T4.1 Progam Flash
#else
#define USE_LFS_PROGM			0	// T4.0/T4.1 Progam Flash
#endif

#define USE_SD				1	// Use either the built-in micro SD card reader or the reader on pin 10 (audio adapter)
#define USE_LFS_SPI			1	// SPI Flash (soldered to the audio adapter or the built-in flash on the prop shield)

// If you use the audio adapter revision A through C on a Teensy LC, 3.2, 3.5,
// or 3.6, it needs to remap the MOSI/SCK pins because those pins (11 and 13)
// are also I2S pins used by the audio shield.  But the prop shield wants to
// use the normal definitions.  In general, I've used the prop shield mostly on
// the Teensy 3.2 and LC, while if I use it, I use the audio shield on the
// Teensy 3.5 or Teensy 3.6.
//
// The Teensy 4.0 and 4.1 uses the audio adapter revision D, and it uses the
// standard SPI pins.
#if defined(ARDUINO_TEENSY35) || defined(ARDUINO_TEENSY36)
#define USE_TEENSY3_AUDIO_SHIELD	1	// Whether to use the Audio shield MOSI/SCLK for Teensy 3.x processors
#else
#define USE_TEENSY3_AUDIO_SHIELD	0	// Use standard configuration
#endif

#if USE_EVENTS==1
  extern "C" int usb_init_events(void);
#else
  int usb_init_events(void) {}
#endif

#if USE_LFS_RAM==1 ||  USE_LFS_PROGM==1 || USE_LFS_QSPI_NOR==1 || USE_LFS_QSPI_NAND==1 || USE_LFS_SPI==1
  #include "LittleFS.h"
#endif

#if defined(__IMXRT1062__)
  // following only as long usb_mtp is not included in cores
  #if !__has_include("usb_mtp.h")
    #include "usb1_mtp.h"
  #endif
#else
  #ifndef BUILTIN_SDCARD 
    #define BUILTIN_SDCARD 254
  #endif
  void usb_mtp_configure(void) {}
#endif


/****  Start device specific change area  ****/
// SDClasses 
#if USE_SD==1
  // edit SPI to reflect your configuration
#if USE_TEENSY3_AUDIO_SHIELD
 // Teensy audio adapter for Teensy 3.2, 3.5, and 3.6 (revision A-C) needs to remap the MOSI and SCK pins
  #define SD_MOSI  7
  #define SD_MISO 12
  #define SD_SCK  14
#else
 // Standard Teensy configuration
  #define SD_MOSI 11
  #define SD_MISO 12
  #define SD_SCK  13
#endif

  #define SPI_SPEED SD_SCK_MHZ(33)				// adjust to sd card 

  #if defined (BUILTIN_SDCARD)
    const char *sd_str[]={"BUILTIN-SD", "AUDIO-SD"};		// edit to reflect your configuration
    const int cs[] = {BUILTIN_SDCARD, 10};			// edit to reflect your configuration
  #else
    const char *sd_str[]={"AUDIO-SD"};				// edit to reflect your configuration
    const int cs[] = {10};					// edit to reflect your configuration
  #endif
  const int nsd = sizeof(sd_str)/sizeof(const char *);

SDClass sdx[nsd];
#endif

//LittleFS classes
#if USE_LFS_RAM==1
  const char *lfs_ram_str[]	= {"RAM1","RAM2"};		// edit to reflect your configuration
  const int lfs_ram_size[]	= {2'000'000, 4'000'000};	// edit to reflect your configuration
  const int nfs_ram		= sizeof(lfs_ram_str)/sizeof(const char *);

  LittleFS_RAM ramfs[nfs_ram]; 
#endif

#if USE_LFS_QSPI_NOR==1
  const char *lfs_qspi_nor_str[]	= {"QSPI-NOR"};		// edit to reflect your configuration
  const int nfs_qspi_nor		= sizeof(lfs_qspi_nor_str)/sizeof(const char *);

  LittleFS_QSPIFlash qspifs_nor[nfs_qspi_nor]; 
#endif

#if USE_LFS_QSPI_NAND==1
  const char *lfs_qspi_nand_str[]	= {"QSPI-NAND"};	// edit to reflect your configuration
  const int nfs_qspi_nand		= sizeof(lfs_qspi_nand_str)/sizeof(const char *);

  LittleFS_QPINAND qspifs_nand[nfs_qspi_nand];
#endif

#if USE_LFS_PROGM==1
  const char *lfs_progm_str[]={"PROGRAM"};			// edit to reflect your configuration
  const int lfs_progm_size[] = {1'000'000};			// edit to reflect your configuration
  const int nfs_progm = sizeof(lfs_progm_str)/sizeof(const char *);

  LittleFS_Program progmfs[nfs_progm]; 
#endif

#if USE_LFS_SPI==1
  const char *lfs_spi_str[]={"SPI-FLASH"};			// edit to reflect your configuration
  const int lfs_cs[] = {6};					// edit to reflect your configuration
  const int nfs_spi = sizeof(lfs_spi_str)/sizeof(const char *);

LittleFS_SPIFlash spifs[nfs_spi];
#endif


MTPStorage_SD storage;
MTPD    mtpd(&storage);

void storage_configure()
{
  #if USE_SD==1
    #if defined SD_SCK
      SPI.setMOSI(SD_MOSI);
      SPI.setMISO(SD_MISO);
      SPI.setSCK(SD_SCK);
    #endif

    for(int ii=0; ii<nsd; ii++)
    { 
      #if defined(BUILTIN_SDCARD)
        if(cs[ii] == BUILTIN_SDCARD)
        {
          if(!sdx[ii].sdfs.begin(SdioConfig(FIFO_SDIO))) 
          { Serial.printf("SDIO Storage %d %d %s failed or missing",ii,cs[ii],sd_str[ii]);  Serial.println();
          }
          else
          {
            storage.addFilesystem(sdx[ii], sd_str[ii]);
            uint64_t totalSize = sdx[ii].totalSize();
            uint64_t usedSize  = sdx[ii].usedSize();
            Serial.printf("SDIO Storage %d %d %s ",ii,cs[ii],sd_str[ii]); 
            Serial.print(totalSize); Serial.print(" "); Serial.println(usedSize);
          }
        }
        else if(cs[ii]<BUILTIN_SDCARD)
      #endif
      {
        pinMode(cs[ii],OUTPUT); digitalWriteFast(cs[ii],HIGH);
        if(!sdx[ii].sdfs.begin(SdSpiConfig(cs[ii], SHARED_SPI, SPI_SPEED))) 
        { Serial.printf("SD Storage %d %d %s failed or missing",ii,cs[ii],sd_str[ii]);  Serial.println();
        }
        else
        {
          storage.addFilesystem(sdx[ii], sd_str[ii]);
          uint64_t totalSize = sdx[ii].totalSize();
          uint64_t usedSize  = sdx[ii].usedSize();
          Serial.printf("SD Storage %d %d %s ",ii,cs[ii],sd_str[ii]); 
          Serial.print(totalSize); Serial.print(" "); Serial.println(usedSize);
        }
      }
    }
    #endif

    #if USE_LFS_RAM==1
    for(int ii=0; ii<nfs_ram;ii++)
    {
      if(!ramfs[ii].begin(lfs_ram_size[ii])) 
      { Serial.printf("Ram Storage %d %s failed or missing",ii,lfs_ram_str[ii]); Serial.println();
      }
      else
      {
        storage.addFilesystem(ramfs[ii], lfs_ram_str[ii]);
        uint64_t totalSize = ramfs[ii].totalSize();
        uint64_t usedSize  = ramfs[ii].usedSize();
        Serial.printf("RAM Storage %d %s ",ii,lfs_ram_str[ii]); Serial.print(totalSize); Serial.print(" "); Serial.println(usedSize);
      }
    }
    #endif

    #if USE_LFS_PROGM==1
    for(int ii=0; ii<nfs_progm;ii++)
    {
      if(!progmfs[ii].begin(lfs_progm_size[ii])) 
      { Serial.printf("Program Storage %d %s failed or missing",ii,lfs_progm_str[ii]); Serial.println();
      }
      else
      {
        storage.addFilesystem(progmfs[ii], lfs_progm_str[ii]);
        uint64_t totalSize = progmfs[ii].totalSize();
        uint64_t usedSize  = progmfs[ii].usedSize();
        Serial.printf("Program Storage %d %s ",ii,lfs_progm_str[ii]); Serial.print(totalSize); Serial.print(" "); Serial.println(usedSize);
      }
    }
    #endif

    #if USE_LFS_QSPI_NOR==1
    for(int ii=0; ii<nfs_qspi_nor;ii++)
    {
      if(!qspifs_nor[ii].begin()) 
      { Serial.printf("QSPI Storage %d %s failed or missing",ii,lfs_qspi_nor_str[ii]); Serial.println();
      }
      else
      {
        storage.addFilesystem(qspifs_nor[ii], lfs_qspi_nor_str[ii]);
        uint64_t totalSize = qspifs_nor[ii].totalSize();
        uint64_t usedSize  = qspifs_nor[ii].usedSize();
        Serial.printf("QSPI NOR Storage %d %s ",ii,lfs_qspi_nor_str[ii]); Serial.print(totalSize); Serial.print(" "); Serial.println(usedSize);
      }
    }
    #endif

    #if USE_LFS_QSPI_NAND==1
    for(int ii=0; ii<nfs_qspi_nand;ii++)
    {
      if(!qspifs_nand[ii].begin()) 
      { Serial.printf("QSPI Storage %d %s failed or missing",ii,lfs_qspi_nand_str[ii]); Serial.println();
      }
      else
      {
        storage.addFilesystem(qspifs_nand[ii], lfs_qspi_nand_str[ii]);
        uint64_t totalSize = qspifs_nand[ii].totalSize();
        uint64_t usedSize  = qspifs_nand[ii].usedSize();
        Serial.printf("QSPI NAND Storage %d %s ",ii,lfs_qspi_nand_str[ii]); Serial.print(totalSize); Serial.print(" "); Serial.println(usedSize);
      }
    }
    #endif

    #if USE_LFS_SPI==1
    for(int ii=0; ii<nfs_spi;ii++)
    {
      if(!spifs[ii].begin(lfs_cs[ii])) 
      { Serial.printf("SPIFlash Storage %d %d %s failed or missing",ii,lfs_cs[ii],lfs_spi_str[ii]); Serial.println();
      }
      else
      {
        storage.addFilesystem(spifs[ii], lfs_spi_str[ii]);
        uint64_t totalSize = spifs[ii].totalSize();
        uint64_t usedSize  = spifs[ii].usedSize();
        Serial.printf("SPIFlash Storage %d %d %s ",ii,lfs_cs[ii],lfs_spi_str[ii]); Serial.print(totalSize); Serial.print(" "); Serial.println(usedSize);
      }
    }
    #endif
}

void setup()
{ 
  while (!Serial && millis () < 3000)
    ;

  Serial.println("MTP_test");
  Serial.printf ("USE_LFS_RAM              = %d\n", USE_LFS_RAM);
  Serial.printf ("USE_LFS_QSPI_NOR         = %d\n", USE_LFS_QSPI_NOR);
  Serial.printf ("USE_LFS_QSPI_NAND        = %d\n", USE_LFS_QSPI_NAND);
  Serial.printf ("USE_LFS_PROGM            = %d\n", USE_LFS_PROGM);
  Serial.printf ("USE_SD                   = %d\n", USE_SD);
  Serial.printf ("USE_LFS_SPI              = %d\n", USE_LFS_SPI);
  Serial.printf ("USE_TEENSY3_AUDIO_SHIELD = %d\n", USE_TEENSY3_AUDIO_SHIELD);
  Serial.println ("");

  #if USE_EVENTS==1
    usb_init_events();
  #endif

  #if !__has_include("usb_mtp.h")
    usb_mtp_configure();
  #endif
  storage_configure();

}

void loop()
{ 
  mtpd.loop();

#if USE_EVENTS==1
  if(Serial.available())
  {
    char ch=Serial.read();
    Serial.println(ch);
    if(ch=='r') 
    {
      Serial.println("Reset");
      mtpd.send_DeviceResetEvent();
    }
  }
#endif
}

If you aren't up to soldering a flash chip to the Teensy 4.1, the company protosupplies will custom solder chips to the Teensy 4.1 for you. While I've done SMT soldering in the past, I generally prefer to have an expert do it these days:
 
Thank you for that option Michael! I am not familiar with MTP, but I will do some more research on that as well.
 
There’s also the option of putting a web server on a Teensy 4.1 and serving up a page with a form. The submitted settings can then be stored locally in flash/SD card/RAM/EEPROM/etc.
 
I have some projects that have around 50 settings. I use a display + touch and a fancy menu system to allow the user to make changes.

All settings are stored in the EEPROM

The process works well and no issues.
 
Back
Top