Introduction: Arduino Watch Core

This instructables show how to use a square color display and Arduino dev board build a watch core.

This will cover the topics of dev board selection, display selection, extra modules selection, UI design, performance tuning and power saving.


I have prototyped some Arduino Watches few years ago. But the hardware sizes at that time are too big to squeeze into a watch case, so the project have suspended at that time.

Step 1: Power Consumption Concern

A watch have limited space to fit a battery, it should be below 200 mAh or even below 100 mAh.

On the other hand, a watch require a long enough battery life. A normal quartz watch can last at least 1 year, a device with color display nearly not possible to last this long period. As a watch, it should at least can last a day and preferred last over a week.

If 100 mAh battery would like to run a week:

100 mAh / 7 days / 24 hours ~= 0.6 mA

So the current usage while standby should not over 0.6 mA.

Step 2: Dev Board Selection

There are tons of dev boards supported by Arduino IDE. Most of them have a USB to Serial chip for communicating with the computer. It is very helpful for development but most of them failed to cut the power consumption while powered by battery. The chip draws few mA continuously, it cannot meet the standby power consumption requirement.

The simplest way to avoid this power consumption is select a board that do not have built-in USB to Serial chip. There are 2 Arduino dev board available:

  • Arduino Pro Mini, it has a FTDI pin header for connecting to an external USB to Serial adaptor, after upload the program you can detach the cable to save the power. It consume below 0.04 mA in deep sleep mode if removed power LED.
  • Sparkfun Pro Micro, the on board MCU ATmega32U4 also can act as an USB transceiver itself. So you can direct communicate with computer without extra adaptor. It consume around 0.1 mA in deep sleep mode if removed power LED.

The above 2 dev boards both have 3.3 V @8MHz version, so it can direct power with Lipo battery without extra step up circuit.

I like Pro Micro not require extra adaptor while uploading new program, so I will choose Pro Micro in this project.


Step 3: Display Selection

I have decided to use an analog watch face, so it is better to use a square display. I can find 3 type of square display in the hobbyist market:

  • ST7735 LCD, resolution is 128 x 128. This is a typical square display in Arduino world. It has various versions come from various manufacturers and many display library support this. But the viewing angle is very limited.
  • SSD1351 OLED, resolution is 128 x 128. OLED have much better viewing angle and also can save much power if the screen layout mainly in black (pixel not turn on). But it require extra step up circuit to 15 V for powering the OLED, so it become very thick in size.
  • ST7789 IPS LCD, resolution is 240 x 240. IPS LCD have very good viewing angle, higher resolution and very thin in size.

ST7789 IPS LCD appear in the market since around 2018, it should be tailor-made for smart watch market. I think it is a much better option for building an Arduino watch.

Step 4: Time Sync

There are few way the watch can sync the correct time:

  • Compile time stamp, it is the simplest way to sync the time without extra hardware. While compile time, it can embed the current computer time in the code, so Arduino can direct use that time add millis() to get the current time. But it have many limitation, e.g. the time will be wrong after reset, the MCU internal timer is not so accurate, it can drift few seconds a day, ... etc.
  • Real Time Clock (RTC) chip, it is an individual chip dedicate to count the time. Some chip e.g. DS3231 built-in crystal oscillator, so no extra electronics component required. It only consume 0.1-0.2 mA continuously.
  • GPS module, GPS signal encapsulated current timestamp for calculating location. So you always can get the correct time once you go outdoor. It consume around 20-40 mA while receiving GPS signal but it enter sleep mode while not in use.
  • WiFi + NTP, computer and smartphone both sync the time with the NTP server on the internet. Arduino watch also can use a WiFi module to connect to an AP and sync the time with NTP server. It require 70-150 mA while connecting WiFi, it also can enter sleep mode while not in use.

Among the above options, RTC chip is the smallest, cheapest, less power consumption and reliable solution, so I will use this way in Arduino Watch Core.

Step 5: Wake Up User Interface

The color display consume around 10-40 mA while turned on, it cannot always on when powered by battery. It is better enter sleep mode after predefined time out period. But how should it wake up again?

Wake up by a push button is the simplest wake up method, but push a button to check the time is not a normal practice for the people wearing a wrist watch.

Some smart watch can detect the motion gesture when you raise up the arm and look at the watch. It actually can easily reproduce by 2 simple vibration sensors. The above picture illustrates how does it work.

Step 6: Assembly

Here are the connection summary:

LCD -> Arduino
GND -> GND           -> DS3231 RTC GND
Vcc -> Vcc           -> DS3231 RTC Vcc
RST -> GPIO 18(A0)
DC  -> GPIO 19(A1)
LED -> GPIO 10
       GPIO  2(SDA)  -> DS3231 RTC SDA
       GPIO  3(SCL)  -> DS3231 RTC SCL
       GPIO 7        -> motion sensor  -> GND

Battery connection is optional, you may refer to my previous Instructables, Dev Board Breadboard, for more details.

Step 7: Arduino_GFX

In my previous Instructables, Select-Color-Display-for-ESP32, I found Adafruit_GFX is a very fast library and supported many display hardwares. But it still have some performance gap compare with TFT_eSPI. On the other hand, TFT_eSPI only support ESP8266 and ESP32. And also, these 2 libraries not support power saving API.

In order to make substantial performance tuning but still support Adafruit like API, I decide rewrite the library as Arduino_GFX and share it to GitHub:

Step 8: Program Preparation

Arduino IDE

Download and install Arduino IDE at:

Add Sparkfun Pro Micro support

Follow the Installation Instructions to add Sparkfun boards support:

After installed the support, remember select the correct board in Arduino IDE:

  • Select Tools menu -> Board -> SparkFun Pro Micro
  • Select Tools menu -> Processor -> ATmega32U4 (3.3V, 8MHz)

Adafruit RTClib library

Use Arduino IDE Library Manager to install RTClib:

Arduino GFX library

Add Arduino_GFX library to Arduino IDE:

If you are not familiar add library from GitHub, simply press the green "Clone or download" button and then "Download ZIP". And then in Arduino IDE, select Sketch menu -> Include Library -> Add .ZIP Library... -> select downloaded ZIP file.

LowPower library

Add Arduino_GFX library to Arduino IDE:

If you are not familiar add library from GitHub, simply press the green "Clone or download" button and then "Download ZIP". And then in Arduino IDE, select Sketch menu -> Include Library -> Add .ZIP Library... -> select downloaded ZIP file.

Arduino Watch Core

Please use Arduino IDE compile and upload RTClibSetRTC.ino first to initial the time in RTC. And then compile and upload the Arduino_Watch.ino afterward.

Step 9: Continuous Sweep Second Hand

I have decided to use an analog watch face. I can find a simple example in TFT_eSPI library, the code comment said it is based on "a sketch by Gilchrist 6/2/2014 1.0".

I start with this example and try to enhance it to continuous sweep second hand.

Step 10: Tuning 1: Various Redraw Methods

The original example become flicker if continuously update the screen. I need to try other method to make it smooth:

  • method 1
    It is the original method, erase old hands and then draw new hands.
  • method 2
    redraw_hands_rect(overwrite_rect), overwrite rectangle area.
    To avoid flicker effect, it require erase and draw at the same time. The simplest method is fill the target rectangle area with new pixels.
  • method 3
    redraw_hands_4_split(overwrite_rect), overwrite rectangle area in 4 split.
    The size of rectangle that can cover all 3 hands can be very large and very time consuming to fill it. Split it in 4 can reduce the total area and reduce the fill time.
  • method 4
    redraw_hands_4_split(spi_raw_overwrite_rect), overwrite at the same SPI transaction.
    There are some overhead for each GFX API call on the SPI begin transaction and set the CS pin low, make all API call in the same loop as a single SPI transaction can save some overhead.
  • method 5
    redraw_hands_4_split(spi_raw_draw_and_erase), overwrite required pixels only.
    Fill rectangle area overwrite some pixels that are no need to overwrite, this method scan the rectangle area and only overwrite required pixels by writePixel().
  • method 6
    redraw_hands_4_split(spi_raw_cache_overwrite_rect), overwrite check with cache.
    method 2-5 use inLine() function check pixel-by-pixel if it is in the 3 hands then determine which color should be filled. according to the benchmark, the inLine() function is very time consuming since it called many times. This method try to cached all line points first, then check the cache array by inCachedPoints() function.
  • method 7
    redraw_hands_cached_lines(), draw new hands and then erase old hands.
    Accourding to the benchmark, inCachedPoints() is slower than inLine() function. But cache still should be the right way, just should not introduce a 3 level loop. we can draw and cache the new hands first then erase old hands but avoid erase the new hands cached points.
  • method 8
    redraw_hands_cached_draw_and_erase(), draw and erase hands at the same loop.
    Check inCachedPoints() in every draw hand loop still too time consuming. If we always draw line start from the centre, we can simple erase old point in the current array item and fill the new point value in it. Then it can reduce 1 loop level of cache check and speed up dramatically.

