Electronics Software Development

Driving A DC Motor With CircuitPython

CircuitPython Motor Graphic
Written by John Woolsey

Skill Level: Intermediate

Table Of Contents

Introduction

This tutorial will teach you how to drive small DC motors with your CircuitPython compatible board. Specifically, we will cover

  • Connecting a motor driver board and two motors to a CircuitPython compatible board,
  • Using the Adafruit_CircuitPython_Motor library in a CircuitPython program to perform basic operations,
  • Enhancing the program by checking for any faults that may be occurring on the motor driver,
  • Showing how to ramp up and down a motor’s speed over a specific time frame, and
  • Demonstrating how to provide variable speed control using a potentiometer.

A basic understanding of electronics and programming is expected along with some familiarity with the CircuitPython ecosystem. If you are new to CircuitPython, or would just like to refresh your knowledge, please see our Getting Started With CircuitPython On Compatible Microcontroller Boards tutorial before proceeding with this one.

This tutorial uses a solderless breadboard to build a circuit from a schematic diagram. The All About Circuit’s Understanding Schematics, SparkFun’s How to Read a Schematic, Core Electronics’ How to Use Breadboards, and Science Buddies’ How to Use a Breadboard guides are good resources for learning how to translate a schematic to a breadboard.

The resources created for this tutorial are available on GitHub for your reference.

What Is Needed

  • Linux, macOS, Or Windows Based Computer With A USB Port
  • Mu Python Editor (recommended)
  • A CircuitPython Compatible Microcontroller Board With Compatible USB Cable (available on Adafruit)
  • Soldering Station (for attaching headers to the motor driver board)
  • Solderless Breadboard (available on Adafruit and SparkFun)
  • Preformed Breadboard Jumper Wire Kit (available on SparkFun and CanaKit)
  • 9 x Male/Male Jumper Wires (available on Adafruit and Arrow)
  • 6 x Alligator Clip Test Leads (available on Adafruit and SparkFun)
  • Adafruit DRV8833 DC/Stepper Motor Driver Breakout Board (available on Adafruit and Digi-Key)
  • 1 or 2 DC Motors (available on Adafruit and PiShop) with optional propellers (available on Adafruit) and motor mounts (available on Adafruit)
  • 10 KΩ Linear Potentiometer (available on Adafruit and Sparkfun)
  • 6-9 Volt Power Supply, e.g. A Benchtop Power Supply (recommended) Or A 9V Battery With Corresponding Battery Clip (available on Digi-Key and Jameco)
  • Small Flat Head Screwdriver

Background Information

You can find many designs, integrated circuit chips, and breakout boards used for controlling motors when searching the internet. Almost all of them are based on the H-bridge circuit that uses four transistors to drive a motor forward as well as in reverse.

Before writing the Driving A DC Motor With An Arduino tutorial, I initially set out to write an article on how to build your own custom H-bridge based motor controller and interface it to your CircuitPython board. However, my research showed that modeling a motor in a circuit simulator is not an easy task and requires specialized test equipment that I do not have. Since I wanted to provide the appropriate details to back up my design choices, this route proved insufficient. I chose instead to use a pre-built motor controller breakout board. Going this route also has the benefits of providing extra features, e.g. over current and fault detection, that I was not planning to add to the original design.

After researching the motor controller boards available, I chose the Adafruit DRV8833 DC/Stepper Motor Driver Breakout Board based on the DRV8833 Dual H-Bridge Motor Driver chip from Texas Instruments. This breakout board has the ability to drive two DC motors (including speed control with PWM) or one stepper motor. It also includes reverse EMF and over current protection circuitry. The board also supports a motor supply voltage range of 2.7 – 10.8 V and an input logic range of 2.7 – 5.75 V.

I am using Adafruit’s Feather M4 Express microcontroller board connected to a macOS based computer with the Mu Python editor for this tutorial. If you are using a different CircuitPython compatible board, computer setup, or code editor, the vast majority of this tutorial should still apply, however, some minor changes may be necessary.

If you need assistance with your particular setup, post a question in the comments section below and I, or someone else, can try to help you.

Building The Circuit

Before connecting any circuitry to your CircuitPython compatible board, disconnect it from power and your computer. This avoids accidental damage during wiring.

The headers of the Adafruit DRV8833 DC/Stepper Motor Driver Breakout Board are not pre-attached. You will need to solder them onto the board in order to easily attach the breakout board to a solderless breadboard.

