Support Hardkernel ODROID-Go

Support for Hardkernel ESP32 device: ODROID-Go (The old one from 2018)

* https://github.com/hardkernel/ODROID-GO/
* https://wiki.odroid.com/odroid_go/odroid_go

What worked:

* Display
* Buttons
* Crossbar
* Wifi
* Battery
* blue LED

TODO:

* Speaker

The blue LED is "coupled" with the button/crossbar press.
This commit is contained in:
JensDiemer
2026-02-12 14:02:11 +01:00
parent 06d64b7ac4
commit a6b010b3e0
5 changed files with 357 additions and 17 deletions
@@ -0,0 +1,253 @@
print("odroid_go.py initialization")
# Hardware initialization for Hardkernel ODROID-Go
# https://github.com/hardkernel/ODROID-GO/
# https://wiki.odroid.com/odroid_go/odroid_go
import time
import ili9341
import lcd_bus
import lvgl as lv
import machine
import mpos.ui
from machine import ADC, Pin
from micropython import const
from mpos import InputManager
# Display settings:
SPI_HOST = const(1)
SPI_FREQ = const(40000000)
LCD_SCLK = const(18)
LCD_MOSI = const(23)
LCD_DC = const(21)
LCD_CS = const(5)
LCD_BL = const(32)
LCD_RST = const(33)
LCD_TYPE = const(2) # ILI9341 type 2
TFT_VER_RES = const(320)
TFT_HOR_RES = const(240)
# Button settings:
BUTTON_MENU = const(13)
BUTTON_VOLUME = const(0)
BUTTON_SELECT = const(27)
BUTTON_START = const(39)
BUTTON_B = const(33)
BUTTON_A = const(32)
# The crossbar pin numbers:
CROSSBAR_X = const(34)
CROSSBAR_Y = const(35)
# Misc settings:
LED_BLUE = const(2)
BATTERY_PIN = const(36)
BATTERY_RESISTANCE_NUM = const(2)
SPEAKER_ENABLE_PIN = const(25)
SPEAKER_PIN = const(26)
print("odroid_go.py turn on blue LED")
blue_led = machine.Pin(LED_BLUE, machine.Pin.OUT)
blue_led.on()
print("odroid_go.py machine.SPI.Bus() initialization")
try:
spi_bus = machine.SPI.Bus(host=SPI_HOST, mosi=LCD_MOSI, sck=LCD_SCLK)
except Exception as e:
print(f"Error initializing SPI bus: {e}")
print("Attempting hard reset in 3sec...")
time.sleep(3)
machine.reset()
print("odroid_go.py lcd_bus.SPIBus() initialization")
display_bus = lcd_bus.SPIBus(spi_bus=spi_bus, freq=SPI_FREQ, dc=LCD_DC, cs=LCD_CS)
print("odroid_go.py ili9341.ILI9341() initialization")
try:
mpos.ui.main_display = ili9341.ILI9341(
data_bus=display_bus,
display_width=TFT_HOR_RES,
display_height=TFT_VER_RES,
color_space=lv.COLOR_FORMAT.RGB565,
color_byte_order=ili9341.BYTE_ORDER_BGR,
rgb565_byte_swap=True,
reset_pin=LCD_RST,
reset_state=ili9341.STATE_LOW,
backlight_pin=LCD_BL,
backlight_on_state=ili9341.STATE_PWM,
)
except Exception as e:
print(f"Error initializing ILI9341: {e}")
print("Attempting hard reset in 3sec...")
time.sleep(3)
machine.reset()
print("odroid_go.py display.init()")
mpos.ui.main_display.init(type=LCD_TYPE)
mpos.ui.main_display.set_rotation(lv.DISPLAY_ROTATION._270)
mpos.ui.main_display.set_power(True)
mpos.ui.main_display.set_color_inversion(False)
mpos.ui.main_display.set_backlight(25)
print("odroid_go.py lv.init() initialization")
lv.init()
print("odroid_go.py Battery initialization...")
from mpos import BatteryManager
def adc_to_voltage(adc_value):
return adc_value * BATTERY_RESISTANCE_NUM
BatteryManager.init_adc(BATTERY_PIN, adc_to_voltage)
print("odroid_go.py button initialization...")
button_menu = Pin(BUTTON_MENU, Pin.IN, Pin.PULL_UP)
button_volume = Pin(BUTTON_VOLUME, Pin.IN, Pin.PULL_UP)
button_select = Pin(BUTTON_SELECT, Pin.IN, Pin.PULL_UP)
button_start = Pin(BUTTON_START, Pin.IN, Pin.PULL_UP) # -> ENTER
# PREV <- B | A -> NEXT
button_b = Pin(BUTTON_B, Pin.IN, Pin.PULL_UP)
button_a = Pin(BUTTON_A, Pin.IN, Pin.PULL_UP)
class CrossbarHandler:
# ADC values are around low: ~236 and high ~511
# So the mid value is around (236+511)/2 = 373.5
CROSSBAR_MIN_ADC_LOW = const(100)
CROSSBAR_MIN_ADC_MID = const(370)
def __init__(self, pin, high_key, low_key):
self.adc = ADC(Pin(pin, mode=Pin.IN))
self.adc.width(ADC.WIDTH_9BIT)
self.adc.atten(ADC.ATTN_11DB)
self.high_key = high_key
self.low_key = low_key
def poll(self):
value = self.adc.read()
if value > self.CROSSBAR_MIN_ADC_LOW:
if value > self.CROSSBAR_MIN_ADC_MID:
return self.high_key
elif value < self.CROSSBAR_MIN_ADC_MID:
return self.low_key
class Crossbar:
def __init__(self, *, up, down, left, right):
self.joy_x = CrossbarHandler(CROSSBAR_X, high_key=left, low_key=right)
self.joy_y = CrossbarHandler(CROSSBAR_Y, high_key=up, low_key=down)
def poll(self):
crossbar_pressed = self.joy_x.poll() or self.joy_y.poll()
return crossbar_pressed
# see: internal_filesystem/lib/mpos/indev/mpos_sdl_keyboard.py
# lv.KEY.UP
# lv.KEY.LEFT - lv.KEY.RIGHT
# lv.KEY.DOWN
#
crossbar = Crossbar(
up=lv.KEY.UP, down=lv.KEY.DOWN, left=lv.KEY.LEFT, right=lv.KEY.RIGHT
)
REPEAT_INITIAL_DELAY_MS = 300 # Delay before first repeat
REPEAT_RATE_MS = 100 # Interval between repeats
next_repeat = None # Used for auto-repeat key handling
def input_callback(indev, data):
global next_repeat
current_key = None
if crossbar_pressed := crossbar.poll():
current_key = crossbar_pressed
elif button_menu.value() == 0:
current_key = lv.KEY.ESC
elif button_volume.value() == 0:
print("Volume button pressed -> reset")
machine.reset()
elif button_select.value() == 0:
current_key = lv.KEY.BACKSPACE
elif button_start.value() == 0:
current_key = lv.KEY.ENTER
elif button_b.value() == 0:
current_key = lv.KEY.PREV
elif button_a.value() == 0:
current_key = lv.KEY.NEXT
else:
# No crossbar/buttons pressed
if data.key: # A key was previously pressed and now released
# print(f"Key {data.key=} released")
data.key = 0
data.state = lv.INDEV_STATE.RELEASED
next_repeat = None
blue_led.off()
return
# A key is currently pressed
blue_led.on() # Blink on key press and auto repeat for feedback
current_time = time.ticks_ms()
repeat = current_time > next_repeat if next_repeat else False # Auto repeat?
if repeat or current_key != data.key:
print(f"Key {current_key} pressed {repeat=}")
data.key = current_key
data.state = lv.INDEV_STATE.PRESSED
if current_key == lv.KEY.ESC: # Handle ESC for back navigation
mpos.ui.back_screen()
elif current_key == lv.KEY.RIGHT:
mpos.ui.focus_direction.move_focus_direction(90)
elif current_key == lv.KEY.LEFT:
mpos.ui.focus_direction.move_focus_direction(270)
elif current_key == lv.KEY.UP:
mpos.ui.focus_direction.move_focus_direction(0)
elif current_key == lv.KEY.DOWN:
mpos.ui.focus_direction.move_focus_direction(180)
if not repeat:
# Initial press: Delay before first repeat
next_repeat = current_time + REPEAT_INITIAL_DELAY_MS
else:
# Faster auto repeat after initial press
next_repeat = current_time + REPEAT_RATE_MS
blue_led.off() # Blink the LED, too
group = lv.group_create()
group.set_default()
# Create and set up the input device
indev = lv.indev_create()
indev.set_type(lv.INDEV_TYPE.KEYPAD)
indev.set_read_cb(input_callback)
indev.set_group(
group
) # is this needed? maybe better to move the default group creation to main.py so it's available everywhere...
disp = lv.display_get_default() # NOQA
indev.set_display(disp) # different from display
indev.enable(True) # NOQA
InputManager.register_indev(indev)
print("odroid_go.py finished")
+61 -14
View File
@@ -39,38 +39,85 @@ def single_address_i2c_scan(i2c_bus, address):
Returns:
True if a device responds at the specified address, False otherwise
"""
print(f"Attempt to write a single byte to I2C bus address 0x{address:02x}...")
try:
# Attempt to write a single byte to the address
# This will raise an exception if no device responds
i2c_bus.writeto(address, b'')
i2c_bus.writeto(address, b"")
print("Write test successful")
return True
except OSError:
# No device at this address
except OSError as e:
print(f"No device at this address: {e}")
return False
except Exception as e:
# Handle any other exceptions gracefully
print(f"single_address_i2c_scan: error scanning address 0x{address:02x}: {e}")
print(f"scan error: {e}")
return False
def fail_save_i2c(sda, scl):
from machine import I2C, Pin
print(f"Try to I2C initialized on {sda=} {scl=}")
try:
i2c0 = I2C(0, sda=Pin(sda), scl=Pin(scl))
except Exception as e:
print(f"Failed: {e}")
return None
else:
print("OK")
return i2c0
def check_pins(*pins):
from machine import Pin
print(f"Test {pins=}...")
for pin in pins:
try:
Pin(pin)
except Exception as e:
print(f"Failed to initialize {pin=}: {e}")
return True
print("All pins initialized successfully")
return True
def detect_board():
import sys
if sys.platform == "linux" or sys.platform == "darwin": # linux and macOS
return "linux"
elif sys.platform == "esp32":
from machine import Pin, I2C
print("Detecting ESP32 board by scanning I2C addresses...")
i2c0 = I2C(0, sda=Pin(39), scl=Pin(38))
if single_address_i2c_scan(i2c0, 0x14) or single_address_i2c_scan(i2c0, 0x5D): # "ghost" or real GT911 touch screen
return "matouch_esp32_s3_spi_ips_2_8_with_camera_ov3660"
print("matouch_esp32_s3_spi_ips_2_8_with_camera_ov3660 ?")
if i2c0 := fail_save_i2c(sda=39, scl=38):
if single_address_i2c_scan(i2c0, 0x14) or single_address_i2c_scan(
i2c0, 0x5D
):
# "ghost" or real GT911 touch screen
return "matouch_esp32_s3_spi_ips_2_8_with_camera_ov3660"
i2c0 = I2C(0, sda=Pin(48), scl=Pin(47)) # IO48 is floating on matouch and therefore, using that for I2C will find many devices, so do this after matouch_esp32_s3_spi_ips_2_8_with_camera_ov3660
if single_address_i2c_scan(i2c0, 0x15) and single_address_i2c_scan(i2c0, 0x6B): # CST816S touch screen and IMU
return "waveshare_esp32_s3_touch_lcd_2"
print("waveshare_esp32_s3_touch_lcd_2 ?")
if i2c0 := fail_save_i2c(sda=48, scl=47):
# IO48 is floating on matouch and therefore, using that for I2C will find many devices, so do this after matouch_esp32_s3_spi_ips_2_8_with_camera_ov3660
if single_address_i2c_scan(i2c0, 0x15) and single_address_i2c_scan(
i2c0, 0x6B
):
# CST816S touch screen and IMU
return "waveshare_esp32_s3_touch_lcd_2"
i2c0 = I2C(0, sda=Pin(9), scl=Pin(18))
if single_address_i2c_scan(i2c0, 0x6B): # IMU (plus possibly the Communicator's LANA TNY at 0x38)
return "fri3d_2024"
print("odroid_go ?")
if check_pins(0, 13, 27, 39):
return "odroid_go"
print("fri3d_2024 ?")
if i2c0 := fail_save_i2c(sda=9, scl=18):
# IMU (plus possibly the Communicator's LANA TNY at 0x38)
if single_address_i2c_scan(i2c0, 0x6B):
return "fri3d_2024"
print("Fallback to fri3d_2026")
# default: if single_address_i2c_scan(i2c0, 0x6A): # IMU but currently not installed
return "fri3d_2026"
+23 -3
View File
@@ -2,8 +2,28 @@
# Make sure the storage partition's lib/ is first in the path, so whatever is placed there overrides frozen libraries.
# This allows any build to be used for development as well, just by overriding the libraries in lib/
import gc
import os
import sys
sys.path.insert(0, 'lib')
print(f"Minimal main.py importing mpos.main with sys.path: {sys.path}")
import mpos.main
sys.path.insert(0, "lib")
print(f"{sys.version=}")
print(f"{sys.implementation=}")
print("Check free space on root filesystem:")
stat = os.statvfs("/")
total_space = stat[0] * stat[2]
free_space = stat[0] * stat[3]
used_space = total_space - free_space
print(f"{total_space=} / {used_space=} / {free_space=} bytes")
gc.collect()
print(
f"RAM: {gc.mem_free()} free, {gc.mem_alloc()} allocated, {gc.mem_alloc() + gc.mem_free()} total"
)
print("Passing execution over to mpos.main")
import mpos.main # noqa: F401
+2
View File
@@ -0,0 +1,2 @@
[format]
quote-style = "double"
+18
View File
@@ -14,6 +14,7 @@ if [ -z "$target" ]; then
echo "Example: $0 unix"
echo "Example: $0 macOS"
echo "Example: $0 esp32"
echo "Example: $0 odroid_go"
exit 1
fi
@@ -100,6 +101,23 @@ if [ "$target" == "esp32" ]; then
rm -rf lib/micropython/ports/esp32/build-ESP32_GENERIC_S3-SPIRAM_OCT/
python3 make.py --ota --partition-size=4194304 --flash-size=16 esp32 BOARD=ESP32_GENERIC_S3 BOARD_VARIANT=SPIRAM_OCT DISPLAY=st7789 INDEV=cst816s USER_C_MODULE="$codebasedir"/micropython-camera-API/src/micropython.cmake USER_C_MODULE="$codebasedir"/secp256k1-embedded-ecdh/micropython.cmake USER_C_MODULE="$codebasedir"/c_mpos/micropython.cmake CONFIG_FREERTOS_USE_TRACE_FACILITY=y CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y "$frozenmanifest"
popd
elif [ "$target" == "odroid_go" ]; then
manifest=$(readlink -f "$codebasedir"/manifests/manifest.py)
frozenmanifest="FROZEN_MANIFEST=$manifest"
echo "Note that you can also prevent the builtin filesystem from being mounted by umounting it and creating a builtin/ folder."
# Build for https://wiki.odroid.com/odroid_go/odroid_go
pushd "$codebasedir"/lvgl_micropython/
rm -rf lib/micropython/ports/esp32/build-ESP32_GENERIC_S3-SPIRAM_OCT/
python3 make.py --ota --partition-size=4194304 --flash-size=16 esp32 \
BOARD=ESP32_GENERIC BOARD_VARIANT=SPIRAM DISPLAY=ili9341 \
USER_C_MODULE="$codebasedir"/micropython-camera-API/src/micropython.cmake \
USER_C_MODULE="$codebasedir"/secp256k1-embedded-ecdh/micropython.cmake \
USER_C_MODULE="$codebasedir"/c_mpos/micropython.cmake \
CONFIG_FREERTOS_USE_TRACE_FACILITY=y \
CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y \
CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y \
"$frozenmanifest"
popd
elif [ "$target" == "unix" -o "$target" == "macOS" ]; then
manifest=$(readlink -f "$codebasedir"/manifests/manifest.py)
frozenmanifest="FROZEN_MANIFEST=$manifest"