Linux DMA(三)

3. 设备树覆盖 (dts/spi_oled_overlay.dts)

/*
 * SPI OLED DMA教学驱动 - 设备树覆盖
 *
 * 📚 设备树中DMA相关属性说明:
 *   dmas: 指定DMA通道 <&dma控制器 通道号>
 *   dma-names: DMA通道名称,驱动中用dma_request_chan("tx")匹配
 *
 * 使用方法 (树莓派为例):
 *   dtc -@ -I dts -O dtb -o spi_oled.dtbo spi_oled_overlay.dts
 *   sudo cp spi_oled.dtbo /boot/overlays/
 *   在config.txt中添加: dtoverlay=spi_oled
 */
/dts-v1/;
/plugin/;

/ {
    compatible = "brcm,bcm2835";  /* 树莓派 */

    fragment@0 {
        target = <&spi0>;
        __overlay__ {
            status = "okay";

            #address-cells = <1>;
            #size-cells = <0>;

            oled: oled@0 {
                compatible = "learn,spi-oled-dma";
                reg = <0>;                      /* CS0 */
                spi-max-frequency = <10000000>; /* 10MHz */

                /*
                 * 📚 GPIO引脚定义
                 * dc-gpios: 数据/命令选择引脚
                 *   0 = 命令模式
                 *   1 = 数据模式
                 * reset-gpios: 硬件复位引脚(可选)
                 */
                dc-gpios = <&gpio 24 0>;        /* GPIO24, 低电平有效 */
                reset-gpios = <&gpio 25 0>;     /* GPIO25, 低电平有效 */

                /*
                 * 📚 DMA通道配置
                 *
                 * 在支持DMA的SPI控制器中,DMA通道通常在
                 * SPI控制器节点中定义,而不是在子设备中。
                 *
                 * 例如bcm2835 SPI控制器:
                 *   &spi0 {
                 *       dmas = <&dma 6>, <&dma 7>;
                 *       dma-names = "tx", "rx";
                 *   };
                 *
                 * SPI控制器驱动会自动使用这些DMA通道
                 * 我们的驱动只需要设置 is_dma_mapped 标志
                 *
                 * 如果想在驱动中直接请求DMA通道:
                 * dmas = <&dma 6>;
                 * dma-names = "tx";
                 */

                /* 显示参数(可选,驱动有默认值) */
                width = <128>;
                height = <64>;
                solomon,com-pins-hardware-config = <0x12>;
            };
        };
    };

    /*
     * 📚 SPI控制器的DMA配置(参考)
     *
     * 不同平台的SPI控制器DMA配置不同,以下是几个常见平台:
     *
     * 树莓派 (BCM2835):
     *   &spi0 {
     *       dmas = <&dma 6>, <&dma 7>;
     *       dma-names = "tx", "rx";
     *   };
     *
     * STM32:
     *   &spi1 {
     *       dmas = <&dma1 3 3 0x400 0x05>,
     *              <&dma1 2 3 0x400 0x05>;
     *       dma-names = "tx", "rx";
     *   };
     *
     * i.MX6:
     *   &ecspi1 {
     *       dmas = <&sdma 3 7 1>, <&sdma 4 7 2>;
     *       dma-names = "tx", "rx";
     *   };
     *
     * DMA通道参数的含义因DMA控制器不同而异:
     *   通常包含: 通道号、请求线、优先级、配置标志等
     */
};

4. Makefile

# SPI OLED DMA 教学驱动 Makefile
#
# 编译方法:
#   本机编译:  make
#   交叉编译:  make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
#   指定内核:  make KDIR=/path/to/kernel/source
#   编译设备树: make dtbo
#   清理:      make clean

# 驱动模块名称
obj-m += spi-oled-dma.o

# 多文件模块(如果需要拆分)
# spi-oled-dma-y := spi_oled_dma.o spi_oled_fb.o

# 内核源码路径
KDIR ?= /lib/modules/$(shell uname -r)/build

# 当前目录
PWD := $(shell pwd)

# DTC (设备树编译器)
DTC ?= dtc

# 额外编译标志
ccflags-y += -DDEBUG
# 启用DMA调试(推荐学习时开启)
# ccflags-y += -DCONFIG_DMA_API_DEBUG

.PHONY: all clean modules dtbo install help

all: modules

modules:
    @echo "╔══════════════════════════════════════╗"
    @echo "║  编译 SPI OLED DMA 教学驱动         ║"
    @echo "╚══════════════════════════════════════╝"
    $(MAKE) -C $(KDIR) M=$(PWD) modules

dtbo: dts/spi_oled_overlay.dts
    @echo "编译设备树覆盖..."
    $(DTC) -@ -I dts -O dtb -o spi_oled.dtbo dts/spi_oled_overlay.dts

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean
    rm -f *.dtbo

install: modules dtbo
    @echo "安装驱动模块..."
    sudo cp spi-oled-dma.ko /lib/modules/$(shell uname -r)/extra/
    sudo depmod -a
    @echo "安装设备树覆盖..."
    sudo cp spi_oled.dtbo /boot/overlays/ 2>/dev/null || true
    @echo "安装完成!"

