Skip to content

Make your game come alive: Control Physical Hardware with Raspberry Pi GPIO in Godot

Posted on:February 1, 2025

Introduction

To build Zero Kelvin, I needed a way to have user input and events from the Godot game engine interact with the physical world. Raspberry Pi support for Godot was limited at the time, but I was able to get a build of the game running on a Raspberry Pi 4 so I was confident it could be done.

But I was still left to figure out how to control Raspberry Pi GPIO from my Godot game!

First attempt: UDP

The first approach used a lightweight UDP server written in Python that ran on the Raspberry Pi. Then, I’d create a UDP client in Godot and pass messages back and forth. You can read about the approach more here, but here’s a simplification (pseudo-code):

# server.py
def start_server(host='127.0.0.1', port=12345):
    while True:
        data, addr = udp_socket.recvfrom(1024)
        try:
            message = json.loads(data)

            # see: https://projects.raspberrypi.org/en/projects/physical-computing/14
            update_motors(message)
        except json.JSONDecodeError as e:
            log.error(f"Error decoding JSON from {addr}: {e}")

if __name__ == "__main__":
    start_server()
# motors.gd
extends Node

func _physics_process(delta):
	# Left Motor
	if Input.is_action_pressed("left_stick_up"):
		up(Motor.LEFT)
	elif Input.is_action_pressed("left_stick_down"):
		down(Motor.LEFT)
	else:
		stop(Motor.LEFT)
	
	if Input.is_action_pressed("right_stick_up"):
		up(Motor.RIGHT)
	elif Input.is_action_pressed("right_stick_down"):
		down(Motor.RIGHT)
	else:
		stop(Motor.RIGHT)

func set_motor(direction: int, speed: float):
	motors[direction] = speed * SPEED

func up(direction: int):
	set_motor(direction, 1)

func down(direction: int):
	set_motor(direction, -1)

func stop(direction: int):
	set_motor(direction, 0)

func _connect():
	client.connect_to_host(host, port)
	call_deferred("set_process", false)  # Stop processing if needed

func _update_daemon():
	var data = JSON.stringify({
		"left_motor_value": motors[Motor.LEFT],
		"right_motor_value": motors[Motor.RIGHT]
	})
	client.put_packet(data.to_utf8_buffer())

Problems

This approach worked and I was actually really impressed with how responsive the controls felt. Still, there were a few things I didn’t like about the design.

  1. It resulted in a massive flood of UDP messages because I was sending a message over UDP in every process call. The actual implementation had things like sequencing built in and there’s lots of methods to control the rate that this happens, but it was still basically betting that the server could handle more traffic than Godot would send. A bit comical when they both run on the same single ARM Cortex-A72 processor!
  2. It required a second language to handle GPIO communication. This became a problem when I started to use Godot’s (fantastic) One-Click Deploy feature. This allows me to deploy the Godot game from my PC (where I work) to the Raspberry Pi (where it runs). However, I wasn’t able to leverage the available configurations to do things like start and stop a Python server, or transfer Python code as text alongside the Godot game. A single language would be much simpler.

Second attempt: pigpio

As it’s been nearly a year since my first post, I decided to give this another shot. The Raspberry Pi ecosystem has advanced enough I figured there must be some software I could install on the Pi that would let me do this without needing to use another language.

Enter: pigpio! pigpio is a popular library for working with GPIO on the Raspberry Pi. It supports software PWM if required and uses a daemon to manage the GPIO state. The great thing about this library is that it comes with an extremely simple and easy to use command line interface.

Here’s an example of a pigpio command. To turn a motor on, I might need to set:

In pigpio, I can do this in Bash with simply:

$ pigs w 6   1   w 13   0   p 18      50

Broken down, that’s:

GPIO write PIN6 HIGH  write PIN13 LOW PWM PIN18 50%
pigs w     6    1     w     13    0   p   18     50

So simple! Combine this with Godot’s OS.execute(), and we’ve got a simple control interface for GPIO in Godot:

class_name Motors extends Node

@export_category("GPIO")
@export var motor_pin_1_up : int
@export var motor_pin_1_down : int
@export var motor_pin_2_up : int
@export var motor_pin_2_down : int
@export var motor_pin_pwm : int

