Everything you need to show the PCF8574 Linux driver at the Open Day: a stand idea visitors can play with, a talk plan for people who have never seen code, and the user-space library and demo app that make the LEDs respond.
Visitors don't need to understand a kernel to be impressed by one. Give them something to do in the first ten seconds, then explain what just happened behind the lights.
A visitor types their name. Each letter appears on the 8 LEDs as its binary code: G is 71, which is 0100 0111. Everyone gets the point instantly: computers store letters as numbers.
The LEDs show a random number. The visitor adds up the values printed under the lit LEDs (128, 64 … 1) and types the answer. Keep a high-score sheet on the table — teenagers will compete.
Demo app: q · Simulator: “Quiz”For the curious: the screen decodes every byte the app sends to your driver, colour-coded like the diagram in your README. This is where you talk about drivers, the kernel and I2C.
Always visible on the demo screenStand beside the table, not behind the screen, so visitors walk up to the keyboard instead of to you.
Same protocol and same rules as the real driver and library. Click an LED, type a letter or start the quiz. Use it to rehearse, or put it on a tablet at the stand.
Which number do the LEDs show? Add up the values under the lit LEDs.
Notice what travels over the I2C bus: not your command, but the driver's full 8-pin picture. The chip can only take all 8 pins at once — that's why the driver keeps them in memory.
Your visitors are 16–18 year olds choosing a study, and their parents. Most have never heard of a kernel, and they don't need to. They should walk away thinking: “I understand what he built — and I could learn that.”
Hand over the keyboard before you say a single technical word.
Begin with what they see. Go one layer deeper only when they ask.
Use an analogy below; say the real term only after the picture lands.
Phones, cars, game consoles, washing machines: all of them are software talking to chips.
Tell them what you didn't know when you started. That's the sentence parents remember.
Use them in this order. Each one adds a layer: you can stop after any of them and the story still makes sense.
The Pi is a full computer the size of a credit card. It runs Linux, the same core that is inside every Android phone.
The Pi has only so many pins. This chip plugs into just 2 wires and gives back 8 switches. Several chips can share the same 2 wires, each with its own number.
Apps aren't allowed to touch hardware: the kernel guards it like a security desk. My driver is the translator at that desk who speaks this chip's language.
Every request is one byte: 8 yes/no boxes. Three say which light, one says on or off, two say what kind of request. That's all it takes.
The chip only accepts the full picture: all 8 lights at once. So the driver keeps a notebook, changes the one line you asked for and sends the whole page.
A phone camera, a car sensor, a smart thermostat: each chip needs someone to write the software that talks to it. That someone can be them.
Three lengths of the same story. Read the visitor: start with 30 seconds and extend only when they lean in or ask a question.
For passers-by and parents. The whole thing happens while they type.
For students who stop and look. Keep your hands off the keyboard; let them drive.
For visitors who already code, or ask “how does it actually work?”. Do the 2-minute version first, then add:
Lights and binary only. Play the quiz, cheer when they get it right. “Computers count with only 0 and 1 — and you just did too.”
The 2-minute story, then: “You don't need to know any of this yet. This is what you learn here.” Show a few lines of the code on screen.
What the skills lead to: embedded software for cars, medical devices, robots, smart homes. And that the study is hands-on: real hardware, not only theory.
Character device and file_operations, i2c_client, Device Tree matching, why the driver keeps a copy of the pin state, and the ioctl commands.
If one of these words slips out, follow it straight away with the plain version.
| Term | Say instead |
|---|---|
| kernel | the core of the operating system: the boss of all the hardware |
| driver | a translator between programs and one specific chip |
| I2C | a two-wire “phone line” many chips share, each with its own number |
| GPIO pin | a pin the computer can switch on or off |
| bit / byte | one yes/no box / a row of 8 of them |
| user space | where normal apps live, with no direct access to hardware |
| system call | the official way an app asks the kernel for something |
| /dev/pcf8574 | the mailbox where apps drop messages for my driver |
| Device Tree | the Pi's list of what is connected to it |
| ioctl | a “special request” form, for things like “all lights off” |
Short pointers, not scripts. The personal ones only work in your own words.
gpio-pcf857x). Building your own from scratch is the point: you learn every layer, from how the kernel finds hardware to how one byte travels from an app to a pin.Everything on this table — the electronics, the C code, the Linux part — is what ICT & Embedded Systems is about.
If you like building things you can actually touch, ask me how I got started.
Parents remember one personal sentence more than any technical detail. Prepare yours before the day:
Before this study I ______. Now I can ______.
Then hand them the QR code or point them to the programme stand.
setPin(6, ON) instead of bitsPrograms call plain functions. The library builds the command byte exactly as drawn in the README's “Communication mechanism” diagram and hands it to the driver. Nobody using it has to think about bits.
Example: setPin(6, ON) → 0011 1110 = 0x3E
| Function | Request | Reaches the driver as |
|---|---|---|
setPin(pin, state) | WRITE · STATE | write(fd, &cmd, 1) |
setPinDirection(pin, dir) | WRITE · DIRECTION | write(fd, &cmd, 1) |
readPin(pin) | READ · STATE | read(fd, buf, 2), bit pin of buf[0] |
readPinDirection(pin) | READ · DIRECTION | read(fd, buf, 2), bit pin of buf[1] |
readAllPins(&s, &d) | both bytes | read(fd, buf, 2) |
setPort(value) | one WRITE per changed pin | write() × changed pins |
setAllPins(state) | bulk | ioctl(fd, 2 or 1) |
initPins() | bulk | ioctl(fd, 0) |
pcfOpen(path) | NULL opens /dev/pcf8574; "sim" opens a built-in simulator for testing without hardware | |
char_pcf8574_write() only looks at bits 4–0. The library sets bit 5 on every write, so it follows the diagram and works with today's driver. To make the driver enforce the diagram again, restore the check from commit 297aa95: if (!(byte & (1 << 5))) goto err;write() would treat it as “set” and switch a pin. That's why the library answers READ requests with read(), which returns the driver's remembered state (not live input from the chip).#ifndef LIBPCF8574_H
#define LIBPCF8574_H
#include <stdint.h>
/* USER-SPACE LIBRARY FOR THE PCF8574 CHARACTER DEVICE DRIVER */
/* Userland programs call setPin(), readPin(), ... and never deal with the binary protocol themselves. */
/* COMMUNICATION PROTOCOL (README.md -> "Communication mechanism") */
/*
* Every request is described by 1 byte:
*
* bit: 7 6 5 4 3 2 1 0
* [ 0 | 0 | OP | BYTE | STATE | PIN NUMBER ]
*
* Bit 5 -> Operation request bit (1 - WRITE; 0 - READ)
* Bit 4 -> Byte update bit (1 - STATE byte; 0 - DIRECTION byte)
* Bit 3 -> Pin state bit (1 - ON; 0 - OFF)
* Bit 2, 1, 0 -> Pin number formation (000 - PIN 0 ... 111 - PIN 7)
*
* WRITE requests are handed to the driver with write().
* READ requests are answered by the driver's read(), which returns the state byte and the direction byte.
*/
#define PCF8574_DEVICE "/dev/pcf8574"
#define PCF8574_SIM "sim" /* pcfOpen(PCF8574_SIM) runs without hardware */
#define PCF8574_PINS 8
#define PCF_OP_BIT 5
#define PCF_BYTE_BIT 4
#define PCF_STATE_BIT 3
#define PCF_PIN_MASK 0x07
enum pcf_op { PCF_READ = 0, PCF_WRITE = 1 };
enum pcf_byte { PCF_DIRECTION = 0, PCF_STATE = 1 };
enum pcf_state { PCF_OFF = 0, PCF_ON = 1 };
enum pcf_dir { PCF_OUTPUT = 0, PCF_INPUT = 1 };
/* Operations understood by the driver's ioctl() */
enum pcf_ioctl {
PCF_IOCTL_INIT = 0, /* all pins OUTPUT and OFF */
PCF_IOCTL_ALL_PINS_LOW = 1,
PCF_IOCTL_ALL_PINS_HIGH = 2
};
/* Called for every request the library makes; used by demos to show the protocol */
/* value is the command byte for WRITE/READ and the ioctl operation for IOCTL */
enum pcf_trace_kind { PCF_TRACE_WRITE, PCF_TRACE_READ, PCF_TRACE_IOCTL };
typedef void (*pcf_trace_fn)(int kind, uint8_t value, const char *call);
/* Opens the device. NULL opens /dev/pcf8574, PCF8574_SIM opens the built-in simulator. 0 on success, -1 on error (errno is set) */
int pcfOpen(const char *path);
void pcfClose(void);
int pcfIsSimulated(void);
void pcfSetTrace(pcf_trace_fn fn);
/* Builds the command byte exactly as in the communication mechanism diagram */
uint8_t pcfEncode(int op, int byte, int state, int pin);
/* All functions below return 0 (or the requested value) on success, -1 on error (errno is set) */
int setPin(int pin, int state); /* WRITE + STATE byte */
int setPinDirection(int pin, int dir); /* WRITE + DIRECTION byte */
int readPin(int pin); /* READ + STATE byte, returns 0 or 1 */
int readPinDirection(int pin); /* READ + DIRECTION byte, returns 0 or 1 */
int readAllPins(uint8_t *state, uint8_t *dir); /* both bytes at once; either pointer may be NULL */
int setPort(uint8_t value); /* bit N of value -> pin N; only changed pins are sent */
int setAllPins(int state); /* one ioctl() for all 8 pins */
int initPins(void); /* ioctl() INIT: all pins OUTPUT and OFF */
#endif
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include "libpcf8574.h"
/* File descriptor of /dev/pcf8574; every request goes through it */
static int fd = -1;
static int simulated = 0;
static pcf_trace_fn trace = NULL;
/* SIMULATOR */
/* Behaves like the driver's write(), read() and ioctl(), so programs can be tested without hardware */
static uint8_t sim_level = 0xFF; /* same start values as the driver's probe() */
static uint8_t sim_dir = 0x00;
static void sim_write(uint8_t cmd){
uint8_t *ptr = (cmd & (1 << PCF_BYTE_BIT)) ? &sim_level : &sim_dir;
uint8_t pin_n = cmd & PCF_PIN_MASK;
if (cmd & (1 << PCF_STATE_BIT)){
*ptr |= (1 << pin_n);
}
else{
*ptr &= ~(1 << pin_n);
}
}
static void sim_ioctl(int operation){
switch(operation){
case PCF_IOCTL_INIT:{
sim_dir = 0x00;
sim_level = 0x00;
break;
}
case PCF_IOCTL_ALL_PINS_LOW:{
sim_level = 0x00;
break;
}
case PCF_IOCTL_ALL_PINS_HIGH:{
sim_level = 0xFF;
break;
}
}
}
/* TRANSPORT */
/* The only place where the library talks to the driver */
/* write() -> char_pcf8574_write() in the driver; the driver accepts exactly 1 byte */
static int dev_write(uint8_t cmd){
if (simulated){
sim_write(cmd);
return 0;
}
ssize_t n = write(fd, &cmd, 1);
if (n == 1){
return 0;
}
if (n >= 0){
errno = EIO;
}
return -1;
}
/* read() -> char_pcf8574_read() in the driver; the driver answers with exactly 2 bytes: {state, direction} */
static int dev_read(uint8_t buf[2]){
if (simulated){
buf[0] = sim_level;
buf[1] = sim_dir;
return 0;
}
ssize_t n = read(fd, buf, 2);
if (n == 2){
return 0;
}
if (n >= 0){
errno = EIO;
}
return -1;
}
/* ioctl() -> char_pcf8574_ioctl() in the driver */
static int dev_ioctl(int operation){
if (simulated){
sim_ioctl(operation);
return 0;
}
return ioctl(fd, operation, 0) < 0 ? -1 : 0;
}
static int check_open(void){
if (fd < 0 && !simulated){
errno = EBADF;
return -1;
}
return 0;
}
static int check_pin(int pin){
if (check_open() < 0){
return -1;
}
if (pin < 0 || pin >= PCF8574_PINS){
errno = EINVAL;
return -1;
}
return 0;
}
/* PUBLIC API */
int pcfOpen(const char *path){
pcfClose();
if (path && strcmp(path, PCF8574_SIM) == 0){
simulated = 1;
return 0;
}
fd = open(path ? path : PCF8574_DEVICE, O_RDWR);
return fd < 0 ? -1 : 0;
}
void pcfClose(void){
if (fd >= 0){
close(fd);
}
fd = -1;
simulated = 0;
}
int pcfIsSimulated(void){
return simulated;
}
void pcfSetTrace(pcf_trace_fn fn){
trace = fn;
}
uint8_t pcfEncode(int op, int byte, int state, int pin){
return (uint8_t)(((op ? 1 : 0) << PCF_OP_BIT) |
((byte ? 1 : 0) << PCF_BYTE_BIT) |
((state ? 1 : 0) << PCF_STATE_BIT) |
(pin & PCF_PIN_MASK));
}
/* Builds a WRITE request and hands it to the driver */
static int write_request(int byte, int pin, int state, const char *call){
uint8_t cmd = pcfEncode(PCF_WRITE, byte, state, pin);
if (trace){
trace(PCF_TRACE_WRITE, cmd, call);
}
return dev_write(cmd);
}
/* Builds a READ request; the driver's read() returns both bytes, so the requested bit is picked here */
static int read_request(int byte, int pin, const char *call){
uint8_t buf[2];
if (trace){
trace(PCF_TRACE_READ, pcfEncode(PCF_READ, byte, PCF_OFF, pin), call);
}
if (dev_read(buf) < 0){
return -1;
}
return ((byte == PCF_STATE ? buf[0] : buf[1]) >> pin) & 1;
}
int setPin(int pin, int state){
char call[32];
if (check_pin(pin) < 0){
return -1;
}
snprintf(call, sizeof(call), "setPin(%d, %s)", pin, state ? "ON" : "OFF");
return write_request(PCF_STATE, pin, state, call);
}
int setPinDirection(int pin, int dir){
char call[40];
if (check_pin(pin) < 0){
return -1;
}
snprintf(call, sizeof(call), "setPinDirection(%d, %s)", pin, dir ? "INPUT" : "OUTPUT");
return write_request(PCF_DIRECTION, pin, dir, call);
}
int readPin(int pin){
char call[32];
if (check_pin(pin) < 0){
return -1;
}
snprintf(call, sizeof(call), "readPin(%d)", pin);
return read_request(PCF_STATE, pin, call);
}
int readPinDirection(int pin){
char call[32];
if (check_pin(pin) < 0){
return -1;
}
snprintf(call, sizeof(call), "readPinDirection(%d)", pin);
return read_request(PCF_DIRECTION, pin, call);
}
int readAllPins(uint8_t *state, uint8_t *dir){
uint8_t buf[2];
if (check_open() < 0 || dev_read(buf) < 0){
return -1;
}
if (state){
*state = buf[0];
}
if (dir){
*dir = buf[1];
}
return 0;
}
int setPort(uint8_t value){
uint8_t current;
if (readAllPins(¤t, NULL) < 0){
return -1;
}
for (int pin = 0; pin < PCF8574_PINS; pin++){
int wanted = (value >> pin) & 1;
if (((current >> pin) & 1) != wanted && setPin(pin, wanted) < 0){
return -1;
}
}
return 0;
}
int setAllPins(int state){
int operation = state ? PCF_IOCTL_ALL_PINS_HIGH : PCF_IOCTL_ALL_PINS_LOW;
if (check_open() < 0){
return -1;
}
if (trace){
trace(PCF_TRACE_IOCTL, (uint8_t)operation, state ? "setAllPins(ON)" : "setAllPins(OFF)");
}
return dev_ioctl(operation);
}
int initPins(void){
if (check_open() < 0){
return -1;
}
if (trace){
trace(PCF_TRACE_IOCTL, PCF_IOCTL_INIT, "initPins()");
}
return dev_ioctl(PCF_IOCTL_INIT);
}
/* blink - the smallest libpcf8574 program: blinks the LED on pin 0 five times */
/* Run "./blink sim" to try it without hardware */
#include <stdio.h>
#include <unistd.h>
#include "libpcf8574.h"
int main(int argc, char **argv){
if (pcfOpen(argc > 1 ? argv[1] : PCF8574_DEVICE) < 0){
perror("pcfOpen");
return 1;
}
for (int i = 0; i < 5; i++){
setPin(0, PCF_ON);
printf("pin 0 is %s\n", readPin(0) ? "ON" : "OFF");
sleep(1);
setPin(0, PCF_OFF);
printf("pin 0 is %s\n", readPin(0) ? "ON" : "OFF");
sleep(1);
}
pcfClose();
return 0;
}
A full-screen terminal app built on the library. Visitors press keys; the LEDs respond; the screen shows the exact byte each request turned into, coloured like the diagram. No extra libraries needed, so it builds on any Pi.
PCF8574 driver demo simulator, no hardware pin 7 6 5 4 3 2 1 0 value 128 64 32 16 8 4 2 1 LEDs ○ ● ○ ○ ○ ● ● ● = 71 'G' 'G' = 0100 0111 = 71 = 64 + 4 + 2 + 1 [Esc] back Last request setPin(6, ON) -> write(fd, &cmd, 1) 0 0 1 1 1 1 1 0 = 0x3E (62) │ │ │ └─┬─┘ │ │ │ └─ pin 110 = pin 6 │ │ └────── state 1 = ON │ └───────── byte 1 = STATE └──────────── op 1 = WRITE History 0x3E 0011 1110 WRITE STATE ON pin 6 setPin(6, ON) 0x3A 0011 1010 WRITE STATE ON pin 2 setPin(2, ON) 0x39 0011 1001 WRITE STATE ON pin 1 setPin(1, ON) 0x38 0011 1000 WRITE STATE ON pin 0 setPin(0, ON) ioctl 1 all 8 pins setAllPins(OFF) [0-7] toggle LED [a] all on [o] all off [i] init [t] type letters [n] number [c] count [k] scanner [q] quiz [x] exit
--active-low--sim--device/* pcf_demo - interactive demo of the PCF8574 driver, built on libpcf8574 */
/* Every request is shown as the command byte from the communication mechanism diagram */
#define _DEFAULT_SOURCE
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
#include "libpcf8574.h"
/* Colours of the diagram: red - operation, orange - byte update, blue - pin state, green - pin number */
#define RESET "\033[0m"
#define BOLD "\033[1m"
#define DIM "\033[2m"
#define C_OP "\033[38;5;196m"
#define C_BYTE "\033[38;5;208m"
#define C_STATE "\033[38;5;33m"
#define C_PIN "\033[38;5;70m"
#define C_LED "\033[1;93m"
#define KEY_NONE -1 /* timeout */
#define KEY_EOF -2 /* stdin closed */
#define KEY_ESC 27
#define HISTORY 5
struct request {
int kind;
uint8_t value;
char call[40];
};
static const char *dev_path = PCF8574_DEVICE;
static int active_low = 0; /* LEDs wired VCC -> resistor -> LED -> pin light up when the pin is LOW */
static char info[256]; /* message line under the LEDs */
static struct request history[HISTORY];
static int history_len = 0;
static struct termios saved_term;
static int term_saved = 0;
/* TERMINAL */
static void restore_terminal(void){
if (term_saved){
tcsetattr(STDIN_FILENO, TCSANOW, &saved_term);
term_saved = 0;
}
fputs("\033[?25h\n", stdout);
fflush(stdout);
}
static void on_signal(int sig){
(void)sig;
if (term_saved){
tcsetattr(STDIN_FILENO, TCSANOW, &saved_term);
}
if (write(STDOUT_FILENO, "\033[?25h\n", 7) < 0){
/* nothing left to do */
}
_exit(130);
}
/* Keys arrive one by one, without Enter and without echo */
static void raw_terminal(void){
struct termios t;
if (!isatty(STDIN_FILENO) || tcgetattr(STDIN_FILENO, &saved_term) < 0){
return;
}
term_saved = 1;
t = saved_term;
t.c_lflag &= ~(ICANON | ECHO);
t.c_cc[VMIN] = 1;
t.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSANOW, &t);
}
/* Waits up to timeout_ms for a key (-1 waits forever) */
static int read_key(int timeout_ms){
fd_set set;
struct timeval tv;
unsigned char c;
FD_ZERO(&set);
FD_SET(STDIN_FILENO, &set);
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
if (select(STDIN_FILENO + 1, &set, NULL, NULL, timeout_ms < 0 ? NULL : &tv) <= 0){
return KEY_NONE;
}
if (read(STDIN_FILENO, &c, 1) != 1){
return KEY_EOF;
}
return c;
}
/* Prints one screen line and clears whatever was left there from the previous frame */
static void line(const char *fmt, ...){
va_list args;
va_start(args, fmt);
vprintf(fmt, args);
va_end(args);
fputs("\033[K\n", stdout);
}
/* HELPERS */
static const char *bin8(uint8_t v){
static char buf[10];
for (int i = 7, j = 0; i >= 0; i--){
buf[j++] = (v >> i) & 1 ? '1' : '0';
if (i == 4){
buf[j++] = ' ';
}
}
buf[9] = '\0';
return buf;
}
/* "77 = 64 + 8 + 4 + 1" */
static const char *place_values(uint8_t v){
static char buf[64];
int len = snprintf(buf, sizeof(buf), "%d", v);
const char *sep = " = ";
for (int bit = 7; bit >= 0 && v; bit--){
if (v & (1 << bit)){
len += snprintf(buf + len, sizeof(buf) - len, "%s%d", sep, 1 << bit);
sep = " + ";
}
}
return buf;
}
static int led_level(int on){
return active_low ? !on : on;
}
static int show_value(uint8_t v){
return setPort(active_low ? (uint8_t)~v : v);
}
static int report_error(int rc){
if (rc < 0){
snprintf(info, sizeof(info), C_OP "Error: %s" RESET " (is the driver loaded? check dmesg)", strerror(errno));
}
return rc;
}
/* Library callback: remembers the latest requests for the screen */
static void on_trace(int kind, uint8_t value, const char *call){
memmove(&history[1], &history[0], sizeof(history[0]) * (HISTORY - 1));
history[0].kind = kind;
history[0].value = value;
snprintf(history[0].call, sizeof(history[0].call), "%s", call);
if (history_len < HISTORY){
history_len++;
}
}
/* SCREEN */
/* The latest request, drawn bit by bit like the communication mechanism diagram */
static void draw_request(const struct request *r){
int b[8];
for (int i = 0; i < 8; i++){
b[i] = (r->value >> i) & 1;
}
if (r->kind == PCF_TRACE_IOCTL){
line(" Last request " BOLD "%s" RESET " -> ioctl(fd, %d, 0)", r->call, r->value);
line("");
line(" One ioctl() call changes all 8 pins at once:");
line(" 0 = INIT (all outputs, all LOW) 1 = all LOW 2 = all HIGH");
for (int i = 0; i < 4; i++){
line("");
}
return;
}
line(" Last request " BOLD "%s" RESET " -> %s", r->call,
r->kind == PCF_TRACE_WRITE ? "write(fd, &cmd, 1)" : "read(fd, buf, 2)");
line("");
line(" " DIM "%d %d" RESET " " C_OP "%d" RESET " " C_BYTE "%d" RESET " " C_STATE "%d" RESET " " C_PIN "%d %d %d" RESET " = 0x%02X (%d)",
b[7], b[6], b[5], b[4], b[3], b[2], b[1], b[0], r->value, r->value);
line(" " C_OP "│" RESET " " C_BYTE "│" RESET " " C_STATE "│" RESET " " C_PIN "└─┬─┘" RESET);
line(" " C_OP "│" RESET " " C_BYTE "│" RESET " " C_STATE "│" RESET " " C_PIN "└─ pin %d%d%d = pin %d" RESET,
b[2], b[1], b[0], r->value & PCF_PIN_MASK);
if (r->kind == PCF_TRACE_WRITE){
line(" " C_OP "│" RESET " " C_BYTE "│" RESET " " C_STATE "└────── state %d = %s" RESET, b[3], b[3] ? "ON" : "OFF");
}
else{
line(" " C_OP "│" RESET " " C_BYTE "│" RESET " " C_STATE "└────── state %d (not used by READ)" RESET, b[3]);
}
line(" " C_OP "│" RESET " " C_BYTE "└───────── byte %d = %s" RESET, b[4], b[4] ? "STATE" : "DIRECTION");
line(" " C_OP "└──────────── op %d = %s" RESET, b[5], b[5] ? "WRITE" : "READ");
}
static void redraw(void){
uint8_t state = 0, dir = 0;
char leds[256];
char letter[4] = "";
int len = 0;
readAllPins(&state, &dir);
uint8_t lit = active_low ? (uint8_t)~state : state;
for (int pin = 7; pin >= 0; pin--){
len += snprintf(leds + len, sizeof(leds) - len, " %s", (lit >> pin) & 1 ? C_LED "●" RESET : DIM "○" RESET);
}
if (lit > 32 && lit < 127){
snprintf(letter, sizeof(letter), "'%c'", lit);
}
fputs("\033[H", stdout);
line("");
line(" " BOLD "PCF8574 driver demo" RESET " " DIM "%s%s" RESET,
pcfIsSimulated() ? "simulator, no hardware" : dev_path, active_low ? " (active-low LEDs)" : "");
line("");
line(" pin 7 6 5 4 3 2 1 0");
line(" value 128 64 32 16 8 4 2 1");
line(" LEDs%s = %3d %s", leds, lit, letter);
line("");
line(" %s", info);
line("");
if (history_len > 0){
draw_request(&history[0]);
}
else{
line(" Every request to the driver will be shown here, bit by bit.");
for (int i = 0; i < 7; i++){
line("");
}
}
line("");
line(" " DIM "History" RESET);
for (int i = 0; i < HISTORY; i++){
if (i >= history_len){
line("");
}
else if (history[i].kind == PCF_TRACE_IOCTL){
line(" " DIM "ioctl %d all 8 pins %s" RESET, history[i].value, history[i].call);
}
else{
uint8_t v = history[i].value;
line(" " DIM "0x%02X %s %-5s %-9s %-3s pin %d %s" RESET, v, bin8(v),
v & (1 << PCF_OP_BIT) ? "WRITE" : "READ",
v & (1 << PCF_BYTE_BIT) ? "STATE" : "DIRECTION",
v & (1 << PCF_OP_BIT) ? (v & (1 << PCF_STATE_BIT) ? "ON" : "OFF") : "",
v & PCF_PIN_MASK, history[i].call);
}
}
line("");
line(" " BOLD "[0-7]" RESET " toggle LED " BOLD "[a]" RESET " all on " BOLD "[o]" RESET " all off " BOLD "[i]" RESET " init");
line(" " BOLD "[t]" RESET " type letters " BOLD "[n]" RESET " number " BOLD "[c]" RESET " count " BOLD "[k]" RESET " scanner " BOLD "[q]" RESET " quiz " BOLD "[x]" RESET " exit");
fputs("\033[J", stdout);
fflush(stdout);
}
/* MODES */
/* Reads up to 3 digits; returns the number or -1 when cancelled */
static int read_number(const char *prompt){
char buf[4] = "";
int len = 0;
for (;;){
snprintf(info, sizeof(info), "%s " BOLD "%s_" RESET, prompt, buf);
redraw();
int k = read_key(-1);
if (k == KEY_EOF || k == KEY_ESC){
return -1;
}
if ((k == '\n' || k == '\r') && len > 0){
return atoi(buf);
}
if ((k == 127 || k == 8) && len > 0){
buf[--len] = '\0';
}
else if (k >= 0 && isdigit(k) && len < 3){
buf[len++] = (char)k;
buf[len] = '\0';
}
}
}
static void toggle_led(int pin){
int level = readPin(pin);
if (report_error(level) < 0){
return;
}
int on = !(active_low ? !level : level);
if (report_error(setPin(pin, led_level(on))) == 0){
snprintf(info, sizeof(info), "LED %d is now %s", pin, on ? "ON" : "OFF");
}
}
static void type_letters(void){
snprintf(info, sizeof(info), "Type letters: each one lights up as its binary code. " BOLD "[Esc]" RESET " back");
for (;;){
redraw();
int k = read_key(-1);
if (k == KEY_EOF || k == KEY_ESC || k == '\n' || k == '\r'){
return;
}
if (k < 33 || k > 126){
continue;
}
if (report_error(show_value((uint8_t)k)) == 0){
snprintf(info, sizeof(info), "'%c' = %s = %s " BOLD "[Esc]" RESET " back", k, bin8((uint8_t)k), place_values((uint8_t)k));
}
}
}
static void show_number(void){
int v = read_number("Number from 0 to 255, then " BOLD "[Enter]" RESET ":");
if (v < 0){
snprintf(info, sizeof(info), "Pick a mode from the menu.");
}
else if (v > 255){
snprintf(info, sizeof(info), "8 LEDs give 256 combinations, so the biggest number is 255.");
}
else if (report_error(show_value((uint8_t)v)) == 0){
snprintf(info, sizeof(info), "%s = %s", bin8((uint8_t)v), place_values((uint8_t)v));
}
}
static void count(void){
for (int v = 0; v <= 255; v++){
if (report_error(show_value((uint8_t)v)) < 0){
return;
}
snprintf(info, sizeof(info), "Counting in binary: %3d = %s " BOLD "[any key]" RESET " stop", v, bin8((uint8_t)v));
redraw();
if (read_key(150) != KEY_NONE){
return;
}
}
}
static void scanner(void){
int pos = 0, step = 1;
for (;;){
if (report_error(show_value((uint8_t)(1 << pos))) < 0){
return;
}
snprintf(info, sizeof(info), "Scanner: 2 setPin() calls per step " BOLD "[any key]" RESET " stop");
redraw();
if (read_key(90) != KEY_NONE){
return;
}
if (pos + step < 0 || pos + step > 7){
step = -step;
}
pos += step;
}
}
static void quiz(void){
int score = 0, rounds = 0;
char prompt[160];
for (;;){
uint8_t v = (uint8_t)(rand() % 255 + 1);
if (report_error(show_value(v)) < 0){
return;
}
snprintf(prompt, sizeof(prompt), "Quiz (%d/%d right): which number do the LEDs show? " BOLD "[Enter]" RESET ":", score, rounds);
int guess = read_number(prompt);
if (guess < 0){
snprintf(info, sizeof(info), "Quiz finished: %d of %d right.", score, rounds);
return;
}
rounds++;
if (guess == v){
score++;
snprintf(info, sizeof(info), "Correct! %s " BOLD "[any key]" RESET " next " BOLD "[Esc]" RESET " stop", place_values(v));
}
else{
snprintf(info, sizeof(info), "Not quite - it was %s " BOLD "[any key]" RESET " next " BOLD "[Esc]" RESET " stop", place_values(v));
}
redraw();
int k = read_key(-1);
if (k == KEY_EOF || k == KEY_ESC){
snprintf(info, sizeof(info), "Quiz finished: %d of %d right.", score, rounds);
return;
}
}
}
static void usage(const char *name){
printf("Usage: %s [--sim] [--device PATH] [--active-low]\n\n", name);
printf(" --sim run without hardware (built-in simulator)\n");
printf(" --device PATH device node to open (default %s)\n", PCF8574_DEVICE);
printf(" --active-low LEDs are wired VCC -> resistor -> LED -> pin, so a LOW pin lights them\n");
}
int main(int argc, char **argv){
for (int i = 1; i < argc; i++){
if (strcmp(argv[i], "--sim") == 0){
dev_path = PCF8574_SIM;
}
else if (strcmp(argv[i], "--device") == 0 && i + 1 < argc){
dev_path = argv[++i];
}
else if (strcmp(argv[i], "--active-low") == 0){
active_low = 1;
}
else{
usage(argv[0]);
return strcmp(argv[i], "--help") == 0 ? 0 : 1;
}
}
if (pcfOpen(dev_path) < 0){
fprintf(stderr, "Cannot open %s: %s\n", dev_path, strerror(errno));
fprintf(stderr, "Load the driver and create the dev node first, or run with --sim.\n");
return 1;
}
pcfSetTrace(on_trace);
srand((unsigned)time(NULL));
raw_terminal();
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
fputs("\033[?25l\033[2J", stdout);
snprintf(info, sizeof(info), "Press a key from the menu below.");
for (;;){
redraw();
int k = read_key(-1);
if (k == KEY_EOF || k == 'x' || k == 'X'){
break;
}
if (k >= '0' && k <= '7'){
toggle_led(k - '0');
continue;
}
switch (tolower(k)){
case 'a':{
if (report_error(setAllPins(led_level(1))) == 0){
snprintf(info, sizeof(info), "All LEDs ON with a single ioctl() call");
}
break;
}
case 'o':{
if (report_error(setAllPins(led_level(0))) == 0){
snprintf(info, sizeof(info), "All LEDs OFF with a single ioctl() call");
}
break;
}
case 'i':{
if (report_error(initPins()) == 0){
snprintf(info, sizeof(info), "Driver reset: all pins are outputs and LOW");
}
break;
}
case 't': type_letters(); break;
case 'n': show_number(); break;
case 'c': count(); break;
case 'k': scanner(); break;
case 'q': quiz(); break;
}
}
restore_terminal();
pcfClose();
return 0;
}
CFLAGS ?= -O2 -Wall -Wextra -std=gnu11
all: libpcf8574.a pcf_demo blink
libpcf8574.a: libpcf8574.o
$(AR) rcs $@ $^
libpcf8574.o pcf_demo.o blink.o: libpcf8574.h
pcf_demo: pcf_demo.o libpcf8574.a
$(CC) $(CFLAGS) -o $@ $^
blink: blink.o libpcf8574.a
$(CC) $(CFLAGS) -o $@ $^
clean:
rm -f *.o *.a pcf_demo blink
.PHONY: all clean
# libpcf8574 + pcf_demo
User-space side of the PCF8574 driver: a small C library that speaks the 1-byte protocol from the
README's "Communication mechanism" diagram, and an interactive demo app built on top of it.
## Files
- `libpcf8574.h` / `libpcf8574.c` - the library (`setPin()`, `readPin()`, `setAllPins()`, ...)
- `pcf_demo.c` - interactive Open Day demo: toggle LEDs, type letters in binary, binary quiz, counter, scanner
- `blink.c` - the smallest possible program using the library
- `Makefile` - builds `libpcf8574.a`, `pcf_demo` and `blink`
## Protocol
```
bit: 7 6 5 4 3 2 1 0
[ 0 | 0 | OP | BYTE | STATE | PIN NUMBER ]
OP (bit 5) 1 - WRITE, 0 - READ
BYTE (bit 4) 1 - STATE byte, 0 - DIRECTION byte
STATE (bit 3) 1 - ON, 0 - OFF
PIN (bit 2-0) 000 - pin 0 ... 111 - pin 7
```
- WRITE requests go to the driver with `write(fd, &cmd, 1)`.
- READ requests are answered by `read(fd, buf, 2)`, which returns `{state byte, direction byte}`;
the library picks out the requested pin. A READ byte is never written to the driver.
- `setAllPins()` / `initPins()` use the driver's `ioctl()` operations 0 (INIT), 1 (all LOW), 2 (all HIGH).
## Build and run (on the Raspberry Pi)
```bash
# driver (repo root)
make
sudo insmod pcf8574.ko
sudo dmesg -l warn # "Major is N" and the probe() message must both be there
sudo mknod /dev/pcf8574 c N 0
sudo chmod 666 /dev/pcf8574 # lets the demo run without sudo
# library + demo (this folder)
make
./pcf_demo # LEDs wired pin -> resistor -> LED -> GND
./pcf_demo --active-low # LEDs wired VCC -> resistor -> LED -> pin
./pcf_demo --sim # no hardware: built-in simulator
```
## Using the library
```c
#include "libpcf8574.h"
pcfOpen(NULL); /* /dev/pcf8574 */
setPin(6, PCF_ON); /* sends 0011 1110 */
int on = readPin(6); /* 1 */
setAllPins(PCF_OFF); /* one ioctl() */
pcfClose();
```
Link with `libpcf8574.a`: `gcc -o app app.c libpcf8574.a`
Do this once at home with the exact hardware you'll bring, then again on the morning of the Open Day.
The PCF8574 can pull a pin firmly to ground but only gives out a fraction of a milliamp when a pin is HIGH. LEDs therefore go from 3.3 V, through a resistor, into the pin: a LOW pin lights the LED. Run the demo with --active-low and “ON” still means “lit”.
Powering the PCF8574 from 3.3 V keeps the I2C lines at the Pi's own voltage. The Pi's pins are not 5 V tolerant.
make
sudo insmod pcf8574.ko
sudo dmesg -l warn | tail -n 5
sudo i2cdetect -y 1 # optional: shows UU at the chip's address once the driver owns it
You need both lines in the log: Major is N and PCF8574 is found in the device tree and probe() is executed by the i2c core!
If the probe() line is missing, don't start the demo. The driver's write() would use an I2C client that was never set and can crash the kernel. Fix the Device Tree entry or address first.
sudo mknod /dev/pcf8574 c N 0 # N = the major number from dmesg sudo chmod 666 /dev/pcf8574 # lets the demo run without sudo
cd userspace make ./pcf_demo --active-low
Zoom the terminal font so the LEDs and the byte can be read from two metres away. If you run it on the Pi's text console instead of a desktop terminal, run sudo dmesg -n 1 first: the driver logs every read and write, and those messages would print over the demo.
If the hardware fails on the day, the conversation doesn't have to: run ./pcf_demo --sim on any Linux laptop (or WSL) and keep this page open on the tablet. Both behave exactly like the driver.
Ticks are saved in this browser only.
Print it next to the LEDs so visitors can open the simulator and the code on their phones.
Right-click (or long-press) the code to save it as an image.
Visitors use it to check their name on the LEDs. Values from left to right: 128 · 64 · 32 · 16 · 8 · 4 · 2 · 1