While designing a library for stepper motors, it bothered me that I had to put stepper driver IC logic across my user code.
To solve that, I made a little driver lib.
First I looked what logic shabaz and I put in our stepper projects, and searched for common activities.
I also looked at the specs of the Allegro A4988 IC, because I'll be using that soon.
Abstract stepper driver base class
With that info at hand, I created a set of classes that provides a minimal interface:
- enable / disable a driver
- initialise it
- tell how many microsteps to take per full step.
If I restrict the code in my firmware to those 3 activities, it's possible to create firmware where there are just a few hardware specific things to modify at declaration.
The rest of the code wouldn't care what driver IC you use.
class stepper_driver { // driver out of sleep as long as object in scope public: virtual bool init() = 0; // configure microsteps / step. return false if not supported virtual bool microsteps(unsigned int microsteps) = 0; virtual void enable(bool enable) = 0; };
There's not much happening. The class is abstract, and has to be subclassed to get something that does activities.
DRV8711 driver class
I could have written a Pico specific DRV8711 child at this moment, but I decided to do this in 2 layers:
A DRV8711 class that's microcontroller independent, but knows DRV8711. In specific the registers, sleep and reset part.
Not putting Pico code at this level, allows that someone that wants to use the driver on a different microcontroller, has this part ready. Only things needed are methods to talk to GPIO and SPI
class drv8711_driver : public stepper_driver { public: virtual bool microsteps(unsigned int microsteps) override { uint16_t reg = read(0x0000); reg &= 0b11111111111111111111111110000111; // clear microsteps uint16_t mode = microsteps_mode(microsteps); mode = mode << 3; reg |= mode; write(reg); return true; } protected: virtual unsigned int microsteps_mode(unsigned int microsteps) { unsigned int mode = true; switch (microsteps) { case 1: mode = 0x0000; break; case 2: mode = 0x0001; break; case 4: mode = 0x0002; break; case 8: mode = 0x0003; break; case 16: mode = 0x0004; break; case 32: mode = 0x0005; break; case 64: mode = 0x0006; break; case 128: mode = 0x0007; break; case 256: mode = 0x0008; break; default: mode = 0x0000; } return mode; } private: virtual void write(uint16_t reg) = 0; virtual uint16_t read(uint16_t address) = 0; virtual void init_spi() = 0; virtual void init_gpio() = 0; virtual void init_registers() = 0; };
Pico DRV8711 driver class
As last, the Pico savvy child:
class drv8711_pico : public drv8711_driver { public: drv8711_pico(spi_inst_t *spi, uint baudrate, uint cs, uint rx, uint tx, uint sck, uint n_sleep, uint reset) : drv8711_driver(), spi_(spi), baudrate_(baudrate), cs_(cs), rx_(rx), tx_(tx), sck_(sck), n_sleep_(n_sleep), reset_(reset) {} virtual bool init() override { init_gpio(); init_spi(); init_registers(); return true; } virtual void enable(bool enable) override { gpio_put(n_sleep_, enable ? 1 : 0); } // changes config register settings virtual bool microsteps(unsigned int microsteps) override { drv8711::reg_ctrl.mode = microsteps_mode(microsteps); write(drv8711::reg_ctrl); return true; } private: void init_spi() override{ // Enable SPI 0 at 1 MHz and connect to GPIOs spi_init(spi_, baudrate_); spi_set_format(spi_, 16, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST); // 16 bit registers gpio_set_function(rx_, GPIO_FUNC_SPI); gpio_set_pulls(rx_, true, false); // drv8711 outputs are open drain gpio_set_function(sck_, GPIO_FUNC_SPI); gpio_set_function(tx_, GPIO_FUNC_SPI); // CS is active-high, invert pin action gpio_set_function(cs_, GPIO_FUNC_SPI); gpio_set_outover(cs_, GPIO_OVERRIDE_INVERT); } void init_gpio() override{ // nsleep as output gpio_init(n_sleep_); gpio_put(n_sleep_, 0); gpio_set_dir(n_sleep_, GPIO_OUT); // reset as output gpio_init(reset_); gpio_put(reset_, 0); gpio_set_dir(reset_, GPIO_OUT); } // initialise all registers from the defaults // defined in module drv8711_config // developer can override values before calling void init_registers() override{ write(drv8711::reg_ctrl); write(drv8711::reg_torque); write(drv8711::reg_off); write(drv8711::reg_blank); write(drv8711::reg_decay); write(drv8711::reg_stall); write(drv8711::reg_drive); write(drv8711::reg_status); } // write to a register virtual void write(uint16_t reg) override { spi_write16_blocking(spi_, ®, 1); } // read from a register virtual uint16_t read(uint16_t reg) override { uint16_t r_buffer; uint16_t w_buffer; w_buffer = reg | 0b1000; w_buffer = w_buffer << 12; spi_write16_read16_blocking (spi_, (&w_buffer), (&r_buffer), 1); return r_buffer & 0b0000111111111111; // first 4 read bits are undefined } private: spi_inst_t * spi_; uint baudrate_; uint cs_; uint rx_; uint tx_; uint sck_; uint n_sleep_; uint reset_; };
This is the class you'd instantiate, if you want to control a DRV8711 on a Pico:
using driver_t = drv8711_pico::drv8711_pico; driver_t driver1( spi_default, 1000 * 1000, // spi PICO_DEFAULT_SPI_CSN_PIN, PICO_DEFAULT_SPI_RX_PIN, // spi PICO_DEFAULT_SPI_TX_PIN, PICO_DEFAULT_SPI_SCK_PIN, // spi 14U, 15U); // n_sleep, reset
In action
A little proof that it works: I discussed earlier that I use an object to enable and disable the stepper driver IC: embedded C++: manage a resource with a tiny* object.
I 've developed this in a way, that the wakeup class thinks it's dealing with the abstract class, but it actually uses your driver object to enable / disable the driver.
class wakeup { // driver out of sleep as long as object in scope public: wakeup(stepper_driver& driver) : driver_(driver) { driver.enable(true); } ~wakeup() { driver_.enable(false); } private: stepper_driver& driver_; };
No matter what driver you use, as long as it's in the hierarchy of stepper_driver, it 'll be able to manage the IC's state.
void full_demo(const commands_t & cmd) { // wake up the stepper driver IC. // It goes back to low power when this object leaves the scope wakeup w(driver1); // ...
And something simpler: the initialisation code:
void init_everything() { stdio_init_all(); driver1.init(); driver1.microsteps(microstep_x); init_pio(); }
This may not seem to be a lot, but it's quite neat.
C++ resource use: these objects require some resources that aren't used in classic procedural programming. But not a lot. Nothing that the smallest ARM can't handle:
- I used virtual methods and inheritance. There is redirection involved, and the virtual table takes some data space.
project: https://github.com/jancumps/pio_drv8711_stepper
driver code (automatically fetched by the project): https://github.com/jancumps/pico_drv8711_lib