All Aspects of Embedded Linux Driver Development

All Aspects of Embedded Linux Driver Development

01

What to Learn in Embedded Driver Development
Embedded development generally falls into the following four areas:
1. Embedded Hardware Development: Familiarity with circuit knowledge, very familiar with various common components, and mastery of analog and digital circuit design capabilities. Proficient in embedded hardware knowledge, familiar with hardware development models and design patterns, familiar with ARM 32-bit processor embedded hardware platform development, and possess product development experience. Proficient in common hardware design tools: Protel/PADS (PowerPCB)/Cadence/OrCad. Generally requires 4 to 8 layers of high-speed PCB design experience.

2. Embedded Driver Development: Proficient in Linux operating system, system architecture, computer organization principles, and data structure-related knowledge. Familiar with embedded ARM development, and at least master Linux character driver program development. Have the ability to port and develop single-chip microcontrollers and ARM embedded processors, understand hardware schematics, can independently complete related hardware driver debugging, possess solid hardware knowledge, and be able to write software driver programs according to chip manuals.

3. Embedded System Development: Master Linux system configuration, proficient in processor architecture, programming environment, instruction set, addressing modes, debugging, assembly, and mixed programming; master Linux file system creation, familiar with various file system formats (YAFFS2, JFFS2, RAMDISK, etc.); familiar with the embedded Linux boot process, familiar with modifying Linux configuration files; master kernel trimming, kernel porting, cross-compilation, kernel debugging, Bootloader programming, root file system creation, and integrated deployment of Linux systems; familiar with setting up Linux software development environments (cross-compilation of library files and environment configuration, etc.);

4. Embedded Software Development: Proficient in the concepts and installation methods of the Linux operating system, basic commands under Linux, management configuration, and editors, including vi editor, gcc compiler, GDB debugger, and Make project management tools; proficient in advanced programming knowledge of C language, including functions and program structures, pointers, arrays, commonly used algorithms, library function usage, etc., basic content of data structures, including linked lists, queues, etc.; master the basic ideas of object-oriented programming, as well as the basics of C++ language; proficient in program design under embedded Linux, proficient in embedded Linux development environments, including system programming, file I/O, multi-process and multi-threading, network programming, GUI graphical interface programming, and database; familiar with programming common graphical libraries such as QT, GTK, miniGUI, fltk, nano-x, etc.

The daily activities of the company depend on the size of the company; larger companies generally only let you be responsible for one module, so you need to be proficient in a bit. If the company is relatively small, you may need to do a bit of everything. You should also understand some hardware.

After viewing so much, the biggest difference between embedded and pure software is:

Pure software learns a language, such as C, C++, Java, or even Python. In essence, a language is just a tool, similar to learning English, French, or Japanese.

However, embedded learning is about software + hardware. In simple terms, it is about making systems and products, focusing on how to break down a product into specific, actionable software and hardware, as well as smaller units.

Many people ask whether to choose driver development or application development for future employment. It can only be said to follow your interests, and driver and application development are not completely separate.

▍PART 01

What we call drivers is not limited to hardware operations, but also includes concepts of operating system principles, process sleep/wake scheduling, etc. If you want to write a good application and solve the problems encountered by applications, everyone should understand this knowledge.

PART 02

The development path for applications is, in my opinion, business proficiency. For example, in the telecommunications industry, IPTV industry, and mobile phone industry, a good understanding of industry demands is essential.

▍PART 03

Doing drivers cannot be simply referred to as “doing drivers”; it can be termed as “doing lower-level systems.” If done well, this can be applied across various industries. For example, if a person has worked for several years in mobile phones, IPTV, and video conferencing, these products are of no difference to him because he only works at the bottom level. When an application encounters a problem and cannot be solved, he can offer advice from a kernel perspective and provide tools. The development direction of the lower level should be that of a technical expert.

▍PART 04

In fact, whether doing lower-level or application development, there is no clear boundary. Having lower-level experience and then moving to application development will feel very solid. With business experience and a little understanding of the lower level, one can quickly form a team.
02

What does the embedded Linux lower-level system include?

Embedded Linux contains four main components: bootloader, kernel, driver program, and root file system.

1. Bootloader

It is a slightly complex bare-metal program. However, understanding and writing this bare-metal program is not easy. The useful tools under Windows have weakened our programming ability. Many people start with ADS or KEIL when they begin embedded development. Can you answer these questions?

Q: When powered on, where does the CPU fetch instructions to execute?

A: Generally from Flash memory.

Q: However, Flash is generally read-only and cannot be written directly. If global variables are used, where are these global variables stored?

A: Global variables should be in memory.

Q: So who puts the global variables into memory?

