Article Published on 1st May 2017
Background
I have experience working with GPS/GSM/4G CANBus Emulators/Simulators and interfacing these modules using Python, Bash, C, and C++. These modules often come with drivers, and sometimes these drivers present issues. This led me to explore how these drivers function.
Objective
Using a Raspberry Pi with an attached RGB LED, I aimed to:
Use the LED as a status indicator.
Control the LED via a kernel module.
Send commands to the kernel module through a character device file to dictate the LED's blinking pattern.
Command Structure
Commands consist of three numbers:
Color:
RED: 3
GREEN: 4
BLUE: 5
Blink Duration:
SHORT: 6
NORMAL: 7
LONG: 8
Blink Count: Between 1 to 9 blinks.
Examples:
3 8 2
: Blink the red LED with a long delay 2 times.5 7 7
: Blink the blue LED with a normal delay 7 times.4 8 1
: Blink the green LED with a long delay once.
Implementation
Code Design
I designed a simple solution that utilizes a linked list to manage LED state tasks. A timer interrupt is used to execute tasks based on the command provided.
Handling the Character Device File
The character device file provides the device driver capabilities to the module. This includes fetching a major number, registering a device class and device, and implementing open, release, read, and write functionalities.
/**
* @file chardev.h
* @author Eshan Shafeeq
* @version 0.1
* @date 29 March 2017
* @brief This file provides the device driver
* capabilities to the module including
* this file. Fetches a major number.
* Registers a device class and a device.
* Also implements open, release, read and write
* functionality for the file operations structure.
*
**/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/device.h>
#include "printops.h"
#include "command_process.h"
#ifndef _CHARDEV_H_
#define _CHARDEV_H_
#define DEVICE_NAME "sled"
#define DEVICE_CLASS "sled_class"
/**
* Module Global Variables
* -----------------------
* @param major_number : To store the major number assigned
* to this device;
* @param cmd_buff : A buffer to store the command from
* the user.
* @param cmd_buff_len : To store the length of the buffer
* recieved from the user.
* @param cmd_device_class : To store the device class structure.
* @param cmd_device : To store the registered device structure.
*
**/
static int major_number;
//static char cmd_buff;
//static short cmd_buff_length;
static struct class* cmd_device_class = NULL;
static struct device* cmd_device = NULL;
/**
* Prototype Functions [FOPS]
* --------------------------
**/
static int device_open( struct inode *,
struct file * );
static int device_release( struct inode *,
struct file * );
static ssize_t device_read( struct file *,
char *,
size_t,
loff_t * );
static ssize_t device_write( struct file *,
const char *,
size_t,
loff_t * );
/**
* File Operations Structure
* -------------------------
**/
static struct file_operations fops = {
.open = device_open,
.release = device_release,
.read = device_read,
.write = device_write
};
/**
* Char device Open
* ----------------
**/
static int device_open( struct inode *ptr_inode, struct file *ptr_file ){
kern_info( 0, "Device has been opened");
return 0;
}
/**
* Char device Release
* -------------------
**/
static int device_release( struct inode *ptr_inode, struct file *ptr_file ){
kern_info( 0, "Device has been released");
return 0;
}
/**
* Char device Read
* ----------------
**/
static ssize_t device_read( struct file *ptr_file, char *buff,
size_t buff_len, loff_t *offset ){
return 0;
}
/**
* Char device Write
* -----------------
**/
static ssize_t device_write( struct file *ptr_file, const char *buff,
size_t buff_len, loff_t *offset ){
kern_info( 0, "Recieved %d bytes from user", buff_len );
process_command( buff, buff_len );
return buff_len;
}
/**
* Setup character device function
* -------------------------------
**/
static int setup_chardev(void){
kern_info( 0, "Character device setup");
//Major number setup
major_number = register_chrdev( 0, DEVICE_NAME, &fops );
if( major_number < 0 ){
kern_alert( 0, "Failed to obtain a MAJOR_NUMBER");
return major_number;
}
kern_info( 3, "Successfully Obtained the MAJOR_NUMBER : %d", major_number );
//Device class setup
cmd_device_class = class_create( THIS_MODULE, DEVICE_CLASS );
if( IS_ERR( cmd_device_class ) ){
unregister_chrdev( major_number, DEVICE_NAME );
kern_alert( 0, "Failed to register the DEVICE CLASS");
return PTR_ERR( cmd_device_class );
}
kern_info( 0, "Successfully Registered the DEIVCE CLASS");
//Deivce Registration setup
cmd_device = device_create( cmd_device_class, NULL, MKDEV( major_number, 0 ),
NULL, DEVICE_NAME );
if( IS_ERR( cmd_device ) ){
class_destroy( cmd_device_class );
unregister_chrdev( major_number, DEVICE_NAME );
kern_alert( 0, "Failed to register device");
return PTR_ERR( cmd_device );
}
kern_info( 0, "Successfully Registered the DEVICE");
return 0;
}
/**
* Remove the character device functionality
* -----------------------------------------
**/
static void remove_chardev(void){
kern_info( 0, "Character device removal");
//Remove the device
device_destroy( cmd_device_class, MKDEV( major_number, 0) );
//Remove class
class_unregister( cmd_device_class );
class_destroy( cmd_device_class );
//Unregbister Major number
unregister_chrdev( major_number, DEVICE_NAME );
}
#endif
Managing GPIO Pins
To control the LED, we need to manage the GPIO pins connected to it. In this example, GPIO pins 4, 17, and 27 on the Raspberry Pi are used.
/**
* @file led_gpio.h
* @author Eshan Shafeeq
* @version 0.1
* @date 31 March 2017
* @brief This file is to give access to
* gpio pins to control leds.
**/
#include <linux/gpio.h>
#include "printops.h"
#ifndef _LED_GPIO_H_
#define _LED_GPIO_H_
/**
* Structure to hold the array of
* leds with initial state
**/
static struct gpio leds[] = {
{ 4, GPIOF_OUT_INIT_HIGH, "REDLED" },
{ 17, GPIOF_OUT_INIT_HIGH, "GREENLED" },
{ 27, GPIOF_OUT_INIT_HIGH, "BLUELED" }
};
/**
* To know whether the gpio's
* have been assigned
**/
static bool initialized;
/**
* Initialization function
*
* @brief Requests for the required
* array of gpios.
* @param ret To store the state of
* the request function.
**/
static inline void initiate_leds( void ){
int ret =0;
ret = gpio_request_array( leds, ARRAY_SIZE( leds ) );
if( ret < 0 ){
kern_alert( 0, "Failed to initialize leds" );
initialized = false;
return;
}
kern_info( 0, "Leds have been initialised Succesfully" );
initialized = true;
}
/**
* Destructor
*
* @brief Release the gpio pins which
* was requested previously.
**/
static inline void release_leds( void ){
size_t i;
for( i=0; i<ARRAY_SIZE( leds ); i++ ){
gpio_set_value( leds[i].gpio, 0 );
}
gpio_free_array( leds, ARRAY_SIZE( leds ) );
kern_info( 0, "Leds have been released" );
initialized = false;
}
/**
* Toggle led
*
* @brief This function sets a given led on or off
*
* @param index The index of the led to be toggled
* @param value The boolean state that the led should have.
*
**/
static inline void toggle_led( size_t index, bool value ){
//kern_info( 5, "index %d state %s", index, value?"true":"false" );
if( value ){
gpio_set_value( leds[ index ].gpio, 0 );
}else{
gpio_set_value( leds[ index ].gpio, 1 );
}
}
#endif
Processing Commands
Commands passed to the character device file are processed to determine the LED's behavior.
/**
* @file process_command.h
* @author Eshan Shafeeq
* @version 0.1
* @date 31 March 2017
* @brief This file is to process commands received
* from the user through character device file
* and assign the appropriate function based on
* the command parameters.
**/
#include <linux/kernel.h>
#include <linux/module.h>
#include "printops.h"
#include "interrupt.h"
#ifndef _PROCESSCOMMAND_H_
#define _PROCESSCOMMAND_H_
/**
* Validate buffer
*
* @brief This function validates the buffer received
* from the user. Makes sure only white space
* and numeric values are given as command.
*
* @param buff The buffer received from the user
* @param buff_len The length of the buffer
* @param buff_i The value of buff[i] converted to
* ACII value.
* @param space To keep count of the white space
* @param num To keep count of the numeric digits
*
**/
static inline bool validate_buffer( const char *buff, size_t buff_len ){
size_t i;
int buff_i = 0;
short space=0;
short num=0;
for( i=0; i<buff_len-1; i++ ){
buff_i = buff[i] - '0';
//debug
//kern_info( 0, "processing #%i : %c - %d", i, buff[i], buff_i );
//
if( buff_i == -16 || buff_i > -1 ){
if( buff_i == -16 )
space++;
if( buff_i > -1 && buff_i < 10 )
num++;
}else{
return false;
}
}
if( space == 2 && num == 3 )
return true;
else
return false;
}
/**
* Process Command
*
* @brief This function processes the command
* received by the buffer and executes
* the proper action.
*
* @param buff The buffer received from the user
* @param buff_len The length of the buffer
* @param color To store the color selected by the user
* @param delay To store the delay length selected by the user
* @param qty To store the number of blinks selected
*
**/
static void process_command( const char *buff, size_t buff_len ){
// size_t i;
short color;
short delay;
short qty;
if( validate_buffer( buff, buff_len ) ){
kern_info( buff_len-2, "buffer : %s", buff);
/* --DEBUG--
* kern_info(0, " index: char|ascii|int cast");
kern_info(0, " --------------------------");
for( i=0; i<buff_len-1; i++ ){
kern_info(0, "analysis #%i: %c | %d | %d", i, buff[i], buff[i], buff[i] - '0');
}*/
color = buff[0] - '0';
delay = buff[2] - '0';
qty = buff[4] - '0';
switch(color){
case RED:
kern_info( 0, "RED color selected");
break;
case GREEN:
kern_info( 0, "GREEN color selected");
break;
case BLUE:
kern_info( 0, "BLUE color selected");
break;
}
switch(delay){
case SHORT:
kern_info( 0, "SHORT time delay selected");
break;
case NORMAL:
kern_info( 0, "NORMAL time delay selected");
break;
case LONG:
kern_info( 0, "LONG time delay selected");
break;
}
kern_info( 0, "QTY : %d", qty );
start_timer_interrupt(color, delay, qty);
}
}
#endif
Timer Interrupt Routines
This section defines the structures used for the linked list, which aids in command processing. A semaphore is used to prevent multiple timer interrupts.
/**
* @file interrupt.h
* @author Eshan Shafeeq
* @version 0.1
* @date 31 March 2017
* @brief This file implements the timer
* interrupt function to blink an
* led the requested amount of times
* concurrently. It uses a semaphore
* to prevent crashes.
*
**/
#include <linux/timer.h>
#include <linux/sched.h>
#include <linux/semaphore.h>
#include <linux/list.h>
#include <linux/slab.h>
#include "printops.h"
#include "led_gpio.h"
#ifndef _INTERRUPT_H_
#define _INTERRUPT_H_
#define DELAY_TIME 50
#define SHORT_DELAY 10
#define NORMAL_DELAY 50
#define LONG_DELAY 100
#define RED 3
#define GREEN 4
#define BLUE 5
#define SHORT 6
#define NORMAL 7
#define LONG 8
/**
* Globals
*
* @param interrupt The timer_list structure to
* interrupt regularly.
* @param running The semaphore to prevent access
* to the timer start function.
* @param state_list The structure to hold the led
* states over time.
* @param state_len To store the size of the state
* list.
*
**/
static struct timer_list interrupt;
static struct semaphore running;
struct led_state {
int id;
short color;
short delay;
bool state;
struct list_head head;
};
struct led_state state_list;
static int state_len;
/**
* Create new Task
*
* @brief To dynamically allocate space for
* a new led_state object and return
* the address to the caller.
*
* @param i Id of the newly created object
* @param c Color of the newly created object
* @param d Delay time of newly created object
* @param s State of the newly created object
* @param new_state The pointer to access the newly created object
*
**/
static struct led_state * create_new_task(int i, short c, short d, bool s){
struct led_state *new_state;
new_state = kmalloc( sizeof( *new_state ), GFP_KERNEL );
new_state->id = i;
new_state->color = c;
switch( d ){
case SHORT:
new_state->delay = SHORT_DELAY;
break;
case NORMAL:
new_state->delay = NORMAL_DELAY;
break;
case LONG:
new_state->delay = LONG_DELAY;
break;
}
new_state->state = s;
INIT_LIST_HEAD( &new_state->head );
state_len++;
return new_state;
}
/**
* Get Delay
*
* @brief obtain the delay amount from the
* object recieved via the value index.
*
* @param value index of the state_list object which
* the delay time should be obtained from.
*
**/
static short get_delay( unsigned long value ){
struct led_state *state;
list_for_each_entry( state, &(state_list.head), head ){
if( value==state->id ){
return state->delay;
}
}
return DELAY_TIME;
}
/**
* Create a task list
*
* @brief Create a linkedlist of led_state type objects.
*
* @param color Color variable of all the objects in the list.
* @param delay Delay time for all the objects
* @param qty Number of blinks for the specific list.
* @param new_state The pointer to hold a reference to newly
* created object.
*
**/
static void create_task_list( short color, short delay, short qty ){
struct led_state *new_state;
size_t i;
kern_info( 0, "Initializing task list" );
INIT_LIST_HEAD( &state_list.head );
state_len=0;
//Populate the list
for( i=0; i<(qty*2); i+=2 ){
new_state = create_new_task(i, color, delay, true);
list_add_tail( &(new_state->head), &(state_list.head) );
new_state = create_new_task(i+1, color, delay, false);
list_add_tail( &(new_state->head), &(state_list.head) );
}
}
/**
* Destroy Task List
*
* @brief Release the memory dynamically allocated
* to the state list once it is not required.
*
* @param state_iter A pointer variable to iterate the list
* @param state_iter_tmp A pointer variable for safe iteration
*
**/
static void destroy_task_list( void ){
struct led_state *state_iter;
struct led_state *state_iter_tmp;
list_for_each_entry_safe( state_iter, state_iter_tmp, &(state_list.head), head ){
list_del( &(state_iter->head) );
kfree( state_iter );
}
}
/**
* Trigger Led
*
* @brief This function triggers the selected
* led based on the number of blinks.
*
* @param state A pointer to obtain the values
* from the chosen state.
* @param value The current index of the chosen
* object from the linked list.
*
**/
static void trigger_led( unsigned long value ){
struct led_state *state;
list_for_each_entry( state, &(state_list.head), head ){
if( value==state->id ){
//kern_info( 0, "color : %hu", state->color );
//kern_info( 0, "delay : %hu", state->delay );
//kern_info( 0, "state : %hu", state->state );
switch( state->color ){
case RED:
toggle_led( 0, state->state );
break;
case GREEN:
toggle_led( 1, state->state );
break;
case BLUE:
toggle_led( 2, state->state );
break;
}
}
}
}
/**
* Interrupt Routine
*
* @brief This is the interrupt routine which
* happen based on the delay time
* requested by the user.
*
* @param value The count of the current iteration.
* also to keep track of the current
* state from the state list.
*
**/
static void interrupt_routine( unsigned long value ){
//kern_info( 0, "routine %ld", value );
trigger_led( value );
interrupt.data = ++value;
interrupt.expires = jiffies + get_delay( value );
if( value < state_len ){
add_timer( &interrupt );
}else{
destroy_task_list();
release_leds();
up( &running );
kern_info( 0, "Timer stopped");
}
}
/**
* State Timer Interrupt
*
* @brief The function which starts all the magic
*
* @param color The color obtained from the command.
* @param delay The delay time obtained from the command.
* @param qty The blink amount obtained from the command.
*
**/
static bool start_timer_interrupt( short color, short delay, short qty ){
kern_info( 0, "Timer started");
if( down_interruptible(&running) == 0 ){
interrupt.data = 0;
create_task_list( color, delay, qty );
initiate_leds();
interrupt.expires = jiffies + DELAY_TIME;
add_timer( &interrupt );
return true;
}else{
return false;
}
}
/**
* Setup Timer Interrupts
*
* @brief Initiates the semaphore and the interrupt function.
*
**/
static void setup_timer_interrupt(void){
kern_info( 0, "Setting up timer interrupt" );
sema_init( &running, 1 );
init_timer( &interrupt );
interrupt.function= interrupt_routine;
}
/**
* Remove Timer
*
* @brief Function to end all the magic happening.
*
**/
static void remove_timer(void){
kern_info( 0, "Removing timer interrupt");
del_timer_sync( &interrupt );
}
#endif
Miscellaneous Code
For convenience, I created functions to simplify kernel logging statements.
/**
* @file printops.h
* @author Eshan Shafeeq
* @version 0.1
* @date 25 March 2017
* @brief This file contains some easy functions
* to print out messages to the kernel log
**/
#include <linux/kernel.h>
#include <linux/module.h>
#ifndef _PRINTOPS_H_
#define _PRINTOPS_H_
/**
* kern_info
*
* @brief Prints a message to the kernel buffer
* with the message priority of KERN_INFO.
* Usually for debugging purposes.
*
**/
static void kern_info( int len, const char *format, ...){
char msg[256];
va_list args;
va_start( args, format );
vsnprintf(msg, strlen( format ) + len + 1, format, args );
va_end( args );
printk(KERN_INFO "[SLED]: %s\n", msg);
}
/**
* kern_alert
*
* @brief Prints a message to the kernel buffer
* with a higher priority. Usually for
* alert messages
*
**/
static void kern_alert( int len, const char *format, ...){
char msg[256];
va_list args;
va_start( args, format );
vsnprintf(msg, strlen( format ) + len + 1, format, args );
va_end( args );
printk(KERN_ALERT "[SLED]: %s\n", msg);
}
#endif
Main File
This file provides the necessary definitions for the kernel module and defines the __init
and __exit
methods.
/**
* @file chardev_command.c
* @author Eshan Shafeeq
* @date 30 March 2017
* @version 0.1
* @brief A character device driver capable
* of receiving commands and distinguishing
* them and giving outputs based on the
* commands received.
**/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include "chardev.h"
#include "interrupt.h"
/**
* Module definitions
* ------------------
**/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Eshan Shafeeq");
MODULE_DESCRIPTION("Device driver to recieve commands and execute them");
MODULE_VERSION("0.1");
MODULE_SUPPORTED_DEVICE(DEVICE_NAME);
/**
* Module Initialization
* ---------------------
**/
static int __init cmd_dev_init(void){
int ret;
ret = setup_chardev();
if( ret != 0 ){
return ret;
}
setup_timer_interrupt();
return 0;
}
/**
* Module Termination
* ------------------
**/
static void __exit cmd_dev_exit(void){
remove_chardev();
remove_timer();
}
module_init( cmd_dev_init );
module_exit( cmd_dev_exit );
Building the Driver
A Makefile is used to build the driver.
obj-m +=sled.o
sled-objs := start.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Usage
To try this kernel module on a Raspberry Pi 3:
Build the module:
$ sudo insmod sled.ko
Check if the module is loaded:
$ dmesg
Set permissions for the device file:
$ sudo chmod 666 /dev/sled
Send commands to the device file:
$ echo "3 8 3" > /dev/sled