Once your headers are attached, place the components and wire up the circuit on a breadboard according to the schematic diagram shown below.

Schematic Diagram Of The DRV8833 Motor Driver Circuit Connected To A CircuitPython Compatible Board
Schematic Diagram Of The DRV8833 Motor Driver Circuit Connected To A CircuitPython Compatible Board

If you have a limited number of GPIO pins available on your board, you can save a couple of pins without losing too much functionality. If you are not interested in directly enabling and disabling the motor driver, you can tie the SLP pin on the driver board high to leave the driver always enabled. Likewise, the board’s FLT pin can remain disconnected if you do not care about checking for driver faults.

Some CircuitPython compatible boards share PWM channels across pins, e.g. pins D9 and D25 on the Adafruit Feather RP2040. If you are using a different board other than the Adafruit Feather M4 Express, make sure to select distinct PWM channels (pins) on your board for use with all of the motor driver’s AIN1/AIN2/BIN1/BIN2 pins since every pin could be using different PWM functionality at the same time.

A 9V battery can be used instead of a benchtop power supply to power the motor(s), but it may only be able to drive one motor at a time and the battery could drain quickly. Alternatively, four AA batteries in series could also be used providing a total voltage of 6 volts, but this will drastically limit your speed control capability.

The circuit should look similar to the one shown below once completed.

Completed DRV8833 Motor Driver Circuit Connected To A CircuitPython Compatible Board
Completed DRV8833 Motor Driver Circuit Connected To A CircuitPython Compatible Board

As you can see, I added some propellers to my motors to make it easier for me to see when the motors are active and at what speed they are running.

Once the circuit is built, connect your CircuitPython compatible board to your computer with the USB cable.

Installing The Library

Before installing any libraries, make sure you are running the latest stable release of CircuitPython on your compatible board.

We will take advantage of the existing Adafruit_CircuitPython_Motor driver library to communicate with the motor(s) attached to the Adafruit DRV8833 DC/Stepper Motor Driver Breakout Board in our circuit. Copy the adafruit_motor library directory from the latest stable bundle to the lib directory of the board’s CIRCUITPY drive.

Writing The Program

Open Mu or your favorite code editor and create a CircuitPython program with the code shown below.

from time import sleep
import board
from digitalio import DigitalInOut, Direction, Pull
from pwmio import PWMOut
from adafruit_motor import motor as Motor

DEBUG = True  # mode of operation; False = normal, True = debug
OP_DURATION = 5  # operation duration in seconds

drv8833_ain1 = PWMOut(board.D9, frequency=50)
drv8833_ain2 = PWMOut(board.D10, frequency=50)
drv8833_bin1 = PWMOut(board.D11, frequency=50)
drv8833_bin2 = PWMOut(board.D12, frequency=50)
drv8833_sleep = DigitalInOut(board.D5)

motor_a = Motor.DCMotor(drv8833_ain1, drv8833_ain2)
motor_b = Motor.DCMotor(drv8833_bin1, drv8833_bin2)

def print_motor_status(motor):
    if motor == motor_a:
        motor_name = "A"
    elif motor == motor_b:
        motor_name = "B"
    else:
        motor_name = "Unknown"
    print(f"Motor {motor_name} throttle is set to {motor.throttle}.")

def basic_operations():
    # Drive forward at full throttle
    motor_a.throttle = 1.0
    if DEBUG: print_motor_status(motor_a)
    sleep(OP_DURATION)

    # Coast to a stop
    motor_a.throttle = None
    if DEBUG: print_motor_status(motor_a)
    sleep(OP_DURATION)

    # Drive backwards at 50% throttle
    motor_a.throttle = -0.5
    if DEBUG: print_motor_status(motor_a)
    sleep(OP_DURATION)

    # Brake to a stop
    motor_a.throttle = 0
    if DEBUG: print_motor_status(motor_a)
    sleep(OP_DURATION)

# Main
drv8833_sleep.direction = Direction.OUTPUT
drv8833_sleep.value = True  # enable (turn on) the motor driver

if DEBUG: print("Running in DEBUG mode.  Turn off for normal operation.")
while True:
    basic_operations()  # perform basic motor control operations on motor A

Let’s take a look at some of the more interesting parts of the code.

Lines 10-13 define the motor control input pins and correspond to the IN1 and IN2 pin pairs of the motor driver board. The driver board’s AIN1 / AIN2 and BIN1 / BIN2 input pin pairs control the motor driver’s AOUT1 / AOUT2 and BOUT1 / BOUT2 output pin pairs for motors A and B respectively. The truth table below provides the basic operations.

