Introduction: World Time Zone Clock

About: Hobbyist. I like creating. I like learning. Multiple interests in tech; rock and roll; audio engineering; building prototypes; Python; Linux; Arduino; Raspberry Pi; ESP32; Home Automation, even Bigfoot. I'm a …

Six-Zone World Time Wall Using Raspberry Pi Pico and 16×2 I²C LCDs or A modern rebuild of the classic newsroom world clock — analog faces with digital OLED placards, one power source, zero batteries.


Overview


I've procrastinated long enough. It's time to finally get this project rolling. Of course, I waited until almost the last minute to do so, but save the time machine build for another day, this will suffice. Once completed I'll either technically bust or beat the deadline (depending on which clock you look at).


This project builds a six-clock world time display.

Pending future mods to power from a single 5V USB source (wall brick or power bank). Each clock runs on a regulated 1.5V rail while six OLED screens display city names and offsets, all driven by one microcontroller.


**I'll update the Instructable when the rest of the parts arrive.**


For a cohesive look, I chose multiple clocks that were the same at a good enough price to justify the project. However, for those that might be interested in the exact clocks I used, I bought them circa 2019 at Bed Bath and Beyond and believe they went the way of the dinosaur with in recent years.


Project Scope


Build a hardware world clock wall that shows six time zones at once using:


A Raspberry Pi Pico


Six 16×2 I²C LCDs (LCD1602 with backpacks)


A single power source


Static labels (city + timezone) for:


Seattle


Denver


Omaha


Boston


London


Tokyo


This version:


Does not show live time (just city/timezone labels)


Is designed to pair with six analog clocks mounted above/below each LCD


Uses one I²C address (0x27) for all six LCDs, solved by:


One hardware I²C bus, plus


Five additional SoftI2C buses on separate pin pairs


With all that out of the way... let's build.

Supplies

Clocks of your choice


Core electronics


1 × Raspberry Pi Pico or Pico H (headers pre-soldered is ideal)


6 × 16×2 LCD1602 displays with I²C backpacks (PCF8574-based, typical address 0x27)


1 × Solderless breadboard


1 × USB-A to Micro-USB data cable (not charge-only)


~30–40 × jumper wires (male–male and female–female)


Power


USB power source (5 V via Pico USB)


(Optional) External 5 V supply if you want to power Pico + clocks from one brick


Optional mechanical / mounting


Cardboard or plastic sheet for temporary standoffs


Hot glue gun / double-sided tape / zip ties


Backing board (plywood, MDF, acrylic, etc.) to mount the displays


Tools


Computer with Thonny installed (or similar MicroPython IDE)


Small screwdriver (for LCD contrast trimpot)


Optional multimeter (to sanity-check voltage)


Buck converter (future mod to step down the voltage powering the clocks)

Step 1: Getting Clocks Centered

This was the hardest part of the build. Getting everything aligned and spaced precisely. I knew I wanted equal gaps between each clock. If I'd used 5 clocks instead of 6 I could have just used the 3rd clock to determine the middle.


I measured each clock as approximately 8 3/4" then did the math on the desired gaps, came out to 74". I used common pine and painted it.

In the photos I'm attempting to show that I was using known references such as a paper plate and an 8x11.5" sheet of paper to build my templates. For the spacer, I used Inkscape to create an SVG PDF that i printed and cut out.

This took a lot longer than expected getting the spacing right. It worked out, the components hadn't arrived yet. If only the postal office was as efficient as the weather service.

Eventually some of the parts showed up.

Step 2: Programming the Raspberry Pi Pico

Unlike other Raspberry pis, the Pico doesn't require an SD car. Rather, you flash it with MicroPython. The Raspberry Pi webpage has instructions on this here :

https://www.raspberrypi.com/documentation/microcontrollers/micropython.html


Flash MicroPython onto the Pico


This step turns the Pico into a MicroPython-ready device.


Download MicroPython UF2 for Pico


Use the firmware for: “Pico” (not Pico W, not Pico 2).


Put the Pico in BOOTSEL mode


Unplug the Pico.


Hold down the BOOTSEL button on the board.


While holding it, plug in the USB cable to your computer.


Release BOOTSEL.


A new drive named RPI-RP2 should appear.


Copy firmware


Drag and drop the downloaded .uf2 file onto RPI-RP2.


The Pico will reboot and the drive will disappear. That’s expected.


At this point, MicroPython is running on your Pico.

Step 3: Thonny

Set Up Thonny and Verify the Pico

  1. Open Thonny.
  2. Go to:
  3. Run → Select interpreter
  4. Choose MicroPython (Raspberry Pi Pico).
  5. Click Stop/Restart in Thonny.


Step 4: Wire a Single LCD to Known-Good I²C Pins

We start with just one LCD, on the canonical I²C pins (GP0/GP1), with 3.3 V power for safety/debugging.

Connections (direct)

  1. LCD GND → Pico GND
  2. LCD VCC → Pico 3V3(OUT)
  3. LCD SDA → Pico GP0
  4. LCD SCL → Pico GP1
