Zephyr: PWM control - simple servo example

20 minute read


Handling PWM next to GPIO control is one of the most popular step in edge device control. Usually pulse width modulation signal is used to steer servo angular position. In this post I would like to show how to enable PWM in Zephyr on ST Nucleo L476RG board with simple sevo example.

Post Overview

  1. PWM signal
  2. PWM in Zephyr
  3. Source code
  4. Building, Flashing and Results

PWM signal

Pulse Width Modulation known as PWM is a method of signal adjustment consisting in changing the signal filling. Typical servo expects a pulse of every period (usually 20 ms). To control servo angular position one should change a pulse width. Usually servo 0° position is connected with pulse width equal to 1 ms:

In turn 180° is described by pulse width equal to 2 ms:

Pulses between 1 and 2 ms width describe angular positions between 0° and 180°. For example 90° is associated with 1.5 ms pulse width.

PWM in Zephyr

Project directory of PWM application should has a form:

It contain 4 significant files:

  • CMakeLists.txt - project’s source files and targets
  • prj.conf - Kconfig file with configuration options
  • <board>.overlay file which override node property values (optional)
  • source code file (main.c)

Firstly, we have to prepare CMakeLists.txt file which sets minimum CMake version and pulls in Zephyr build system which creates a CMake target name app. With target_sources function we can include the sources to build.

# Boilerplate code, which pulls in the Zephyr build system.
cmake_minimum_required(VERSION 3.13.1)
include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE)
project(first)

# Add your source file to the "app" target. This must come after
# the boilerplate code, which defines the target.
target_sources(app PRIVATE src/main.c)

We have also set application configuration options (Kconfig Configuration) in prj.conf file.

CONFIG_STDOUT_CONSOLE=y
CONFIG_PRINTK=y
CONFIG_PWM=y

Using nucleo_l476rg.overlay file in application folder we can override node property values and enable PWM pin which is part of timer2 node.

&timers2 
{   
    status = "okay"; 
    pwm {
        status = "okay";
        label = "PWM_2";
    };
};

Originally both timer2 and his pwm are disabled.

This is very important to change their status to okay if you want to use them. Devicetree is in dts directory in Zephyr folder and specific boards devicetree files should be find in dts/<ARCH>/<vendor>/<soc>.dtsi.

Source code

Application source code is in main.c file in src/ directory.

Firstly, we should include some libraries to enable for example using PWM or printing via serial port:

#include <zephyr.h>
#include <device.h>
#include <drivers/gpio.h>
#include <sys/printk.h>
#include <drivers/pwm.h>
#include <kernel.h>

In the second step we define alias for PWM pin which is equal to PWM_2 according to ST Nucleo L476RG Zephyr documentation (PWM_2_CH1 : PA0):

#define DT_ALIAS_PWM_0_LABEL "PWM_2"

Next we declare some macros which help us in PWM control:

/* period of servo motor signal ->  20ms (50Hz) */
#define PERIOD (USEC_PER_SEC / 50U)

/* all in micro second */
#define STEP 100    /* PWM pulse step */
#define MINPULSEWIDTH 1000  /* Servo 0 degrees */
#define MIDDLEPULSEWIDTH 1500   /* Servo 90 degrees */
#define MAXPULSEWIDTH 2000  /* Servo 180 degrees */

#define SLEEP_TIME_S 1  /* Pause time in seconds*/

To control PWM pulse width is used the following function:

void PWM_control(u8_t *dir, u32_t *pulse_width)
{
	if (*dir == 1U)
	{
		if (*pulse_width < MAXPULSEWIDTH)
		{
			*pulse_width += STEP;
		}
		else
		{
			*dir = 0U;
		}
	}
	else if (*dir == 0U)
	{
		if (*pulse_width > MINPULSEWIDTH)
		{
			*pulse_width -= STEP;
		}
		else
		{
			*dir = 1U;
		}
	}
}

It control the direction of angular position change with variable dir and properly increase or decrease pulse_width value.

Another step is to change pulse width from time to more readable format - degrees. We can do it in simple way:

void get_deegrees(u8_t *degrees, u32_t *pulse_width)
{
	*degrees =  (u8_t)((float)(*pulse_width - MINPULSEWIDTH) / (float)(MAXPULSEWIDTH - MINPULSEWIDTH) * 180.0);
}

The main function is bigger than previous code blocks so I explain its parts below.

void main(void)
{
	printk("Servo control program\n");
	/* Set PWM starting positions as 0 degrees */
	u32_t pulse_width = MINPULSEWIDTH;
	u8_t dir = 0U;
	u8_t degrees = 0U;

        struct device *pwm_dev;

	pwm_dev = device_get_binding(DT_ALIAS_PWM_0_LABEL);
	if (!pwm_dev)
	{
		printk("Cannot find PWM device!\n");
		return;
	}
	else
	{
		printk("PWM device find\n");
		printk("%s\n", DT_ALIAS_PWM_0_LABEL);
	}

	while (1)
	{
		printk("PWM device cycle\n");
		if (pwm_pin_set_usec(pwm_dev, 1, PERIOD, pulse_width, 0))
		{
			printk("PWM pin set fails\n");
			return;
		}
		get_deegrees(&degrees, &pulse_width);

		printk("PWM pulse width: %d\n", pulse_width);
		printk("Degrees: %d\n", degrees);
                
                PWM_control(&dir, &pulse_width);

                printk("\n");

		k_sleep(K_SECONDS(SLEEP_TIME_S));
	}
}

At the beginning of main function we should define device structrue and create device object by device_get_binding function (documentation)

struct device *pwm_dev;

pwm_dev = device_get_binding(DT_ALIAS_PWM_0_LABEL);
if (!pwm_dev)
{
	printk("Cannot find PWM device!\n");
	return;
}
else
{
	printk("PWM device find\n");
	printk("%s\n", DT_ALIAS_PWM_0_LABEL);
}

In while loop we can find 4 functions:

pwm_pin_set_usec(pwm_dev, 1, PERIOD, pulse_width, 0)

This function set the period and pulse width for a single PWM output (documentation).

get_deegrees(&degrees, &pulse_width);

It is our function presented above which change pulse width value to degrees.

PWM_control(&dir, &pulse_width);

This function change pulse width and it is also shown above.

k_sleep(K_SECONDS(SLEEP_TIME_S));

k_sleep function puts the current thread to sleep for given duration (documentation).

Building, Flashing and Results

Having written implementation of PWM use in Zephyr and enabled it in devicetree overlay we can go to project directory and build it with the command:

west build -p auto -b nucleo_l476rg .

After correct building we should connect servo with board pins:

  • GND (brown) <–> board GND
  • 5V (red) <–> board 5V
  • Signal (orange) <–> A0 pin

Next we can flash application to device:

west flash

As a results servo moves between 0° and 180° with 18° step.

STM <--> servo connection

We can also check program results using the serial port communication with picocom application with baud rate equal to 115200 and ST device connected with USB to port /ttyACM0:

picocom -b 115200 /dev/ttyACM0

To check device /tty you can use dmesg command in shell. Result of this command in my example:

Serial port messages:

Summary

To sum up, I think that PWM is very useful in particular if you use servo. Zephyr on board ST Nucleo L476RG ensures this fincjonality. When I started my adventure with Zephyr I didn’t find simple explanation how to use PWM in Zephyr system on my board so I think that this post will be helpful if you want to start with Zephyr’s PWM.

Leave a comment