StackChan

Support the following products:

StackChan

UiFlow2 Example

Servo zero calibration

Note

Mechanical assembly varies between units. After flashing new firmware, calibrate the servo zero reference manually.

Open the stackchan_servo_zero_calibrate.m5f2 project in UiFlow2.

  1. Run the program.

  2. Move the head by hand: on X, align the display with the base orientation; on Y, set the display perpendicular to the base.

  3. Tap Save button.

UiFlow2 Code Block:

stackchan_servo_zero_calibrate.png

Example output:

None

Servo angle read

Open the stackchan_servo_read_example.m5f2 project in UiFlow2.

This example demonstrates reading X and Y servo angles in degrees with torque disabled so the head can move freely.

Note

Torque on holds the last target and resists moving by hand. Torque off lets you pose the head freely while readings update—handy for checking calibration.

UiFlow2 Code Block:

stackchan_servo_read_example.png

Example output:

None

Servo control

Open the stackchan_servo_control_example.m5f2 project in UiFlow2.

This example demonstrates moving the servos to commanded positions and driving the X servo in PWM mode using set_servo_angle and set_servo_x_pwm.

UiFlow2 Code Block:

stackchan_servo_control_example.png

Example output:

None

Face tracking

Open the stackchan_face_tracking_example.m5f2 project in UiFlow2.

This demo implements face tracking.

UiFlow2 Code Block:

stackchan_face_tracking_example.png

Example output:

None

Servo power info

Open the stackchan_servo_power_example.m5f2 project in UiFlow2.

This example demonstrates read and display servo power information.

UiFlow2 Code Block:

stackchan_servo_power_example.png

Example output:

None

Touch & RGB

Open the stackchan_tp_rgb_example.m5f2 project in UiFlow2.

This example demonstrates mapping touch zones to RGB strip colours (three logical touch points on two strips).

UiFlow2 Code Block:

stackchan_tp_rgb_example.png

Example output:

None

NFC

Open the stackchan_nfc_detect_example.m5f2 project in UiFlow2.

This example demonstrates detecting NFC tags and displaying UID and tag type on screen. For the full NFC Unit API reference (detect, read/write, tag types, etc.), see NFC Unit.

UiFlow2 Code Block:

stackchan_nfc_detect_example.png

Example output:

None

Infrared (IR)

Open the stackchan_ir_tx_rx_example.m5f2 project in UiFlow2.

This example demonstrates infrared transmit and receive in NEC style.

UiFlow2 Code Block:

stackchan_ir_tx_rx_example.png

Example output:

None

MicroPython Example

Servo zero calibration

Note

Mechanical assembly varies between units. After flashing new firmware, calibrate the servo zero reference manually.

  1. Run the program.

  2. Move the head by hand: on X, align the display with the base orientation; on Y, set the display perpendicular to the base.

  3. Tap Save button.

