Skip to content

Commit 96164fe

Browse files
feat: add M5Stack Core2 board support (#91)
Add hardware support for M5Stack Core2 (ESP32-D0WDQ6-V3): New files: - drivers/power/axp192.py: AXP192 power management IC driver handling voltage regulation (DCDC1/3, LDO2/3), GPIO control (LCD/touch reset, speaker enable), battery monitoring via ADC, and charging control. - mpos/board/m5stack_core2.py: Board initialization including ILI9342C display (SPI), FT6336U capacitive touch (I2C), NS4168 I2S speaker, SPM1423 PDM microphone, MPU6886 IMU, and AXP192-based battery voltage reading. Modified files: - mpos/main.py: Add Core2 detection via AXP192 I2C probe (0x34) in detect_board(). Guard ESP32-S3 specific GPIO probes (pins 10/11, 39/38, 48/47) with chip variant check to prevent WDT reset on ESP32. Hardware peripherals supported: - Display: ILI9342C 320x240 via SPI (SCLK=18, MOSI=23, DC=15, CS=5) - Touch: FT6336U via shared I2C host 0 (SDA=21, SCL=22, addr=0x38) - Power: AXP192 via shared I2C host 0 (addr=0x34) - Audio: NS4168 speaker (I2S: BCLK=12, LRCK=0, DATA=2) - Mic: SPM1423 (PDM: CLK=0, DATA=34) - IMU: MPU6886 via shared I2C host 0 (addr=0x68) - Battery: Read via AXP192 ADC (no dedicated ADC pin needed) All I2C devices share a single hardware I2C(0) controller. LCD reset and backlight are controlled via AXP192 GPIO4 and DCDC3 respectively. Tested on M5Stack Core2 (original version). Signed-off-by: imliubo <[email protected]> Co-authored-by: Thomas Farstrike <[email protected]>
1 parent 14ebe55 commit 96164fe

3 files changed

Lines changed: 515 additions & 45 deletions

File tree

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
# AXP192 Power Management IC Driver for M5Stack Core2
2+
# I2C address: 0x34
3+
# Datasheet reference + M5Stack Core2 specific initialization
4+
5+
from micropython import const
6+
7+
# Registers
8+
_REG_POWER_STATUS = const(0x00)
9+
_REG_CHARGE_STATUS = const(0x01)
10+
_REG_POWER_OUTPUT_CTRL = const(0x12)
11+
_REG_DCDC1_VOLTAGE = const(0x26)
12+
_REG_DCDC3_VOLTAGE = const(0x27)
13+
_REG_LDO23_VOLTAGE = const(0x28)
14+
_REG_VBUS_IPSOUT = const(0x30)
15+
_REG_POWER_OFF = const(0x32)
16+
_REG_CHARGE_CTRL1 = const(0x33)
17+
_REG_BACKUP_CHG = const(0x35)
18+
_REG_PEK_PARAMS = const(0x36)
19+
_REG_ADC_ENABLE1 = const(0x82)
20+
_REG_GPIO0_FUNCTION = const(0x90)
21+
_REG_GPIO0_LDO_VOLTAGE = const(0x91)
22+
_REG_GPIO1_FUNCTION = const(0x92)
23+
_REG_GPIO2_FUNCTION = const(0x93)
24+
_REG_GPIO_SIGNAL = const(0x94)
25+
_REG_GPIO4_FUNCTION = const(0x95)
26+
_REG_GPIO34_SIGNAL = const(0x96)
27+
_REG_COULOMB_CTRL = const(0xB8)
28+
29+
# ADC data registers
30+
_REG_BAT_VOLTAGE_H = const(0x78)
31+
_REG_BAT_CURRENT_IN_H = const(0x7A)
32+
_REG_BAT_CURRENT_OUT_H = const(0x7C)
33+
_REG_APS_VOLTAGE_H = const(0x7E)
34+
35+
# Power output control bits (register 0x12)
36+
_BIT_EXTEN = const(6) # EXTEN (5V boost)
37+
_BIT_DCDC2 = const(4)
38+
_BIT_LDO3 = const(3)
39+
_BIT_LDO2 = const(2)
40+
_BIT_DCDC3 = const(1)
41+
_BIT_DCDC1 = const(0)
42+
43+
# Bus power mode
44+
_MBUS_MODE_INPUT = const(0)
45+
_MBUS_MODE_OUTPUT = const(1)
46+
47+
I2C_ADDR = const(0x34)
48+
49+
50+
class AXP192:
51+
"""AXP192 power management driver for M5Stack Core2."""
52+
53+
def __init__(self, i2c, addr=I2C_ADDR):
54+
self._i2c = i2c
55+
self._addr = addr
56+
self._buf1 = bytearray(1)
57+
self._buf2 = bytearray(2)
58+
59+
def _read_reg(self, reg):
60+
self._buf1[0] = reg
61+
self._i2c.readfrom_mem_into(self._addr, reg, self._buf1)
62+
return self._buf1[0]
63+
64+
def _write_reg(self, reg, val):
65+
self._buf1[0] = val
66+
self._i2c.writeto_mem(self._addr, reg, self._buf1)
67+
68+
def _read_12bit(self, reg):
69+
self._i2c.readfrom_mem_into(self._addr, reg, self._buf2)
70+
return (self._buf2[0] << 4) | self._buf2[1]
71+
72+
def _read_13bit(self, reg):
73+
self._i2c.readfrom_mem_into(self._addr, reg, self._buf2)
74+
return (self._buf2[0] << 5) | self._buf2[1]
75+
76+
def init_core2(self):
77+
"""Initialize AXP192 for M5Stack Core2 hardware."""
78+
# VBUS-IPSOUT path: set N_VBUSEN pin control, auto VBUS current limit
79+
self._write_reg(_REG_VBUS_IPSOUT, (self._read_reg(_REG_VBUS_IPSOUT) & 0x04) | 0x02)
80+
81+
# GPIO1: Open-drain output (Touch RST control)
82+
self._write_reg(_REG_GPIO1_FUNCTION, self._read_reg(_REG_GPIO1_FUNCTION) & 0xF8)
83+
84+
# GPIO2: Open-drain output (Speaker enable control)
85+
self._write_reg(_REG_GPIO2_FUNCTION, self._read_reg(_REG_GPIO2_FUNCTION) & 0xF8)
86+
87+
# RTC battery charge: 3.0V, 200uA
88+
self._write_reg(_REG_BACKUP_CHG, (self._read_reg(_REG_BACKUP_CHG) & 0x1C) | 0xA2)
89+
90+
# Set ESP32 core voltage (DCDC1) to 3350mV
91+
self.set_dcdc1_voltage(3350)
92+
93+
# Set LCD backlight voltage (DCDC3) to 2800mV
94+
self.set_dcdc3_voltage(2800)
95+
96+
# Set LDO2 (LCD logic + SD card) to 3300mV
97+
self.set_ldo2_voltage(3300)
98+
99+
# Set LDO3 (vibration motor) to 2000mV (low to keep motor off initially)
100+
self.set_ldo3_voltage(2000)
101+
102+
# Enable LDO2 (LCD logic power)
103+
self.set_ldo2_enable(True)
104+
105+
# Enable DCDC3 (LCD backlight)
106+
self.set_dcdc3_enable(True)
107+
108+
# Disable LDO3 at startup (vibration motor off)
109+
self.set_ldo3_enable(False)
110+
111+
# Set charging current to 100mA
112+
self.set_charge_current(0) # 0 = 100mA
113+
114+
# GPIO4: NMOS open-drain output (LCD RST)
115+
self._write_reg(_REG_GPIO4_FUNCTION, (self._read_reg(_REG_GPIO4_FUNCTION) & 0x72) | 0x84)
116+
117+
# PEK parameters: power key settings
118+
self._write_reg(_REG_PEK_PARAMS, 0x4C)
119+
120+
# Enable all ADCs
121+
self._write_reg(_REG_ADC_ENABLE1, 0xFF)
122+
123+
# Check power input and configure bus power mode
124+
if self._read_reg(_REG_POWER_STATUS) & 0x08:
125+
self._write_reg(_REG_VBUS_IPSOUT, self._read_reg(_REG_VBUS_IPSOUT) | 0x80)
126+
self._set_bus_power_mode(_MBUS_MODE_INPUT)
127+
else:
128+
self._set_bus_power_mode(_MBUS_MODE_OUTPUT)
129+
130+
# Perform LCD + Touch reset sequence (both share AXP192 GPIO4)
131+
self.set_lcd_reset(False)
132+
import time
133+
time.sleep_ms(100)
134+
self.set_lcd_reset(True)
135+
time.sleep_ms(300) # FT6336U needs ~300ms after reset to be ready
136+
137+
# Enable speaker amp after init
138+
self.set_speaker_enable(True)
139+
140+
# -- Voltage setters --
141+
142+
def set_dcdc1_voltage(self, mv):
143+
"""Set DCDC1 voltage (ESP32 core). Range: 700-3500mV, step 25mV."""
144+
val = max(0, min(127, (mv - 700) // 25))
145+
self._write_reg(_REG_DCDC1_VOLTAGE, (self._read_reg(_REG_DCDC1_VOLTAGE) & 0x80) | val)
146+
147+
def set_dcdc3_voltage(self, mv):
148+
"""Set DCDC3 voltage (LCD backlight). Range: 700-3500mV, step 25mV."""
149+
val = max(0, min(127, (mv - 700) // 25))
150+
self._write_reg(_REG_DCDC3_VOLTAGE, (self._read_reg(_REG_DCDC3_VOLTAGE) & 0x80) | val)
151+
152+
def set_ldo2_voltage(self, mv):
153+
"""Set LDO2 voltage. Range: 1800-3300mV, step 100mV."""
154+
val = max(0, min(15, (mv - 1800) // 100))
155+
self._write_reg(_REG_LDO23_VOLTAGE, (self._read_reg(_REG_LDO23_VOLTAGE) & 0x0F) | (val << 4))
156+
157+
def set_ldo3_voltage(self, mv):
158+
"""Set LDO3 voltage. Range: 1800-3300mV, step 100mV."""
159+
val = max(0, min(15, (mv - 1800) // 100))
160+
self._write_reg(_REG_LDO23_VOLTAGE, (self._read_reg(_REG_LDO23_VOLTAGE) & 0xF0) | val)
161+
162+
# -- Power output enable/disable --
163+
164+
def _set_power_output(self, bit, enable):
165+
reg = self._read_reg(_REG_POWER_OUTPUT_CTRL)
166+
if enable:
167+
reg |= (1 << bit)
168+
else:
169+
reg &= ~(1 << bit)
170+
self._write_reg(_REG_POWER_OUTPUT_CTRL, reg)
171+
172+
def set_dcdc1_enable(self, enable):
173+
self._set_power_output(_BIT_DCDC1, enable)
174+
175+
def set_dcdc3_enable(self, enable):
176+
self._set_power_output(_BIT_DCDC3, enable)
177+
178+
def set_ldo2_enable(self, enable):
179+
self._set_power_output(_BIT_LDO2, enable)
180+
181+
def set_ldo3_enable(self, enable):
182+
self._set_power_output(_BIT_LDO3, enable)
183+
184+
# -- GPIO control (used for peripherals) --
185+
186+
def set_lcd_reset(self, state):
187+
"""Control LCD reset via AXP192 GPIO4."""
188+
data = self._read_reg(_REG_GPIO34_SIGNAL)
189+
if state:
190+
data |= 0x02
191+
else:
192+
data &= ~0x02
193+
self._write_reg(_REG_GPIO34_SIGNAL, data)
194+
195+
def set_touch_reset(self, state):
196+
"""Control touch controller reset via AXP192 GPIO4 (shared with LCD)."""
197+
self.set_lcd_reset(state)
198+
199+
def set_speaker_enable(self, state):
200+
"""Control speaker amplifier enable via AXP192 GPIO2."""
201+
data = self._read_reg(_REG_GPIO_SIGNAL)
202+
if state:
203+
data |= 0x04
204+
else:
205+
data &= ~0x04
206+
self._write_reg(_REG_GPIO_SIGNAL, data)
207+
208+
def _set_bus_power_mode(self, mode):
209+
if mode == _MBUS_MODE_INPUT:
210+
# GPIO0 LDO output, pull up N_VBUSEN to disable 5V from BUS
211+
data = self._read_reg(0x91)
212+
self._write_reg(0x91, (data & 0x0F) | 0xF0)
213+
data = self._read_reg(0x90)
214+
self._write_reg(0x90, (data & 0xF8) | 0x02)
215+
# Enable EXTEN for 5V boost
216+
data = self._read_reg(_REG_POWER_OUTPUT_CTRL)
217+
self._write_reg(_REG_POWER_OUTPUT_CTRL, data | 0x40)
218+
else:
219+
# Disable 5V boost
220+
data = self._read_reg(_REG_POWER_OUTPUT_CTRL)
221+
self._write_reg(_REG_POWER_OUTPUT_CTRL, data & 0xBF)
222+
# GPIO0 floating, external pulldown enables BUS_5V supply
223+
data = self._read_reg(0x90)
224+
self._write_reg(0x90, (data & 0xF8) | 0x01)
225+
226+
# -- Charging --
227+
228+
def set_charge_current(self, level):
229+
"""Set charge current. 0=100mA, 1=190mA, ..., 8=780mA, etc."""
230+
data = self._read_reg(_REG_CHARGE_CTRL1)
231+
data = (data & 0xF0) | (level & 0x0F)
232+
self._write_reg(_REG_CHARGE_CTRL1, data)
233+
234+
# -- Battery / power readings --
235+
236+
def get_battery_voltage(self):
237+
"""Get battery voltage in volts."""
238+
return self._read_12bit(_REG_BAT_VOLTAGE_H) * 1.1 / 1000.0
239+
240+
def get_battery_current(self):
241+
"""Get net battery current in mA (positive=charging, negative=discharging)."""
242+
current_in = self._read_13bit(_REG_BAT_CURRENT_IN_H) * 0.5
243+
current_out = self._read_13bit(_REG_BAT_CURRENT_OUT_H) * 0.5
244+
return current_in - current_out
245+
246+
def is_charging(self):
247+
return bool(self._read_reg(_REG_POWER_STATUS) & 0x04)
248+
249+
def is_vbus_present(self):
250+
return bool(self._read_reg(_REG_POWER_STATUS) & 0x20)
251+
252+
def get_battery_level(self):
253+
"""Estimate battery percentage (simple linear approximation)."""
254+
v = self.get_battery_voltage()
255+
if v < 3.2:
256+
return 0
257+
pct = (v - 3.12) * 100.0
258+
return min(100, max(0, int(pct)))
259+
260+
# -- Screen brightness via DCDC3 --
261+
262+
def set_screen_brightness(self, percent):
263+
"""Set screen brightness 0-100% by adjusting DCDC3 voltage (2500-3300mV)."""
264+
percent = max(0, min(100, percent))
265+
mv = 2500 + int(percent * 8) # 2500mV-3300mV
266+
self.set_dcdc3_voltage(mv)
267+
268+
# -- Power control --
269+
270+
def power_off(self):
271+
"""Cut all power except RTC (LDO1)."""
272+
self._write_reg(_REG_POWER_OFF, self._read_reg(_REG_POWER_OFF) | 0x80)
273+
274+
def set_vibration(self, enable):
275+
"""Enable/disable vibration motor via LDO3."""
276+
self.set_ldo3_enable(enable)

0 commit comments

Comments
 (0)