Monday, 24 October 2016

Make 3 partitions of SD card for TI Device

Introduction

The Linux SD card provided with a Sitara AMx EVM may be re-created from the Sitara Linux SDK. This guide describes the process for creating an SD card which be loaded with all the system collateral needed to boot and run a Linux system on a Sitara EVM. PLEASE READ AND UNDERSTAND THIS ENTIRE ARTICLE BEFORE RUNNING ANY OF THE SCRIPTS DESCRIBED HERE.

How Many Partitions Do I Need?

For AM35x EVM or AM37 EVM
These EVMs utilize 3 partitions.  The card will have a small (~70M) bootable DOS partition for system binaries (x-loader, u-boot, uImage), an ext3 partition (~1G) for a Linux root file system, and another ext3 partition for a Linux installer executable and SDK source code.  The third partition will take up the remainder of the card space, so if the card is a 2G card the third partition will be about 1G.  If it is a 4G card then the third partition will be about 3G.

Partition Name
Type
OSContents
BootFAT32Windows/LinuxOut of Box Demo; windows_user.htm
File SystemEXT3LinuxRoot File System
START_HEREEXT3LinuxSDK Installer; setup.htm

For AM180x EVM or AM1810 EVM
These EVMs utilize 2 partitions.  There is an alternate script which will create a 2 partition card with only the boot and file system partitions.  The boot and file system partition are the only ones necessary to boot the system from an SD card.  The third partition is optional.


This process involves formatting and partitioning of drives. The user must be careful of the device names passed to the script used in this process. It is very possible to format and destroy the Linux host machine hard drive if the wrong device is passed to the script used here.

Hardware Assumptions

  • Linux host machine with available USB ports for an SD card reader (virtual machines work well).
  • USB SD card reader
  • SD card (at least 4G recommended)

Software Assumptions

  • Linux system binaries (x-loader, u-boot, uImage).
  • Linux embedded system root filesystem

How to Make 3 Partition SD Card

The following procedures pertain to the following Sitara processors:
  • AM35x
  • AM37x

Prepare Script

Create a file on the Linux host named mk3PartSDCard.  Copy the contents of the script (see below) to this file and save it.  See the later section for the contents of the two partition script.
This script requires a single input parameter which must be the device which is connected to the USB card reader.  DO NOT RUN THE SCRIPT IF YOU ARE NOT COMPLETELY SURE THAT YOU HAVE ENTERED THE CORRECT DEVICE TO USE.  If you specify the device connect to the host machine hard drive instead of the SD card reader, you will destroy the hard drive of your host machine.  The next section shows how to figure out which device is connected the SD card and which is connected to the hard drive.
#! /bin/sh
# mk3PartSDCard.sh v0.3
# Licensed under terms of GPLv2

DRIVE=$1

dd if=/dev/zero of=$DRIVE bs=1024 count=1024

SIZE=`fdisk -l $DRIVE | grep Disk | awk '{print $5}'`

echo DISK SIZE - $SIZE bytes

CYLINDERS=`echo $SIZE/255/63/512 | bc`

sfdisk -D -H 255 -S 63 -C $CYLINDERS $DRIVE << EOF
,9,0x0C,*
10,115,,-
126,,,-
EOF

mkfs.vfat -F 32 -n "boot" ${DRIVE}1
umount ${DRIVE}1
mkfs.ext3 -L "rootfs" ${DRIVE}2
umount ${DRIVE}2
mkfs.ext3 -L "START_HERE" ${DRIVE}3


Make the script executable with the following command
user@UbuntuVbox1004:~$ chmod 755 mk3PartSDCard

Examine Linux System

The script requires a single input parameter.  This parameter must be the device that is used to connect to the USB SD card reader.  The df -hT command can be used to examine the Linux system.  See the example below.