MicroPython Code Block:

  1# SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
  2#
  3# SPDX-License-Identifier: MIT
  4
  5import os, sys, io
  6import M5
  7from M5 import *
  8import m5ui
  9import lvgl as lv
 10import time
 11from hardware.stackchan import StackChan
 12
 13
 14page0 = None
 15label_title = None
 16button_save = None
 17label_angle_x = None
 18label_angle_y = None
 19label_tip = None
 20stackchan = None
 21x_angle = None
 22y_angle = None
 23last_time = None
 24
 25
 26def button_save_short_clicked_event(event_struct):
 27    global \
 28        page0, \
 29        label_title, \
 30        button_save, \
 31        label_angle_x, \
 32        label_angle_y, \
 33        label_tip, \
 34        stackchan, \
 35        x_angle, \
 36        y_angle, \
 37        last_time
 38    stackchan.set_servo_zero()
 39    label_tip.set_text(str("Tip: Calibration success"))
 40    Speaker.tone(1000, 100)
 41
 42
 43def button_save_event_handler(event_struct):
 44    global \
 45        page0, \
 46        label_title, \
 47        button_save, \
 48        label_angle_x, \
 49        label_angle_y, \
 50        label_tip, \
 51        stackchan, \
 52        x_angle, \
 53        y_angle, \
 54        last_time
 55    event = event_struct.code
 56    if event == lv.EVENT.SHORT_CLICKED and True:
 57        button_save_short_clicked_event(event_struct)
 58    return
 59
 60
 61def setup():
 62    global \
 63        page0, \
 64        label_title, \
 65        button_save, \
 66        label_angle_x, \
 67        label_angle_y, \
 68        label_tip, \
 69        stackchan, \
 70        x_angle, \
 71        y_angle, \
 72        last_time
 73
 74    M5.begin()
 75    Widgets.setRotation(1)
 76    m5ui.init()
 77    page0 = m5ui.M5Page(bg_c=0x000000)
 78    label_title = m5ui.M5Label(
 79        "Servo Calibration",
 80        x=55,
 81        y=5,
 82        text_c=0x0DC9F4,
 83        bg_c=0x000000,
 84        bg_opa=0,
 85        font=lv.font_montserrat_24,
 86        parent=page0,
 87    )
 88    button_save = m5ui.M5Button(
 89        text="Save",
 90        x=128,
 91        y=195,
 92        bg_c=0x2196F3,
 93        text_c=0xFFFFFF,
 94        font=lv.font_montserrat_14,
 95        parent=page0,
 96    )
 97    label_angle_x = m5ui.M5Label(
 98        "X-Axis Servo Angle:",
 99        x=10,
100        y=130,
101        text_c=0x0DC9F4,
102        bg_c=0x000000,
103        bg_opa=0,
104        font=lv.font_montserrat_18,
105        parent=page0,
106    )
107    label_angle_y = m5ui.M5Label(
108        "Y-Axis Servo Angle:",
109        x=8,
110        y=160,
111        text_c=0x0DC9F4,
112        bg_c=0x000000,
113        bg_opa=0,
114        font=lv.font_montserrat_18,
115        parent=page0,
116    )
117    label_tip = m5ui.M5Label(
118        "Tip:Move by hand, tap Save.",
119        x=33,
120        y=70,
121        text_c=0xD2E711,
122        bg_c=0xFFFFFF,
123        bg_opa=0,
124        font=lv.font_montserrat_18,
125        parent=page0,
126    )
127
128    button_save.add_event_cb(button_save_event_handler, lv.EVENT.ALL, None)
129
130    stackchan = StackChan(i2c=1, uart=1)
131    page0.screen_load()
132    stackchan.set_servo_power(enable=True)
133    stackchan.set_servo_torque(stackchan.SERVO_ID_X, enable=False)
134    stackchan.set_servo_torque(stackchan.SERVO_ID_Y, enable=False)
135    Speaker.begin()
136    Speaker.setVolumePercentage(0.6)
137    Speaker.tone(1000, 100)
138
139
140def loop():
141    global \
142        page0, \
143        label_title, \
144        button_save, \
145        label_angle_x, \
146        label_angle_y, \
147        label_tip, \
148        stackchan, \
149        x_angle, \
150        y_angle, \
151        last_time
152    M5.update()
153    if (time.ticks_diff((time.ticks_ms()), last_time)) >= 100:
154        x_angle = stackchan.get_servo_angle(stackchan.SERVO_ID_X)
155        y_angle = stackchan.get_servo_angle(stackchan.SERVO_ID_Y)
156        label_angle_x.set_text(str((str("X-Axis Servo Angle:") + str(x_angle))))
157        label_angle_y.set_text(str((str("Y-Axis Servo Angle:") + str(y_angle))))
158
159
160if __name__ == "__main__":
161    try:
162        setup()
163        while True:
164            loop()
165    except (Exception, KeyboardInterrupt) as e:
166        try:
167            m5ui.deinit()
168            from utility import print_error_msg
169
170            print_error_msg(e)
171        except ImportError:
172            print("please update to latest firmware")

Example output:

None

Servo angle read

This example demonstrates reading X and Y servo angles in degrees with torque disabled so the head can move freely.

Note

Torque on holds the last target and resists moving by hand. Torque off lets you pose the head freely while readings update—handy for checking calibration.

MicroPython Code Block:

 1import os, sys, io
 2import M5
 3from M5 import *
 4import m5ui
 5import lvgl as lv
 6import time
 7from hardware.stackchan import StackChan
 8
 9
10page0 = None
11label_title = None
12label_agnle_x = None
13label_angle_y = None
14stackchan = None
15last_time = None
16x_angle = None
17y_angle = None
18
19
20def setup():
21    global page0, label_title, label_agnle_x, label_angle_y, stackchan, last_time, x_angle, y_angle
22
23    M5.begin()
24    Widgets.setRotation(1)
25    m5ui.init()
26    page0 = m5ui.M5Page(bg_c=0x000000)
27    label_title = m5ui.M5Label(
28        "Servo Read Example",
29        x=34,
30        y=10,
31        text_c=0x0DC9F4,
32        bg_c=0x000000,
33        bg_opa=0,
34        font=lv.font_montserrat_24,
35        parent=page0,
36    )
37    label_agnle_x = m5ui.M5Label(
38        "X-Axis Servo Angle:",
39        x=10,
40        y=80,
41        text_c=0x0DC9F4,
42        bg_c=0x000000,
43        bg_opa=0,
44        font=lv.font_montserrat_24,
45        parent=page0,
46    )
47    label_angle_y = m5ui.M5Label(
48        "Y-Axis Servo Angle:",
49        x=10,
50        y=125,
51        text_c=0x0DC9F4,
52        bg_c=0x000000,
53        bg_opa=0,
54        font=lv.font_montserrat_24,
55        parent=page0,
56    )
57
58    stackchan = StackChan(i2c=1, uart=1)
59    page0.screen_load()
60    stackchan.set_servo_power(enable=True)
61    stackchan.set_servo_torque(stackchan.SERVO_ID_X, enable=False)
62    stackchan.set_servo_torque(stackchan.SERVO_ID_Y, enable=False)
63
64
65def loop():
66    global page0, label_title, label_agnle_x, label_angle_y, stackchan, last_time, x_angle, y_angle
67    M5.update()
68    if (time.ticks_diff((time.ticks_ms()), last_time)) >= 100:
69        last_time = time.ticks_ms()
70        x_angle = stackchan.get_servo_angle(stackchan.SERVO_ID_X)
71        y_angle = stackchan.get_servo_angle(stackchan.SERVO_ID_Y)
72        label_agnle_x.set_text(str((str("X-Axis Servo Angle: ") + str(x_angle))))
73        label_angle_y.set_text(str((str("Y-Axis Servo Angle: ") + str(y_angle))))
74
75
76if __name__ == "__main__":
77    try:
78        setup()
79        while True:
80            loop()
81    except (Exception, KeyboardInterrupt) as e:
82        try:
83            m5ui.deinit()
84            from utility import print_error_msg
85
86            print_error_msg(e)
87        except ImportError:
88            print("please update to latest firmware")

