ESP32 with TMC2130 stepper driver

For anyone who might be trying to use a TMC2130 stepper motor driver with the ESP32, I’ve got a great library for you. It took me a decent amount of time to pull together.

The code is at https://github.com/chilipeppr/robot-actuator-esp32-v2/tree/master/esp32

tmc2130_spi.lua - Main library file

-- TMC2130 driver
-- Communicating with the TMC2130 happens over SPI. Trinamic expects
-- 40 bits sent in (5 bytes) and then 40 bits are sent back. So,
-- even if you want to just update one flag, you have to send all
-- of the other parameters for that register as well. This library
-- makes all of that easy for you.

local m = {}
-- m = {}

bitstr = require("bitstr_v2")

-- for version 1.2
m.pinSdi = 19
m.pinSck = 18
m.pinCs = 5
m.pinSdo = 23

-- for original 1.0
-- m.pinSdi = 12
-- m.pinSck = 13
-- m.pinCs = 15
-- m.pinSdo = 5

-- for version 1.2
m.pinStep = 22 -- GPIO22, pin36 on esp32-wroom-32d, Orig 2
m.pinDir = 21 -- pin33 on esp32-wroom-32d, Orig 14
m.pinSleep = 4 --17 -- ENN pin28 GPIO17 on esp32-wroom-32d, Orig 15 or 0

-- for original 1.0 design
-- m.pinStep = 2 
-- m.pinDir = 14
-- m.pinSleep = 0 --15

m.IRUN = 1

m.isDebug = false

m._isInitted = false
-- Pass in a table of settings
-- @param tbl.initStepDirEnPins  Defaults to false
-- @param tbl.initAsStealthChop  Defaults to false
-- @param tbl.IRUN  Defaults to 1 for lowest current. Can go to 31.
-- @param tbl.pinStep  Defaults to 22
-- @param tbl.pinDir   Defaults to 21
-- @param tbl.pinEn    Defaults to 4
-- @param tbl.isDebug  Defaults to false. Turn on for extra logging.
-- Example motor.init({initStepDirEnPins=true, initAsStealthChop=true, IRUN=2})
function m.init(tbl)
  
  if m._isInitted then 
    print("TMC2130 already initted")
    return 
  end
  
  m._isInitted = true
  
  if tbl.IRUN ~= nil then m.IRUN = tbl.IRUN end
  if tbl.pinStep ~= nil then m.pinStep = tbl.pinStep end
  if tbl.pinDir ~= nil then m.pinDir = tbl.pinDir end
  if tbl.pinEn ~= nil then m.pinSleep = tbl.pinEn end
  if tbl.isDebug == true then m.isDebug = true end
  
  -- defaults to false
  if tbl.initStepDirEnPins == true then
    
    
    gpio.config({
      gpio= {
        m.pinStep, m.pinSleep, m.pinDir
      },
      dir=gpio.IN_OUT,
      -- pull=gpio.PULL_UP
    })
    gpio.write(m.pinStep, 0)
    gpio.write(m.pinSleep, 0) -- needs to be low for TMC2130 to go into spi mode
    gpio.write(m.pinDir, 1)
    
    -- use pull up approach so the pulse counter can read this
    -- port value without chewing up a 2nd input port
    -- gpio.config( { gpio=m.pinDir, dir=gpio.IN_OUT, pull=gpio.PULL_UP } )

  end
  
  -- Make sure ENABLE pin is set to LOW (not HIGH) correctly for TMC2130 before
  -- trying to do SPI to it, or it won't respond
  
  -- Setup spi bus
  local busmaster_config = {
    sclk = m.pinSck, 
    mosi = m.pinSdi, 
    miso = m.pinSdo
  }
  m.busmaster = spi.master(spi.HSPI, busmaster_config)
  
  local device_config = {
    cs = m.pinCs,
    mode = 3, 
    freq = 16000000/8, --, MSBFIRST, SPI_MODE3,
    -- command_bits = 
    
  }
  m.dev1 = m.busmaster:device(device_config)
  
  -- read GSTAT cuz it can nicely reset stuff
  m.readGSTAT()
  
  -- See if they want stealthchop init
  if tbl.initAsStealthChop then
    m.initAsStealthChop()
  end
  
end

