H-Bridge Unit v1.1 + M5Dial: possible library bug, HPWR issues & safety note — test report (please correct me if I'm wrong)
Hardware: M5Stack Dial v1 (ESP32-S3) + M5Stack H-Bridge Unit v1.1 (STM32F030 + RZ7899, FW v2)
Arduino Core: ESP32 3.3.7 | M5Unified: 0.2.15 | M5GFX: 0.2.21 | M5UnitHbridge: 1.0.0
Overview
We spent several sessions trying to get the H-Bridge Unit v1.1 working reliably with the M5Stack Dial v1. What started as a simple "connect and drive a motor" task turned into a systematic investigation of all possible port, voltage, and I2C combinations.
This post documents the full test matrix with serial output and current measurements. We observed three things that surprised us — please read carefully and let us know if we misunderstood something or if there is a known solution we missed:
The official M5UnitHbridge library does not seem to work with this hardware — we believe it is due to a missing Repeated Start condition in its I2C read implementation, but we may be wrong.
Wire.begin() must be called after M5Dial.begin() — contrary to what you might expect, this re-configures the hardware I2C peripheral to Port A and works correctly. Is this intended behavior?
HPWR mode does not behave as expected — forward direction produces no output, backward direction appears limited to ~9% effective duty cycle regardless of speed value. We tried PWM frequency changes, common GND, different ports and I2C methods — all without improvement. Could this be a firmware issue, or are we missing a required initialization step?
We hope this is useful to others, and we very much welcome corrections or explanations from M5Stack or the community.
TL;DR
Getting the H-Bridge Unit v1.1 working with M5Dial is not straightforward. This post documents a complete test matrix covering all port/voltage/I2C combinations. The short version:
5V mode + Hardware I2C (Wire) + correct registers = works, but USB power limits motor load to ~500mA
M5UnitHbridge library = broken for this hardware (wrong I2C read protocol, motor does not respond)
HPWR mode appears non-functional in FW v2: forward channel dead, backward works at fixed ~9% duty cycle regardless of speed value — firmware bug suspected
Hardware Setup
Item
Detail
Controller
M5Stack Dial v1 (ESP32-S3)
Driver
M5Stack H-Bridge Unit v1.1 (STM32F030 + RZ7899, FW v2)
Grove Port A
SDA=G13, SCL=G15
Grove Port B
SDA=G2, SCL=G1
HPWR supply
13.5V @ external terminals, 470µF cap across VIN+/GND
Key Discovery 1: Wire.begin() After M5Dial.begin()
M5Dial.begin() initializes the internal I2C bus (touch controller) on G11/G12.
Wire after M5Dial.begin() → internal bus, not Port A
Wire1.begin(13, 15) → G15 held LOW by touch controller → SCL blocked
✅ Solution: Call Wire.begin(13, 15) after M5Dial.begin() — this re-configures the hardware I2C peripheral to Port A pins and works correctly
auto cfg = M5.config();
M5Dial.begin(cfg, false, false);
// ...
Wire.begin(13, 15);
Wire.setClock(100000);
Key Discovery 2: Correct Register Map
The H-Bridge register layout differs from various online sources. Verified by M5UnitHbridge library source:
Register
Address
Description
REG_DIR
0x00
Direction: 0=STOP, 1=FORWARD, 2=BACKWARD
REG_SPEED8
0x01
Speed 8-bit (0–255)
REG_SPEED16
0x02
Speed 16-bit little-endian
REG_PWM_FREQ
0x04
PWM frequency little-endian
REG_CURRENT
0x30
Motor current IEEE-754 float, 4 bytes (v1.1 only)
REG_FW
0xFE
Firmware version (uint8)
Key Discovery 3: M5UnitHbridge Library Broken for This Hardware
The official M5UnitHbridge library uses endTransmission() (with STOP condition) before requestFrom():
// Library readBytes() — BROKEN for H-Bridge v1.1
_wire->beginTransmission(addr);
_wire->write(reg);
_wire->endTransmission(); // ← sends STOP
_wire->requestFrom(addr, length); // ← new START (not Repeated Start)
The STM32F030 in the H-Bridge requires Repeated Start between write and read. Without it:
getFirmwareVersion() returns 255 instead of 2
Write operations appear to succeed but the motor does not respond
✅ Solution: Use Wire directly with endTransmission(false) for reads:
Wire.beginTransmission(HBRIDGE_ADDR);
Wire.write(reg);
Wire.endTransmission(false); // Repeated Start — NO stop condition
Wire.requestFrom(HBRIDGE_ADDR, len);
Complete Test Matrix
#
Port
Pins
Power
I2C
Result
Notes
1a
COM A
G13/G15
5V
Hardware Wire
✅ Motor runs
Brownout at ~78% load — USB 5V insufficient for full load
1b
COM A
G13/G15
5V
M5UnitHbridge lib
❌ No movement
FW=255, writes silent. Library incompatible.
2a
COM A
G13/G15
5V
Bit-Banging
✅ Motor runs
Brownout at ~24% — GPIO switching adds extra current spikes
2b
COM B
G2/G1
5V
Bit-Banging
✅ Motor twitches
Brownout at ~25%, same as 2a. Port makes no difference.
3a
COM A
G13/G15
HPWR
Hardware Wire
❌ Forward only ~4mV
FW=2 ✓, writes accepted (readback confirmed), forward channel inactive
3b
COM A
G13/G15
HPWR
Bit-Banging
⚡ Asymmetric
Forward ❌ (4mV, inactive). Backward ✅ (-1.2V, 128mA, clean ramp to 255 and back)
4
COM B
G2/G1
HPWR
Bit-Banging
❌ No movement
Same HPWR issue, FW=2 ✓, readback correct, 79mA draw, 4mV output
Serial Console Excerpts
Test 1a — 5V, Hardware Wire, correct registers (motor runs, brownout at ~78%)
FW-Version: 2
[1] Vorwaerts: Rampe 0->255 (100ms/Schritt)
write reg=0x00 val=1 ... err=0
write reg=0x01 val=0 ... err=0
spd= 0
...
write reg=0x01 val=198 ... err=0
spd=198
E BOD: Brownout detector was triggered
Test 1b — 5V, M5UnitHbridge library (motor does not respond)
begin: OK
FW-Version: 255 ← should be 2 — Repeated Start missing in library
[1] Vorwaerts: Rampe 0->255 (100ms/Schritt)
spd= 0 ... spd=255
[2] Vorwaerts: halte 100% fuer 3s
...
=== Fertig ===
← no brownout, no motor current, motor never moved
Test 4 — HPWR, Bit-Banging, with register readback (motor does not respond)
FW-Version: 2 (read OK)
[1] Readback-Test: dir=1 spd=128
Readback: dir=1 (soll=1) spd=128 (soll=128) ← registers confirmed correct
[2] Vorwaerts: Rampe 0->255 (100ms/Schritt)
spd=0 ... spd=255
[3] Vorwaerts: halte 100% fuer 3s
Motor dreht nicht
Messungen: 13.5V confirmed, 79mA from supply, 4mV at motor terminals
Test 3b — HPWR, Bit-Banging, COM A (asymmetric: forward dead, backward works)
FW-Version: 2
[1] Vorwaerts: Rampe 0->255 → Motor does NOT turn, ~4mV at terminals
[5] Rueckwaerts: Rampe 0->255 → Motor RUNS, -1.2V at terminals, 128mA from supply
(~9% duty cycle at 13.5V, regardless of speed value)
[7] Rueckwaerts: Rampe 255->0 → Motor slows cleanly and stops at spd=0
Test 5 — HPWR, register snapshot (default PWM frequency)
[R] Register-Snapshot (Firmware-Defaults):
0x00 DIR = 0 (0=STOP,1=FWD,2=BWD)
0x01 SPD8 = 0
0x02 SPD16 = 0
0x04 FREQ = 1000 Hz ← default is audible range — explains motor beeping
HPWR Mode — Appears Non-Functional in Firmware v2
After extensive testing across all port/I2C combinations, our conclusion is that HPWR mode is functionally broken in firmware v2.
Additional tests (Tests 5 & 6)
Default PWM frequency: 1000 Hz (register 0x04) — audible range, explains the motor "beeping"
Setting frequency to 10000 Hz: no improvement, forward direction still inactive
Connecting external supply GND to Grove GND: no improvement
HPWR test results across all combinations
Test
Port
I2C
Forward
Backward
Notes
3a
COM A
Hardware Wire
✗ 4mV
n/a
Writes confirmed OK via err=0
3b
COM A
Bit-Banging
✗ 4mV
✓ -1.2V / 128mA
Asymmetric — only backward works
4
COM B
Bit-Banging
✗ 4mV
n/a
Readback confirmed dir/spd correct
Asymmetric behavior detail (Test 3b)
Forward (dir=1):
~4mV at motor terminals regardless of speed value
Brief 120mA spike from supply at spd=0–10, drops to 80µA by spd=40
Motor does not turn
Backward (dir=2):
-1.2V at motor terminals — only ≈9% effective duty cycle at 13.5V supply, regardless of speed value (0–255)
128mA from external supply
Motor runs through full ramp 0→255→0 and stops correctly
Requires a manual push past static friction at low speeds
Evidence pointing to firmware bug
I2C communication confirmed working (FW=2 readable, register readback correct)
Speed value has no effect on output voltage in HPWR mode (9% fixed regardless of spd=1 or spd=255)
PWM frequency change has no effect
GND connection has no effect
Issue present on both COM A and COM B
Issue present with both Hardware I2C and Bit-Banging
5V mode works correctly on the same hardware
Our interpretation: The STM32F030 firmware v2 may not properly implement HPWR mode — the DIP switch position appears to be detected but the RZ7899 drive logic may not be correctly configured for external supply operation. However, we acknowledge we could be missing a required initialization step or configuration. We would greatly appreciate clarification from M5Stack or anyone who has successfully used HPWR mode.
Working Configuration (Minimal Code)
#include <M5Dial.h>
#define HBRIDGE_ADDR 0x20
#define REG_DIR 0x00
#define REG_SPEED8 0x01
bool hb_write(uint8_t reg, uint8_t val) {
Wire.beginTransmission(HBRIDGE_ADDR);
Wire.write(reg);
Wire.write(val);
return Wire.endTransmission() == 0;
}
bool hb_read(uint8_t reg, uint8_t* out) {
Wire.beginTransmission(HBRIDGE_ADDR);
Wire.write(reg);
Wire.endTransmission(false); // Repeated Start
return Wire.requestFrom((uint8_t)HBRIDGE_ADDR, (uint8_t)1) == 1
&& (*out = Wire.read(), true);
}
void setup() {
auto cfg = M5.config();
M5Dial.begin(cfg, false, false);
Wire.begin(13, 15); // COM A, after M5Dial.begin()
Wire.setClock(100000);
// DIP switch: 5V mode — HPWR forward channel does not work
hb_write(REG_DIR, 1); // FORWARD
hb_write(REG_SPEED8, 128); // 50%
}
⚠️ Safety Note: H-Bridge Runs Independently After ESP32 Reset
The STM32F030 inside the H-Bridge has a lower minimum operating voltage than the ESP32-S3. This means:
Scenario 1 — Brownout:
The ESP32-S3 crashes (brownout at ~700mA from Grove 5V). The STM32F030 keeps running and holds the last direction + speed. The motor continues turning until power is physically removed.
Scenario 2 — Reset button:
Pressing the M5Dial reset button resets the ESP32-S3, but the STM32F030 is unaffected. Motor keeps running.
Scenario 3 — Software crash:
Any ESP32-S3 crash leaves the H-Bridge in its last state.
Safe practice — always send STOP as the first action in setup():
Wire.begin(13, 15);
Wire.setClock(100000);
Wire.beginTransmission(HBRIDGE_ADDR);
Wire.write(0x00); Wire.write(0); // DIR = STOP
Wire.endTransmission();
Wire.beginTransmission(HBRIDGE_ADDR);
Wire.write(0x01); Wire.write(0); // SPEED = 0
Wire.endTransmission();
// ... rest of setup
Current Measurements (5V Mode, Grove Side)
State
Motor Voltage
Grove 5V Current
Idle (M5Dial + H-Bridge logic)
—
~300mA
Motor running (after manual push)
3.3V
~700mA
Brownout threshold (USB)
—
~500mA
Note: USB 2.0 limit is 500mA — sustained motor load exceeds this. Use a powered USB hub or a 5V supply with higher current rating. Do not rely on HPWR mode — it is non-functional in FW v2.
Summary
Issue
Root Cause
Fix
Wire on Port A silent
M5Dial.begin() uses Wire on G11/G12
Call Wire.begin(13,15) after M5Dial.begin()
Wire1 SCL blocked
Touch controller holds G15 LOW
Don't use Wire1
M5UnitHbridge FW=255
Library uses STOP instead of Repeated Start
Use Wire directly with endTransmission(false)
5V mode brownout
USB 5V insufficient for motor load >~500mA
Use powered USB hub or higher-current 5V supply
HPWR mode non-functional
STM32F030 FW v2 bug — forward dead, backward fixed at ~9% duty cycle regardless of speed
No workaround — requires firmware fix from M5Stack
Tested May 2026 on M5Stack Dial v1 (ESP32-S3) + H-Bridge Unit v1.1 (STM32F030 + RZ7899, FW v2) / Arduino IDE 2.3.8 / ESP32 Core 3.3.7 / M5Unified 0.2.15
Appendix: Full Serial Output per Test
Test 1a — COM A | 5V | Hardware Wire | correct registers
=== Test 1a: Wire direkt, korrekte Register ===
FW-Version: 2
[1] Vorwaerts: Rampe 0->255 (100ms/Schritt)
write reg=0x00 val=1 ... err=0
write reg=0x01 val=0 ... err=0
spd= 0
write reg=0x01 val=198 ... err=0
spd=198
E BOD: Brownout detector was triggered
Lauf mit Ammeter (Grove-Seite):
write reg=0x01 val=52 ... err=0
spd= 52
E BOD: Brownout detector was triggered
Strom: ~300mA Ruhestrom, ~700mA beim Laufen (nach Anschieben), 3.3V an Motor
Test 1b — COM A | 5V | M5UnitHbridge Library
=== Test 1b: M5UnitHbridge Library ===
begin: OK
FW-Version: 255 ← should be 2
[1] Vorwaerts: Rampe 0->255 (100ms/Schritt)
spd= 0 ... spd=255
[2] Vorwaerts: halte 100% fuer 3s
[3] Vorwaerts: Rampe 255->0
[4] STOP ... [8] STOP
=== Fertig ===
← no brownout, no motor current, motor never moved
Test 2a — COM A | 5V | Bit-Banging
=== Test 2a: COM A | 5V | Bit-Banging ===
[0] I2C Scan... Gefunden: 0x20 <- H-Bridge
FW-Version: 2
[1] Vorwaerts: Rampe 0->255 (100ms/Schritt)
spd= 0 ... spd= 62
E BOD: Brownout detector was triggered
← Motor dreht. Brownout bei spd=62 (~24%). GPIO-Umschalten erhoht Strombedarf.
Test 2b — COM B | 5V | Bit-Banging
=== Test 2b: COM B | 5V | Bit-Banging ===
SDA=G2 (gelb), SCL=G1 (weiss)
[0] I2C Scan... Gefunden: 0x20 <- H-Bridge
FW-Version: 2 (read OK)
[1] Vorwaerts: Rampe 0->255 (100ms/Schritt)
spd= 0 ... spd= 63
E BOD: Brownout detector was triggered
← Motor hat gezuckt, Spannung messbar. Brownout bei spd=63, identisch mit 2a.
Test 3a — COM A | HPWR | Hardware Wire
=== Test 3a: COM A | HPWR | Wire direkt ===
SDA=G13, SCL=G15
FW-Version: 2
[1] Vorwaerts: Rampe 0->255 (100ms/Schritt)
write reg=0x00 val=1 ... err=0
write reg=0x01 val=0 ... err=0
spd= 0 ... spd=255
[2] Vorwaerts: halte 100% fuer 3s
[3] Vorwaerts: Rampe 255->0
=== Fertig ===
Messungen: 13.5V OK, 0.083mA aus Netzteil, Motor dreht nicht.
Test 3b — COM A | HPWR | Bit-Banging
=== Test 3b: COM A | HPWR | Bit-Banging ===
SDA=G13, SCL=G15
[0] I2C Scan... Gefunden: 0x20 <- H-Bridge
FW-Version: 2
[1] Vorwaerts: Rampe 0->255 → Motor dreht NICHT, ~4mV, 120mA-Spike bei spd=0..10
[5] Rueckwaerts: Rampe 0->255 → Motor dreht ✓, -1.2V, 128mA aus Netzteil
[7] Rueckwaerts: Rampe 255->0 → Motor haelt sauber bei spd=0
=== Fertig ===
Asymmetrie: Vorwaerts tot, Rueckwaerts ~9% eff. Duty Cycle bei 13.5V
Test 4 — COM B | HPWR | Bit-Banging
=== Test 4: COM B | HPWR | Bit-Banging ===
SDA=G2 (gelb), SCL=G1 (weiss)
[0] I2C Scan... Gefunden: 0x20 <- H-Bridge
FW-Version: 2 (read OK)
[1] Readback-Test: dir=1 spd=128
Readback: dir=1 (soll=1) spd=128 (soll=128) ← Register korrekt geschrieben
[2] Vorwaerts: Rampe 0->255 ... Motor dreht nicht
Messungen: 13.5V OK, 79mA aus Netzteil, 4mV an Motor.
Test 5 — COM A | HPWR | Bit-Banging | PWM-Frequenz auslesen
=== Test 5: COM A | HPWR | Bit-Banging | FreqRead ===
SDA=G13, SCL=G15
[0] I2C Scan... Gefunden: 0x20 <- H-Bridge
FW-Version: 2
[R] Register-Snapshot (Firmware-Defaults):
0x00 DIR = 0 (0=STOP,1=FWD,2=BWD)
0x01 SPD8 = 0
0x02 SPD16 = 0
0x04 FREQ = 1000 Hz ← default PWM frequency
Motor dreht nicht.
Test 6 — COM A | HPWR | Bit-Banging | PWM-Frequenz auf 10kHz gesetzt
=== Test 6: COM A | HPWR | Bit-Banging | FreqSet 10kHz ===
SDA=G13, SCL=G15
[F] Setze PWM-Frequenz auf 10000 Hz...
FREQ nach Set = 10000 Hz ← write accepted
[R] Register-Snapshot:
0x04 FREQ = 10000 Hz
[1] Vorwaerts: Rampe 0->255 → Motor dreht nicht. Frequenz hat keinen Einfluss.