The Sunday Blog: GPIO on Nextion vs. Arduino
Part 1: The theory
Did you know that your Nextion HMI of the Enhanced or Intelligent series have 8 GPIO (General Purpose In-Out) pins, either for smaller autonomous (without extra MCU) projects or to extend the available IO of a connected MCU? This can be very helpful and is a great addition if you have just a few extra physical keys, LEDs, beepers, servos, or rotary encoders to control, or if you want to acquire data from an external sensor!
Today, we’ll see the “how to” in Nextion code, side by side with the equivalent Arduino code to ease the understanding. From the next episode on, we’ll see here small demo projects for everything in practice.
Since the Nextion HMI displays are primarily targeted at industrial applications, the 8 GPIO pins are available together with +5V and GND on a 1mm pitch 10pin FCC connector, intended for a corresponding flat ribbon cable. I admit, this is not very breadboard-friendly for the hobbyist, but there is a solution: We at Nextion and our authorized distributors and resellers sell the Nextion IO Adapter which comes with a 10cm (4″) flat ribbon cable and a small printed circuit board which accepts pin headers in the more common 2.54mm (0.1″) raster. Ideal for one-shot projects and prototyping!
As an Arduino user, you are most probably familiar with the fact that GPIO pins have to be individually configured as inputs or outputs with the pinMode() function. As an example, to use pin 0 as an input, you’d write the line pinMode(0,INPUT); or to use pin 1 as an output, write pinMode(1,OUTPUT); To use a pin as an input with a single key or switch towards GND, Arduino allows to save on an external pull-up resistor by activating the internal one, using pinMode(0,INPUT_PULLUP);
Our Nextion programming language gives us a similar but even mightier command: cfgpio. It allows the use of up to 5 pin modes, saving us the equivalents of attachInterrupt() and analogWrite() as we will see below. For the moment, let’s see how to configure a pin as an input. The cfgpio syntax is as follows: cfgpio <pin>,<mode>,<object>:
<pin> is a number from 0 to 7, corresponding to the 8 IO pins IO_0 to IO_7,
<mode> is a number between 0 and 4, corresponding to the selected mode, and
<object> is the id of an optionally bound object and is always 0 for simple IO operations.
To configure a pin as an input with activated pull-up (these are always activated on Nextion), we chose mode 0. Thus, turning IO_0 into a simple input, we use cfgpio 0,0,0 which corresponds to pinMode(0,INPUT_PULLUP). Using a pin as a “normal” digital output requires mode 2. So, configuring IO_1 as an output requires cfgpio 1,2,0, equivalent of pinMode(1,OUTPUT). It’s as simple as that!
Just for the sake of completeness, there is another output mode on the Nextion: The open drain output. It has no equivalent on AVR based Arduinos but several Arduino based framework extensions for ARM based MCUs allow the use of pinMode(1,OUTPUT_OPEN_DRAIN) for the use with either external pull-up resistors and/or hard wired OR-ing of multiple outputs on the same rail, for example in bus systems. On our Nextions, the mode 4 does that. Thus cfgpio 1,4,0 corresponds to pinMode(1,OUTPUT_OPEN_DRAIN).
Reading and writing GPIO pin levels
Now, after having configured our GPIO pins, we need to read or write them, equivalent of using digitalRead() and digitalWrite(). The Nextion environment makes it simple for us: There are 8 numeric system variables, pio0 to pio7, which can take two values: 0 for LOW, and 1 for HIGH. Thus driving our previously configured output pin IO_1 high is as easy as pio1=1, like digitalWrite(1,HIGH); or low with pio1=0, like digitalWrite(1,LOW);
Reading pins is also easy with this system variable approach: To read IO_0 and save the value in a numeric variable, just code for example n0.val=pio0 or sys0=pio0. It would be written as myvar=digitalRead(0); in an Arduino environment. Sometimes, you may even use the pio system variable directly, for example if(pio0==1)…
And now to GPIO triggered events
Arduino’s attachInterrupt() function allows to run an arbitrary function, interrupting the normal program flow, when the logic level of the pin to which the interrupt function is attached changes. The Nextion framework has a slightly different and (in my humble opinion) even more efficient approach: Since there is no dedicated container for running arbitrary code available on Nextion, GPIO events can be bound to a component. Once bound to a component, each change from high to low will trigger a TouchPress event and a change from low to high a TouchRelease event for this component and the corresponding event code is executed. This allows basically to execute distinct functionalities, depending on if the pin level is falling or raising with one single configuration, which is not so easy in Arduino C/C++.
If we don’t want a visual component for that on our screen, we can always take a small (5 x 5px) hotspot and hide it in a corner.
To configure a pin as an input AND bind it to a component to trigger events, we use cfgpio mode 1. Let’s assume that the hotspot whose event code we want to trigger has the object id 8 and that the want to use IO_2 for this purpose, we put simply cfgpio 2,1,8… where? in the page’s preinitialize event… why? As soon as we change the current page, all its components are unloaded from memory, and thus, their gpio bindings are lost. Thus, we have to make sure that the bindings are restored when the page is loaded again, and that’s why we put the binding configuration in the preinitialize event.
Finally, lets do some PWM
Using a pin as a PWM output is very simple on an Arduino. Just use the analogWrite() function. It makes even sure that the pin will be configured as an output if it isn’t already. On the Nextion, we have to use cfgpio to configure a pin as a PWM output, first, using mode 3. For example, configuring pin IO_7 as a PWM output, requires the line cfgpio 7,3,0
Note: Not all the Nextion’s GPIO pins are PWM capable, only IO_4 to IO_7 on the Enhanced series and IO_6 and IO_7 on the Intelligent series.
By default, Arduino’s analogWrite() accepts values from 0 to 1023, corresponding to a 10bit resolution. For some Arduino variants, a language extension exists with analogWriteResolution(), but in most cases, the driver values are not proper powers of two, so most people struggle with the map() function to scale everything to fit. The Nextion framework makes it much simpler since PWM is most times used to generate analog output voltages (through a low pass filter) or to drive servos where the PWM duty cycle in % is the important element. So, to control our PWM output, we simply write a value between 0 and 100 to the corresponding ppm system variable pwm4 to pwm7. And if we want to prevent undefined states (important for driving servos!), we set a first duty cycle before even configuring the pin as a PWM output. For example, starting PWM with a 30% duty cycle on pin IO_7 is a two-liner: pwm7=30 and then cfgpio 7,3,0. Afterwards, we can change the duty cycle without reconfiguring the pin, simply with pwm7=65.
Another important parameter is the PWM frequency, which is a trade between physical resolution and frequency, depending on the processor’s internal timer clock source and frequency. Some Arduino based frameworks give us the analogWriteFrequency() function, but it isn’t available in the standard Arduino environment. That’s why for driving a servo at 50Hz, you often need a special servo library which works around the Arduino limitations by manipulating the timer registers in the bare metal way after doing lots of scaling and conversion. On the Nextion, the default PWM frequency is 1000Hz which is great if you need the PWM output as a DAC equivalent to generate a varying DC voltage using a low pass filter. But you can change it globally for all PWM pins in a range from 1Hz to 65535Hz, just by setting the pwmf system variable to the corresponding value. Setting up a Nextion HMI to drive a servo is as easy as writing pwmf=50.
That’s a lot of theory and I will leave it at it for today. In the next article, we’ll see how to apply this new knowledge in practice.