Breaking the Barrier from MCU to Linux Development

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. 1. Security: Programs cannot directly access other programs’ memory or hardware.
  2. 2. Multitasking: Each program thinks it is using the complete address space.
  3. 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. 1. Security: Prevents applications from damaging the system.
  2. 2. Stability: Application crashes do not affect the kernel.
  3. 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. 1. Performance Differences: PCs are powerful and compile quickly; target boards have limited resources and compile slowly.
  2. 2. Toolchain: The target board may not have a complete development tool.
  3. 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. 1. Provides System Commands: <span>ls</span>, <span>cd</span>, <span>cat</span>, etc.
  2. 2. Device Management: Access hardware through the <span>/dev</span> directory.
  3. 3. Configuration Management: Manage configurations through the <span>/etc</span> directory.
  4. 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. 1. Choose a base system (e.g., BusyBox).
  2. 2. Add necessary commands and tools.
  3. 3. Configure system services.
  4. 4. Add applications.
  5. 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. 1. Include only necessary functionalities: Reduce kernel size and boot time.
  2. 2. Drivers can be compiled as modules: Load when needed, unload when not needed.
  3. 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.

Leave a Comment