A: Friends who have used ADS or KEIL for a long time, can you answer? This requires “relocation”. In ADS or KEIL, the relocation code is written by the company that made these tools for you. Have you ever read it?

Q: Memory is so large, how do I know where to read the “content that originally existed on Flash” into memory?

A: This address is determined by the “link script”. In ADS, there is a scatter file; KEIL also has a similar file. But have you studied it?

Q: You say that relocation is copying the program from Flash to memory. Can this program read Flash?

A: Yes, it needs to be able to operate Flash. Of course, there are other things, such as setting the clock to make the system run faster, etc.

Let’s stop here for self-questioning and self-answering. For the bootloader, there are actually three key points:

① Operation of Hardware

To operate hardware, one needs to look at schematics and chip manuals. This requires some hardware knowledge; it is not required to design hardware, but at least one should be able to read it. One does not need to understand analog circuits, but should at least understand digital circuits. This ability can be learned in school; two books, Microcomputer Principles and Digital Circuits, are sufficient. If you want to learn quickly, just skip this part; if you don’t understand, just GOOGLE or post a question. Additionally, chip manuals must be read; do not look for Chinese versions, just read the English ones. It starts off very painfully, but later you will find that once you are familiar with those grammars and vocabulary, reading any chip manual becomes easy.

② Understanding ARM Architecture Processors

Understanding ARM architecture processors can be referred to in Du Chunlei’s , which discusses assembly instructions, exception modes, MMU, etc. Only these three areas need to be understood.
③ Basic Concepts of Programs: Relocation, Stack, Code Segment, Data Segment, BSS Segment, etc.

The basic concepts of programs can be learned from Compiler Principles. Unfortunately, this type of book is extremely difficult to understand. Unless you are a super genius, it’s better not to read it. You can read Wei Dongshan’s .

For the bootloader, one can first read , and then write programs to experiment with various hardware, such as GPIO, clocks, SDRAM, UART, NAND. Once you have clarified all of these, it will be easy to understand u-boot once assembled together.

To summarize, understand hardware schematics and read chip manuals; this requires you to find your own materials.

2. Kernel

For those who want to learn quickly, skip learning the kernel and directly learn how to write drivers. To become an expert, a deep understanding of the kernel is essential. Note, it is understanding; one should be familiar with scheduling mechanisms, memory management mechanisms, file management mechanisms, etc.

Two books are recommended:

1. Read , please refer to the thinner version.

2. Selectively read ; read the relevant section when you want to understand a specific area.

3. Drivers

Drivers consist of two parts: operation of the hardware itself and the framework of the driver program. Again, regarding hardware, one must be able to read schematics and understand chip manuals; practice more.
① Operation of the Hardware Itself

When it comes to driver frameworks, there are some books that introduce the topic. LDD3, namely , written by foreigners, introduces many concepts and is worth reading. However, its function is limited to introducing concepts. It can be used to familiarize oneself with concepts before getting started.

② Framework of the Driver Program

A comprehensive introduction to drivers can be found in Song Baohua’s . If you want to dive deeper into a specific area, is definitely a super five-star recommendation. Don’t expect to finish reading it; it’s over 1800 pages, in two volumes. When you are unclear about a certain part, just flip through it. Any section can be elaborated for 200-300 pages, very detailed. It also guides you to analyze the kernel source code with a specific goal. It takes Linux 2.4 as an example, but the principles are similar and applicable to other versions of Linux.

Try writing a driver for all the hardware involved in your development board. If there are problems, first “painfully think” about it; during the thinking process, many unrelated knowledge points will connect, ultimately leading to understanding.

4. Root File System

Have you ever thought about these two questions:

Q: For products made with Linux, some are used for monitoring, some for mobile phones, and some for tablets. So after the kernel starts up, after mounting the root file system, which application should be started?

A: The kernel does not know or care which user program should be started. It only starts the init application, which corresponds to /sbin/init.

Obviously, this application must read the configuration file and start the user program (monitor, manual interface, tablet interface, etc.). This question reminds us that the content of the file system has some conventions, such as needing /sbin/init and a configuration file.
Q: Have you ever thought about who implemented the printf used in your hello world program?
A: This function was not implemented by you; it was implemented by a library function. It must find the library at runtime.
This question reminds us that the file system must also contain libraries.
To summarize, to deeply understand, one can look at the busybox init.c to know what the init process does.

Of course, you can also refer to the chapter on building root file systems in .

03

Five Methods for Driver Program Design

1. Use Design Patterns

Design patterns are solutions to problems that frequently occur in software. Developers can choose to waste precious time and budget reinventing a solution from scratch or select the most suitable one from their toolbox. When microprocessors first appeared, low-level drivers were already mature, so why not utilize existing mature solutions?