help:
    @echo ""
    @echo "=== SPI OLED DMA 教学驱动 ==="
    @echo ""
    @echo "编译目标:"
    @echo "  make          - 编译驱动模块"
    @echo "  make dtbo     - 编译设备树覆盖"
    @echo "  make install  - 安装驱动和设备树"
    @echo "  make clean    - 清理编译产物"
    @echo ""
    @echo "加载驱动:"
    @echo "  sudo insmod spi-oled-dma.ko                  # 默认Coherent DMA模式"
    @echo "  sudo insmod spi-oled-dma.ko dma_mode=0       # Coherent DMA"
    @echo "  sudo insmod spi-oled-dma.ko dma_mode=1       # Streaming DMA"
    @echo "  sudo insmod spi-oled-dma.ko dma_mode=2       # Scatter-Gather DMA"
    @echo "  sudo insmod spi-oled-dma.ko dma_mode=3       # PIO (无DMA对比)"
    @echo ""
    @echo "查看DMA信息:"
    @echo "  cat /sys/devices/.../dma_tutorial    # DMA学习指南"
    @echo "  cat /sys/devices/.../dma_stats       # DMA统计"
    @echo "  cat /sys/devices/.../dma_addresses   # DMA地址映射"
    @echo "  echo N > /sys/devices/.../dma_mode   # 切换DMA模式"
    @echo ""
    @echo "内核DMA调试:"
    @echo "  echo 'file spi_oled_dma.c +p' > /sys/kernel/debug/dynamic_debug/control"
    @echo "  cat /sys/kernel/debug/dma-api/errors  # DMA API错误"
    @echo ""

5. 用户空间测试程序 (test/test_oled.c)

/*
 * test_oled.c - SPI OLED DMA驱动用户空间测试程序
 *
 * 编译: gcc -o test_oled test_oled.c -lm
 * 运行: sudo ./test_oled /dev/fb0
 *
 * 功能:
 *   1. 向framebuffer写入测试图案
 *   2. 切换DMA模式并对比性能
 *   3. 读取DMA统计信息
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <linux/fb.h>
#include <time.h>
#include <math.h>
#include <dirent.h>

#define OLED_WIDTH  128
#define OLED_HEIGHT 64
#define FB_SIZE     (OLED_WIDTH * OLED_HEIGHT / 8)  /* 1024 bytes */

/* sysfs路径查找 */
static char sysfs_path[512] = {0};

/**
 * find_sysfs_path - 查找驱动的sysfs路径
 */
static int find_sysfs_path(void)
{
    FILE *fp;
    char cmd[] = "find /sys/devices -name 'dma_mode' -path '*spi*' 2>/dev/null | head -1";
    char result[512];

    fp = popen(cmd, "r");
    if (!fp)
        return -1;

    if (fgets(result, sizeof(result), fp)) {
        /* 去掉 /dma_mode 后缀和换行 */
        char *p = strrchr(result, '/');
        if (p) *p = '\0';
        p = strchr(result, '\n');
        if (p) *p = '\0';

        strncpy(sysfs_path, result, sizeof(sysfs_path) - 1);
        pclose(fp);
        return 0;
    }

    pclose(fp);
    return -1;
}

/**
 * read_sysfs - 读取sysfs文件内容
 */
static int read_sysfs(const char *attr, char *buf, size_t len)
{
    char path[600];
    int fd, n;

    snprintf(path, sizeof(path), "%s/%s", sysfs_path, attr);
    fd = open(path, O_RDONLY);
    if (fd < 0) {
        perror(path);
        return -1;
    }

    n = read(fd, buf, len - 1);
    close(fd);
    if (n > 0) buf[n] = '\0';
    return n;
}

/**
 * write_sysfs - 写入sysfs文件
 */
static int write_sysfs(const char *attr, const char *value)
{
    char path[600];
    int fd, ret;

    snprintf(path, sizeof(path), "%s/%s", sysfs_path, attr);
    fd = open(path, O_WRONLY);
    if (fd < 0) {
        perror(path);
        return -1;
    }

    ret = write(fd, value, strlen(value));
    close(fd);
    return ret;
}

/**
 * set_pixel - 在framebuffer中设置一个像素
 */
static void set_pixel(unsigned char *fb, int x, int y, int on)
{
    if (x < 0 || x >= OLED_WIDTH || y < 0 || y >= OLED_HEIGHT)
        return;

    int byte_idx = y * (OLED_WIDTH / 8) + (x / 8);
    int bit_idx = 7 - (x % 8);

    if (on)
        fb[byte_idx] |= (1 << bit_idx);
    else
        fb[byte_idx] &= ~(1 << bit_idx);
}

/**
 * draw_line - Bresenham直线算法
 */
static void draw_line(unsigned char *fb, int x0, int y0, int x1, int y1)
{
    int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
    int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
    int err = dx + dy, e2;

    for (;;) {
        set_pixel(fb, x0, y0, 1);
        if (x0 == x1 && y0 == y1) break;
        e2 = 2 * err;
        if (e2 >= dy) { err += dy; x0 += sx; }
        if (e2 <= dx) { err += dx; y0 += sy; }
    }
}

/**
 * draw_rect - 画矩形
 */
static void draw_rect(unsigned char *fb, int x, int y, int w, int h)
{
    draw_line(fb, x, y, x + w - 1, y);
    draw_line(fb, x + w - 1, y, x + w - 1, y + h - 1);
    draw_line(fb, x + w - 1, y + h - 1, x, y + h - 1);
    draw_line(fb, x, y + h - 1, x, y);
}

/**
 * draw_circle - 画圆
 */
static void draw_circle(unsigned char *fb, int cx, int cy, int r)
{
    int x = r, y = 0, err = 0;

    while (x >= y) {
        set_pixel(fb, cx + x, cy + y, 1);
        set_pixel(fb, cx + y, cy + x, 1);
        set_pixel(fb, cx - y, cy + x, 1);
        set_pixel(fb, cx - x, cy + y, 1);
        set_pixel(fb, cx - x, cy - y, 1);
        set_pixel(fb, cx - y, cy - x, 1);
        set_pixel(fb, cx + y, cy - x, 1);
        set_pixel(fb, cx + x, cy - y, 1);

        if (err <= 0) {
            y += 1;
            err += 2 * y + 1;
        }
        if (err > 0) {
            x -= 1;
            err -= 2 * x + 1;
        }
    }
}

