Add DRV2605 haptic driver with boot and sleep feedback

Minimal I2C driver for the DRV2605 ERM vibration motor. 117 built-in
effects via the ERM effect library. Named constants for watch use
cases (click, bump, alert, buzz, tick, etc.).

Boot: sharp click on startup. Sleep: soft bump before entering deep
sleep. XL9555 DRV_EN and DISP_EN now explicitly enabled at boot.

Updated Makefile upload-twatch_ultra to set firmware hash after JTAG flash.
This commit is contained in:
GlassOnTin 2026-03-27 17:45:01 +00:00
commit 9f034e8d0d
3 changed files with 151 additions and 1 deletions

136
DRV2605.h Normal file
View file

@ -0,0 +1,136 @@
// DRV2605 Haptic Driver — Minimal I2C driver for T-Watch Ultra
// ERM vibration motor with 117 built-in effects
// EN pin controlled by XL9555 EXPANDS_DRV_EN (port 0, pin 6)
#ifndef DRV2605_H
#define DRV2605_H
#if BOARD_MODEL == BOARD_TWATCH_ULT
#include <Wire.h>
#define DRV2605_ADDR 0x5A
// Registers
#define DRV2605_STATUS 0x00
#define DRV2605_MODE 0x01
#define DRV2605_RTPIN 0x02
#define DRV2605_LIBRARY 0x03
#define DRV2605_WAVESEQ1 0x04
#define DRV2605_GO 0x0C
#define DRV2605_OVERDRIVE 0x0D
#define DRV2605_SUSTAINPOS 0x0E
#define DRV2605_SUSTAINNEG 0x0F
#define DRV2605_BRAKE 0x10
#define DRV2605_FEEDBACK 0x1A
#define DRV2605_CONTROL3 0x1D
// Named effects for watch use cases (ERM Library 1, effects 1-117)
#define HAPTIC_STRONG_CLICK 1 // Strong Click - 100%
#define HAPTIC_MEDIUM_CLICK 2 // Strong Click - 60%
#define HAPTIC_LIGHT_CLICK 3 // Strong Click - 30%
#define HAPTIC_SHARP_CLICK 4 // Sharp Click - 100%
#define HAPTIC_SOFT_BUMP 7 // Soft Bump - 100%
#define HAPTIC_DOUBLE_CLICK 10 // Double Click - 100%
#define HAPTIC_TRIPLE_CLICK 12 // Triple Click - 100%
#define HAPTIC_BUZZ 14 // Strong Buzz - 100%
#define HAPTIC_ALERT 15 // 750ms Alert - 100%
#define HAPTIC_LONG_ALERT 16 // 1000ms Alert - 100%
#define HAPTIC_TICK 4 // Sharp Click (subtle tick)
#define HAPTIC_TRANSITION 47 // Transition Click - 100%
static bool drv2605_ready = false;
static void drv2605_write(uint8_t reg, uint8_t val) {
Wire.beginTransmission(DRV2605_ADDR);
Wire.write(reg);
Wire.write(val);
Wire.endTransmission();
}
static uint8_t drv2605_read(uint8_t reg) {
Wire.beginTransmission(DRV2605_ADDR);
Wire.write(reg);
Wire.endTransmission(false);
Wire.requestFrom((uint8_t)DRV2605_ADDR, (uint8_t)1);
return Wire.available() ? Wire.read() : 0;
}
bool drv2605_init() {
// Probe device
Wire.beginTransmission(DRV2605_ADDR);
if (Wire.endTransmission() != 0) return false;
// Verify chip ID (bits 7:5 of STATUS should be 3 or 7)
uint8_t id = drv2605_read(DRV2605_STATUS) >> 5;
if (id != 3 && id != 7) return false;
// Exit standby
drv2605_write(DRV2605_MODE, 0x00);
// Disable real-time playback input
drv2605_write(DRV2605_RTPIN, 0x00);
// Select ERM mode: clear bit 7 of FEEDBACK register
uint8_t fb = drv2605_read(DRV2605_FEEDBACK);
drv2605_write(DRV2605_FEEDBACK, fb & 0x7F);
// Enable open-loop drive: set bit 5 of CONTROL3
uint8_t ctrl3 = drv2605_read(DRV2605_CONTROL3);
drv2605_write(DRV2605_CONTROL3, ctrl3 | 0x20);
// Select ERM effect library 1
drv2605_write(DRV2605_LIBRARY, 1);
// Clear timing offsets
drv2605_write(DRV2605_OVERDRIVE, 0);
drv2605_write(DRV2605_SUSTAINPOS, 0);
drv2605_write(DRV2605_SUSTAINNEG, 0);
drv2605_write(DRV2605_BRAKE, 0);
// Clear all waveform slots
for (uint8_t i = 0; i < 8; i++) {
drv2605_write(DRV2605_WAVESEQ1 + i, 0);
}
drv2605_ready = true;
return true;
}
// Play a single effect (1-117 from ERM library)
void drv2605_play(uint8_t effect) {
if (!drv2605_ready) return;
drv2605_write(DRV2605_MODE, 0x00); // Internal trigger mode
drv2605_write(DRV2605_WAVESEQ1, effect); // Effect in slot 1
drv2605_write(DRV2605_WAVESEQ1 + 1, 0); // End sequence
drv2605_write(DRV2605_GO, 1); // Start playback
}
// Play a sequence of up to 8 effects
void drv2605_sequence(const uint8_t *effects, uint8_t count) {
if (!drv2605_ready || count == 0) return;
if (count > 8) count = 8;
drv2605_write(DRV2605_MODE, 0x00);
for (uint8_t i = 0; i < count; i++) {
drv2605_write(DRV2605_WAVESEQ1 + i, effects[i]);
}
if (count < 8) {
drv2605_write(DRV2605_WAVESEQ1 + count, 0); // Terminate
}
drv2605_write(DRV2605_GO, 1);
}
// Stop any playing effect
void drv2605_stop() {
if (!drv2605_ready) return;
drv2605_write(DRV2605_GO, 0);
}
// Check if an effect is still playing
bool drv2605_busy() {
if (!drv2605_ready) return false;
return drv2605_read(DRV2605_GO) & 1;
}
#endif // BOARD_MODEL == BOARD_TWATCH_ULT
#endif // DRV2605_H