Driver design patterns can be roughly divided into the following four categories: Bit bang, polling, interrupt-driven, and direct memory access (DMA).

Bit bang mode: When the microcontroller has no peripherals to execute functions, or when all peripherals have been used, and a new request arises, developers should choose the Bit bang design mode. Bit bang mode solutions are efficient but often require a lot of software overhead to ensure their implementation. Bit bang mode allows developers to manually complete communication protocols or external behaviors.

Polling mode is used to simply monitor events in a polling scheduling manner. Polling mode is suitable for very simple systems, but many modern applications require interrupts.

Interrupts allow developers to handle events as they occur without waiting for the code to check manually.

DMA (Direct Memory Access) mode allows other peripherals to handle data transfer requirements without driver intervention.

2. Understand Real-Time Behavior

A real-time system’s ability to meet real-time requirements depends on its driver programs. Poorly written drivers are inefficient and may cause uninformed developers to abandon system performance. Designers need to consider two characteristics of drivers: blocking and non-blocking. A blocking driver will prevent any other software from executing until it completes its work. For example, a USART driver can load a character into the transmission buffer and wait until it receives a transmission end flag before proceeding to the next operation.

On the other hand, non-blocking drivers generally utilize interrupts to achieve their functionality. The use of interrupts can prevent the driver from intercepting other software’s execution while waiting for an event to occur. The USART driver can load a character into the transmission buffer and then wait for the main program to issue the next instruction. The setting of the transmission end flag will trigger an interrupt to allow the driver to proceed to the next operation.

Regardless of the type, to maintain real-time performance and prevent failures in the system, developers must understand the average execution time of the driver and its worst-case execution time. A complete system may face larger safety issues due to a potential risk.

3. Reuse Designs

Why reinvent the wheel when time and budget are tight? In driver program development, reuse, portability, and maintainability are key requirements for driver design. Many of these features can be illustrated through the design and use of a hardware abstraction layer (HAL).

The hardware abstraction layer (HAL) provides developers with a way to create a standard interface to control the microcontroller’s peripherals. Abstraction hides implementation details and instead provides visual functions, such as Usart_Init and Usart_Transmit. This method allows any USART, SPI, PWM, or other peripherals to have common characteristics supported by all microcontrollers. Using HAL hides the details of lower-level, specific devices, allowing application developers to focus on application needs rather than how the underlying hardware works. At the same time, HAL provides a container for reuse.

4. Refer to Data Manuals

Microcontrollers have become increasingly complex over the past few years. Previously, to fully understand a microcontroller, one needed to master a single data manual of about 500 pages. Nowadays, a 32-bit microcontroller typically comprises a portion of data manuals, datasheets for the entire microcontroller series, hundreds of datasheets for each peripheral, and all errata. Developers wishing to master this content fully need to understand thousands of pages of documents.

Unfortunately, all of these data manuals are what a driver program truly needs to be implemented reasonably. Developers must collect and sort the information contained in each data manual from the beginning. Typically, each of them needs to be accessed for the peripherals to start and run. Key information is scattered (or hidden) across each type of data manual.

5. Beware of Peripheral Failures

Recently, I had the opportunity to port a series of microcontroller drivers to other microprocessors. Manufacturers and data manuals indicated that the PWM peripherals were the same between the two microcontroller series. However, the reality was that there were significant differences when running the PWM driver. The driver only worked on the original microcontroller and was ineffective on the new series of microcontrollers.

After repeatedly reviewing the data manuals, I found a completely unrelated note in the data manual indicating that the PWM peripheral would be in a fault state when powered on and needed to clear a flag hidden in a register. At the start of driver implementation, confirm any potential faults of the peripheral and check for errors in other seemingly unrelated registers.

04

Advice from Experts on Embedded Driver Development

1) For future development, besides considering breadth, it is more important to pay attention to the depth of knowledge.

For example, if you have worked on network drivers, have you only stayed at the surface level of writing drivers or have you deeply understood the network structure of the Linux kernel and the TCP/IP protocol?

2) In Linux development, many times you need to utilize existing resources; it is unnecessary to do everything yourself. The key is whether you understand the things behind the original author’s writing once it becomes your driver. You should not just make it work. When writing drivers, consider its performance issues and provide testing methods (of course, you can use many existing tools, such as netperf for testing network performance).

When you have written Flash drivers, you may realize how important Flash performance can be.

3) Self-cultivation of C programs: have you considered some aspects of software engineering, such as the maintainability and extensibility of the program? For example, for the LCD driver, is it only necessary to modify a few places from Sharp to NEC?

For different brands of Flash, how can you make the Flash driver more flexible?

