Source: https://mokxi.com/learn/raspberry-pi-gpio-python
Updated: 2026-10-25

Learn

# Raspberry Pi GPIO with Python, with no Pi on the desk

Beginner

Written by the Mokxi team, updated October 25, 2026

Start building free Open the editor

Or see every lesson.

- 455 parts on the bench
- 43 boards running now
- 1.00x real time, on every board

Click to open it in the editor

Press Run. gpiozero turns GPIO17 on and off once a second, and print() shows each change.

Mokxi has the Raspberry Pi 4 Model B, the Raspberry Pi 5 and the Raspberry Pi Zero 2 W on its canvas. Put one down, write main.py with gpiozero or RPi.GPIO, the two libraries every Pi tutorial uses, and press Run. Your program drives the board’s 40-pin header, and the LEDs, buttons, servos and sensors wired to it behave as they would on the bench.

There is no Linux in the page. Your Python runs in a small Python engine (MicroPython, built for the browser), with gpiozero 2.0.1 and RPi.GPIO 0.7.1 written to behave like the real libraries: the same classes and arguments, the same warnings, the same errors. time.sleep(1) is one simulated second, so what a program measures here is what it measures on a Pi.

The circuit above is the first program on any Pi: an LED on GPIO17, header pin 11, through a 330 ohm resistor. Press Run and it is on for a second, off for a second, and the Serial Monitor shows each change. Measured on the pin, each half of the cycle is 1000 ms to within 0.2 ms.

## A button that toggles an LED

gpiozero’s Button calls a function of yours when it is pressed, and signal.pause() keeps the program waiting for it. The button here goes from GPIO2 to ground. GPIO2 has the board’s own 1.8 k pull-up, so it reads high until the button is pressed.

A real button bounces: its contacts open and close for a few milliseconds before they settle, and the simulated one does too. With bounce_time=0.05 gpiozero reports a change only once the pin has held it for 50 ms, so each press toggles the LED once. Leave it out and one press can toggle it more than once, as on a real Pi.

main.py: a button toggles an LED

## A servo on RPi.GPIO’s PWM

A hobby servo wants a pulse every 20 ms, 50 Hz, and the pulse’s length sets the angle. GPIO.PWM(18, 50) makes that pulse on GPIO18, and ChangeDutyCycle() sets its length as a percentage of the 20 ms. This servo reads 0.544 ms as 0 degrees and 2.4 ms as 180, so 0, 45, 90, 135 and 180 degrees are 2.72%, 5.04%, 7.36%, 9.68% and 12.00% duty, and the servo on the canvas moves to each angle in turn.

On a real Pi, RPi.GPIO’s PWM is software, and a busy Linux makes the pulses jitter, so a servo twitches. Here the pulses are exact. gpiozero’s AngularServo does the arithmetic for you; on a Pi 5 it sets the duty in whole percent, so a 50 Hz pulse moves in 10 µs steps, as it does there.

main.py: sweep a servo with RPi.GPIO

## An HC-SR04 distance sensor on a Pi 5

gpiozero’s DistanceSensor sends the HC-SR04 a trigger pulse and times the echo. The echo is a 5 V signal, more than a Pi’s 3.3 V pin can take, so it comes through a 1 k and 2 k divider: 5 V times 2/3 is 3.3 V. Leave the divider out and the Circuit Doctor says so.

Set the sensor to 100 cm and the program prints 100.0 cm, to within 0.5 cm; at 15 cm it prints 15.0 cm and the LED on GPIO17 lights, because threshold_distance=0.2 makes 20 cm the edge of “in range”.

main.py: read an HC-SR04 with gpiozero

## BCM or BOARD: two ways to number the pins

RPi.GPIO needs to be told how you count. GPIO.setmode(GPIO.BCM) uses the chip’s GPIO numbers, which gpiozero always uses: GPIO17. GPIO.setmode(GPIO.BOARD) uses the pin’s place on the header, counting from pin 1 at the corner: pin 11. They are the same pin. The program below lights header pins 11 and 13 in turn, half a second each, and they are GPIO17 and GPIO27.

Ask for a pin that is not a GPIO in the current numbering, such as BOARD pin 2, which is 5V, and RPi.GPIO stops with “The channel sent is invalid on a Raspberry Pi”, as it does on a real one. Set up a pin twice without GPIO.cleanup() and it warns that the channel is already in use, with the line.

main.py: RPi.GPIO with BOARD numbering

## An OLED on I2C with smbus2

The header’s I2C bus is /dev/i2c-1: SDA on GPIO2 (pin 3) and SCL on GPIO3 (pin 5), as on Raspberry Pi OS with I2C turned on in raspi-config. smbus2 and the older smbus talk to it, and the parts on it answer at their addresses: an SSD1306 OLED at 0x3C, a BME280 at 0x76. The program below draws text with a small driver in its own tab, sending the picture in 16-byte blocks the way the classic Pi tutorials do.

