Skip to main content

GPIO module

The GPIO module drives the board's input and output pins: it reads and writes digital levels, reads analog values with the converter (ADC), outputs PWM signals and calls a function when a pin changes (interrupts). On Windows, the pins are those of the IDE's GPIO simulator.

use GPIO

JARU GPIO module screenshot for configuring digital pins, analog inputs, PWM, and interrupts

Platforms​

WindowsESP32 / ESP32-S3ESP32-P4ESP32-S31
The pinsThose of the GPIO simulator, from 0 to 48The chip'sThe chip'sThe chip's
GPIO.read of a pin in GPIO.OUTPUTThe last thing writtenThe last thing written00
Default PWM5000 Hz and 8 bits (from 0 to 255)2000 Hz and 10 bits (from 0 to 1023)SameSame
PWM channels1616 on the ESP32 and 8 on the ESP32-S388
Maximum PWM resolution16 bits20 bits on the ESP32 and 14 on the ESP32-S320 bits20 bits
GPIO.areadFrom 0 to 4095: whatever the simulator card saysFrom 0 to 4095From 0 to 4095 on the ADC1 pins, and 0 on the restThe raw value of its ADC, 17-bit and uncalibrated
resolution(bits) and frequency(hz) without a pinGive an errorChange every channelSameSame
GPIO.ONLOW and GPIO.ONHIGHFire once on reaching the levelFire again and again while the pin stays at that levelSameSame
Arguments that aren't validException (except pinmode, write and read, which don't check them)An error value and, nearly always, a message on the serial monitorSameSame

On Windows, the module touches no hardware: it reads and writes in a memory it shares with the simulator, whether it's open or not. Without the simulator, nobody moves the inputs: an input pin reads 0 (1 with GPIO.PULLUP), and GPIO.aread, 0. Interrupts are simulated: the VM looks at the pins every few milliseconds.

On the ESP32-P4 and the ESP32-S31, an output pin can't be read

On those boards, GPIO.pinmode(pin, GPIO.OUTPUT) leaves the pin output-only: GPIO.read() of that pin always returns 0, and GPIO.onInterrupt() doesn't fire with what the program writes. On the ESP32 and ESP32-S3 it does. If you need to know how you left an output pin, keep it in a variable.

Functions​

FunctionWhat it doesReturns
GPIO.pinmode(pin, mode)Says whether a pin is an input or an output, and with which resistortrue
GPIO.write(pin, value)Sets an output pin high or lowtrue
GPIO.read(pin)Reads the level of a pin0 or 1
GPIO.aread(pin)Reads the analog value of a pinAn integer
GPIO.awrite(pin, value)Outputs a PWM signal on a pintrue
GPIO.resolution(bits) / GPIO.resolution(pin, bits)Changes the PWM resolutiontrue
GPIO.frequency(hz) / GPIO.frequency(pin, hz)Changes the PWM frequencytrue
GPIO.getduty(pin)Reads the PWM value of a pinAn integer
GPIO.detach(pin)Releases the PWM of a pintrue
GPIO.onInterrupt(pin, mode, function)Calls a function when a pin changestrue
GPIO.offInterrupt(pin)Stops watching a pintrue

pin is the GPIO number, as on the chip: GPIO.read(15) reads GPIO 15. The VM doesn't check that the pin exists on your board.

pinmode(pin, mode)​

Gets pin pin ready for the use you're going to give it. mode is one of the constants:

ModeWhat for
GPIO.INPUTInput, with no resistor.
GPIO.OUTPUTOutput. On the ESP32 and ESP32-S3, the pin can also be read; on the P4 and the S31, it can't (see above).
GPIO.PULLUPInput with the internal resistor to the high level: with nothing connected it reads 1. It's the usual one for a button that connects the pin to GND.
GPIO.PULLDOWNInput with the internal resistor to the low level: with nothing connected it reads 0.
GPIO.OPEN_DRAINOpen-drain output: the pin pulls down to GND or is left floating.

On a board, a mode that isn't one of these gives an error value and writes Invalid pin mode in 'gpio.pinmode()'. on the serial monitor. On Windows, any mode is accepted.

On the ESP32, GPIOs 34 to 39 are input-only and have no internal resistors: there, GPIO.PULLUP and GPIO.PULLDOWN do nothing. It's covered in Buttons, pins and gamepad.

use GPIO

GPIO.pinmode(2, GPIO.OUTPUT) // GPIO 2, as an output
GPIO.pinmode(15, GPIO.PULLUP) // GPIO 15, as an input with pull-up

write(pin, value)​

Sets the output pin pin high with value equal to 1, true or HIGH, and low with 0, false or LOW. Any other number that isn't 0 counts as high.

use GPIO

GPIO.pinmode(2, GPIO.OUTPUT)

// An LED that blinks 5 times
for (var i = 0; i < 5; i++)
GPIO.write(2, HIGH)
pause(500)
GPIO.write(2, LOW)
pause(500)
end

read(pin)​

Reads the level of pin pin: 1 (high) or 0 (low).

use GPIO

GPIO.pinmode(15, GPIO.PULLUP)

if (GPIO.read(15) == 0) then
println("Button pressed") // with PULLUP, pressed is 0
else
println("Button released")
end

aread(pin)​

Reads the value of the analog converter (ADC) on pin pin: the higher the voltage, the higher the number. On the ESP32, ESP32-S3 and P4 it goes from 0 to 4095 (12 bits).

  • Only some pins have an ADC: check your board's pinout. On the P4, the ones that aren't on ADC1 always read 0.
  • On the ESP32-S31, the value is the raw one from its ADC, 17-bit and uncalibrated: it doesn't go from 0 to 4095. Measure which values your circuit gives you.
use GPIO

var value = GPIO.aread(34)
println("ADC: ", value, " Volts: ", value * 3.3 / 4095)

The volts calculation assumes 4095 is 3.3 V: take it as an approximation.

awrite(pin, value)​

Outputs a PWM signal on pin pin: a wave that switches very fast between high and low, and that on an LED looks like an in-between brightness. value is the time spent high (the duty), from 0 to the maximum of the resolution: by default, from 0 to 1023 on a board and from 0 to 255 on Windows.

The first time you use a pin with PWM, the VM assigns it one of its PWM channels. You don't need pinmode.

On a board, a value above the maximum is clipped to the maximum, and a negative one counts as 0; on Windows they're clipped between 0 and 65535. If there are no free channels left (how many there are, in Platforms), it throws ERR_OUT_OF_MEMORY (No free LEDC channels.); detach frees a pin's one.

use GPIO

GPIO.resolution(2, 8) // 8 bits on GPIO 2: from 0 to 255, as on Windows
GPIO.frequency(2, 5000)

// An LED that breathes 3 times
for (var n = 0; n < 3; n++)
for (var i = 0; i < 256; i = i + 5)
GPIO.awrite(2, i)
pause(20)
end
for (var i = 255; i >= 0; i = i - 5)
GPIO.awrite(2, i)
pause(20)
end
end

resolution(pin, bits)​

Changes the PWM resolution of pin pin to bits bits: with 8, awrite goes from 0 to 255; with 10, from 0 to 1023; with 12, from 0 to 4095. Without pin, resolution(bits) changes every channel; on Windows that form gives ERR_INVALID_ARG_NUMBER, so always pass the pin.

On Windows, bits is clipped between 1 and 16. On a board it isn't checked: beyond the chip's maximum (see Platforms), the channel doesn't change and there's no warning.

use GPIO

GPIO.resolution(2, 12) // GPIO 2, from 0 to 4095

frequency(pin, hz)​

Changes the PWM frequency of pin pin to hz hertz. Without pin, frequency(hz) changes every channel; like resolution, that form gives an error on Windows.

On a board, PWM channels go in pairs: the first and the second pin you use with PWM share frequency and resolution, and so do the third and the fourth, and so on. Changing one's changes the other's. If you need two different frequencies (a servo at 50 Hz and an LED at 5000 Hz), the two pins have to fall in different pairs: for instance, the first and the third to use PWM.

use GPIO

GPIO.frequency(2, 1000) // 1 kHz on GPIO 2

getduty(pin)​

Returns the value the PWM of pin pin has: the last one you gave it with awrite.

A pin without PWM returns 0.

use GPIO

GPIO.awrite(2, 512)
println(GPIO.getduty(2)) // 512

detach(pin)​

Disconnects pin pin from its PWM channel: the pin stops outputting the signal.

The channel becomes free for another pin, and a later awrite on the same pin gives it one again.

use GPIO

GPIO.awrite(2, 512)
// … use the PWM …
GPIO.detach(2)

onInterrupt(pin, mode, function)​

Calls function each time pin pin does what mode says:

ModeIt fires when…
GPIO.RISINGThe pin goes from low to high.
GPIO.FALLINGThe pin goes from high to low. It's the one for a button with GPIO.PULLUP.
GPIO.CHANGEThe pin changes, either way.
GPIO.ONLOWThe pin is low. On a board it fires again and again while it stays that way; on Windows, once when it gets there.
GPIO.ONHIGHThe pin is high, like ONLOW.