@onready var GPIO = {
	Motor.LEFT: {
		'UP': motor_pin_1_up,
		'DOWN': motor_pin_1_down
	},
	Motor.RIGHT: {
		'UP': motor_pin_2_up,
		'DOWN': motor_pin_2_down
	}
}

enum Motor { LEFT, RIGHT }

@onready var VALUES = {
	Motor.LEFT: 0,
	Motor.RIGHT: 0
}

func up(motor: Motor):
	set_motor_speed(motor, 50)

func down(motor: Motor):
	set_motor_speed(motor, -50)

func stop(motor: Motor):
	set_motor_speed(motor, 0)

func set_motor_speed(motor: Motor, new_speed: int):
	var current = VALUES[motor]
	if current != new_speed: # if the value changes, update GPIO
		_set_pwm(GPIO[motor].UP, GPIO[motor].DOWN, new_speed)
		VALUES[motor] = new_speed

func _set_pwm(pin1, pin2, duty : int):
	if not engaged:
		print_debug("Ignoring request to set PWM beacuse motors are not engaged")

	var clamped_duty = max(-100, min(duty, 100))
	if duty != clamped_duty:
		push_warning("Invalid duty specified: %d. Clamping to: %d." % [duty, clamped_duty])
	
	# command:   ~~>  pigs w PIN1 DIR1 w PIN2 DIR2 p PWM_PIN DUTY%
	var cmd = String("pigs w %d   %d   w %d   %d   p %d      %d" % [pin1, int(duty > 0), pin2, int(duty < 0), motor_pin_pwm, abs(duty)])

	# run GPIO command using pigpio
	OS.execute("bash", ["-c", cmd], [], true)

Gotcha: Hardware PWM Channels

I ran into an interesting gotcha with the Raspberry Pi. This only affects you if your hardware uses PWM channels (for example, controlling servo motors).

To drive servo motors with the L298N motor driver, there’s a few ways I could control the motors. On the L298N, we can provide 4 inputs (from the Pi):

To drive them, I explored a few options:

Zero PWM Channels!

Using zero hardware PWM channels, I could drive 5v directly to ENA and ENB. This worked and was quite simple, but has the limtation of only being able to run the motors at a constant speed. The speed can be changed by installing resistors or other hardware in the circuit, it cannot be controlled by GPIO.

You could also connect ENA and ENB to regular GPIO ports and use software PWM, but the accuracy and jitter of this option made the motors sound a bit crunchy and it seemed like something you’d be able to feel when playing the game.

Two PWM Channels?

The ideal setup typically uses two hardware PWM channels. You connect one to ENA and one to ENB, sending a PWM signal over each that allows you to control the speed of each motor. The two pins control the direction.

However, the Raspberry Pi 4 only comes with two hardware PWM channels, and one of those channels is unavailable if the 3.5mm audio jack is engaged (as it is used for audio). Since I want to drive the sound of the game from Godot, and thus the Raspberry pi, I’d like to be able to keep one channel to run audio.

One PWM Channel.

So… using just one hardware PWM channel, I can connect it to both ENA and ENB. This drives both motors with the same speed but that is a variable speed, and I can still allowing me to use the 3.5mm port to drive audio for the game. A great compromise and a great example of the importance of requirement specification. All three of these approaches will drive motors with GPIO, but each come with thorns that can be costly later in a project.

Conclusion / How to

So, if you’re looking to build a Godot powered game that interacts with the physical world, here’s how I did it:

  1. Make sure you have a Raspberry Pi 4 with appropriate hardware (motors, drivers, etc.)
  2. Install Raspberry Pi OS (formerly Raspbian Linux) and connect your hardware
  3. Install pigpio on your pi (this is what allows you to control GPIO pins)
  4. Start the pigpio service and be sure to enable it so that it starts automatically when your pi boots up
  5. Write a Godot script that uses OS.execute to call pigpiod’s CLI (called pigs) and control the GPIO pins
  6. Configure Godot’s “One CLick Deploy” and enable SSH Remote Deploy (provide your Raspberry Pi’s SSH credentials)

You should now be able to write a Godot script that controls physical hardware connected to your Raspberry Pi!

extends Node

func _ready():
    # Example: Set GPIO 17 high
    OS.execute("pigs w 17 1")