IN1IN2OUT1OUT2Function
LowLowHigh ZHigh ZPassively coast to a stop
LowHighLowHighDrive backwards
HighLowHighLowDrive forward
HighHighLowLowActively brake to a stop

This control logic is typical for most H-bridge based motor drivers and is not specific to the DRV8833 chip and board.

We can also apply pulse width modulation (PWM) to the input pins to control the motor’s speed. This is enabled through the use of the PWMOut object of the pwmio library. In addition, specifying a PWM frequency of less than 100 Hz typically improves the low speed operation of brushed DC motors, which was done here.

Lines 16 and 17 define the motor instances using the control input pin pairs defined above.

Since we will be covering multiple examples of functionality, I separated them into their own distinct functions. This first one, named basic_operations(), demonstrates the basic operations available within the DCMotor class of the Adafruit_CircuitPython_Motor library.

The throttle property utilizes a floating point range from -1.0 (100% throttle in reverse) to 1.0 (100% forward throttle). For instance, a value of 0.6 corresponds to a 60% throttle moving forward and a value of -0.35 corresponds to a 35% throttle moving in reverse. The value of 0 is a special case as it actually informs the motor library to apply the brakes to stop the motor. To specify that you would rather coast to a stop, use a value of None instead.

If the DEBUG mode is enabled, via line 7, the motor status is printed using the print_motor_status() function after each operation is executed. A delay is also implemented in between each operation with the time, in seconds, specified on line 8 with OP_DURATION. This can be adjusted higher or lower to give you enough time to view the motor throttle status and see how the motor reacts to the operation just given.

The SLP pin of the motor driver board, defined on line 14, is pulled low by default which disables the motor driver itself. If you connected this pin to your CircuitPython compatible board, versus directly tying it high, then line 51 pulls the pin high to enable the driver for use.

If there is something that needs further explanation, please let me know in the comment section and I will try to answer your question.

Running And Testing The Program

Now that our circuit is built and our software is written, it is time to run and test our program.

If you are using the Mu editor with your CircuitPython compatible board, click the Serial icon within the menu bar at the top to open the serial console. It will appear at the bottom of the Mu editor’s window and will be used to view the program’s output. Save the program as code.py to the top level of the board’s CIRCUITPY drive and it will begin running automatically.

You should see the motor start running through the basic operations while displaying the status of each operation within the serial console.

We have covered all of the basic operations for controlling a motor with the attached motor driver board. You are now armed with the information you need to use them for your own ideas and projects. But I thought it would be nice to cover some additional functionality and offer you some more examples of how you could control the motor. Please continue reading if you are interested.

Adding Fault Checking

The DRV8833 motor driver chip used in the motor driver board provides a pin, named FLT, that is driven low when the chip detects a fault, e.g. thermal shutdown or overcurrent situations. When this happens, the chip shuts down the offending H-bridge but leaves the other one operational. Once the fault has cleared, the pin goes high again. We can watch this pin to let us know if faults are occurring during our motor control operations.

Import the countio library just after the import board line in the imports section at the top of the program.

from countio import Counter, Edge

Using this library, add a pin edge counter to the end of the list of pins, just after the drv8833_sleep pin, that will count the number of falling edges on the DRV8833 FLT (fault) pin.

drv8833_fault_counter = Counter(board.D6, edge=Edge.FALL, pull=Pull.UP)

This counter will be used to detect, and count, the faults that occurred on the motor driver board.

Next, add the check_for_motor_driver_fault() function after the basic_operations() function but before the main section.

def check_for_motor_driver_fault():
    if drv8833_fault_counter.count > 0:
        if DEBUG:
            print(f"Motor driver fault(s) detected: {drv8833_fault_counter.count}")
        drv8833_fault_counter.reset()  # reset the fault count back to 0

This function simply checks the fault counter and reports the number of faults that have occurred since the last time the check was performed.

Lastly, add the function to the endless loop in the main section.

while True:
    check_for_motor_driver_fault()
    basic_operations()  # perform basic motor control operations on motor A

Save and run your program to test the code. It should function in the same manner as before, but this time, it will also let you know if any faults are occurring. When I run the program, I do see a few faults happening on each pass of the loop; I don’t know why.