Example output:

None

Servo control

This example demonstrates moving the servos to commanded positions and driving the X servo in PWM mode using set_servo_angle and set_servo_x_pwm.

MicroPython Code Block:

 1# SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
 2#
 3# SPDX-License-Identifier: MIT
 4
 5import os, sys, io
 6import M5
 7from M5 import *
 8import m5ui
 9import lvgl as lv
10import time
11from hardware.stackchan import StackChan
12
13
14page0 = None
15label_title = None
16label_status = None
17stackchan = None
18
19
20def setup():
21    global page0, label_title, label_status, stackchan
22
23    M5.begin()
24    Widgets.setRotation(1)
25    m5ui.init()
26    page0 = m5ui.M5Page(bg_c=0x000000)
27    label_title = m5ui.M5Label(
28        "Servo Control Example",
29        x=20,
30        y=5,
31        text_c=0x0DC9F4,
32        bg_c=0x000000,
33        bg_opa=0,
34        font=lv.font_montserrat_24,
35        parent=page0,
36    )
37    label_status = m5ui.M5Label(
38        "--",
39        x=153,
40        y=115,
41        text_c=0x0DC9F4,
42        bg_c=0xFFFFFF,
43        bg_opa=0,
44        font=lv.font_montserrat_16,
45        parent=page0,
46    )
47
48    stackchan = StackChan(i2c=1, uart=1)
49    page0.screen_load()
50    Speaker.begin()
51    Speaker.setVolumePercentage(0.5)
52    stackchan.set_servo_power(enable=True)
53    stackchan.set_servo_torque(stackchan.SERVO_ID_X, enable=True)
54    stackchan.set_servo_torque(stackchan.SERVO_ID_X, enable=True)
55    stackchan.set_servo_angle(stackchan.SERVO_ID_X, 0, 1000, 0)
56    stackchan.set_servo_angle(stackchan.SERVO_ID_Y, 45, 1000, 0)
57    Speaker.tone(678, 300)
58    time.sleep_ms(2000)
59    label_status.set_text(str("Rotate counterclockwise"))
60    label_status.align_to(page0, lv.ALIGN.CENTER, 0, 0)
61    stackchan.set_servo_x_pwm(-50)
62    time.sleep_ms(3000)
63    label_status.set_text(str("Rotate clockwise"))
64    label_status.align_to(page0, lv.ALIGN.CENTER, 0, 0)
65    stackchan.set_servo_x_pwm(50)
66    time.sleep_ms(3000)
67    label_status.set_text(str("Go back to center"))
68    label_status.align_to(page0, lv.ALIGN.CENTER, 0, 0)
69    stackchan.set_servo_angle(stackchan.SERVO_ID_X, 0, 1000, 0)
70
71
72def loop():
73    global page0, label_title, label_status, stackchan
74    M5.update()
75
76
77if __name__ == "__main__":
78    try:
79        setup()
80        while True:
81            loop()
82    except (Exception, KeyboardInterrupt) as e:
83        try:
84            m5ui.deinit()
85            from utility import print_error_msg
86
87            print_error_msg(e)
88        except ImportError:
89            print("please update to latest firmware")

Example output:

None

Face tracking

This example implements face tracking.

MicroPython Code Block:

  1import os, sys, io
  2import M5
  3from M5 import *
  4import camera
  5import dl
  6import image
  7from hardware.stackchan import StackChan
  8
  9
 10stackchan = None
 11
 12
 13import math
 14
 15img = None
 16dl_detection_result = None
 17dl_detector = None
 18lost_frame = None
 19res = None
 20bbox = None
 21f_x = None
 22f_y = None
 23neutral = None
 24SMOOTH = None
 25f_w = None
 26DEADZONE_NORM = None
 27f_h = None
 28Y_NEUTRAL = None
 29f_cx = None
 30img_cx = None
 31f_cy = None
 32img_cy = None
 33ex = None
 34angle_x = None
 35ey = None
 36angle_y = None
 37x_target = None
 38y_target = None
 39
 40
 41def setup():
 42    global \
 43        stackchan, \
 44        img, \
 45        dl_detection_result, \
 46        dl_detector, \
 47        lost_frame, \
 48        res, \
 49        bbox, \
 50        f_x, \
 51        f_y, \
 52        neutral, \
 53        SMOOTH, \
 54        f_w, \
 55        DEADZONE_NORM, \
 56        f_h, \
 57        Y_NEUTRAL, \
 58        f_cx, \
 59        img_cx, \
 60        f_cy, \
 61        img_cy, \
 62        ex, \
 63        angle_x, \
 64        ey, \
 65        angle_y, \
 66        x_target, \
 67        y_target
 68
 69    M5.begin()
 70    Widgets.setRotation(1)
 71    Widgets.fillScreen(0x222222)
 72
 73    stackchan = StackChan(i2c=1, uart=1)
 74    camera.init(pixformat=camera.RGB565, framesize=camera.QVGA)
 75    dl_detector = dl.ObjectDetector(dl.model.HUMAN_FACE_DETECT)
 76    stackchan.set_servo_power(enable=True)
 77    stackchan.set_servo_torque(stackchan.SERVO_ID_X, enable=True)
 78    stackchan.set_servo_torque(stackchan.SERVO_ID_Y, enable=True)
 79    stackchan.set_servo_angle(stackchan.SERVO_ID_X, 0, 0, 0)
 80    stackchan.set_servo_angle(stackchan.SERVO_ID_Y, 45, 0, 0)
 81    SMOOTH = 0.1
 82    DEADZONE_NORM = 0.06
 83    Y_NEUTRAL = 45
 84    img_cx = 160
 85    img_cy = 120
 86    angle_x = 0
 87    angle_y = Y_NEUTRAL
 88    neutral = True
 89
 90
 91def loop():
 92    global \
 93        stackchan, \
 94        img, \
 95        dl_detection_result, \
 96        dl_detector, \
 97        lost_frame, \
 98        res, \
 99        bbox, \