Here are the benchmark result:

4337: method 1: redraw_hands_erase_and_draw();
64348: maxLoopMs: 27, minLoopMs: 6, drawCount: 778, TotalMs: 60013
64495: method 2: redraw_hands_rect(overwrite_rect);
124641: maxLoopMs: 899, minLoopMs: 34, drawCount: 136, TotalMs: 60146
124788: method 3: redraw_hands_4_split(overwrite_rect);
184909: maxLoopMs: 621, minLoopMs: 47, drawCount: 171, TotalMs: 60119
185057: method 4: redraw_hands_4_split(spi_raw_overwrite_rect);
245221: maxLoopMs: 514, minLoopMs: 41, drawCount: 206, TotalMs: 60164
245370: method 5: redraw_hands_4_split(spi_raw_draw_and_erase);
305750: maxLoopMs: 809, minLoopMs: 67, drawCount: 131, TotalMs: 60378
305897: method 6: redraw_hands_4_split(spi_raw_cache_overwrite_rect);
369383: maxLoopMs: 5456, minLoopMs: 297, drawCount: 26, TotalMs: 63484
369530: method 7: redraw_hands_cached_lines();
429694: maxLoopMs: 156, minLoopMs: 71, drawCount: 492, TotalMs: 60164
429842: method 8: redraw_hands_cached_draw_and_erase();
489889: maxLoopMs: 90, minLoopMs: 41, drawCount: 778, TotalMs: 60047

Only the last method performance is comparable to the original method and also it can totally eliminated flicker, so I will use the last method.

Note 1:

writePixel() require extra set address position command call before send the color to the display, so method 5 is slower than method 4 even lesser pixels updated.

Note 2:

If you run the benchmark in ESP32 dev board, most methods can reduce flicker and run smooth except method 6 :P

Step 11: Tuning 2: Integer Trigonometry

Arduino Pro Micro 3.3 V only running at 8 MHz, the trigonometry calculation of watch hands may be too heavy to this MCU. Someone suggest use integer mapping method to avoid heavy trigonometry calculation. Here are the benchmark result:

3145: float version
3817: maxLoopMs: 3, minLoopMs: 0, TotalMs: 672
3817: ifloat version
4038: maxLoopMs: 3, minLoopMs: 0, TotalMs: 221
4038: int version
4190: maxLoopMs: 3, minLoopMs: 0, TotalMs: 152

The integer version is much faster than original float version. But the overhead is too minor compare with the whole drawing loop. And also integer version only have 360 coordinate combination in one round, but the 100 pixels long second hand can have 780 coordinate combination in float version. So it is better keep to use float version to make the sweep movement more smooth.


Step 12: Tuning 3: InLine() Function

Overwrite hands method 2-5 substantially depends on the inLine() function. Here are some benchmark of different inLine() function version:

490037: redraw_hands_rect(overwrite_rect_no_draw_float);
550225: maxLoopMs: 1231, minLoopMs: 56, drawCount: 98, TotalMs: 60186
550227: redraw_hands_rect(overwrite_rect_no_draw_int);
610277: maxLoopMs: 381, minLoopMs: 18, drawCount: 283, TotalMs: 60050
610277: redraw_hands_rect(overwrite_rect_no_draw);
670347: maxLoopMs: 439, minLoopMs: 25, drawCount: 239, TotalMs: 60070

The first 2 version use interpolation to check a point is in the line, integer version is much faster than float version. But use interpolation checking method is not the same as GFX draw line algorithm, the GFX draw a line look nicer. So the 3rd version try to reproduce same algorithm for the in line checking.

Even using fastest integer interpolation version cannot help too much, overwrite hands method 7 and 8 that are not using inLine() function still much faster than method 2-5.

Step 13: Power Consumption

The watch enter sleep mode after 20 seconds timeout and wake by the motion sensor.

At normal mode, the power consumption is around 15-35 mA.

At sleep mode, the power consumption is around 0.4-0.5 mA.

The break down should be:

  • dev board ~0.1 mA
  • IPS LCD ~0.3 mA
  • RTC ~ 0.1 mA

Step 14: Happy Coding!

Now you have the Arduino Watch Core as a starting point.

Let's add your own code and hardware to make it your own fully customized smart watch!