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函数 |

浙公网安备 33010602011771号