100        f_x, \
101        f_y, \
102        neutral, \
103        SMOOTH, \
104        f_w, \
105        DEADZONE_NORM, \
106        f_h, \
107        Y_NEUTRAL, \
108        f_cx, \
109        img_cx, \
110        f_cy, \
111        img_cy, \
112        ex, \
113        angle_x, \
114        ey, \
115        angle_y, \
116        x_target, \
117        y_target
118    M5.update()
119    img = camera.snapshot()
120    dl_detection_result = dl_detector.infer(img)
121    if dl_detection_result:
122        lost_frame = 0
123        res = dl_detection_result[0]
124        bbox = res.bbox()
125        f_x = res.x()
126        f_y = res.y()
127        f_w = res.w()
128        f_h = res.h()
129        f_cx = int(f_x + f_w * 0.5)
130        f_cy = int(f_y + f_h * 0.5)
131        ex = (f_cx - img_cx) / img_cx
132        ey = (f_cy - img_cy) / img_cy
133        if math.fabs(ex) < DEADZONE_NORM:
134            ex = 0
135        if math.fabs(ey) < DEADZONE_NORM:
136            ey = 0
137        x_target = min(max(ex * -135, -135), 135)
138        y_target = min(max(Y_NEUTRAL - Y_NEUTRAL * ey, 0), 90)
139        angle_x = angle_x + SMOOTH * (x_target - angle_x)
140        angle_y = angle_y + SMOOTH * (y_target - angle_y)
141        stackchan.set_servo_angle(stackchan.SERVO_ID_X, angle_x, 100, 0)
142        stackchan.set_servo_angle(stackchan.SERVO_ID_Y, angle_y, 100, 0)
143        neutral = False
144        lost_frame = 0
145        img.draw_rectangle(f_x, f_y, f_w, f_h, color=0x6600CC, thickness=3, fill=False)
146    else:
147        lost_frame = (lost_frame if isinstance(lost_frame, (int, float)) else 0) + 1
148        if lost_frame > 20 and not neutral:
149            stackchan.set_servo_angle(stackchan.SERVO_ID_X, 0, 1000, 0)
150            stackchan.set_servo_angle(stackchan.SERVO_ID_Y, 45, 1000, 0)
151            neutral = True
152    M5.Lcd.show(img, 0, 0, 320, 240)
153
154
155if __name__ == "__main__":
156    try:
157        setup()
158        while True:
159            loop()
160    except (Exception, KeyboardInterrupt) as e:
161        try:
162            from utility import print_error_msg
163
164            print_error_msg(e)
165        except ImportError:
166            print("please update to latest firmware")

Example output:

None

Servo power info

This example demonstrates read and display servo power information.

MicroPython Code Block:

  1# SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
  2#
  3# SPDX-License-Identifier: MIT
  4
  5import os, sys, io
  6import M5
  7from M5 import *
  8import m5ui
  9import lvgl as lv
 10import time
 11from hardware.stackchan import StackChan
 12
 13
 14page0 = None
 15label_title = None
 16label_voltage = None
 17label_current = None
 18label_power = None
 19stackchan = None
 20last_time = None
 21volatge = None
 22current = None
 23power = None
 24
 25
 26def setup():
 27    global \
 28        page0, \
 29        label_title, \
 30        label_voltage, \
 31        label_current, \
 32        label_power, \
 33        stackchan, \
 34        last_time, \
 35        volatge, \
 36        current, \
 37        power
 38
 39    M5.begin()
 40    Widgets.setRotation(1)
 41    m5ui.init()
 42    page0 = m5ui.M5Page(bg_c=0x000000)
 43    label_title = m5ui.M5Label(
 44        "Servo Power Info",
 45        x=56,
 46        y=5,
 47        text_c=0x0DC9F4,
 48        bg_c=0x000000,
 49        bg_opa=0,
 50        font=lv.font_montserrat_24,
 51        parent=page0,
 52    )
 53    label_voltage = m5ui.M5Label(
 54        "Voltage:",
 55        x=10,
 56        y=80,
 57        text_c=0x0DC9F4,
 58        bg_c=0x000000,
 59        bg_opa=0,
 60        font=lv.font_montserrat_24,
 61        parent=page0,
 62    )
 63    label_current = m5ui.M5Label(
 64        "Current:",
 65        x=10,
 66        y=115,
 67        text_c=0x0DC9F4,
 68        bg_c=0x000000,
 69        bg_opa=0,
 70        font=lv.font_montserrat_24,
 71        parent=page0,
 72    )
 73    label_power = m5ui.M5Label(
 74        "Power:",
 75        x=25,
 76        y=150,
 77        text_c=0x0DC9F4,
 78        bg_c=0x000000,
 79        bg_opa=0,
 80        font=lv.font_montserrat_24,
 81        parent=page0,
 82    )
 83
 84    stackchan = StackChan(i2c=1, uart=1)
 85    page0.screen_load()
 86
 87
 88def loop():
 89    global \
 90        page0, \
 91        label_title, \
 92        label_voltage, \
 93        label_current, \
 94        label_power, \
 95        stackchan, \
 96        last_time, \
 97        volatge, \
 98        current, \
 99        power
