1

I am building a small art project to display my current age with an accuracy of 1/100th of a second. I am using a XIAO SAMD21, a DS3231 RTC, and a 128x32 OLED screen. Here is what it looks like right now without the battery:

enter image description here

My aim is to fit this and a battery into a locket / necklace. I want to make as low-power as possible so that it can last at least an hour or two on a 75 mAh Li-po. I can't use a bigger battery since it wouldn't fit in the necklace.

I wrote the code to the best of my ability and it looks like it works really well. I just want a sanity check to make sure there isn't a simpler way of doing things. In particular I feel like the calculation of the age in the main loop, the syncing with RTC, and maybe the display updates, could be done in a better way. Maybe there are some things I do with interrupts and the 'alarms' in the RTC? Just power on the display every other second maybe. I am also considering using an epaper display and removing the power LED.

The code is attached here. I use the Adafruit libraries to drive the SSD1306 on the OLED screen, and one of the DS3231 libraries to interface with the RTC. Parts of it are adapted from examples, so there may be some extraneous imports or declarations I don't need.

// TUNIO 2024
// Age Pendant

// TO DO:
// current loop is 17 to 18 milliseconds long - need less than 5 for accurate centi-second counting

// Libraries
#include <SPI.h>
#include <Wire.h>
#include <DS3231.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// OLED Setup ~ copied from example
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels
#define OLED_RESET     -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C // See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// RTC Setup
RTClib myRTC;

// Initializations for periodic sync with RTC
uint32_t remote_time = 0;
uint32_t sync_interval_ms = 1800000; // sync with RTC every sync_interval_ms milliseconds // about an hour is good
uint32_t last_millis_synced = 0; // the last time we synced with RTC
uint32_t system_time = 0; // the local system time

// Initializations for age calcs
// QUESTION us 32 bit enough to store the age of someone? What if they live to 100?
uint32_t born_utime = 788888888; // date of birth in unixtime // 788888888
uint32_t total_age_secs = 0;
uint32_t sec_remainder = 0;
uint8_t age_years = 0;
uint8_t age_months = 0;
uint8_t age_days = 0;
uint8_t age_hours = 0;
uint8_t age_mins = 0;
uint8_t age_secs = 0;

// initializations for centi-second counting
uint8_t oldsec = -1;
uint8_t newsec = -1;
uint32_t offset = 0;
uint8_t age_cs = 0;

// screen saving vars
bool invert = false;

//// debug
//uint32_t start_millis=0;
//uint32_t end_millis=0;

void setup() {
  Serial.begin(57600);
  Wire.begin();
  delay(250);

  // display setup
  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;);
  }
  display.dim(true);
  display.setTextColor(SSD1306_WHITE);
  display.clearDisplay();
  display.setTextSize(2); // 1 default
  display.setRotation(3); //0 default, 1, 2, 3

  // turn off built in LED
  pinMode(LED_BUILTIN , OUTPUT);
  digitalWrite(LED_BUILTIN , HIGH);

  // do first sync
  sync_time_with_rtc();
}


void loop() {
//  //debug
//  start_millis = millis();

  // sync every sync_interval_ms with RTC
  if (millis() - last_millis_synced >= sync_interval_ms) {
    sync_time_with_rtc();
  }

  // Calculate time elapsed since birth in years, months, etc.
  system_time = local_time_now();
  total_age_secs = system_time - born_utime; // age in seconds
  age_years = num_time_units(total_age_secs, 31558149.756); // second arg is num of: secs in a year
  age_months = num_time_units(sec_remainder, 2629745.568); // ... secs in a month
  age_days = num_time_units(sec_remainder, 86401.505); // ... secs in day
  age_hours = num_time_units(sec_remainder, 3600); // ... in an hour
  age_mins = num_time_units(sec_remainder, 60); // ... in an minute
  age_secs = sec_remainder;

  // display the age calculated
  show_age_on_display(5, 3); // input top left coordinates of where the age will be displayed

//  //debug
//  end_millis = millis();
//  Serial.println(end_millis - start_millis,DEC);
}


