Linux Device Driver Development: Everything you need to start with device driver development for Linux kernel and embedded Linux - podcast episode cover

Linux Device Driver Development: Everything you need to start with device driver development for Linux kernel and embedded Linux

Nov 12, 202516 min
--:--
--:--
Download Metacast podcast app
Listen to this episode in Metacast mobile app
Don't just listen to podcasts. Learn from them with transcripts, summaries, and chapters for every episode. Skim, search, and bookmark insights. Learn more

Episode description

This technical resource is intended for experienced users like embedded engineers and kernel hackers and requires prior knowledge of C programming and Linux commands. The content offers comprehensive instruction on developing Linux drivers for kernel version v5.10 and embedded Linux systems, covering essential topics such as setting up the development environment, understanding kernel module concepts, implementing character device drivers (including file operations like read, write, and ioctl), and utilizing the Device Tree mechanism. Furthermore, the book explores advanced kernel subsystems, including the Linux Device Model (LDM), Regmap API for register abstraction, IRQ framework and interrupt handling, locking primitives (spinlocks and mutexes), time management, and the IIO and GPIO frameworks.

You can listen and download our episodes for free on more than 10 different platforms:
https://linktr.ee/cyber_security_summary

Get the Book now from Amazon:
https://www.amazon.com/Linux-Device-Driver-Development-development/dp/1803240067?&linkCode=ll1&tag=cvthunderx-20&linkId=31340f91cb91cbe60c484199bfdaed38&language=en_US&ref_=as_li_ss_tl

Discover our free courses in tech and cybersecurity, Start learning today:
https://linktr.ee/cybercode_academy

Transcript

Speaker 1

Welcome to the deep dive. Today, we're jumping into the Linux kernel, specifically how it handles hardware. If you've ever wanted to write a device driver, or you know, just understand how your keyboard actually talks to your OS, this is for you. We're building the conceptual map exactly.

Speaker 2

Our mission today is really about the architecture. We've looked at some pretty comprehensive guides on kernel and driver development, and we want to quickly lay out that core blueprint. You know, what do you have to know? What's different? How does the kernel see the world of devices?

Speaker 1

Okay, so starting basic the device driver, it's fundamentally a translator right sitting in the middle.

Speaker 2

It's the crucial link. Yeah, it's that specialized code connecting your apps, your user space stuff to the actual physical hardware and it all goes to the kernel. You could almost say it's the only bit of software allowed to like directly touch the hardware ports.

Speaker 1

Got it. Okay, So let's start where most drivers start, the kernel module. This is how Linux stays flexible. Right, Let's you add stuff without a.

Speaker 2

Full reboot precisely, modules like you extend the kernel dynamically at run time, and our sources. Definitely emphasize this isn't for absolute beginners. You need solid C skills and to be comfortable on the Linux command line before you really dive into writing kernel code.

Speaker 1

Right, So, if we're building one, we need the basic structure, an entry point, and ideally an exit point.

Speaker 2

That's the core. You use module in it that declares the function that runs when your module gets loaded you know withins modern mod probe. And then module exit declares the cleanup function for when you unload it with a romedet.

Speaker 1

Okay, now here's where it gets interesting. Compared to normal programming. Memory optimization. Yeah, the kernel uses these macros in and exit. Why are they so important? Ah?

Speaker 2

Yeah, they're not just comments. There are instructions for the compiler and linker really important. When you mark your in it function within it, you're telling the linker put this code in a special memory section.

Speaker 1

And what's special about that section?

Speaker 2

Well, if your module is built into the kernel statically compiled, the kernel actually freeze that memory once the init function is done, because, I mean, it's never going to call that in it function again until the next reboot, right, so why keep the code around. It's a smart optimization. Use it, lose it.

Speaker 1

That is clever proactively cleaning up its own startup code. And exit is that the flip side pretty much? Yeah.

Speaker 2

Exit tills the compiler to just leave out the exit functions code entirely if the module is built in, because if it's built in, you can unload it anyway, so save space removes code you'd never run.

Speaker 1

Okay, moving to metadata. Every module needs info like module author, But the big one seems to be module license. Why is that one so sensitive?

Speaker 2

It really is. It's about the GPL and the kernel's ecosystem. You must declare a GPL compatible license using module license, especially if you want your module to use certain kernel functions, ones that are specifically exported only for GPL modules using exports symbol GPL.

Speaker 1

And if you don't, or if you use a proprietary.

Speaker 2

License, then boot your kernel gets more distainted. The kernel sits a fly. It basically says warning, non open or untrusted code has been loaded. If you then get a crash a kernel panic, well good luck getting help from the community. They'll likely see the taint flag and you know, say they can't support it because proprietary code might be the cause. It's a serious line.

Speaker 1

Okay, that definitely sets the stage. Now let's make that mental shift. We're not writing userspace programs anymore. We're inside the kernel. The rules change, the safety nets.

Speaker 2

Gone completely gone. In user space, if something goes terribly wrong, your program crashes. The OS cleans up fine. In the kernel, no way. If you allocate memory, grab an ieoport, whatever, you must clean it up yourself before your function returns. Fail to do that and your risk leaks instability, maybe even a full system crash.

Speaker 1

So error handling is totally different. No more just returning zero for success and one for failure.

Speaker 2

Right. The standard is really strict. Kernel functions that interact with system calls. They must return errors as negative values like return error code. So you'll see you're an AIO for an IO error or an enulmum if you couldn't get memory. It's a clear convention, so errors get passed up correctly, and.

Speaker 1

To manage that cleanup, especially if you have say five steps that work then the sixth one fails. The kernel style actually recommends using go to. That sounds controversial. It does.

Speaker 2

It goes against a lot of Standard C teaching, but in the kernel it's pragmatic. You use go to strictly for error cleanup paths. You set up labels like air free, buffer, air release, lock, maybe out. When an error happens, you go to the appropriate label. This jumps you straight into the sequence of cleanup steps needed, executed in reverse order of allocation.

Speaker 1

But doesn't that make the code harder to follow all those jumps.

Speaker 2

You'd think so, but it actually tends to make it cleaner. In this context. The alternative is deeply nested eifles blocks which get really hard to read and are super easy to mess up, like forgetting a cleanup step inside one branch. The go to approach enforces a clear, linear cleanup path. It's considered the safest and most readable pattern for kernel error handling. Strange but true.

Speaker 1

Okay, I can see the logic there, given the stakes. What about functions that need to return a pointer but might also fail. You can't return both a pointer and a negative error code, ah.

Speaker 2

Classic C problem. The kernel has neat macros for this, or ptr iSER and per tier. If your function fails and needs to return say anviol invalid argument instead of a pointer, it uses airptr and ball fail. This converts the air code into a special pointer value. The code calling that function then checks the return pointer using iSER. If it returns true, it means it's an error pointer. Then if you need the actual air code back, you use ptr on that pointer and it gives you ANVOL.

It avoids ambiguity with returning anal MP.

Speaker 1

That's ill again. Okay, last bit on the programming shift logging, we're supposed to move beyond just using print, right.

Speaker 2

Yeah. While print is still the underlying engine, the recommendation is strongly towards using specific elper functions. Don't just use print with log levels directly, use things like prayer or prinfo for general module messages. But and this is important, if you're in a device driver, use the dev versions UH or dev infostruct device DEV.

Speaker 1

And why the dev prefix ones specifically for drivers.

Speaker 2

Because they automatically include context about the specific device the message is coming from, its name is positioned in the system, makes debugging way easier when you have multiple identical devices. They're even netdev versions for network drivers. They all tie the message to a specific kernel object.

Speaker 1

Got it? And if we want our modules messages to stand out in the flood of kernel logs, how do we add a prefix?

Speaker 2

Simple. You use the prfmt macro, you define hashtag, define prfmt fmt KDI. You're building the devmt at the top of your source file, Katie. Build mode name gets set automatically during the build to your module's name. So now every print FO, dev etc. Automatically prints like my driver error occurred. Super helpful.

Speaker 1

Okay, we've got the rules of engagement down. Now. The big danger zone concurrency, especially on SMP systems symmetric multiprocessing, where multiple CPU use can access the same memory the same hardware simultaneously. Locks are essential.

Speaker 2

This is absolutely where things get tricky, even for experienced devs. The key is understanding the context. Are you in code that can sleep user context or code that absolutely cannot sleep, like an interrupt handler atomic context.

Speaker 1

Let's start with the fast ones. Spin locks meant for short atomic operation.

Speaker 2

Exactly very short critical sections. When a CPU grabs a standard spin lock. Using spin lock, it disables preemption on that specific CPU, so other tasks in the same core won't interrupt it. But and this is the critical part, a standard spin lock does not stop hardware interrupts from firing on that cpu.

Speaker 1

Ah, and that's the classic deadlock scenario waiting to happen sisily.

Speaker 2

Imagine task A on CPU zero called spin lock to protect some data. Then bam, hardware interrupt fires on CPU zero. The CPU jumps to the interrupt handler IRQ. Now, if that IRQ handler needs the same data and tries to acquire the same spin lock, the IRQ spins forever, waiting task A to release the lock. But Task A is preempted by the IRQ and can't run to release.

Speaker 1

It out catastrophic. So the rule is, if data protected by a spin lock might also be touched by an interrupt handler, you need something more.

Speaker 2

Yes, unless you are absolutely one hundred percent certain that no interrupt handler will ever try to access that data or acquire that lock, you must use the IRQ save variants. Those are functions like spin locker save and spin a locker store. They do two things, disable preemption and disabled hardware interrupts on the local CPU. This makes the critical section truly atomic on that core. It's a vital lesson.

Speaker 1

Okay, So spin locks are for short atomic contexts, possibly needing IRQ disabling. How do utexts differ?

Speaker 2

Mutexes are conceptually simpler but have different rules they're built using spin locks underneath, but their behavior is different. If task B tries to acquire a mutex held by task A, instead of spinning, task B is put to sleep. The kernel puts it on a weight queue and lets the CPU schedule some other.

Speaker 1

Task, so they yield the CPU much more better for potentially longer critical sections.

Speaker 2

Then, yes, much better for contention and longer holds. But and this is a huge butt because they involve sleeping. You can only use mutexes in context where sleeping is allowed. That mean primarily user context like when handling a system call. You can never acquire a mutex from an interrupt handler or any other atomic context because those contexts absolutely cannot sleep using a mutex. There boom, system panic, got it.

Speaker 1

Spin locks for atomic mutexes for sleeping contexts. Let's briefly touch on time. We hear about Jiffey's but things are more abstract now.

Speaker 2

Yeah, Chiffees was the old way. It is. The kernel counter the increments at a certain frequency defined by the h z value. It gave fairly low resolution timing. The modern kernel abstracts this. It relies on two types of hardware components. For time.

Speaker 1

The things that track time and the things that schedule events in time.

Speaker 2

You got it. First, clock source devices. These provide a high resolution, always increasing monotonic counter. Think of it as the master clock for accurate time stamps. Second clock event devices these are the programmable timers. They can be told fire and interrupt exactly n nanoseconds from now. They're crucial for things like high resolution timers or timers much more precise than Jiffy's.

Speaker 1

Okay, and before we leave this area, tasklts they were for deferring work from interrupts, right the bottom half.

Speaker 2

Yes, they were a common mechanism. Yeah, but the sources we looked at had a very very strong warning. Task lits are being actively deprecated. They're slated for removal. You should not use them in any new code. They really only exist now for historical pedagogic reasons.

Speaker 1

Okay, loud and clear. So for deferring work now, the answer is work cues.

Speaker 2

Absolutely use workques. They provide a much more flexible and robust way to q work to be executed by dedicated kernel threads. Those threads can sleep, so they can handle much longer, more complex tasks safely.

Speaker 1

Right. Let's zoom out now to the big picture, how the kernel organizes all this hardware, the Linux Device Model LDM. This is how physical stuff turns into those directories we see insists.

Speaker 2

LDM is the framework, the glue it creates that hierarchy. Buses contain devices, drivers attached to devices, and it uses three core low level structures. At the very bottom, the most fundamental piece is the object. A cobject isn't a device itself or a driver. It represents the connection or an object that can appear in sifts. Basically, every single directory you navigate in the CYS Virtual file system corresponds to a object in kernel memory. It's the basic building block of that hierarchy.

Speaker 1

Ah okay, So cobject is the foundation for creating that topology and exposing attributes out to user space through sifts. That clicks.