100    M5.update()
101    if (time.ticks_diff((time.ticks_ms()), last_time)) >= 200:
102        last_time = time.ticks_ms()
103        volatge = stackchan.get_battery_voltage()
104        current = stackchan.get_battery_current()
105        power = stackchan.get_battery_power()
106        label_voltage.set_text(str((str("Voltage: ") + str((str(volatge) + str(" V"))))))
107        label_current.set_text(str((str("Current: ") + str((str(current) + str(" A"))))))
108        label_power.set_text(str((str("Power: ") + str((str(power) + str(" W"))))))
109
110
111if __name__ == "__main__":
112    try:
113        setup()
114        while True:
115            loop()
116    except (Exception, KeyboardInterrupt) as e:
117        try:
118            m5ui.deinit()
119            from utility import print_error_msg
120
121            print_error_msg(e)
122        except ImportError:
123            print("please update to latest firmware")

Example output:

None

Touch & RGB

This example demonstrates mapping touch zones to RGB strip colours (three logical touch points on two strips).

MicroPython Code Block:

  1# SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
  2#
  3# SPDX-License-Identifier: MIT
  4
  5import os, sys, io
  6import M5
  7from M5 import *
  8import m5ui
  9import lvgl as lv
 10import time
 11from hardware.stackchan import StackChan
 12
 13
 14page0 = None
 15label_title = None
 16stackchan = None
 17tp = None
 18tp1 = None
 19last_time = None
 20tp2 = None
 21tp3 = None
 22
 23
 24def setup():
 25    global page0, label_title, stackchan, tp, tp1, last_time, tp2, tp3
 26
 27    M5.begin()
 28    Widgets.setRotation(1)
 29    m5ui.init()
 30    page0 = m5ui.M5Page(bg_c=0x000000)
 31    label_title = m5ui.M5Label(
 32        "TP & RGB Strip Example",
 33        x=13,
 34        y=10,
 35        text_c=0x0DC9F4,
 36        bg_c=0x000000,
 37        bg_opa=0,
 38        font=lv.font_montserrat_24,
 39        parent=page0,
 40    )
 41
 42    stackchan = StackChan(i2c=1, uart=1)
 43    page0.screen_load()
 44    last_time = [0, 0, 0]
 45    Speaker.begin()
 46    Speaker.setVolumePercentage(0.5)
 47
 48
 49def loop():
 50    global page0, label_title, stackchan, tp, tp1, last_time, tp2, tp3
 51    M5.update()
 52    tp = stackchan.get_touch()
 53    tp1 = tp[0]
 54    tp2 = tp[1]
 55    tp3 = tp[2]
 56    if tp1:
 57        last_time[0] = time.ticks_ms()
 58        stackchan.set_rgb_color(0, 0, 0x33CC00)
 59        stackchan.set_rgb_color(0, 1, 0x33CC00)
 60        stackchan.set_rgb_color(1, 0, 0x33CC00)
 61        stackchan.set_rgb_color(1, 1, 0x33CC00)
 62        Speaker.tone(700, 50)
 63    else:
 64        if (time.ticks_diff((time.ticks_ms()), (last_time[0]))) > 300:
 65            stackchan.set_rgb_color(0, 0, 0x000000)
 66            stackchan.set_rgb_color(0, 1, 0x000000)
 67            stackchan.set_rgb_color(1, 0, 0x000000)
 68            stackchan.set_rgb_color(1, 1, 0x000000)
 69    if tp2:
 70        last_time[1] = time.ticks_ms()
 71        stackchan.set_rgb_color(0, 2, 0x00CCCC)
 72        stackchan.set_rgb_color(0, 3, 0x00CCCC)
 73        stackchan.set_rgb_color(1, 2, 0x00CCCC)
 74        stackchan.set_rgb_color(1, 3, 0x00CCCC)
 75        Speaker.tone(900, 50)
 76    else:
 77        if (time.ticks_diff((time.ticks_ms()), (last_time[1]))) > 300:
 78            stackchan.set_rgb_color(0, 2, 0x000000)
 79            stackchan.set_rgb_color(0, 3, 0x000000)
 80            stackchan.set_rgb_color(1, 2, 0x000000)
 81            stackchan.set_rgb_color(1, 3, 0x000000)
 82    if tp3:
 83        last_time[2] = time.ticks_ms()
 84        stackchan.set_rgb_color(0, 4, 0x000099)
 85        stackchan.set_rgb_color(0, 5, 0x000099)
 86        stackchan.set_rgb_color(1, 4, 0x000099)
 87        stackchan.set_rgb_color(1, 5, 0x000099)
 88        Speaker.tone(1100, 50)
 89    else:
 90        if (time.ticks_diff((time.ticks_ms()), (last_time[2]))) > 300:
 91            stackchan.set_rgb_color(0, 4, 0x000000)
 92            stackchan.set_rgb_color(0, 5, 0x000000)
 93            stackchan.set_rgb_color(1, 4, 0x000000)
 94            stackchan.set_rgb_color(1, 5, 0x000000)
 95
 96
 97if __name__ == "__main__":
 98    try:
 99        setup()