/* 简易5x7字体 (部分字符) */
static const unsigned char font5x7[][5] = {
    ['D'] = {0x7F, 0x41, 0x41, 0x22, 0x1C},
    ['M'] = {0x7F, 0x02, 0x0C, 0x02, 0x7F},
    ['A'] = {0x7E, 0x09, 0x09, 0x09, 0x7E},
    ['T'] = {0x01, 0x01, 0x7F, 0x01, 0x01},
    ['E'] = {0x7F, 0x49, 0x49, 0x49, 0x41},
    ['S'] = {0x26, 0x49, 0x49, 0x49, 0x32},
    ['O'] = {0x3E, 0x41, 0x41, 0x41, 0x3E},
    ['K'] = {0x7F, 0x08, 0x14, 0x22, 0x41},
    ['!'] = {0x00, 0x00, 0x5F, 0x00, 0x00},
    [' '] = {0x00, 0x00, 0x00, 0x00, 0x00},
    ['0'] = {0x3E, 0x51, 0x49, 0x45, 0x3E},
    ['1'] = {0x00, 0x42, 0x7F, 0x40, 0x00},
    ['2'] = {0x42, 0x61, 0x51, 0x49, 0x46},
    ['3'] = {0x21, 0x41, 0x45, 0x4B, 0x31},
};

/**
 * draw_char - 画一个字符
 */
static void draw_char(unsigned char *fb, int x, int y, char c)
{
    int i, j;
    unsigned char idx = (unsigned char)c;

    if (idx >= sizeof(font5x7) / sizeof(font5x7[0]))
        return;

    for (i = 0; i < 5; i++) {
        for (j = 0; j < 7; j++) {
            if (font5x7[idx][i] & (1 << j))
                set_pixel(fb, x + i, y + j, 1);
        }
    }
}

/**
 * draw_string - 画字符串
 */
static void draw_string(unsigned char *fb, int x, int y, const char *str)
{
    while (*str) {
        draw_char(fb, x, y, *str);
        x += 6;
        str++;
    }
}

/**
 * test_pattern_checkerboard - 棋盘格测试图案
 */
static void test_pattern_checkerboard(unsigned char *fb)
{
    int x, y;

    memset(fb, 0, FB_SIZE);
    for (y = 0; y < OLED_HEIGHT; y++) {
        for (x = 0; x < OLED_WIDTH; x++) {
            if ((x / 8 + y / 8) % 2)
                set_pixel(fb, x, y, 1);
        }
    }
}

/**
 * test_pattern_shapes - 几何图形测试图案
 */
static void test_pattern_shapes(unsigned char *fb)
{
    memset(fb, 0, FB_SIZE);

    /* 边框 */
    draw_rect(fb, 0, 0, OLED_WIDTH, OLED_HEIGHT);

    /* 圆 */
    draw_circle(fb, 32, 32, 20);

    /* 矩形 */
    draw_rect(fb, 70, 10, 40, 30);

    /* 对角线 */
    draw_line(fb, 70, 10, 110, 40);
    draw_line(fb, 110, 10, 70, 40);

    /* 文字 */
    draw_string(fb, 10, 55, "DMA TEST OK!");
}

/**
 * test_pattern_animation - 动画测试(用于压力测试DMA)
 */
static void test_pattern_animation(unsigned char *fb, int frame)
{
    int x, y;
    double angle = frame * 0.1;

    memset(fb, 0, FB_SIZE);

    /* 旋转线条 */
    int cx = OLED_WIDTH / 2;
    int cy = OLED_HEIGHT / 2;
    int len = 25;

    for (int i = 0; i < 6; i++) {
        double a = angle + i * M_PI / 3;
        int x1 = cx + (int)(len * cos(a));
        int y1 = cy + (int)(len * sin(a));
        draw_line(fb, cx, cy, x1, y1);
    }

    /* 移动的圆 */
    int bx = (int)(64 + 50 * cos(angle * 0.7));
    int by = (int)(32 + 20 * sin(angle * 1.3));
    draw_circle(fb, bx, by, 8);

    /* 帧计数 */
    char buf[20];
    snprintf(buf, sizeof(buf), "%03d", frame % 1000);
    draw_string(fb, 0, 0, buf);
}

/**
 * benchmark_dma_mode - 基准测试某个DMA模式
 */
static void benchmark_dma_mode(int fb_fd, unsigned char *fb_mmap,
                                int mode, int frames)
{
    char mode_str[4];
    char stats_buf[1024];
    struct timespec start, end;
    double elapsed;
    int i;

    printf("\n  --- 测试DMA模式 %d ---\n", mode);

    /* 切换DMA模式 */
    snprintf(mode_str, sizeof(mode_str), "%d", mode);
    write_sysfs("dma_mode", mode_str);
    usleep(100000);  /* 等待模式切换完成 */

    /* 运行动画帧 */
    clock_gettime(CLOCK_MONOTONIC, &start);

    for (i = 0; i < frames; i++) {
        test_pattern_animation(fb_mmap, i);

        /* 写入framebuffer触发刷新 */
        lseek(fb_fd, 0, SEEK_SET);
        if (write(fb_fd, fb_mmap, FB_SIZE) != FB_SIZE) {
            perror("write fb");
            break;
        }

        usleep(10000);  /* 10ms间隔 */
    }

    clock_gettime(CLOCK_MONOTONIC, &end);
    elapsed = (end.tv_sec - start.tv_sec) +
              (end.tv_nsec - start.tv_nsec) / 1e9;

    printf("  帧数: %d, 耗时: %.2f秒, FPS: %.1f\n",
           frames, elapsed, frames / elapsed);

    /* 读取DMA统计 */
    if (read_sysfs("dma_stats", stats_buf, sizeof(stats_buf)) > 0) {
        printf("  DMA统计:\n%s\n", stats_buf);
    }
}

