第四章 嵌入式平台适配:ARM/Linux驱动框架、设备树配置、DMA传输优化与缓存管理

做热成像系统,最头疼的往往不是传感器本身,而是怎么让ARM芯片跟它好好“说话”。我这些年调过的板子,少说也有七八种平台,从全志到瑞芯微,从NXP到TI,每个平台都有自己的脾气。但万变不离其宗——驱动框架、设备树、DMA、缓存管理,这四样东西搞明白了,基本上所有ARM平台都能拿下。

4.1 ARM/Linux驱动框架:从字符设备到V4L2

热成像传感器本质上就是个数据源。它不像摄像头那样有标准的CSI接口,很多都是SPI或并行接口输出原始温度数据。所以驱动怎么写,完全取决于你用什么接口。

最简单的做法:字符设备驱动

如果你只是想把数据读出来,字符设备驱动就够了。我早期做的一个项目,用的就是SPI接口的MLX90640,直接注册一个字符设备,open/read/ioctl三件套搞定。

static int thermal_open(struct inode *inode, struct file *filp) {
    // 初始化SPI、GPIO、时钟等
    return 0;
}

static ssize_t thermal_read(struct file *filp, char __user *buf,
                            size_t count, loff_t *f_pos) {
    // 读取一帧温度数据
    uint16_t frame[FRAME_SIZE];
    spi_read(thermal_spi, frame, FRAME_SIZE * 2);
    if (copy_to_user(buf, frame, count))
        return -EFAULT;
    return count;
}

static const struct file_operations thermal_fops = {
    .owner   = THIS_MODULE,
    .open    = thermal_open,
    .read    = thermal_read,
};

嗯,这里要注意:copy_to_user 是必须的,内核空间不能直接访问用户空间指针。我见过有人直接 memcpy 到用户空间,结果系统直接崩了。

进阶方案:V4L2 框架

如果你的热成像模组输出的是视频流(比如每秒30帧的热像视频),那最好用V4L2框架。V4L2的好处是上层应用可以直接用 ffmpeggstreamer 处理,省去很多麻烦。

关键点:V4L2 驱动需要实现 v4l2_file_operations 结构体,并注册 video_device。核心是 vidioc_dqbufvidioc_qbuf 这对缓冲区操作。

4.2 设备树配置:让内核认识你的硬件

设备树(Device Tree)说白了就是硬件的“身份证”。内核启动时通过它知道:哪个GPIO是复位脚、哪个SPI片选、中断号是多少。我刚开始接触设备树时,觉得这东西就是多此一举,直到有一次在BSP里改了个引脚配置,重新编译内核花了半小时...从那以后,我老老实实学设备树。

一个典型的热成像设备树节点:

thermal_sensor: thermal@0 {
    compatible = "mlx,mlx90640";
    reg = <0>;                    // SPI片选0
    spi-max-frequency = <20000000>; // 20MHz
    interrupt-parent = <&gpio1>;
    interrupts = <15 IRQ_TYPE_EDGE_FALLING>;
    reset-gpios = <&gpio2 5 GPIO_ACTIVE_LOW>;
    vdd-supply = <®_3v3>;
    status = "okay";
};

我的经验:设备树里最容易出错的是 interrupts 属性。不同芯片的 interrupt-parent 可能不同,有的用 &gic,有的用 &intc。建议先看芯片手册里的中断控制器章节。

设备树覆盖(Overlay)技巧

如果你在开发阶段,频繁改引脚配置,别每次都编译整个设备树。用设备树覆盖(overlay)机制,只加载修改的部分。我习惯把热成像的配置单独写一个 .dts 文件,然后 #include 到主设备树里。

4.3 DMA传输优化:别让CPU干苦力

热成像数据量其实不大,比如80×60像素的传感器,一帧也就9600字节。但如果你用SPI接口,每帧要传输19200次(16位数据),CPU如果逐字节读取,基本就干不了别的了。

为什么用DMA?

说白了,DMA就是让数据自己走,CPU只管发个“开始”指令,然后去干别的事。数据传完了,DMA控制器发个中断通知你。我做过一个对比测试:同样读取MLX90640,用CPU轮询方式,CPU占用率85%;用DMA方式,CPU占用率降到12%。

DMA驱动配置要点:

static int thermal_dma_init(struct platform_device *pdev) {
    struct dma_slave_config config;
    
    // 配置DMA通道
    config.direction = DMA_DEV_TO_MEM;
    config.src_addr = spi_phys_base + SPI_DR_REG;
    config.src_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES;
    config.src_maxburst = 16;
    
    dmaengine_slave_config(thermal_dma_chan, &config);
    
    // 准备DMA描述符
    thermal_dma_desc = dmaengine_prep_slave_single(
        thermal_dma_chan,
        dma_buf_phys,
        FRAME_SIZE * 2,
        DMA_DEV_TO_MEM,
        DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
    
    thermal_dma_desc->callback = thermal_dma_callback;
    thermal_dma_desc->callback_param = thermal_dev;
    
    return 0;
}

避坑指南:我曾经在配置DMA时,忘了设置 src_maxburst,结果每次只传2字节就触发一次中断,效率反而比轮询还低。burst size要根据SPI FIFO深度来设,一般设成FIFO深度的一半比较稳妥。

4.4 缓存管理:数据一致性的噩梦

DMA传输有个大坑——缓存一致性问题。CPU有L1/L2缓存,DMA直接读写内存,两边看到的数据可能不一样。你想想看,DMA已经把数据写到内存了,但CPU读到的还是缓存里的旧数据,这帧图像就花了。

解决方案:DMA一致性缓冲区

最直接的办法是用 dma_alloc_coherent 分配内存,这种内存不会被CPU缓存,DMA和CPU看到的是同一份数据。

thermal_dma_buf = dma_alloc_coherent(dev, 
                                     FRAME_SIZE * 2,
                                     &thermal_dma_phys,
                                     GFP_KERNEL);
if (!thermal_dma_buf) {
    dev_err(dev, "DMA buffer allocation failed\n");
    return -ENOMEM;
}

流式DMA映射:更灵活的方式

如果你不想用一致性缓冲区(比如缓冲区很大,或者需要频繁重新分配),可以用流式DMA映射。每次DMA传输前,调用 dma_map_single 并指定方向,传输完成后调用 dma_unmap_single

dma_addr_t dma_handle;
dma_handle = dma_map_single(dev, buffer, size, DMA_FROM_DEVICE);
// 启动DMA传输...
dma_unmap_single(dev, dma_handle, size, DMA_FROM_DEVICE);

关键区别:一致性缓冲区适合长期使用的缓冲区(比如帧缓冲区),流式映射适合临时传输(比如每次读取一帧数据)。我个人的习惯是:帧缓冲区用一致性,控制命令用流式映射。

4.5 实战经验:一个完整的DMA传输流程

说了这么多理论,咱们看一个实际的热成像DMA传输流程。这是我做的一个项目,传感器是80×64像素,SPI接口,每秒30帧。

  1. 初始化阶段:分配DMA一致性缓冲区,配置SPI控制器为DMA模式
  2. 启动传输:CPU写SPI控制寄存器,触发一次DMA传输
  3. DMA传输中:CPU可以去做其他事,比如处理上一帧数据
  4. 传输完成:DMA中断触发,CPU从缓冲区读取数据
  5. 数据后处理:温度换算、坏点校正、伪彩色映射
static irqreturn_t thermal_dma_callback(void *param) {
    struct thermal_dev *tdev = (struct thermal_dev *)param;
    
    // 从DMA缓冲区读取数据
    memcpy(tdev->frame, tdev->dma_buf, FRAME_SIZE * 2);
    
    // 通知应用层有新数据
    wake_up_interruptible(&tdev->read_wait);
    
    // 启动下一次DMA传输
    thermal_start_dma(tdev);
    
    return IRQ_HANDLED;
}

嗯,这里有个细节:中断处理函数里要尽量少做事。我一般只做数据拷贝和唤醒等待队列,温度换算这些重活放到内核线程或用户空间去做。

4.6 性能调优:让每一帧都不丢

热成像系统最怕丢帧。如果DMA传输速度跟不上传感器输出速度,数据就会覆盖。我遇到过一个问题:传感器输出30帧/秒,但DMA传输一帧要40ms,结果每3帧就丢1帧。

解决方案:双缓冲(Double Buffering)

用两个DMA缓冲区,一个在传输,一个在处理。这样传输和处理可以流水线进行。

struct thermal_dma_buf {
    void *cpu_addr;
    dma_addr_t dma_addr;
    size_t size;
    bool busy;
};

// 两个缓冲区交替使用
struct thermal_dma_buf buf[2];
int current_buf = 0;

static void thermal_dma_callback(void *param) {
    // 标记当前缓冲区可用
    buf[current_buf].busy = false;
    
    // 切换到另一个缓冲区
    current_buf ^= 1;
    
    // 如果另一个缓冲区空闲,启动传输
    if (!buf[current_buf].busy) {
        buf[current_buf].busy = true;
        thermal_start_dma(&buf[current_buf]);
    }
}

我的建议:双缓冲的缓冲区大小要设成帧大小的整数倍。我习惯设成2帧大小,这样即使偶尔延迟,也不会丢数据。当然,内存占用会翻倍,但现在的ARM芯片内存都够用。

4.7 总结:一个清晰的适配流程

最后,我用一张图总结一下整个嵌入式平台适配的流程。这张图是我自己画的,每次做新平台适配时都会参考。

嵌入式平台适配流程 1. 硬件初始化 GPIO/SPI/时钟配置 2. 设备树配置 中断/DMA/电源管理 3. 驱动框架 字符设备/V4L2 4. DMA传输优化 通道配置/双缓冲/中断 5. 缓存管理 一致性/流式映射 6. 性能调优 丢帧检测/延迟 7. 验证测试 数据完整性/帧率 虚线表示迭代优化,实际项目中往往需要多次循环

这张图展示了我做平台适配时的完整流程。从硬件初始化开始,到设备树配置、驱动框架选择、DMA优化、缓存管理,最后是性能调优和验证。虚线表示如果验证不通过,需要回到前面的步骤重新调整。

说实话,这个流程我走了不下十遍。每次遇到新平台,都是先搭好基础框架,然后一点点优化。最怕的是上来就想一步到位,结果某个环节出问题,排查起来特别费劲。

核心要点回顾:

  • 驱动框架选型:简单数据用字符设备,视频流用V4L2
  • 设备树配置:注意中断控制器和GPIO的引用方式
  • DMA优化:双缓冲是解决丢帧问题的利器
  • 缓存管理:一致性缓冲区 vs 流式映射,按需选择

好了,这一章的内容就到这里。记住,嵌入式平台适配没有银弹,每个平台都有自己的坑。但只要你把驱动框架、设备树、DMA和缓存管理这四块吃透了,遇到任何新平台都能快速上手。


公众号:蓝海资料掘金营,微信deep3321