100        while True:
101            loop()
102    except (Exception, KeyboardInterrupt) as e:
103        try:
104            m5ui.deinit()
105            from utility import print_error_msg
106
107            print_error_msg(e)
108        except ImportError:
109            print("please update to latest firmware")

Example output:

None

NFC

This example demonstrates detecting NFC tags and displaying UID and tag type on screen. For the full NFC Unit API reference (detect, read/write, tag types, etc.), see NFC Unit.

MicroPython Code Block:

  1# SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
  2#
  3# SPDX-License-Identifier: MIT
  4
  5import os, sys, io
  6import M5
  7from M5 import *
  8import m5ui
  9import lvgl as lv
 10import time
 11from hardware.stackchan import StackChan
 12
 13
 14page0 = None
 15label_title = None
 16label_uid = None
 17label_type = None
 18label_size = None
 19stackchan = None
 20card_0 = None
 21card_uid = None
 22new = None
 23card_type = None
 24card_size = None
 25last_time = None
 26
 27
 28def setup():
 29    global \
 30        page0, \
 31        label_title, \
 32        label_uid, \
 33        label_type, \
 34        label_size, \
 35        stackchan, \
 36        card_0, \
 37        card_uid, \
 38        new, \
 39        card_type, \
 40        card_size, \
 41        last_time
 42
 43    M5.begin()
 44    Widgets.setRotation(1)
 45    m5ui.init()
 46    page0 = m5ui.M5Page(bg_c=0x000000)
 47    label_title = m5ui.M5Label(
 48        "NFC Card detect",
 49        x=58,
 50        y=5,
 51        text_c=0x13C2EB,
 52        bg_c=0xFFFFFF,
 53        bg_opa=0,
 54        font=lv.font_montserrat_24,
 55        parent=page0,
 56    )
 57    label_uid = m5ui.M5Label(
 58        "UID:",
 59        x=18,
 60        y=70,
 61        text_c=0xFFFFFF,
 62        bg_c=0xFFFFFF,
 63        bg_opa=0,
 64        font=lv.font_montserrat_16,
 65        parent=page0,
 66    )
 67    label_type = m5ui.M5Label(
 68        "Tyep:",
 69        x=10,
 70        y=100,
 71        text_c=0xFFFFFF,
 72        bg_c=0xFFFFFF,
 73        bg_opa=0,
 74        font=lv.font_montserrat_16,
 75        parent=page0,
 76    )
 77    label_size = m5ui.M5Label(
 78        "Size:",
 79        x=16,
 80        y=130,
 81        text_c=0xFFFFFF,
 82        bg_c=0xFFFFFF,
 83        bg_opa=0,
 84        font=lv.font_montserrat_16,
 85        parent=page0,
 86    )
 87
 88    page0.screen_load()
 89    stackchan = StackChan(i2c=1, uart=1)
 90    Speaker.begin()
 91    Speaker.setVolumePercentage(0.6)
 92
 93
 94def loop():
 95    global \
 96        page0, \
 97        label_title, \
 98        label_uid, \
 99        label_type, \
100        label_size, \
101        stackchan, \
102        card_0, \
103        card_uid, \
104        new, \
105        card_type, \
106        card_size, \
107        last_time
108    M5.update()
109    card_0 = stackchan.nfc.detect()
110    if card_0:
111        card_uid = card_0.uid_str
112        card_type = card_0.type_name
113        card_size = card_0.user_memory
114        label_uid.set_text(str((str("UID: ") + str(card_uid))))
115        label_type.set_text(str((str("Tyep: ") + str(card_type))))
116        label_size.set_text(str((str("Size: ") + str(card_size))))
117        if (time.ticks_diff((time.ticks_ms()), last_time)) >= 3000 or new:
118            last_time = time.ticks_ms()
119            stackchan.set_rgb_color(0x009900)
120            Speaker.tone(1234, 100)
121            time.sleep_ms(100)
122            stackchan.set_rgb_color(0x000000)
123        new = False
124    else:
125        new = True
126
127
128if __name__ == "__main__":
129    try:
130        setup()
131        while True:
132            loop()
133    except (Exception, KeyboardInterrupt) as e:
134        try:
135            m5ui.deinit()
136            from utility import print_error_msg
137
138            print_error_msg(e)
139        except ImportError:
140            print("please update to latest firmware")

Example output:

None

Infrared (IR)

This example demonstrates infrared transmit and receive in NEC style.

