A simple operating system for the ESP32-based M5StickC
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

8.0 KiB

watchos

A simple framework for developing on the M5StickC.

Features

  • Kernel which handles task scheduling, running, signalling, and more.
  • Eeprom abstraction layer which creates an allocation table to allow multiple modules to share storage without hardcoding offsets or addresses everywhere.
  • User Input module which handles tracking button presses, debouncing, and converting the inputs into inter-process signals to provide a unified way for handling events.
  • Interface library which provides an easy way to handle creating tiling layouts and rotation.
  • Power management library to handle screen dimming and putting the processor into deep sleep to conserve battery while giving tasks an opportunity to save their state.
  • The base system uses less than 10% of the ESP32's storage and 5% of its RAM.

Kernel

The Kernel is the core of watchos. It provides a kind of cooperative multitasking.

Tasks can be registered and set to run on a schedule or based on interrupts. Tasks are given the opportunity for clean startup and shutdown, to handle user input, to handle updating the display, and more. Tasks can be paused and resumed and exited tasks can return an exit code consumable by other processes.

The Kernel tries to intelligently conserve power by putting the processor into light sleep in between scheduled executions, while making use of hardware features to wake up sooner if events of interest occur.

EAT

The "EEPROM Allocation Table" a la FAT/File Allocation Table. A few bytes are reserved at the start of flash storage to track how many bytes have been allocated and to which module. A read/write abstraction is provided to allow modules to read/write from their allocated space. This serves to prevent conflicts between multiple independent pieces of code attempting to use the storage device.

User Input

A simple module that tracks if either button has been pressed, handles debouncing (not detecting another press until they have first been released), and sends interprocess signals to other processes when the events occur. This simplifies input handling and prevents the need for duplication of things like debounce logic.

Interface

A take on a tiling window manager supplies a simple way to lay out multiple modules on the display in a flexible way as well as provide for adjusting the layout for various display rotations. Each module can register as many screen sections as it wants as well as as many nested tiles as it wants in order to create a layout.

Power Management

A simple power management library which handles tracking idle time and taking appropriate actions when the device is no longer in use. After a short delay, the display's backlight is dimmed. After a longer delay it will signal all running tasks to exit (giving them a chance to save state to eeprom or quickly finish any outstanding tasks) then the processor will be placed into deep sleep and the display and backlight powered off. This, combined with the Kernel's intelligent use of light sleep helps to extend the M5StickC's battery life well beyond what is typically expected.

Resource Efficient

The base system (Kernel, EAT, User Input, User Interface, Power Management) uses only around 10% of available program flash and 5% of dynamic memory leaving plenty of room to build on top of.

Getting Started

Your App

Include all of the watchos files in your project, then simply initialize the kernel and register your tasks in your setup() method, then call the kernel during each invocation of your loop() method. For example:

#include <M5StickC.h>
#include "Kernel.h"

int MyTask(int pid, unsigned int signal)
{
  return 0;
}

void setup()
{
  M5.begin();
  Kernel_setup();
  Kernel_start(&MyTask, 5000);
}

void loop()
{
  Kernel_tick();
}

If your tasks make use of the EAT module, it will need to be initialized before first use and a call to load the allocation table will need to be made on startup. You can either add the initialization call to run once then remove from your code later, or provide the ability to trigger initialization by, for example, holding a button during power up. For example:

void setup()
{
  M5.begin();
  Kernel_setup();
  Kernel_start(&MyTask, 5000);

  M5.update();
  if (M5.BtnB.isPressed())
  {
    EAT_initialize();
  }
  else
  {
    EAT_load();
  }
}

If the side button is held during power up, the EEPROM will be reinitialized with an empty allocation table, otherwise the existing table will be loaded (causing a panic if no table exits or is valid).

Writing Your Task

Each task method requires a simple signature:

int MyTask(int pid, unsigned int signal)
{
  [...]
}

This method takes its own process id and any pending signals as arguments, and is expected to return an exit code as its return value.

The id is generated each time the task is started. If you expect to start a task multiple times, this allows you to manage keeping independent data stores for each invocation.

The signals are a way for the kernel and other tasks to notify you of events. This will tell you whether your task was just started, is being requested to shutdown, has reached its invocation interval, needs to redraw the display, etc.

The returned exit code should always be 0 if your program expects to continue. Any other value will signal the kernel to stop executing your task, and that value will be available to other running tasks.

Signals

Signals are defined in Kernel.h and include:

  • SIGNAL_TICK - your task's configured run interval has elapsed
  • SIGNAL_START - this is the first invocation of your task method and it should perform first-startup initialization; this will only be sent once
  • SIGNAL_STOP - your task is requested to shutdown; you should return a non-zero exit code in response to this, though if your task needs to continue running for a brief time to finish shutting down cleanly it may return 0 and quit on a later invocation
  • SIGNAL_INPUT_A - the main (home) button has been pressed; only delivered once per press
  • SIGNAL_INPUT_B - the secondary (side) button has been pressed; only delivered once per press
  • SIGNAL_REDRAW - the display needs to be redrawn and you should redraw anything you have on the screen -- you should only ever perform draws in response to this signal

To see the full benefit of this system, you should read and understand the further documentation on the kernel and how exactly you should use and respond to these signals.

Signal Mask

Your application may not be interested in all of these signals. To avoid your method being called unnecessarily, you can set a signal mask -- this tells the kernel what signals you're interested in. The mask should include all signals you do want to receive. For example:

Kernel_signal_mask(mypid, SIGNAL_START | SIGNAL_STOP | SIGNAL_TICK);

Example

A simple framework for a method:

#include "Kernel.h"

int MyTask(int pid, unsigned long signal)
{
  if (signal & SIGNAL_START)
  {
    // Perform startup here
  }

  if (signal & SIGNAL_STOP)
  {
    // Perform shutdown here
    return 255;
  }

  if (signal & SIGNAL_TICK)
  {
    // Your requested run interval has transpired; perform any regular
    // logic here to update state.

    // If you need to update the screen, do *not* do it here, instead trigger
    // a redraw so the entire screen can be cleared and redrawn:
    Kernel_signal(SIGNAL_REDRAW);
    // Your application will also receive this REDRAW signal and can perform its
    // draws in response to that.
  }

  if (signal & SIGNAL_REDRAW)
  {
    // Update the screen from your state variables here.
  }

  return 0;
}

TODO:

  • Better kernel module system to support EAT as well as wifi/bluetooth/etc.
  • More efficient draws: if we're using the tiling wm, no reason we need to clear the entire screen; if we know which parts need a redraw we could instead drawRect and only redraw those.
  • Adjust voltage range on battery meter -- goes to 75% pretty much immediately.
  • Figure out why Power Management isn't accurately reflecting configured timeouts.