/**
 * print_usage - 打印使用说明
 */
static void print_usage(const char *prog)
{
    printf("\n");
    printf("╔══════════════════════════════════════════════════════╗\n");
    printf("║  SPI OLED DMA 教学驱动 - 测试工具                  ║\n");
    printf("╠══════════════════════════════════════════════════════╣\n");
    printf("║                                                      ║\n");
    printf("║  用法: %s <命令> [参数]                       ║\n", prog);
    printf("║                                                      ║\n");
    printf("║  命令:                                               ║\n");
    printf("║    pattern <fb>    - 显示测试图案                    ║\n");
    printf("║    checker <fb>    - 显示棋盘格                      ║\n");
    printf("║    animate <fb> N  - 运行N帧动画                    ║\n");
    printf("║    benchmark <fb>  - DMA模式性能对比                 ║\n");
    printf("║    info            - 显示DMA信息                     ║\n");
    printf("║    tutorial        - 显示DMA教程                     ║\n");
    printf("║    mode <N>        - 切换DMA模式(0-3)                ║\n");
    printf("║                                                      ║\n");
    printf("║  示例:                                               ║\n");
    printf("║    %s pattern /dev/fb0                        ║\n", prog);
    printf("║    %s benchmark /dev/fb0                      ║\n", prog);
    printf("║    %s mode 1                                  ║\n", prog);
    printf("║                                                      ║\n");
    printf("╚══════════════════════════════════════════════════════╝\n");
    printf("\n");
}

int main(int argc, char *argv[])
{
    int fb_fd = -1;
    unsigned char fb_buf[FB_SIZE];
    char sysfs_buf[4096];

    if (argc < 2) {
        print_usage(argv[0]);
        return 1;
    }

    /* 查找sysfs路径 */
    if (find_sysfs_path() != 0) {
        printf("⚠️  未找到SPI OLED DMA驱动的sysfs路径\n");
        printf("    确保驱动已加载: lsmod | grep spi_oled\n");
        printf("    部分功能可能不可用\n\n");
    } else {
        printf("找到驱动sysfs路径: %s\n", sysfs_path);
    }

    /* 命令: info */
    if (strcmp(argv[1], "info") == 0) {
        printf("\n=== DMA信息 ===\n");

        if (read_sysfs("dma_stats", sysfs_buf, sizeof(sysfs_buf)) > 0)
            printf("%s\n", sysfs_buf);

        if (read_sysfs("dma_addresses", sysfs_buf, sizeof(sysfs_buf)) > 0)
            printf("%s\n", sysfs_buf);

        return 0;
    }

    /* 命令: tutorial */
    if (strcmp(argv[1], "tutorial") == 0) {
        if (read_sysfs("dma_tutorial", sysfs_buf, sizeof(sysfs_buf)) > 0)
            printf("%s\n", sysfs_buf);
        return 0;
    }

    /* 命令: mode */
    if (strcmp(argv[1], "mode") == 0) {
        if (argc < 3) {
            printf("用法: %s mode <0-3>\n", argv[0]);
            return 1;
        }
        write_sysfs("dma_mode", argv[2]);
        printf("DMA模式已切换到: %s\n", argv[2]);

        if (read_sysfs("dma_addresses", sysfs_buf, sizeof(sysfs_buf)) > 0)
            printf("\n%s\n", sysfs_buf);

        return 0;
    }

    /* 以下命令需要framebuffer设备 */
    if (argc < 3) {
        print_usage(argv[0]);
        return 1;
    }

    fb_fd = open(argv[2], O_RDWR);
    if (fb_fd < 0) {
        perror("打开framebuffer失败");
        printf("提示: 确保驱动已加载并且framebuffer设备存在\n");
        printf("  ls -la /dev/fb*\n");
        return 1;
    }

    /* 命令: pattern */
    if (strcmp(argv[1], "pattern") == 0) {
        printf("绘制测试图案...\n");
        test_pattern_shapes(fb_buf);

        lseek(fb_fd, 0, SEEK_SET);
        if (write(fb_fd, fb_buf, FB_SIZE) != FB_SIZE) {
            perror("写入framebuffer");
        } else {
            printf("测试图案已发送到OLED\n");
        }
    }

    /* 命令: checker */
    else if (strcmp(argv[1], "checker") == 0) {
        printf("绘制棋盘格...\n");
        test_pattern_checkerboard(fb_buf);

        lseek(fb_fd, 0, SEEK_SET);
        if (write(fb_fd, fb_buf, FB_SIZE) != FB_SIZE) {
            perror("写入framebuffer");
        } else {
            printf("棋盘格已发送到OLED\n");
        }
    }

    /* 命令: animate */
    else if (strcmp(argv[1], "animate") == 0) {
        int frames = 100;
        if (argc >= 4)
            frames = atoi(argv[3]);

        printf("运行 %d 帧动画...\n", frames);

        for (int i = 0; i < frames; i++) {
            test_pattern_animation(fb_buf, i);

            lseek(fb_fd, 0, SEEK_SET);
            write(fb_fd, fb_buf, FB_SIZE);

            usleep(33000);  /* ~30fps */

            /* 每秒打印一次状态 */
            if (i > 0 && i % 30 == 0) {
                printf("  帧: %d/%d\n", i, frames);
            }
        }
        printf("动画完成\n");
    }

    /* 命令: benchmark */
    else if (strcmp(argv[1], "benchmark") == 0) {
        int frames = 50;

        printf("\n╔══════════════════════════════════════╗\n");
        printf("║  DMA模式性能基准测试                 ║\n");
        printf("╚══════════════════════════════════════╝\n");
        printf("\n每种模式运行 %d 帧:\n", frames);

        unsigned char *tmp_fb = malloc(FB_SIZE);
        if (!tmp_fb) {
            perror("malloc");
            close(fb_fd);
            return 1;
        }

        /* 测试每种DMA模式 */
        const char *mode_names[] = {
            "Coherent DMA",
            "Streaming DMA",
            "Scatter-Gather DMA",
            "PIO (无DMA)"
        };

        for (int mode = 0; mode <= 3; mode++) {
            printf("\n══ 模式 %d: %s ══\n", mode, mode_names[mode]);
            benchmark_dma_mode(fb_fd, tmp_fb, mode, frames);
        }

        free(tmp_fb);

        printf("\n╔══════════════════════════════════════╗\n");
        printf("║  基准测试完成                        ║\n");
        printf("║                                      ║\n");
        printf("║  📚 观察要点:                        ║\n");
        printf("║  1. DMA模式vs PIO的传输时间差异      ║\n");
        printf("║  2. 不同DMA模式间的性能差异          ║\n");
        printf("║  3. DMA传输期间CPU是否更空闲         ║\n");
        printf("║     (可用top/htop观察CPU使用率)      ║\n");
        printf("╚══════════════════════════════════════╝\n");
    }

    else {
        printf("未知命令: %s\n", argv[1]);
        print_usage(argv[0]);
    }

    if (fb_fd >= 0)
        close(fb_fd);

    return 0;
}

