Introduction to Android Kernel Module Development
In the realm of embedded systems, gaining low-level control over hardware components is paramount. Android, while primarily an application-centric OS, fundamentally relies on the Linux kernel. This means that for custom hardware integrations, such as controlling General Purpose Input/Output (GPIO) pins, developers often need to extend the kernel’s capabilities. Linux Kernel Modules (LKMs) provide a robust mechanism to achieve this, allowing developers to add or remove code from the kernel at runtime without recompiling the entire kernel. This article delves into the process of creating a custom LKM for Android, demonstrating GPIO control as a practical example for embedded systems.
Developing device drivers for Android presents unique challenges due to its specific build environment and deployment strategies. However, the underlying Linux driver model remains consistent. By focusing on LKMs, we can achieve modularity, easier debugging, and quicker iteration cycles.
Prerequisites and Setting Up the Build Environment
Before diving into driver development, ensure you have the following prerequisites:
- Android Kernel Source Code: Obtain the kernel source code that precisely matches your target Android device’s kernel version. Mismatched kernel versions can lead to compilation errors or stability issues.
- Cross-Compilation Toolchain: You’ll need a cross-compiler (e.g., `aarch64-linux-android-`) that matches your target architecture. This is typically found within the Android NDK or provided by your SoC vendor.
- Target Android Device with Root Access: To load and test the LKM, your device must be rooted to allow `insmod`, `rmmod`, and `mknod` commands.
- ADB (Android Debug Bridge): For pushing files and executing commands on the device.
Setting up the Build Environment
Assuming your kernel source is in `~/android-kernel/`, and your toolchain is correctly set up in your PATH, the essential `Makefile` variables are `ARCH` (e.g., `arm64`) and `CROSS_COMPILE` (e.g., `aarch64-linux-gnu-` or `aarch64-linux-android-`).
export ARCH=arm64
export CROSS_COMPILE=/path/to/your/toolchain/bin/aarch64-linux-android-
KERNEL_DIR=~/android-kernel/
Understanding Linux Kernel GPIO
In the Linux kernel, GPIOs are managed through a generic framework. Drivers interact with GPIO pins using a set of standard functions rather than direct register access, promoting portability. Key functions include:
- `gpio_request_one()`: Request and configure a single GPIO pin.
- `gpio_direction_output()`: Set a GPIO pin as an output.
- `gpio_direction_input()`: Set a GPIO pin as an input.
- `gpio_set_value()`: Set the value (high/low) of an output GPIO pin.
- `gpio_get_value()`: Read the value of an input (or output) GPIO pin.
- `gpio_free()`: Release a previously requested GPIO pin.
The specific GPIO numbers often map to hardware-specific pins defined in the device tree (DTS/DTB) for modern ARM systems. For this example, we’ll assume a known GPIO number (e.g., `GPIO_PIN_NUMBER`).
Developing the Custom LKM: GPIO Control
Our LKM will create a character device file (`/dev/my_gpio_driver`). User-space applications can then open this file and use `read()` and `write()` system calls to control the GPIO pin.
Module Structure
Every LKM requires `module_init()` and `module_exit()` functions, which are called when the module is loaded and unloaded, respectively. We also define `MODULE_LICENSE()` as GPL.
GPIO Initialization and Device Operations
The `module_init` function will:
- Request the GPIO pin.
- Register a character device driver with the kernel.
- Create a device file in `/dev` for user-space interaction.
The character device requires `struct file_operations` to define how `open`, `release`, `read`, and `write` calls are handled.
Read/Write Operations
- `my_gpio_open` and `my_gpio_release`: Simple functions to handle opening and closing the device file.
- `my_gpio_read`: Reads the current state of the GPIO pin and copies it to the user-space buffer using `copy_to_user()`.
- `my_gpio_write`: Reads a value from the user-space buffer using `copy_from_user()` and sets the GPIO pin’s state using `gpio_set_value()`.
LKM Code Example (`my_gpio_driver.c`)
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/gpio.h>
#include <asm/uaccess.h> // Required for copy_to_user/copy_from_user
#define DRIVER_NAME "my_gpio_driver"
#define CLASS_NAME "my_gpio_class"
// --- CHANGE THIS TO YOUR DEVICE'S ACTUAL GPIO NUMBER ---
#define GPIO_PIN_NUMBER 20 // Example GPIO number
// --------------------------------------------------------
static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_gpio_class;
static int my_gpio_open(struct inode *i, struct file *f)
{
printk(KERN_INFO "my_gpio_driver: Device opened.n");
return 0;
}
static int my_gpio_release(struct inode *i, struct file *f)
{
printk(KERN_INFO "my_gpio_driver: Device closed.n");
return 0;
}
static ssize_t my_gpio_read(struct file *f, char __user *buf, size_t len, loff_t *off)
{
int value = gpio_get_value(GPIO_PIN_NUMBER);
char kbuf[2]; // '0' or '1' plus null terminator
printk(KERN_INFO "my_gpio_driver: Reading GPIO %d, value: %dn", GPIO_PIN_NUMBER, value);
snprintf(kbuf, sizeof(kbuf), "%d", value);
if (copy_to_user(buf, kbuf, sizeof(kbuf))) {
return -EFAULT;
}
return sizeof(kbuf);
}
static ssize_t my_gpio_write(struct file *f, const char __user *buf, size_t len, loff_t *off)
{
char kbuf;
if (copy_from_user(&kbuf, buf, 1)) {
return -EFAULT;
}
if (kbuf == '0') {
gpio_set_value(GPIO_PIN_NUMBER, 0);
printk(KERN_INFO "my_gpio_driver: Setting GPIO %d to LOWn", GPIO_PIN_NUMBER);
} else if (kbuf == '1') {
gpio_set_value(GPIO_PIN_NUMBER, 1);
printk(KERN_INFO "my_gpio_driver: Setting GPIO %d to HIGHn", GPIO_PIN_NUMBER);
} else {
printk(KERN_WARNING "my_gpio_driver: Invalid write value '%c'. Use '0' or '1'.n", kbuf);
return -EINVAL;
}
return 1;
}
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_gpio_open,
.release = my_gpio_release,
.read = my_gpio_read,
.write = my_gpio_write,
};
static int __init my_gpio_init(void)
{
int ret;
printk(KERN_INFO "my_gpio_driver: Initializing module.n");
// 1. Allocate device numbers
ret = alloc_chrdev_region(&dev_num, 0, 1, DRIVER_NAME);
if (ret < 0) {
printk(KERN_ALERT "my_gpio_driver: Failed to allocate device numbers.n");
return ret;
}
printk(KERN_INFO "my_gpio_driver: Device numbers allocated (Major: %d, Minor: %d).n", MAJOR(dev_num), MINOR(dev_num));
// 2. Initialize cdev structure and register it
cdev_init(&my_cdev, &my_fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
printk(KERN_ALERT "my_gpio_driver: Failed to add cdev.n");
unregister_chrdev_region(dev_num, 1);
return ret;
}
// 3. Create a device class
my_gpio_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(my_gpio_class)) {
printk(KERN_ALERT "my_gpio_driver: Failed to create device class.n");
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
return PTR_ERR(my_gpio_class);
}
// 4. Create device file in /dev
if (IS_ERR(device_create(my_gpio_class, NULL, dev_num, NULL, DRIVER_NAME))) {
printk(KERN_ALERT "my_gpio_driver: Failed to create device.n");
class_destroy(my_gpio_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
return -1; // Specific error code might be better
}
// 5. Request and configure GPIO pin
if (gpio_request_one(GPIO_PIN_NUMBER, GPIOF_OUT_INIT_LOW, DRIVER_NAME)) {
printk(KERN_ALERT "my_gpio_driver: Failed to request GPIO %d.n", GPIO_PIN_NUMBER);
device_destroy(my_gpio_class, dev_num);
class_destroy(my_gpio_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
return -ENODEV;
}
printk(KERN_INFO "my_gpio_driver: GPIO %d requested and set to LOW (output).n", GPIO_PIN_NUMBER);
printk(KERN_INFO "my_gpio_driver: Module loaded successfully.n");
return 0;
}
static void __exit my_gpio_exit(void)
{
printk(KERN_INFO "my_gpio_driver: Exiting module.n");
gpio_set_value(GPIO_PIN_NUMBER, 0); // Ensure GPIO is low on exit
gpio_free(GPIO_PIN_NUMBER);
device_destroy(my_gpio_class, dev_num);
class_destroy(my_gpio_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "my_gpio_driver: Module unloaded.n");
}
module_init(my_gpio_init);
module_exit(my_gpio_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple GPIO control LKM for Android embedded systems.");
MODULE_VERSION("0.1");
Building the LKM
Create a `Makefile` in the same directory as `my_gpio_driver.c`:
obj-m := my_gpio_driver.o
KDIR := $(KERNEL_DIR) # KERNEL_DIR from environment setup
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
Now, build the module:
make
This should produce `my_gpio_driver.ko` in the current directory.
Deploying and Testing on Android
1. Push the module to the device:
adb push my_gpio_driver.ko /data/local/tmp/
2. Load the module:
adb shell
su
cd /data/local/tmp/
insmod my_gpio_driver.ko
You should see `my_gpio_driver: Initializing module.` and `my_gpio_driver: Module loaded successfully.` in `dmesg` or `logcat -k`.
3. Verify device node:
ls -l /dev/my_gpio_driver
The module’s `device_create` function should automatically create `/dev/my_gpio_driver`. If it’s not present, you might need to create it manually, though it’s less common for modern kernels when `class_create` and `device_create` are used:
# Find major/minor numbers from dmesg (e.g., Major: 247, Minor: 0)
mknod /dev/my_gpio_driver c 247 0
chmod 666 /dev/my_gpio_driver
4. Test GPIO control from shell:
echo "1" > /dev/my_gpio_driver # Sets GPIO high
sleep 1
echo "0" > /dev/my_gpio_driver # Sets GPIO low
cat /dev/my_gpio_driver # Reads GPIO state
You should observe the physical GPIO pin changing state and the `cat` command returning ‘0’ or ‘1’.
5. Unload the module:
rmmod my_gpio_driver
Verify with `dmesg` for `my_gpio_driver: Exiting module.`
User-Space Interaction Example (`user_app.c`)
To interact with the driver from a user-space application, you can write a simple C program:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define DEVICE_NODE "/dev/my_gpio_driver"
int main()
{
int fd;
char buffer[2];
// Open the device file
fd = open(DEVICE_NODE, O_RDWR);
if (fd < 0) {
perror("Failed to open device");
return 1;
}
printf("Device opened successfully.n");
// Write '1' (HIGH) to GPIO
if (write(fd, "1", 1) < 0) {
perror("Failed to write to device");
close(fd);
return 1;
}
printf("GPIO set to HIGH.n");
sleep(1);
// Write '0' (LOW) to GPIO
if (write(fd, "0", 1) < 0) {
perror("Failed to write to device");
close(fd);
return 1;
}
printf("GPIO set to LOW.n");
sleep(1);
// Read GPIO state
memset(buffer, 0, sizeof(buffer));
if (read(fd, buffer, 1) < 0) {
perror("Failed to read from device");
close(fd);
return 1;
}
printf("GPIO state: %cn", buffer[0]);
// Close the device file
close(fd);
printf("Device closed.n");
return 0;
}
Compile this with your Android NDK toolchain for the target device (e.g., `aarch64-linux-android-gcc user_app.c -o user_app`), push it to the device, and execute it.
Conclusion
Building custom Linux Kernel Modules for Android embedded systems empowers developers to directly interface with hardware, bypassing the limitations of user-space APIs. This tutorial demonstrated how to create a simple GPIO control driver, highlighting the crucial steps from environment setup and kernel module coding to compilation, deployment, and user-space interaction. While this example focused on basic GPIO toggling, the principles extend to more complex peripherals, laying the groundwork for advanced hardware integration in custom Android devices.
Android Mobile Specs & Compare Directory
Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!
Compare Devices Specs →