DEV Community

Rubens Zimbres
Rubens Zimbres

Posted on • Originally published at Medium on

Creating a Binary Watch from Scratch with LILYGO Programmable Device

I bought a LYLIGO T-Watch-2020 V3 almost 4 years ago but hadn’t played with it yet. This watch is a programmable device that comes with a USB connection and you can burn anything into it. It has a rechargeable battery, it is very simple but it is not waterproof. It is an ESP32-based smartwatch designed by Shenzhen Xinyuan Electronics Co., Ltd.

Lately, a friend of mine bought a binary watch and I said to myself: “Wow, maybe finally I have an interesting project for my programmable watch!”. This article is a step by step tutorial on how to get it done. The LILYGO T-Watch can be obtained in LILYGO website for $ 36.55.


Original watch box

The code provided here creates a binary watch application for the LILYGO T-Watch that displays time using the same binary number system computers use internally. Instead of showing traditional numbers like “12:30”, it represents each digit using patterns of green and gray dots, where green means “1” and gray means “0”. This serves as both a functional timepiece and an excellent way to learn how computers actually store numbers behind the scenes.

The program’s main loop continuously updates the binary time display while managing power consumption to preserve battery life. After six seconds of inactivity, the watch automatically goes to sleep by turning off the display, but keeps the power management chip active so it can instantly wake up when you press the button. The display also shows a real-time battery indicator that changes color from green to yellow to red as the battery level decreases.

The most technically interesting feature is the interrupt-driven wake-up system that lets the watch respond instantly to button presses even while sleeping. The code includes automatic time setting using the compilation timestamp and sophisticated battery monitoring that reads actual voltage and converts it to percentage using realistic lithium battery discharge curves. These features work together to create an educational tool that teaches binary numbers while providing reliable timekeeping and intelligent power management. The result is that the battery consumes only 7% per day. This means the battery charge will last 10 days 😁

The TWatch repo is here:

https://github.com/Xinyuan-LilyGO/TTGO_TWatch_Library

First of all, download the Arduino IDE software here.


Arduino Software download

Then, we have to install the necessary libraries. Create a folder Arduino/libraries and inside this folder, run:

git clone https://github.com/Xinyuan-LilyGO/TTGO_TWatch_Library
git clone https://github.com/lewisxhe/AXP202X_Library.git
Enter fullscreen mode Exit fullscreen mode

Then, go to Arduino/File/Preferences and set up this folder as the Sketchbook location:

Google’s Gemini provided me with the transcription of the Internal Hardware Connections , given the electronic schematic diagram :


Electronic schematic diagram for the LYLIGO T-Watch-2020 V3

These are the ESP32 GPIO pins that are already connected to the internal components of the watch.

Display (ST7789V TFT)

  • MOSI: GPIO 19 (SPI Data)
  • SCLK: GPIO 18 (SPI Clock)
  • CS: GPIO 5 (SPI Chip Select)
  • DC: GPIO 27 (Data/Command)
  • RST: GPIO 26 (Reset)
  • Backlight: GPIO 12 (Controlled by AXP202 LDO2)

Power Management (AXP202 PMIC)

  • SDA: GPIO 21 (I2C Data)
  • SCL: GPIO 22 (I2C Clock)
  • IRQ: GPIO 35 (Interrupt Request)

Touch Screen (FT6236)

  • SDA: GPIO 21 (Shared on I2C Bus)
  • SCL: GPIO 22 (Shared on I2C Bus)
  • IRQ: GPIO 38 (Touch Interrupt)
  • Accelerometer (BMA423)
  • SDA: GPIO 21 (Shared on I2C Bus)
  • SCL: GPIO 22 (Shared on I2C Bus)
  • IRQ: GPIO 39 (Sensor Interrupt)
  • Real-Time Clock (PCF8563)
  • SDA: GPIO 21 (Shared on I2C Bus)
  • SCL: GPIO 22 (Shared on I2C Bus)

