Frontend Labs
⋯

54. Capstone: one sensor, driven two ways

Take the LM75-style I2C temperature sensor from Section 48.2 and build it both ways from Section 47's table, so the trade-off is concrete rather than theoretical.

Version A — user space (what you'd actually ship for this task): exactly Section 48.2's read_temperature, wrapped in a small CLI (Section 24) that prints a reading every second:

#include <stdio.h>
#include <unistd.h>

int read_temperature(const char *bus_path);   // from Section 48.2

int main(void) {
    while (1) {
        int c = read_temperature("/dev/i2c-1");
        if (c != -1) printf("%d C\n", c);
        sleep(1);
    }
}

No kernel code, no reboot risk, ships as an ordinary binary — this is correctly the default answer for "read a sensor" per Section 47.

Version B — a minimal kernel driver, for when the sensor's readings need to feed a kernel subsystem directly (for example, the kernel's hwmon framework, so /sys/class/hwmon/.../temp1_input and every existing temperature-monitoring tool works automatically, with no custom user-space program at all):

// lm75_mini.c — sketch: an i2c_driver that reads the same register Section 48.2 did,
// but from kernel context, matched to the hardware via the i2c core instead of a
// hand-opened /dev/i2c-1 node
#include <linux/module.h>
#include <linux/i2c.h>

static int lm75_read_temp(struct i2c_client *client) {
    s32 raw = i2c_smbus_read_word_data(client, 0x00);   // same register as Section 48.2
    if (raw < 0) return raw;
    s16 be = (s16)((raw << 8) | (raw >> 8));              // the chip returns big-endian
    return (be >> 7) / 2;                                   // identical math to Section 48.2
}

static int lm75_probe(struct i2c_client *client) {
    int temp = lm75_read_temp(client);
    dev_info(&client->dev, "lm75_mini: %d C\n", temp);
    return 0;
}

static const struct i2c_device_id lm75_id[] = { { "lm75_mini", 0 }, { } };
MODULE_DEVICE_TABLE(i2c, lm75_id);

static struct i2c_driver lm75_driver = {
    .driver = { .name = "lm75_mini" },
    .probe  = lm75_probe,
    .id_table = lm75_id,
};
module_i2c_driver(lm75_driver);   // registers the driver; the i2c core calls .probe
                                    // automatically once a matching device is found
MODULE_LICENSE("GPL");

The line worth sitting with is .probe = lm75_probe: Version A decides when to talk to the sensor (your while loop calls the shots). Version B instead registers interest — module_i2c_driver hands control to the kernel's i2c core, which calls lm75_probe on your behalf whenever a device matching lm75_id (typically declared in the board's Device Tree, outside the scope of this guide) actually shows up. That inversion — you stop calling the hardware and start being called by a framework when the hardware appears — is the core idea behind every Linux driver model (i2c, SPI, USB, platform, PCI): each framework differs in its match rules and callback set, but every one of them replaces your main()-style control flow with a probe/remove pair the kernel drives.


Generated from real-life-c-guide.md · 8 Oct 2026 16:36