Since the DRV8833 chip itself takes care of shutting down the offending circuity, checking for faults isn’t really necessary, but it is nice to know when it happens. The check_for_motor_driver_fault() function only runs once during each pass of the endless loop. If you see multiple faults occurring on each pass, you can add more granularity to your fault reporting by adding a check_for_motor_driver_fault() function call after each operation within the basic_operations() function.

Ramping Up And Down The Speed

This example code shows how you can ramp up and down the speed of a motor over a specific time frame. Add the following routines to the end of your functions list, just before the main section.

def ramp_up(motor, direction, duration):
    for speed in [x * 0.01 for x in range(0, 101)]:  # 0.0 to 1.0
        motor.throttle = speed if direction == "forward" else -speed
        sleep(duration / 100)

def ramp_down(motor, direction, duration):
    for speed in [x * 0.01 for x in reversed(range(0, 101))]:  # 1.0 to 0.0
        motor.throttle = speed if direction == "forward" else -speed
        sleep(duration / 100)

Given the direction, forward or reverse, and the length of time to take to ramp up (down) the speed from 0% to 100% (100% to 0%) throttle, these routines use a for loop to increase (decrease) the speed and pause for the appropriate amount of time between speed changes.

To utilize these functions, let’s create another example function, just below those above,

def ramping_speed():
    ramp_up(motor_a, "forward", OP_DURATION)
    ramp_down(motor_a, "forward", OP_DURATION)

and then add it to our endless loop at the end of the program. In addition, comment out the basic_operations() function so that it no longer runs.

while True:
    check_for_motor_driver_fault()
    # basic_operations()  # perform basic motor control operations on motor A
    ramping_speed()  # ramp up and down the speed of motor A

Save and run your program to test the code. You should now see motor A ramp up and down continuously.

Using Variable Control

We haven’t used motor B yet, let’s fix that by incorporating it into this last example. Here we will utilize the potentiometer attached to the CircuitPython compatible board to manually adjust the speed of the motor.

Import the analogio library just after the import board line in the imports section at the top of the program

from analogio import AnalogIn

and then add the potentiometer to our list of pins.

pot = AnalogIn(board.A0)

While retrieving the raw analog readings from the potentiometer during testing for this tutorial, I noticed that the values tended to jump around quite a bit. I decided to implement a digital smoothing (averaging) filter to address the noisy signal. This is accomplished by keeping track of the last five potentiometer readings and averaging those values into a single filtered value.

Add the following two global variables to the program, just before the definition of the motor instances.

previous_raw_pot_readings = [0, 0, 0, 0, 0]
previous_filtered_pot_reading = 0

The first one stores the last five raw potentiometer readings. The second one saves the previous output from the digital smoothing filter.

Now add the digital filter function itself to your program.

def smoothing_filter(current_value):
    previous_raw_pot_readings[4] = previous_raw_pot_readings[3]
    previous_raw_pot_readings[3] = previous_raw_pot_readings[2]
    previous_raw_pot_readings[2] = previous_raw_pot_readings[1]
    previous_raw_pot_readings[1] = previous_raw_pot_readings[0]
    previous_raw_pot_readings[0] = current_value
    return sum(previous_raw_pot_readings) / len(previous_raw_pot_readings)  # average of raw values

Next, we need to incorporate that digital filter into an example function that reads the potentiometer and adjusts the motor speed to match.

def potentiometer_control():
    global previous_filtered_pot_reading
    current_raw_pot_reading = pot.value
    current_filtered_pot_reading = smoothing_filter(current_raw_pot_reading)
    if abs(current_filtered_pot_reading - previous_filtered_pot_reading) > 656:  # minimize unnecessary updates, about a 1% change
        if DEBUG: print(f"Potentiometer reading: {current_filtered_pot_reading}")
        motor_b.throttle = current_filtered_pot_reading / 65535  # a value between 0.0 and 1.0
        if DEBUG: print_motor_status(motor_b)
        previous_filtered_pot_reading = current_filtered_pot_reading

This function reads the raw analog value of the potentiometer (line 3) and then runs that value through the digital filter (line 4) to get the filtered (averaged) value. Line 7 uses that filtered value to update the motor speed with the appropriate value between 0.0 (0% throttle) and 1.0 (100% throttle). Lines 2, 5, and 9 are used to limit constantly updating the motor by only changing the motor speed when the user actually adjusts the potentiometer. About a 1% change in this case.

If you are only using one motor in your design, remove the instantiation of motor_b, towards the top of the program, and then change all the references to motor_b in the potentiometer_control() function to motor_a.

Finally, add the example function to your program and then call that function from within the endless loop.