User Button

  • The side button is connected to the PEK input of the AXP202 power chip. You interact with it through the library (ttgo->power->isPEKShortPress()).

Vibration Motor

  • Motor: GPIO 4

As it has been 3–4 years I bought the watch, I had to check the state of the rechargeable battery with a Voltimeter. The adjustment of the battery inside of the watch is critical, because the battery pins are very sensitive.


Battery testing with a Voltimeter

An important step: define the board you are going to use. Go to Arduino/Tools and define board as TTGO-T-Watch. Also, set Core Debug Level to Verbose. Then, Erase All Flash Before Sketch Upload to True , Partition Scheme set to default , Board Revision to T-Watch-2020-V3 , Upload Speed 921600 , and Programmer esptool.

Now, get the file LilyGoWatch.h and uncomment the following line of code:

#define LILYGO_WATCH_2020_V3
Enter fullscreen mode Exit fullscreen mode

Your Arduino IDE will look like this:

Now, let’s get the code for the Binary Watch in C++:

/*
 * LIBRARY INCLUDES AND GLOBAL VARIABLE DECLARATIONS
 * This section imports the necessary libraries and sets up global variables that will be used
 * throughout the program. Think of this as gathering all the tools and materials you'll need
 * before starting a project. The LilyGoWatch library provides specific functions for the T-Watch
 * hardware, while the global pointers give us access to the display, power management, and main
 * watch object from anywhere in our program.
 */
#include <LilyGoWatch.h>

TTGOClass *watch = nullptr;
TFT_eSPI *tft = nullptr;
AXP20X_Class *power = nullptr;

#define LED_ON_COLOR TFT_GREEN
#define LED_OFF_COLOR TFT_DARKGREY
#define BG_COLOR TFT_BLACK
#define TEXT_COLOR TFT_WHITE

#define DISPLAY_TIMEOUT 6000 // Turn off display after 6 seconds
unsigned long lastActivity = 0;
bool displayOn = true;
bool irq = false; // Flag for AXP202 interrupt

void displayBinaryWatch(int hours, int minutes);
void setInitialTimeFromCompiler();
void goToSleep();
void wakeUp();

/*
 * INTERRUPT SERVICE ROUTINE FOR POWER MANAGEMENT
 * This is a special type of function that gets called automatically when the power management
 * chip (AXP202) detects an event like a button press. The IRAM_ATTR tells the compiler to store
 * this function in fast internal RAM so it can respond quickly to interrupts. Think of this as
 * a doorbell - when someone presses the button, this function immediately "rings" to let the
 * main program know something happened.
 */
// AXP202 interrupt service routine
void IRAM_ATTR axp202_irq() {
    irq = true;
}

/*
 * INITIAL SETUP AND CONFIGURATION
 * This setup() function runs once when the device starts up and is responsible for initializing
 * all the hardware components and configuring them properly. It's like setting up a workspace -
 * turning on the lights, arranging your tools, and making sure everything is ready to use.
 * The power management configuration here is particularly critical because it determines whether
 * the device can wake up properly from sleep mode when the button is pressed.
 */
