Raspberry Pi Variable Pulse Generator C Code Using libgpiod v2.2 With Timing Results!

Raspberry Pi Variable Pulse Generator C Code Using libgpiod v2.2 With Timing Results!

Convert your Raspberry Pi to a Variable Speed Pulse Generator! This article was human reviewed and tested (the code was written by Grok 4). All internet articles should be explicit and transparent about their AI article state as responsible writers.

  • We needed a bit-banger we could control from the keyboard, here it is.
  • Please note this is for libgpiod v2.2+ the latest you may need to update your packages..
  • We have included the new very-valuable prompt

Check your library before you ask Grok or an LLM to write the code - otherwise you will get possibly the wrong v1 API calls or etc.

For the following libraries write an application that toggles a gpio pins speed based on 'z' slow down 1%, x slow down .1% c slow down 1 us, v increase 1 us, b increase .1% n increase 1%, Ctrl-c graceful exit The user can pass the output gpio and the starting speed in us from the command line

Usage is as follows

  • z : slow down 1%
  • x: slow down .1 %
  • c: slow down 1 us
  • v: increase 1 us
  • b: increase  .1%
  • n: increase 1%
  • Ctrl-c graceful exit
./pulse_control 17 250000
# output gpio 17 at 250,000 us pulse duration

Install your Support Libraries: (note gpio is v2 API rated).

sudo apt install gpiod libgpiod-dev
/*
 * gpio_pulse_control.c
 * Fixed for your libgpiod signature: gpiod_chip_request_lines(chip, req_cfg, line_cfg)
 */

#include <gpiod.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <termios.h>
#include <fcntl.h>
#include <signal.h>

#define CHIP_PATH          "/dev/gpiochip0"
#define POLL_INTERVAL_NS   250000000LL

static struct gpiod_chip         *chip = NULL;
static struct gpiod_line_request *req  = NULL;
static struct termios             orig_term;
static int                        gpio_offset;
static long                       pulse_duration_us = 500000;
static volatile sig_atomic_t      running = 1;

static void cleanup(void)
{
    if (req)   gpiod_line_request_release(req);
    if (chip)  gpiod_chip_close(chip);
    tcsetattr(STDIN_FILENO, TCSANOW, &orig_term);
    printf("\nGPIO released. Terminal restored.\n");
}

static void signal_handler(int sig) { (void)sig; running = 0; }

static int read_key(void)
{
    char ch;
    return (read(STDIN_FILENO, &ch, 1) == 1) ? (unsigned char)ch : -1;
}

static void adjust_duration(int key)
{
    switch (key) {
        case 'z': pulse_duration_us = (long)(pulse_duration_us * 1.01 + 0.5); break;
        case 'x': pulse_duration_us = (long)(pulse_duration_us * 1.001 + 0.5); break;
        case 'c': pulse_duration_us += 1; break;
        case 'v': if (pulse_duration_us > 1) pulse_duration_us -= 1; break;
        case 'b': pulse_duration_us = (long)(pulse_duration_us * 0.999); break;
        case 'n': pulse_duration_us = (long)(pulse_duration_us * 0.99); break;
        default: return;
    }
    if (pulse_duration_us < 1) pulse_duration_us = 1;

    printf("\rDuration: %6ld µs   Freq: %.1f Hz     ",
           pulse_duration_us, 1000000.0 / (2.0 * pulse_duration_us));
    fflush(stdout);
}