Speaker 2

It's fundamental. These objects are then grouped into ksets, which often correspond to directories containing multiple objects, and they have associated cobduy types which define behavior like attribute handling. Together they build the whole.

Speaker 1

Structure, and within the structure we find different kinds of buses.

Speaker 2

Broadly, yeah, you have discoverable buses. Think PCI USB. You plug something in the bus itself can figure out what it is until the kernel the hardware handles the numeration. Then you have nondiscoverable buses. The platform bus is a common example, especially in embedded systems, where peripherals are just hardwired to the CPU. The bus itself doesn't announce devices. For these, the kernel needs to be told that a device exists at a certain address or uses certain resources.

This information needs to be provided somehow.

Speaker 1

Which brings us neatly to the modern way of providing that information, the device tree. This replaced the old hard coded board files exactly.

Speaker 2

Device tree or DT is a huge step for portability. Instead of c code describing the hardware layout for one specific board, you use a textual format dot DTS source files. These files describe the hardware using nodes representing devices, buses, and properties like memory addresses, interrupt numbers. This dot dts file is compiled into a binary blob, the dot DTB

or FDT Flatten device tree. The bootloader loads the dot DPB into memory, and the kernel parses it at boot time to learn about the non discoverable hardware present.

Speaker 1

What about adding hardware after boot or changing configurations dynamic.

Speaker 2

That's where dtoverlays come in. These are smaller partial dot DTBO device tree blob overlay files. They act like patches. You can load it dot DTB at runtime, usually via can figs, and it modifies the live in memory device tree, maybe enabling an interface, changing a pin configuration, adding a new IT two C device. It's very powerful.

Speaker 1

So when my driver starts up and it needs to know its memory address or its interrupt number, how does it get that from the device tree? Reliably?

Speaker 2

It uses specific API calls to query the tree. Crucially, the device tree uses named properties, so instead of asking for the first interrupts, your driver asked for the interrupt named four or similar functions like platforms yourt resource by name or of property read thirty two. Let the driver request resources by name as defined in the dot dts. This decouples the driver from the exact physical wiring order.

Speaker 1

Makes sense, Okay, last piece, let's connect this architecture to a real world subsystem. The example given was industrial io iiO right io is.

Speaker 2

A great example. It's a whole kernel subsystem specifically designed for sensors. Analog to digital converters ADCs, Digital to analog converters dcs basically data acquisition hardware.

Speaker 1

It manages all the complexity of reading raw sens or values, scaling them, handling triggers.

Speaker 2

Exactly, But how does it expose that data to userspace. It doesn't require custom applications for every sensor.

Speaker 1

It uses the device model and sifts precisely.

Speaker 2

The io framework models sensors and their data points as channels. Each channel like the X axis acceleration or temperature reading, gets exposed as attributes within sifts, So reading the raw value from an accelerometer might be as simple as reading a file like cispusiodevi COO dot device zero in SLX. User space just interacts with these standard file interfaces underpinned by LDM and the cobject structure representing that channel attribute.

Speaker 1

Wow. Okay, we've covered a lot, from those tiny innit macros affecting memory all the way up to these complex frameworks like iiO built on the core architecture.

Speaker 2

Yeah, we hit the big points. The mindset shift for kernel programming error clean up with go to logging, then the concurrency mindfield spin locks versus mutex is, atomic versus sleeping contexts.

Speaker 1

And then the overall structure. How the Linux device model uses cobjects to build the hierarchy, how the device tree describes hardware, and how subsystems like iiO plug into that.

Speaker 2

So if there's one final thought to leave you with, it's this, remember that object. It seems abstract, almost trivial, but everything in the device hierarchy, from the simplest led driver to the most complex network interface or iiO sensor framework, ultimately relies on objects to establish its existence and its attributes within the kernel's world, making it visible and controllable through sifts. That tiny structure is the bedrock of Linux's entire device organization.

Speaker 1

Amazing how that one fundamental piece underpins so much complexity. That's a great takeaway. Thank you for walking us through this.

Speaker 2

My pleasure is fascinating stuff.

Speaker 1

It really is. We'll catch you next time for another deep dive.

Transcript source: Provided by creator in RSS feed: download file
For the best experience, listen in Metacast app for iOS or Android