On the LCD backpack, verify the header is labeled:
GND VCC SDA SCL
and wire accordingly.


Step 5: Scan the I²C Bus and Confirm the LCD Address

In Thonny, run:


from machine import Pin, I2C
import time

i2c = I2C(0, sda=Pin(0), scl=Pin(1), freq=100000)

while True:
print("scan:", i2c.scan())
time.sleep(2)

You should see:


scan: [39]
scan: [39]
...
  1. 39 (decimal) = 0x27 (hex), which is the common LCD I²C address.
  2. If you see [63], then your address is 0x3F instead.

Remember that value; we’ll use 0x27 in this write-up.

Stop the script (Ctrl+C or Stop button).

Step 6: Add the LCD Driver (i2c_lcd.py)

We’ll use a simple MicroPython driver for HD44780 LCDs over PCF8574 I²C backpacks.

  1. In Thonny, create a new file:
  2. File → New
  3. Paste this:

from machine import I2C
import time

class I2cLcd:
def __init__(self, i2c, addr, num_lines=2, num_columns=16):
self.i2c = i2c
self.addr = addr
self.num_lines = num_lines
self.num_columns = num_columns
self.backlight = 0x08 # bit 3

# control bits
self.PIN_RS = 0x01
self.PIN_RW = 0x02
self.PIN_EN = 0x04

self._init_lcd()

def _write_byte(self, data):
self.i2c.writeto(self.addr, bytes([data | self.backlight]))

def _pulse_en(self, data):
self._write_byte(data | self.PIN_EN)
time.sleep_us(500)
self._write_byte(data & ~self.PIN_EN)
time.sleep_us(100)

def _write4bits(self, data):
self._write_byte(data)
self._pulse_en(data)

def _send(self, value, mode=0):
high = value & 0xF0
low = (value << 4) & 0xF0
self._write4bits(high | mode)
self._write4bits(low | mode)

def command(self, cmd):
self._send(cmd, mode=0)

def write_char(self, char_val):
self._send(char_val, mode=self.PIN_RS)

def clear(self):
self.command(0x01)
time.sleep_ms(2)

def move_to(self, col, row):
row_offsets = [0x00, 0x40, 0x14, 0x54]
if row >= self.num_lines:
row = self.num_lines - 1
addr = 0x80 | (col + row_offsets[row])
self.command(addr)

def putstr(self, string):
for ch in string:
self.write_char(ord(ch))

def _init_lcd(self):
time.sleep_ms(50)

# init sequence for 4-bit mode
self._write4bits(0x30)
time.sleep_ms(5)
self._write4bits(0x30)
time.sleep_us(150)
self._write4bits(0x30)
time.sleep_us(150)
self._write4bits(0x20)
time.sleep_us(150)

self.command(0x28) # 4-bit, 2 line, 5x8 font
self.command(0x0C) # display on, no cursor, no blink
self.clear()
self.command(0x06) # entry mode
self.backlight_on()

def backlight_on(self):
self.backlight = 0x08
self._write_byte(0)

def backlight_off(self):
self.backlight = 0x00
self._write_byte(0)
  1. Save it to the Pico as:
  2. i2c_lcd.py


Step 7: Test the First LCD (Seattle) on GP0/GP1

Create a new script:


from machine import Pin, I2C
import time
from i2c_lcd import I2cLcd

LCD_ADDR = 0x27 # Use 0x3F if your scan said so

i2c = I2C(0, sda=Pin(0), scl=Pin(1), freq=100000)
lcd = I2cLcd(i2c, LCD_ADDR, num_lines=2, num_columns=16)

def center_16(text: str) -> str:
text = text[:16]
spaces = (16 - len(text)) // 2
return " " * spaces + text

lcd.clear()
lcd.move_to(0, 0)
lcd.putstr(center_16("SEATTLE"))
lcd.move_to(0, 1)
lcd.putstr(center_16("UTC-8 (PST)"))

while True:
time.sleep(1)

Run it.

If you see nothing but the backlight, tweak the little blue contrast pot on the backpack until characters appear.

Once this works, you’ve proved:

  1. Firmware is good
  2. Driver works
  3. Wiring is correct

Seattle is now your reference display.

Step 7 – Move to 5 V and Introduce the Breadboard Rails

Now move from direct wiring to a breadboard power bus so you can support all six LCDs.

Power distribution:

  1. Pico VSYS → breadboard +5 V rail
  2. Pico GND → breadboard GND rail
  3. All LCD VCC → +5 V rail
  4. All LCD GND → GND rail

Keep Seattle’s LCD data pins on:

  1. SDA → GP0
  2. SCL → GP1

Re-run the Seattle test script to confirm it still works off 5 V rails.

Step 8 – Add Denver on Its Own SoftI2C Bus (GP2/GP3)

Wire the second LCD (Denver):

  1. VCC → +5 V rail
  2. GND → GND rail
  3. SDA → GP2
  4. SCL → GP3

Denver test script:


from machine import Pin, SoftI2C
import time
from i2c_lcd import I2cLcd

LCD_ADDR = 0x27