// Prints the provided age to the LCD with the correct formatting
void show_age_on_display(int col, int line) {
  newsec = age_secs;
  if (newsec != oldsec) {
    offset = -1 * (millis() % 1000);
    if (age_secs % 11 == 0) {invert = ! invert;} // invert display to save pixels burning every 11 secs
    oldsec = age_secs;
  }

  display.clearDisplay();
  display_at_location(age_years, col, line);
  display_at_location(age_months, col, line + (13 + 5) * 1);
  display_at_location(age_days, col, line + (13 + 5) * 2);
  display_at_location(age_hours, col, line + (13 + 5) * 3);
  display_at_location(age_mins, col, line + (13 + 5) * 4);
  display_at_location(age_secs, col, line + (13 + 5) * 5);
  age_cs = floor(((millis() + offset) % 1000) / 10);
  display_at_location(age_cs, col, line + (13 + 5) * 6);
  
  display.fillRect(0, display.height() - 18, display.width(), 18 - ((18 * age_cs) / 100), SSD1306_INVERSE); // progress bar completes every second

  display.invertDisplay(invert);
  display.display();
}

// display integer value <100 at given location with "0" padding
void display_at_location(uint8_t value, uint8_t this_col, uint8_t this_line){
  display.setCursor(this_col, this_line);
  if (value < 10) display.print(F("0"));
  display.print(value, DEC);
}


// input seconds and returns number of whole time unit, sets remainder variable
uint32_t num_time_units( uint32_t secs, float time_unit) {
  int num_units = floor(secs / time_unit);
  sec_remainder = secs - round(num_units * time_unit);
  return num_units;
}


// syncs time with the RTC: call on sync_interval, power ons, AND when wake from sleep
void sync_time_with_rtc() {
  digitalWrite(LED_BUILTIN , LOW); // briefly flash the LED every sync with the RTC    
  remote_time = myRTC.now().unixtime();
  last_millis_synced = millis();
  system_time = local_time_now();
  digitalWrite(LED_BUILTIN , HIGH);
}


// fetches the unixtime according to Arduino
uint32_t local_time_now() {
  return remote_time + floor((millis() - last_millis_synced) / 1000);
}
2
  • 1
    use a function to replace repeating code
    – jsotola
    Commented Feb 15 at 17:31
  • One thing I discovered: millis() will roll over every ~49 days if for some reason the Arduino doesn't have a power cycle. So I follow this and update the code arduino.stackexchange.com/questions/12587/…
    – Mir
    Commented Feb 16 at 8:42

1 Answer 1

0

I see quite a bit of floating point computations here. Since the SAMD21 doesn't have a floating point unit, all float operations are implemented in software, which can be quite expensive. As an optimization, I suggest to stay as much as possible in the integer realm. For example:

uint32_t total_minutes = total_age_secs / 60;
age_secs = total_seconds - total_minutes * 60;
uint32_t total_hours = total_minutes / 60;
age_mins = total_minutes - total_hours * 60;
uint16_t total_days = total_hours / 24;
age_hours = total_hours - total_days * 24;
age_years = total_days / 365;
uint16_t remaining_days = total_days - age_years * 365;
age_months = remaining_days * 12 / 365;
age_days = remaining_days - age_months * 365 / 12;

Note that there is some rounding here: a day is assumed to be exactly 86400 seconds instead of 86399.9999 (I don't know where you found the number 86401.505), a year is assumed to be 365 days, and all months are assumed to be equal. If you found this approximations too rough (mostly the year), you may use 365.25 = 1461 / 4 days per year and 1461 / 48 days per month. You can do this by replacing the last four lines with:

age_years = total_days * 4 / 1461;
uint16_t remaining_days = total_days - age_years * 1461 / 4;
age_months = remaining_days * 48 / 1461;
age_days = remaining_days - age_months * 1461 / 48;
1
  • I'm almost done updating the code, you may be interested to see it. I am removing all floating point operations and attempting to max out the resolution using only 32 bit integers and a few correction factors. I think I got the "86401.505" by naively dividing the length of sidereal year in seconds by 365.25 days, I fix that in the next version. I got a bit confused with the several different definitions of a year I guess.
    – Mir
    Commented Feb 19 at 9:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.