6. DMA概念总结图

📚 DMA 完整知识体系总结

╔══════════════════════════════════════════════════════════════════════╗
║                        Linux DMA 架构全景                           ║
╠══════════════════════════════════════════════════════════════════════╣
║                                                                      ║
║  用户空间                                                            ║
║  ┌──────────────────────────────────────────────────────────┐       ║
║  │  应用程序 (test_oled)                                    │       ║
║  │  write(/dev/fb0, data)                                   │       ║
║  └──────────────────┬───────────────────────────────────────┘       ║
║                     │ 系统调用                                       ║
║  ═══════════════════╪═══════════════════════════════════════════     ║
║  内核空间            │                                               ║
║                     ▼                                                ║
║  ┌──────────────────────────────────────────────────────────┐       ║
║  │  Framebuffer子系统  (fb_write → oled_fb_write)           │       ║
║  └──────────────────┬───────────────────────────────────────┘       ║
║                     │                                                ║
║                     ▼                                                ║
║  ┌──────────────────────────────────────────────────────────┐       ║
║  │  OLED驱动 (spi-oled-dma)                                 │       ║
║  │                                                          │       ║
║  │  ┌─────────────┐ ┌──────────────┐ ┌───────────────┐    │       ║
║  │  │ Coherent    │ │ Streaming    │ │ Scatter-Gather│    │       ║
║  │  │ DMA模式     │ │ DMA模式      │ │ DMA模式       │    │       ║
║  │  │             │ │              │ │               │    │       ║
║  │  │ dma_alloc_  │ │ dma_map_     │ │ dma_map_sg() │    │       ║
║  │  │ coherent()  │ │ single()     │ │              │    │       ║
║  │  └──────┬──────┘ └──────┬───────┘ └──────┬────────┘    │       ║
║  │         │               │                │              │       ║
║  │         └───────────────┴────────────────┘              │       ║
║  │                         │                                │       ║
║  │                         ▼                                │       ║
║  │  ┌──────────────────────────────────────────────┐       │       ║
║  │  │  SPI传输 (spi_sync / spi_async)              │       │       ║
║  │  │  设置 is_dma_mapped = true                   │       │       ║
║  │  └──────────────────────┬───────────────────────┘       │       ║
║  └─────────────────────────┼────────────────────────────────┘       ║
║                            │                                         ║
║                            ▼                                         ║
║  ┌──────────────────────────────────────────────────────────┐       ║
║  │  SPI控制器驱动 (如 spi-bcm2835, spi-stm32)              │       ║
║  │                                                          │       ║
║  │  if (xfer->is_dma_mapped)                               │       ║
║  │      使用 xfer->tx_dma 地址                              │       ║
║  │  else                                                    │       ║
║  │      自己调用 dma_map_single()                           │       ║
║  │                                                          │       ║
║  │  配置DMA控制器寄存器:                                     │       ║
║  │    源地址 = tx_dma                                        │       ║
║  │    目的地址 = SPI_DR (SPI数据寄存器物理地址)              │       ║
║  │    长度 = xfer->len                                       │       ║
║  │    方向 = MEM_TO_DEV                                      │       ║
║  └──────────────────────────┬───────────────────────────────┘       ║
║                             │                                        ║
║                             ▼                                        ║
║  ┌──────────────────────────────────────────────────────────┐       ║
║  │  DMA Engine子系统 (dmaengine)                            │       ║
║  │                                                          │       ║
║  │  dmaengine_prep_slave_single() → 创建DMA描述符          │       ║
║  │  dmaengine_submit()            → 提交到队列              │       ║
║  │  dma_async_issue_pending()     → 开始传输                │       ║
║  └──────────────────────────┬───────────────────────────────┘       ║
║                             │                                        ║
║  ═══════════════════════════╪════════════════════════════════════    ║
║  硬件层                     │                                        ║
║                             ▼                                        ║
║  ┌──────────────────────────────────────────────────────────┐       ║
║  │                    DMA控制器硬件                          │       ║
║  │                                                          │       ║
║  │  ┌──────────┐                                            │       ║
║  │  │ 通道0    │  源地址寄存器: 0x12340000 (内存)           │       ║
║  │  │          │  目的寄存器:   0x3F204004 (SPI_DR)         │       ║
║  │  │          │  计数寄存器:   1024                        │       ║
║  │  │          │  控制寄存器:   MEM→DEV, 8bit, 突发16      │       ║
║  │  └────┬─────┘                                            │       ║
║  │       │ 自动搬运数据,不需要CPU参与                       │       ║
║  │       │                                                  │       ║
║  │       ▼                                                  │       ║
║  │  ┌──────────┐    总线    ┌──────────────┐               │       ║
║  │  │  内存    │ ========> │ SPI控制器     │               │       ║
║  │  │ (显示   │            │              │               │       ║
║  │  │  数据)  │            │  TX FIFO ──MOSI──> OLED     │       ║
║  │  └──────────┘            │  CLK    ──SCLK──> OLED     │       ║
║  │                          │  CS     ──CS───> OLED      │       ║
║  │                          └──────────────┘               │       ║
║  │                                                          │       ║
║  │  传输完成 → 产生中断 → CPU处理中断 → 调用callback       │       ║
║  └──────────────────────────────────────────────────────────┘       ║
╚══════════════════════════════════════════════════════════════════════╝


