8. 异步SPI传输 + DMA完整示例
/**
* async_transfer_complete - 异步传输完成回调
*
* 📚 这个回调在什么时候被调用:
* 1. DMA传输完成
* 2. SPI控制器驱动收到DMA完成中断
* 3. SPI控制器驱动处理完中断后调用 spi_finalize_current_message()
* 4. SPI框架调用 message->complete() 回调 = 这个函数
*
* 执行上下文:
* 可能在中断上下文(softirq/tasklet)
* 也可能在SPI控制器的工作线程中
* 具体取决于SPI控制器驱动实现
*
* 注意事项:
* - 不要做耗时操作
* - 不要调用可能睡眠的函数 (如mutex_lock, kmalloc(GFP_KERNEL))
* - 可以: complete(), wake_up(), tasklet_schedule()
* - 如果需要做复杂后处理,应该调度到工作队列
*/
struct async_transfer_ctx {
struct spi_oled_device *oled;
struct completion done;
int status; /* 传输结果 */
ktime_t submit_time; /* 提交时间 */
ktime_t complete_time; /* 完成时间 */
int frame_id; /* 帧编号(调试用) */
};
static void async_transfer_complete(void *context)
{
struct async_transfer_ctx *ctx = context;
ctx->complete_time = ktime_get();
ctx->status = 0; /* spi_message.status 已由SPI框架设置 */
dev_dbg(ctx->oled->dev,
"📚 异步传输完成! 帧=%d, CPU空闲时间=%lld ns\n",
ctx->frame_id,
ktime_to_ns(ktime_sub(ctx->complete_time, ctx->submit_time)));
/*
* 📚 唤醒等待的线程
*
* 在真正的双缓冲场景中,这里不一定要唤醒
* 可以直接启动下一次传输(如果下一帧已准备好)
*/
complete(&ctx->done);
}
/**
* oled_async_dma_transfer - 异步DMA传输完整示例
*
* 📚 异步传输 vs 同步传输对比:
*
* 同步传输 (spi_sync):
* ─时间──────────────────────────────────>
* CPU: [准备数据][spi_sync()...等待...返回][准备下一帧]
* DMA: [========传输========]
* ↑ CPU在这段时间被阻塞 ↑
*
* 异步传输 (spi_async):
* ─时间──────────────────────────────────>
* CPU: [准备数据][spi_async()返回][准备下一帧!!][callback]
* DMA: [========传输========]
* ↑ CPU在这段时间可以工作 ↑
*
* 异步传输+乒乓缓冲 (最优):
* ─时间──────────────────────────────────────────>
* CPU: [写bufA][spi_async(A)][写bufB][spi_async(B)][写bufA]...
* DMA: [==传输bufA===] [==传输bufB===]
* CPU和DMA完全并行,没有任何等待!
*/
static int oled_async_dma_transfer(struct spi_oled_device *oled)
{
struct async_transfer_ctx ctx;
struct spi_transfer xfer = {0};
struct spi_message msg;
unsigned long timeout;
int ret;
dev_info(oled->dev, "\n📚 === 异步DMA传输演示 ===\n");
/* 确保有数据可传输 */
if (!oled->coherent_buf.vaddr) {
dev_err(oled->dev, " 没有可用的DMA缓冲区\n");
return -EINVAL;
}
/* 初始化上下文 */
memset(&ctx, 0, sizeof(ctx));
ctx.oled = oled;
ctx.frame_id = 0;
init_completion(&ctx.done);
/* 准备数据 */
memset(oled->coherent_buf.vaddr, 0xAA, oled->coherent_buf.size);
/* DC = 1 数据模式 */
gpiod_set_value_cansleep(oled->dc_gpio, 1);
/*
* 📚 构建SPI消息
*
* 关键: 设置 msg.complete 回调
* spi_async完成后会调用这个回调
*/
spi_message_init(&msg);
xfer.tx_buf = oled->coherent_buf.vaddr;
xfer.tx_dma = oled->coherent_buf.dma_addr;
xfer.len = oled->coherent_buf.size;
xfer.is_dma_mapped = true;
xfer.bits_per_word = 8;
xfer.speed_hz = oled->spi->max_speed_hz;
spi_message_add_tail(&xfer, &msg);
/* 设置完成回调 */
msg.complete = async_transfer_complete;
msg.context = &ctx;
ctx.submit_time = ktime_get();
/*
* 📚 spi_async() - 异步提交SPI传输
*
* 与spi_sync()的区别:
* spi_sync(): 提交 + 等待完成,阻塞调用
* spi_async(): 仅提交到队列,立即返回,非阻塞
*
* spi_async() 内部流程:
* 1. 验证message参数
* 2. 将message加入SPI控制器的传输队列
* 3. 如果队列之前为空,触发SPI控制器开始处理
* 4. 立即返回 (不等待传输完成)
*
* 传输完成时:
* SPI控制器驱动 → spi_finalize_current_message()
* → 调用 msg.complete() 回调
*
* 返回值:
* 0 = 成功加入队列 (不是传输成功!)
* 负值 = 加入队列失败
*
* ⚠️ 注意:
* - spi_async可以在中断上下文中调用
* - 在msg.complete回调被调用之前,不能修改msg和xfer
* - 不能在回调被调用之前释放相关的DMA缓冲区
*/
ret = spi_async(oled->spi, &msg);
if (ret) {
dev_err(oled->dev, " spi_async() 提交失败: %d\n", ret);
return ret;
}
dev_info(oled->dev, " spi_async() 已返回! (传输在后台进行)\n");
dev_info(oled->dev, " CPU现在可以做其他事情...\n");
/*
* 📚 CPU空闲时间 - 这是DMA的核心价值
*
* 在这段时间里,CPU可以:
* - 准备下一帧的显示数据
* - 处理用户输入
* - 运行其他任务
* - 进入低功耗状态
*
* 我们这里简单地模拟一些"有用的工作"
*/
{
volatile int dummy = 0;
int j;
dev_info(oled->dev, " [模拟CPU做其他工作...]\n");
for (j = 0; j < 10000; j++)
dummy += j; /* 模拟计算 */
dev_info(oled->dev, " [其他工作完成,现在等待DMA]\n");
}
/*
* 📚 等待传输完成
*
* 虽然我们用了异步传输,但最终还是要等完成
* 因为我们不能在传输完成前修改缓冲区
*
* 在真正的乒乓模式中,不需要等待:
* CPU继续写另一个缓冲区
* DMA完成回调中切换缓冲区
*/
timeout = wait_for_completion_timeout(&ctx.done, msecs_to_jiffies(5000));
if (timeout == 0) {
dev_err(oled->dev, " 异步传输超时!\n");
/*
* 📚 超时处理
*
* 对于spi_async,需要取消未完成的传输:
* 没有直接的spi_cancel()函数
* 通常需要reset SPI控制器
*/
return -ETIMEDOUT;
}
/* 检查传输结果 */
if (msg.status != 0) {
dev_err(oled->dev, " 异步传输错误: %d\n", msg.status);
return msg.status;
}
dev_info(oled->dev, " ✅ 异步传输成功完成!\n");
dev_info(oled->dev, " 提交到完成时间: %lld ns\n",
ktime_to_ns(ktime_sub(ctx.complete_time, ctx.submit_time)));
/*
* 📚 spi_async的实际字节数
* msg.actual_length 包含实际传输的字节数
*/
dev_info(oled->dev, " 实际传输: %u / %u 字节\n",
msg.actual_length, oled->coherent_buf.size);
dev_info(oled->dev, "📚 === 异步DMA传输演示结束 ===\n\n");
return 0;
}
/* ============================================================
* 📚 补充: 完整的异步乒乓DMA传输
* ============================================================
*/
/**
* oled_async_pingpong - 真正的异步乒乓DMA传输
*
* 📚 这是DMA最高效的使用方式
* CPU准备数据和DMA传输完全并行
*/
#define NUM_PINGPONG_BUFS 2
struct pingpong_state {
struct spi_oled_device *oled;
/* 两个DMA缓冲区 */
struct oled_dma_buf bufs[NUM_PINGPONG_BUFS];
/* 对应的SPI消息和传输描述符 */
struct spi_message msgs[NUM_PINGPONG_BUFS];
struct spi_transfer xfers[NUM_PINGPONG_BUFS];
/* 完成信号 */
struct completion buf_done[NUM_PINGPONG_BUFS];
/* 统计 */
int current_buf; /* 当前CPU正在写的缓冲区 */
int frames_completed;
ktime_t start_time;
};
static void pingpong_complete_callback(void *context)
{
struct pingpong_state *state = context;
/*
* 📚 DMA传输完成回调
*
* 在中断/softirq上下文中执行
* 只做最少的工作: 标记完成
*/
state->frames_completed++;
complete(&state->buf_done[state->current_buf ^ 1]); /* 唤醒等另一个buf的线程 */
}
static int oled_async_pingpong(struct spi_oled_device *oled)
{
struct pingpong_state state;
struct device *dev = oled->dev;
int i, ret;
int num_frames = 100;
dev_info(dev, "\n📚 === 异步乒乓DMA传输演示 ===\n");
memset(&state, 0, sizeof(state));
state.oled = oled;
/* 分配两个一致性DMA缓冲区 */
for (i = 0; i < NUM_PINGPONG_BUFS; i++) {
state.bufs[i].size = OLED_BUF_SIZE;
state.bufs[i].vaddr = dma_alloc_coherent(dev,
state.bufs[i].size,
&state.bufs[i].dma_addr,
GFP_KERNEL);
if (!state.bufs[i].vaddr) {
dev_err(dev, " 分配缓冲区 %d 失败\n", i);
ret = -ENOMEM;
goto err_free;
}
init_completion(&state.buf_done[i]);
/* 初始标记为"完成",这样第一次写入不用等 */
complete(&state.buf_done[i]);
dev_info(dev, " BUF[%d]: vaddr=%px, dma=%pad\n",
i, state.bufs[i].vaddr, &state.bufs[i].dma_addr);
}
state.current_buf = 0;
state.start_time = ktime_get();
dev_info(dev, " 开始 %d 帧异步乒乓传输...\n", num_frames);
for (i = 0; i < num_frames; i++) {
int cpu_idx = state.current_buf;
int dma_idx = cpu_idx ^ 1; /* 另一个缓冲区 */
unsigned long timeout;
/*
* 📚 等待当前CPU缓冲区可用
*
* 如果上一次该缓冲区的DMA传输还没完成,需要等待
* 正常情况下:
* - CPU处理时间 > DMA传输时间: 不需要等待
* - CPU处理时间 < DMA传输时间: 需要短暂等待
*/
timeout = wait_for_completion_timeout(&state.buf_done[cpu_idx],
msecs_to_jiffies(1000));
if (timeout == 0) {
dev_err(dev, " 帧 %d: 等待缓冲区 %d 超时\n", i, cpu_idx);
ret = -ETIMEDOUT;
break;
}
/*
* 📚 CPU写入当前缓冲区
*
* 使用一致性DMA,写入立即对设备可见
* 此时另一个缓冲区可能正在被DMA传输
*/
reinit_completion(&state.buf_done[cpu_idx]);
memset(state.bufs[cpu_idx].vaddr, (i * 17) & 0xFF,
state.bufs[cpu_idx].size);
/*
* 📚 提交异步DMA传输
*/
gpiod_set_value_cansleep(oled->dc_gpio, 1);
spi_message_init(&state.msgs[cpu_idx]);
memset(&state.xfers[cpu_idx], 0, sizeof(struct spi_transfer));
state.xfers[cpu_idx].tx_buf = state.bufs[cpu_idx].vaddr;
state.xfers[cpu_idx].tx_dma = state.bufs[cpu_idx].dma_addr;
state.xfers[cpu_idx].len = state.bufs[cpu_idx].size;
state.xfers[cpu_idx].is_dma_mapped = true;
state.xfers[cpu_idx].bits_per_word = 8;
state.xfers[cpu_idx].speed_hz = oled->spi->max_speed_hz;
spi_message_add_tail(&state.xfers[cpu_idx], &state.msgs[cpu_idx]);
state.msgs[cpu_idx].complete = pingpong_complete_callback;
state.msgs[cpu_idx].context = &state;
ret = spi_async(oled->spi, &state.msgs[cpu_idx]);
if (ret) {
dev_err(dev, " 帧 %d: spi_async失败: %d\n", i, ret);
break;
}
/* 切换到另一个缓冲区 */
state.current_buf = dma_idx;
}
/* 等待最后一次传输完成 */
for (i = 0; i < NUM_PINGPONG_BUFS; i++) {
wait_for_completion_timeout(&state.buf_done[i],
msecs_to_jiffies(1000));
}
{
ktime_t end = ktime_get();
s64 total_ns = ktime_to_ns(ktime_sub(end, state.start_time));
dev_info(dev, " 乒乓传输结果:\n");
dev_info(dev, " 完成帧数: %d\n", state.frames_completed);
dev_info(dev, " 总耗时: %lld ns (%.2lld ms)\n",
total_ns, total_ns / 1000000);
dev_info(dev, " 平均每帧: %lld ns\n",
state.frames_completed > 0 ?
total_ns / state.frames_completed : 0);
dev_info(dev, " 等效FPS: %lld\n",
state.frames_completed > 0 ?
(s64)1000000000LL * state.frames_completed / total_ns : 0);
}
err_free:
for (i = 0; i < NUM_PINGPONG_BUFS; i++) {
if (state.bufs[i].vaddr) {
dma_free_coherent(dev, state.bufs[i].size,
state.bufs[i].vaddr, state.bufs[i].dma_addr);
}
}
dev_info(dev, "📚 === 异步乒乓DMA演示结束 ===\n\n");
return ret;
}
/* ============================================================
* 📚 补充: DMA调试技巧
* ============================================================
*/
/**
* oled_debug_dma_info - 输出系统DMA调试信息
*
* 帮助用户了解当前系统的DMA环境
*/
static void oled_debug_dma_info(struct spi_oled_device *oled)
{
struct device *dev = oled->dev;
dev_info(dev, "\n📚 === 系统DMA环境信息 ===\n");
/* DMA掩码信息 */
dev_info(dev, " 设备DMA掩码: %#llx\n",
dev->dma_mask ? *dev->dma_mask : 0);
dev_info(dev, " 一致性DMA掩码: %#llx\n",
dev->coherent_dma_mask);
/* DMA地址范围 */
dev_info(dev, " DMA可寻址范围: 0 ~ %#llx (%llu MB)\n",
dev->dma_mask ? *dev->dma_mask : 0,
dev->dma_mask ? (*dev->dma_mask + 1) / (1024 * 1024) : 0);
/* IOMMU信息 */
#ifdef CONFIG_IOMMU_API
dev_info(dev, " IOMMU: 已启用 (CONFIG_IOMMU_API=y)\n");
if (dev->iommu_group)
dev_info(dev, " IOMMU组: 存在\n");
else
dev_info(dev, " IOMMU组: 不存在 (设备可能不在IOMMU后面)\n");
#else
dev_info(dev, " IOMMU: 未启用\n");
dev_info(dev, " DMA地址 = 物理地址\n");
#endif
/* DMA ops信息 */
#ifdef CONFIG_DMA_OPS
dev_info(dev, " DMA操作: 使用平台特定DMA ops\n");
#else
dev_info(dev, " DMA操作: 使用直接映射 (dma-direct)\n");
#endif
/* 平台信息 */
#ifdef CONFIG_ARM
dev_info(dev, " 架构: ARM\n");
dev_info(dev, " Cache: 通常为VIVT/VIPT,需要显式flush/invalidate\n");
dev_info(dev, " DMA sync操作会执行真正的cache维护\n");
#elif defined(CONFIG_ARM64)
dev_info(dev, " 架构: ARM64 (AArch64)\n");
dev_info(dev, " Cache: PIPT,DMA sync执行cache维护\n");
dev_info(dev, " 一致性DMA映射通常使用non-cacheable属性\n");
#elif defined(CONFIG_X86)
dev_info(dev, " 架构: x86/x86_64\n");
dev_info(dev, " Cache: 硬件一致性 (Cache snooping)\n");
dev_info(dev, " DMA sync操作通常是空操作 (no-op)\n");
dev_info(dev, " ⚠️ 注意: 在x86上不做sync也可能正常工作\n");
dev_info(dev, " 但代码移植到ARM后就会出bug!\n");
dev_info(dev, " 所以始终应该正确使用DMA API\n");
#endif
dev_info(dev, "\n 📋 调试命令:\n");
dev_info(dev, " # 启用DMA API调试跟踪\n");
dev_info(dev, " echo 'file dma-debug.c +p' > /sys/kernel/debug/dynamic_debug/control\n");
dev_info(dev, " \n");
dev_info(dev, " # 查看DMA映射错误\n");
dev_info(dev, " cat /sys/kernel/debug/dma-api/errors\n");
dev_info(dev, " \n");
dev_info(dev, " # 查看所有活跃的DMA映射\n");
dev_info(dev, " cat /sys/kernel/debug/dma-api/dump\n");
dev_info(dev, " \n");
dev_info(dev, " # 查看DMA API统计\n");
dev_info(dev, " cat /sys/kernel/debug/dma-api/num_errors\n");
dev_info(dev, " cat /sys/kernel/debug/dma-api/num_free\n");
dev_info(dev, " cat /sys/kernel/debug/dma-api/min_free_entries\n");
dev_info(dev, "\n 📋 DMA问题排查清单:\n");
dev_info(dev, " □ 是否设置了正确的DMA掩码?\n");
dev_info(dev, " □ 是否检查了dma_mapping_error()?\n");
dev_info(dev, " □ map/unmap是否配对?\n");
dev_info(dev, " □ DMA方向是否正确?\n");
dev_info(dev, " □ 是否在正确的时机做了sync?\n");
dev_info(dev, " □ 是否在DMA完成前取消了映射?\n");
dev_info(dev, " □ 缓冲区是否用kmalloc分配(不是vmalloc/栈)?\n");
dev_info(dev, " □ 是否有IOMMU相关的错误?\n");
dev_info(dev, "📚 === 系统DMA环境信息结束 ===\n\n");
}
/* ============================================================
* 📚 最终总结: DMA学习路线图
* ============================================================
*
* 第1阶段: 基础概念
* ✅ 什么是DMA,为什么需要DMA
* ✅ PIO vs DMA对比
* ✅ DMA地址 vs 虚拟地址 vs 物理地址
* ✅ DMA掩码
*
* 第2阶段: DMA映射API
* ✅ 一致性DMA (dma_alloc_coherent / dma_free_coherent)
* ✅ 流式DMA (dma_map_single / dma_unmap_single)
* ✅ DMA同步 (dma_sync_single_for_cpu / dma_sync_single_for_device)
* ✅ Scatter-Gather (sg_alloc_table / dma_map_sg / dma_unmap_sg)
* ✅ DMA Pool (dma_pool_create / dma_pool_alloc)
*
* 第3阶段: Cache一致性
* ✅ 为什么需要Cache同步
* ✅ DMA方向与Cache操作的关系
* ✅ 所有权模型 (CPU拥有 vs 设备拥有)
* ✅ 不同架构的Cache行为差异
*
* 第4阶段: DMA Engine API
* ✅ DMA通道 (dma_request_chan)
* ✅ 从设备配置 (dmaengine_slave_config)
* ✅ 描述符 (dmaengine_prep_slave_single / prep_slave_sg)
* ✅ 提交和启动 (dmaengine_submit / dma_async_issue_pending)
* ✅ 完成回调和等待
*
* 第5阶段: 高级技巧
* ✅ 流式DMA映射重用 (sync代替map/unmap)
* ✅ 乒乓缓冲区 (Double Buffering)
* ✅ 异步传输 (spi_async + callback)
* ✅ Bounce Buffer概念
* ✅ IOMMU与DMA的关系
* ✅ 内存屏障 (DMA Barriers)
* ✅ DMA调试技巧
*
* 第6阶段: 实践应用
* □ 阅读真实的SPI控制器驱动源码 (如 spi-bcm2835.c)
* □ 阅读网卡驱动的DMA实现 (如 stmmac)
* □ 阅读块设备驱动的DMA实现 (如 mmc)
* □ 阅读USB控制器驱动的DMA实现
* □ 在真实硬件上运行本驱动,观察DMA行为
* □ 使用ftrace/perf跟踪DMA操作
*
* 推荐阅读:
* - Documentation/core-api/dma-api.rst (内核DMA API文档)
* - Documentation/core-api/dma-api-howto.rst (DMA API使用指南)
* - Documentation/core-api/dma-isa-lpc.rst (ISA DMA)
* - drivers/spi/spi-bcm2835.c (树莓派SPI驱动,很好的DMA参考)
* - drivers/dma/ (各种DMA控制器驱动)
*/
9. 完整编译测试脚本 (scripts/run_test.sh)
#!/bin/bash
#
# SPI OLED DMA 驱动 - 自动测试脚本
#
# 用法: sudo ./scripts/run_test.sh
#
set -e
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
MODULE_NAME="spi_oled_dma"
MODULE_FILE="spi-oled-dma.ko"
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
log_title() { echo -e "\n${BLUE}═══ $* ═══${NC}\n"; }
# 检查root权限
if [ "$(id -u)" -ne 0 ]; then
log_error "请使用sudo运行此脚本"
exit 1
fi
# 查找sysfs路径
find_sysfs() {
SYSFS_PATH=$(find /sys/devices -name 'dma_mode' -path '*spi*' 2>/dev/null | head -1 | xargs dirname 2>/dev/null)
if [ -z "$SYSFS_PATH" ]; then
log_warn "未找到驱动sysfs路径"
return 1
fi
log_info "sysfs路径: $SYSFS_PATH"
return 0
}
# 读取sysfs属性
read_attr() {
local attr="$1"
if [ -f "$SYSFS_PATH/$attr" ]; then
cat "$SYSFS_PATH/$attr"
else
log_warn "属性 $attr 不存在"
fi
}
# 写入sysfs属性
write_attr() {
local attr="$1"
local value="$2"
if [ -f "$SYSFS_PATH/$attr" ]; then
echo "$value" > "$SYSFS_PATH/$attr"
log_info "写入 $attr = $value"
else
log_warn "属性 $attr 不存在"
fi
}
# ═══════════════════════════════════════
# 测试1: 编译驱动
# ═══════════════════════════════════════
test_compile() {
log_title "测试1: 编译驱动模块"
if [ ! -f "Makefile" ]; then
log_error "未找到Makefile,请在驱动源码目录运行"
exit 1
fi
log_info "开始编译..."
make clean 2>/dev/null || true
if make; then
log_info "✅ 编译成功"
ls -la "$MODULE_FILE"
else
log_error "❌ 编译失败"
exit 1
fi
}
# ═══════════════════════════════════════
# 测试2: 加载驱动
# ═══════════════════════════════════════
test_load() {
local mode=${1:-0}
log_title "测试2: 加载驱动 (DMA模式=$mode)"
# 先卸载已有模块
if lsmod | grep -q "$MODULE_NAME"; then
log_info "卸载已有模块..."
rmmod "$MODULE_NAME" 2>/dev/null || true
sleep 1
fi
if [ ! -f "$MODULE_FILE" ]; then
log_error "模块文件不存在: $MODULE_FILE"
return 1
fi
log_info "加载模块 (dma_mode=$mode)..."
if insmod "$MODULE_FILE" dma_mode="$mode"; then
log_info "✅ 模块加载成功"
else
log_error "❌ 模块加载失败"
log_info "查看dmesg获取详细信息:"
dmesg | tail -20
return 1
fi
# 等待设备初始化
sleep 2
# 显示加载日志
log_info "驱动初始化日志:"
dmesg | grep -i "oled\|dma" | tail -30
echo ""
# 查找sysfs路径
if find_sysfs; then
log_info "✅ sysfs接口已就绪"
else
log_warn "sysfs接口未找到(可能需要检查设备树)"
fi
# 检查framebuffer
if ls /dev/fb* 2>/dev/null; then
log_info "✅ Framebuffer设备已创建"
else
log_warn "未发现framebuffer设备"
fi
}
# ═══════════════════════════════════════
# 测试3: DMA信息查看
# ═══════════════════════════════════════
test_dma_info() {
log_title "测试3: 查看DMA信息"
if ! find_sysfs; then
log_error "驱动未加载或sysfs不可用"
return 1
fi
echo ""
log_info "📚 DMA教程:"
echo "─────────────────────────────────────"
read_attr "dma_tutorial"
echo ""
log_info "📊 DMA统计:"
echo "─────────────────────────────────────"
read_attr "dma_stats"
echo ""
log_info "🔍 DMA地址映射:"
echo "─────────────────────────────────────"
read_attr "dma_addresses"
echo ""
}
# ═══════════════════════════════════════
# 测试4: DMA模式切换
# ═══════════════════════════════════════
test_mode_switch() {
log_title "测试4: DMA模式切换"
if ! find_sysfs; then
log_error "驱动未加载或sysfs不可用"
return 1
fi
local modes=("0:Coherent" "1:Streaming" "2:Scatter-Gather" "3:PIO")
for mode_info in "${modes[@]}"; do
local mode="${mode_info%%:*}"
local name="${mode_info##*:}"
echo ""
log_info "切换到模式 $mode ($name)..."
write_attr "dma_mode" "$mode"
sleep 1
# 验证切换成功
local current=$(read_attr "dma_mode")
if [ "$current" = "$mode" ]; then
log_info "✅ 模式切换成功: $name"
else
log_error "❌ 模式切换失败: 期望=$mode, 实际=$current"
fi
# 显示地址信息
log_info "当前DMA地址:"
read_attr "dma_addresses" | head -10
# 触发一次刷新
if [ -f "$SYSFS_PATH/refresh" ]; then
echo 1 > "$SYSFS_PATH/refresh"
log_info "已触发一次刷新"
fi
sleep 1
done
# 切回Coherent模式
write_attr "dma_mode" "0"
}
# ═══════════════════════════════════════
# 测试5: 性能基准测试
# ═══════════════════════════════════════
test_benchmark() {
log_title "测试5: DMA性能基准测试"
if ! find_sysfs; then
log_error "驱动未加载或sysfs不可用"
return 1
fi
local fb_dev=""
for fb in /dev/fb*; do
if [ -c "$fb" ]; then
fb_dev="$fb"
break
fi
done
if [ -z "$fb_dev" ]; then
log_warn "未找到framebuffer设备,使用sysfs刷新方式测试"
fb_dev=""
fi
local modes=("0:Coherent" "1:Streaming" "2:Scatter-Gather" "3:PIO")
local results=()
echo ""
echo "╔══════════════════════════════════════════════╗"
echo "║ DMA模式性能基准测试 ║"
echo "╠══════════════════════════════════════════════╣"
for mode_info in "${modes[@]}"; do
local mode="${mode_info%%:*}"
local name="${mode_info##*:}"
local num_transfers=50
echo "║ ║"
echo "║ 测试模式: $name (mode=$mode) ║"
# 切换模式
write_attr "dma_mode" "$mode" 2>/dev/null
sleep 1
# 执行多次刷新
local start_time=$(date +%s%N)
for ((i=0; i<num_transfers; i++)); do
if [ -n "$fb_dev" ]; then
# 写随机数据到framebuffer
dd if=/dev/urandom of="$fb_dev" bs=1024 count=1 2>/dev/null
else
# 使用sysfs触发刷新
echo 1 > "$SYSFS_PATH/refresh" 2>/dev/null
fi
done
local end_time=$(date +%s%N)
local elapsed_ms=$(( (end_time - start_time) / 1000000 ))
local avg_us=0
if [ $num_transfers -gt 0 ]; then
avg_us=$(( (end_time - start_time) / num_transfers / 1000 ))
fi
echo "║ 次数: $num_transfers ║"
echo "║ 总耗时: ${elapsed_ms}ms ║"
echo "║ 平均: ${avg_us}us/次 ║"
results+=("$name: ${avg_us}us/transfer")
# 读取内核统计
local kernel_stats=$(read_attr "dma_stats" 2>/dev/null | grep "平均传输时间" | awk '{print $NF}')
if [ -n "$kernel_stats" ]; then
echo "║ 内核测量: ${kernel_stats} ║"
fi
sleep 1
done
echo "║ ║"
echo "╠══════════════════════════════════════════════╣"
echo "║ 📊 结果汇总: ║"
for r in "${results[@]}"; do
printf "║ %-40s ║\n" "$r"
done
echo "║ ║"
echo "║ 📚 观察要点: ║"
echo "║ - DMA模式通常在大数据量时优势明显 ║"
echo "║ - 对于1KB数据,DMA开销可能抵消其优势 ║"
echo "║ - 查看CPU使用率来评估DMA的真正价值 ║"
echo "╚══════════════════════════════════════════════╝"
# 切回默认模式
write_attr "dma_mode" "0" 2>/dev/null
}
# ═══════════════════════════════════════
# 测试6: DMA调试信息
# ═══════════════════════════════════════
test_debug() {
log_title "测试6: 系统DMA调试信息"
echo "📋 DMA相关内核配置:"
echo "─────────────────────────────────────"
if [ -f /boot/config-$(uname -r) ]; then
grep -E "CONFIG_DMA" /boot/config-$(uname -r) 2>/dev/null | head -20
elif [ -f /proc/config.gz ]; then
zcat /proc/config.gz | grep -E "CONFIG_DMA" 2>/dev/null | head -20
else
log_warn "无法读取内核配置"
fi
echo ""
echo "📋 IOMMU信息:"
echo "─────────────────────────────────────"
dmesg | grep -i "iommu\|smmu" | head -10
if [ -d /sys/kernel/iommu_groups ]; then
local num_groups=$(ls /sys/kernel/iommu_groups/ 2>/dev/null | wc -l)
echo "IOMMU组数量: $num_groups"
else
echo "IOMMU: 未启用或不可用"
fi
echo ""
echo "📋 DMA API调试信息:"
echo "─────────────────────────────────────"
if [ -d /sys/kernel/debug/dma-api ]; then
echo "DMA API错误:"
cat /sys/kernel/debug/dma-api/num_errors 2>/dev/null || echo " 不可用"
echo ""
echo "活跃DMA映射:"
cat /sys/kernel/debug/dma-api/num_free 2>/dev/null || echo " 不可用"
echo ""
echo "DMA映射详情 (前10条):"
head -10 /sys/kernel/debug/dma-api/dump 2>/dev/null || echo " 不可用"
else
log_warn "DMA API调试不可用"
echo "启用方法: 内核配置 CONFIG_DMA_API_DEBUG=y"
echo "或启动参数添加: dma_debug=on"
fi
echo ""
echo "📋 DMA控制器:"
echo "─────────────────────────────────────"
if [ -d /sys/class/dma ]; then
ls /sys/class/dma/ 2>/dev/null
else
echo " /sys/class/dma 不存在"
fi
echo ""
echo "📋 SPI控制器DMA能力:"
echo "─────────────────────────────────────"
for spi_dev in /sys/bus/spi/devices/*/; do
if [ -d "$spi_dev" ]; then
echo " $spi_dev"
cat "$spi_dev/modalias" 2>/dev/null
fi
done
}
# ═══════════════════════════════════════
# 测试7: 压力测试
# ═══════════════════════════════════════
test_stress() {
log_title "测试7: DMA压力测试"
if ! find_sysfs; then
log_error "驱动未加载"
return 1
fi
local duration=${1:-30}
echo "运行 ${duration}秒 压力测试..."
echo "监控: dmesg | grep -i 'error\|fail\|warn'"
echo ""
# 记录初始错误计数
local initial_errors=$(read_attr "dma_stats" 2>/dev/null | grep "错误" | awk '{print $NF}')
local start_time=$(date +%s)
local frame_count=0
local mode_switches=0
while true; do
local now=$(date +%s)
local elapsed=$((now - start_time))
if [ $elapsed -ge $duration ]; then
break
fi
# 每5秒切换一次DMA模式
if [ $((elapsed % 5)) -eq 0 ] && [ $((elapsed / 5 % 2)) -eq 0 ]; then
local new_mode=$((RANDOM % 4))
write_attr "dma_mode" "$new_mode" 2>/dev/null
mode_switches=$((mode_switches + 1))
fi
# 持续刷新
echo 1 > "$SYSFS_PATH/refresh" 2>/dev/null
frame_count=$((frame_count + 1))
# 每10秒报告一次状态
if [ $((frame_count % 100)) -eq 0 ]; then
printf "\r 进度: %d/%d秒, 帧数: %d, 模式切换: %d" \
"$elapsed" "$duration" "$frame_count" "$mode_switches"
fi
# 短暂延时
usleep 10000 2>/dev/null || sleep 0.01
done
echo ""
echo ""
# 检查结果
local final_errors=$(read_attr "dma_stats" 2>/dev/null | grep "错误" | awk '{print $NF}')
local new_errors=$((final_errors - initial_errors))
echo "╔══════════════════════════════════════╗"
echo "║ 压力测试结果 ║"
echo "╠══════════════════════════════════════╣"
printf "║ 运行时间: %-24s ║\n" "${duration}秒"
printf "║ 总帧数: %-26s ║\n" "$frame_count"
printf "║ 模式切换: %-24s ║\n" "$mode_switches"
printf "║ 新增DMA错误: %-20s ║\n" "$new_errors"
if [ "$new_errors" -eq 0 ]; then
echo "║ 状态: ✅ 通过 ║"
else
echo "║ 状态: ❌ 有错误发生 ║"
fi
echo "╚══════════════════════════════════════╝"
# 检查内核日志中的错误
local kernel_errors=$(dmesg | tail -100 | grep -ic "error\|fail\|bug\|oops" || true)
if [ "$kernel_errors" -gt 0 ]; then
log_warn "内核日志中发现 $kernel_errors 条错误/警告信息:"
dmesg | tail -100 | grep -i "error\|fail\|bug\|oops"
fi
}
# ═══════════════════════════════════════
# 测试8: 卸载驱动
# ═══════════════════════════════════════
test_unload() {
log_title "测试8: 卸载驱动"
if lsmod | grep -q "$MODULE_NAME"; then
log_info "卸载模块..."
rmmod "$MODULE_NAME"
sleep 1
log_info "卸载日志:"
dmesg | tail -10
# 验证卸载
if lsmod | grep -q "$MODULE_NAME"; then
log_error "❌ 模块卸载失败"
else
log_info "✅ 模块已成功卸载"
fi
# 检查DMA资源泄漏
if [ -d /sys/kernel/debug/dma-api ]; then
local leaked=$(cat /sys/kernel/debug/dma-api/num_errors 2>/dev/null)
if [ -n "$leaked" ] && [ "$leaked" -gt 0 ]; then
log_warn "检测到DMA API错误,可能存在资源泄漏!"
cat /sys/kernel/debug/dma-api/dump 2>/dev/null | head -5
else
log_info "✅ 无DMA资源泄漏"
fi
fi
else
log_info "模块未加载"
fi
}
# ═══════════════════════════════════════
# 主菜单
# ═══════════════════════════════════════
print_menu() {
echo ""
echo "╔══════════════════════════════════════════════════╗"
echo "║ SPI OLED DMA 教学驱动 - 测试套件 ║"
echo "╠══════════════════════════════════════════════════╣"
echo "║ ║"
echo "║ 用法: $0 <命令> ║"
echo "║ ║"
echo "║ 命令: ║"
echo "║ all - 运行所有测试 ║"
echo "║ compile - 编译驱动 ║"
echo "║ load [N] - 加载驱动 (N=DMA模式0-3) ║"
echo "║ info - 查看DMA信息 ║"
echo "║ switch - 测试模式切换 ║"
echo "║ bench - 性能基准测试 ║"
echo "║ debug - 系统DMA调试信息 ║"
echo "║ stress [S]- 压力测试 (S=秒数,默认30) ║"
echo "║ unload - 卸载驱动 ║"
echo "║ ║"
echo "║ 示例: ║"
echo "║ sudo $0 all ║"
echo "║ sudo $0 load 1 ║"
echo "║ sudo $0 stress 60 ║"
echo "║ ║"
echo "╚══════════════════════════════════════════════════╝"
echo ""
}
# 主程序
case "${1:-}" in
all)
test_compile
test_load 0
test_dma_info
test_mode_switch
test_benchmark
test_debug
test_stress 10
test_unload
echo ""
log_info "🎉 所有测试完成!"
;;
compile)
test_compile
;;
load)
test_load "${2:-0}"
;;
info)
test_dma_info
;;
switch)
test_mode_switch
;;
bench)
test_benchmark
;;
debug)
test_debug
;;
stress)
test_stress "${2:-30}"
;;
unload)
test_unload
;;
*)
print_menu
;;
esac