4) If you have spare time, you can pay attention to the development of the Linux kernel. For example, has the LCD driver considered the V4L2 universal architecture? Has the network driver used NAPI? Of course, this assumes you have already understood LDD3 and ULK2 relatively well.

5) The drivers you are currently working on are not considered very core components. If you want better development, consider moving towards audio, video, and net aspects. You should pay more attention to what kind of talents the entire industry needs. Each of the above requires a solid foundation; for example, video requires understanding MPEG4, H264, etc., which takes 1 to 2 years to enter the industry. Therefore, I suggest not to just bury your head in doing things but to pay appropriate attention to current applications.

6) Supplementing hardware knowledge: working in embedded Linux requires reading hardware specs; if you understand the working mechanisms of hardware thoroughly, it will help you write better-performing driver programs.

By the way, timely improving your English proficiency will definitely help your career (don’t wait until you need it to improve, it’s too late).

7) If you have time, pay attention to understanding/accumulating knowledge about Linux application programming; it will also help you write good functionality in driver programs.

8) Never assume that just because you have done a lot, there are still many unknown areas regarding drivers, such as TVIN/TVOUT, USB, SDIO, etc. It is difficult to clarify where the issue lies before it is resolved.

Sometimes it is just a sentence in the datasheet that you overlooked, and there have been several occasions where I couldn’t get it to work and later found out it was a PCB issue, so it can be particularly confusing at times.

05

Reflections of Self-Learners in Embedded Drivers

After years of self-learning in embedded systems, it can be said that I have been constantly seeking survival amidst despair. Due to my personality, I have a tendency to want to understand every aspect of what I learn, questioning whether there is no other way to do things. When I cannot find an explanation that I can accept for a problem, my learning journey almost comes to a halt, perhaps because I detest having only a superficial understanding.

It may be related to being taught as a child not to be a bookworm; as a child, obedient and serious children are greatly influenced by the education of elders. Many influences, if you do not carefully observe yourself, you cannot perceive these ingrained concepts. In my growth process, the education from these elders only formed my viewpoints when I thoroughly recognized that a certain concept was incorrect, but these viewpoints are just a drop in the ocean among all values.

This dislike for superficial understanding is extreme in today’s society because many things you learn are not starting from zero. For instance, the programming languages you use are high-level rather than low-level or machine code, so my entire learning process is very slow. Let’s say, as mentioned earlier, I have been learning for more than half a year, but I have been in embedded systems for two years now, meaning that I have spent over a year in stagnation during the learning period.

Learning embedded systems, or modern computer programming, requires you to accept its settings and models. Conversely, when you truly accept its settings and models and remember them, I believe you have learned well.

Yesterday, I had another moment of rebirth. Recently, I have been working on drivers, and I almost gave up on continuing down the embedded path due to an LCD driver. Last night, unable to sleep, I opened learning videos and lay in a room that had been dark for a long time, probably around 3 or 4 o’clock in the morning. Previously, I had been learning to write driver source code, which was almost no different from bare-metal programming, just using some kernel registration interface functions.

Recently, I wanted to switch things up because many device drivers come pre-installed in the kernel, and there are device drivers for various platforms. I thought if I could familiarize myself with the programming of the kernel’s built-in drivers, I would only need to modify them when writing a driver for a specific device in the future. Through studying the LCD platform device driver, I understood its programming ideas and even recognized this idea, which made me question the significance of learning to write drivers from scratch; why not directly teach how to modify kernel source drivers?

So I continued to modify the kernel driver source code according to the book, but the issue was that the book stated their modifications ran successfully, yet no matter how I debugged, I failed. I repeatedly checked whether my modifications matched those in the book, and after many checks, I still didn’t find any differences. However, I discovered that the kernel source code in the book had slight differences from the source code I was using (of course, the book didn’t provide all the source code, just the modified parts nearby). This is the discovery that these unrelated source codes and my source code had slight differences, such as my source code containing some additional settings (which seemed unimportant).

After confirming that my modifications matched the book but still failed, I compared them with the successfully running self-written driver and gradually found that I had not activated the device and did not illuminate the backlight. It seemed that there were also issues with the register settings after memory allocation because the addresses were calculated using various macro definitions, and I was unsure whether the final calculated addresses matched my register addresses, as the final addresses calculated in the driver source code were virtual addresses. So I compared the self-written driver and made small modifications, but I still didn’t succeed before going to bed.

I wanted to learn in a way that I could confidently identify issues at a glance and easily resolve them. I admit I have been a bit impatient. However, after a night of desperate thinking and struggle, I seem to have come to an understanding: Why do embedded learning videos teach writing drivers from scratch?

Now let me discuss the issues with self-written drivers and kernel driver source code:

Self-written Drivers:

The program is simple and concise; it can only drive a specific device. If the device changes and support for another model is needed, you need to modify that driver again; if the system needs to support two types of LCDs simultaneously, it will become complex, and the simplicity advantage of the kernel driver will decrease significantly; if you want the driver to support multiple devices, then self-written drivers will lose their simplicity advantage compared to kernel driver source code due to differences in programming ideas.

Kernel Built-in Driver Source Code:

① From the system perspective, variable and macro definitions are used extensively; some macro definition values are designed to allow different values to be called at different stages, turning a simple assignment into multiple calculations to check whether the value meets usage requirements. Since we are not the coders of that driver, we are unclear about the benefits of this approach. Perhaps the kernel driver source code developers consider this an approach to reduce overall system code, making it more concise. For each device, assigning specific values would add hundreds of lines of code for all devices in the system. So it is better to let each device calculate suitable values based on various platforms for a certain macro, while similar calculations are integrated together to reduce the number of lines of code in the system. Thus, the driver source code in the system is an integration of the system developers’ work, based on the overall framework of the system.

② The kernel built-in driver also contains some code for compatibility with previous versions. For instance, when hardware memory resources were scarce, methods like using palettes were needed to reduce memory usage during program execution. This can also increase the complexity of the code; this step, while not necessary, if not handled well, may cause the LCD driver to fail to work normally.

③ The program is complex; to accommodate multiple device models, it adds different device drivers more simply. The kernel abstracts and separates the driver into platform management, driver code (hardware-independent code), and device code (hardware-related code). When users add new device drivers, they only need to check the matching information in the platform management section and provide a hardware device-related code (in a specified format) file.

Now, let’s evaluate from the perspective of a driver developer rather than a system developer.

① Self-written drivers are concise and have clear points. For driver developers, this is useful because regardless of which kernel version or chip platform you are using, you can easily confirm the status of the hardware device. If the self-written code passes, you can use the parameters used in the self-written code to cross-check and modify the kernel, then test it again. If it fails, you can first shield the extra settings in the kernel, compile it, and find the error location based on the prompts to make modifications. Because these extra settings can be problematic if set incorrectly; it is challenging to locate the error.

Self-written drivers are useful in this regard because they allow you to eliminate the likelihood of unrelated failure issues and identify error locations with less code. If you confirm that your settings meet the necessary requirements for that device and it still fails, you can confidently suspect a hardware issue. If the self-written code succeeds, it can also serve as a standard for modifying kernel drivers.

② The kernel driver source code’s support for managing multiple device models is the reason I use it. First, understand the device driver structure for this version of the platform; if adding model support, you can use the parameters and settings from the self-written driver. If it is the first time starting this type of device, you need to check the integrity of the structure. If the structure is complete, and the parameters are correct, but it still shows an error, then simplify the kernel driver source code to the straightforward settings of the self-written driver. Ultimately, it transforms into a self-written driver based on the kernel driver architecture. If it still fails, it is undoubtedly a structural issue.

Therefore, self-written drivers have their value. Kernel driver source code content may change, and platforms may change, but self-written drivers become the smallest and easiest to implement for driving purposes. They are an indispensable and important part of achieving objectives.
06

How to Write Embedded Linux Device Driver Programs?

1. The Concept of Linux Device Driver

System calls are the interface between the operating system kernel and application programs, while device driver programs are the interface between the operating system kernel and machine hardware. Device drivers shield hardware details from application programs, so that in the eyes of application programs, hardware devices are merely device files, and application programs can operate hardware devices as if they were ordinary files. Device drivers are part of the kernel and perform the following functions:

1. Initialize and release the device;
2. Transfer data from kernel to hardware and read data from hardware;
3. Read data sent to the device file by application programs and return data requested by application programs;

4. Detect and handle errors occurring with the device.

Under the Linux operating system, there are three main types of device files: character devices, block devices, and network devices. The main difference between character devices and block devices is that when a read/write request is issued to a character device, the actual hardware I/O generally occurs immediately. In contrast, block devices use a portion of system memory as a buffer; when a user process requests a device, if the request can be satisfied, the requested data is returned. If not, a request function is called to perform the actual I/O operation. Block devices are primarily designed for slow devices such as disks to avoid consuming excessive CPU time waiting.

It has been mentioned that user processes interact with actual hardware through device files. Each device file has its file attributes (c/b), indicating whether it is a character device or a block device. Additionally, each file has two device numbers: the first is the major device number, which identifies the driver program, and the second is the minor device number, which distinguishes different hardware devices using the same device driver program. For example, if there are two floppy disks, the minor device number can be used to differentiate them. The major device number of the device file must match the major device number applied for during driver registration; otherwise, the user process will not be able to access the driver program.

