From 998113c260f9c9b7889fcdf39c8446add37390ff Mon Sep 17 00:00:00 2001
From: Jeremy90307 <jeremy90307@gmail.com>
Date: Thu, 19 Dec 2024 15:36:54 +0800
Subject: [PATCH] Add an LED driver example using GPIO

Use GPIO to control LED on/off and add related GPIO knowledge.

Test detail:

- Tested on Raspberry Pi 5B with Raspberry Pi OS (Debian 12, Linux
  version 6.12.1-v8-16k+)

- Verify that LED example compiles and loads successfully

- Verify that LED turns on and off sucessfully
---
 .ci/non-working   |   1 +
 examples/Makefile |   1 +
 examples/led.c    | 203 ++++++++++++++++++++++++++++++++++++++++++++++
 lkmpg.tex         |  50 ++++++++++++
 4 files changed, 255 insertions(+)
 create mode 100644 examples/led.c

diff --git a/.ci/non-working b/.ci/non-working
index 31648232..6f2c90eb 100644
--- a/.ci/non-working
+++ b/.ci/non-working
@@ -3,3 +3,4 @@ bh_threaded
 intrpt
 vkbd
 syscall-steal
+led
diff --git a/examples/Makefile b/examples/Makefile
index 01ab18b7..df05b032 100644
--- a/examples/Makefile
+++ b/examples/Makefile
@@ -31,6 +31,7 @@ obj-m += ioctl.o
 obj-m += vinput.o
 obj-m += vkbd.o
 obj-m += static_key.o
+obj-m += led.o
 
 PWD := $(CURDIR)
 
diff --git a/examples/led.c b/examples/led.c
new file mode 100644
index 00000000..a016c7a3
--- /dev/null
+++ b/examples/led.c
@@ -0,0 +1,203 @@
+/*
+ * led.c - Using GPIO to control the LED on/off
+ */
+
+#include <linux/cdev.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/fs.h>
+#include <linux/gpio.h>
+#include <linux/init.h>
+#include <linux/module.h>
+#include <linux/printk.h>
+#include <linux/types.h>
+#include <linux/uaccess.h>
+#include <linux/version.h>
+
+#include <asm/errno.h>
+
+#define SUCCESS 0
+#define DEVICE_NAME "gpio_led"
+#define DEVICE_CNT 1
+#define BUF_LEN 2
+
+static char control_signal[BUF_LEN];
+static unsigned long device_buffer_size = 0;
+
+struct LED_dev {
+    dev_t dev_num;
+    int major_num;
+    int minor_num;
+    struct cdev cdev;
+    struct class *cls;
+    struct device *dev;
+};
+
+static struct LED_dev led_device;
+
+/* Define GPIOs for LEDs.
+ * TODO: According to the requirements, search /sys/kernel/debug/gpio to 
+ * find the corresponding GPIO location.
+ */
+static struct gpio leds[] = { { 4, GPIOF_OUT_INIT_LOW, "LED 1" } };
+
+/* This is called whenever a process attempts to open the device file */
+static int device_open(struct inode *inode, struct file *file)
+{
+    pr_info("device_open(%p)\n", file);
+
+    return SUCCESS;
+}
+
+static int device_release(struct inode *inode, struct file *file)
+{
+    pr_info("device_release(%p,%p)\n", inode, file);
+
+    return SUCCESS;
+}
+
+/* called when somebody tries to write into our device file. */
+static ssize_t device_write(struct file *file, const char __user *buffer,
+                            size_t length, loff_t *offset)
+{
+    pr_info("device_write(%p,%p,%ld)", file, buffer, length);
+
+    device_buffer_size = min(BUF_LEN, length);
+
+    if (copy_from_user(control_signal, buffer, device_buffer_size)) {
+        return -EFAULT;
+    }
+
+    /* Determine the received signal to decide the LED on/off state. */
+    switch (control_signal[0]) {
+    case '0':
+        gpio_set_value(leds[0].gpio, 0);
+        pr_info("LED OFF");
+        break;
+    case '1':
+        gpio_set_value(leds[0].gpio, 1);
+        pr_info("LED ON");
+        break;
+    default:
+        pr_warn("Invalid value!\n");
+        break;
+    }
+
+    *offset += device_buffer_size;
+
+    /* Again, return the number of input characters used. */
+    return device_buffer_size;
+}
+
+static struct file_operations fops = {
+    .owner = THIS_MODULE,
+    .write = device_write,
+    .open = device_open,
+    .release = device_release,
+};
+
+/* Initialize the module - Register the character device */
+static int __init led_init(void)
+{
+    int ret = 0;
+
+    /* Determine whether dynamic allocation of the device number is needed. */
+    if (led_device.major_num) {
+        led_device.dev_num = MKDEV(led_device.major_num, led_device.minor_num);
+        ret =
+            register_chrdev_region(led_device.dev_num, DEVICE_CNT, DEVICE_NAME);
+    } else {
+        ret = alloc_chrdev_region(&led_device.dev_num, 0, DEVICE_CNT,
+                                  DEVICE_NAME);
+    }
+
+    /* Negative values signify an error */
+    if (ret < 0) {
+        pr_alert("%s failed with %d\n",
+                 "Sorry, registering the character device ", ret);
+        return ret;
+    }
+
+    pr_info("Major = %d, Minor = %d\n", MAJOR(led_device.dev_num),
+            MINOR(led_device.dev_num));
+
+    /* Prevents module unloading while operations are in use */
+    led_device.cdev.owner = THIS_MODULE;
+
+    cdev_init(&led_device.cdev, &fops);
+    ret = cdev_add(&led_device.cdev, led_device.dev_num, 1);
+    if (ret) {
+        pr_err("Failed to add the device to the system\n");
+        goto fail1;
+    }
+
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)
+    led_device.cls = class_create(DEVICE_NAME);
+#else
+    led_device.cls = class_create(THIS_MODULE, DEVICE_NAME);
+#endif
+    if (IS_ERR(led_device.cls)) {
+        pr_err("Failed to create class for device\n");
+        ret = PTR_ERR(led_device.cls);
+        goto fail2;
+    }
+
+    led_device.dev = device_create(led_device.cls, NULL, led_device.dev_num,
+                                   NULL, DEVICE_NAME);
+    if (IS_ERR(led_device.dev)) {
+        pr_err("Failed to create the device file\n");
+        ret = PTR_ERR(led_device.dev);
+        goto fail3;
+    }
+
+    pr_info("Device created on /dev/%s\n", DEVICE_NAME);
+
+    ret = gpio_request(leds[0].gpio, leds[0].label);
+
+    if (ret) {
+        pr_err("Unable to request GPIOs for LEDs: %d\n", ret);
+        goto fail4;
+    }
+
+    ret = gpio_direction_output(leds[0].gpio, leds[0].flags);
+
+    if (ret) {
+        pr_err("Failed to set GPIO %d direction\n", leds[0].gpio);
+        goto fail5;
+    }
+
+    return 0;
+
+fail5:
+    gpio_free(leds[0].gpio);
+
+fail4:
+    device_destroy(led_device.cls, led_device.dev_num);
+
+fail3:
+    class_destroy(led_device.cls);
+
+fail2:
+    cdev_del(&led_device.cdev);
+
+fail1:
+    unregister_chrdev_region(led_device.dev_num, DEVICE_CNT);
+
+    return ret;
+}
+
+static void __exit led_exit(void)
+{
+    gpio_set_value(leds[0].gpio, 0);
+    gpio_free(leds[0].gpio);
+
+    device_destroy(led_device.cls, led_device.dev_num);
+    class_destroy(led_device.cls);
+    cdev_del(&led_device.cdev);
+    unregister_chrdev_region(led_device.dev_num, DEVICE_CNT);
+}
+
+module_init(led_init);
+module_exit(led_exit);
+
+MODULE_LICENSE("GPL");
diff --git a/lkmpg.tex b/lkmpg.tex
index 1f5f6685..b33f0428 100644
--- a/lkmpg.tex
+++ b/lkmpg.tex
@@ -1816,6 +1816,56 @@ \subsection{Flashing keyboard LEDs}
 Adding debug code can change the situation enough to make the bug seem to disappear.
 Thus, you should keep debug code to a minimum and make sure it does not show up in production code.
 
