Device Driver¶
In this tutorial, we will develop a device driver for the Linux kernel. This device will abstract the control of the LED and the access to the SW key on the development kit.
Kernel¶
Linux is a monolithic kernel, where all code related to the operating system occurs in kernel space.
The Linux kernel is responsible, among other things, for:
- Managing processes and tasks
- Scheduling
- Virtualization, controlling groups/users
- Memory management/pagination
- Inter-process communication
- Input/output
- File management
- Device drivers
- Hardware abstraction
Map of the internal architecture of the Linux kernel
Microkernel
The microkernel is an alternative to the monolithic kernel. In this architecture, the kernel implements the bare minimum.
GNU
The Linux kernel uses various software created and made available by the GNU community (such as gcc, gdb, make, and many others, so it is often known as GNU/Linux.
Userspace vs Kernel Space¶
Most of this section was translated from https://www.ctrlinux.com/blog/?p=40
Userspace is a region of virtual memory where all user programs are executed. The programs that are part of userspace run in a CPU operating mode called unprivileged, where not all instructions and registers are available.
ARM supports a total of 7 execution modes, with only user mode being unprivileged:
- User mode – All user level applications run in this mode.
- FIQ Fast Interrupt Mode – Used to handle interrupts classified as “fast”, get in and get out type of interrupts.
- IRQ Mode – Used to handle device interrupts and non FIQ interrupts.
- Supervisor mode (default CPU boot mode). Used by the OS and software interrupts (SWIs).
- Abort – Used to handle violations caused by erroneous memory accesses
- Undefined – Used to handle undefined instructions
- System – A privileged mode with the same register set as user mode, used to run exception handlers.
Material taken from: https://www.ctrlinux.com/blog/?p=40
Userspace¶
To better understand userspace, let's imagine what happens when a program tries to access a region of memory that belongs to the kernel (privileged mode).
When we create a pointer and access memory, this command is translated into a LOAD type instruction in assembly. The LOAD instruction enters the CPU pipeline and at the execution stage, the CPU passes the instruction address to the hardware responsible for managing memory: Memory Management Unit (MMU). The MMU translates the virtual memory address to the physical address (memory page), at this point the MMU checks the current execution mode of the CPU and if the memory region can be accessed by the current mode. In our case, the CPU will change the processor mode to Abort and handle the unauthorized memory address access, causing the program to terminate with a seg fault
.
Programs in userspace communicate with the kernel via system calls (http://www.linux.it/~rubini/docs/ksys/), these calls turn into software interruptions that will be processed by the kernel.
Kernel Space¶
The advantages of working with kernel space are: being able to directly manipulate hardware peripherals, being able to handle hardware interruptions. With great powers comes great responsibilities, errors in this mode can cause kernel panic and freeze the entire system.
Kernel Module / Device driver¶
A kernel module is a compiled code that can be linked with the kernel at runtime (like changing the wing of a moving airplane), a module can be a device driver, but not necessarily.
Device driver¶
Device drivers are responsible for implementing the low-level part of hardware configuration, communication, and management (peripherals, memory, CPU). In the Linux kernel repository, drivers are located in: linux/drivers/
and are organized by category:
accessibility dca ide mfd pnp spmi
acpi devfreq idle misc power ssb
amba dio iio mmc powercap staging
android dma infiniband modules.builtin pps target
ata dma-buf input mtd ps3 tc
atm edac iommu net ptp thermal
auxdisplay eisa ipack nfc pwm thunderbolt
base extcon irqchip ntb rapidio tty
bcma firewire isdn nubus ras uio
block firmware Kconfig nvdimm regulator usb
bluetooth fmc leds nvme remoteproc uwb
built-in.a fpga lguest nvmem reset vfio
built-in.o gpio lightnvm of rpmsg vhost
bus gpu macintosh oprofile rtc video
cdrom hid mailbox parisc s390 virt
char hsi Makefile parport sbus virtio
clk hv mcb pci scsi vlynq
clocksource hwmon md pcmcia sfi vme
connector hwspinlock media perf sh w1
cpufreq hwtracing memory phy sn watchdog
cpuidle i2c memstick pinctrl soc xen
crypto i3c message platform spi zorro
In Linux, drivers can be developed/classified into basically three types: char module
, block module
, network module
.
-
character device: It is used in the case that the driver can be accessed with a stream of bytes (like a file). It must implement at least the following system calls:
open/close/read/write
. The/dev/console
and/dev/tty
are examples of this type of device. A difference between a file and achar dev
is that in the file you can move the pointer forward and backward, but in this type of device you can't, you can only move forward. -
block device: Used to implement physical disk access, this type of driver operates with blocks of bytes (usually 512 bytes).
-
network device: Used to implement a network interface (like
loopback/ wlan0/ ...
). This interface is capable of receiving and sending data packets, controlled by the kernel's network system.
Device Tree (dts)¶
How does the Linux kernel know what devices and drivers are associated with them? A configuration file called the device tree is passed by the boot to the Linux kernel indicating the peripherals, and what drivers are associated with them.
U-boot¶
The programming of the FPGA is carried out by u-boot, before the initialization of the Linux Kernel. In our case, the u-boot has been pre-configured to read the file soc_system.rbf
. FPGA hardawre programming is performed by U-boot, before Linux Kernel initialization. In our case, U-boot was preconfigured to read the soc_system.rbf
file located in the SDCARD partition along with the kernel (zImage
).
Video
Before initializing the Linux kernel, U-boot fetches this file from the SDCARD partition, extracts it, and "magically" programs the FPGA. In this same partition, we have two more files: u-boot.scr
and socfpga.dtb
. The first one is a boot initialization script that U-boot reads to know which steps it should execute (whether it needs to load the fpga, where the kernel is, etc., these are the initialization steps). The socfpga.dtb
is the Linux device tree, the dtb is a binary, which was created from another file, the .dts
, and it contains information about the hardware that is passed to the kernel at startup.
Video
dtb
(compiled dts)¶
The dtb
is used as a tool to indicate to the kernel what the available hardware configurations are. You do not need to recompile the kernel if the memory address of some peripheral changes, just inform it in the dts
. This tool is very important for embedded systems, in which, each hardware has its specificity.
The dtb
is generated from a text file in dts
format, which is then generated by the hardware information extracted from the Platform Designer and saved in the .sopcinfo
file. This is the same file used by Eclipse-NIOS to generate the BSP in the previous tutorials. The Linux BSP is called dts
and has a standard format that must be followed!
Understanding the DTS¶
The start of our .dts
has the definition of the CPUs that are available on the CHIP:
cpus {
#address-cells = <1>;
#size-cells = <0>;
enable-method = "altr,socfpga-smp"; /* appended from boardinfo */
hps_0_arm_a9_0: cpu@0x0 {
device_type = "cpu";
compatible = "arm,cortex-a9-16.1", "arm,cortex-a9";
reg = <0x00000000>;
next-level-cache = <&hps_0_L2>; /* appended from boardinfo */
}; //end cpu@0x0 (hps_0_arm_a9_0)
hps_0_arm_a9_1: cpu@0x1 {
device_type = "cpu";
compatible = "arm,cortex-a9-16.1", "arm,cortex-a9";
reg = <0x00000001>;
next-level-cache = <&hps_0_L2>; /* appended from boardinfo */
}; //end cpu@0x1 (hps_0_arm_a9_1)
}; //end cpus
Let's look at hps_0_uart0
from our dts
in more detail:
hps_0_uart0: serial@0xffc02000 {
compatible = "snps,dw-apb-uart-16.1", "snps,dw-apb-uart";
reg = <0xffc02000 0x00000100>;
interrupt-parent = <&hps_0_arm_gic_0>;
interrupts = <0 162 4>;
clocks = <&l4_sp_clk>;
reg-io-width = <4>; /* embeddedsw.dts.params.reg-io-width type NUMBER */
reg-shift = <2>; /* embeddedsw.dts.params.reg-shift type NUMBER */
status = "okay"; /* embeddedsw.dts.params.status type STRING */
}; //end serial@0xffc02000 (hps_0_uart0)
}; //end serial@0x100020000 (jtag_uart)
It indicates that in our hardware, we have a serial component at address 0xffc02000
that is compatible with the drivers: snps,dw-apb-uart-16.1
and/or snps,dw-apb-uart
, which is implemented in the 8250 driver in the Linux kernel: https://github.com/torvalds/linux/blob/master/drivers/tty/serial/8250/8250_dw.c.
And this driver is configured as active in our kernel:
And it is because of this that we can access the kit with USB (screen).
CONFIG_SERIAL_8250_CONSOLE: │
│ │
│ If you say Y here, it will be possible to use a serial port as the │
│ system console (the system console is the device which receives all │
│ kernel messages and warnings and which allows logins in single user │
│ mode). This could be useful if some terminal or printer is connected │
│ to that serial port. │
│ │
│ Even if you say Y here, the currently visible virtual console │
│ (/dev/tty0) will still be used as the system console by default, but │
│ you can alter that using a kernel command line option such as │
│ "console=ttyS1". (Try "man bootparam" or see the documentation of │
│ your boot loader (grub or lilo or loadlin) about how to pass options │
│ to the kernel at boot time.) │
│ │
│ If you don't have a VGA card installed and you say Y here, the │
│ kernel will automatically use the first serial line, /dev/ttyS0, as │
│ system console. │
│ │
│ You can set that using a kernel command line option such as │
│ "console=uart8250,io,0x3f8,9600n8" │
│ "console=uart8250,mmio,0xff5e0000,115200n8". │
│ and it will switch to normal serial console when the corresponding │
│ port is ready. │
│ "earlycon=uart8250,io,0x3f8,9600n8" │
│ "earlycon=uart8250,mmio,0xff5e0000,115200n8". │
│ it will not only setup early console. │
│ │
│ If unsure, say N. │
│ │
│ Symbol: SERIAL_8250_CONSOLE [=y] │
│ Type : boolean │
│ Prompt: Console on 8250/16550 and compatible serial port
│ Location: │
│ -> Device Drivers │
│ -> Character devices │
│ -> Serial drivers │
│ -> 8250/16550 and compatible serial support (SERIAL_8250 [=y]) │
│ Defined at drivers/tty/serial/8250/Kconfig:60 │
│ Depends on: TTY [=y] && HAS_IOMEM [=y] && SERIAL_8250 [=y]=y │
│ Selects: SERIAL_CORE_CONSOLE [=y] && SERIAL_EARLYCON [=y] │
│
For more information about the dts
: