Skip to main content

Buttons, pins and gamepad

On Windows, the controls of your game are keys. On a board, they are buttons wired to its GPIO pins, the Adafruit I2C gamepad or, on the ESP32-S31, a USB keyboard. Your program reads them the same way in every case: it assigns each action with Input.map and asks for it with Input.pressed. The functions are in the Input module; this page covers what changes on the board.

If your game is made of scene editor rooms, you don't write Input.map: the controls, their pins and the gamepad are set on the Game tab, with Edit controls..., and the code the editor generates assigns them (see The Game controls window).

Input.map on the board​

What Input.map accepts depends on where the program runs:

Input.map(action, …)WindowsESP32 / ESP32-S3ESP32-P4ESP32-S31
With a key name, such as "LEFT"YesNo: throws ERR_TYPE_MISMATCHNo: throws ERR_TYPE_MISMATCHYes: a key of the USB keyboard
With a GPIO, such as 39No: throws ERR_TYPE_MISMATCHYes, from 0 to 63Yes, from 0 to 63Yes, from 0 to 63

A GPIO outside 0 to 63 throws ERR_INVALID_VALUE. They are exceptions: unless you catch them with try, they stop the program (see Errors and exceptions).

For the same program to work on Windows and on the board, choose the mapping with platform(), as the examples that come with JARU IDE do:

use Input

const MOVE_LEFT = 0
const MOVE_RIGHT = 1
const JUMP = 2

if (platform() == HW_WIN32) then
Input.map(MOVE_LEFT, "LEFT")
Input.map(MOVE_RIGHT, "RIGHT")
Input.map(JUMP, "SPACE")
elsif (platform() == HW_ESP32) then
// The three buttons of the LILYGO T4, the ones the examples use
Input.map(MOVE_LEFT, 39)
Input.map(MOVE_RIGHT, 38)
Input.map(JUMP, 37)
end

Compare with HW_ESP32, not with "any board": the pins of one board are no good on another (see Pins you cannot use for a button). The constants of each chip, such as HW_ESP32S3 or HW_ESP32P4, are those of platform(), in the standard library.

How to wire a button​

Input.map sets the pin as an input, with its internal pull-up resistor, and reads it inverted: the action is pressed while the pin is at 0 V. Wire the button between the pin and GND. At rest, the resistor keeps the pin at 3.3 V and the action released.

Board profiles don't say which pin each button is on: check your board's schematic. The VM doesn't check it: it accepts any number from 0 to 63, even if your chip has no such GPIO or the board uses it for something else.

The GPIO of each chip​

ChipGPIO it has
ESP320 to 39, except 24 and 28 to 31. 34 to 39 are input only.
ESP32-S30 to 48, except 22 to 25.
ESP32-P40 to 54.
ESP32-S310 to 61, except 29 and 41.

Pins you cannot use for a button​

ChipPinsWhy
AllThose of the display, the SD card, the sound and the I2C busThey already do something else. They are in your board's profile: the Display, Storage and Audio pages of ESP32 Tools.
ESP321 and 3They are the serial console: what your program prints and the VM's messages come out through them.
ESP3234 to 39They have no internal pull-up resistor. Without an external one, the pin floats, and the action may read as pressed with nobody touching it. The buttons of the LILYGO T4 have their own.
ESP32-S343 and 44The serial console.
ESP32-S319 and 20They are the chip's own USB, which some boards connect through, such as the Waveshare ESP32-S3-Touch-LCD-2.8 and the JaruCade.
ESP32-S333 to 37They belong to the octal PSRAM, the memory the S3 VM works with. With Input.map on 37, the board hangs and restarts over and over.
ESP32-P437 and 38The serial console.
ESP32-S3158 and 59The serial console.

A button that gives 3.3 V when pressed​

If your button pulls the pin to 3.3 V when pressed, Input.map reads it inverted. Read it yourself with the GPIO module and pass its state to the action with Input.set, once per frame and before asking for the actions:

use GPIO
use Input
use Display

const BUTTON = 21 // the GPIO of your button
const JUMP = 2

GPIO.pinmode(BUTTON, GPIO.PULLDOWN) // released, the pin stays at 0 V

Display.open(320, 240)
Display.fps = 30
while (true)
Input.set(JUMP, GPIO.read(BUTTON) == 1) // first thing in the frame
if (Input.justPressed(JUMP)) then
println("Jump!")
end
Display.update()
end

With Input.set, the action works like the others: Input.pressed, Input.justPressed and Input.released work as usual. GPIO 34 to 39 of the ESP32 have no internal pull-down either.

Read pins set as inputs

To read a pin, set it as an input: GPIO.INPUT, GPIO.PULLUP or GPIO.PULLDOWN. On the ESP32-P4 and the ESP32-S31, a pin in GPIO.OUTPUT is output only: GPIO.read() always returns 0 (see Supported boards).

The I2C gamepad​

The I2C gamepad is the Adafruit Mini I2C Gamepad (seesaw, product 5743): a joystick and six buttons, A, B, X, Y, Start and Select. It connects to the board's I2C bus and answers at address 0x50 (0x51 to 0x53 if you cut its A0 and A1 jumpers). It is the JaruCade's gamepad.

The bus pins are in the board's profile, on the Storage page, I2C Bus group: SDA 11 and SCL 10 on the JaruCade and the Waveshare ESP32-S3-Touch-LCD-2.8, and SDA 7 and SCL 8 on the two ESP32-P4 boards. The VM doesn't read them: the scene editor and the I2C driver manager use them to write your code.

With the scene editor​