void setup() {
    Serial.begin(115200);

    watch = TTGOClass::getWatch();
    watch->begin();

    // Get power management instance
    power = watch->power;

    watch->openBL();
    tft = watch->tft;

    tft->setRotation(2);
    tft->fillScreen(BG_COLOR);
    tft->setTextColor(TEXT_COLOR, BG_COLOR);
    tft->setTextDatum(MC_DATUM);

    setInitialTimeFromCompiler();

    // Critical: Proper AXP202 configuration for wake-up
    // Enable ADC for power monitoring
    power->adc1Enable(AXP202_BATT_VOL_ADC1 | AXP202_BATT_CUR_ADC1 | 
                      AXP202_VBUS_VOL_ADC1 | AXP202_VBUS_CUR_ADC1, true);

    // Configure AXP202 interrupts - this is the key to proper wake-up
    power->enableIRQ(AXP202_PEK_SHORTPRESS_IRQ | AXP202_PEK_LONGPRESS_IRQ, true);
    power->clearIRQ();

    // Attach interrupt to AXP202 interrupt pin (GPIO 35)
    pinMode(AXP202_INT, INPUT);
    attachInterrupt(AXP202_INT, axp202_irq, FALLING);

    // Configure essential power outputs to stay on during sleep
    // Note: We only configure the power outputs that are actually defined in this library version
    power->setPowerOutPut(AXP202_LDO2, true); // Display and sensors
    power->setPowerOutPut(AXP202_LDO3, true); // Additional peripherals
    power->setPowerOutPut(AXP202_DCDC2, true); // ESP32 core power
    power->setPowerOutPut(AXP202_EXTEN, false); // External enable (not needed for basic operation)

    lastActivity = millis();
    tft->fillScreen(BG_COLOR);

    Serial.println("T-Watch Binary Watch Ready");
}

/*
 * MAIN PROGRAM LOOP - THE HEART OF THE WATCH
 * This loop() function runs continuously while the device is awake, like the main engine of the
 * watch. It handles three key responsibilities: detecting button presses through interrupt flags,
 * updating the time display when the screen is on, and managing when to go to sleep to save battery.
 * The loop checks for events, updates the display, and manages power - think of it as the watch's
 * "thinking process" that never stops while it's awake.
 */
void loop() {
    unsigned long currentTime = millis();

    // Handle AXP202 interrupt (button press or other power events)
    if (irq) {
        irq = false;
        power->readIRQ();

        // Check for button press
        if (power->isPEKShortPressIRQ()) {
            Serial.println("Short press detected");
            power->clearIRQ();

            if (!displayOn) {
                wakeUp();
            }
            lastActivity = currentTime;
        }

        if (power->isPEKLongPressIRQ()) {
            Serial.println("Long press detected");
            power->clearIRQ();

            if (!displayOn) {
                wakeUp();
            }
            lastActivity = currentTime;
        }

        // Clear any remaining interrupts
        power->clearIRQ();
    }

    // Update display if it's on
    if (displayOn) {
        RTC_Date datetime = watch->rtc->getDateTime();
        displayBinaryWatch(datetime.hour, datetime.minute);

        // Check if we should turn off display
        if (currentTime - lastActivity > DISPLAY_TIMEOUT) {
            goToSleep();
        }
    }

    delay(1000);
}

/*
 * SLEEP MODE MANAGEMENT FOR POWER CONSERVATION
 * This function handles putting the watch into a low-power sleep state to preserve battery life.
 * It's like putting the watch into a "hibernation" mode where most systems shut down, but the
 * power management chip stays alert to wake the device when the button is pressed. The process
 * involves carefully shutting down the display, configuring wake-up sources, and entering a
 * light sleep that maintains enough functionality to respond to button presses.
 */
void goToSleep() {
    Serial.println("Going to sleep...");

    // Turn off display
    tft->fillScreen(TFT_BLACK);
    watch->closeBL();
    displayOn = false;

    // Clear any pending interrupts before sleep
    power->clearIRQ();

    // Configure ESP32 wake-up source - AXP202 interrupt on GPIO 35
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_35, 0);

    // Put the display to sleep to save power
    tft->writecommand(ST7789_SLPIN);

    // Reduce CPU frequency for power savings
    setCpuFrequencyMhz(80);

    // Enter light sleep - this keeps the AXP202 active for wake-up
    Serial.println("Entering light sleep");
    esp_light_sleep_start();

    // When we reach here, we've been woken up
    Serial.println("Woke up from sleep!");

    // Restore CPU frequency
    setCpuFrequencyMhz(240);

    // Wake up the display
    tft->writecommand(ST7789_SLPOUT);
    delay(120); // Display needs time to wake up
}