View file

@ -121,11 +121,13 @@ firmware-twatch_ultra:
arduino-cli compile --log --fqbn "esp32:esp32:esp32s3:CDCOnBoot=cdc" -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=-DBOARD_MODEL=0x45"
upload-twatch_ultra: firmware-twatch_ultra
@echo "Flashing T-Watch Ultra via OpenOCD JTAG..."
@echo "Flashing T-Watch Ultra app via JTAG (no BOOT+RST needed)..."
$(HOME)/.arduino15/packages/esp32/tools/openocd-esp32/v0.12.0-esp32-20230921/bin/openocd \
-s $(HOME)/.arduino15/packages/esp32/tools/openocd-esp32/v0.12.0-esp32-20230921/share/openocd/scripts \
-f board/esp32s3-builtin.cfg \
-c "program_esp build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin 0x10000 verify reset exit"
@sleep 5
rnodeconf /dev/ttyACM4 --firmware-hash $$(./partition_hashes ./build/esp32.esp32.esp32s3/RNode_Firmware.ino.bin)
firmware-lora32_v10: check_bt_buffers
arduino-cli compile --log --fqbn esp32:esp32:ttgo-lora32 -e --build-property "build.partitions=no_ota" --build-property "upload.maximum_size=2097152" --build-property "compiler.cpp.extra_flags=\"-DBOARD_MODEL=0x39\""

View file

@ -20,6 +20,7 @@
#if BOARD_MODEL == BOARD_TWATCH_ULT
#include "XL9555.h"
#include "CO5300.h"
#include "DRV2605.h"
#endif
#define CHANNEL_FIFO_SIZE (CONFIG_UART_BUFFER_SIZE / NUM_CHANNELS)
@ -274,6 +275,11 @@ void setup() {
#if BOARD_MODEL == BOARD_TWATCH_ULT
xl9555_init();
xl9555_enable_lora_antenna();
xl9555_set(EXPANDS_DRV_EN, true); // Enable haptic motor driver
xl9555_set(EXPANDS_DISP_EN, true); // Enable display power gate
delay(10);
drv2605_init();
if (drv2605_ready) drv2605_play(HAPTIC_SHARP_CLICK); // Boot feedback
// Beacon timer wakeup: if we woke from deep sleep via timer,
// take the fast path — init GPS/LoRa only, transmit, sleep again.
@ -1999,6 +2005,12 @@ void loop() {
// Safely shuts down peripherals and enters ESP32 deep sleep.
// Does not return — device reboots on wake.
void twatch_enter_deep_sleep(bool beacon_timer) {
// 0. Haptic feedback before sleep
if (drv2605_ready) {
drv2605_play(HAPTIC_SOFT_BUMP);
delay(150); // Let the motor spin briefly before powering down
}
// 1. Put display controller into sleep mode (must happen before SPI.end)
#if HAS_DISPLAY
co5300_sleep();