╔══════════════════════════════════════════════════════════════════════╗
║                    三种DMA映射方式对比                                ║
╠══════════════════════════════════════════════════════════════════════╣
║                                                                      ║
║  ┌─────────────┬─────────────────┬───────────────┬──────────────┐   ║
║  │   特性       │ Coherent        │ Streaming     │ Scatter-     │   ║
║  │             │ (一致性)         │ (流式)         │ Gather       │   ║
║  ├─────────────┼─────────────────┼───────────────┼──────────────┤   ║
║  │ 分配API     │ dma_alloc_      │ kmalloc +     │ kmalloc +    │   ║
║  │             │ coherent()      │ dma_map_      │ sg_alloc_ +  │   ║
║  │             │                 │ single()      │ dma_map_sg() │   ║
║  ├─────────────┼─────────────────┼───────────────┼──────────────┤   ║
║  │ Cache       │ non-cacheable   │ cacheable     │ cacheable    │   ║
║  │             │ (硬件保证一致)   │ (需手动sync)  │ (需手动sync) │   ║
║  ├─────────────┼─────────────────┼───────────────┼──────────────┤   ║
║  │ 需要sync?   │ ❌ 不需要       │ ✅ 需要       │ ✅ 需要      │   ║
║  ├─────────────┼─────────────────┼───────────────┼──────────────┤   ║
║  │ 物理连续?   │ ✅ 必须连续     │ ✅ 必须连续   │ ❌ 可不连续  │   ║
║  ├─────────────┼─────────────────┼───────────────┼──────────────┤   ║
║  │ 最小分配    │ 1页(4KB)        │ 任意大小      │ 任意大小     │   ║
║  ├─────────────┼─────────────────┼───────────────┼──────────────┤   ║
║  │ 生命周期    │ 长期            │ 短期/每次传输  │ 短期/每次    │   ║
║  ├─────────────┼─────────────────┼───────────────┼──────────────┤   ║
║  │ CPU访问速度 │ 较慢(no cache)  │ 快(cached)    │ 快(cached)   │   ║
║  ├─────────────┼─────────────────┼───────────────┼──────────────┤   ║
║  │ 适用场景    │ 频繁读写的      │ 一次性传输    │ 不连续内存   │   ║
║  │             │ 控制结构/描述符  │ 大块数据      │ 网络/存储    │   ║
║  ├─────────────┼─────────────────┼───────────────┼──────────────┤   ║
║  │ 本驱动用途  │ OLED显示缓冲区  │ OLED显示数据  │ 按页面刷新   │   ║
║  └─────────────┴─────────────────┴───────────────┴──────────────┘   ║
╚══════════════════════════════════════════════════════════════════════╝


╔══════════════════════════════════════════════════════════════════════╗
║                    DMA传输时序对比                                    ║
╠══════════════════════════════════════════════════════════════════════╣
║                                                                      ║
║  PIO方式 (CPU全程参与):                                              ║
║  ──时间──────────────────────────────────────────>                   ║
║  CPU: [写byte0][等][写byte1][等][写byte2][等]...[写byte1023][等]    ║
║       ████████████████████████████████████████████████████████       ║
║       CPU 100%占用!                                                 ║
║                                                                      ║
║  DMA方式 (CPU只负责配置):                                            ║
║  ──时间──────────────────────────────────────────>                   ║
║  CPU: [配置DMA][          空闲,可做其他事          ][处理完成中断]   ║
║       ████                                          ██               ║
║  DMA:        [===========自动传输1024字节===========]                ║
║              ████████████████████████████████████████                ║
║       CPU利用率大幅降低!                                            ║
║                                                                      ║
║  Scatter-Gather DMA (不连续内存):                                    ║
║  ──时间──────────────────────────────────────────>                   ║
║  CPU: [配置SG表][     空闲     ][中断]                               ║
║       ██████                     ██                                  ║
║  DMA:          [块0][块1][块2]...[块7]  ← 自动跳转到下一块          ║
║                ██████████████████████                                ║
║       一次配置,多块自动传输!                                         ║
║                                                                      ║
╚══════════════════════════════════════════════════════════════════════╝