/*
 * WAKE-UP PROCESS AND DISPLAY REACTIVATION
 * This function handles bringing the watch back to full operation after it has been sleeping.
 * It's like turning the lights back on and getting everything ready to work again. The function
 * reactivates the display backlight, sets the proper state flags, and clears any leftover
 * interrupt signals to ensure the watch is ready for normal operation.
 */
void wakeUp() {
    Serial.println("Display waking up");

    // Turn on backlight
    watch->openBL();
    displayOn = true;
    lastActivity = millis();

    // Clear any pending interrupts to start fresh
    power->clearIRQ();
}

/*
 * AUTOMATIC TIME SETTING FROM COMPILATION TIMESTAMP
 * This clever function sets the watch's time automatically using the date and time when the
 * program was compiled. It's like having the watch "remember" when it was built and use that
 * as a starting point for keeping time. The function parses the compiler's __DATE__ and __TIME__
 * macros, converts text month names to numbers, and programs the real-time clock chip with this
 * information so the watch starts with approximately the correct time.
 */
void setInitialTimeFromCompiler() {
    tft->fillScreen(BG_COLOR);
    tft->drawString("Setting Time...", 120, 120);

    char month_str[4];
    int day, year, hour, minute, second;

    sscanf( __DATE__ , "%s %d %d", month_str, &day, &year);
    sscanf( __TIME__ , "%d:%d:%d", &hour, &minute, &second);

    const char* months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
                           "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
    int month = 1;

    for (int i = 0; i < 12; i++) {
        if (strcmp(month_str, months[i]) == 0) {
            month = i + 1;
            break;
        }
    }

    watch->rtc->setDateTime(year, month, day, hour, minute, second);
    delay(1000);
}

/*
 * BINARY TIME DISPLAY VISUALIZATION WITH BATTERY MONITORING
 * This function creates the visual representation of time in binary format on the watch screen.
 * Binary representation shows time using only 1s and 0s (green dots for 1, gray dots for 0),
 * which is how computers actually store and process numbers internally. Each row represents either
 * hours or minutes as a 6-bit binary number, allowing display of hours 0-23 and minutes 0-59.
 * The function also shows regular time below the binary display for easy reference and learning.
 * Additionally, it includes real-time battery monitoring by reading the actual voltage from the
 * power management chip and converting it to a percentage based on typical lithium battery curves.
 */
