We’ll create a driver for the 28BYJ-48 5 volts unipolar stepper motor
in the next few videos. This motor is readily available and cheap.
The motor is often sold together with a driver board PCB containing a
ULN2003A IC and some LED indicator lights.
The ULN2003A is an array of 7 darlington NPN transistors. It takes the
5 volts power supply, amplifies the current and handles the kick-back
with a diode to common.
Click this link to download a PDF of the schematic. Note that
the ULN2003A inverts the input signal, so that 5 volts is supplied to
the motor from the ULN2003A whenever the corresponding input is LOW.
To drive the motor, we connect 4 wires from the microcontroller. Let
us choose four GPIO pins and call them IN1, IN2, IN3, IN4 to
correspond to the driver inputs.
A word of caution. This motor is produced by many different
manufacturers, and sometimes the color coding on the cables may be
different to what my motor has. Read the datasheet that came with your
motor, if there is one, or do a bit of trial and error through the
various permutations. Also note, there is a 12 volts version of this
motor. Finally, if you have another type of stepper motor lying
around, you can probably adapt the driver to work with that as well.
As a bit of a heads-up, in the next video, we’ll drive two motors. So
you may want to buy two sets of motor and driver board PCB if you plan
to follow along.
I’m currently running Zephyr version 3.4.0-rc2 with Zephyr SDK 0.16.1.
We’re first going to hardcode everything for the 28BYJ-48 motor in the
main.c
file. Once we get things working we will refactor and clean
up. Listing 1 shows the initial code to get started.
#include <stdint.h>
enum rotation_direction {
CLOCKWISE,
COUNTER_CLOCKWISE,
};
int take_steps(const uint32_t target_num_steps,
const enum rotation_direction rot_dir,
const int32_t sleep_time_ms)
{
return 0;
}
void main(void)
{
return;
}
A stepper motor moves by taking discrete steps, so let’s create a
function called take_steps
to serve as our driver’s first
application programming interface, or API. The application will call
this API to rotate the motor. The function will return 0 if the call
was successful, and takes three arguments.
First, we need to know how many steps to rotate the motor. Let’s call
that target_num_steps
.
Second, we can rotate the motor either clockwise or
counterclockwise. Let’s create an enum to capture these two options
and call it rotation_direction
with CLOCKWISE
and
COUNTER_CLOCKWISE
as its members. The second argument to the
take_steps
function can then use this enum type, and let’s call
the local variable rot_dir
.
The third argument tells the driver how long it should sleep
between each step. This determines the speed of the rotation. A lower
number indicates a shorter pause, and hence a faster rotation
speed. We’ll call it sleep_time_ms
to indicate that we are expecting
the argument to be in milliseconds.
Finally, let’s include stdint.h
for the 32-bit integer types,
uint32_t
etc.
Zephyr uses CMake to build projects, so we need to provide some
suitable configuration. Listing 2 shows a basic
CMakeLists.txt
file in the application root folder that will get us
started.
cmake_minimum_required(VERSION 3.22)
set(BOARD esp32)
find_package(Zephyr)
project(app LANGUAGES C)
target_sources(app PRIVATE src/main.c)
Create an empty prj.conf
file in the project root directory, and
build main.c
using west
to ensure we have the basic project set
up correctly.
So far, so good. But our application doesn’t actually do anything. Not
to worry, we’ll fix that now. To rotate the motor, as we learned in
the beginning of this article, we have to set and clear the GPIO pins
in a specific pattern. Let’s first specify the GPIO pins we have
connected from the microcontroller to the darlington transistor driver
pins: GPIO pins 25, 26, 27 and 14. It’s important that these are in
the order that they connect to IN1, IN2, IN3, and IN4 on the
transistor driver. We’ll create an array of type gpio_pin_t
called
IN
, and include zephyr/drivers/gpio.h
for the declaration of that
type.
#include <zephyr/drivers/gpio.h>
...
static const gpio_pin_t IN[] = {25, 26, 27, 14};
Next, we’ll get a pointer to the struct device
for GPIO controller
gpio0
, that controls the state of the four GPIO pins we’re
interested in. The header file zephyr/device.h
contains the
declaration for DEVICE_DT_GET
and DT_NODELABEL
, so let us include
that in main.c
as well.
#include <zephyr/device.h>
...
static const struct device *gpio_dev = DEVICE_DT_GET(DT_NODELABEL(gpio0));
Let’s check whether the device is ready in the take_steps
function,
and log an error message and return if it is not. After all, what is
the use of a stepper motor driver if the GPIO controller is not ready?
We include the zephyr/logging/log.h
header and register the log
module using LOG_MODULE_REGISTER
with the name stepper_driver
and
set the log level to debug using LOG_LEVEL_DBG
. This helps us to
understand which module wrote the log message.
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(stepper_driver, LOG_LEVEL_DBG);
... take_steps(...) ...
if (!device_is_ready(gpio_dev)) {
LOG_ERR("device %s is not ready", gpio_dev->name);
return -ENODEV;
}
For the logs to work, we also have to enable the Kconfig option by
entering CONFIG_LOG=y
in the file prj.conf
in our application root
directory.
CONFIG_LOG=y
Next, let’s implement the GPIO pin pattern to make the motor take
steps and rotate. The actual details of the implementation are not
important, since this article focuses on how to structure a device
driver in Zephyr.
First, let’s configure all four GPIO pins and set them low.
for (size_t i = 0; i < sizeof(IN); i++) {
if (gpio_pin_configure(gpio_dev, IN[i], GPIO_OUTPUT_LOW) != 0) {
LOG_ERR("could not set GPIO pin %zu to 0", i);
return -EIO;
}
}
With the initialization taken care of, it’s time to step the motor.
First, we need some way to track how many steps the motor has already
taken. We’ll declare curr_steps_count
to do this and initialize it to
zero. The steps should continue as long as curr_steps_count
is less
than target_num_steps
, so we’ll capture that in a while loop. Then
we slog through changing the pins according to the set sequence,
taking care to turn them on and off at the right time, and updating
the curr_steps_count
variable for each step taken.
We’ll also include the zephyr/kernel.h
header file where the k_msleep
function is declared.
#include <zephyr/kernel.h>
...
uint32_t curr_steps_count = 0U;
while (curr_steps_count < target_num_steps) {
// STEP 1
k_msleep(sleep_time_ms);
gpio_pin_set_raw(gpio_dev, IN[3], 0);
gpio_pin_set_raw(gpio_dev, IN[1], 1);
curr_steps_count++;
// STEP 2
k_msleep(sleep_time_ms);
gpio_pin_set_raw(gpio_dev, IN[0], 0);
gpio_pin_set_raw(gpio_dev, IN[2], 1);
curr_steps_count++;
// STEP 3
k_msleep(sleep_time_ms);
gpio_pin_set_raw(gpio_dev, IN[1], 0);
gpio_pin_set_raw(gpio_dev, IN[3], 1);
curr_steps_count++;
// STEP 4
k_msleep(sleep_time_ms);
gpio_pin_set_raw(gpio_dev, IN[2], 0);
gpio_pin_set_raw(gpio_dev, IN[0], 1);
curr_steps_count++;
}
To get the motor spinning, we call the take_steps
function from the
main
function. Let’s ask the motor to take 2048 steps, which happens
to be one full rotation on this particular stepper motor. We’ll tell
the function to step in the clockwise direction, although, if you’ve
been following carefully, the take_steps
function doesn’t use this
argument yet. Finally, the sleep value. This may vary a bit from
stepper motor to stepper motor, but let’s start with a value of 50ms.
if (take_steps(2048, CLOCKWISE, 50) != 0) {
LOG_ERR("could not take steps on motor");
return;
}
Let’s flash this firmware onto the ESP32, and see that the stepper
motor rotates 360 degrees, albeit slowly. The flashing lights indicate
which pins are activated, which is a nice feature of this commonly
available stepper driver board.
With the clockwise direction working, let’s implement the
counterclockwise direction as well. First, we check if the rotation
direction is clockwise or counter-clockwise using an if
statement. If the direction is CLOCKWISE
, we run through the while loop we
already created. If it is COUNTER_CLOCKWISE
, we will use another
while loop that works in the reverse. Finally, let’s log an error if
another direction has been passed in, just in case.
Listing 10 shows the implementation of the
counterclockwise direction.
if (rot_dir == CLOCKWISE) {
// Existing code....
}
} else if (rot_dir == COUNTER_CLOCKWISE) {
while (curr_steps_count < target_num_steps) {
// STEP 1
k_msleep(sleep_time_ms);
gpio_pin_set_raw(gpio_dev, IN[0], 0);
gpio_pin_set_raw(gpio_dev, IN[2], 1);
curr_steps_count++;
// STEP 2
k_msleep(sleep_time_ms);
gpio_pin_set_raw(gpio_dev, IN[3], 0);
gpio_pin_set_raw(gpio_dev, IN[1], 1);
curr_steps_count++;
// STEP 3
k_msleep(sleep_time_ms);
gpio_pin_set_raw(gpio_dev, IN[2], 0);
gpio_pin_set_raw(gpio_dev, IN[0], 1);
curr_steps_count++;
// STEP 4
k_msleep(sleep_time_ms);
gpio_pin_set_raw(gpio_dev, IN[1], 0);
gpio_pin_set_raw(gpio_dev, IN[3], 1);
curr_steps_count++;
}
} else {
LOG_ERR("direction should be CLOCKWISE or COUNTER_CLOCKWISE");
return -EINVAL;
}
Let’s add a call from the main function to rotate counterclockwise one
full rotation, and to speed things up, change the sleep time to 4ms
for both directions.
if (take_steps(2048, CLOCKWISE, 4) != 0) {
LOG_ERR("could not take steps on motor");
return;
}
if (take_steps(2048, COUNTER_CLOCKWISE, 4) != 0) {
LOG_ERR("could not take steps on motor");
return;
}
Flash the firmware again and see if we get the expected
behaviour. And, indeed, we see that the motor first rotates 360
degrees in the clockwise direction, and then 360 degrees in the
counter-clockwise direction. It rotates much faster now as well due to
the lower sleep time.