i2c = SoftI2C(sda=Pin(2), scl=Pin(3), freq=100000)
lcd = I2cLcd(i2c, LCD_ADDR, num_lines=2, num_columns=16)

def center_16(text):
text = text[:16]
return text.center(16)

lcd.clear()
lcd.move_to(0, 0)
lcd.putstr(center_16("DENVER"))
lcd.move_to(0, 1)
lcd.putstr(center_16("UTC-7 (MT)"))

while True:
time.sleep(1)

Run it. Now you should see Seattle on LCD #1 and Denver on LCD #2.

Step 8: Full Code and Wiring

Use more SoftI2C buses, each with its own SDA/SCL pin pair:

CityLCD #SDASCL

Seattle

1

GP0

GP1 (hardware I²C0)

Denver

2

GP2

GP3

Omaha

3

GP4

GP5

Boston

4

GP6

GP7

London

5

GP8

GP9

Tokyo

6

GP10

GP11

Wire each LCD’s SDA/SCL pair exactly like that (all VCC/GND already on rails).







Put this on the Pico as main.py:


from machine import Pin, I2C, SoftI2C

import time

from i2c_lcd import I2cLcd


LCD_ADDR = 0x27 # From your scan


def center_16(text: str) -> str:

"""Return text centered in a 16-character field."""

text = text[:16]

spaces = (16 - len(text)) // 2

return " " * spaces + text


def setup_seattle():

"""LCD #1 – Seattle on hardware I2C0 (GP0/GP1)."""

i2c = I2C(0, sda=Pin(0), scl=Pin(1), freq=100000)

lcd = I2cLcd(i2c, LCD_ADDR, num_lines=2, num_columns=16)


city = center_16("SEATTLE")

zone = center_16("UTC-8 (PST)")


lcd.clear()

lcd.move_to(0, 0)

lcd.putstr(city)

lcd.move_to(0, 1)

lcd.putstr(zone)


return lcd


def setup_denver():

"""LCD #2 – Denver on SoftI2C (GP2/GP3)."""

i2c = SoftI2C(sda=Pin(2), scl=Pin(3), freq=100000)

lcd = I2cLcd(i2c, LCD_ADDR, num_lines=2, num_columns=16)


city = center_16("DENVER")

zone = center_16("UTC-7 (MT)")


lcd.clear()

lcd.move_to(0, 0)

lcd.putstr(city)

lcd.move_to(0, 1)

lcd.putstr(zone)


return lcd


def setup_omaha():

"""LCD #3 – Omaha on SoftI2C (GP4/GP5)."""

i2c = SoftI2C(sda=Pin(4), scl=Pin(5), freq=100000)

lcd = I2cLcd(i2c, LCD_ADDR, num_lines=2, num_columns=16)


city = center_16("OMAHA")

zone = center_16("UTC-6 (CT)")


lcd.clear()

lcd.move_to(0, 0)

lcd.putstr(city)

lcd.move_to(0, 1)

lcd.putstr(zone)


return lcd


def setup_boston():

"""LCD #4 – Boston on SoftI2C (GP6/GP7)."""

i2c = SoftI2C(sda=Pin(6), scl=Pin(7), freq=100000)

lcd = I2cLcd(i2c, LCD_ADDR, num_lines=2, num_columns=16)


city = center_16("BOSTON")

zone = center_16("UTC-5 (ET)")


lcd.clear()

lcd.move_to(0, 0)

lcd.putstr(city)

lcd.move_to(0, 1)

lcd.putstr(zone)


return lcd


def setup_london():

"""LCD #5 – London on SoftI2C (GP8/GP9)."""

i2c = SoftI2C(sda=Pin(8), scl=Pin(9), freq=100000)

lcd = I2cLcd(i2c, LCD_ADDR, num_lines=2, num_columns=16)


city = center_16("LONDON")

zone = center_16("UTC+0 (GMT)")


lcd.clear()

lcd.move_to(0, 0)

lcd.putstr(city)

lcd.move_to(0, 1)

lcd.putstr(zone)


return lcd


def setup_tokyo():

"""LCD #6 – Tokyo on SoftI2C (GP10/GP11)."""

i2c = SoftI2C(sda=Pin(10), scl=Pin(11), freq=100000)

lcd = I2cLcd(i2c, LCD_ADDR, num_lines=2, num_columns=16)


city = center_16("TOKYO")

zone = center_16("UTC+9 (JST)")


lcd.clear()

lcd.move_to(0, 0)

lcd.putstr(city)

lcd.move_to(0, 1)

lcd.putstr(zone)


return lcd


def main():

setup_seattle()

time.sleep_ms(100)


setup_denver()

time.sleep_ms(100)


setup_omaha()

time.sleep_ms(100)


setup_boston()

time.sleep_ms(100)


setup_london()

time.sleep_ms(100)


setup_tokyo()

time.sleep_ms(100)


# Keep the MCU alive; displays stay static

while True:

time.sleep(1)


main()



Now:


Save as main.py on the Pico.


Unplug and replug USB.


All six LCDs should come up with city/timezone labels on boot.