user@UbuntuVbox1004:~$ df -hT
Filesystem  Type  Size  Used  Avail  Use%  Mounted on
/dev/sda1  ext4  19G  16G  2.5G  87%  /
none       devtmpfs  245M  308K  245M  1%  /dev
none          tmpfs  249M  192K  249M  1%  /dev/shm
none          tmpfs  249M  340K  249M  1%  /var/run
none          tmpfs  249M  0  249M  0%  /var/lock
none          tmpfs  249M  0  249M  0%  /lib/init/rw
/dev/sdb1  vfat  1.9G  4.0K  1.9G  1%  /media/00F8-E7F0
user@UbuntuVbox1004:~$


The example above shows the result of the df command on a Linux system which has an SD card reader attach to device /dev/sdb. And the card that is in this reader is a 2G card with a standard Windows FAT formatted single partition. This is typical of an SD card purchased through a retail channel. More importantly to note here is that the Linux host partition (mounted on /) is on /dev/sda1. This indicates that the host machine has a SATA drive on /dev/sda. If the host machine had an older IDE drive, the device would be /dev/hda.

So now we know that the parameter which must be passed to the script is /dev/sdb. And we know that passing /dev/sda to the script would be disasterous. Passing the device associated with the hard drive of the host machine to this script will destroy the host machine hard drive.

Finally, the above details are just an example.  Other systems may have hardware located on different devices in the system.  Some systems may have more than one card reader.  So there may be a card reader on /dev/sdb and another on /dev/sdc.  It is also possible to have a system where the host is mounted on an IDE hard drive at /dev/hda and have a USB SD card reader on /dev/sda.  In this case it would be proper to pass /dev/sda to the script.  In any case, it is up to the user to determine which one to pass to the script.
 Passing the device associated with the hard drive of the host machine to this script will destroy the host machine hard drive.

Run Script

After verifying the correct device that you must send to the script, it is necessary to unmount any directory that is mounted to the device.  In the example above, the directory /media/disk is mounted to /dev/sdb1.  To unmount run the following command
user@Ubuntu1004:~$ umount /dev/sdb1
The script must be executed with supre-user permissions.  In Ubuntu, this is done by pre-pending the command with "sudo".  When prompted for a password by sudo, use the password of the user account.
user@Ubuntu1004:~$ sudo ./mk3PartSDCard /dev/sdb
On a successful execution the terminal will look something like the following.  The error that may come from sfdisk (as shown below) can be safely ignired.
user@UbuntuVbox1004:~$ sudo ./mk3PartSD /dev/sdb
[sudo] password for user:
1024+0 records in
1024+0 records out
1048576 bytes (1.0 MB) copied, 1.53109 s, 685 kB/s
Disk /dev/sdb doesn't contain a valid partition table
DISK SIZE - 1977614336 bytes
Checking that no-one is using this disk right now ...
OK

Disk /dev/sdb: 240 cylinders, 255 heads, 63 sectors/track
sfdisk: ERROR: sector 0 does not have an msdos signature
/dev/sdb: unrecognized partition table type
Old situation:
No partitions found
New situation:
Units = cylinders of 8225280 bytes, blocks of 1024 bytes, counting from 0

Device  Boot  Start  End  #cyls  #blocks   Id System
/dev/sdb1 *  0+  8  9-  72261   c W95 FAT32 (LBA)
/dev/sdb2  10  124  115  923737+   83 Linux
/dev/sdb3  126  239  114  915705   83 Linux
/dev/sdb4  0  - 0  0  0   Empty
Successfully wrote the new partition table
Re-reading the partition table ...

If you created or changed a DOS partition, /dev/foo7, say, then use dd(1)
to zero the first 512 bytes: dd if=/dev/zero of=/dev/foo7 bs=512 count=1
(See fdisk(8).)
mkfs.vfat 3.0.7 (24 Dec 2009)
umount: /dev/sdb1: not mounted
mke2fs 1.41.11 (14-Mar-2010)
Filesystem label=rootfs
OS type: Linux
Block size=4096 (log=2)
Fragment size=4096 (log=2)
Stride=0 blocks, Stripe width=0 blocks
57856 inodes, 230934 blocks
11546 blocks (5.00%) reserved for the super user
First data block=0
Maximum filesystem blocks=239075328
8 block groups
32768 blocks per group, 32768 fragments per group
7232 inodes per group
Superblock backups stored on blocks:
32768, 98304, 163840, 229376
Writing inode tables: done
Creating journal (4096 blocks): done
Writing superblocks and filesystem accounting information: done