int main(int argc, char *argv[])
{
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <gpio_offset> <initial_half_period_us>\n", argv[0]);
        return 1;
    }

    gpio_offset       = atoi(argv[1]);
    pulse_duration_us = atol(argv[2]);
    if (pulse_duration_us < 1) pulse_duration_us = 1;

    chip = gpiod_chip_open(CHIP_PATH);
    if (!chip) { perror("gpiod_chip_open"); return 1; }

    struct gpiod_line_settings *settings = gpiod_line_settings_new();
    gpiod_line_settings_set_direction(settings, GPIOD_LINE_DIRECTION_OUTPUT);
    gpiod_line_settings_set_output_value(settings, 0);

    struct gpiod_line_config *line_cfg = gpiod_line_config_new();
    unsigned int offset_u = (unsigned int)gpio_offset;
    gpiod_line_config_add_line_settings(line_cfg, &offset_u, 1, settings);

    // Matches your header: req_cfg, then line_cfg (no consumer string)
    req = gpiod_chip_request_lines(chip, NULL, line_cfg);
    if (!req) {
        perror("gpiod_chip_request_lines");
        goto cleanup;
    }

    tcgetattr(STDIN_FILENO, &orig_term);
    struct termios raw = orig_term;
    raw.c_lflag &= ~(ECHO | ICANON);
    tcsetattr(STDIN_FILENO, TCSANOW, &raw);
    fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK);

    signal(SIGINT,  signal_handler);
    signal(SIGTERM, signal_handler);

    printf("Pulse generator on BCM %d\nInitial: %ld µs (%.1f Hz)\n",
           gpio_offset, pulse_duration_us, 1000000.0 / (2.0 * pulse_duration_us));
    printf("Keys: z/x/c slower   v/b/n faster\n\n");

    struct timespec last_poll = {0};
    clock_gettime(CLOCK_MONOTONIC, &last_poll);

    while (running) {
        gpiod_line_request_set_value(req, 0, 1);
        struct timespec high = {0, pulse_duration_us * 1000L};
        nanosleep(&high, NULL);

        gpiod_line_request_set_value(req, 0, 0);
        struct timespec low = {0, pulse_duration_us * 1000L};
        nanosleep(&low, NULL);

        struct timespec now;
        clock_gettime(CLOCK_MONOTONIC, &now);
        long long elapsed = (now.tv_sec - last_poll.tv_sec) * 1000000000LL +
                            (now.tv_nsec - last_poll.tv_nsec);

        if (elapsed >= POLL_INTERVAL_NS) {
            int key = read_key();
            if (key != -1) adjust_duration(key);
            last_poll = now;
        }
    }

cleanup:
    cleanup();
    return 0;
}

Compilation & Usage

gcc -Wall -O2 -o gpio_pulse_control gpio_pulse_control.c -lgpiod
./gpio_pulse_control 17 100000

Implementation Notes: (Author)

  • We had to get Grok 4 to rewrite this multiple times, however this should work for libraries  2.2.
  • Grok consistently did not pass the command-line parameter gpio-offset to the corresponding pin driver.  So we had to manually change:
gpiod_line_request_set_value(req, 0, 1);
// to
gpiod_line_request_set_value(req, gpio_offset, 1);

Output looks as:

Pulse generator on BCM 17
Initial: 100000 µs (5.0 Hz)
Keys: z/x/c slower   v/b/n faster

Testing (Saelae Logic Generic Logic Analyzer to 24 Mhz Used)

  • We noted in this article that pigpio(d) really misses timings - go read it, it's interesting.. Once we realized it was a overbuilt library that was opening port 8888 allowing for remote control, we went for a more simpler library

5.00 Hz (Very Good)

Pulse generator on BCM 14
Initial: 100000 µs (5.0 Hz)
Keys: z/x/c slower   v/b/n faster

10.00 Hz (0.25ms out)

Pulse generator on BCM 14
Initial: 50000 µs (10.0 Hz)
Keys: z/x/c slower   v/b/n faster

20.00 Hz (Returns 19.90 Hz 0.25 ms out)

Pulse generator on BCM 14
Initial: 25000 µs (20.0 Hz)
Keys: z/x/c slower   v/b/n faster

40 Hz (Returns 39.583 Hz 0.26 ms out)

Pulse generator on BCM 14
Initial: 12500 µs (40.0 Hz)
Keys: z/x/c slower   v/b/n faster

100 Hz (Returns 97.35 Hz)

Pulse generator on BCM 14
Initial: 5000 µs (100.0 Hz)
Keys: z/x/c slower   v/b/n faster

250 Hz (Returns 228.3 Hz)

Pulse generator on BCM 14
Initial: 2000 µs (250.0 Hz)
Keys: z/x/c slower   v/b/n faster

500 Hz (Returns  443.13Hz )

Pulse generator on BCM 14
Initial: 1000 µs (500.0 Hz)
Keys: z/x/c slower   v/b/n faster

Etc...

Summary:

If you look at the code - it is a respectibly tight loop consider:

while (running) {
gpiod_line_request_set_value(req, gpio_offset, 1);
struct timespec high = {0, pulse_duration_us * 1000L};
nanosleep(&high, NULL);

gpiod_line_request_set_value(req, gpio_offset, 0);
struct timespec low = {0, pulse_duration_us * 1000L};
nanosleep(&low, NULL);

struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
long long elapsed = (now.tv_sec - last_poll.tv_sec) * 1000000000LL +
(now.tv_nsec - last_poll.tv_nsec);

if (elapsed >= POLL_INTERVAL_NS) {
int key = read_key();
if (key != -1) adjust_duration(key);
last_poll = now;
}

These tests were run on a 700 Mhz Raspberry Pi Zero, which incidentally has a universal 19.2 Mhz reference clock.  Interrupts can be dimissed effectively as the pulses were clean and consistent.

  • The general ideation is to simply manually tweak your us duration to get it as close as possible to a know reference.
Linux Rocks Every Day