void displayBinaryWatch(int hours, int minutes) {
    tft->fillScreen(BG_COLOR);

    // Battery monitoring - Read actual voltage and convert to meaningful percentage
    // Lithium batteries typically range from 3.2V (empty) to 4.2V (full)
    // We use a more realistic curve that accounts for how lithium batteries actually discharge
    float batteryVoltage = power->getBattVoltage() / 1000.0; // Convert millivolts to volts
    int batteryPercent;

    // Convert voltage to percentage using realistic lithium battery discharge curve
    // This isn't linear because batteries don't discharge linearly
    if (batteryVoltage >= 4.1) {
        batteryPercent = 100;
    } else if (batteryVoltage >= 3.9) {
        // 90-100% range: voltage drops slowly at high charge
        batteryPercent = 90 + (int)((batteryVoltage - 3.9) * 50);
    } else if (batteryVoltage >= 3.7) {
        // 50-90% range: more linear discharge in middle range
        batteryPercent = 50 + (int)((batteryVoltage - 3.7) * 200);
    } else if (batteryVoltage >= 3.4) {
        // 10-50% range: faster voltage drop
        batteryPercent = 10 + (int)((batteryVoltage - 3.4) * 133);
    } else if (batteryVoltage >= 3.2) {
        // 0-10% range: rapid voltage drop when nearly empty
        batteryPercent = (int)((batteryVoltage - 3.2) * 50);
    } else {
        batteryPercent = 0; // Battery critically low
    }

    // Ensure percentage stays within valid range
    batteryPercent = constrain(batteryPercent, 0, 100);

    // Display battery percentage in top right corner with visual indicator
    tft->setTextSize(1);
    tft->setTextColor(TEXT_COLOR, BG_COLOR);

    // Draw simple battery icon outline (rectangle with terminal)
    int battX = 210;
    int battY = 25;
    int battWidth = 20;
    int battHeight = 10;

    // Show percentage text - positioned to center above the battery icon
    // Calculate center of battery: battX + (battWidth / 2)
    char batteryText[8];
    sprintf(batteryText, "%d%%", batteryPercent);
    int textCenterX = battX + (battWidth / 2); // Center the text over the battery
    tft->drawString(batteryText, textCenterX, 15);

    // Battery outline
    tft->drawRect(battX, battY, battWidth, battHeight, TEXT_COLOR);
    // Battery terminal (small rectangle on right side)
    tft->fillRect(battX + battWidth, battY + 2, 2, battHeight - 4, TEXT_COLOR);

    // Fill battery based on percentage with color coding
    int fillWidth = (battWidth - 2) * batteryPercent / 100;
    uint16_t fillColor;

    if (batteryPercent > 50) {
        fillColor = TFT_GREEN; // Green when battery is good
    } else if (batteryPercent > 20) {
        fillColor = TFT_YELLOW; // Yellow when getting low
    } else {
        fillColor = TFT_RED; // Red when critically low
    }

    if (fillWidth > 0) {
        tft->fillRect(battX + 1, battY + 1, fillWidth, battHeight - 2, fillColor);
    }

    int ledSize = 12;
    int ledSpacing = 30;
    int startX = 54;
    int hoursY = 70;
    int minutesY = 130;

    tft->setTextSize(1);
    tft->setTextColor(TEXT_COLOR, BG_COLOR);
    tft->drawString("Hours", startX + (ledSpacing * 2.5), hoursY - 25);

    // Display hours in binary (6 bits for values 0-23 == 24 hours)
    for (int i = 5; i >= 0; i--) {
        bool bitSet = (hours >> i) & 1;
        int x = startX + ((5 - i) * ledSpacing);
        uint16_t color = bitSet ? LED_ON_COLOR : LED_OFF_COLOR;
        tft->fillCircle(x, hoursY, ledSize, color);
    }

    tft->drawString("Minutes", startX + (ledSpacing * 2.5), minutesY - 25);

    // Display minutes in binary (6 bits for values 0-59)
    for (int i = 5; i >= 0; i--) {
        bool bitSet = (minutes >> i) & 1;
        int x = startX + ((5 - i) * ledSpacing);
        uint16_t color = bitSet ? LED_ON_COLOR : LED_OFF_COLOR;
        tft->fillCircle(x, minutesY, ledSize, color);
    }

    RTC_Date datetime = watch->rtc->getDateTime();

    // Format the date as DD/MM
    char dateStr[6]; // String to hold "DD/MM" plus null terminator
    sprintf(dateStr, "%02d/%02d", datetime.day, datetime.month);

    // Display the formatted date
    tft->setTextColor(LED_OFF_COLOR, BG_COLOR);
    tft->setTextSize(2);
    tft->drawString(dateStr, 120, 190);
}
Enter fullscreen mode Exit fullscreen mode

Now we can upload the code to the watch, by clicking the Right Arrow:


Ready to upload the code

You will see a successful output. If not, and if you need to reboot or restore factory settings in the watch, and have to remove the battery, be very careful because the battery pins are very delicate and may change position. If this happens, the watch won’t turn on while disconnected from USB. It happened to me.


Successful upload

If you check the Serial Monitor, you will see:

RESULT

You will get a working watch:


The 24 hours Binary Watch ready. Date in DD/MM made with this tutorial.

This is an improved version, with minor changes to the code:


Version 2 of the 24 hours Binary Watch


Matrix version of the 24 hours Binary Watch

Top comments (0)