Finally, it is essential to mention that when a user process calls a driver program, the system enters kernel mode, where preemptive scheduling no longer occurs. This means the system must complete the sub-function of your driver program before it can proceed with other tasks. If your driver program enters an infinite loop, unfortunately, you will have to restart the machine, followed by a lengthy fsck.

2. Example Analysis

Let’s write the simplest character device driver program. Although it does nothing, it will help us understand how Linux device drivers work. Input the following C code into the machine, and you will obtain a real device driver program.

Since user processes interact with hardware through device files, the methods of operating device files involve some system calls such as open, read, write, close, etc. Note, it is not fopen or fread. But how do we associate system calls with the driver program? This requires understanding a very critical data structure:

struct file_operations {int (*seek) (struct inode *, struct file *, off_t, int); int (*read) (struct inode *, struct file *, char *, int); int (*write) (struct inode *, struct file *, off_t, int); int (*readdir) (struct inode *, struct file *, struct dirent *, int); int (*select) (struct inode *, struct file *, int, select_table *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); int (*mmap) (struct inode *, struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*release) (struct inode *, struct file *); int (*fsync) (struct inode *, struct file *); int (*fasync) (int, struct file *, int); int (*check_media_change) (struct inode *, struct file *); int (*revalidate) (dev_t dev); }

Each member of this structure corresponds to a system call. User processes utilize system calls for operations such as read/write on device files. The system call finds the corresponding device driver program through the major device number of the device file, then reads the function pointers of this data structure, and subsequently hands control over to that function. This is the basic principle of how Linux device drivers work. Therefore, the main task of writing a device driver program is to write sub-functions and fill in the various fields of file_operations.

Now let’s start writing the subprograms.

#include <linux/types.h> Basic type definitions#include <linux/fs.h> Related header files for file systems#include <linux/mm.h> #include <linux/errno.h> #include <asm/segment.h> unsigned int test_major = 0; static int read_test(struct inode *inode, struct file *file, char *buf, int count){ int left; User space and kernel spaceif (verify_area(VERIFY_WRITE, buf, count) == -EFAULT ) return -EFAULT; for(left = count ; left > 0 ; left--) { __put_user(1, buf, 1); buf++; } return count; }

This function is prepared for the read call. When read is called, read_test() is invoked, writing 1 to the entire user buffer. buf is a parameter of the read call. However, when read_test is called, the system enters kernel mode. Therefore, the buf address cannot be used; instead, __put_user() must be used, which is a function provided by the kernel for transmitting data to users. Additionally, there are many similar functions. Please note that before copying data to user space, buf must be verified for usability. This is done using the verify_area function to validate that BUF is usable.

static int write_test(struct inode *inode, struct file *file, const char *buf, int count){ return count; } static int open_test(struct inode *inode, struct file *file ){MOD_INC_USE_COUNT; Increment module count to indicate that the current kernel has a device loaded into the kernelreturn 0; } static void release_test(struct inode *inode, struct file *file ){ MOD_DEC_USE_COUNT; }
These functions are all no-ops. They do nothing when called; they merely provide function pointers for the structure below.
struct file_operations test_fops = {  read_test,  write_test,  open_test,  release_test,};

The main body of the device driver program can be considered complete. Now, the driver program needs to be embedded within the kernel. The driver program can be compiled in two ways. One is to compile it into the kernel, and the other is to compile it into modules. If compiled into the kernel, it will increase the kernel size, require modifications to the kernel source files, and cannot be dynamically unloaded, making debugging inconvenient. Therefore, it is recommended to use the module method.