This filesystem will be automatically checked every 20 mounts or
180 days, whichever comes first. Use tune2fs -c or -i to override.
umount: /dev/sdb2: not mounted
mke2fs 1.41.11 (14-Mar-2010)
Filesystem label=START_HERE
OS type: Linux
Block size=4096 (log=2)
Fragment size=4096 (log=2)
Stride=0 blocks, Stripe width=0 blocks
57232 inodes, 228926 blocks
11446 blocks (5.00%) reserved for the super user
First data block=0
Maximum filesystem blocks=234881024
7 block groups
32768 blocks per group, 32768 fragments per group
8176 inodes per group
Superblock backups stored on blocks:
32768, 98304, 163840
Writing inode tables: done
Creating journal (4096 blocks): done
Writing superblocks and filesystem accounting information: done

This filesystem will be automatically checked every 27 mounts or
180 days, whichever comes first. Use tune2fs -c or -i to override.

The script will run until you see the default prompt on the Terminal again.   At this point it may be necessary to unmount anything on /dev/sdb
user@Ubuntu1004:~$ umount /dev/sdb1
user@Ubuntu1004:~$ umount /dev/sdb2
user@Ubuntu1004:~$ umount /dev/sdb3
Now physically remove the SD card from the USB SD card reader and re-insert it. Ubuntu will automatically mount the new partitions. Running df -hT should result in the following:

user@Ubuntu1004:~$ df -hT
Filesystem  Type  Size  Used  Avail  Use%  Mounted on
/dev/sda1  ext4  19G  16G  2.5G  87%  /
none       devtmpfs  245M  360K  245M  1%  /dev
none   tmpfs  249M  252K  249M  1%  /dev/shm
none   tmpfs  249M  340K  249M  1%  /var/run
none   tmpfs  249M  0  249M  0%  /var/lock
none   tmpfs  249M  0  249M  0%  /lib/init/rw
/dev/sdb1  vfat  70M  512  70M  1%  /media/boot
/dev/sdb2  ext3  888M  18M  826M  3%  /media/rootfs
/dev/sdb3  ext3  881M  17M  819M  3%  /media/START_HERE
user@Ubuntu1004:~$

The boot partion is a bootable partition which (minimally) must contain x-loader (MLO), u-boot.bin and uImage.
The rootfs partition is the root file system for the system.

How to Make 2 Partition SD Card

The following procedures pertain to the following Sitara processors:
  • AM180x
  • AM1810

An SD card with two partitions can be made using a slightly modified script instead of the one above.  Save the script below to a file called mk2PartSDCard.  Use it with all of the same procedures and caution in the section above.  Again this script is very dangerous if you don't know what you are doing. 
After using this script the SD card will have a VFAT partition called "boot" with a size of 70M.  A second Linux ext3 partition will be called "rootfs" and will take up the remainder of the card.  This script works very well with 2G cards.
#! /bin/sh
# mk2PartSDCard.sh v0.1
# Licensed under terms of GPLv2

DRIVE=$1

dd if=/dev/zero of=$DRIVE bs=1024 count=1024

SIZE=`fdisk -l $DRIVE | grep Disk | awk '{print $5}'`

echo DISK SIZE - $SIZE bytes

CYLINDERS=`echo $SIZE/255/63/512 | bc`

sfdisk -D -H 255 -S 63 -C $CYLINDERS $DRIVE << EOF
,9,0x0C,*
10,114,,,
EOF

mkfs.vfat -F 32 -n "boot" ${DRIVE}1
umount ${DRIVE}1
mkfs.ext3 -L "rootfs" ${DRIVE}2

Copy Bootloaders, Linux Kernel and File System to SD card

