You've already forked MicroPythonOS
mirror of
https://github.com/m5stack/MicroPythonOS.git
synced 2026-05-20 11:51:27 -07:00
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:
@@ -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")
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user