1. 交叉编译环境搭建

嵌入式开发最核心的概念就是交叉编译——在PC上编写代码,然后编译成能在ARM开发板上运行的二进制程序。这就像在中国写好了菜谱,然后送到美国的厨房去做菜,需要确保那边的厨具和食材都能对得上。

1.1 安装必备工具链

首先确保你的Ubuntu系统已经安装了基础开发工具。打开终端(Ctrl+Alt+T),执行以下命令更新软件源并安装必备组件:

sudo apt update
sudo apt install build-essential
sudo apt install ubuntu-make

这些基础包包含了gcc、g++、make等核心开发工具。验证安装是否成功:

gcc --version
g++ --version
make --version

如果看到版本信息输出,说明基础环境已经就绪。我刚开始玩嵌入式的时候,曾经因为漏装这些基础包折腾了一整天,编译时各种莫名其妙的错误,所以这一步千万别跳过。

1.2 获取交叉编译工具链

友善之臂官方提供了专门的交叉编译工具链。你可以从官网下载,或者使用我提供的百度网盘链接(提取码:yccy)。下载完成后,你会得到一个名为arm-linux-gcc-4.6.4-arm-x86_64.tar.bz2的压缩包。

将下载的文件放到桌面,然后在终端中进入桌面目录:

cd ~/Desktop

给压缩包添加执行权限并解压:

sudo chmod 777 arm-linux-gcc-4.6.4-arm-x86_64.tar.bz2
tar -jxvf arm-linux-gcc-4.6.4-arm-x86_64.tar.bz2

解压后会得到一个gcc-4.6.4的目录,这就是我们的交叉编译工具链。

1.3 配置环境变量

为了让系统能找到我们的交叉编译工具,需要将其路径添加到环境变量中。创建工具链的安装目录:

sudo mkdir /usr/local/arm
sudo chmod 777 /usr/local/arm

将解压得到的gcc-4.6.4目录复制到刚创建的目录中:

sudo cp -r gcc-4.6.4 /usr/local/arm/

接下来编辑系统环境变量配置文件:

sudo gedit /etc/profile

在文件末尾添加以下两行内容:

export PATH=$PATH:/usr/local/arm/gcc-4.6.4/bin
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/arm/gcc-4.6.4/lib

第一行添加执行程序路径,第二行添加库文件路径。保存退出后,使用source命令使配置立即生效:

source /etc/profile

1.4 验证安装结果

现在来验证交叉编译工具链是否安装成功:

arm-none-linux-gnueabi-gcc -v

如果看到输出类似"gcc version 4.6.4"的版本信息,恭喜你!交叉编译环境已经配置成功。我第一次看到这个输出时简直欣喜若狂,毕竟这意味着终于可以开始真正的嵌入式开发了。

2. 驱动开发基础入门

驱动开发是嵌入式Linux中最有意思的部分,它让你能够直接与硬件对话。就像学会了一门外语,可以直接和当地人交流一样。

2.1 Linux驱动模型理解

Linux驱动本质上是一个内核模块,它可以动态加载和卸载。与普通应用程序不同,驱动运行在内核空间,具有更高的权限,但也更容易导致系统崩溃。

驱动的主要作用包括:

  • 设备抽象:为应用程序提供统一的设备访问接口
  • 硬件操作:直接读写硬件寄存器
  • 中断处理:响应硬件中断事件
  • 数据传输:管理设备与内存之间的数据流动

在开始编写驱动前,建议先查看Tiny4412的原理图,找到LED对应的GPIO引脚。通常LED会连接到GPX2系列的引脚上,但具体要看你的板子版本。

2.2 第一个LED驱动编写

创建一个名为led_driver.c的文件,输入以下代码:

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/io.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "led_demo"
#define CLASS_NAME "led"

static int major_num;
static struct class* led_class = NULL;
static struct device* led_device = NULL;
static void __iomem *GPX2CON;
static void __iomem *GPX2DAT;

static int led_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "LED device opened\n");
    return 0;
}

static int led_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "LED device closed\n");
    return 0;
}

static long led_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    switch(cmd) {
        case 0: // LED off
            writel(readl(GPX2DAT) & ~(1 << arg), GPX2DAT);
            break;
        case 1: // LED on
            writel(readl(GPX2DAT) | (1 << arg), GPX2DAT);
            break;
    }
    return 0;
}

static struct file_operations fops = {
    .open = led_open,
    .release = led_release,
    .unlocked_ioctl = led_ioctl,
};

