When first encountering embedded Linux, one may feel confused:
- • Why can’t I directly manipulate hardware? On an MCU, you can directly write
<span>GPIOA->ODR |= 0x01</span>, but in Linux, you have to operate through the file system. - • What are kernel space and user space? All code on an MCU runs in the same address space.
The core reason: The MCU is a “bare-metal” system with complete control; whereas Linux is a multitasking operating system that requires adherence to the rules and abstractions of the operating system.
Understanding the Differences
MMU and Virtual Memory: Addresses are no longer “real”
MCU Thinking (Direct Mapping)
On an MCU, addresses are “real”:
// In STM32, 0x40020000 is the physical address of GPIOA
GPIOA->ODR = 0x01; // Directly manipulate the physical address
Characteristics:
- • Address 0x40020000 is the actual location of the hardware register.
- • All programs share the same address space.
- • Programs can directly access any address (including hardware registers).
Linux Thinking (Virtual Memory)
In Linux, addresses are “virtual”:
// In Linux, the address you see 0x40020000 may be a virtual address
// The actual physical address may be completely different.
The Role of MMU (Memory Management Unit):
- • Address Translation: Converts virtual addresses used by programs into actual physical addresses.
- • Memory Protection: Prevents programs from accessing memory areas that do not belong to them.
- • Memory Sharing: Multiple programs can share the same physical memory segment.
Metaphorical Comparison:
- • MCU: Like living directly in a house, the house number is the real address.
- • Linux: Like staying in a hotel, the room number (virtual address) and the actual floor (physical address) are separate, with the front desk (MMU) responsible for the conversion.
Why is virtual memory needed?
- 1. Security: Programs cannot directly access other programs’ memory or hardware.
- 2. Multitasking: Each program thinks it is using the complete address space.
- 3. Memory Management: The operating system can flexibly allocate and reclaim memory.
Impact on Development:
- • Cannot directly access hardware registers; must go through drivers.
- • Address 0x40020000 in your program may point to a completely different location.
- • Need to understand the address mapping relationship between user space and kernel space.
Processes and Threads: From “Single-task” to “Multitask”
MCU Thinking (Foreground-Background System or RTOS)
On an MCU, it is usually:
// Foreground-background system
void main() {
while(1) {
task1(); // Task 1
task2(); // Task 2
task3(); // Task 3
}
}
// Or RTOS
void task1(void *pvParameters) {
while(1) {
// Task code
vTaskDelay(100);
}
}
Characteristics:
- • All tasks share the same address space.
- • Tasks can directly access global variables.
- • Task switching is managed by the RTOS scheduler.
Linux Thinking (Processes and Threads)
In Linux, there are concepts of processes and threads:
Process:
- • Independent address space.
- • Has independent resources (file descriptors, memory, etc.).
- • Inter-process communication requires special mechanisms (pipes, shared memory, message queues, etc.).
Thread:
- • Shares the address space of the process.
- • Shares the resources of the process.
- • Threads can directly access shared variables.
Metaphorical Comparison:
- • MCU Tasks: Like different employees in the same office, sharing all resources.
- • Linux Processes: Like different companies, each with its own office and resources.
- • Linux Threads: Like different departments within the same company, sharing company resources but working independently.
Impact on Development:
- • Need to understand inter-process communication (IPC) mechanisms.
- • Multithreaded programming requires attention to synchronization and mutual exclusion.
- • Program crashes will not affect other processes (which is a good thing!).
Kernel Space and User Space: Separation of Privileges
MCU Thinking (Unified Space)
On an MCU:
// All code runs at the same privilege level
void gpio_init() {
// Directly manipulate registers
GPIOA->MODER |= 0x01;
}
void user_function() {
gpio_init(); // User code can call directly
}
Characteristics:
- • All code has the same privileges.
- • Can directly access hardware.
- • No privilege protection.
Linux Thinking (Space Separation)
In Linux, the system is divided into two spaces:
User Space:
- • Runs application programs.
- • Cannot directly access hardware.
- • Cannot directly access kernel memory.
- • Interacts with the kernel through system calls.
Kernel Space:
- • Runs the operating system kernel.
- • Can access all hardware.
- • Can access all memory.
- • Has the highest privileges.
┌─────────────────────────────────────┐
│ User Space │
│ ┌─────────┐ ┌─────────┐ │
│ │ App1 │ │ App2 │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ └──────┬─────┘ │
│ │ System Call │
├──────────────┼──────────────────────┤
│ Kernel Space │
│ ┌──────────────────────────────┐ │
│ │ Device Drivers, File System, │ │
│ │ Network Stack │ │
│ └──────────────────────────────┘ │
│ │ │
│ │ Direct Access │
├──────────────┼──────────────────────┤
│ Hardware Layer │
│ GPIO, UART, I2C, etc. │
└─────────────────────────────────────┘
Metaphorical Comparison:
- • MCU: Like everyone being in the same room, able to operate any device freely.
- • Linux: Like having regular employees (user space) and administrators (kernel space), where regular employees need to go through administrators to operate devices.
Impact on Development:
- • Applications cannot directly manipulate hardware; must go through drivers.
- • Drivers run in kernel space and require special permissions.
- • User programs and kernel programs use different APIs.
Why is this separation needed?
- 1. Security: Prevents applications from damaging the system.
- 2. Stability: Application crashes do not affect the kernel.
- 3. Portability: Applications do not need to care about hardware details.
Transformation of the Development Process
Cross-compilation: Why can’t I compile on the target board?
MCU Thinking (Local Compilation)
In MCU development:
PC (Development Environment)
↓ Compile
.hex / .bin File
↓ Flash
MCU (Target Board)
Characteristics:
- • The compiler and target platform architecture are the same (both are ARM Cortex-M).
- • Can compile directly on the PC to generate target code.
- • Compilation speed is fast, and the toolchain is simple.
Linux Thinking (Cross-compilation)
In Linux embedded development:
PC (x86_64 architecture)
↓ Cross-compilation Toolchain
Executable File for ARM Architecture
↓ Transfer to Target Board
MPU (ARM Architecture, Running Linux)
Why is cross-compilation needed?
- 1. Performance Differences: PCs are powerful and compile quickly; target boards have limited resources and compile slowly.
- 2. Toolchain: The target board may not have a complete development tool.
- 3. Development Efficiency: Developing and debugging on a PC is more convenient.
Components of Cross-compilation Toolchain:
- • gcc: Cross-compiler (e.g.,
<span>arm-linux-gnueabihf-gcc</span>) - • binutils: Binary tools (e.g.,
<span>objdump</span>,<span>objcopy</span>) - • glibc: C standard library (for the target architecture)
- • Kernel Header Files: Needed when compiling drivers.
Common Cross-compilation Toolchains:
- • ARM:
<span>arm-linux-gnueabihf-gcc</span>(hard float) - • ARM64:
<span>aarch64-linux-gnu-gcc</span> - • Raspberry Pi: Can use the official toolchain provided.
Practical Usage Example:
# Compile ARM program on PC
arm-linux-gnueabihf-gcc hello.c -o hello
# Transfer the compiled program to the target board
scp hello [email protected]:/home/root/
# Run on the target board
./hello
Root File System: Linux’s “File Organization Method”
MCU Thinking (Simple Storage)
On an MCU:
Flash Storage
├── Bootloader (Startup Code)
├── Application (Application Program)
└── Data (Data Area, Optional)
Characteristics:
- • Storage structure is simple.
- • Usually no file system.
- • Data is stored in binary form.
Linux Thinking (File System)
In Linux, everything is a file:
Root File System (/)
├── bin/ (Basic Commands)
├── sbin/ (System Commands)
├── etc/ (Configuration Files)
├── dev/ (Device Files)
├── proc/ (Process Information)
├── sys/ (System Information)
├── usr/ (User Programs)
├── var/ (Variable Data)
└── home/ (User Directory)
Functions of the Root File System:
- 1. Provides System Commands:
<span>ls</span>,<span>cd</span>,<span>cat</span>, etc. - 2. Device Management: Access hardware through the
<span>/dev</span>directory. - 3. Configuration Management: Manage configurations through the
<span>/etc</span>directory. - 4. Program Execution: Provides dynamic libraries and runtime environment.
Common Types of Root File Systems:
- • BusyBox: Lightweight, suitable for resource-constrained systems.
- • Buildroot: Automation build tool, can customize root file system.
- • Yocto: More powerful build system, suitable for complex projects.
- • Debian/Ubuntu: Complete Linux distribution, feature-rich.
Steps to Build a Root File System:
- 1. Choose a base system (e.g., BusyBox).
- 2. Add necessary commands and tools.
- 3. Configure system services.
- 4. Add applications.
- 5. Package into an image file.
Impact on Development:
- • Need to understand the Linux file system structure.
- • Applications are usually placed in
<span>/usr/bin</span>or<span>/home</span>directories. - • Configuration files are placed in
<span>/etc</span>directory. - • Devices are accessed through the
<span>/dev</span>directory.
Kernel Trimming and Compilation: Customize Your Linux Kernel
MCU Thinking (Fixed Firmware)
On an MCU:
Select Chip Model
↓
Use Official Firmware Library
↓
Compile to Generate Firmware
Characteristics:
- • Firmware functionality is relatively fixed.
- • Mainly focuses on application layer development.
- • Rarely needs to modify underlying code.
Linux Thinking (Customizable Kernel)
In Linux, the kernel can be trimmed and customized:
Kernel Configuration Options:
- • Driver Support: Choose the required device drivers.
- • File System: Choose supported file system types.
- • Network Protocols: Choose network functionalities.
- • Debugging Features: Choose debugging tools.
Kernel Compilation Process:
# 1. Get Kernel Source Code
git clone https://github.com/raspberrypi/linux.git
# 2. Configure Kernel
make menuconfig # Graphical configuration interface
# or
make defconfig # Use default configuration
# 3. Compile Kernel
make -j4 # Use 4 threads for parallel compilation
# 4. Install Kernel Modules
make modules_install
# 5. Install Kernel
make install
Principles of Kernel Trimming:
- 1. Include only necessary functionalities: Reduce kernel size and boot time.
- 2. Drivers can be compiled as modules: Load when needed, unload when not needed.
- 3. Retain debugging features: Keep during development, can be removed during release.
Common Configuration Tools:
- • menuconfig: Text interface based on ncurses.
- • xconfig: Graphical interface based on Qt.
- • gconfig: Graphical interface based on GTK.
Impact on Development:
- • Need to understand kernel configuration options.
- • Driver development requires recompiling the kernel or modules.
- • Kernel version affects driver compatibility.
Driver Development: From Register Operations to file_operations
MCU Driver Development Thinking
On an MCU, drivers are usually like this:
// STM32 GPIO Driver Example
void gpio_init(GPIO_TypeDef* GPIOx, uint16_t pin) {
// Directly manipulate registers
GPIOx->MODER |= (1 << (pin * 2)); // Set to output mode
}
void gpio_set(GPIO_TypeDef* GPIOx, uint16_t pin) {
GPIOx->BSRR = (1 << pin); // Set pin high
}
void gpio_clear(GPIO_TypeDef* GPIOx, uint16_t pin) {
GPIOx->BSRR = (1 << (pin + 16)); // Set pin low
}
// Usage
gpio_init(GPIOA, 5);
gpio_set(GPIOA, 5);
Characteristics:
- • Directly manipulate registers.
- • Function calls are simple and direct.
- • Less code, clear logic.
Linux Driver Development Thinking
In Linux, drivers must follow the operating system framework:
Basic Framework of Character Device Driver
The core of a Linux character device driver is the <span>file_operations</span> structure:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
// Device structure
struct my_device {
struct cdev cdev;
// Other device-specific data
};
// Open device
static int my_open(struct inode *inode, struct file *file) {
// Initialize device
return 0;
}
// Close device
static int my_release(struct inode *inode, struct file *file) {
// Clean up resources
return 0;
}
// Read data
static ssize_t my_read(struct file *file, char __user *buf,
size_t count, loff_t *pos) {
// Read data from device to user space
return count;
}
// Write data
static ssize_t my_write(struct file *file, const char __user *buf,
size_t count, loff_t *pos) {
// Write data from user space to device
return count;
}
// Control operations (ioctl)
static long my_ioctl(struct file *file, unsigned int cmd,
unsigned long arg) {
// Device-specific control operations
return 0;
}
// file_operations structure: defines operations supported by the driver
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release,
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
};
// Module initialization
static int __init my_init(void) {
// Register character device
// Create device node
return 0;
}
// Module exit
static void __exit my_exit(void) {
// Unregister device
// Remove device node
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
Understanding Key Concepts
1. file_operations Structure
- • This is the “interface contract” between the driver and the application program.
- • Applications access devices through system calls (like
<span>open</span>,<span>read</span>,<span>write</span>). - • The kernel routes these system calls to the corresponding
<span>file_operations</span>functions.
2. Data Exchange Between User Space and Kernel Space
// Copy data from kernel space to user space
copy_to_user(user_buf, kernel_buf, size);
// Copy data from user space to kernel space
copy_from_user(kernel_buf, user_buf, size);
Why are copy_to_user/copy_from_user needed?
- • User space and kernel space use different address mappings.
- • Cannot directly use pointers to access.
- • Requires safe copy functions provided by the kernel.
3. Device Nodes (/dev/xxx)
- • After the driver is registered, a device node will be created in the
<span>/dev</span>directory. - • Applications access the driver by opening this device file.
- • For example:
<span>/dev/gpio</span>,<span>/dev/led</span>, etc.
How Applications Use Drivers:
// Application code
int fd = open("/dev/mydevice", O_RDWR); // Open device
read(fd, buffer, size); // Read data
write(fd, data, size); // Write data
ioctl(fd, CMD, arg); // Control operation
close(fd); // Close device
Key Differences in Driver Development
| Aspect | MCU Driver | Linux Driver |
| Code Location | Application Layer | Kernel Layer |
| Access Method | Direct Function Call | Through File System |
| Privileges | Unlimited | Requires Kernel Privileges |
| Error Handling | Simple Return | Return Error Code |
| Concurrency Control | Usually Not Needed | Requires Mutexes, etc. |
| Code Volume | Tens of Lines | Hundreds to Thousands of Lines |
Practical Process of Driver Development
Step 1: Write Driver Code
- • Implement necessary functions in
<span>file_operations</span><span> structure.</span> - • Handle hardware initialization.
- • Implement data read/write logic.
Step 2: Compile Driver
# Write Makefile
obj-m += mydriver.o
# Compile
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
Step 3: Load Driver
# Load module
insmod mydriver.ko
# View loaded modules
lsmod
# Unload module
rmmod mydriver
Step 4: Create Device Node
# After loading the driver, you may need to manually create the device node
mknod /dev/mydevice c 250 0
# Or use udev to automatically create the device node
Step 5: Test Driver
// Write test program
int main() {
int fd = open("/dev/mydevice", O_RDWR);
// Test read/write operations
close(fd);
return 0;
}
Conclusion
Transitioning from MCU to Linux embedded development is not just about learning new technologies, but also a shift in mindset. MCU development emphasizes direct control and real-time response, while Linux development emphasizes system abstraction and resource management.