In the previous post, we created our first binding to describe GPIOs as simple pins for an LED driver. Now it’s time to write some functions that allow us to control those pins as actual LEDs. For now, four functions will be more than enough. Let’s create the following couple of files.

src/simple_leds.h

#ifndef __ZEPHYR_SIMPLE_LEDS_H__
#define __ZEPHYR_SIMPLE_LEDS_H__

#include <zephyr/drivers/gpio.h>

void simple_leds_init(const struct gpio_dt_spec *led);
void simple_leds_on( const struct gpio_dt_spec *led );
void simple_leds_off( const struct gpio_dt_spec *led );
void simple_leds_toggle( const struct gpio_dt_spec *led );

#endif

src/simple_leds.c

#include "simple_leds.h"

/*configure a given pin to output*/
void simple_leds_init( const struct gpio_dt_spec *led )
{
    gpio_pin_configure_dt(led, GPIO_OUTPUT_INACTIVE);
}

/*set to high a given pin*/
void simple_leds_on( const struct gpio_dt_spec *led )
{
    gpio_pin_set_dt(led, 1);
}

/*set to low a given pin*/
void simple_leds_off( const struct gpio_dt_spec *led )
{
    gpio_pin_set_dt(led, 0);
}

/*just flip a given pin value*/
void simple_leds_toggle( const struct gpio_dt_spec *led )
{
    gpio_pin_toggle_dt(led);
}

In our main.c file, let’s modify the code to use the newly created LED functions instead of the previous GPIO ones. Keep in mind that we are still using the same line of code to extract the port, pin, and flags from the DeviceTree — nothing changes in that part.

src/main.c

#include <zephyr/kernel.h>
#include <zephyr/drivers/gpio.h>
#include "simple_leds.h"

const struct gpio_dt_spec my_led0 = GPIO_DT_SPEC_GET(DT_NODELABEL(myled0), gpios);

int main(void)
{
    simple_leds_init(&my_led0);
    while(true)
    {
        simple_leds_toggle(&my_led0);
        k_msleep(1000);
    }
        
	return 0;
}

If we place these files inside the src folder, the only things we need to do are:

  • Include led.h in our source file using an #include directive.
  • Modify the corresponding line in CMakeLists.txt to add led.c to the build.

That’s enough for the build system to compile and link our new LED driver functions with the application.

...
# Project and source files
target_sources(app PRIVATE src/main.c src/simple_leds.c)

By the way, this is what our directory structure looks like so far.

app
├── app.overlay
├── CMakeLists.txt
├── dts
│   └── bindings
│       └── simple,leds.yaml
├── prj.conf
├── src
│   ├── main.c
│   ├── simple_leds.c
│   └── simple_leds.h
└── west.yml

The Zephyr way

This is the usual way of handling a library in C. However, with the Zephyr Project, if we truly want to create a proper library, we need to add a few more elements to the equation.

Create a new directory called drivers/simple_leds and move both files (led.c and led.h) into it. Then, update your CMakeLists.txt file to include this new directory so it becomes part of the build system. After that, run a build to verify everything compiles correctly.

...
# Project and source files
target_include_directories(app PRIVATE drivers/simple_leds)
target_sources(app PRIVATE src/main.c drivers/simple_leds/simple_leds.c)

So far, everything looks very similar to a traditional C project. But now let’s give it the proper Zephyr treatment. Inside the same drivers/simple_leds directory, create a new CMakeLists.txt file. This file will define how Zephyr should treat this module as part of its build system.

# Add your source file directory
zephyr_include_directories(.)

# Add the source file you want to compile
zephyr_library_sources(simple_leds.c)

We’re not done yet. Create a new subdirectory called zephyr inside your driver directory, and within it add a new file named module.yaml.

This file tells the build system that your driver should be treated as a proper Zephyr module. It allows Zephyr to discover your library automatically and integrate it cleanly into the build system.

name: simple_leds
build:
  cmake: .

Modify your main CMakeLists.txt file to register the new module and keep your application sources properly defined. ZEPHYR_EXTRA_MODULES tells the build system to include your drivers/simple_leds directory as an external Zephyr module. It should look like this:

...
set(ZEPHYR_EXTRA_MODULES "${CMAKE_SOURCE_DIR}/drivers/simple_leds")
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})

# Project and source files
project(demo)
target_sources(app PRIVATE src/main.c)

After the new change, run a clean build to verify that Zephyr correctly detects and builds your new module. So far our directory should looks like this

app
├── app.overlay
├── CMakeLists.txt
├── drivers
│   └── simple_leds
│       ├── CMakeLists.txt
│       ├── simple_leds.c
│       ├── simple_leds.h
│       └── zephyr
│           └── module.yaml
├── dts
│   └── bindings
│       └── simple,leds.yaml
├── prj.conf
├── src
│   └── main.c
└── west.yml

Adding Kconfig

With these new changes, the project can be built again. However, just like other Zephyr modules, we may want to add optional features — for example, making the entire driver optional or enabling only certain parts of it.

Create a new file called Kconfig inside your drivers/simple_leds directory. This file will allow you to define configuration options that can be enabled or disabled from menuconfig (or prj.conf).

# Create a new option in menuconfig
config SIMPLE_LEDS
    bool "Basic led handling functions"
    default n   # Set the library to be disabled by default
    depends on GPIO   # Make it dependent on GPIOS
    help
        Adds init on, off and toggle fucntionalities for led support.

You can notice that we defined this LEDS module as not operational by default. Because of that, we need to add a precondition in its corresponding CMakeLists.txt file so the driver is compiled only if the Kconfig option is enabled.

In your module drivers/simple_leds/CMakeLists.txt, you can wrap the source inclusion like this:

# Check if LEDS is set in Kconfig
if(CONFIG_SIMPLE_LEDS)
    # Add your source file directory
    zephyr_include_directories(.)

    # Add the source file you want to compile
    zephyr_library_sources(simple_leds.c)
endif()

In the zephyr/module.yaml file, add one final line to indicate that the module contains a Kconfig file in its root directory. The kconfig: Kconfig line tells Zephyr’s build system that this module provides configuration options defined in a Kconfig file located in the module directory.

It should look something like this:

name: simple_leds
build:
  cmake: .
  kconfig: Kconfig

With the last previous addition, your driver is now fully integrated as a proper Zephyr module — including CMake, Kconfig, and build system support. Finally, in your prj.conf file, enable the new LED driver by adding its Kconfig symbol:

CONFIG_SIMPLE_LEDS=y

Once and again, our project directory structure…

app
├── app.overlay
├── CMakeLists.txt
├── drivers
│   └── simple_leds
│       ├── CMakeLists.txt
│       ├── Kconfig
│       ├── simple_leds.c
│       ├── simple_leds.h
│       └── zephyr
│           └── module.yaml
├── dts
│   └── bindings
│       └── simple,leds.yaml
├── prj.conf
├── src
│   └── main.c
└── west.yml

Well, that’s pretty much everything you need to do for this part.

Our driver is not fully complete yet, but as you can see, we’ve learned how to integrate it as a proper Zephyr module. To achieve that, we made use of Kconfig and CMake to connect the driver to the build system and make it configurable and modular.