After the card has been processed using the procedure above, the system files can be added to the card.  Pre-built system files are included with the Sitara SDK.  For AM35x/37x EVM's, bootloader and kernel image files can be placed in the boot partition of the SD card.  For AM18x/181x EVM's only the kernel image should on the SD card because these boards must get a bootloader (u-boot) already stored in SPI flash.  The bootloader and kernel files are typically in a sub-directory of the SDK installation folder called psp/prebuilt-images.  The root filesystem is in a tarball in a sub-directory of the SDK installation folder called filesystem.  The example below shows these locations for the AM37x SDK.
user@UbuntuVbox1004:~/ti-sdk-am37x-evm-4.0.1.0/filesystem$ ls -l
total 10504
-rw-r--r-- 1 user user 10751480 2011-01-26 09:13 base-rootfs-am37x-evm.tar.gz
drwxr-xr-x 18 user user 4096 2011-02-07 14:40 SDK_NFS
user@UbuntuVbox1004:~/ti-sdk-am37x-evm-4.0.1.0/filesystem$ cd ..
user@UbuntuVbox1004:~/ti-sdk-am37x-evm-4.0.1.0$ cd psp/prebuilt-images/
user@UbuntuVbox1004:~/ti-sdk-am37x-evm-4.0.1.0/psp/prebuilt-images$ ls -l
total 51168
lrwxrwxrwx 1 user user 40 2011-02-11 16:09 MLO -> MLO-am37x-evm-1.46-psp03.00.01.06.sdk-r0
-rwxr-xr-x 1 user user 20060 2011-01-22 09:30 MLO-am37x-evm-1.46-psp03.00.01.06.sdk-r0
-rwxr-xr-x 1 user user 216572 2011-01-22 09:30 u-boot-am37x-evm-2009.11-psp03.00.01.06.sdk-r0.bin
lrwxrwxrwx 1 user user 50 2011-02-11 16:09 u-boot.bin -> u-boot-am37x-evm-2009.11-psp03.00.01.06.sdk-r0.bin
lrwxrwxrwx 1 user user 13 2011-02-11 16:09 uImage -> uImage-2.6.32
-rw-r--r-- 1 user user 2409528 2011-01-25 17:12 uImage-2.6.32
-rw-r--r-- 1 user user 49743774 2011-01-25 17:12 vmlinux-2.6.32
user@UbuntuVbox1004:~/ti-sdk-am37x-evm-4.0.1.0/psp/prebuilt-images$

For the AM18x/AM181x EVM, the board always boots u-boot out of SPI flash.  Instructions for putting u-boot into SPI flash are here

For the AM35x/37x EVM, the bootloader files are MLO and u-boot.bin.  These two files must be copied to the boot partition of the SD card.

For all EVM's the kernel image is the file uImage.  This file should be copied to the boot partition of the SD card.

The root filesystem must placed on the rootfs partition of the SD card.  Tarball's containing a root filesystem are available in the SDK under the filesystem directory.  There may actually two different tarball's.  The example below shows how AM181x SDK contains two root filesystem tarball's.  The one labeled with the prefix tisdk- is a tarball of the filesytem that comes with the retail EVM and provides the full out-of-the-boc experience with the Matrix GUI and all of the example apps.  The other tarball is a "base" filesytem with no extra apps that can be used as a baseline Linux filesytem.

user@UbuntuVbox1004:~/ti-sdk-am181x-evm-4.0.1.0/filesystem$ ls -l
total 75264
-rw-r--r-- 1 user user 11055454 2011-01-26 10:44 base-rootfs-am181x-evm.tar.gz
-rw-r--r-- 1 user user 66010750 2011-01-26 10:44 tisdk-rootfs-am181x-evm.tar.gz
user@UbuntuVbox1004:~/ti-sdk-am181x-evm-4.0.1.0/filesystem$
It is important to directly un-tar one of these filesystems to the rootfs partition of the SD card.  Doing a simple "copy" or "drag-and-drop" of an existing filesystem will not work.  This is because the tar command used to create the tarball in the SDK has preserved permissions, soft-links, and device/file nodes that are important in the filesystem.  Also, these tarballs will not create a new sub-directory.  They are designed to be extracted in place at the SD card rootfs partition. NOTE: You must use the sudo command when untarring the file system to allow creating the device node file correctly.
The best way to do the un-tar is to first change to the /media/rootfs directory (or wherever the card is mounted in the host system).  Second, run the tar command that will extract the tarball into the current directory.  And lastly, but maybe most important, is to run the sync command a couple of times after the root filesystem has been un-tarred to the SD card.  This will flush data and ensure that everything has been written to the card.  See the example below.

