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.

3 Likes

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.

1 Like

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)

1 Like