In a game made of rooms, the gamepad is turned on in the Game tab, with Edit controls...: you choose which gamepad button drives each control, and the code the editor generates starts the gamepad and reads it in every room. It's all in The I2C gamepad, and a full case, with StarterPlatformer, in Your first game with rooms.

The scene editor's gamepad on the ESP32-S31

On the ESP32-S31, with the gamepad on and no control with a GPIO, the game doesn't use the keys of the USB keyboard: if the gamepad doesn't answer, it is left with no input. If you play with the keyboard, turn the gamepad off with Gamepad on, which becomes Gamepad off.

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

From your code​

Outside the scene editor, the gamepad is handled with the SeesawGamepad driver. Add it to the project from the I2C driver manager, in the Input category, with Inser Driver: it copies it to Code/Drivers and writes its import (see I2C drivers).

The driver has no actions of its own: you tell it which Input action each button and each joystick direction drives, and on every frame, with update(), it reads the gamepad and passes it to the actions with Input.set. Your game keeps asking with Input.pressed.

use I2C
use Input
use Display
import "Drivers/DRV_SEESAW_GAMEPAD.aru"

const MOVE_LEFT = 0
const MOVE_RIGHT = 1
const JUMP = 2

var pad = nil
if (platform() == HW_WIN32) then
Input.map(MOVE_LEFT, "LEFT")
Input.map(MOVE_RIGHT, "RIGHT")
Input.map(JUMP, "SPACE")
else
I2C.init(0, 11, 10, 400000) // the JaruCade's bus: SDA 11 and SCL 10
pad = SeesawGamepad(0)
if (pad.beginChecked()) then
pad.bind(JUMP, pad.BTN_A)
pad.bindAxes(MOVE_LEFT, MOVE_RIGHT, -1, -1, 200)
else
println("No gamepad: error ", pad.getLastError())
pad = nil
end
end

Display.open(320, 240)
Display.fps = 30
while (true)
if (pad != nil) then pad.update() end // first thing in the frame
if (Input.pressed(JUMP)) then
println("Jump")
end
Display.update()
end
MethodWhat it does
SeesawGamepad(port)Creates the driver on an I2C port already opened with I2C.init.
configureAddress(addr)Changes the address, if you have cut the jumpers. By default, 0x50.
beginChecked()Looks for the gamepad, checks that it is the 5743 and sets it up. Returns false if it isn't there; getLastError() says why.
bind(action, button)The button drives the action: BTN_A, BTN_B, BTN_X, BTN_Y, BTN_START or BTN_SELECT.
bindAxes(left, right, up, down, deadZone)The four joystick directions drive those actions; -1, none. deadZone is how far it moves from the center before it counts: the joystick goes from 0 to 1023, with the center at 512.
update()Reads the gamepad and sets the actions. Call it once per frame, before reading the actions. Returns false if a reading failed.
pressed(button), axisX() and axisY()The button and the joystick as the last update() read them.

Make your Input.map calls before creating the gamepad: Input.map clears what the gamepad has put in that action. A button and a direction can drive the same action.

The ESP32-S31 USB keyboard​

The ESP32-S31 board, the Korvo-1, has a USB-A port where you plug in a keyboard. With it, Input.map accepts the same key names as on Windows (the list is in Input), and GPIO too:

use Input

const MOVE_LEFT = 0
const MOVE_RIGHT = 1
const JUMP = 2

if (platform() == HW_WIN32 or platform() == HW_ESP32S31) then
// The same keys on Windows and on the ESP32-S31 USB keyboard
Input.map(MOVE_LEFT, "LEFT")
Input.map(MOVE_RIGHT, "RIGHT")
Input.map(JUMP, "SPACE")
end
  • It is turned on in ESP32 Tools, with the USB keyboard checkbox on the Hardware page, which only shows up with the ESP32-S31. The Korvo-1 profile comes with it ticked.
  • The keyboard, in its simplest mode. The VM opens the keyboard in the USB boot mode (the boot protocol). A keyboard without it isn't read, and neither are mice, USB gamepads or multimedia keys.
  • Two keyboards at once, at most, and up to six keys at once on each, not counting SHIFT, CTRL and ALT. With more, the keyboard doesn't say which ones, and the VM keeps the ones it had.
  • SHIFT, CTRL and ALT are the left ones, as on Windows.
  • You can plug and unplug it with the program running. The serial monitor tells you: "USB host ready: plug a keyboard into the USB-A port." at startup, and then "USB keyboard connected" and "USB keyboard disconnected".
  • If it stops responding, the monitor says "USB keyboard transfer error: unplug it and plug it again.": unplug it and plug it in again. If the board stops seeing the keyboard altogether, turn the board's power off and on again.

The Korvo-1 buttons​

The four buttons of the Korvo-1 (SET, MODE, VOL- and VOL+) don't each go to their own GPIO: they all go to GPIO 42, each with a different voltage. Input.map can't read them; read them with GPIO.aread(42). On the board the VM was tested with, GPIO.aread(42) gives 0 with nothing pressed, 315 with SET, 853 with MODE, 1369 with VOL- and 1816 with VOL+. This function puts the boundary halfway between each two readings:

use GPIO

// The four Korvo-1 buttons go to GPIO 42, each with its own voltage.
// Returns the one pressed, or "" if there is none.
func KorvoButton()
var v = GPIO.aread(42)
if (v < 157) then return "" end
if (v < 584) then return "SET" end
if (v < 1111) then return "MODE" end
if (v < 1592) then return "VOL-" end
return "VOL+"
end

Check your board's numbers first, with println(GPIO.aread(42)). With two buttons pressed at once, the reading doesn't say which ones. To use them as actions, pass them with Input.set, as in A button that gives 3.3 V when pressed.