static int __init led_init(void)
{
    // 动态申请设备号
    major_num = register_chrdev(0, DEVICE_NAME, &fops);
    if (major_num < 0) {
        printk(KERN_ALERT "Failed to register device\n");
        return major_num;
    }
    
    // 创建设备类
    led_class = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(led_class)) {
        unregister_chrdev(major_num, DEVICE_NAME);
        return PTR_ERR(led_class);
    }
    
    // 创建设备节点
    led_device = device_create(led_class, NULL, MKDEV(major_num, 0), NULL, DEVICE_NAME);
    if (IS_ERR(led_device)) {
        class_destroy(led_class);
        unregister_chrdev(major_num, DEVICE_NAME);
        return PTR_ERR(led_device);
    }
    
    // 映射GPIO寄存器
    GPX2CON = ioremap(0x11000C40, 0x20);
    GPX2DAT = GPX2CON + 0x04;
    
    // 配置GPIO为输出模式
    writel((readl(GPX2CON) & ~(0xF << 0)) | (0x1 << 0), GPX2CON);
    
    printk(KERN_INFO "LED driver initialized\n");
    return 0;
}

static void __exit led_exit(void)
{
    iounmap(GPX2CON);
    device_destroy(led_class, MKDEV(major_num, 0));
    class_destroy(led_class);
    unregister_chrdev(major_num, DEVICE_NAME);
    printk(KERN_INFO "LED driver removed\n");
}

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple LED Driver for Tiny4412");

这个驱动实现了最基本的LED控制功能。它创建了一个字符设备,应用程序可以通过ioctl系统调用来控制LED的亮灭。

2.3 编写Makefile

创建一个对应的Makefile来编译这个驱动:

obj-m := led_driver.o

KDIR := /path/to/your/kernel/source
PWD := $(shell pwd)

all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean

注意要将KDIR替换为你实际的内核源码路径。Tiny4412通常使用Linux 3.5内核,你需要先编译好内核源码。

3. 实战编译与测试

理论学得再多不如动手实践一次。让我们完整走一遍从编译到测试的整个流程。

3.1 编译内核模块

在终端中进入驱动源码所在目录,执行编译命令:

make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi-

如果一切顺利,你会看到生成了一个led_driver.ko文件。这就是我们的内核模块文件。第一次编译时可能会遇到各种依赖问题,需要确保内核源码配置正确。

3.2 加载驱动模块

将编译好的ko文件拷贝到开发板上,可以使用U盘、SD卡或者网络传输。在开发板上执行:

insmod led_driver.ko

使用dmesg命令查看内核日志,应该能看到"LED driver initialized"的输出。还可以检查/dev目录下是否出现了led_demo设备节点:

ls /dev/led_demo

3.3 编写测试应用程序

创建一个简单的测试程序来验证驱动是否正常工作:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

int main()
{
    int fd = open("/dev/led_demo", O_RDWR);
    if (fd < 0) {
        perror("Failed to open device");
        return -1;
    }
    
    // 闪烁LED
    for (int i = 0; i < 5; i++) {
        ioctl(fd, 1, 0); // 点亮LED
        sleep(1);
        ioctl(fd, 0, 0); // 熄灭LED
        sleep(1);
    }
    
    close(fd);
    return 0;
}

使用交叉编译工具编译这个测试程序:

arm-none-linux-gnueabi-gcc -o led_test led_test.c

将生成的可执行文件拷贝到开发板上运行,你应该能看到LED开始闪烁。这一刻的成就感是无与伦比的——你终于让硬件听从你的指令了!

4. 常见问题与调试技巧

驱动开发过程中难免会遇到各种问题,这里分享一些我踩过的坑和解决方法。

4.1 编译常见错误

头文件找不到:确保内核源码路径正确,并且已经配置好内核。可以尝试先编译内核本身,确保内核编译环境正常。

权限问题:加载驱动需要root权限,确保你在开发板上使用sudo或者直接以root用户操作。

版本不匹配:驱动模块必须与运行的内核版本完全匹配,否则无法加载。使用uname -r查看开发板内核版本,然后用相同版本的内核源码进行编译。

4.2 调试技巧

printk调试:在内核代码中添加printk语句是最直接的调试方法。通过dmesg可以查看输出信息。

使用/proc文件系统:可以通过/proc/interrupts查看中断信息,/proc/iomem查看内存映射情况。

硬件排查:如果软件层面找不到问题,别忘了检查硬件连接。用万用表测量GPIO引脚电压,确保硬件电路正常工作。

逐步验证:从最简单的代码开始,逐步添加功能。先确保能正确加载模块,再添加设备节点创建,最后实现具体的硬件操作。

记得我第一次写驱动时,花了三天时间才发现是硬件接触不良,所以硬件问题永远不能忽视。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