╔══════════════════════════════════════════════════════════════════════╗
║                    Cache一致性问题图解                                ║
╠══════════════════════════════════════════════════════════════════════╣
║                                                                      ║
║  问题场景 (DMA_TO_DEVICE, 不做sync):                                ║
║                                                                      ║
║   CPU写入数据0xFF:                                                   ║
║   ┌──────┐    ┌────────────┐    ┌────────┐                          ║
║   │ CPU  │──>│ Cache: 0xFF│    │内存:0x00│ ← 旧数据!               ║
║   └──────┘    └────────────┘    └────────┘                          ║
║                                      ↑                               ║
║                                 DMA读这里 = 读到0x00 = 错误!        ║
║                                                                      ║
║  正确做法 (map时flush cache):                                        ║
║                                                                      ║
║   dma_map_single(..., DMA_TO_DEVICE):                                ║
║   ┌──────┐    ┌────────────┐ flush ┌────────┐                       ║
║   │ CPU  │    │ Cache: 0xFF│ ────> │内存:0xFF│ ← 最新数据!          ║
║   └──────┘    └────────────┘       └────────┘                        ║
║                                        ↑                             ║
║                                   DMA读这里 = 读到0xFF = 正确!      ║
║                                                                      ║
║  问题场景 (DMA_FROM_DEVICE, 不做sync):                               ║
║                                                                      ║
║   DMA写入数据0xAB到内存:                                             ║
║   ┌──────┐    ┌────────────┐    ┌────────┐                          ║
║   │ CPU  │<──│Cache: 0x00 │    │内存:0xAB│ ← DMA写入了新数据       ║
║   └──────┘    └────────────┘    └────────┘                          ║
║   CPU读到Cache中的0x00 = 旧数据 = 错误!                             ║
║                                                                      ║
║   正确做法 (unmap时invalidate cache):                                ║
║   ┌──────┐    ┌────────────┐ inv  ┌────────┐                        ║
║   │ CPU  │<──│Cache: 无效  │ <──  │内存:0xAB│                       ║
║   └──────┘    └────────────┘      └────────┘                        ║
║   CPU cache miss → 从内存读取0xAB = 正确!                           ║
║                                                                      ║
╚══════════════════════════════════════════════════════════════════════╝


╔══════════════════════════════════════════════════════════════════════╗
║                    DMA API 速查表                                    ║
╠══════════════════════════════════════════════════════════════════════╣
║                                                                      ║
║  📋 设置DMA能力:                                                    ║
║     dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))                ║
║                                                                      ║
║  📋 一致性DMA:                                                      ║
║     vaddr = dma_alloc_coherent(dev, size, &dma_addr, GFP_KERNEL)    ║
║     dma_free_coherent(dev, size, vaddr, dma_addr)                   ║
║                                                                      ║
║  📋 流式DMA:                                                        ║
║     dma_addr = dma_map_single(dev, vaddr, size, direction)          ║
║     if (dma_mapping_error(dev, dma_addr)) { /* 处理错误 */ }        ║
║     /* ... DMA传输 ... */                                            ║
║     dma_unmap_single(dev, dma_addr, size, direction)                ║
║                                                                      ║
║  📋 流式DMA同步 (重复使用同一映射):                                  ║
║     dma_sync_single_for_cpu(dev, dma_addr, size, direction)         ║
║     /* CPU修改数据 */                                                ║
║     dma_sync_single_for_device(dev, dma_addr, size, direction)      ║
║     /* 再次DMA传输 */                                                ║
║                                                                      ║
║  📋 Scatter-Gather:                                                  ║
║     sg_alloc_table(&sgt, nents, GFP_KERNEL)                        ║
║     for_each_sg(sgt.sgl, sg, nents, i)                              ║
║         sg_set_buf(sg, buf, len)                                    ║
║     mapped = dma_map_sg(dev, sgt.sgl, nents, direction)             ║
║     /* ... DMA传输 (用sg_dma_address/sg_dma_len) ... */             ║
║     dma_unmap_sg(dev, sgt.sgl, nents, direction)                    ║
║     sg_free_table(&sgt)                                             ║
║                                                                      ║
║  📋 DMA池 (小块分配):                                               ║
║     pool = dma_pool_create(name, dev, size, align, boundary)        ║
║     vaddr = dma_pool_alloc(pool, GFP_KERNEL, &dma_addr)            ║
║     dma_pool_free(pool, vaddr, dma_addr)                            ║
║     dma_pool_destroy(pool)                                          ║
║                                                                      ║
║  📋 DMA Engine:                                                     ║
║     chan = dma_request_chan(dev, "tx")                               ║
║     dmaengine_slave_config(chan, &config)                            ║
║     desc = dmaengine_prep_slave_single(chan, addr, len, dir, flags) ║
║     desc->callback = my_callback                                    ║
║     cookie = dmaengine_submit(desc)                                 ║
║     dma_async_issue_pending(chan)                                    ║
║     /* wait for callback or poll dma_async_is_tx_complete() */      ║
║     dma_release_channel(chan)                                        ║
║                                                                      ║
╚══════════════════════════════════════════════════════════════════════╝

7. 实验指导手册

╔══════════════════════════════════════════════════════════════╗
║           SPI OLED DMA 驱动 - 实验指导                      ║
╚══════════════════════════════════════════════════════════════╝

