In this article I will talk about Linux
driver development to embedded systems. There is a certain lack of
documentation or tutorials on the subject. I hope with this article
fill some of this gap.
Introduction
User applications cannot directly
communicate with hardware because Linux does not allow. Linux divides
RAM memory into two regions: kernel space and user space. The kernel space is where Linux runs and provide their services and where device drivers reside. User space is the area of memory where the user processes are executed. The kernel space can be accessed by user processes only through the use of system calls.
Thus, preferably the processes in kernel space can communicate with the hardware and access the peripherals. You can also use user space drivers to access the hardware. This mechanism allows developers to make software without worrying
about hardware details. And protects the user from inadvertently
accessing devices and somehow damage them. So far it has working
fine.
What is Device Driver?
It is a program or process that runs on a special memory region and through which the user can access a device or resource of a processor. He serves as a mediator between software and hardware.
For example, when you take a photo on your Android phone, the camera driver interacts with the software and passes information to him regarding the captured images and other information. The end result is the photo. Or when you want to play your favorite game, but it is not possible without the driver for the video card, right? Through the driver, the game accesses features video card that allow amazing experience that games provide. This is idea, serve as mediator between the user and the hardware or between software and hardware.
Drivers as modules or built into the kernel
The drivers in the Linux kernel can be compiled as modules that can be loaded at runtime or embedded into kernel itself. The drivers in modules are known as Loadable Kernel Module and behave similarly to Windows DLLs. An LKM (Loadable Kernel Module) is composed of a single ELF object file, usually named as "serial.o" for the 2.4.x kernel or "serial.ko" to the kernel 2.6.x and later versions. A big advantage is that it can be loaded into memory at runtime by a simple command. And another advantage is that when you change the code LKM, it is not necessary to compile the whole kernel, just compile only the LKM.
The embedded or monolithic drivers are modules that are built as part of the kernel. They form, together with the Linux subsystems and other components, a single image, the end result is the kernel itself. Advantages of using monolithic drivers:
- once accepted into the official Linux kernel, it will be maintained by developers;
- free cost of maintenance, repair security flaws and improvements in code;
- easy access to the source code by users;
Device Tree
Device tree is a data structure for describing hardware. Instead of including hardware code in the operating system, many aspects of the hardware can be described in a data structure that is passed to the kernel at boot time.
The Device Tree was used until recently only in systems with PowerPC and SPARC processors. But it was recently ported to ARM processors in the 3.7 kernel and its use is now mandatory in the development of new drivers. It is a great advantage for embedded systems, because we can easily describe the different types of boards that use the same processor or different processors.
The data structure itself is a simple tree of nodes and properties with names. The nodes contain properties and child nodes. Properties are a pair of name-value. Example description of an SPI controller:
spi@10115000 {
compatible = "arm,pl022";
reg = <0x10115000 0x1000 >;
};
compatible = "arm,pl022";
reg = <0x10115000 0x1000 >;
};
With the Device Tree, it is now possible to boot any board with the same kernel! But this requires that the processor drivers have support to Device Tree.
Toolchain
If you do not know, the same way you need a cross compiler or toolchain to generate executables for a given platform, you need a cross compiler to generate device drivers. Nowadays many Linux distributions provide precompiled binaries of cross compilers and tools for easy installation for a variety of architectures. You can also compile a manually if you prefer.
The compiler used will depend on the architecture of the SoC (System-on-a-chip) chosen. The most commonly used architectures in Embedded Linux today are ARM, MIPS and PowerPC. It is also sometimes used the Intel x86. You need to compile or download a variant of gcc for the specified target. It's what we call cross-compiler or toolchain. Then we would have something to ARM as arm-linux-gcc. For MIPS, mips-linux-gcc. And so on.
Kernel version
“The 2.5 kernel implements a unified device driver model that will make driver development for 2.6 easier.”
The
kernel version is very important when programming Linux device
drivers. The Linux kernel API changes constantly over time. An I2C
driver written for the 2.6.30 version, does not work for version
2.6.36 or greater. And the differences between version 2.4 and 2.6
are even greater.
From book "Linux Device Drivers, 3rd Edition" - Figure 14-1 (A small piece of the device model)
You have to port a driver written for 2.4 if you intend to use it in version 2.6 or greater. And there are always changes in the kernel API over time. Why does kernel API change constantly? Read this document for more details: stable_api_nonsense.txt
One interesting thing about the evolution of the Linux kernel is that new versions always add new attributes and in recent years many specific attributes for embedded systems has been added. A good example is the IIO (Industrial I / O) subsystem. It was assigned to support ADC and DAC converters, accelerometer, light sensor, proximity sensor, magnetrômetro, etc.
Under Linux, there are essentially three types of devices: network devices, block devices and character devices. Most devices fall into the category of character devices. However, nowadays, many device drivers are not implemented directly as character devices. They are developed under one specific framework for a given device. Examples of frameworks: framebuffer (graphics), V4L2 (video capture), IIO (Industrial I / O), etc.
For example, If you want to use a camera for which is not supported on Linux kernel, you have to write a device driver using Video4Linux2 framework.
Interruptions
If you already have some knowledge or experience with embedded systems, you probably know that interruptions is an important part in the development of firmware. However, unfortunately not as simple to use interrupts in Embedded Linux. You can only use interrupts in drivers, or in kernel space or by means of poll()/select() user space. But the use of poll()/select() offers few resources.
The form of interruption that we are accustomed to use in embedded systems is possible only in device drivers. You can not create a routine or function of treatment interruption in software. Another alternative is to use drivers in user space, through the use of UIO.
The form of interruption that we are accustomed to use in embedded systems is possible only in device drivers. You can not create a routine or function of treatment interruption in software. Another alternative is to use drivers in user space, through the use of UIO.
Writing Portable Device Drivers
Follow
the kernel team's rules to make your drivers work on all
architectures.
When dealing with embedded systems there is a good chance you use multiple types of processors along the career. It is important you keep in mind that you should always write portable device drivers. Almost all Linux kernel device drivers work on more than just one type of processor. So you have to know concepts like proper variable types, memory page sizes, endian issues, proper data alignment, etc.
For example, ARM processors have memory pages of 4K, 16K or 32K, but the i386 Intel has only 4K memory pages.
Memory Mapping
Some processors use the method of Port I/O and have special instructions to access ports, like intel x86. Other processors use the method of Memory-Mapped I/O, like ARM. Thus the family of functions in() and out() works only to x86. To ARM use family of functions write() and read().
For either method, or MMIO PMIO in order to access the RAM or the ports I/O on Linux, it is necessary to map physical memory to virtual memory. To access I/O memory in a portable manner, you must call ioremap() to gain access to a memory region and iounmap() to release access. However these functions are being replaced by devm_ioremap_resource() and eventually become obsolete. If the allocation of memory region does not return an error, then one can use the family of functions read()/write() to read and write memory mapped. This is the typical method for accessing registers of a processor that uses MMIO.
Conclusion
In this article I tried to address the most relevant topics in driver development for Embedded Linux and showing differences between programming drivers for Linux Desktop and Embedded Linux. These differences are most noticeable when drivers platform program.
References
Linux Kernel and Driver Development Training - Free Electrons
Essential Linux Device Drivers
Linux Device Drivers – capitulo 14 (The Linux Device Model)
http://www.embarcados.com.br/device-drivers-para-linux-embarcado-intro/
http://www.embarcados.com.br/device-drivers-para-linux-embarcado-intro/