MicroPython Code Block:

  1# SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
  2#
  3# SPDX-License-Identifier: MIT
  4
  5import os, sys, io
  6import M5
  7from M5 import *
  8from hardware import IR
  9from hardware.stackchan import StackChan
 10
 11
 12label_title = None
 13label_tx_addr = None
 14label_tx_data = None
 15label_rx_addr = None
 16label_rx_data = None
 17ir = None
 18stackchan = None
 19ir_data = None
 20ir_addr = None
 21tx_data = None
 22ir_tx = None
 23tx_addr = None
 24
 25
 26def ir_rx_event(_data, _addr, _ctrl):
 27    global \
 28        label_title, \
 29        label_tx_addr, \
 30        label_tx_data, \
 31        label_rx_addr, \
 32        label_rx_data, \
 33        ir, \
 34        stackchan, \
 35        ir_data, \
 36        ir_addr, \
 37        tx_data, \
 38        ir_tx, \
 39        tx_addr
 40    ir_data = _data
 41    ir_addr = _addr
 42    label_rx_addr.setText(str((str("RX Addr: ") + str(ir_addr))))
 43    label_rx_data.setText(str((str("RX Data: ") + str(ir_data))))
 44    Speaker.tone(700, 100)
 45
 46
 47def btn_pwr_was_clicked_event(state):
 48    global \
 49        label_title, \
 50        label_tx_addr, \
 51        label_tx_data, \
 52        label_rx_addr, \
 53        label_rx_data, \
 54        ir, \
 55        stackchan, \
 56        ir_data, \
 57        ir_addr, \
 58        tx_data, \
 59        ir_tx, \
 60        tx_addr
 61    tx_data = (tx_data if isinstance(tx_data, (int, float)) else 0) + 1
 62    if tx_data > 255:
 63        tx_data = 0
 64    ir.tx(tx_addr, tx_data)
 65    label_tx_addr.setText(str((str("TX Addr: ") + str(tx_addr))))
 66    label_tx_data.setText(str((str("TX Data: ") + str(tx_data))))
 67
 68
 69def setup():
 70    global \
 71        label_title, \
 72        label_tx_addr, \
 73        label_tx_data, \
 74        label_rx_addr, \
 75        label_rx_data, \
 76        ir, \
 77        stackchan, \
 78        ir_data, \
 79        ir_addr, \
 80        tx_data, \
 81        ir_tx, \
 82        tx_addr
 83
 84    M5.begin()
 85    Widgets.setRotation(1)
 86    Widgets.fillScreen(0x000000)
 87    label_title = Widgets.Label(
 88        "IR TX & RX Example", 41, 5, 1.0, 0x0DC9F4, 0x000000, Widgets.FONTS.Montserrat24
 89    )
 90    label_tx_addr = Widgets.Label(
 91        "TX Addr:", 9, 59, 1.0, 0xFFFFFF, 0x000000, Widgets.FONTS.Montserrat18
 92    )
 93    label_tx_data = Widgets.Label(
 94        "TX Data:", 170, 59, 1.0, 0xFFFFFF, 0x000000, Widgets.FONTS.Montserrat18
 95    )
 96    label_rx_addr = Widgets.Label(
 97        "RX Addr:", 10, 100, 1.0, 0xFFFFFF, 0x000000, Widgets.FONTS.Montserrat18
 98    )
 99    label_rx_data = Widgets.Label(
100        "RX Data:", 170, 100, 1.0, 0xFFFFFF, 0x000000, Widgets.FONTS.Montserrat18
101    )
102
103    BtnPWR.setCallback(type=BtnPWR.CB_TYPE.WAS_CLICKED, cb=btn_pwr_was_clicked_event)
104
105    stackchan = StackChan(i2c=1, uart=1)
106    ir = IR()
107    ir.rx_cb(ir_rx_event)
108    tx_addr = 1
109    tx_data = 0
110    Speaker.begin()
111    Speaker.setVolumePercentage(0.5)
112    label_tx_addr.setText(str((str("TX Addr: ") + str(tx_addr))))
113
114
115def loop():
116    global \
117        label_title, \
118        label_tx_addr, \
119        label_tx_data, \
120        label_rx_addr, \
121        label_rx_data, \
122        ir, \
123        stackchan, \
124        ir_data, \
125        ir_addr, \
126        tx_data, \
127        ir_tx, \
128        tx_addr
129    M5.update()
130    if ir_tx:
131        ir_tx = False
132
133
134if __name__ == "__main__":
135    try:
136        setup()
137        while True:
138            loop()
139    except (Exception, KeyboardInterrupt) as e:
140        try:
141            from utility import print_error_msg
142
143            print_error_msg(e)
144        except ImportError:
145            print("please update to latest firmware")

Example output:

None

API

StackChan

class hardware.stackchan.StackChan

StackChan board driver: SCS serial servos on UART, RGB and servo power on M5IOE1, Si12T touch, INA226 (battery bus), and onboard NFC (ST25R3916) as unit.nfc.NFCUnit.

The class is a singleton; always construct with the same i2c and uart ids.

Parameters:
  • i2c (int) – I2C peripheral id.

  • uart (int) – UART id for the 1 Mbaud servo bus.

After init, the instance exposes nfc (a unit.nfc.NFCUnit—see NFC Unit for the complete API), touch, i2c, and low-level servo (Scscl instance) for advanced use.

Module constants include SERVO_ID_X (1), SERVO_ID_Y (2) and related limits—also available as class attributes on StackChan.

UiFlow2 Code Block:

stackchan_api_init.png

MicroPython Code Block:

from hardware.stackchan import StackChan, SERVO_ID_X, SERVO_ID_Y

sc = StackChan(i2c=1, uart=1)
set_servo_zero()

Save logical zero for both axes into NVS (namespace servo, keys zero_pos_1 / zero_pos_2).

UiFlow2 Code Block:

stackchan_api_set_servo_zero.png

MicroPython Code Block:

sc.set_servo_zero()
set_servo_power(enable=True)

Enable or disable servo rail power via the IO expander.

Parameters:

enable (bool) – Power on or off.

UiFlow2 Code Block:

stackchan_api_set_servo_power.png

MicroPython Code Block:

sc.set_servo_power(True)
set_servo_torque(servo_id, enable=True)

Enable or disable torque on one servo.

Parameters:
  • servo_id (int) – SERVO_ID_X or SERVO_ID_Y.

  • enable (bool) – Torque on or off.

UiFlow2 Code Block:

stackchan_api_set_servo_torque.png

MicroPython Code Block:

sc.set_servo_torque(SERVO_ID_X, True)
set_servo_angle(servo_id, angle_deg, time_ms=10, speed=0)

Move the given servo to angle_deg (degrees). Use about -135°~135° for the X axis (SERVO_ID_X / pan) and 0°~90° for the Y axis (SERVO_ID_Y / tilt).

Parameters:
  • servo_id (int) – SERVO_ID_X or SERVO_ID_Y.

  • angle_deg (float) – Target angle in degrees (-135~135 for X, 0~90 for Y).

  • time_ms (int) – Move time (ms) passed to the controller; 0 means the time parameter does not take effect.

  • speed (int) – User speed 0~100 (mapped to the bus); 0 means the speed parameter does not take effect.

UiFlow2 Code Block:

stackchan_api_set_servo_angle_time.png

stackchan_api_set_servo_angle_speed.png

MicroPython Code Block:

sc.set_servo_angle(SERVO_ID_X, 0.0, 500, 0)
sc.set_servo_angle(SERVO_ID_X, 0.0, 0, 50)
get_servo_angle(servo_id)

Read the servo angle in degrees.

Parameters:

servo_id (int) – SERVO_ID_X or SERVO_ID_Y.

Returns:

Angle in degrees, or None if the read failed.

UiFlow2 Code Block:

stackchan_api_get_servo_angle.png

MicroPython Code Block:

deg = sc.get_servo_angle(SERVO_ID_X)
set_servo_x_pwm(value)

Run the X servo in PWM mode for continuous rotation. User range is -100~100; the sign selects rotation direction, and the magnitude sets drive strength.

Parameters:

value (int) – Signed PWM strength (clamped). Positive and negative values rotate in opposite directions; 0 stops output.

UiFlow2 Code Block:

stackchan_api_set_servo_x_pwm.png

MicroPython Code Block:

sc.set_servo_x_pwm(50)
set_rgb_color(*args)

Set RGB LEDs on the strip.

  • One argument: fill all LEDs with color.

  • Two arguments: strip (0 or 1) and color for that logical strip.

  • Three arguments: strip, index, color for a single LED (strip 1 index order matches the driver).

Returns:

True on success where applicable.

UiFlow2 Code Block:

stackchan_api_set_rgb_color_all.png

stackchan_api_set_rgb_color_strip.png

stackchan_api_set_rgb_color.png

MicroPython Code Block:

sc.set_rgb_color(0x00FF00)
sc.set_rgb_color(0, 0x0000FF)
sc.set_rgb_color(0, 0, 0xFF0000)
get_rgb_color(strip, index)

Get RGB color of a single LED.

Parameters:
  • strip (int) – 0 or 1.

  • index (int) – 0~5 per logical strip.

Returns:

tuple (r, g, b).

UiFlow2 Code Block:

stackchan_api_get_rgb_color.png

MicroPython Code Block:

r, g, b = sc.get_rgb_color(0, 0)
get_touch(index=None)

Read touch state (three logical slots).

Parameters:

index (int) – If None, return a list of three levels; if 0, 1, or 2, return that slot’s level.

Returns:

OUTPUT_NONEOUTPUT_HIGH style values, or None on failure.

UiFlow2 Code Block:

stackchan_api_get_touch_all.png

stackchan_api_get_touch.png

MicroPython Code Block:

tp = sc.get_touch()
one = sc.get_touch(0)
get_battery_voltage()

Bus voltage from the INA226 (volts).

Returns:

float or None if unavailable.

UiFlow2 Code Block:

stackchan_api_get_battery_voltage.png

MicroPython Code Block:

v = sc.get_battery_voltage()
get_battery_current()

Current from the INA226 (A).

Returns:

float or None.

UiFlow2 Code Block:

stackchan_api_get_battery_current.png

MicroPython Code Block:

a = sc.get_battery_current()
get_battery_power()

Power from the INA226 (W), when both voltage and current are valid.

Returns:

float or None.

UiFlow2 Code Block:

stackchan_api_get_battery_power.png

MicroPython Code Block:

p = sc.get_battery_power()