实验环境准备:
  - Linux开发板 (树莓派/STM32MP1/i.MX6等)
  - SSD1306 OLED屏幕 (128x64, SPI接口)
  - 接线: MOSI, SCLK, CS, DC, RESET

═══ 实验1: 编译与加载驱动 ═══

  # 编译
  $ make

  # 开启内核DMA调试 (推荐学习时开启)
  $ echo 'file spi_oled_dma.c +p' > /sys/kernel/debug/dynamic_debug/control

  # 加载驱动 (默认Coherent模式)
  $ sudo insmod spi-oled-dma.ko dma_mode=0

  # 查看加载日志
  $ dmesg | tail -80

  预期输出:
    - DMA掩码设置信息
    - 一致性DMA缓冲区的虚拟地址和DMA地址
    - OLED初始化序列
    - DMA Pool演示信息
    - Bounce Buffer说明

═══ 实验2: 观察DMA地址映射 ═══

  $ cat /sys/devices/.../dma_addresses

  观察要点:
  1. 虚拟地址 vs DMA地址的区别
  2. 在不同平台上,DMA地址的范围
  3. 切换模式后地址变化

═══ 实验3: 对比四种传输模式 ═══

  # 切换到各种模式并观察
  $ echo 0 > /sys/devices/.../dma_mode   # Coherent
  $ cat /sys/devices/.../dma_stats

  $ echo 1 > /sys/devices/.../dma_mode   # Streaming
  $ cat /sys/devices/.../dma_stats

  $ echo 2 > /sys/devices/.../dma_mode   # Scatter-Gather
  $ cat /sys/devices/.../dma_stats

  $ echo 3 > /sys/devices/.../dma_mode   # PIO
  $ cat /sys/devices/.../dma_stats

  观察要点:
  1. 不同模式的平均传输时间
  2. DMA模式 vs PIO模式的CPU占用差异
  3. 在传输期间用top观察CPU使用率

═══ 实验4: 性能基准测试 ═══

  $ gcc -o test_oled test/test_oled.c -lm
  $ sudo ./test_oled benchmark /dev/fb0

  这会依次测试四种模式的FPS
  记录并比较结果

═══ 实验5: 动画压力测试 ═══

  # 用Coherent模式运行动画
  $ echo 0 > /sys/devices/.../dma_mode
  $ sudo ./test_oled animate /dev/fb0 300

  # 同时在另一个终端观察
  $ watch -n 1 cat /sys/devices/.../dma_stats
  $ top   # 观察CPU使用率

═══ 实验6: DMA错误注入 (高级) ═══

  # 开启内核DMA调试
  # 在内核配置中启用: CONFIG_DMA_API_DEBUG=y

  # 查看DMA API使用错误
  $ cat /sys/kernel/debug/dma-api/errors

  # 查看所有DMA映射
  $ cat /sys/kernel/debug/dma-api/dump

═══ 实验7: 源码阅读理解 ═══

  按以下顺序阅读源码:

  1. spi_oled_dma.h
     - 理解数据结构和DMA缓冲区定义
     - 阅读所有📚标注的注释

  2. spi_oled_dma.c - 第2部分
     - oled_dma_init_coherent()  → 理解一致性DMA
     - oled_dma_init_streaming() → 理解流式DMA  
     - oled_dma_init_sg()        → 理解SG DMA

  3. spi_oled_dma.c - 第3部分
     - oled_dma_transfer_coherent()  → 一致性DMA传输流程
     - oled_dma_transfer_streaming() → 流式DMA的map/sync/unmap
     - oled_dma_transfer_sg()        → SG映射与链式传输

  4. spi_oled_dma.c - 第8部分
     - oled_dma_engine_transfer()  → DMA Engine API完整流程
     - oled_demo_dma_pool()        → DMA Pool概念
     - oled_demo_bounce_buffer()   → Bounce Buffer概念

═══ 思考题 ═══

  1. 为什么Coherent DMA不需要sync操作?
     提示: 看Cache映射方式

  2. 如果忘记调用dma_unmap_single()会怎样?
     提示: 资源泄漏 + IOMMU

  3. 为什么dma_map_single()需要指定方向?
     提示: Cache flush vs invalidate

  4. Scatter-Gather在什么场景下比单块DMA更有优势?
     提示: 内存碎片 + 不连续数据

  5. 在我们的OLED驱动中(1024字节数据),DMA真的比PIO快吗?
     提示: DMA配置开销 vs 传输时间

  6. 如果OLED数据量变为百万像素(如LCD),DMA优势会更明显吗?
     提示: 数据量越大,DMA优势越大

  7. is_dma_mapped=true和false有什么区别?谁负责映射?
     提示: 查看SPI控制器驱动源码

这个完整的驱动程序涵盖了以下 DMA 核心知识点:

知识点 对应代码位置
DMA掩码设置 oled_dma_init_coherent()
一致性DMA分配/释放 oled_dma_init_coherent() / oled_dma_cleanup()
流式DMA映射/同步/取消 oled_dma_transfer_streaming()
Scatter-Gather表创建/映射 oled_dma_init_sg() / oled_dma_transfer_sg()
DMA Engine完整流程 oled_dma_engine_transfer()
DMA Pool小块分配 oled_demo_dma_pool()
Bounce Buffer概念 oled_demo_bounce_buffer()
Cache一致性原理 所有📚注释
DMA方向与Cache操作关系 oled_demo_dma_direction()
PIO vs DMA对比 oled_pio_transfer() vs DMA函数
posted @ 2026-02-26 16:16  21code  阅读(2)  评论(0)    收藏  举报