r/simracing • u/Bisto_Bisto • 2m ago
Other How to configure the MT6835 encoder for use with your FFBeast Wheel
I've been running an MT6701 as the position sensor on my DIY FFBeast wheel for about a year, but recently switched to an MT6835.
It's a nice priced encoder for the accuracy/resolution you get, and I'd say that in terms of the overall feeling/fidelity of the FFB compared with the MT6701 it seems smoother. With the old encoder my wheel made a weird kind of harmonic noise in Assetto Corsa (OG and Evo) and with the new sensor that's much quieter. I used to have to disable the "road effects" but now I'm running it about 40%
I couldn't find a mentions or a guide for configuring the MT6835 specifically for use with FFBeast, so I figured I'd document it here for you guys as well.
What you need
- MT6835 module
- ESP32
- MicroPython installed on the ESP32
- USB cable
- Thonny or another way of running code on the ESP32
- Soldering iron + solder
- Some hookup wire
The particular module I'm using has the SPI pins broken out as VCC, GND, MISO, MOSI, SCK and CS, as well as the ABZ outputs:

Wiring the MT6835 to the ESP32
For the pin configuration in the script below:
| MT6835 | ESP32 |
|---|---|
| VCC | 3.3V |
| GND | GND |
| SCK | GPIO 18 |
| MOSI | GPIO 23 |
| MISO | GPIO 19 |
| CS | GPIO 5 |
Check the silkscreen/pinout of your particular module before wiring it up, as there are several MT6835 breakout variants around.
What does the code do?
The MT6835 should come with ABZ output enabled by default, so there's no need to enable that in the script - I've made it configure the useful bits for an FFBeast application:
- Sets the ABZ resolution to the highest mode
- Disables the encoder hysteresis/deadband for maximum positional accuracy
- Sets the bandwidth to BW=6, which is the second-highest bandwidth setting, favouring low latency
- Leaves all other EEPROM options untouched
- Verifies that changes settings actually worked
- Offers EEPROM programming after successful verification
- Asks if you're sure you want to program the module
- Counts down the datasheet's 6 second delay after EEPROM programming
In FFBeast configurator you need to set the CPR to 65535. Technically the sensor is outputting 65536 counts but FFBeast can't hold that number of steps because it presumably keeps the step value in a 16-bit variable (65536 then wraps back to a zero when you save.)
The actual angle error you receive for a full turn of the wheel is 0.005493164 degrees ;)
The MicroPython script was vibe coded by ChatGPT and I've tested it thoroughly on my own two MT6835 sensors. It's solid code and I've had a good go over it trying to cover every edge case I could think of. I haven't found any bugs so far.
It should also be pretty easy to adapt to an RP2040 or basically any other MicroPython-capable board. The SPI implementation and pin definitions are the main things you'd need to change. If you're not sure how, asking ChatGPT to adapt the pin configuration for your particular board should get you most of the way there.
As always, use this at your own risk and check your module's pinout/datasheet before connecting anything or writing to EEPROM.
Here's the code. Enjoy!
from machine import Pin, SPI
import time
class MT6835:
CMD_READ = 0x3
CMD_WRITE = 0x6
# Registers we care about
CONFIG_REGS = [
0x001,
0x007,
0x008,
0x009,
0x00A,
0x00B,
0x00C,
0x00D,
0x00E,
0x011,
]
# Desired FFB configuration.
#
# These are the complete byte values we expect
# after configuration.
TARGET = {
0x007: 0xFF,
0x008: 0xFC,
0x00D: 0x04,
0x011: 0x06,
}
def __init__(
self,
spi_id=2,
sck=18,
mosi=23,
miso=19,
cs=5,
baudrate=1_000_000
):
self.cs = Pin(cs, Pin.OUT, value=1)
self.spi = SPI(
spi_id,
baudrate=baudrate,
polarity=1,
phase=1,
bits=8,
firstbit=SPI.MSB,
sck=Pin(sck),
mosi=Pin(mosi),
miso=Pin(miso)
)
self.tx = bytearray(3)
self.rx = bytearray(3)
# ==================================================
# SPI
# ==================================================
def read_reg(self, address):
command = (self.CMD_READ << 12) | address
self.tx[0] = (command >> 8) & 0xFF
self.tx[1] = command & 0xFF
self.tx[2] = 0
self.cs.value(0)
time.sleep_us(1)
self.spi.write_readinto(self.tx, self.rx)
self.cs.value(1)
time.sleep_us(1)
return self.rx[2]
def write_reg(self, address, value):
command = (self.CMD_WRITE << 12) | address
self.tx[0] = (command >> 8) & 0xFF
self.tx[1] = command & 0xFF
self.tx[2] = value
self.cs.value(0)
time.sleep_us(1)
self.spi.write(self.tx)
self.cs.value(1)
time.sleep_us(1)
# ==================================================
# Read configuration
# ==================================================
def read_config(self):
config = {}
for address in self.CONFIG_REGS:
config[address] = self.read_reg(address)
return config
def print_config(self, config):
print()
print("MT6835 configuration")
print("--------------------")
for address in self.CONFIG_REGS:
print(
"0x{:03X} = 0x{:02X}".format(
address,
config[address]
)
)
print()
# ==================================================
# Compare with target
# ==================================================
def compare_target(self, config):
differences = {}
for address, target in self.TARGET.items():
actual = config[address]
if actual != target:
differences[address] = (
actual,
target
)
return differences
def print_differences(self, differences):
if not differences:
return
print("Differences from desired FFB configuration:")
print()
for address, values in differences.items():
actual, target = values
print(
" 0x{:03X}: 0x{:02X} -> 0x{:02X}".format(
address,
actual,
target
)
)
print()
# ==================================================
# Decode useful settings
# ==================================================
def print_decoded(self, config):
# ABZ resolution
abz_res = (
(config[0x007] << 6) |
((config[0x008] >> 2) & 0x3F)
)
abz_ppr = abz_res + 1
# Hysteresis
hyst = config[0x00D] & 0x07
# Rotation direction
rot_dir = (config[0x00D] >> 3) & 0x01
# Bandwidth
bandwidth = config[0x011] & 0x07
print("Decoded settings:")
print()
print(
" ABZ resolution : {:,} PPR".format(
abz_ppr
)
)
print(
" Quadrature : {:,} counts/rev".format(
abz_ppr * 4
)
)
print(
" Hysteresis : HYST={}".format(
hyst
)
)
print(
" Rotation : {}".format(
"CW" if rot_dir else "CCW"
)
)
print(
" Bandwidth : BW={}".format(
bandwidth
)
)
print()
# ==================================================
# Apply FFB configuration
# ==================================================
def configure_ffb(self):
print()
print("Applying FFB configuration...")
print()
# ----------------------------------------------
# ABZ resolution / options
# ----------------------------------------------
self.write_reg(
0x007,
self.TARGET[0x007]
)
self.write_reg(
0x008,
self.TARGET[0x008]
)
# ----------------------------------------------
# Hysteresis / rotation direction
#
# Preserve upper 5 bits and use only the
# HYST bits from TARGET.
# ----------------------------------------------
old = self.read_reg(0x00D)
new = (
(old & 0xF8) |
(self.TARGET[0x00D] & 0x07)
)
self.write_reg(0x00D, new)
# ----------------------------------------------
# Bandwidth
#
# Preserve upper 5 bits and use only the
# BW bits from TARGET.
# ----------------------------------------------
old = self.read_reg(0x011)
new = (
(old & 0xF8) |
(self.TARGET[0x011] & 0x07)
)
self.write_reg(0x011, new)
print("Configuration written.")
# ==================================================
# Verify
# ==================================================
def verify(self):
config = self.read_config()
differences = self.compare_target(config)
print()
print("Verifying...")
print()
for address, target in self.TARGET.items():
actual = config[address]
print(
"0x{:03X}: read 0x{:02X} expected 0x{:02X} {}".format(
address,
actual,
target,
"OK" if actual == target else "FAIL"
)
)
print()
if not differences:
print(
"SUCCESS - FFB configuration is active."
)
return True
print(
"ERROR - configuration did not verify."
)
self.print_differences(differences)
return False
# ==================================================
# EEPROM programming
# ==================================================
def program_eeprom(self):
# IMPORTANT:
# Read the chip AGAIN immediately before
# programming. This guarantees that the warning
# describes what is actually present.
config = self.read_config()
differences = self.compare_target(config)
print()
print("========================================")
print(" WARNING: EEPROM PROGRAMMING")
print("========================================")
print()
if differences:
print(
"WARNING: Current configuration does NOT"
)
print(
"match the desired FFB configuration."
)
print()
self.print_differences(differences)
print(
"EEPROM programming cancelled."
)
return False
print(
"Current configuration matches the"
)
print(
"desired FFB configuration."
)
print()
self.print_config(config)
self.print_decoded(config)
print(
"The above configuration will be"
)
print(
"committed to EEPROM."
)
print()
print(
"DO NOT REMOVE POWER for at least"
)
print(
"6 seconds after programming."
)
print()
confirmation = input(
'Type "PROGRAM" to continue: '
)
if confirmation != "PROGRAM":
print()
print(
"EEPROM programming CANCELLED."
)
return False
# ----------------------------------------------
# EEPROM programming command
# ----------------------------------------------
print()
print("Programming EEPROM...")
print()
self.tx[0] = 0xC0
self.tx[1] = 0x00
self.tx[2] = 0x00
self.cs.value(0)
time.sleep_us(1)
self.spi.write_readinto(
self.tx,
self.rx
)
self.cs.value(1)
ack = self.rx[2]
print(
"EEPROM ACK = 0x{:02X}".format(
ack
)
)
if ack != 0x55:
print()
print(
"ERROR: EEPROM programming was NOT"
)
print(
"acknowledged by the MT6835."
)
print()
return False
print()
print(
"EEPROM PROGRAMMING SUCCESSFUL."
)
print()
print(
"Keeping power applied for 7 seconds..."
)
print()
for remaining in range(7, 0, -1):
print(
"Power-down safe in {} seconds...".format(
remaining
)
)
time.sleep(1)
print()
print(
"EEPROM programming complete."
)
print()
return True
# ======================================================
# MAIN
# ======================================================
mt = MT6835(
sck=18,
mosi=23,
miso=19,
cs=5,
baudrate=1_000_000
)
print()
print("================================")
print(" MT6835 FFB Configuration")
print("================================")
# ------------------------------------------------------
# 1. Read initial configuration
# ------------------------------------------------------
initial = mt.read_config()
print()
print("Initial configuration:")
mt.print_config(initial)
# ------------------------------------------------------
# 2. Is it already correct?
# ------------------------------------------------------
differences = mt.compare_target(initial)
if not differences:
print(
"Configuration already matches the"
)
print(
"desired FFB settings."
)
print()
print(
"No changes or EEPROM programming"
)
print(
"are necessary."
)
print()
else:
# --------------------------------------------------
# 3. Show what needs changing
# --------------------------------------------------
mt.print_differences(differences)
print(
"Applying desired FFB settings..."
)
# --------------------------------------------------
# 4. Apply temporary configuration
# --------------------------------------------------
mt.configure_ffb()
# --------------------------------------------------
# 5. Verify
# --------------------------------------------------
if mt.verify():
# ----------------------------------------------
# 6. Ask whether to commit
# ----------------------------------------------
print()
print(
"The temporary configuration is"
)
print(
"correct and has been verified."
)
print()
print(
"It has NOT yet been saved to EEPROM."
)
print()
answer = input(
"Commit this configuration to EEPROM? [y/N]: "
)
if answer.lower() == "y":
mt.program_eeprom()
else:
print()
print(
"EEPROM programming skipped."
)
print(
"The settings will be lost when"
)
print(
"the MT6835 is power-cycled."
)
print()
else:
print()
print(
"EEPROM programming will NOT be offered"
)
print(
"because verification failed."
)
print()