2. 主驱动程序 (spi_oled_dma.c)
/*
* spi_oled_dma.c - SPI OLED DMA教学驱动
*
* 这个驱动的主要目的是通过一个实际的SPI OLED显示驱动,
* 来学习和理解Linux内核中DMA的各种概念和用法。
*
* 支持的DMA模式:
* 1. Coherent DMA - 一致性DMA,最简单
* 2. Streaming DMA - 流式DMA,最灵活
* 3. Scatter-Gather - 分散/聚集DMA,最高效
* 4. PIO - 无DMA对比基准
*
* 作者: 教学示例
* 许可: GPL v2
*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/spi/spi.h>
#include <linux/gpio/consumer.h>
#include <linux/delay.h>
#include <linux/slab.h>
#include <linux/dma-mapping.h>
#include <linux/dmaengine.h>
#include <linux/scatterlist.h>
#include <linux/workqueue.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include "spi_oled_dma.h"
/* 模块参数 - 选择DMA模式 */
static int dma_mode = DMA_MODE_COHERENT;
module_param(dma_mode, int, 0644);
MODULE_PARM_DESC(dma_mode, "DMA mode: 0=coherent, 1=streaming, 2=scatter-gather, 3=PIO");
/* 刷新率(ms) */
static int refresh_interval = 33; /* ~30fps */
module_param(refresh_interval, int, 0644);
MODULE_PARM_DESC(refresh_interval, "Display refresh interval in ms (default: 33)");
/* ============================================================
* 第1部分: SPI基础通信函数
* ============================================================
*
* SSD1306有两种数据类型:
* - 命令 (DC=0): 控制显示器行为
* - 数据 (DC=1): 显示内容(像素数据)
*
* DC引脚用于区分命令和数据
*/
/**
* oled_write_cmd - 向OLED发送单个命令
* @oled: OLED设备结构
* @cmd: 命令字节
*
* DC引脚拉低表示发送的是命令
* 这里使用普通SPI传输(非DMA),因为命令通常很短
*/
int oled_write_cmd(struct spi_oled_device *oled, u8 cmd)
{
int ret;
/* DC = 0 表示命令模式 */
gpiod_set_value_cansleep(oled->dc_gpio, 0);
/*
* spi_write() 是最简单的SPI发送函数
* 内部可能使用PIO或DMA,取决于SPI控制器驱动的实现
* 对于单字节传输,通常使用PIO
*/
ret = spi_write(oled->spi, &cmd, 1);
if (ret)
dev_err(oled->dev, "Failed to write command 0x%02x: %d\n", cmd, ret);
return ret;
}
/**
* oled_write_cmd_seq - 发送命令序列
* @oled: OLED设备结构
* @cmds: 命令数组
* @len: 命令数量
*/
int oled_write_cmd_seq(struct spi_oled_device *oled, const u8 *cmds, size_t len)
{
int ret;
gpiod_set_value_cansleep(oled->dc_gpio, 0);
ret = spi_write(oled->spi, cmds, len);
if (ret)
dev_err(oled->dev, "Failed to write command sequence: %d\n", ret);
return ret;
}
/**
* oled_write_data_pio - 使用PIO方式发送显示数据(无DMA对比)
* @oled: OLED设备结构
* @data: 数据缓冲区
* @len: 数据长度
*
* 📚 PIO (Programmed I/O) 说明:
* CPU直接参与每个字节的传输:
* for each byte:
* CPU读取内存 -> CPU写入SPI数据寄存器 -> 等待传输完成
*
* 问题:
* - CPU全程占用,无法执行其他任务
* - 对于大量数据传输效率低下
* - 每次传输都有CPU中断/轮询开销
*/
static int oled_write_data_pio(struct spi_oled_device *oled, const u8 *data, size_t len)
{
int ret;
ktime_t start, end;
start = ktime_get();
/* DC = 1 表示数据模式 */
gpiod_set_value_cansleep(oled->dc_gpio, 1);
/*
* spi_write 内部流程:
* 1. 构建 spi_message 和 spi_transfer
* 2. 调用 spi_sync() 同步发送
* 3. SPI控制器驱动决定使用PIO还是DMA
* - 对于小数据量,通常使用PIO
* - spi_transfer没有设置tx_dma,控制器需要自己映射
*/
ret = spi_write(oled->spi, data, len);
end = ktime_get();
/* 更新统计 */
spin_lock(&oled->stats_lock);
oled->stats.pio_transfers++;
oled->stats.last_transfer_start = start;
oled->stats.last_transfer_end = end;
spin_unlock(&oled->stats_lock);
if (ret)
dev_err(oled->dev, "PIO write failed: %d\n", ret);
return ret;
}
/* ============================================================
* 第2部分: DMA初始化和清理
* ============================================================
*
* 📚 DMA初始化流程:
* 1. 设置DMA掩码 - 告诉内核设备能访问多大的地址空间
* 2. 分配DMA缓冲区 - 根据需要选择一致性或流式
* 3. 获取DMA通道(可选)- 如果需要直接控制DMA引擎
*/
/**
* oled_dma_init_coherent - 初始化一致性DMA缓冲区
*
* 📚 一致性DMA (Coherent DMA) 详解:
*
* 分配函数: dma_alloc_coherent(dev, size, &dma_handle, gfp)
* - dev: 设备结构(用于确定DMA地址空间)
* - size: 缓冲区大小
* - dma_handle: 输出参数,返回DMA总线地址
* - gfp: 内存分配标志
*
* 返回值: CPU可用的虚拟地址
*
* 特点:
* ┌─────────┐ ┌──────────────┐ ┌──────────┐
* │ CPU │────>│ 一致性内存 │<────│ DMA控制器│
* │(虚拟地址)│ │(非缓存/写透) │ │(DMA地址) │
* └─────────┘ └──────────────┘ └──────────┘
*
* - 内存通常映射为non-cacheable或write-through
* - 硬件保证CPU写入立即对设备可见
* - 设备写入立即对CPU可见
* - 无需手动同步操作
* - 适合生命周期长、频繁访问的缓冲区
*
* 缺点:
* - 因为non-cacheable,CPU访问速度较慢
* - 分配的内存是物理连续的,大块分配可能失败
*/
static int oled_dma_init_coherent(struct spi_oled_device *oled)
{
struct device *dev = oled->dev;
dev_info(dev, "📚 初始化一致性DMA缓冲区\n");
/*
* 📚 第一步: 设置DMA掩码
*
* DMA掩码告诉内核该设备能够寻址的内存范围。
* 例如:
* 32位掩码 (0xFFFFFFFF) = 设备可以访问4GB以内的地址
* 24位掩码 (0x00FFFFFF) = 设备只能访问16MB以内的地址(ISA设备)
*
* 内核根据这个掩码来分配DMA缓冲区,确保地址在设备可达范围内。
*
* dma_set_mask_and_coherent() 同时设置:
* - streaming DMA mask (dma_mask)
* - coherent DMA mask (coherent_dma_mask)
*/
if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))) {
dev_err(dev, "Failed to set DMA mask\n");
return -EIO;
}
dev_info(dev, " DMA掩码设置为32位 (可访问4GB地址空间)\n");
/*
* 📚 第二步: 分配一致性DMA缓冲区
*
* dma_alloc_coherent() 做了什么:
* 1. 分配物理连续的内存页
* 2. 将内存映射为non-cacheable(在大多数架构上)
* 3. 如果存在IOMMU,设置IOMMU映射
* 4. 返回CPU虚拟地址和DMA总线地址
*
* 注意: 返回的两个地址指向同一块物理内存,但"视角"不同
* - 虚拟地址: CPU通过MMU翻译后访问
* - DMA地址: DMA控制器通过总线/IOMMU访问
*/
oled->coherent_buf.size = OLED_BUF_SIZE;
oled->coherent_buf.vaddr = dma_alloc_coherent(
dev,
oled->coherent_buf.size,
&oled->coherent_buf.dma_addr, /* 输出: DMA地址 */
GFP_KERNEL /* 可以睡眠的分配 */
);
if (!oled->coherent_buf.vaddr) {
dev_err(dev, "Failed to allocate coherent DMA buffer\n");
return -ENOMEM;
}
oled->coherent_buf.is_coherent = true;
dev_info(dev, " 一致性DMA缓冲区已分配:\n");
dev_info(dev, " CPU虚拟地址: %px\n", oled->coherent_buf.vaddr);
dev_info(dev, " DMA总线地址: %pad\n", &oled->coherent_buf.dma_addr);
dev_info(dev, " 大小: %zu bytes\n", oled->coherent_buf.size);
/* 清零缓冲区 */
memset(oled->coherent_buf.vaddr, 0, oled->coherent_buf.size);
return 0;
}
/**
* oled_dma_init_streaming - 初始化流式DMA
*
* 📚 流式DMA (Streaming DMA) 详解:
*
* 与一致性DMA不同,流式DMA使用普通内存(可能被CPU缓存)。
* 需要在CPU和设备之间传递"所有权"。
*
* 工作流程(CPU到设备方向):
* 1. CPU写入数据到缓冲区(数据可能在CPU Cache中)
* 2. dma_map_single() - 映射缓冲区,刷新Cache到内存
* 3. 启动DMA传输
* 4. 等待DMA完成
* 5. dma_unmap_single() - 取消映射
*
* 所有权模型:
* ┌─────┐ map() ┌───────┐ 完成后 ┌─────┐
* │ CPU │ -------> │ 设备 │ -------> │ CPU │
* │拥有 │ (同步) │ 拥有 │ (unmap) │拥有 │
* └─────┘ └───────┘ └─────┘
*
* DMA方向:
* DMA_TO_DEVICE - CPU -> 设备 (如发送数据到OLED)
* DMA_FROM_DEVICE - 设备 -> CPU (如接收传感器数据)
* DMA_BIDIRECTIONAL - 双向 (性能最差,因为要双向同步)
*/
static int oled_dma_init_streaming(struct spi_oled_device *oled)
{
struct device *dev = oled->dev;
dev_info(dev, "📚 初始化流式DMA缓冲区\n");
/* 设置DMA掩码 */
if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))) {
dev_err(dev, "Failed to set DMA mask\n");
return -EIO;
}
/*
* 📚 流式DMA使用普通kmalloc分配内存
*
* 注意: 不是任何内存都可以做DMA!
* 要求:
* 1. 物理连续(kmalloc满足,vmalloc不满足)
* 2. 不在栈上(栈可能不是DMA-safe的)
* 3. 对齐到cache line(避免cache line共享问题)
*
* kmalloc + GFP_DMA: 分配在DMA区域(低地址)
* kmalloc + GFP_KERNEL: 分配在普通区域(通常也OK,因为DMA掩码是32位)
*
* 使用 devm_kmalloc 可以自动在设备移除时释放
*/
oled->streaming_buf.size = OLED_BUF_SIZE;
oled->streaming_buf.vaddr = devm_kmalloc(
dev,
oled->streaming_buf.size,
GFP_KERNEL | __GFP_DMA /* 确保在DMA可达区域 */
);
if (!oled->streaming_buf.vaddr) {
dev_err(dev, "Failed to allocate streaming DMA buffer\n");
return -ENOMEM;
}
oled->streaming_buf.is_coherent = false;
oled->streaming_buf.dma_addr = 0; /* 尚未映射 */
dev_info(dev, " 流式DMA缓冲区已分配:\n");
dev_info(dev, " CPU虚拟地址: %px\n", oled->streaming_buf.vaddr);
dev_info(dev, " 大小: %zu bytes\n", oled->streaming_buf.size);
dev_info(dev, " 注意: DMA地址将在每次传输时动态映射\n");
memset(oled->streaming_buf.vaddr, 0, oled->streaming_buf.size);
return 0;
}
/**
* oled_dma_init_sg - 初始化Scatter-Gather DMA
*
* 📚 Scatter-Gather DMA 详解:
*
* 问题场景: 数据分布在多个不连续的内存块中
* 例如: framebuffer在虚拟地址上连续,但物理页可能不连续
* 或者: 需要将多个小缓冲区合并在一次DMA操作中传输
*
* 传统DMA: 只能传输一块连续的内存
* ┌──────────────────┐
* │ 连续内存块 │ ──DMA──> 设备
* └──────────────────┘
*
* Scatter-Gather DMA: 可以同时传输多个不连续的内存块
* ┌──────┐
* │ 块1 │──┐
* └──────┘ │
* ┌──────┐ ├──DMA──> 设备 (硬件自动按顺序传输)
* │ 块2 │──┤
* └──────┘ │
* ┌──────┐ │
* │ 块3 │──┘
* └──────┘
*
* scatterlist结构: 描述一个内存块
* - page: 内存页
* - offset: 页内偏移
* - length: 数据长度
* - dma_addr: DMA映射后的总线地址
*
* sg_table: 管理一组scatterlist
* - sgl: scatterlist数组的首地址
* - nents: 原始条目数
* - orig_nents: 初始分配的条目数
*
* 优点:
* 1. 减少内存碎片要求(不需要大块连续物理内存)
* 2. 可以一次传输提交多个缓冲区
* 3. DMA控制器硬件支持链式传输,无需CPU介入
*
* 在OLED场景中的应用:
* 将1024字节的显示数据分成8个128字节的页(page)
* 每个页对应OLED的一行(8像素高)
* 可以实现局部刷新(只更新变化的页)
*/
static int oled_dma_init_sg(struct spi_oled_device *oled)
{
struct device *dev = oled->dev;
struct scatterlist *sg;
int i, ret;
dev_info(dev, "📚 初始化Scatter-Gather DMA\n");
/* 设置DMA掩码 */
if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))) {
dev_err(dev, "Failed to set DMA mask\n");
return -EIO;
}
/*
* 📚 我们将显示数据分成OLED_PAGES(8)个块
* 每个块对应OLED的一页 = 128字节
* 这样可以实现按页刷新
*/
oled->sg_nents = OLED_PAGES;
/*
* 📚 sg_alloc_table() - 分配scatter-gather表
*
* 内部分配一个scatterlist数组
* 如果条目数超过SG_MAX_SINGLE_ALLOC(通常128),
* 会分配链式的scatterlist
*/
ret = sg_alloc_table(&oled->sg_table, oled->sg_nents, GFP_KERNEL);
if (ret) {
dev_err(dev, "Failed to allocate SG table: %d\n", ret);
return ret;
}
/*
* 📚 为每个SG条目分配独立的内存块
*
* 故意分开分配,模拟"不连续内存"的场景
* 实际中这些内存块在物理上可能不连续
*/
i = 0;
for_each_sg(oled->sg_table.sgl, sg, oled->sg_nents, i) {
void *buf;
size_t chunk_size = OLED_WIDTH; /* 每页128字节 */
/*
* 用kmalloc分配每个小块
* 这些块在物理上很可能不连续
*/
buf = kmalloc(chunk_size, GFP_KERNEL | __GFP_DMA);
if (!buf) {
dev_err(dev, "Failed to allocate SG buffer %d\n", i);
goto err_free_bufs;
}
memset(buf, 0, chunk_size);
/*
* 📚 sg_set_buf() - 设置scatterlist条目
*
* 将一个虚拟地址缓冲区关联到scatterlist条目
* 内部调用 virt_to_page() 获取页结构
* 并计算页内偏移
*
* sg_set_buf(sg, buf, len) 等价于:
* sg_set_page(sg, virt_to_page(buf), len, offset_in_page(buf));
*/
sg_set_buf(sg, buf, chunk_size);
dev_info(dev, " SG[%d]: vaddr=%px, size=%zu\n", i, buf, chunk_size);
}
dev_info(dev, " Scatter-Gather表已创建: %d个条目\n", oled->sg_nents);
dev_info(dev, " 总大小: %d bytes (每块 %d bytes)\n",
OLED_BUF_SIZE, OLED_WIDTH);
return 0;
err_free_bufs:
/* 释放已分配的缓冲区 */
{
struct scatterlist *sg_iter;
int j;
for_each_sg(oled->sg_table.sgl, sg_iter, i, j) {
kfree(sg_virt(sg_iter));
}
sg_free_table(&oled->sg_table);
}
return -ENOMEM;
}
/**
* oled_dma_init - DMA总初始化函数
*
* 根据选择的模式初始化对应的DMA资源
*/
int oled_dma_init(struct spi_oled_device *oled)
{
int ret;
dev_info(oled->dev, "====== DMA初始化开始 ======\n");
dev_info(oled->dev, "选择的DMA模式: %d\n", oled->dma_mode);
/* 初始化完成量,用于等待DMA传输完成 */
init_completion(&oled->dma_completion);
switch (oled->dma_mode) {
case DMA_MODE_COHERENT:
dev_info(oled->dev, "模式: 一致性DMA (Coherent)\n");
ret = oled_dma_init_coherent(oled);
break;
case DMA_MODE_STREAMING:
dev_info(oled->dev, "模式: 流式DMA (Streaming)\n");
ret = oled_dma_init_streaming(oled);
break;
case DMA_MODE_SG:
dev_info(oled->dev, "模式: Scatter-Gather DMA\n");
ret = oled_dma_init_sg(oled);
break;
case DMA_MODE_PIO:
dev_info(oled->dev, "模式: PIO (无DMA,CPU直接传输)\n");
/* PIO不需要DMA初始化,只需要普通缓冲区 */
oled->streaming_buf.size = OLED_BUF_SIZE;
oled->streaming_buf.vaddr = devm_kmalloc(oled->dev,
OLED_BUF_SIZE,
GFP_KERNEL);
if (!oled->streaming_buf.vaddr)
return -ENOMEM;
ret = 0;
break;
default:
dev_err(oled->dev, "Unknown DMA mode: %d\n", oled->dma_mode);
return -EINVAL;
}
if (ret == 0) {
dev_info(oled->dev, "====== DMA初始化完成 ======\n");
}
return ret;
}
/**
* oled_dma_cleanup - 清理DMA资源
*
* 📚 DMA资源释放的重要性:
* - 必须在设备移除或驱动卸载时释放所有DMA资源
* - 释放顺序通常与分配顺序相反
* - 未释放的DMA映射会导致内存泄漏和IOMMU资源泄漏
* - 内核的DMA debug功能(CONFIG_DMA_API_DEBUG)可以检测泄漏
*/
void oled_dma_cleanup(struct spi_oled_device *oled)
{
struct device *dev = oled->dev;
dev_info(dev, "清理DMA资源...\n");
switch (oled->dma_mode) {
case DMA_MODE_COHERENT:
/*
* 📚 dma_free_coherent() - 释放一致性DMA缓冲区
*
* 必须传入与分配时相同的:
* - 设备指针
* - 大小
* - 虚拟地址
* - DMA地址
*
* 内部操作:
* 1. 解除IOMMU映射(如果有)
* 2. 释放物理页
* 3. 解除虚拟地址映射
*/
if (oled->coherent_buf.vaddr) {
dma_free_coherent(dev,
oled->coherent_buf.size,
oled->coherent_buf.vaddr,
oled->coherent_buf.dma_addr);
oled->coherent_buf.vaddr = NULL;
dev_info(dev, " 一致性DMA缓冲区已释放\n");
}
break;
case DMA_MODE_STREAMING:
/*
* 📚 流式DMA缓冲区清理
*
* 如果当前有未完成的DMA映射,需要先unmap
* 这里使用devm_kmalloc分配的内存会自动释放
* 但DMA映射必须手动取消
*/
if (oled->streaming_buf.dma_addr) {
dma_unmap_single(dev,
oled->streaming_buf.dma_addr,
oled->streaming_buf.size,
DMA_TO_DEVICE);
oled->streaming_buf.dma_addr = 0;
dev_info(dev, " 流式DMA映射已取消\n");
}
/* devm_kmalloc的内存会自动释放 */
break;
case DMA_MODE_SG:
/*
* 📚 Scatter-Gather DMA清理
*
* 需要:
* 1. 取消SG表的DMA映射 (dma_unmap_sg)
* 2. 释放每个SG条目对应的缓冲区
* 3. 释放SG表本身
*/
if (oled->sg_table.sgl) {
struct scatterlist *sg;
int i;
/* 如果SG表已被映射,先取消映射 */
/* dma_unmap_sg() 在需要时调用 */
/* 释放每个块的内存 */
for_each_sg(oled->sg_table.sgl, sg, oled->sg_nents, i) {
void *buf = sg_virt(sg);
if (buf)
kfree(buf);
}
/* 释放SG表 */
sg_free_table(&oled->sg_table);
dev_info(dev, " SG表和缓冲区已释放\n");
}
break;
case DMA_MODE_PIO:
/* devm_kmalloc的内存会自动释放 */
dev_info(dev, " PIO模式无DMA资源需要释放\n");
break;
}
/* 释放DMA通道 */
if (oled->dma_chan_tx) {
dma_release_channel(oled->dma_chan_tx);
oled->dma_chan_tx = NULL;
dev_info(dev, " DMA通道已释放\n");
}
}
/* ============================================================
* 第3部分: DMA数据传输
* ============================================================
*
* 这是驱动最核心的部分
* 演示了三种DMA传输方式的具体实现
*/
/**
* oled_dma_transfer_coherent - 使用一致性DMA传输数据
*
* 📚 一致性DMA传输流程:
*
* ┌─────────────────────────────────────────────────────┐
* │ 1. CPU将数据写入一致性缓冲区 (memcpy) │
* │ - 因为是一致性映射,写入立即对DMA控制器可见 │
* │ - 不需要任何同步操作! │
* ├─────────────────────────────────────────────────────┤
* │ 2. 配置SPI传输,指定DMA地址 │
* │ - spi_transfer.tx_buf = 虚拟地址 │
* │ - spi_transfer.tx_dma = DMA地址 │
* │ - spi_transfer.is_dma_mapped = 1 (告诉SPI框架 │
* │ 缓冲区已经映射好了,不要再映射) │
* ├─────────────────────────────────────────────────────┤
* │ 3. 提交SPI传输 │
* │ - SPI控制器驱动直接使用我们提供的DMA地址 │
* │ - DMA控制器从缓冲区读取数据,发送到SPI │
* │ - CPU在此期间可以做其他事情 │
* ├─────────────────────────────────────────────────────┤
* │ 4. 等待传输完成 │
* │ - DMA控制器传输完成后产生中断 │
* │ - SPI驱动收到中断后通知我们 │
* └─────────────────────────────────────────────────────┘
*
* is_dma_mapped 标志的作用:
* 当设为1时,告诉SPI控制器驱动:
* "我已经做好了DMA映射,直接用tx_dma地址就行"
*
* 如果设为0(默认),SPI控制器驱动会:
* 自己调用dma_map_single()来映射tx_buf
*/
int oled_dma_transfer_coherent(struct spi_oled_device *oled)
{
struct spi_transfer xfer = {0};
struct spi_message msg;
ktime_t start, end;
s64 elapsed_ns;
int ret;
dev_dbg(oled->dev, "📚 一致性DMA传输开始\n");
mutex_lock(&oled->buf_lock);
start = ktime_get();
/*
* 📚 步骤1: 数据已经在一致性缓冲区中
* oled_convert_fb_to_oled() 已经将数据写入 coherent_buf.vaddr
* 因为是一致性映射,不需要任何sync操作
*/
/* DC = 1 表示数据模式 */
gpiod_set_value_cansleep(oled->dc_gpio, 1);
/*
* 📚 步骤2: 配置SPI传输描述符
*/
spi_message_init(&msg);
xfer.tx_buf = oled->coherent_buf.vaddr; /* CPU使用的虚拟地址 */
xfer.tx_dma = oled->coherent_buf.dma_addr; /* DMA控制器使用的总线地址 */
xfer.len = oled->coherent_buf.size;
xfer.bits_per_word = 8;
xfer.speed_hz = oled->spi->max_speed_hz;
/*
* 📚 关键标志: is_dma_mapped
*
* = true: 我们自己管理DMA映射
* SPI控制器驱动直接使用tx_dma地址,不做额外映射
* 适用于一致性DMA缓冲区(已经有DMA地址了)
*
* = false: SPI控制器驱动负责DMA映射
* 驱动会调用dma_map_single(tx_buf)来获取DMA地址
* 适用于普通内存缓冲区
*/
xfer.is_dma_mapped = true; /* 告诉SPI: 我已经映射好了! */
spi_message_add_tail(&xfer, &msg);
/*
* 📚 步骤3: 同步提交SPI传输
*
* spi_sync() 会阻塞直到传输完成
* 内部流程:
* 1. 将message加入SPI控制器的队列
* 2. SPI控制器驱动配置DMA控制器
* 3. DMA控制器从coherent_buf.dma_addr读取数据
* 4. DMA控制器将数据写入SPI数据寄存器
* 5. SPI控制器将数据通过MOSI线发送
* 6. DMA完成 -> 中断 -> 唤醒等待的线程
*
* 也可以使用 spi_async() 异步传输,不阻塞
*/
ret = spi_sync(oled->spi, &msg);
end = ktime_get();
elapsed_ns = ktime_to_ns(ktime_sub(end, start));
/* 更新统计 */
spin_lock(&oled->stats_lock);
if (ret == 0) {
oled->stats.dma_transfers++;
oled->stats.dma_bytes += oled->coherent_buf.size;
oled->stats.avg_transfer_time_ns =
(oled->stats.avg_transfer_time_ns * (oled->stats.dma_transfers - 1)
+ elapsed_ns) / oled->stats.dma_transfers;
} else {
oled->stats.dma_errors++;
}
oled->stats.last_transfer_start = start;
oled->stats.last_transfer_end = end;
spin_unlock(&oled->stats_lock);
mutex_unlock(&oled->buf_lock);
if (ret)
dev_err(oled->dev, "Coherent DMA transfer failed: %d\n", ret);
else
dev_dbg(oled->dev, " 传输完成, 耗时: %lld ns\n", elapsed_ns);
return ret;
}
/**
* oled_dma_transfer_streaming - 使用流式DMA传输数据
*
* 📚 流式DMA传输流程 (这是最重要的DMA概念之一):
*
* ┌──────────────────────────────────────────────────────────────┐
* │ 阶段A: CPU拥有缓冲区 │
* │ │
* │ CPU可以读写缓冲区 │
* │ 此时设备不能访问缓冲区 │
* │ │
* │ [CPU Cache] [内存] │
* │ ┌─────────┐ ┌─────────┐ │
* │ │新数据 │ │旧数据 │ <-- Cache和内存可能不一致! │
* │ └─────────┘ └─────────┘ │
* ├──────────────────────────────────────────────────────────────┤
* │ 阶段B: dma_map_single() - 将所有权交给设备 │
* │ │
* │ 对于DMA_TO_DEVICE方向: │
* │ - 将CPU Cache中的脏数据刷新(flush/writeback)到内存 │
* │ - 确保设备能看到最新的数据 │
* │ │
* │ [CPU Cache] [内存] │
* │ ┌─────────┐ ┌─────────┐ │
* │ │新数据 │───>│新数据 │ <-- Cache flush到内存 │
* │ └─────────┘ └─────────┘ │
* │ ↓ │
* │ [DMA控制器可以安全读取] │
* │ │
* │ ⚠️ 此时CPU不能再访问这块内存! │
* ├──────────────────────────────────────────────────────────────┤
* │ 阶段C: DMA传输进行中 │
* │ │
* │ DMA控制器从内存读取数据,发送到SPI控制器 │
* │ CPU不参与,可以执行其他任务 │
* │ │
* │ [内存] ──DMA──> [SPI TX FIFO] ──MOSI──> [OLED] │
* ├──────────────────────────────────────────────────────────────┤
* │ 阶段D: dma_unmap_single() - 将所有权还给CPU │
* │ │
* │ 对于DMA_TO_DEVICE方向: │
* │ - 无效化(invalidate) Cache中对应的行 │
* │ - 确保CPU下次读取时从内存获取最新数据 │
* │ │
* │ 现在CPU可以再次安全访问这块内存 │
* └──────────────────────────────────────────────────────────────┘
*
* Cache一致性问题详解:
*
* 为什么需要sync?
*
* 现代CPU使用Cache来加速内存访问。CPU写数据时,
* 数据可能只写到Cache中(write-back策略),而不是立即写到内存。
*
* DMA控制器直接访问物理内存,看不到CPU Cache中的数据。
* 如果不做sync,DMA可能读到旧数据!
*
* ┌───────┐ ┌───────────┐ ┌─────────┐
* │ CPU │────>│ L1/L2 Cache│ │ 内存 │
* │ │ │ (新数据) │ │ (旧数据) │
* └───────┘ └───────────┘ └─────────┘
* ↑
* DMA读取的是这里!
* = 旧数据 = BUG!
*
* 解决: dma_map_single() 中的cache flush
*
* ┌───────┐ ┌───────────┐ ┌─────────┐
* │ CPU │ │ L1/L2 Cache│───>│ 内存 │
* │ │ │ (新数据) │flush│ (新数据) │ ← DMA现在能读到正确数据
* └───────┘ └───────────┘ └─────────┘
*/
int oled_dma_transfer_streaming(struct spi_oled_device *oled)
{
struct spi_transfer xfer = {0};
struct spi_message msg;
struct device *dev = oled->dev;
dma_addr_t dma_addr;
ktime_t start, end;
s64 elapsed_ns;
int ret;
dev_dbg(dev, "📚 流式DMA传输开始\n");
mutex_lock(&oled->buf_lock);
start = ktime_get();
/*
* 📚 步骤1: 数据已经由CPU写入streaming_buf.vaddr
* 此时数据可能还在CPU Cache中,尚未写入物理内存
*/
/*
* 📚 步骤2: dma_map_single() - DMA映射
*
* 函数签名: dma_map_single(dev, vaddr, size, direction)
*
* 参数说明:
* dev - 执行DMA的设备
* vaddr - CPU虚拟地址(要映射的缓冲区)
* size - 映射大小
* direction - DMA方向:
* DMA_TO_DEVICE = CPU -> 设备 (TX, 我们这里用这个)
* DMA_FROM_DEVICE = 设备 -> CPU (RX)
* DMA_BIDIRECTIONAL = 双向
*
* 返回值: DMA总线地址 (设备使用的地址)
*
* 内部操作 (以DMA_TO_DEVICE为例):
* 1. 计算虚拟地址对应的物理地址
* 2. 如果有IOMMU: 在IOMMU中建立映射
* 3. Cache操作: clean (write-back) 对应的cache行
* 确保Cache中的脏数据写入内存
* 4. 返回DMA总线地址
*
* 映射后的规则 (DMA_TO_DEVICE):
* - CPU不能再写入缓冲区 (写入的数据可能不可见)
* - CPU不应该读取缓冲区 (可能读到缓存的旧数据)
* - 只有DMA控制器可以访问
*
* 错误检查: 必须用dma_mapping_error()检查返回值!
*/
dma_addr = dma_map_single(
dev, /* 设备 */
oled->streaming_buf.vaddr, /* CPU虚拟地址 */
oled->streaming_buf.size, /* 大小 */
DMA_TO_DEVICE /* 方向: CPU -> 设备 */
);
/*
* 📚 检查映射错误
*
* DMA映射可能失败的原因:
* 1. IOMMU地址空间不足
* 2. 内存区域不可映射
* 3. bounce buffer分配失败
*
* 注意: 不要用 if (dma_addr == 0) 来检查!
* 0是一个合法的DMA地址,必须用dma_mapping_error()
*/
if (dma_mapping_error(dev, dma_addr)) {
dev_err(dev, "DMA mapping failed!\n");
mutex_unlock(&oled->buf_lock);
return -EIO;
}
dev_dbg(dev, " DMA映射成功:\n");
dev_dbg(dev, " 虚拟地址: %px -> DMA地址: %pad\n",
oled->streaming_buf.vaddr, &dma_addr);
/* 保存DMA地址,用于后续unmap */
oled->streaming_buf.dma_addr = dma_addr;
/*
* 📚 (可选) 如果在map之后、DMA启动之前还需要修改数据:
*
* dma_sync_single_for_cpu(dev, dma_addr, size, direction);
* // ... 修改数据 ...
* dma_sync_single_for_device(dev, dma_addr, size, direction);
*
* sync_for_cpu: 使CPU能看到设备写入的数据 (invalidate cache)
* sync_for_device: 使设备能看到CPU写入的数据 (flush cache)
*
* 这对于需要多次重复使用同一映射的场景很有用:
*
* dma_map_single() // 初始映射
* for each transfer:
* sync_for_cpu() // CPU获取所有权
* // CPU修改缓冲区
* sync_for_device() // 设备获取所有权
* // 启动DMA传输
* // 等待完成
* dma_unmap_single() // 最终取消映射
*
* 好处: 避免反复map/unmap的开销
* 但必须严格遵守所有权协议!
*/
/* DC = 1 表示数据模式 */
gpiod_set_value_cansleep(oled->dc_gpio, 1);
/*
* 📚 步骤3: 配置SPI传输
*/
spi_message_init(&msg);
xfer.tx_buf = oled->streaming_buf.vaddr;
xfer.tx_dma = dma_addr; /* 使用我们映射的DMA地址 */
xfer.len = oled->streaming_buf.size;
xfer.bits_per_word = 8;
xfer.speed_hz = oled->spi->max_speed_hz;
xfer.is_dma_mapped = true; /* 我们自己管理DMA映射 */
spi_message_add_tail(&xfer, &msg);
/*
* 📚 步骤4: 执行传输
*
* 此时DMA控制器将:
* 从 dma_addr 读取数据 -> 写入SPI TX FIFO -> 串行发送到OLED
*/
ret = spi_sync(oled->spi, &msg);
/*
* 📚 步骤5: dma_unmap_single() - 取消DMA映射
*
* 传输完成后,必须取消映射!
*
* 函数签名: dma_unmap_single(dev, dma_addr, size, direction)
*
* 内部操作 (DMA_TO_DEVICE方向):
* 1. invalidate 对应的CPU Cache行
* (虽然对于TO_DEVICE方向不是严格必须,但实现通常会做)
* 2. 如果有IOMMU: 解除IOMMU映射
* 3. 如果使用了bounce buffer: 释放bounce buffer
*
* 取消映射后:
* - CPU重新获得缓冲区的所有权
* - DMA地址不再有效,不能再使用
* - CPU可以安全地读写缓冲区
*
* ⚠️ 常见错误:
* - 忘记unmap (内存/IOMMU泄漏)
* - 传输完成前unmap (数据损坏)
* - unmap后继续使用DMA地址 (未定义行为)
*/
dma_unmap_single(dev,
dma_addr,
oled->streaming_buf.size,
DMA_TO_DEVICE);
oled->streaming_buf.dma_addr = 0; /* 标记为未映射 */
dev_dbg(dev, " DMA映射已取消\n");
end = ktime_get();
elapsed_ns = ktime_to_ns(ktime_sub(end, start));
/* 更新统计 */
spin_lock(&oled->stats_lock);
if (ret == 0) {
oled->stats.dma_transfers++;
oled->stats.dma_bytes += oled->streaming_buf.size;
oled->stats.avg_transfer_time_ns =
(oled->stats.avg_transfer_time_ns * (oled->stats.dma_transfers - 1)
+ elapsed_ns) / oled->stats.dma_transfers;
} else {
oled->stats.dma_errors++;
}
oled->stats.last_transfer_start = start;
oled->stats.last_transfer_end = end;
spin_unlock(&oled->stats_lock);
mutex_unlock(&oled->buf_lock);
if (ret)
dev_err(dev, "Streaming DMA transfer failed: %d\n", ret);
else
dev_dbg(dev, " 传输完成, 耗时: %lld ns\n", elapsed_ns);
return ret;
}
/**
* oled_dma_transfer_sg - 使用Scatter-Gather DMA传输数据
*
* 📚 Scatter-Gather DMA传输流程:
*
* ┌─────────────────────────────────────────────────────────┐
* │ │
* │ 内存中不连续的缓冲区: │
* │ │
* │ 地址100: ┌──────┐ │
* │ │ 页0 │ 128 bytes (OLED第0页) │
* │ └──────┘ │
* │ 地址500: ┌──────┐ │
* │ │ 页1 │ 128 bytes (OLED第1页) │
* │ └──────┘ │
* │ 地址900: ┌──────┐ │
* │ │ 页2 │ 128 bytes (OLED第2页) │
* │ └──────┘ │
* │ ... │
* │ │
* │ SG列表: [100,128] -> [500,128] -> [900,128] -> ... │
* │ │
* │ dma_map_sg()后: │
* │ 每个SG条目得到一个DMA地址 │
* │ DMA控制器按链表顺序自动传输所有块 │
* │ │
* │ 好处: 不需要大块连续物理内存! │
* │ DMA控制器硬件处理跳转,无需CPU参与 │
* └─────────────────────────────────────────────────────────┘
*
* IOMMU与SG的关系:
*
* 没有IOMMU时:
* dma_map_sg() 返回的条目数 == 输入条目数
* 每个物理不连续的块对应一个DMA传输
* DMA控制器需要支持链式传输
*
* 有IOMMU时:
* IOMMU可以将物理不连续的页映射到DMA地址连续的区域
* dma_map_sg() 可能合并多个条目!
* 返回的条目数 <= 输入条目数
*
* 物理内存: IOMMU映射: 设备看到:
* ┌──────┐ ┌─────────┐ ┌──────────┐
* │ 0x100│───┐ │ │ ┌──>│ DMA 0x00 │
* └──────┘ └──>│ IOMMU │───┘ │ 连续! │
* ┌──────┐ ┌──>│ 页表 │───┐ │ │
* │ 0x500│───┘ │ │ └──>│ │
* └──────┘ └─────────┘ └──────────┘
*/
int oled_dma_transfer_sg(struct spi_oled_device *oled)
{
struct device *dev = oled->dev;
struct spi_transfer *xfers;
struct spi_message msg;
struct scatterlist *sg;
int i, mapped_nents;
ktime_t start, end;
s64 elapsed_ns;
int ret;
dev_dbg(dev, "📚 Scatter-Gather DMA传输开始\n");
mutex_lock(&oled->buf_lock);
start = ktime_get();
/*
* 📚 步骤1: dma_map_sg() - 映射整个SG表
*
* 函数签名: dma_map_sg(dev, sglist, nents, direction)
*
* 参数:
* dev - 设备
* sglist - scatterlist数组首地址
* nents - SG条目数量
* direction - DMA方向
*
* 返回值: 映射后的条目数量
* - 可能小于nents (IOMMU合并了条目)
* - 返回0表示失败
*
* 内部操作:
* 对每个SG条目:
* 1. 获取其物理地址
* 2. 做Cache操作 (flush/invalidate)
* 3. 设置DMA地址 (直接映射或通过IOMMU)
* 4. 尝试合并相邻的条目 (如果IOMMU允许)
*
* 映射后:
* 使用 for_each_sg() 遍历,用 sg_dma_address() 和
* sg_dma_len() 获取DMA地址和长度
* (而不是 sg_phys() 和 sg->length)
*/
mapped_nents = dma_map_sg(dev,
oled->sg_table.sgl,
oled->sg_nents,
DMA_TO_DEVICE);
if (mapped_nents == 0) {
dev_err(dev, "SG DMA mapping failed!\n");
mutex_unlock(&oled->buf_lock);
return -EIO;
}
dev_dbg(dev, " SG映射: %d个输入条目 -> %d个DMA条目%s\n",
oled->sg_nents, mapped_nents,
(mapped_nents < oled->sg_nents) ? " (IOMMU合并了部分条目)" : "");
/* DC = 1 表示数据模式 */
gpiod_set_value_cansleep(oled->dc_gpio, 1);
/*
* 📚 步骤2: 为每个SG条目创建SPI传输
*
* SPI框架不直接支持SG列表传输
* 我们需要将每个SG条目转换为一个spi_transfer
* 然后将所有transfer添加到一个spi_message中
*
* spi_message支持链式transfer:
* message: [xfer0] -> [xfer1] -> [xfer2] -> ...
* SPI控制器会依次执行所有transfer
* 如果支持DMA链式传输,可以一次设置所有DMA描述符
*/
xfers = kcalloc(mapped_nents, sizeof(struct spi_transfer), GFP_KERNEL);
if (!xfers) {
ret = -ENOMEM;
goto err_unmap;
}
spi_message_init(&msg);
i = 0;
for_each_sg(oled->sg_table.sgl, sg, mapped_nents, i) {
/*
* 📚 sg_dma_address() vs sg_phys()
*
* sg_dma_address(sg) - 返回DMA映射后的总线地址
* 这是DMA控制器应该使用的地址
* 如果有IOMMU,这是IOMMU虚拟地址
*
* sg_phys(sg) - 返回物理地址
* 不应该用于DMA传输!
* 除非确定没有IOMMU且DMA地址 == 物理地址
*
* sg_dma_len(sg) - 返回DMA映射后的长度
* 可能与sg->length不同(IOMMU合并后)
*
* sg_virt(sg) - 返回虚拟地址
* 用于CPU访问
*/
xfers[i].tx_buf = sg_virt(sg);
xfers[i].tx_dma = sg_dma_address(sg);
xfers[i].len = sg_dma_len(sg);
xfers[i].bits_per_word = 8;
xfers[i].speed_hz = oled->spi->max_speed_hz;
xfers[i].is_dma_mapped = true;
/*
* 📚 cs_change标志:
* 默认情况下,SPI在message的最后一个transfer后取消CS
* cs_change = 1 在非最后transfer中: 传输后短暂拉高CS再拉低
* cs_change = 0 (默认): transfer之间保持CS低电平
*
* 对OLED: 我们希望CS在整个message期间保持低
*/
xfers[i].cs_change = 0;
spi_message_add_tail(&xfers[i], &msg);
dev_dbg(dev, " SG[%d]: DMA地址=%pad, 长度=%u\n",
i, &xfers[i].tx_dma, xfers[i].len);
}
/*
* 📚 步骤3: 提交SPI消息
*
* 所有transfer会被链式执行
* SPI控制器驱动可能:
* a) 用一个大的DMA链表一次性传输所有transfer
* b) 依次为每个transfer设置DMA
* 取决于SPI控制器驱动的实现
*/
ret = spi_sync(oled->spi, &msg);
/* 释放transfer数组 */
kfree(xfers);
err_unmap:
/*
* 📚 步骤4: dma_unmap_sg() - 取消SG映射
*
* 函数签名: dma_unmap_sg(dev, sglist, nents, direction)
*
* 注意: nents参数必须是原始的条目数(不是mapped_nents)!
* dma_unmap_sg需要知道原始映射了多少条目
*
* 内部操作:
* 对每个条目执行cache操作并解除DMA/IOMMU映射
*/
dma_unmap_sg(dev,
oled->sg_table.sgl,
oled->sg_nents, /* 注意: 用原始数量,不是mapped_nents */
DMA_TO_DEVICE);
end = ktime_get();
elapsed_ns = ktime_to_ns(ktime_sub(end, start));
/* 更新统计 */
spin_lock(&oled->stats_lock);
if (ret == 0) {
oled->stats.dma_transfers++;
oled->stats.dma_bytes += OLED_BUF_SIZE;
oled->stats.avg_transfer_time_ns =
(oled->stats.avg_transfer_time_ns * (oled->stats.dma_transfers - 1)
+ elapsed_ns) / oled->stats.dma_transfers;
} else {
oled->stats.dma_errors++;
}
oled->stats.last_transfer_start = start;
oled->stats.last_transfer_end = end;
spin_unlock(&oled->stats_lock);
mutex_unlock(&oled->buf_lock);
if (ret)
dev_err(dev, "SG DMA transfer failed: %d\n", ret);
else
dev_dbg(dev, " 传输完成, 耗时: %lld ns\n", elapsed_ns);
return ret;
}
/**
* oled_pio_transfer - PIO传输(无DMA对比)
*
* 📚 PIO vs DMA 对比:
*
* PIO (Programmed I/O):
* CPU循环:
* 读取内存[i] -> 写入SPI_DR寄存器 -> 等待TXE标志 -> i++
*
* 优点: 简单,无需配置DMA
* 缺点: CPU全程占用,不能做其他事情
* 适用: 小量数据(几个字节)
*
* DMA:
* CPU配置DMA -> DMA自动传输 -> CPU做其他事 -> DMA完成中断
*
* 优点: CPU空闲,可以处理其他任务
* 缺点: 配置开销较大
* 适用: 大量数据(数百字节以上)
*
* 何时选择DMA:
* 通常SPI控制器驱动有一个阈值:
* 数据量 < 阈值: 使用PIO (配置DMA的开销 > 传输时间)
* 数据量 >= 阈值: 使用DMA (CPU节省的时间 > DMA配置开销)
* 阈值通常为 64-256 字节
*/
int oled_pio_transfer(struct spi_oled_device *oled)
{
ktime_t start, end;
s64 elapsed_ns;
int ret;
dev_dbg(oled->dev, "📚 PIO传输开始 (无DMA)\n");
mutex_lock(&oled->buf_lock);
start = ktime_get();
ret = oled_write_data_pio(oled, oled->streaming_buf.vaddr,
oled->streaming_buf.size);
end = ktime_get();
elapsed_ns = ktime_to_ns(ktime_sub(end, start));
spin_lock(&oled->stats_lock);
oled->stats.pio_transfers++;
oled->stats.last_transfer_start = start;
oled->stats.last_transfer_end = end;
spin_unlock(&oled->stats_lock);
mutex_unlock(&oled->buf_lock);
dev_dbg(oled->dev, " PIO传输完成, 耗时: %lld ns\n", elapsed_ns);
return ret;
}
/* ============================================================
* 第4部分: Framebuffer格式转换与显示刷新
* ============================================================
*/
/**
* oled_convert_fb_to_oled - 将framebuffer格式转换为OLED格式
*
* Linux framebuffer: 逐行扫描,每像素1bit
* byte0: [px7 px6 px5 px4 px3 px2 px1 px0] <- 第0行的8个像素
*
* SSD1306 OLED: 按页(page)组织,每页8行,逐列扫描
* byte0: [row7 row6 row5 row4 row3 row2 row1 row0] <- 第0列,第0页的8行像素
*
* 需要做位重排
*/
void oled_convert_fb_to_oled(struct spi_oled_device *oled, u8 *dst)
{
u8 *src = oled->fb_vmem;
int x, page;
int src_line_len = OLED_WIDTH / 8; /* 每行字节数 = 16 */
for (page = 0; page < OLED_PAGES; page++) {
for (x = 0; x < OLED_WIDTH; x++) {
u8 byte = 0;
int bit;
for (bit = 0; bit < 8; bit++) {
int y = page * 8 + bit;
int src_byte_idx = y * src_line_len + (x / 8);
int src_bit_idx = 7 - (x % 8);
if (src[src_byte_idx] & (1 << src_bit_idx))
byte |= (1 << bit);
}
dst[page * OLED_WIDTH + x] = byte;
}
}
}
/**
* oled_refresh_display - 刷新OLED显示内容
*
* 核心刷新函数: 转换格式 + DMA传输
*/
int oled_refresh_display(struct spi_oled_device *oled)
{
u8 *dst_buf;
int ret;
/* 设置OLED写入区域为全屏 */
u8 cmds[] = {
SSD1306_SET_COL_ADDR, 0, OLED_WIDTH - 1,
SSD1306_SET_PAGE_ADDR, 0, OLED_PAGES - 1,
};
ret = oled_write_cmd_seq(oled, cmds, sizeof(cmds));
if (ret)
return ret;
/* 根据DMA模式选择目标缓冲区并传输 */
switch (oled->dma_mode) {
case DMA_MODE_COHERENT:
/*
* 📚 一致性DMA: 直接写入一致性缓冲区
* CPU写入后,DMA控制器立即可见,无需sync
*/
dst_buf = oled->coherent_buf.vaddr;
oled_convert_fb_to_oled(oled, dst_buf);
ret = oled_dma_transfer_coherent(oled);
break;
case DMA_MODE_STREAMING:
/*
* 📚 流式DMA: 写入普通缓冲区
* 在dma_map_single()时会自动flush cache
*/
dst_buf = oled->streaming_buf.vaddr;
oled_convert_fb_to_oled(oled, dst_buf);
ret = oled_dma_transfer_streaming(oled);
break;
case DMA_MODE_SG:
/*
* 📚 SG DMA: 将数据分散写入各个SG缓冲区
* 每个SG条目对应OLED的一页
*/
{
u8 tmp_buf[OLED_BUF_SIZE];
struct scatterlist *sg;
int i, offset = 0;
/* 先转换到临时缓冲区 */
oled_convert_fb_to_oled(oled, tmp_buf);
/* 然后分散复制到各SG缓冲区 */
for_each_sg(oled->sg_table.sgl, sg, oled->sg_nents, i) {
void *sg_buf = sg_virt(sg);
size_t chunk = sg->length;
memcpy(sg_buf, tmp_buf + offset, chunk);
offset += chunk;
}
ret = oled_dma_transfer_sg(oled);
}
break;
case DMA_MODE_PIO:
/*
* 📚 PIO: 直接CPU传输,作为对比基准
*/
dst_buf = oled->streaming_buf.vaddr;
oled_convert_fb_to_oled(oled, dst_buf);
ret = oled_pio_transfer(oled);
break;
default:
ret = -EINVAL;
}
return ret;
}
/**
* oled_refresh_work_func - 周期性刷新工作函数
*/
static void oled_refresh_work_func(struct work_struct *work)
{
struct spi_oled_device *oled =
container_of(work, struct spi_oled_device, refresh_work.work);
if (!oled->powered_on)
return;
oled_refresh_display(oled);
/* 重新调度下一次刷新 */
if (oled->refresh_pending) {
schedule_delayed_work(&oled->refresh_work,
msecs_to_jiffies(refresh_interval));
}
}
/* ============================================================
* 第5部分: SSD1306 OLED硬件初始化
* ============================================================
*/
/**
* oled_hw_reset - 硬件复位OLED
*/
void oled_hw_reset(struct spi_oled_device *oled)
{
if (!oled->reset_gpio)
return;
gpiod_set_value_cansleep(oled->reset_gpio, 1); /* 拉高复位 */
msleep(10);
gpiod_set_value_cansleep(oled->reset_gpio, 0); /* 释放复位 */
msleep(10);
gpiod_set_value_cansleep(oled->reset_gpio, 1); /* 正常工作 */
msleep(10);
}
/**
* oled_hw_init - 初始化SSD1306 OLED
*/
int oled_hw_init(struct spi_oled_device *oled)
{
/* SSD1306 初始化序列 */
static const u8 init_cmds[] = {
SSD1306_DISPLAY_OFF,
SSD1306_SET_DISPLAY_CLOCK_DIV, 0x80,
SSD1306_SET_MUX_RATIO, 0x3F, /* 64行 */
SSD1306_SET_DISPLAY_OFFSET, 0x00,
SSD1306_SET_START_LINE | 0x00,
SSD1306_CHARGE_PUMP, 0x14, /* 使能内部charge pump */
SSD1306_SET_MEMORY_MODE, 0x00, /* 水平寻址模式 */
SSD1306_SET_SEG_REMAP, /* 段重映射 */
SSD1306_SET_COM_SCAN_DEC, /* COM扫描方向 */
SSD1306_SET_COM_PINS, 0x12,
SSD1306_SET_CONTRAST, 0xCF,
SSD1306_SET_PRECHARGE, 0xF1,
SSD1306_SET_VCOM_DETECT, 0x40,
SSD1306_DISPLAY_ALL_ON_RESUME,
SSD1306_SET_NORMAL_DISPLAY,
SSD1306_DISPLAY_ON,
};
int i, ret;
dev_info(oled->dev, "初始化SSD1306 OLED...\n");
oled_hw_reset(oled);
/* 逐个发送初始化命令 */
for (i = 0; i < sizeof(init_cmds); i++) {
ret = oled_write_cmd(oled, init_cmds[i]);
if (ret)
return ret;
}
oled->powered_on = true;
oled->contrast = 0xCF;
dev_info(oled->dev, "SSD1306 OLED初始化完成\n");
return 0;
}
/* ============================================================
* 第6部分: Sysfs接口 - 用于调试和学习
* ============================================================
*
* 通过sysfs可以:
* - 查看DMA统计信息
* - 动态切换DMA模式
* - 对比不同模式的性能
*/
/**
* 显示DMA统计信息
* cat /sys/devices/.../dma_stats
*/
static ssize_t dma_stats_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct spi_oled_device *oled = dev_get_drvdata(dev);
struct oled_dma_stats stats;
s64 last_elapsed;
spin_lock(&oled->stats_lock);
stats = oled->stats;
spin_unlock(&oled->stats_lock);
last_elapsed = ktime_to_ns(ktime_sub(stats.last_transfer_end,
stats.last_transfer_start));
return sysfs_emit(buf,
"=== DMA统计信息 ===\n"
"当前DMA模式: %d (%s)\n"
"DMA传输次数: %llu\n"
"DMA传输字节: %llu\n"
"DMA错误次数: %llu\n"
"PIO传输次数: %llu\n"
"平均传输时间: %llu ns\n"
"上次传输时间: %lld ns\n"
"\n"
"=== DMA模式说明 ===\n"
"0: Coherent - 一致性DMA (无需同步)\n"
"1: Streaming - 流式DMA (需要map/unmap)\n"
"2: SG - 分散聚集DMA\n"
"3: PIO - CPU直接传输(无DMA)\n",
oled->dma_mode,
(oled->dma_mode == 0) ? "Coherent" :
(oled->dma_mode == 1) ? "Streaming" :
(oled->dma_mode == 2) ? "Scatter-Gather" : "PIO",
stats.dma_transfers,
stats.dma_bytes,
stats.dma_errors,
stats.pio_transfers,
stats.avg_transfer_time_ns,
last_elapsed
);
}
static DEVICE_ATTR_RO(dma_stats);
/**
* 显示和设置DMA模式
* echo 1 > /sys/devices/.../dma_mode # 切换到Streaming模式
* cat /sys/devices/.../dma_mode # 查看当前模式
*/
static ssize_t dma_mode_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct spi_oled_device *oled = dev_get_drvdata(dev);
return sysfs_emit(buf, "%d\n", oled->dma_mode);
}
static ssize_t dma_mode_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct spi_oled_device *oled = dev_get_drvdata(dev);
int new_mode;
int ret;
ret = kstrtoint(buf, 10, &new_mode);
if (ret)
return ret;
if (new_mode < 0 || new_mode > DMA_MODE_PIO) {
dev_err(dev, "Invalid DMA mode: %d (valid: 0-3)\n", new_mode);
return -EINVAL;
}
if (new_mode == oled->dma_mode)
return count;
dev_info(dev, "📚 切换DMA模式: %d -> %d\n", oled->dma_mode, new_mode);
/*
* 📚 切换DMA模式流程:
* 1. 暂停刷新工作
* 2. 清理旧模式的DMA资源
* 3. 切换模式
* 4. 初始化新模式的DMA资源
* 5. 恢复刷新工作
*
* 这展示了DMA资源管理的生命周期
*/
oled->refresh_pending = false;
cancel_delayed_work_sync(&oled->refresh_work);
/* 清理旧DMA资源 */
oled_dma_cleanup(oled);
/* 切换模式并初始化新资源 */
oled->dma_mode = new_mode;
ret = oled_dma_init(oled);
if (ret) {
dev_err(dev, "Failed to init new DMA mode: %d\n", ret);
/* 回退到PIO模式 */
oled->dma_mode = DMA_MODE_PIO;
oled_dma_init(oled);
}
/* 重置统计 */
spin_lock(&oled->stats_lock);
memset(&oled->stats, 0, sizeof(oled->stats));
spin_unlock(&oled->stats_lock);
/* 恢复刷新 */
oled->refresh_pending = true;
schedule_delayed_work(&oled->refresh_work,
msecs_to_jiffies(refresh_interval));
return count;
}
static DEVICE_ATTR_RW(dma_mode);
/**
* 显示DMA地址信息 - 帮助理解地址映射
* cat /sys/devices/.../dma_addresses
*/
static ssize_t dma_addresses_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct spi_oled_device *oled = dev_get_drvdata(dev);
int len = 0;
len += sysfs_emit_at(buf, len,
"=== 📚 DMA地址映射信息 ===\n\n"
"地址类型说明:\n"
" 虚拟地址(vaddr): CPU通过MMU翻译后使用的地址\n"
" DMA地址(dma_addr): DMA控制器使用的总线地址\n"
" 物理地址: 实际内存芯片上的地址\n\n"
);
switch (oled->dma_mode) {
case DMA_MODE_COHERENT:
len += sysfs_emit_at(buf, len,
"模式: 一致性DMA (Coherent)\n"
" 虚拟地址: %px\n"
" DMA地址: %pad\n"
" 大小: %zu bytes\n"
" 特点: 两个地址指向同一物理内存\n"
" 内存被映射为non-cacheable\n"
" CPU写入后设备立即可见\n",
oled->coherent_buf.vaddr,
&oled->coherent_buf.dma_addr,
oled->coherent_buf.size
);
break;
case DMA_MODE_STREAMING:
len += sysfs_emit_at(buf, len,
"模式: 流式DMA (Streaming)\n"
" 虚拟地址: %px\n"
" DMA地址: %s\n"
" 大小: %zu bytes\n"
" 特点: DMA地址在每次传输时动态映射\n"
" 需要map/unmap操作\n"
" 内存是cacheable的(性能更好)\n",
oled->streaming_buf.vaddr,
oled->streaming_buf.dma_addr ? "已映射" : "未映射(传输时才映射)",
oled->streaming_buf.size
);
break;
case DMA_MODE_SG:
{
struct scatterlist *sg;
int i;
len += sysfs_emit_at(buf, len,
"模式: Scatter-Gather DMA\n"
" SG条目数: %d\n\n",
oled->sg_nents
);
for_each_sg(oled->sg_table.sgl, sg, oled->sg_nents, i) {
len += sysfs_emit_at(buf, len,
" SG[%d]: vaddr=%px, length=%u\n",
i, sg_virt(sg), sg->length
);
}
len += sysfs_emit_at(buf, len,
"\n 特点: 多个不连续的内存块\n"
" DMA控制器自动链式传输\n"
" IOMMU可能合并相邻条目\n"
);
}
break;
case DMA_MODE_PIO:
len += sysfs_emit_at(buf, len,
"模式: PIO (无DMA)\n"
" 缓冲区地址: %px\n"
" 大小: %zu bytes\n"
" 特点: CPU直接搬运数据,无DMA参与\n",
oled->streaming_buf.vaddr,
oled->streaming_buf.size
);
break;
}
return len;
}
static DEVICE_ATTR_RO(dma_addresses);
/**
* 手动触发一次显示刷新(用于测试)
* echo 1 > /sys/devices/.../refresh
*/
static ssize_t refresh_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct spi_oled_device *oled = dev_get_drvdata(dev);
int ret;
ret = oled_refresh_display(oled);
if (ret)
return ret;
return count;
}
static DEVICE_ATTR_WO(refresh);
/**
* DMA学习笔记 - 输出综合教学信息
* cat /sys/devices/.../dma_tutorial
*/
static ssize_t dma_tutorial_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
return sysfs_emit(buf,
"╔══════════════════════════════════════════════════════════╗\n"
"║ 📚 Linux DMA 学习指南 ║\n"
"╠══════════════════════════════════════════════════════════╣\n"
"║ ║\n"
"║ 1. DMA是什么? ║\n"
"║ Direct Memory Access - 无需CPU参与的数据搬运 ║\n"
"║ CPU只需告诉DMA:从哪里、到哪里、搬多少 ║\n"
"║ ║\n"
"║ 2. 为什么需要DMA? ║\n"
"║ - CPU不用等待慢速I/O设备 ║\n"
"║ - 数据传输期间CPU可以处理其他任务 ║\n"
"║ - 对于大量数据传输,DMA效率远高于PIO ║\n"
"║ ║\n"
"║ 3. DMA映射类型: ║\n"
"║ Coherent: 简单安全,但内存non-cacheable较慢 ║\n"
"║ Streaming: 需手动同步,但性能更好 ║\n"
"║ SG: 不连续内存也能高效传输 ║\n"
"║ ║\n"
"║ 4. 实验方法: ║\n"
"║ echo 0 > dma_mode # 切换到Coherent模式 ║\n"
"║ echo 1 > dma_mode # 切换到Streaming模式 ║\n"
"║ echo 2 > dma_mode # 切换到SG模式 ║\n"
"║ echo 3 > dma_mode # 切换到PIO模式(对比) ║\n"
"║ cat dma_stats # 查看传输统计 ║\n"
"║ cat dma_addresses # 查看地址映射 ║\n"
"║ ║\n"
"║ 5. 关键API: ║\n"
"║ dma_alloc_coherent() - 分配一致性DMA缓冲区 ║\n"
"║ dma_map_single() - 流式DMA映射 ║\n"
"║ dma_unmap_single() - 取消流式DMA映射 ║\n"
"║ dma_map_sg() - 映射scatter-gather表 ║\n"
"║ dma_sync_single_for_*() - 同步缓冲区 ║\n"
"║ dma_set_mask() - 设置DMA地址掩码 ║\n"
"║ ║\n"
"╚══════════════════════════════════════════════════════════╝\n"
);
}
static DEVICE_ATTR_RO(dma_tutorial);
/* sysfs属性组 */
static struct attribute *oled_dma_attrs[] = {
&dev_attr_dma_stats.attr,
&dev_attr_dma_mode.attr,
&dev_attr_dma_addresses.attr,
&dev_attr_refresh.attr,
&dev_attr_dma_tutorial.attr,
NULL,
};
ATTRIBUTE_GROUPS(oled_dma);
/* ============================================================
* 第7部分: Framebuffer接口
* ============================================================
*/
/**
* oled_fb_write - framebuffer写操作
* 用户空间写入framebuffer时触发刷新
*/
static ssize_t oled_fb_write(struct fb_info *info, const char __user *buf,
size_t count, loff_t *ppos)
{
struct spi_oled_device *oled = info->par;
ssize_t ret;
ret = fb_sys_write(info, buf, count, ppos);
if (ret > 0) {
/* 用户写入了数据,触发刷新 */
schedule_delayed_work(&oled->refresh_work, 0);
}
return ret;
}
/**
* oled_fb_fillrect / copyarea / imageblit
* framebuffer绘图操作,完成后触发刷新
*/
static void oled_fb_fillrect(struct fb_info *info,
const struct fb_fillrect *rect)
{
struct spi_oled_device *oled = info->par;
sys_fillrect(info, rect);
schedule_delayed_work(&oled->refresh_work, 0);
}
static void oled_fb_copyarea(struct fb_info *info,
const struct fb_copyarea *area)
{
struct spi_oled_device *oled = info->par;
sys_copyarea(info, area);
schedule_delayed_work(&oled->refresh_work, 0);
}
static void oled_fb_imageblit(struct fb_info *info,
const struct fb_image *image)
{
struct spi_oled_device *oled = info->par;
sys_imageblit(info, image);
schedule_delayed_work(&oled->refresh_work, 0);
}
/**
* oled_fb_blank - framebuffer blank/unblank
*/
static int oled_fb_blank(int blank, struct fb_info *info)
{
struct spi_oled_device *oled = info->par;
switch (blank) {
case FB_BLANK_UNBLANK:
oled_write_cmd(oled, SSD1306_DISPLAY_ON);
oled->powered_on = true;
break;
default:
oled_write_cmd(oled, SSD1306_DISPLAY_OFF);
oled->powered_on = false;
break;
}
return 0;
}
static const struct fb_ops oled_fb_ops = {
.owner = THIS_MODULE,
.fb_read = fb_sys_read,
.fb_write = oled_fb_write,
.fb_fillrect = oled_fb_fillrect,
.fb_copyarea = oled_fb_copyarea,
.fb_imageblit = oled_fb_imageblit,
.fb_blank = oled_fb_blank,
};
/**
* oled_fb_register - 注册framebuffer设备
*/
int oled_fb_register(struct spi_oled_device *oled)
{
struct fb_info *info;
int ret;
/* 分配fb_info */
info = framebuffer_alloc(0, oled->dev);
if (!info)
return -ENOMEM;
oled->fb_info = info;
/* 分配framebuffer显存 */
oled->fb_vmem_size = (OLED_WIDTH * OLED_HEIGHT) / 8;
oled->fb_vmem = devm_kzalloc(oled->dev, oled->fb_vmem_size, GFP_KERNEL);
if (!oled->fb_vmem) {
ret = -ENOMEM;
goto err_fb_release;
}
/* 配置fb_info */
info->fbops = &oled_fb_ops;
info->screen_buffer = oled->fb_vmem;
info->screen_size = oled->fb_vmem_size;
info->par = oled;
info->fix.type = FB_TYPE_PACKED_PIXELS;
info->fix.visual = FB_VISUAL_MONO10;
info->fix.line_length = OLED_WIDTH / 8;
info->fix.smem_len = oled->fb_vmem_size;
strscpy(info->fix.id, "ssd1306", sizeof(info->fix.id));
info->var.xres = OLED_WIDTH;
info->var.yres = OLED_HEIGHT;
info->var.xres_virtual = OLED_WIDTH;
info->var.yres_virtual = OLED_HEIGHT;
info->var.bits_per_pixel = 1;
info->var.red.length = 1;
info->var.green.length = 1;
info->var.blue.length = 1;
ret = register_framebuffer(info);
if (ret) {
dev_err(oled->dev, "Failed to register framebuffer: %d\n", ret);
goto err_fb_release;
}
dev_info(oled->dev, "Framebuffer注册成功: /dev/fb%d (%dx%d, 1bpp)\n",
info->node, OLED_WIDTH, OLED_HEIGHT);
return 0;
err_fb_release:
framebuffer_release(info);
oled->fb_info = NULL;
return ret;
}
void oled_fb_unregister(struct spi_oled_device *oled)
{
if (oled->fb_info) {
unregister_framebuffer(oled->fb_info);
framebuffer_release(oled->fb_info);
oled->fb_info = NULL;
}
}
/* ============================================================
* 第8部分: 高级DMA功能演示 - DMA Engine API
* ============================================================
*
* 📚 DMA Engine API 详解
*
* Linux内核有两层DMA API:
*
* 1. DMA映射API (我们前面用的):
* dma_alloc_coherent(), dma_map_single(), etc.
* 只负责内存管理和地址转换
* 不直接控制DMA控制器
*
* 2. DMA Engine API (这里演示的):
* dmaengine_*() 系列函数
* 直接与DMA控制器交互
* 可以提交DMA传输请求
*
* DMA Engine传输流程:
*
* ┌──────────────────────────────────────────────┐
* │ 1. 请求DMA通道 │
* │ chan = dma_request_chan(dev, "tx") │
* │ 从设备树中查找DMA通道绑定 │
* ├──────────────────────────────────────────────┤
* │ 2. 配置DMA通道 │
* │ dmaengine_slave_config(chan, &config) │
* │ 设置传输宽度、方向、FIFO地址等 │
* ├──────────────────────────────────────────────┤
* │ 3. 准备DMA描述符 │
* │ desc = dmaengine_prep_slave_single( │
* │ chan, dma_addr, len, │
* │ DMA_MEM_TO_DEV, flags) │
* │ 描述符包含传输的所有参数 │
* ├──────────────────────────────────────────────┤
* │ 4. 设置回调函数 │
* │ desc->callback = my_dma_callback │
* │ 传输完成时调用 │
* ├──────────────────────────────────────────────┤
* │ 5. 提交描述符 │
* │ cookie = dmaengine_submit(desc) │
* │ 加入DMA通道的待传输队列 │
* ├──────────────────────────────────────────────┤
* │ 6. 启动传输 │
* │ dma_async_issue_pending(chan) │
* │ DMA控制器开始实际传输 │
* ├──────────────────────────────────────────────┤
* │ 7. 等待完成 │
* │ wait_for_completion(&done) │
* │ 或轮询 dma_async_is_tx_complete() │
* └──────────────────────────────────────────────┘
*/
/**
* oled_dma_callback - DMA传输完成回调
*
* 📚 回调在什么上下文执行?
* - 通常在中断上下文 (tasklet/BH context)
* - 不能调用可能睡眠的函数
* - 不能调用mutex_lock()
* - 可以调用 complete() 来唤醒等待的线程
*/
static void oled_dma_callback(void *data)
{
struct spi_oled_device *oled = data;
dev_dbg(oled->dev, "📚 DMA传输完成回调被调用 (中断上下文)\n");
/* 唤醒等待传输完成的线程 */
complete(&oled->dma_completion);
}
/**
* oled_dma_engine_transfer - 使用DMA Engine API直接控制DMA传输
*
* 这个函数演示了如何绕过SPI框架,直接使用DMA Engine API
* 通常SPI框架内部会做这些,这里只是为了学习
*
* 注意: 实际项目中通常不需要这样做
* SPI控制器驱动会自动使用DMA Engine
*/
static int oled_dma_engine_transfer(struct spi_oled_device *oled)
{
struct dma_chan *chan;
struct dma_async_tx_descriptor *desc;
struct dma_slave_config config = {0};
dma_cookie_t cookie;
dma_addr_t dma_addr;
unsigned long timeout;
int ret;
dev_info(oled->dev, "📚 DMA Engine API 演示传输\n");
/*
* 📚 步骤1: 请求DMA通道
*
* dma_request_chan(dev, name):
* 从设备树中查找名为"name"的DMA通道
* 设备树示例:
* dmas = <&dma_controller channel_id>;
* dma-names = "tx";
*
* 也可以用 dma_request_slave_channel() (旧API)
*/
chan = dma_request_chan(oled->dev, "tx");
if (IS_ERR(chan)) {
dev_warn(oled->dev, " 无法获取DMA通道: %ld (这在很多平台上是正常的)\n",
PTR_ERR(chan));
dev_info(oled->dev, " 说明: SPI控制器驱动通常已经管理了DMA通道\n");
return PTR_ERR(chan);
}
oled->dma_chan_tx = chan;
dev_info(oled->dev, " DMA通道已获取: %s\n", dma_chan_name(chan));
/*
* 📚 步骤2: 配置DMA通道(slave config)
*
* 对于内存到外设(MEM_TO_DEV)的传输:
* dst_addr: 外设的FIFO物理地址(SPI TX数据寄存器)
* dst_addr_width: 每次传输的宽度(通常1字节=8位)
* dst_maxburst: 每次突发传输的最大数据个数
* direction: 传输方向
*
* 这些信息告诉DMA控制器:
* "把数据从内存搬到这个地址,每次搬1字节,最多突发N次"
*/
config.direction = DMA_MEM_TO_DEV;
config.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
config.dst_maxburst = 16;
/* config.dst_addr = SPI_TX_FIFO_PHYS_ADDR; 通常由SPI驱动设置 */
ret = dmaengine_slave_config(chan, &config);
if (ret) {
dev_err(oled->dev, " DMA slave配置失败: %d\n", ret);
goto err_release;
}
/*
* 📚 步骤3: 映射源缓冲区
*/
dma_addr = dma_map_single(oled->dev,
oled->coherent_buf.vaddr,
oled->coherent_buf.size,
DMA_TO_DEVICE);
if (dma_mapping_error(oled->dev, dma_addr)) {
ret = -EIO;
goto err_release;
}
/*
* 📚 步骤4: 准备DMA描述符
*
* dmaengine_prep_slave_single():
* 准备一个单块内存的DMA传输描述符
*
* 参数:
* chan - DMA通道
* dma_addr - 源数据的DMA地址
* len - 传输长度
* dir - 传输方向
* flags - 标志:
* DMA_PREP_INTERRUPT: 传输完成后产生中断
* DMA_CTRL_ACK: 描述符可以被重用
*
* 返回: DMA传输描述符(可以看作一个"DMA任务")
*
* 还有其他prep函数:
* dmaengine_prep_slave_sg() - SG列表传输
* dmaengine_prep_dma_memcpy() - 内存到内存拷贝
* dmaengine_prep_dma_cyclic() - 循环传输(音频DMA常用)
*/
desc = dmaengine_prep_slave_single(
chan,
dma_addr,
oled->coherent_buf.size,
DMA_MEM_TO_DEV,
DMA_PREP_INTERRUPT | DMA_CTRL_ACK
);
if (!desc) {
dev_err(oled->dev, " DMA描述符准备失败\n");
ret = -ENOMEM;
goto err_unmap;
}
/*
* 📚 步骤5: 设置完成回调
*
* DMA传输是异步的。我们需要知道什么时候传输完成。
* 方法1: 设置回调函数 (在中断上下文中调用)
* 方法2: 轮询 dma_async_is_tx_complete()
*
* 回调函数 + completion 是最常见的模式
*/
reinit_completion(&oled->dma_completion);
desc->callback = oled_dma_callback;
desc->callback_param = oled;
/*
* 📚 步骤6: 提交描述符
*
* dmaengine_submit() 将描述符加入通道的待处理队列
* 但还没有开始传输!
*
* 返回一个cookie,用于后续查询传输状态
*
* cookie的作用:
* - 标识这个特定的传输
* - 用于 dma_async_is_tx_complete() 查询状态
* - 正值表示成功,负值表示失败
*/
cookie = dmaengine_submit(desc);
if (dma_submit_error(cookie)) {
dev_err(oled->dev, " DMA提交失败\n");
ret = -EIO;
goto err_unmap;
}
dev_info(oled->dev, " DMA描述符已提交, cookie=%d\n", cookie);
/*
* 📚 步骤7: 启动传输
*
* dma_async_issue_pending() 通知DMA控制器:
* "队列中有新的传输任务,请开始处理"
*
* 这是一个触发点。之前submit只是排队,
* 调用这个函数后DMA控制器才真正开始搬运数据。
*
* DMA控制器会:
* 1. 读取描述符中的源地址、目标地址、长度
* 2. 配置DMA通道
* 3. 开始数据搬运
* 4. 完成后产生中断 (因为我们设置了DMA_PREP_INTERRUPT)
* 5. 中断处理中调用callback
*/
dma_async_issue_pending(chan);
dev_info(oled->dev, " DMA传输已启动,CPU现在可以做其他事...\n");
/*
* 📚 步骤8: 等待传输完成
*
* wait_for_completion_timeout() 会让当前线程睡眠
* 直到callback中调用complete(),或超时
*
* 在等待期间,CPU可以调度其他任务运行
* 这就是DMA的核心价值:CPU不用傻等I/O!
*
* 等待方式的选择:
* wait_for_completion() - 无限等待(危险!)
* wait_for_completion_timeout() - 有超时保护(推荐)
* poll dma_async_is_tx_complete() - 轮询模式(浪费CPU)
*
* 超时值的选择:
* 1024 bytes / SPI 10MHz = ~0.8ms
* 加上一些余量,5秒超时足够安全
*/
timeout = wait_for_completion_timeout(&oled->dma_completion,
msecs_to_jiffies(5000));
if (timeout == 0) {
dev_err(oled->dev, " ⚠️ DMA传输超时!\n");
dev_err(oled->dev, " 可能的原因:\n");
dev_err(oled->dev, " 1. DMA控制器配置错误\n");
dev_err(oled->dev, " 2. 中断未正确连接\n");
dev_err(oled->dev, " 3. 外设未就绪\n");
/*
* 📚 超时后的恢复:
* dmaengine_terminate_sync() 会:
* 1. 停止DMA通道上所有正在进行的传输
* 2. 等待所有已提交的callback完成
* 3. 将通道恢复到空闲状态
*
* 还有 dmaengine_terminate_async(),
* 不等待callback完成就返回(用于不能睡眠的上下文)
*/
dmaengine_terminate_sync(chan);
ret = -ETIMEDOUT;
} else {
/*
* 📚 传输完成后,可选地检查最终状态
*
* dma_async_is_tx_complete() 返回:
* DMA_COMPLETE - 传输成功完成
* DMA_IN_PROGRESS - 还在进行(不应该出现在这里)
* DMA_ERROR - 传输出错
* DMA_PAUSED - 传输被暂停
*/
enum dma_status status;
status = dma_async_is_tx_complete(chan, cookie, NULL, NULL);
if (status == DMA_COMPLETE) {
dev_info(oled->dev, " ✅ DMA Engine传输完成!\n");
ret = 0;
} else {
dev_err(oled->dev, " ❌ DMA传输状态异常: %d\n", status);
ret = -EIO;
}
}
err_unmap:
dma_unmap_single(oled->dev, dma_addr,
oled->coherent_buf.size, DMA_TO_DEVICE);
err_release:
if (oled->dma_chan_tx) {
dma_release_channel(oled->dma_chan_tx);
oled->dma_chan_tx = NULL;
}
return ret;
}
/* ============================================================
* 第9部分: 高级DMA概念演示函数
* ============================================================
*
* 以下函数不是OLED驱动必须的
* 而是用于演示一些高级DMA概念
*/
/**
* oled_demo_dma_pool - 演示DMA池 (DMA Pool)
*
* 📚 DMA Pool 概念:
*
* 问题: dma_alloc_coherent() 分配最少一个页面(4KB)
* 如果我们需要很多小的DMA缓冲区(比如每个64字节)
* 直接用dma_alloc_coherent会浪费大量内存
*
* 解决: DMA Pool - 预分配一大块DMA内存,然后分割使用
*
* dma_alloc_coherent() dma_pool
* ┌──────────────┐ ┌──────────────┐
* │ │ │┌────┐┌────┐ │
* │ 4KB │ ││ 64B││ 64B│ │
* │ (即使只要 │ │└────┘└────┘ │
* │ 64字节) │ │┌────┐┌────┐ │
* │ │ ││ 64B││ 64B│ │
* └──────────────┘ │└────┘└────┘ │
* 浪费 3968 bytes! │ 高效利用! │
* └──────────────┘
*
* 适用场景:
* - USB请求块(URB)
* - 网络驱动的描述符
* - 需要大量小的DMA缓冲区的场景
*/
static int oled_demo_dma_pool(struct spi_oled_device *oled)
{
struct dma_pool *pool;
void *buf1, *buf2, *buf3;
dma_addr_t dma1, dma2, dma3;
dev_info(oled->dev, "\n📚 === DMA Pool 演示 ===\n");
/*
* 📚 dma_pool_create() - 创建DMA池
*
* 参数:
* name - 池名称(用于调试)
* dev - 设备
* size - 每个分配块的大小
* align - 对齐要求(通常为cache line大小或设备要求)
* boundary - 不能跨越的边界(0表示无限制)
* 例如: boundary=4096 表示分配不能跨越4KB边界
*
* 内部实现:
* 1. 用dma_alloc_coherent()分配一大块内存(1页或多页)
* 2. 将大块内存切分成size大小的小块
* 3. 用位图或链表管理空闲小块
*/
pool = dma_pool_create(
"oled_cmd_pool", /* 池名称 */
oled->dev, /* 设备 */
64, /* 每个块64字节 */
4, /* 4字节对齐 */
0 /* 无边界限制 */
);
if (!pool) {
dev_err(oled->dev, " 创建DMA池失败\n");
return -ENOMEM;
}
dev_info(oled->dev, " DMA池已创建: 块大小=64B, 对齐=4B\n");
/*
* 📚 dma_pool_alloc() - 从池中分配
*
* 比dma_alloc_coherent()快得多(不需要整页分配)
* 同样返回虚拟地址和DMA地址
*/
buf1 = dma_pool_alloc(pool, GFP_KERNEL, &dma1);
buf2 = dma_pool_alloc(pool, GFP_KERNEL, &dma2);
buf3 = dma_pool_alloc(pool, GFP_KERNEL, &dma3);
if (buf1 && buf2 && buf3) {
dev_info(oled->dev, " 从池中分配了3个缓冲区:\n");
dev_info(oled->dev, " buf1: vaddr=%px, dma=%pad\n", buf1, &dma1);
dev_info(oled->dev, " buf2: vaddr=%px, dma=%pad\n", buf2, &dma2);
dev_info(oled->dev, " buf3: vaddr=%px, dma=%pad\n", buf3, &dma3);
/*
* 📚 观察DMA地址的规律:
* 连续分配的地址通常相差64字节(= 块大小)
* 说明它们来自同一大块DMA内存的不同偏移
*/
dev_info(oled->dev, " 地址差: buf2-buf1=%lld, buf3-buf2=%lld\n",
(s64)(dma2 - dma1), (s64)(dma3 - dma2));
}
/*
* 📚 dma_pool_free() - 归还到池中
* 不是真正释放内存,只是标记为可用
* 下次dma_pool_alloc可以重用
*/
if (buf3) dma_pool_free(pool, buf3, dma3);
if (buf2) dma_pool_free(pool, buf2, dma2);
if (buf1) dma_pool_free(pool, buf1, dma1);
/*
* 📚 dma_pool_destroy() - 销毁整个池
* 释放所有底层的DMA一致性内存
* 调用前必须归还所有分配的块!
*/
dma_pool_destroy(pool);
dev_info(oled->dev, " DMA池已销毁\n");
dev_info(oled->dev, "📚 === DMA Pool 演示结束 ===\n\n");
return 0;
}
/**
* oled_demo_bounce_buffer - 演示DMA Bounce Buffer概念
*
* 📚 Bounce Buffer (弹跳缓冲区) 概念:
*
* 问题: 有些内存区域不能直接做DMA
* - 栈上的数据 (可能不对齐,可能被换出)
* - vmalloc分配的内存 (物理不连续)
* - 高端内存 (high memory, 超出设备DMA范围)
*
* 解决: 内核自动使用bounce buffer
*
* ┌──────────────┐ ┌──────────────┐ ┌──────┐
* │ 不可DMA的内存│─→ │ bounce buffer│─→ │ 设备 │
* │ (如high mem) │copy│ (低端内存) │DMA │ │
* └──────────────┘ └──────────────┘ └──────┘
*
* 内核会:
* 1. 在DMA可达区域分配一个临时缓冲区(bounce buffer)
* 2. 将原始数据复制到bounce buffer
* 3. 用bounce buffer做DMA传输
* 4. (如果是读操作) 将结果复制回原始缓冲区
*
* 性能影响:
* - 额外的内存拷贝 (降低性能)
* - 通常内核会静默处理,驱动不需要特别关心
* - 但在性能敏感的场景中应该避免
*
* 如何避免bounce buffer:
* - 使用kmalloc而不是vmalloc
* - 不使用栈上的缓冲区
* - 设置正确的DMA掩码
* - 使用dma_alloc_coherent分配DMA缓冲区
*/
static int oled_demo_bounce_buffer(struct spi_oled_device *oled)
{
void *good_buf, *vmalloc_buf;
dma_addr_t good_dma, bad_dma;
dev_info(oled->dev, "\n📚 === Bounce Buffer 概念演示 ===\n");
/* 好的做法: 使用kmalloc */
good_buf = kmalloc(256, GFP_KERNEL | __GFP_DMA);
if (!good_buf)
return -ENOMEM;
good_dma = dma_map_single(oled->dev, good_buf, 256, DMA_TO_DEVICE);
if (!dma_mapping_error(oled->dev, good_dma)) {
dev_info(oled->dev, " ✅ kmalloc缓冲区: 直接DMA映射成功\n");
dev_info(oled->dev, " vaddr=%px, dma=%pad\n", good_buf, &good_dma);
dev_info(oled->dev, " 不需要bounce buffer\n");
dma_unmap_single(oled->dev, good_dma, 256, DMA_TO_DEVICE);
}
kfree(good_buf);
/*
* 📚 vmalloc的内存物理不连续,不能直接做DMA
* 在某些架构上,内核会使用bounce buffer
* 在另一些架构上,dma_map_single会失败
*/
vmalloc_buf = vmalloc(256);
if (vmalloc_buf) {
dev_info(oled->dev, " ⚠️ vmalloc缓冲区:\n");
dev_info(oled->dev, " vaddr=%px\n", vmalloc_buf);
dev_info(oled->dev, " vmalloc内存物理上可能不连续\n");
dev_info(oled->dev, " 不推荐用于DMA (可能需要bounce buffer)\n");
/*
* 注意: 实际上直接dma_map vmalloc内存是不安全的
* 应该使用 dma_map_page() 逐页映射
* 或使用 SG表将多个物理页组合
*
* 这里只是概念演示,不做实际映射
*/
vfree(vmalloc_buf);
}
/*
* 📚 栈上的缓冲区也不应用于DMA
*/
dev_info(oled->dev, " ⚠️ 栈缓冲区:\n");
dev_info(oled->dev, " 不要使用栈上的变量做DMA!\n");
dev_info(oled->dev, " 原因: 栈内存可能不对齐,不保证DMA安全\n");
dev_info(oled->dev, " 示例: u8 buf[128]; dma_map_single(&buf) // 错误!\n");
dev_info(oled->dev, "\n 📋 DMA内存分配最佳实践:\n");
dev_info(oled->dev, " ✅ kmalloc(size, GFP_KERNEL) - 推荐\n");
dev_info(oled->dev, " ✅ dma_alloc_coherent() - 最佳\n");
dev_info(oled->dev, " ✅ dma_pool_alloc() - 小块最佳\n");
dev_info(oled->dev, " ❌ vmalloc() - 不要用\n");
dev_info(oled->dev, " ❌ 栈变量 - 不要用\n");
dev_info(oled->dev, " ❌ 全局/静态变量 - 不推荐\n");
dev_info(oled->dev, "📚 === Bounce Buffer 演示结束 ===\n\n");
return 0;
}
/**
* oled_demo_dma_direction - 演示DMA方向的重要性
*
* 📚 DMA方向 (DMA Direction) 详解:
*
* 为什么方向很重要?
* 因为Cache一致性操作取决于方向:
*
* DMA_TO_DEVICE (CPU -> 设备, 如OLED写入):
* map时: clean(flush) cache -> 确保内存有最新数据
* unmap时: 无需操作 (设备只读,不会修改内存)
*
* DMA_FROM_DEVICE (设备 -> CPU, 如传感器读取):
* map时: invalidate cache -> 确保CPU不用旧缓存
* unmap时: invalidate cache -> 确保CPU读到设备写入的新数据
*
* DMA_BIDIRECTIONAL (双向):
* map时: clean + invalidate cache
* unmap时: invalidate cache
* 性能最差,但最安全
*
* ⚠️ 方向错误可能导致:
* - 数据损坏(设备读到旧数据)
* - 静默错误(在某些架构上看起来正常,换个架构就出bug)
* - 安全漏洞(Cache中残留敏感数据)
*/
static void oled_demo_dma_direction(struct spi_oled_device *oled)
{
dev_info(oled->dev, "\n📚 === DMA方向说明 ===\n");
dev_info(oled->dev, " 本驱动使用 DMA_TO_DEVICE 方向\n");
dev_info(oled->dev, " 因为我们只向OLED发送数据(写入像素)\n\n");
dev_info(oled->dev, " DMA方向与Cache操作的对应关系:\n");
dev_info(oled->dev, " ┌─────────────────┬──────────────┬──────────────┐\n");
dev_info(oled->dev, " │ 方向 │ map时操作 │ unmap时操作 │\n");
dev_info(oled->dev, " ├─────────────────┼──────────────┼──────────────┤\n");
dev_info(oled->dev, " │ DMA_TO_DEVICE │ flush cache │ (无) │\n");
dev_info(oled->dev, " │ DMA_FROM_DEVICE │ inv cache │ inv cache │\n");
dev_info(oled->dev, " │ DMA_BIDIRECTIONAL│flush+inv │ inv cache │\n");
dev_info(oled->dev, " └─────────────────┴──────────────┴──────────────┘\n");
dev_info(oled->dev, " flush = 将cache脏数据写回内存\n");
dev_info(oled->dev, " inv = 使cache行无效(下次CPU读取时从内存加载)\n");
dev_info(oled->dev, "📚 === DMA方向说明结束 ===\n\n");
}
/* ============================================================
* 第10部分: SPI驱动探测与移除
* ============================================================
*/
/**
* spi_oled_probe - SPI设备探测函数
*
* 当内核发现匹配的SPI设备时调用
* 这里进行所有的初始化工作
*/
static int spi_oled_probe(struct spi_device *spi)
{
struct spi_oled_device *oled;
int ret;
dev_info(&spi->dev, "╔══════════════════════════════════════╗\n");
dev_info(&spi->dev, "║ SPI OLED DMA 教学驱动 - 探测开始 ║\n");
dev_info(&spi->dev, "╚══════════════════════════════════════╝\n");
/* 分配设备结构 */
oled = devm_kzalloc(&spi->dev, sizeof(*oled), GFP_KERNEL);
if (!oled)
return -ENOMEM;
oled->spi = spi;
oled->dev = &spi->dev;
oled->dma_mode = dma_mode; /* 从模块参数获取 */
mutex_init(&oled->buf_lock);
spin_lock_init(&oled->stats_lock);
INIT_DELAYED_WORK(&oled->refresh_work, oled_refresh_work_func);
spi_set_drvdata(spi, oled);
/* 配置SPI模式 */
spi->mode = SPI_MODE_0;
spi->bits_per_word = 8;
if (!spi->max_speed_hz)
spi->max_speed_hz = 10000000; /* 10MHz */
ret = spi_setup(spi);
if (ret) {
dev_err(&spi->dev, "SPI setup failed: %d\n", ret);
return ret;
}
dev_info(&spi->dev, "SPI配置: mode=%d, bits=%d, speed=%dHz\n",
spi->mode, spi->bits_per_word, spi->max_speed_hz);
/* 获取GPIO引脚 */
oled->dc_gpio = devm_gpiod_get(&spi->dev, "dc", GPIOD_OUT_LOW);
if (IS_ERR(oled->dc_gpio)) {
dev_err(&spi->dev, "Failed to get DC GPIO: %ld\n",
PTR_ERR(oled->dc_gpio));
return PTR_ERR(oled->dc_gpio);
}
oled->reset_gpio = devm_gpiod_get_optional(&spi->dev, "reset",
GPIOD_OUT_HIGH);
if (IS_ERR(oled->reset_gpio)) {
dev_err(&spi->dev, "Failed to get reset GPIO: %ld\n",
PTR_ERR(oled->reset_gpio));
return PTR_ERR(oled->reset_gpio);
}
/*
* 📚 核心: 初始化DMA资源
* 这里会根据选择的模式分配DMA缓冲区
*/
ret = oled_dma_init(oled);
if (ret) {
dev_err(&spi->dev, "DMA initialization failed: %d\n", ret);
return ret;
}
/* 初始化OLED硬件 */
ret = oled_hw_init(oled);
if (ret) {
dev_err(&spi->dev, "OLED hardware init failed: %d\n", ret);
goto err_dma_cleanup;
}
/* 注册Framebuffer */
ret = oled_fb_register(oled);
if (ret) {
dev_err(&spi->dev, "Framebuffer registration failed: %d\n", ret);
goto err_dma_cleanup;
}
/* 运行DMA学习演示 */
oled_demo_dma_direction(oled);
oled_demo_dma_pool(oled);
oled_demo_bounce_buffer(oled);
/* 启动周期性刷新 */
oled->refresh_pending = true;
schedule_delayed_work(&oled->refresh_work,
msecs_to_jiffies(refresh_interval));
dev_info(&spi->dev, "╔══════════════════════════════════════╗\n");
dev_info(&spi->dev, "║ SPI OLED DMA 教学驱动 - 探测完成 ║\n");
dev_info(&spi->dev, "║ ║\n");
dev_info(&spi->dev, "║ 查看教程: cat dma_tutorial ║\n");
dev_info(&spi->dev, "║ 查看统计: cat dma_stats ║\n");
dev_info(&spi->dev, "║ 切换模式: echo N > dma_mode ║\n");
dev_info(&spi->dev, "╚══════════════════════════════════════╝\n");
return 0;
err_dma_cleanup:
oled_dma_cleanup(oled);
return ret;
}
/**
* spi_oled_remove - SPI设备移除函数
*
* 📚 资源释放顺序很重要:
* 与分配/注册相反的顺序释放
* 先停止活动 -> 注销接口 -> 释放资源
*/
static void spi_oled_remove(struct spi_device *spi)
{
struct spi_oled_device *oled = spi_get_drvdata(spi);
dev_info(&spi->dev, "SPI OLED DMA 驱动移除中...\n");
/* 1. 停止刷新工作 */
oled->refresh_pending = false;
cancel_delayed_work_sync(&oled->refresh_work);
/* 2. 注销framebuffer */
oled_fb_unregister(oled);
/* 3. 关闭OLED */
oled_write_cmd(oled, SSD1306_DISPLAY_OFF);
oled->powered_on = false;
/* 4. 清理DMA资源 */
oled_dma_cleanup(oled);
dev_info(&spi->dev, "SPI OLED DMA 驱动已移除\n");
}
/**
* 电源管理
*/
static int __maybe_unused spi_oled_suspend(struct device *dev)
{
struct spi_oled_device *oled = dev_get_drvdata(dev);
dev_info(dev, "驱动挂起\n");
oled->refresh_pending = false;
cancel_delayed_work_sync(&oled->refresh_work);
oled_write_cmd(oled, SSD1306_DISPLAY_OFF);
return 0;
}
static int __maybe_unused spi_oled_resume(struct device *dev)
{
struct spi_oled_device *oled = dev_get_drvdata(dev);
dev_info(dev, "驱动恢复\n");
oled_write_cmd(oled, SSD1306_DISPLAY_ON);
oled->refresh_pending = true;
schedule_delayed_work(&oled->refresh_work,
msecs_to_jiffies(refresh_interval));
return 0;
}
static SIMPLE_DEV_PM_OPS(spi_oled_pm_ops, spi_oled_suspend, spi_oled_resume);
/* 设备树匹配表 */
static const struct of_device_id spi_oled_of_match[] = {
{ .compatible = "learn,spi-oled-dma" },
{ .compatible = "solomon,ssd1306" },
{ }
};
MODULE_DEVICE_TABLE(of, spi_oled_of_match);
/* SPI设备ID表 */
static const struct spi_device_id spi_oled_ids[] = {
{ "spi-oled-dma", 0 },
{ "ssd1306", 0 },
{ }
};
MODULE_DEVICE_TABLE(spi, spi_oled_ids);
/* SPI驱动结构 */
static struct spi_driver spi_oled_driver = {
.driver = {
.name = "spi-oled-dma",
.of_match_table = spi_oled_of_match,
.pm = &spi_oled_pm_ops,
.dev_groups = oled_dma_groups, /* sysfs属性组 */
},
.probe = spi_oled_probe,
.remove = spi_oled_remove,
.id_table = spi_oled_ids,
};
module_spi_driver(spi_oled_driver);
MODULE_AUTHOR("DMA Learning Driver");
MODULE_DESCRIPTION("SPI OLED Driver with DMA tutorial - Learn DMA concepts");
MODULE_LICENSE("GPL v2");
MODULE_VERSION("1.0");