The bus runs at Raspberry Pi OS’s default 100 kHz on the simulated clock, so a 1 KB frame takes as long as it does on a desk: the program prints 100 to 125 ms a frame. Ask for an address nobody answers and smbus raises OSError: [Errno 121] Remote I/O error, the error a loose wire gives on a real Pi.

main.py: text on an SSD1306 with smbus2

## A potentiometer through an MCP3008

A Pi has no analog inputs, so a knob or a light sensor goes through an analog-to-digital converter, and the MCP3008 is the one gpiozero’s documentation uses. It sits on SPI0: CLK to GPIO11, DOUT to GPIO9, DIN to GPIO10 and CS to GPIO8, which is /dev/spidev0.0. MCP3008(channel=0) reads it through spidev, and led.source = pot hands the reading to a PWMLED.

The chip’s code is 1024 times the input over its reference, so a knob turned to 800 of 1023 on 3.3 V reads 800, and gpiozero’s value is 0.782, scaled as gpiozero 2.0.1 scales it. spidev works on its own too: spi.xfer2([1, 0x80, 0]) is the same reading by hand.

main.py: an MCP3008 with gpiozero

## RPi.GPIO on a Raspberry Pi 5

RPi.GPIO does not work on a Pi 5. Its pins are on a new chip, the RP1, where RPi.GPIO cannot find them, and the first GPIO.setup() stops with RuntimeError: Cannot determine SOC peripheral base address. Mokxi’s Pi 5 does the same, and the editor says to use gpiozero, which is what the Raspberry Pi documentation uses and which works on all three boards.

## What is simulated, and what is not

Simulated: the 40-pin header of each board, with its 3.3 V and 5 V pins and its grounds, GPIO 0 to 27 as inputs with their pull-ups and pull-downs or as 3.3 V outputs, PWM, edge callbacks with their debounce, I2C and SPI through smbus, smbus2, spidev and gpiozero’s SPI devices, and Python’s time on the simulated clock. print() and input() use the Serial Monitor, and Ctrl-C stops the program with KeyboardInterrupt.

Not simulated: Linux itself, so there is no desktop, no terminal and no sudo; pip and apt, so only the modules that come with the board can be imported; networking of any kind; the camera, the display and the audio; threads; and the I2C bus’s speed setting and clock stretching, so I2C runs at the default 100 kHz. The engine is MicroPython, not CPython, so a few things in the language differ: isinstance(True, int) is False, for one. A program that drives the pins runs unchanged.

## Questions

Is this a full Raspberry Pi emulator?

No. There is no Linux and no Arm processor in the page. Your Python program runs in a Python engine in the browser, and its gpiozero and RPi.GPIO drive the board’s 40-pin header. Everything a circuit can see of a Pi is there; the desktop, the shell and the operating system are not.

Which Raspberry Pi boards are there?

The Raspberry Pi 4 Model B, the Raspberry Pi 5 and the Raspberry Pi Zero 2 W, each with the same 40-pin header. RPi.GPIO works on the Pi 4 and the Zero 2 W and fails on the Pi 5, as it does on the real one; gpiozero works on all three.

Do I2C and SPI work?

Yes. `/dev/i2c-1` and `/dev/spidev0.0` and `0.1` are there, as on Raspberry Pi OS with both interfaces turned on, and smbus, smbus2, spidev and gpiozero’s MCP3008 talk to the I2C and SPI parts on the canvas at the speed of the bus.

Can I pip install a library?

No. pip and apt are not simulated. Add a library’s .py file as a tab of its own and import it from main.py, which works for libraries written in plain Python.

Does it download anything?

The first Run of a Raspberry Pi project downloads the Python engine, about 150 KB, and the browser keeps it for every later Run. An Arduino project never downloads it.

Related

## Keep going

Raspberry Pi Pico MicroPython in Your Browser Raspberry Pi Pico Pinout: GPIO, PWM, ADC and Power The Raspberry Pi board page The Raspberry Pi simulator Open the gpiozero blink in the editor A PIR motion sensor with gpiozero Beginner Traffic lights and a crossing button with gpiozero Beginner An MQ-2 gas sensor through an MCP3008 Intermediate The project page, with the full sketch Beginner

Sources

## Where the facts on this page come from

- gpiozero documentation (2.0.1)
- RPi.GPIO 0.7.1 on PyPI
- Raspberry Pi documentation: the GPIO header

## Build this for real

Open the editor, change a value and watch the number move with it. Nothing to install, and no account needed.

Start building free Open the editor