-- Sets up motor for stealthChop operation which is the default
function m.initAsStealthChop()
  --  enables stealthChop (with default PWM_CONF)
  m.writeGCONF({
    en_pwm_mode=true, -- 1: stealthChop voltage PWM mode enabled (depending on velocity thresholds).
    direct_mode=false, 
    stop_enable=false,
    I_scale_analog=1, -- 1: Use voltage supplied to AIN as current reference, 0: Normal operation, use internal reference voltage
    internal_Rsense=0, -- 0: Normal operation, 1: Internal sense resistors. Use current supplied into AIN as reference for internal sense resistor, 
  })
  
  -- IRUN current to run motors at. 0..31, so 10 is 1/3rd of 2a or 600ma
  -- the small steppers expect 150ma, so running it at intense current
  -- IHOLD should be 0 for no current at standstill
  -- IHOLDDELAY is 0..15 Controls the number of clock cycles for motor
  -- power down after a motion as soon as standstill is
  -- detected (stst=1) and TPOWERDOWN has expired.
  -- The smooth transition avoids a motor jerk upon
  -- power down.
  -- : 0x9000061F0A; // IHOLD_IRUN: IHOLD=10, IRUN=31 (max. current), IHOLDDELAY=6
  m.writeIHOLD_IRUN({
    IRUN=m.IRUN, -- 1 to 31
    IHOLD=0, -- 0 is no current at standstill
    IHOLDDELAY=15
  })
  
  -- motor.readCHOPCONF()
  -- : 0xEC000100C3; // CHOPCONF: TOFF=3, HSTRT=4, HEND=1, TBL=2, CHM=0 (spreadCycle)
  -- 
  m.STEPS = 1
  m.writeCHOPCONF({
    intpol=1, 
    MRES=m.STEPFULL, 
    CHM=1, -- chm Chopper Mode. 0 = Standard mode (spreadCycle). 1 = Constant off time with fast decay time. 
    TOFF=3, 
    HSTRT=4, 
    HEND=1, 
    TBL=2, 
    vsense=1 -- vsense=1 allows 55% of current setting for vsense=0
  })
  
  -- Delay before power down in stand still. 255 is 4 seconds. 127 is 2 seconds.
  -- TPOWERDOWN sets the delay time after stand still (stst) of the
  -- motor to motor current power down. Time range is about 0 to
  -- 4 seconds.
  m.writeTPOWERDOWN(127) 
  
  -- turn on freewheeling and pwm_autoscale so we enable automatic current control
  -- this also lets us freewheel with IHOLD=0 
  -- motor.FREEWHEEL_FREEWHEEL or motor.FREEWHEEL_SHORT_LS or motor.FREEWHEEL_SHORT_HS, or FREEWHEEL_NORMAL
  m.writePWMCONF({
    freewheel=m.FREEWHEEL_FREEWHEEL, 
    pwm_autoscale=1
  })
  
  m.enable()
end 

m.DIR_FWD = 1
m.DIR_REV = 0
m._dir = nil
function m.setDir(dir)
  if dir == m.DIR_FWD then
    if m._dir == m.DIR_FWD then
      -- already set. ignore.
      if m.isDebug then print("Dir fwd already set. Ignoring.") end
      return
    end
    gpio.write(m.pinDir,1)
    -- use pull up approach so the pulse counter can read this
    -- port value without chewing up a 2nd input port
    -- gpio.config( { gpio=m.pinDir, dir=gpio.IN, pull=gpio.PULL_UP } )
    m._dir = m.DIR_FWD
    if m.isDebug then print("Set dir fwd") end
  else
    if m._dir == m.DIR_REV then
      -- already set. ignore.
      if m.isDebug then print("Dir rev already set. Ignoring.") end
      return
    end
    gpio.write(m.pinDir,0)
    -- use pull up approach so the pulse counter can read this
    -- port value without chewing up a 2nd input port
    -- gpio.config( { gpio=m.pinDir, dir=gpio.IN, pull=gpio.PULL_DOWN } )
    m._dir = m.DIR_REV
    if m.isDebug then print("Set dir rev") end
  end
end

function m.dirFwd()
  m.setDir(m.DIR_FWD)
end

function m.dirRev()
  m.setDir(m.DIR_REV)
end

function m.dirToggle()
  if m._dir == m.DIR_FWD then
    m.dirRev()
  else
    m.dirFwd()
  end
end

function m.disable()
  gpio.write(m.pinSleep, 1)
end 

function m.enable()
  gpio.write(m.pinSleep, 0)
end

function m.readGCONF()
  -- print("readGCONF")
  local rx = m.send2130(0x00)
  local r = {}
  
  local b 
  
  -- get 5th byte, bits 0 to 7
  b = string.byte(rx,5)
  r.I_scale_analog = bit.isset(b, 0)
  r.internal_Rsense = bit.isset(b, 1)
  r.en_pwm_mode = bit.isset(b, 2)
  r.enc_commutation = bit.isset(b, 3)
  r.shaft = bit.isset(b, 4)
  r.diag0_error = bit.isset(b, 5) 
  r.diag0_otpw = bit.isset(b, 6) 
  r.diag0_stall = bit.isset(b, 7) 
  
  -- get 4th byte, bits 8 to 15
  b = string.byte(rx,4)
  r.diag1_stall = bit.isset(b, 0)
  r.diag1_index = bit.isset(b, 1)
  r.diag1_onstate = bit.isset(b, 2)
  r.diag1_steps_skipped = bit.isset(b, 3)
  r.diag0_int_pushpull = bit.isset(b, 4)
  r.diag1_pushpull = bit.isset(b, 5) 
  r.small_hysteresis = bit.isset(b, 6) 
  r.stop_enable = bit.isset(b, 7) 
  
  -- get 3rd byte, bits 16 to 23
  b = string.byte(rx,4)
  r.direct_mode = bit.isset(b, 0)
  r.test_mode = bit.isset(b, 1)
  
  if m.isDebug then print("GCONF:", sjson.encode(r)) end
  return r

end

The rest is at https://github.com/chilipeppr/robot-actuator-esp32-v2/tree/master/esp32 since this forum doesn’t allow more than 32,000 chars to be posted.

Will this work for TMC2208 in SPI mode also?

The TMC2208 uses UART, not SPI to communicate, and the protocols are different.

I just started trying to get a TMC2208 going as well. I can’t seem to get UART going yet on ESP32. That 1K resistor they have you add to the TMC2208 is an interesting twist. I only had a 1.1K resistor lying around so wondering if it’s that sensitive to the resistance/voltage level and that’s why I couldn’t get comms or if I’m doing something else wrong.

BTW, I’ve blown 6 TMC2130’s now playing around and damn that adds up. I finally started using the silentstepstick protector and that seems to help. Nothing blown so far.

Nvm, I was thinking of something completely different :confounded:

There also is a ESP32 version of GRBL that supports TMC2130 (see https://github.com/bdring/Grbl_Esp32/issues/108)