Control Servo motors with Nextion HMI – Part 2


As we have seen in part 1 of this article series, we need an external MCU, for example an Arduino, to control servo motors with our beloved Nextion HMIs. Now, let’s see how this can be achieved in practice. Since we are already building a universal IO engine with the ongoing Nextion Mega IO project, we will not reinvent the wheel. We’ll just extend what we already have at hands.

Thus, this article builds on

– The Nextion MEGA IO project – Part 1 – Nextion
– The Nextion MEGA IO project – Part 2 – Nextion
– The Nextion MEGA IO project – Part 3 – Nextion
– The Nextion Mega I/O project – Part 4 – Nextion
– The Nextion Mega I/O Project – Part 5 – Nextion
– Working with bit fields – optimize your code – Nextion … and …
Boosting the Nextion Mega I/O project with new knowledge (6)

I recommend to read these beforehand if you haven’t already.

First design thoughts

Since our Mega IO uses the Mega’s pins 2 to 13 for the 12 PWM channels, and A0 to A15 for the 16 ADC channels, we have the 32 GPIO pins from 22 to 53 free. Naturally, these are primarily intended for using these as “real” GPIO pins, but we have plenty of them. Thus, I thought that we could simply use a setting to tell the Mega that we’ll take a few, minimum 0 – maximum 8 of them for servo control.

On startup, we may then use a slider on the Nextion HMI to set the desired number of servo channels, which will then reduce the number of GPIO channels accordingly. To avoid any confusion and interference, we’ll take the servo pins from top down. Servo channel 0 will be on pin 53, channel 1 on pin 52 and so on. This makes sure that the first 24 GPIO channels (yet to implement) will always remain fix on pins 22 to 45. Only the range from 46 to 53 will be variable and affected.

The servo control code

All we need to do, is including the Servo library which comes with the Arduino IDE, declare the maximum of 8 servo objects, and have two functions:

resetServos() will check all 8 servo objects if they are attached and detach them if needed to free up the GPIO pins.

initServos(uint8_t channels) will attach the number channels of servos to the corresponding pins. For example, if channels is 2, NexServo[0] will be attached to pin 53, and NexServo[1] to pin 52.

To keep our already complex NexMegaIO.h file clean, I’ve put all the servo stuff in a separate file by adding a tab and naming it NexServo.h – here it is:

#ifndef NexServo_h
#define NexServo_h

#define MAXSERVOS 8
Servo NexServo[MAXSERVOS];

void resetServos() {
  for (uint8_t i = 0; i < MAXSERVOS; i++) {
    if (NexServo[i].attached()) {
      NexServo[i].detach();  // Detach all attached servos and free up the pins
void initServos(uint8_t channels) {
  for (uint8_t i = 0; i < channels; i++) {
    NexServo[i].attach(53 - i);  // Attach servos from the End (first on pin 53, second on pin 52 etc.)


Now, we have to include everything in the main file, NexMegaIOdemo.ino and we have to take care of the order. First, we include Servo.h because everything after will need access to the core servo functionality. Then, we include NexServo.h because the later included NexMegaIO needs it to “talk” to the servos.

The first lines of our NexMegaIOdemo.ino look now like this, everything else remains unchanged:

#include <Servo.h>
#include "NexServo.h"
#include "NexMegaIO.h"

Now to the protocol handler

In Part 1, we defined our hyper efficient and compact 2 (+1) byte custom protocol which had already a data type for “Settings” planed. The 3 least significant bits (data type) of the command byte have to be 0 for that. This allows then, by using the channel bits 3 to 6 of the first data byte as configuration IDs, to transmit up to 16 informations. For the number of servos, a single configuration value is sufficient. I chose the highest number (15) for that, leaving the lower categories (0 to 14) for the (yet to come) detailed GPIO configuration.

Thus, in a first time, we extend the core of our NexMegaIO.h – the function process(uint8_t abyte) in the read and write sections as follows:

  case 0x00:                      // Settings
    onUpstreamSETread(_channel);  // call the specific handler


  case 0x00:                                           // Settings
    sdta = _rawdata[1] + ((_rawdata[0] & 0x07) << 7);  // extract 10bit SET data
    upstreamSETwrite(_channel, sdta);                  // call the specific handler

Now, as we are calling the new handler functions onUpstreamSETread(_channel) and upstreamSETwrite(_channel, sdta), we need also to implement them. I let you look at the code which you’ll find at the end of the article. Everything is very similar to writing and reading PWM values. It’s just a different data format, and a filter for the channel (which we use as configuration ID) = 15. The write part does naturally then call our initServos() function.

After the settings are done and the servos initialized, we may send position data (values between 0 and 180) to the servos. Since there are data types already used or planned, 0 for settings, 1 for GPIO, 2 for PWM, and 3 for ADC, I decided to keep with the previous top down approach and gave the servo data the type 7. Then, again, extending the read and write handlers as follows

  case 0x07:                        // Servo
    onUpstreamSERVOread(_channel);  // call the specific handler


  case 0x07:                                                // Servo
    data = _rawdata[1] + ((_rawdata[0] & 0x01) ? 128 : 0);  // extract 8bit Servo data
    upstreamSERVOwrite(_channel, data);                     // call the specific handler

This is naturally followed by the onUpstreamSERVOread(_channel) and upstreamSERVOwrite(_channel, data) implementations which are pretty similar to their already known PWM counterparts. I let you discover this, too, by lurking at the code.

But that’s again the proof that even complex things can easily be maintained and even extended if everything is well thought from the beginning and if there is enough headroom left.

Here is all the Arduino stuff. The (much easier) Nextion side is for the next article.

For today, thank you for your patient reading and happy Nextioning!

Questions, comments, critics, suggestions? Just send me an email to thierry (at) itead (dot) cc! 🙂

And, by the way, if you like what I write, and you are about to order Nextion stuff with Itead, please use my referral link! To you, it won’t make a change for your order. But it will pay me the one or the other beer or coffee. And that will motivate me to write still more interesting blogs 😉