user@UbuntuVbox1004:~/ti-sdk-am181x-evm-4.0.1.0/filesystem$ cd /media/rootfs
user@UbuntuVbox1004:/media/rootfs$ sudo tar -xzvf ~/ti-sdk-am181x-evm-4.0.1.0/filesystem/tisdk-rootfs-am181x-evm.tar.gz
....
.... a lot scrolls by if you use the -v switch with tar
....
user@UbuntuVbox1004:/media/rootfs$ sync
user@UbuntuVbox1004:/media/rootfs$ sync

It is of course possible to put all of this in a script and this can be helpful if you are making more than one SD card.  The example below shows a script made for copying system files for the AM37x SDK. 

Before running the script the paths inside the script must edited. 
Contents of script.

#! /bin/sh
# cpSDCard v0.1

export PATH_TO_SDK=<path to SDK containing bootloaders, kernel, and filesystem>
export PATH_TO_SDBOOT=<path to boot partition of SD card>
export PATH_TO_SDROOTFS=<path to rootfs partition of SD card>

cp $PATH_TO_SDK/psp/prebuilt-images/MLO $PATH_TO_SDBOOT
cp $PATH_TO_SDK/psp/prebuilt-images/u-boot.bin $PATH_TO_SDBOOT
cp $PATH_TO_SDK/psp/prebuilt-images/uImage $PATH_TO_SDBOOT

cd $PATH_TO_SDROOTFS
tar -xzvf $PATH_TO_SDK/filesystem/tisdk-rootfs-am37x-evm.tar.gz
sync
sync


Be sure to change the export PATH_TO_SDK... to match the location of the PSP installed within your SDK.  Here is an example showing paths to the AM37 SDK and to the mounted SD card.

export PATH_TO_SDK=/home/user/ti-sdk-AM37x-evm-4.0.1.0
export PATH_TO_SDBOOT=/media/boot
export PATH_TO_SDROOTFS=/media/rootfs
To run the cpSDCard script use ./cpSDCard in the Terminal.
Let the script run until you see the default prompt in the Terminal again. This will take some time to complete because the "tar" and "sync" commands can take several minutes.

Wednesday, 20 July 2016

An Introduction to Device Drivers in the Linux Kernel

In the article ‘An Introduction to the Linux Kernel’ in the August 2014 issue of OSFY, we wrote and compiled a kernel module. In the second article in this series, we move on to device drivers.
Have you ever wondered how a computer plays audio or shows video? The answer is: by using device drivers. A few years ago we would always install audio or video drivers after installing MS Windows XP. Only then we were able to listen the audio. Let us explore device drivers in this column.
A device driver (often referred to as ‘driver’) is a piece of software that controls a particular type of device which is connected to the computer system. It provides a software interface to the hardware device, and enables access to the operating system and other applications. There are various types of drivers present in GNU/Linux such as Character, Block, Network and USB drivers. In this column, we will explore only character drivers.
Character drivers are the most common drivers. They provide unbuffered, direct access to hardware devices. One can think of character drivers as a long sequence of bytes — same as regular files but can be accessed only in sequential order. Character drivers support at least the open(), close(), read() and write() operations. The text console, i.e., /dev/console, serial consoles /dev/stty*, and audio/video drivers fall under this category.
To make a device usable there must be a driver present for it. So let us understand how an application accesses data from a device with the help of a driver. We will discuss the following four major entities.
  • User-space application: This can be any simple utility like echo, or any complex application.
  • Device file: This is a special file that provides an interface for the driver. It is present in the file system as an ordinary file. The application can perform all supported operation on it, just like for an ordinary file. It canmove, copy, delete, rename, read and write these device files.
  • Device driver: This is the software interface for the device and resides in the kernel space.
  • Device: This can be the actual device present at the hardware level, or a pseudo device.
