Skip to content

GPIOs

Microcontrollers' primary peripherals are digital I/O ports, commonly known as General Purpose Input/Output or short GPIO. Typically, these digital ports operate in a three-state mode, which means that for any given bit:

  • Output Mode: The pin can be driven to either a logic 0 or logic 1.
  • Input Mode: The pin's output drivers are switched to a high-impedance (Hi‑Z) state.

Note that not all GPIO pins support all three states; some may be input-only.

Microcontrollers offer a limited number of pins and the built-in peripherals might require more connections than are available, each GPIO pin often supports several alternative functions. For instance, a single physical pin might serve as a basic digital I/O, an analog-to-digital (A/D) channel, or as part of a communication interface. The diagram excerpt below from the Raspberry Pi Pico datasheet clearly illustrates these alternative functions.


Pins with alternative functions

In addition to handling the physical connection of the GP27 digital I/O pin, pin 32 can be reconfigured via software to operate either as an A/D input (ADC1) or as the clock signal (I2C1 SCL) for one of the I2C communication buses. Although the switching is managed by high-level functions that abstract the hardware details, these functions actually modify specific bits in the microcontroller's registers.


Digital outputs

While digital outputs are primarily intended to control digital inputs, modern microcontrollers allow these outputs to drive relatively heavy loads. The diagram below shows an LED on the Raspberry Pi Pico being driven directly from a port pin. Notice that the LED is not connected directly to the pin; instead, a current-limiting resistor is used to control the LED's current.

The simplest way to connect a push button to a digital input is to enable either pull-up or pull-down resistors, and then connect the push button between the input and ground or the supply voltage. For example, with a pull-up resistor enabled, an open circuit results in a logic 1, while a closed circuit produces a logic 0 on the digital input.


⚡Hands-on tasks

Let's create a program to control the LED connected to GPIO25 on the Raspberry Pi's microcontroller! The green LED conencted to GPIO25 can be found on the Pico board not on the robot.

Setup a project and connect to the Pico Robot

If you need a refresher on setting up the project or connecting to the Pico Robot, refer to Getting Started

main.py
# Modules required for the task: machine and time
import machine
import time

# The onboard LED on the Pico 2 is accessed via Pin("LED")
led = machine.Pin("LED", machine.Pin.OUT)

# The LED state can be toggled using the toggle() function
led.toggle()

Run the program multiple times as a test and verify its functionality!


Now, extend the program to make the LED blink at 1 Hz. This means the LED should change state every 500 ms.

A simple way to achieve blinking is to execute led.toggle() inside the while True: infinite loop, with a 0.5-second delay after each toggle. This ensures cyclic execution and proper timing, making the LED blink at 1 Hz period (one blink per second).

main.py
# Modules required for the task: machine and time
import machine
import time

# The onboard LED on the Pico 2 is accessed via Pin("LED")
led = machine.Pin("LED", machine.Pin.OUT)

# The LED state can be toggled using the toggle() function
while True:
    led.toggle()
    time.sleep(0.5)

Task 1: Replace the toggle() function with explicit digital output control functions (on() and off()). For more details find the according section in the MicroPython Documentation at section Pins and GPIO.

What does toggle() do?

The toggle() function inverts the current state of the digital output. In other words, if the output is on, it turns it off, and if it is off, it turns it on. This can be achieved manually by reading the current state of the pin and then setting it to the opposite value.

In the following explicit control example, we use pin.on() to turn the LED on and pin.off() to turn it off. We also print the state of the pin after each operation by calling pin.value().

    from machine import Pin
    from utime import sleep

    pin = Pin("LED", Pin.OUT)

    while True:
        pin.on()
        print("Pin state: ", pin.value())
        sleep(1) # sleep before turning off
        pin.off()
        print("Pin state: ", pin.value())
        sleep(1)   # sleep before turning on in the next loop iteration
Instead of explicitly calling on() and off(), you can achieve the same effect by toggling the current state. In Python, you use the not operator to invert a Boolean value. This is similar to the logical inversion operator ! in C. The following code reads the current state with pin.value(), inverts it using not, and then writes the inverted value back to the pin:

    from machine import Pin
    from utime import sleep

    pin = Pin("LED", Pin.OUT)

    while True:
        pin.value(not pin.value())
        print("Pin state: ", pin.value())
        sleep(1)   # sleep before toggle in the next loop iteration

Each method—on(), off(), value(), and toggle()—has its own advantages depending on the task. Using on() and off() ensures clear and predictable control, while value() allows both reading and writing the pin state dynamically. The toggle() method or logical inversion with value(not pin.value()) provides a concise way to switch states without tracking them manually. Choosing the right approach depends on the clarity, readability, and specific needs of your application.


Handling digital input

Digital inputs in CMOS technology tend to "float" when not actively driven, meaning they may reside at an undefined logic level. To avoid this, pull-down or pull-up resistors can be activated. These resistors ensure that the input maintains a defined logic level when left unconnected, and because their resistance values are high, they do not interfere with external signals.

Microcontrollers support pull-up and pull-down resistors to ensure a stable logic level when an input is not actively driven. In this example, we enable a pull-up resistor, which means:

  • By default, GP9 will be at logic level 1 (HIGH) when not externally controlled.
  • When GP9 is connected to GND, it will be pulled down to logic level 0 (LOW).

Switch with pull up resistor


We will enhance the previous program by adding a check for the GP9 input state. To achieve this, we must define GP9 as an input in the microcontroller.

To control GP9, use a jumper wire to connect it to GND pin next to it on the same pin head, which will pull it down to logic 0. 👉 GP9 can be found next to the main switch for easy access.

Follow the example control code and test its functionality to ensure proper operation.

main.py
# Import required modules
import machine
import time

# Configure the onboard LED as an output
led = machine.Pin("LED", machine.Pin.OUT)

# Configure GPIO9 as an input with a pull-up resistor (default state is LOW)
GP9_in = machine.Pin(9, machine.Pin.IN, machine.Pin.PULL_UP)

# Infinite loop to control LED blinking speed based on GP9 input
while True:
    led.toggle()  # Toggle LED state
    if GP9_in.value() == 0:  # If GP9 is LOW (0), blink the LED faster
        time.sleep(0.5)  # Delay 500 milliseconds
    else:
        time.sleep(2)  # Delay 2 seconds

Task 2: Modify the program to import only the required functions from the machine and time modules.

Importing Methods from Modules in Python

In Python, functions defined within a module can be accessed in two ways:

1️⃣ Importing specific methods from a module 2️⃣ Importing the entire module and referencing methods within it

📌 Note: While we often refer to them as functions, when they are part of a module or an object, the correct term is "method." Methods are similar to functions but belong to an object or module. We won't dive deeper into Object-Oriented Programming (OOP) here—this is just to ensure we use the proper terminology.

Let's examine both approaches with an example. Suppose we have a module named modul1, and we want to use a method called func1 to assign a value to a variable x.


Method 1: Importing a Specific Method

By importing only the required method, we can call it directly without referencing the module name.

from machine import Pin
led = Pin("LED", Pin.OUT)  # Direct method call

Advantage: Cleaner and more concise code. ❌ Disadvantage: Can lead to naming conflicts if multiple modules contain methods with the same name.


Method 2: Importing the Entire Module

This approach allows access to all methods within the module but requires using the module name as a prefix.

import machine
led = machine.Pin("LED", Pin.OUT)  # Method call with module reference

Advantage: Avoids naming conflicts and improves code readability. ❌ Disadvantage: Requires longer method calls since the module name must be specified.


Importing Multiple Methods

If multiple methods are needed from a module, they can be imported in a single line by separating them with commas:

from machine import Pin, PWM

💡 Best Practice:

  • Use Method 1 when only a few methods are needed.
  • Use Method 2 when multiple methods from the same module will be used frequently.

Task 3: Modify the program to print the current value of GP9 to the console.

📌 Hint: Add an else branch to the conditional statement and use the print() function in both branches.

  • If you don't want to use the solution from the linked documentation, you can print any text inside quotation marks, e.g., print("input").
  • You can also print multiple constants and variables separated by commas, e.g., print("input value", GP9_in.value()).

Task 4: Modify the program to implement a counter that increments each time GP9 changes state. Toggle the LED whenever GP9 changes and print the current counter value to the console.

Tip

# Import required modules
import machine
import time

# Configure the onboard LED as an output
led = machine.Pin("LED", machine.Pin.OUT)

# Configure GPIO9 as an input with a pull-up resistor (default state is LOW)
GP9_in = machine.Pin(9, machine.Pin.IN, machine.Pin.PULL_UP)

counter = 0
prev_state = GP9_in.value() # Read the input state as an initial state
# Infinite loop to control LED blinking speed based on GP9 input
while True:
    new_state = GP9_in.value() # Read the input state
    if new_state != prev_state:  # Compare the new state to the previous state, if GP9 is changed...
        prev_state = new_state   # save the new state as previous for the comparision in the next loop
        led.toggle()  # Toggle LED state
        counter += 1    # Increase counter value
        print("counter: ", counter)  # Display the counter value in terminal
📌 Hint: Run the code as it is and observe the behavior. You will notice that the output appears to "bounce" or flicker due to rapid updates. This happens because there is no delay between iterations. To fix this, modify the code by adding a sleep function to introduce a small delay, which will smooth out the updates.


➡ Next Steps