Note: I noticed upon occasion that I would experience too many faults while running the potentiometer control code which made it difficult to see the potentiometer and motor throttle values being printed to the serial console. I suggest you also comment out the check_for_motor_driver_fault() line when running the code the first time just to make sure everything is working before enabling the motor driver fault checks again.

while True:
    # check_for_motor_driver_fault()
    # basic_operations()  # perform basic motor control operations on motor A
    # ramping_speed()  # ramp up and down the speed of motor A
    potentiometer_control()  # control motor B speed with a potentiometer

Save your program to run the updated code. When you adjust the potentiometer, you should now see the speed of the motor change accordingly along with the potentiometer and motor values being displayed in the serial console. You will also notice that the motor stalls at slower speeds. The stall speed is dependent on the specifications of the attached motor along with the motor supply voltage being used.

Additional Resources

The following is a list of additional resources you may find helpful.

Summary

In this tutorial, we learned how to connect and control a small DC motor with a CircuitPython compatible board. We installed and utilized the Adafruit_CircuitPython_Motor library in our program to control a motor in a variety of ways, from performing basic operations to using a potentiometer to control the speed of the motor. We even added the ability to detect and report faults occurring on the motor driver board.

The final source code and schematic used for this tutorial are available on GitHub. The GitHub version of the code is fully commented to include additional information, such as the program’s description, circuit connections, code clarifications, and other details. The comments are also Sphinx compatible in case you want to generate the code documentation.

Thank you for joining me on this journey and I hope you enjoyed the experience. Please feel free to share your thoughts or questions in the comments section below.

This tutorial is provided as a free service to our valued readers. Please help us continue this endeavor by considering a donation.

About the author

John Woolsey

John is an electrical engineer who loves science, math, and technology and teaching it to others even more.
 
He knew he wanted to work with electronics from an early age, building his first robot when he was in 8th grade. His first computer was a Timex/Sinclair 2068 followed by the Tandy 1000 TL (aka really old stuff).
 
He put himself through college (The University of Texas at Austin) by working at Motorola where he worked for many years afterward in the Semiconductor Products Sector in Research and Development.
 
John started developing mobile app software in 2010 for himself and for other companies. He has also taught programming to kids for summer school and enjoyed years of judging kids science projects at the Austin Energy Regional Science Festival.
 
Electronics, software, and teaching all culminate in his new venture to learn, make, and teach others via the Woolsey Workshop website.

4 Comments

  • John, a great description. Fantastic code and example. Thank you.

    I have the AdaFruit TB6612, instead of the DRV 8833. Additionally, I’m running off of a Pi (Model B+). I’m struggling a bit w/ the wiring. I think I see how to adapt the code to the right pinouts, if I was able to just nail the pins. I would love your thoughts.

    Many thanks,
    Doug

    • Sure, after doing some reasearch, I believe I understand what changes need to be made. The primary differences I see between the TB6612 and DRV8833 boards is that the TB6612 has extra VCC, PWMA, and PWMB pins, does not have a fault pin, and STBY (TB6612) is enabled by default where the SLP (DRV8833) pin is not. In addition, the Raspberry Pi does not have any analog inputs so we can not use the potentiometer to adjust the speed. I don’t have the TB6612 board so I can’t verify anything, but I will help where I can.

      For the hardware, it appears the following pin connection changes are required:

      • Connect VCC, PWMA, and PWMB on the TB6612 to 3V3 on the RPi.
      • Disconnect the FLT pin as there is no such pin on the TB6612.
      • Disconnect the STBY pin, SLP pin on DRV8833.
      • Do not connect the potentiometer.
      • All other pin connections should remain the same.

      As for the software,

      • Make sure you have the Blinka library installed for CircuitPython language support.
      • Remove the analogio and countio library imports.
      • Add import RPi.GPIO as GPIO to the imports.
      • Remove the drv8833_sleep, drv8833_fault_counter, pot, previous_raw_pot_readings, and previous_filtered_pot_reading global variables.
      • Remove the check_for_motor_driver_fault(), smoothing_filter(), potentiometer_control(), and init() functions.
      • Add GPIO cleanup code to the main() function like shown below.
      def main():
          if DEBUG: print("Running in DEBUG mode.  Turn off for normal operation.")
          print("Press CTRL-C to exit.")
          try:
              while True:
                  basic_operations()  # perform basic motor control operations on motor A
                  # ramping_speed()  # ramp up and down the speed of motor A
          finally:
              GPIO.cleanup()

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.