Let us take an example where a user-space application sends data to a character device. Instead of using an actual device we are going to use a pseudo device. As the name suggests, this device is not a physical device. In GNU/Linux/dev/null is the most commonly used pseudo device. This device accepts any kind of data (i.e., input) and simply discards it. And it doesn’t produce any output.
Let us send some data to the /dev/null pseudo device:
[mickey]$ echo -n 'a' /dev/null
In the above example, echo is a user-space application and null is a special file present in the /dev directory. There is a null driver present in the kernel to control the pseudo device.
To send or receive data to and from the device or application, use the corresponding device file that is connected to the driver through the Virtual File System (VFS) layer. Whenever an application wants to perform any operation on the actual device, it performs this on the device file. The VFS layer redirects those operations to the appropriate functions that are implemented inside the driver. This means that whenever an application performs the open()operation on a device file, in reality the open() function from the driver is invoked, and the same concept applies to the other functions. The implementation of these operations is device-specific.
Major and minor numbers
We have seen that the echo command directly sends data to the device file. Hence, it is clear that to send or receive data to and from the device, the application uses special device files. But how does communication between the device file and the driver take place? It happens via a pair of numbers referred to as ‘major’ and ‘minor’ numbers.
The command below lists the major and minor numbers associated with a character device file:
[bash]$ ls -l /dev/null
crw-rw-rw- 1 root root 1, 3 Jul 11 20:47 /dev/null
In the above output there are two numbers separated by a comma (1 and 3). Here, ‘1’ is the major and ‘3’ is the minor number. The major number identifies the driver associated with the device, i.e., which driver is to be used. The minor number is used by the kernel to determine exactly which device is being referred to. For instance, a hard disk may have three partitions. Each partition will have a separate minor number but only one major number, because the same storage driver is used for all the partitions.
Older kernels used to have a separate major number for each driver. But modern Linux kernels allow multiple drivers to share the same major number. For instance, /dev/full, /dev/null, /dev/random and /dev/zero use the same major number but different minor numbers. The output below illustrates this:
[bash]$ ls -l /dev/full /dev/null /dev/random /dev/zero
crw-rw-rw- 1 root root 1, 7 Jul 11 20:47 /dev/full
crw-rw-rw- 1 root root 1, 3 Jul 11 20:47 /dev/null
crw-rw-rw- 1 root root 1, 8 Jul 11 20:47 /dev/random
crw-rw-rw- 1 root root 1, 5 Jul 11 20:47 /dev/zero
The kernel uses the dev_t type to store major and minor numbers. dev_t type is defined in the <linux/types.h> header file. Given below is the representation of dev_t type from the header file:
#ifndef _LINUX_TYPES_H
#define _LINUX_TYPES_H
#define __EXPORTED_HEADERS__
#include <uapi/linux/types.h>
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
dev_t is an unsigned 32-bit integer, where 12 bits are used to store the major number and the remaining 20 bits are used to store the minor number. But don’t try to extract the major and minor numbers directly. Instead, the kernel provides MAJOR and MINOR macros that can be used to extract the major and minor numbers. The definition of the MAJOR and MINOR macros from the <linux/kdev_t.h> header file is given below:
#ifndef _LINUX_KDEV_T_H
#define _LINUX_KDEV_T_H
#include <uapi/linux/kdev_t.h>
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
If you have major and minor numbers and you want to convert them to the dev_t type, the MKDEV macro will do the needful. The definition of the MKDEV macro from the <linux/kdev_t.h> header file is given below:
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
We now know what major and minor numbers are and the role they play. Let us see how we can allocate major numbers. Here is the prototype of the register_chrdev():
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
This function registers a major number for character devices. Arguments of this function are self-explanatory. Themajor argument implies the major number of interest, name is the name of the driver and appears in the /proc/devicesarea and, finally, fops is the pointer to the file_operations structure.
Certain major numbers are reserved for special drivers; hence, one should exclude those and use dynamically allocated major numbers. To allocate a major number dynamically, provide the value zero to the first argument, i.e.,major == 0. This function will dynamically allocate and return a major number.
To deallocate an allocated major number use the unregister_chrdev() function. The prototype is given below and the parameters of the function are self-explanatory:
void unregister_chrdev(unsigned int major, const char *name)
The values of the major and name parameters must be the same as those passed to the register_chrdev() function; otherwise, the call will fail.
File operations
So we know how to allocate/deallocate the major number, but we haven’t yet connected any of our driver’s operations to the major number. To set up a connection, we are going to use the file_operations structure. This structure is defined in the <linux/fs.h> header file.
Each field in the structure must point to the function in the driver that implements a specific operation, or be left NULL for unsupported operations. The example given below illustrates that.
Without discussing lengthy theory, let us write our first ‘null’ driver, which mimics the functionality of a /dev/nullpseudo device. Given below is the complete working code for the ‘null’ driver.
Open a file using your favourite text editor and save the code given below as null_driver.c:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
static int major;
static char *name = "null_driver";
static int null_open(struct inode *i, struct file *f)
{
printk(KERN_INFO "Calling: %s\n", __func__);
return 0;
}
static int null_release(struct inode *i, struct file *f)
{
printk(KERN_INFO "Calling: %s\n", __func__);
return 0;
}
static ssize_t null_read(struct file *f, char __user *buf, size_t len, loff_t *off)
{
printk(KERN_INFO "Calling: %s\n", __func__);
return 0;
}
static ssize_t null_write(struct file *f, const char __user *buf, size_t len, loff_t *off)
{
printk(KERN_INFO "Calling: %s\n", __func__);
return len;
}
static struct file_operations null_ops =
{
.owner = THIS_MODULE,
.open = null_open,
.release = null_release,
.read = null_read,
.write = null_write
};
static int __init null_init(void)
{
major = register_chrdev(0, name, &null_ops);
if (major < 0) {
printk(KERN_INFO "Failed to register driver.");
return -1;
}
printk(KERN_INFO "Device registered successfully.\n");
return 0;
}
static void __exit null_exit(void)
{
unregister_chrdev(major, name);
printk(KERN_INFO "Device unregistered successfully.\n");
}
module_init(null_init);
module_exit(null_exit);
MODULE_AUTHOR("Narendra Kangralkar.");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Null driver");
Our driver code is ready. Let us compile and insert the module. In the article last month, we did learn how to writeMakefile for kernel modules.
[mickey]$ make
[root]# insmod ./null_driver.ko
We are now going to create a device file for our driver. But for this we need a major number, and we know that our driver’s register_chrdev() function will allocate the major number dynamically. Let us find out this dynamically allocated major number from /proc/devices, which shows the currently loaded kernel modules:
[root]# grep "null_driver" /proc/devices
248 null_driver
From the above output, we are going to use ‘248’ as a major number for our driver. We are only interested in the major number, and the minor number can be anything within a valid range. I’ll use ‘0’ as the minor number. To create the character device file, use the mknod utility. Please note that to create the device file you must have superuser privileges:
[root]# mknod /dev/null_driver c 248 0
Now it’s time for the action. Let us send some data to the pseudo device using the echo command and check the output of the dmesg command:
[root]# echo "Hello" > /dev/null_driver
[root]# dmesg
Device registered successfully.
Calling: null_open
Calling: null_write
Calling: null_release
Yes! We got the expected output. When open, write, close operations are performed on a device file, the appropriate functions from our driver’s code get called. Let us perform the read operation and check the output of the dmesgcommand:
[root]# cat /dev/null_driver
[root]# dmesg
Calling: null_open
Calling: null_read
Calling: null_release
To make things simple I have used printk() statements in every function. If we remove these statements, then/dev/null_driver will behave exactly the same as the /dev/null pseudo device. Our code is working as expected. Let us understand the details of our character driver.
First, take a look at the driver’s function. Given below are the prototypes of a few functions from the file_operationsstructure:
int (*open)(struct inode *i, struct file *f);
int (*release)(struct inode *i, struct file *f);
ssize_t (*read)(struct file *f, char __user *buf, size_t len, loff_t *off);
ssize_t (*write)(struct file *f, const char __user buf*, size_t len, loff_t *off);
The prototype of the open() and release() functions is exactly same. These functions accept two parameters—the first is the pointer to the inode structure. All file-related information such as size, owner, access permissions of the file, file creation timestamps, number of hard-links, etc, is represented by the inode structure. And each open file is represented internally by the file structure. The open() function is responsible for opening the device and allocation of required resources. The release() function does exactly the reverse job, which closes the device and deallocates the resources.
As the name suggests, the read() function reads data from the device and sends it to the application. The first parameter of this function is the pointer to the file structure. The second parameter is the user-space buffer. The third parameter is the size, which implies the number of bytes to be transferred to the user space buffer. And, finally, the fourth parameter is the file offset which updates the current file position. Whenever the read() operation is performed on a device file, the driver should copy len bytes of data from the device to the user-space buffer buf and update the file offset off accordingly. This function returns the number of bytes read successfully. Our null driver doesn’t read anything; that is why the return value is always zero, i.e., EOF.
The driver’s write() function accepts the data from the user-space application. The first parameter of this function is the pointer to the file structure. The second parameter is the user-space buffer, which holds the data received from the application. The third parameter is len which is the size of the data. The fourth parameter is the file offset. Whenever the write() operation is performed on a device file, the driver should transfer len bytes of data to the device and update the file offset off accordingly. Our null driver accepts input of any length; hence, return value is always len, i.e., all bytes are written successfully.
In the next step we have initialised the file_operations structure with the appropriate driver’s function. In initialisationfunction we have done a registration related job, and we are deregistering the character device in cleanup function.
Implementation of the full pseudo driver
Let us implement one more pseudo device, namely, full. Any write operation on this device fails and gives the ‘ENOSPC’ error. This can be used to test how a program handles disk-full errors. Given below is the complete working code of the full driver:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
static int major;
static char *name = "full_driver";
static int full_open(struct inode *i, struct file *f)
{
return 0;
}
static int full_release(struct inode *i, struct file *f)
{
return 0;
}
static ssize_t full_read(struct file *f, char __user *buf, size_t len, loff_t *off)
{
return 0;
}
static ssize_t full_write(struct file *f, const char __user *buf, size_t len, loff_t *off)
{
return -ENOSPC;
}
static struct file_operations full_ops =
{
.owner = THIS_MODULE,
.open = full_open,
.release = full_release,
.read = full_read,
.write = full_write
};
static int __init full_init(void)
{
major = register_chrdev(0, name, &full_ops);
if (major < 0) {
printk(KERN_INFO "Failed to register driver.");
return -1;
}
return 0;
}
static void __exit full_exit(void)
{
unregister_chrdev(major, name);
}
module_init(full_init);
module_exit(full_exit);
MODULE_AUTHOR("Narendra Kangralkar.");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Full driver");
Let us compile and insert the module.
[mickey]$ make
[root]# insmod ./full_driver.ko
[root]# grep "full_driver" /proc/devices
248 full_driver
[root]# mknod /dev/full_driver c 248 0
[root]# echo "Hello" > /dev/full_driver
-bashecho: write error: No space left on device
If you want to learn more about GNU/Linux device drivers, the Linux kernel’s source code is the best place to do so. You can browse the kernel’s source code from http://lxr.free-electrons.com/. You can also download the latest source code from https://www.kernel.org/. Additionally, there are a few good books available in the market like ‘Linux Kernel Development’ (3rd Edition) by Robert Love, and ‘Linux Device Drivers’ (3rd Edition) which is a free book. You can download it from http://lwn.net/Kernel/LDD3/. These books also explain kernel debugging tools and techniques.