Linux DMA(四)

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
posted @ 2026-02-26 16:21  21code  阅读(2)  评论(0)    收藏  举报