int init_module(void){ int result; result = register_chrdev(0, "test", &test_fops); Register the entire interface for device operationsif (result < 0) { printk(KERN_INFO "test: can't get major number
"); return result; } if (test_major == 0) test_major = result; /* dynamic */return 0; }
When the compiled module is loaded into memory using the insmod command, the init_module function is called. Here, init_module only does one thing: it registers a character device in the system’s character device table. register_chrdev takes three parameters: the first is the desired device number; if it is zero, the system will choose an unoccupied device number and return it. The second parameter is the device file name, and the third is the pointer to the functions that perform the actual operations of the driver program.
If the registration is successful, it returns the major device number; if not, it returns a negative value.
void cleanup_module(void){ unregister_chrdev(test_major, "test"); }
When the rmmod command is used to unload the module, the cleanup_module function is called, which releases the entry occupied by the character device test in the system character device table.

An extremely simple character device can be considered written; let’s name the file test.c.

Now compile:

$ gcc -O2 -DMODULE -D__KERNEL__ -c test.c 

-c indicates the output specified name, automatically generating the .o file.

The file test.o is the device driver program.

If the device driver program has multiple files, compile each file using the command above, then

ld -r file1.o file2.o -o modulename

The driver program has been compiled; now let’s install it into the system.

$ insmod -f test.o

If the installation is successful, you can see the device test in the /proc/devices file, along with its major device number. To uninstall, run:

$ rmmod test

The next step is to create the device file.

mknod /dev/test c major minor

c indicates character device, major is the major device number, which can be found in /proc/devices.

Use the shell command

$ cat /proc/devices

to obtain the major device number; you can add the above command line into your shell script.

The minor device number can be set to 0.

Now we can access our driver program through the device file. Let’s write a small test program.
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> main() {     int testdev;     int i;     char buf[10];     testdev = open("/dev/test", O_RDWR);    if ( testdev == -1 )     {         printf("Cann't open file 
");         exit(0);     }    read(testdev, buf, 10);     for (i = 0; i < 10;i++)         printf("%d
", buf[i]);     close(testdev); }
Compile and run it; see if it prints out all 1s?

The above is just a simple demonstration. A truly practical driver program is much more complex and needs to handle issues such as interrupts, DMA, I/O ports, etc. These are the real challenges. The above provides a framework and principle for writing a simple character device driver; more complex writing requires serious study of the Linux kernel’s operating mechanisms and the specific mechanisms of device operation, etc. I hope everyone thoroughly masters the methods of writing Linux device driver programs.

07

Analysis of Embedded Driver Structure

Writing driver programs on the Linux system is both simple and difficult. It is challenging due to the algorithm writing and device control aspects, which can be quite troublesome; however, it is simple because there is already a set pattern for driver development under Linux. When writing, you only need to follow this pattern, which consists of predefined structures; when writing drivers, you just need to appropriately fill these structures based on the device’s requirements to complete the driver writing.
First, in Linux, everything is treated as a file, and driver devices are also viewed as files. For simple file operations, it is merely open/close/read/write. In Linux, the key data structure for file operations is file_operations, defined in the source code directory under include/linux/fs.h, as follows:

[cpp] view plain copy

1. struct file_operations {

2. struct module *owner;

3. loff_t (*llseek) (struct file *, loff_t, int);

4. ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

5. ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

6. ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);

7. ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);

8. int (*readdir) (struct file *, void *, filldir_t);

9. unsigned int (*poll) (struct file *, struct poll_table_struct *);

10. int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

11. long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

12. long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

13. int (*mmap) (struct file *, struct vm_area_struct *);

14. int (*open) (struct inode *, struct file *);

15. int (*flush) (struct file *, fl_owner_t id);

16. int (*release) (struct inode *, struct file *);

17. int (*fsync) (struct file *, int datasync);

18. int (*aio_fsync) (struct kiocb *, int datasync);

19. int (*fasync) (int, struct file *, int);

20. int (*lock) (struct file *, int, struct file_lock *);

21. ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

22. unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);

23. int (*check_flags)(int);

24. int (*flock) (struct file *, int, struct file_lock *);

25. ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);

26. ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);

27. int (*setlease)(struct file *, long, struct file_lock **);

28. };

For the elements in this structure, you can see that each function name has a “*” in front, so they are all function pointers. Currently, we only need to focus on

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

int (*open) (struct inode *, struct file *);

int (*release) (struct inode *, struct file *);

This article is called Simple Driver, which involves reading (read), writing (write), controlling (ioctl), opening (open), and unloading (release). This structure’s role in the driver is to associate system calls with the driver program; it is essentially a collection of function pointers, each corresponding to a system call.

However, file_operations is a structure defined for files, so when writing drivers, some elements may not be used. Hence, version 2.6 introduced a structure framework specifically for drivers: platform, described by struct platform_device for devices and struct platform_driver for device drivers. They are defined in the source code directory under include/linux/platform_device.h, as follows:

[cpp] view plain copy

1. struct platform_device {

2. const char * name;

3. int id;

4. struct device dev;

5. u32 num_resources;

6. struct resource * resource;

7. const struct platform_device_id *id_entry;

8. /* arch specific additions */

9. struct pdev_archdata archdata;

10. };

11. struct platform_driver {

12. int (*probe)(struct platform_device *);

13. int (*remove)(struct platform_device *);

14. void (*shutdown)(struct platform_device *);

15. int (*suspend)(struct platform_device *, pm_message_t state);

16. int (*resume)(struct platform_device *);

17. struct device_driver driver;

18. const struct platform_device_id *id_table;

19. };

For the first structure, its role is to register a device, akin to a device’s identity card; it must have a name, ID number, and address. Of course, other elements are directly copied from the old identity card, which is essentially struct device dev, meaning it is a traditional device encapsulation. The second structure uses structures and function pointers to implement a kind of “class” structure, which is not available in C language, making the driver model an object-oriented structure. The struct device_driver driver describes the basic data structure of device drivers and is defined in the source code directory under include/linux/device.h, as follows:

[cpp] view plain copy

1. struct device_driver {

2. const char *name;

3. struct bus_type *bus;

4. struct module *owner;

5. const char *mod_name; /* used for built-in modules */

6. bool suppress_bind_attrs; /* disables bind/unbind via sysfs */

7. #if defined(CONFIG_OF)

8. const struct of_device_id *of_match_table;

9. #endif

10. int (*probe) (struct device *dev);

11. int (*remove) (struct device *dev);

12. void (*shutdown) (struct device *dev);

13. int (*suspend) (struct device *dev, pm_message_t state);

14. int (*resume) (struct device *dev);

15. const struct attribute_group **groups;

16. const struct dev_pm_ops *pm;

17. struct driver_private *p;

18. };

Again, all elements are defined as pointers. For the driver section, each item certainly requires a function to be implemented. If they are not grouped together, management becomes difficult, and it is easy to lose track. Furthermore, for different driver devices, each function’s name must differ; thus, during development, it becomes inconvenient to search for the corresponding source code when needed. Therefore, encapsulation is necessary; in C language, a good way to encapsulate functions is to use function pointers within structures. This method can also be used in regular program development, embodying an object-oriented idea.

In the Linux system, devices can be broadly categorized into three types: character devices, block devices, and network devices. Each type contains different subsystems. Due to their specific properties, some devices cannot be classified into existing subclasses. Thus, Linux has categorized these subsystems into a new class: misc, described by the struct miscdevice, defined in the source code directory under include/linux/miscdevice.h, as follows:

[cpp] view plain copy

1. struct miscdevice {

2. int minor;

3. const char *name;

4. const struct file_operations *fops;

5. struct list_head list;

6. struct device *parent;

7. struct device *this_device;

8. const char *nodename;

9. mode_t mode;

10. };

These devices all share a common major device number of 10, so they are differentiated by minor device numbers. The elements inside should be familiar, and we can confirm that list_head is indeed a bridge.

In fact, for the structures introduced above, the roles of the elements can be inferred from their names, so there is no need for further elaboration. Writing a driver module essentially involves filling the aforementioned structures, writing corresponding functions based on the device’s functionalities and purposes, and linking them to the structure’s pointers. Finally, write an entry and an exit (which are the init and exit in module programming) is all that is needed. Generally, the entry program is about registering platform_device and platform_driver (of course, this applies specifically to drivers written in platform mode).

08

Recommended Embedded Books

1. Books on Hardware:

Microcomputer Principles, Digital Circuits, university textbooks.

2. Books on Linux:

, the one written by foreigners

When doing drivers, you will definitely need to use kernel-related materials or need to cooperate with certain modules in the kernel, so you must understand how certain parts of the kernel are implemented. Ultimately, you should have a good grasp of the overall framework of the Linux kernel.

These are all improvements, things you need to summarize in your repeated development. If you do not summarize, you will always start from scratch (or you will never understand why others’ code is written that way when you modify it and it works, and that’s it), and you will never improve. In the end, you will feel that you are nothing and understand nothing.

One more point to clarify: many people are engaged in Linux development but do not use Linux systems as their working platform. In such cases, it is challenging to understand the implementation mechanisms of the Linux kernel and why certain methods are used for implementation.

If you have never used a Linux system, it is impossible to implement a project that conforms to Linux’s operational mechanisms. Even if your project succeeds, it will definitely not be optimal or adhere to Linux’s usage habits (including kernel extensions and application implementations).

Therefore, I want to say that you must regularly summarize what you have done during this period, what you have gained from it, and what you should look at to do similar work better in the future; secondly, you must at least use Linux as your regular working platform in your development environment and not rely on virtual machines or servers. Only by fully understanding how to use Linux can you develop projects that conform to its rules.

Copyright statement: This article comes from the internet, free to convey knowledge, and the copyright belongs to the original author. If there are copyright issues, please contact me for deletion.

You may also like:

A Great Smart Distribution Network Solution!

Sharing an Embedded Software Tools List!

Sharing a Must-Have Drawing Tool for Embedded Systems!

Sharing a Compact and Useful Code Comparison Tool

A Multitasking Management OS Implemented in Over 300 Lines of Code

Sharing Several Useful Shell Scripts in Embedded Systems!

Reply 1024 in the public account chat interface to obtain embedded resources; reply m to view article summaries.

Click Read the Original to see more shares.

Give a thumbs upAll Aspects of Embedded Linux Driver Development!

Leave a Comment