+\section{GPIO}
+\label{sec:gpio}
+\subsection{GPIO}
+\label{sec:gpio_introduction}
+General Purpose Input/Output (GPIO) appears on the development board as pins. It acts as a bridge for communication between the development board and external devices. You can think of it like a switch: users can turn it on or off (Input), and the development board can also turn it on or off (Output).
+
+To implement GPIO, you use the \cpp|gpio_request()| function to enable a specific GPIO pin. After successfully enabling it, you can check that the pin is being used by looking at /sys/kernel/debug/gpio.
+
+\begin{codebash}
+cat /sys/kernel/debug/gpio
+\end{codebash}
+
+There are other ways to register GPIOs. For example, you can use \cpp|gpio_request_one()| to register a GPIO while setting its direction (input or output) and initial state at the same time. You can also use \cpp|gpio_request_array()| to register multiple GPIOs at once. However, note that \cpp|gpio_request_array()| has been removed since Linux v6.10+.
+
+When using GPIO, you must set it as either output with \cpp|gpio_direction_output()| or \cpp|input with gpio_direction_input()|.
+
+\begin{itemize}
+  \item when the GPIO is set as output, you can use \cpp|gpio_set_value()| to choose to set it to high voltage or low voltage.
+  \item when the GPIO is set as input, you can use \cpp|gpio_get_value()| to read whether the voltage is high or low.
+\end{itemize}
+
+\subsection{Control the LED's on/off state}
+\label{sec:gpio_led}
+In Section \ref{sec:device_files}, we learned how to communicate with Device Files. Therefore, we will further use Device Files to control the LED on and off.
+
+In the implementation, a pull-down resistor is used. The positive electrode of the LED is connected to GPIO4, and the negative electrode is connected to GND. The materials used include a Raspberry Pi 5, an LED, single-core wires, and a 220$\Omega$ resistor.
+
+\samplec{examples/led.c}
+
+Make and install the module:
+\begin{codebash}
+make
+sudo insmod led.ko
+\end{codebash}
+
+Switch on the LED:
+\begin{codebash}
+echo "1" | sudo tee /dev/gpio_led
+\end{codebash}
+
+Switch off the LED:
+\begin{codebash}
+echo "0" | sudo tee /dev/gpio_led
+\end{codebash}
+
+Finally, remove the module:
+\begin{codebash}
+sudo rmmod led
+\end{codebash}
+
 \section{Scheduling Tasks}
 \label{sec:scheduling_tasks}
 There are two main ways of running tasks: tasklets and work queues.