function can be a function (onPress), a method of an object (button.onPress) or the name of a global function in a string ("onPress"). It takes two parameters: the pin number and its level (0 or 1) at the moment of the change. If it has another number of parameters, the events are dropped without a word.

  • onInterrupt doesn't set the pin up: call pinmode first.
  • There can be 16 pins with interrupts at once. Another onInterrupt on a pin that already has one changes its function and its mode.
  • For a button, FALLING or RISING are better than ONLOW or ONHIGH: a pin that stays at one level would fill the VM's event queue.

If something isn't valid, on Windows it throws an exception: ERR_TYPE_MISMATCH if an argument isn't of the right type, ERR_INVALID_VALUE if the mode isn't in the table, and ERR_OUT_OF_MEMORY if there are already 16. On a board it returns an error value: ERR_TYPE_MISMATCH, ERR_OUT_OF_MEMORY, or ERR_INVALID_OPERATION if the pin doesn't accept interrupts; the mode isn't checked.

use GPIO

var presses = 0

func onPress(pin, level)
presses = presses + 1
println("Button on pin ", pin, ": ", presses)
end

GPIO.pinmode(15, GPIO.PULLUP)
GPIO.onInterrupt(15, GPIO.FALLING, onPress)

// The program stays alive to receive the interrupts
while (true)
pause(100)
end

With a method, the function runs with its this, and the interrupt keeps the instance alive while it's registered:

use GPIO

class Button
def init(pin)
this.pin = pin
this.presses = 0
GPIO.pinmode(pin, GPIO.PULLUP)
GPIO.onInterrupt(pin, GPIO.FALLING, this.onPress)
end

def onPress(pin, level)
this.presses = this.presses + 1
end
end

var button = Button(15)
When the function runs

The chip detects the change instantly, but your function is run by the VM inside the program, like the Timer callbacks: one at a time, at the safe points (when a loop goes round and when calling or returning from a function) and never inside pause(). That's why the program has to stay alive, with a loop of short pauses. When the program ends, the interrupts are removed.

offInterrupt(pin)​

Stops watching pin pin and returns true, also if it had no interrupt. On a board, a pin that doesn't accept interrupts returns an error value (ERR_INVALID_OPERATION).

use GPIO

func onPress(pin, level)
println("Pressed")
end

GPIO.pinmode(15, GPIO.PULLUP)
GPIO.onInterrupt(15, GPIO.FALLING, onPress)
// … later on
GPIO.offInterrupt(15)

Constants​

The pinmode modes:

ConstantValueMode
GPIO.INPUT0x01Input
GPIO.OUTPUT0x03Output
GPIO.PULLUP0x05Input with pull-up
GPIO.PULLDOWN0x09Input with pull-down
GPIO.OPEN_DRAIN0x13Open-drain output

The onInterrupt modes:

ConstantValueIt fires when…
GPIO.RISING1It goes up
GPIO.FALLING2It goes down
GPIO.CHANGE3It changes
GPIO.ONLOW4It's low
GPIO.ONHIGH5It's high

And the two levels, which belong to the language and are written without GPIO.:

ConstantValue
HIGH1
LOW0

Complete example​

A button on GPIO 15 turns an LED on GPIO 2 on and off. The interrupt function ignores the button's bounces: a mechanical button gives several edges on each press.

use GPIO

const BUTTON_PIN = 15
const LED_PIN = 2

GPIO.pinmode(LED_PIN, GPIO.OUTPUT)
GPIO.pinmode(BUTTON_PIN, GPIO.PULLUP) // the button connects the pin to GND: pressed is 0

var lit = false
var lastPress = 0

func onPress(pin, level)
if (clock() - lastPress > 200) then // the button's bounces, out
lastPress = clock()
lit = !lit
GPIO.write(LED_PIN, lit)
end
end

GPIO.onInterrupt(BUTTON_PIN, GPIO.FALLING, onPress)

while (true)
pause(50)
end

In the Windows GPIO simulator, connect a Pushbutton to GP15. To see the LED in the simulator, read the warning in Platforms.

Pitfalls and known issues​

  • resolution and frequency without a pin, on Windows. They give ERR_INVALID_ARG_NUMBER: always pass the pin.
  • Different default PWM on Windows. On a board, awrite goes from 0 to 1023 by default; on Windows, from 0 to 255. Call GPIO.resolution(pin, bits) at the start so that it behaves the same on both.
  • PWM channels in pairs, on a board. Two pins with PWM in the same pair of channels share frequency and resolution.
  • The board's buzzer sound also uses a PWM channel.
  • Interrupts need the program alive. They don't run inside pause(), and when the program ends they're removed.
  • Button bounces. Each press of a mechanical button gives several edges: discard the ones that arrive very close together, as in the example.

All the known issues of JARU, together, are in Known issues.