第一部分:Amlogic Canvas —— 视频像素缓冲区元数据中间件
1.1 设计精髓分析
Amlogic Canvas本质上是一个硬件级别的像素缓冲区描述符池。它存储每个编号对应的宽度、高度、物理地址、包裹模式、块模式(GXBB及之后还支持端序)等元数据。视频解码器、显示控制器等IP不直接操作物理地址,而是通过Canvas索引读写像素数据,这带来了以下核心优势:
地址隔离与安全:IP无需知晓真实物理地址,简化地址空间管理,便于实施内存保护。
多IP共享零拷贝:解码器将解码帧写入Canvas N,显示引擎直接从同一个Canvas N读取,无需拷贝或复杂IPC。
动态重配置:只需更新Canvas条目即可改变缓冲区属性,无需中断数据流。
硬件资源有限:SoC规定为256个Canvas,驱动必须高效管理这一稀缺资源。
设计精髓:Canvas驱动不是一个普通的字符设备或单独功能模块,而是一个系统级中间件。它需要向上层(如视频解码器驱动、DRM/KMS显示驱动)提供统一的分配、释放、配置API,并保证并发安全。
1.2 从模块/组件/中间件三维度看Canvas驱动编写规范
1.2.1 作为内核模块(Module)
规范:实现为
platform_driver,在probe时映射寄存器,初始化一个全局的Canvas资源池(如位图或IDR),并导出符号供其他驱动使用。关键点:模块加载应尽早(通过
subsys_initcall或设备链接),因为在显示或解码驱动加载时可能需要Canvas服务。错误处理:若寄存器映射失败,必须
-ENODEV,但若仅是资源池初始化失败,应仍可加载但拒绝分配。
1.2.2 作为组件(Component)
在某些内核中,Canvas可能作为媒体子系统的一个组件,通过
component framework与解码器、显示控制器绑定,但通常更简单的方式是直接导出API。然而,若存在复杂的硬件依赖(如时钟、电源域),可使用component_add将Canvas注册为master,等待其它组件就绪。规范建议:若Canvas本身依赖始终开启的时钟,应使用
devm_clk_get_optional,并在配置寄存器前确保时钟使能。
1.2.3 作为中间件(Middleware)
精髓在于提供面向服务的抽象接口。典型API设计:
int meson_canvas_alloc(struct device *dev, u8 *canvas_id)int meson_canvas_free(struct device *dev, u8 canvas_id)int meson_canvas_config(struct device *dev, u8 canvas_id, struct meson_canvas_desc *desc)
这些函数导出为EXPORT_SYMBOL_GPL,并在内部使用自旋锁保护Canvas池。
中间件必须优雅处理分配耗尽,返回
-ENOSPC,并可通过debugfs导出当前使用情况。
1.3 驱动实现步骤精髓
步骤精髓:
定义全局池:使用
DECLARE_BITMAP和spinlock。probe中初始化硬件:尽管Canvas只是查找表,寄存器区域很小(示例中0x48起始0x14大小),但需要确保它可以被访问。启用相关时钟。
导出分配/释放接口:这是Canvas驱动的灵魂,必须原子操作。
配置接口:根据用户传入的描述符(phyaddr, width, height, wrap, block_mode, endian)写入硬件寄存器。注意每个Canvas条目对应固定的寄存器偏移,计算方式为:
base + (id * entry_size)。调试接口:创建debugfs文件,cat时打印所有已分配Canvas的ID和属性。
代码骨架:
/** * @file meson-canvas.c * @brief Amlogic Canvas Video Lookup Table Driver * * Provides allocation and configuration APIs for canvas entries used by * video decoders, display controllers, etc. */ #include <linux/module.h> #include <linux/platform_device.h> #include <linux/spinlock.h> #include <linux/bitmap.h> #include <linux/debugfs.h> #include <linux/export.h> #include <linux/io.h> /* 硬件参数 */ #define MESON_CANVAS_NUM 256 #define MESON_CANVAS_ENTRY_SIZE 0x4 /* 每个条目4字节,实际根据硬件调整 */ /** * struct meson_canvas_desc - Canvas descriptor to be written to hw * @phyaddr: physical address of pixel buffer * @width: width in pixels * @height: height in pixels * @wrap: wrapping mode * @block_mode: block mode * @endian: 0 little, 1 big */ struct meson_canvas_desc { dma_addr_t phyaddr; u16 width; u16 height; u8 wrap; u8 block_mode; u8 endian; }; /** * struct meson_canvas - Device private data * @dev: device pointer * @base: mapped register base * @lock: spinlock protecting @used bitmap and hw access * @used: bitmap of allocated canvas IDs * @debugfs: debugfs directory entry */ struct meson_canvas { struct device *dev; void __iomem *base; spinlock_t lock; /* 保护位图与寄存器访问的序列化 */ DECLARE_BITMAP(used, MESON_CANVAS_NUM); struct dentry *debugfs; }; static struct meson_canvas *global_canvas; /* 全局实例,方便导出API使用 */ /** * @brief Write a canvas descriptor to hardware registers * @param canvas driver data * @param id canvas index 0..255 * @param desc descriptor to write * @note Caller must hold canvas->lock */ static void meson_canvas_hw_write(struct meson_canvas *canvas, u8 id, struct meson_canvas_desc *desc) { void __iomem *reg = canvas->base + id * MESON_CANVAS_ENTRY_SIZE; u32 val; /* 根据实际寄存器布局拼装数值,这里仅为示例 */ val = (desc->phyaddr & 0xfffffff0) | (desc->wrap << 3) | (desc->block_mode << 2); writel(val, reg); /* 如果有额外寄存器设置宽高等,继续写入 */ } /** * @brief Allocate a canvas ID * @param dev consumer device (unused but kept for API consistency) * @param canvas_id output parameter, allocated id * @return 0 on success, -ENOSPC if none available, -EINVAL if args invalid * * Must be called from client drivers. Id is valid until freed. */ int meson_canvas_alloc(struct device *dev, u8 *canvas_id) { unsigned long flags; int id; if (!global_canvas) return -ENODEV; spin_lock_irqsave(&global_canvas->lock, flags); id = find_first_zero_bit(global_canvas->used, MESON_CANVAS_NUM); if (id >= MESON_CANVAS_NUM) { spin_unlock_irqrestore(&global_canvas->lock, flags); dev_err(dev, "No free canvas available\n"); return -ENOSPC; } __set_bit(id, global_canvas->used); spin_unlock_irqrestore(&global_canvas->lock, flags); *canvas_id = id; return 0; } EXPORT_SYMBOL_GPL(meson_canvas_alloc); /** * @brief Free a previously allocated canvas ID * @param dev consumer device * @param canvas_id the id to free * @return 0 on success, -EINVAL if id out of range or not allocated */ int meson_canvas_free(struct device *dev, u8 canvas_id) { unsigned long flags; if (!global_canvas || canvas_id >= MESON_CANVAS_NUM) return -EINVAL; spin_lock_irqsave(&global_canvas->lock, flags); if (!test_bit(canvas_id, global_canvas->used)) { spin_unlock_irqrestore(&global_canvas->lock, flags); dev_err(dev, "Canvas %u not in use\n", canvas_id); return -EINVAL; } __clear_bit(canvas_id, global_canvas->used); /* 可选:清除硬件条目以免残留错误配置 */ spin_unlock_irqrestore(&global_canvas->lock, flags); return 0; } EXPORT_SYMBOL_GPL(meson_canvas_free); /** * @brief Configure a canvas with pixel buffer metadata * @param dev consumer device * @param canvas_id canvas to configure * @param desc pointer to descriptor * @return 0 on success, -EINVAL if id invalid or not allocated */ int meson_canvas_config(struct device *dev, u8 canvas_id, struct meson_canvas_desc *desc) { unsigned long flags; int ret = 0; if (!global_canvas || canvas_id >= MESON_CANVAS_NUM || !desc) return -EINVAL; spin_lock_irqsave(&global_canvas->lock, flags); if (!test_bit(canvas_id, global_canvas->used)) { ret = -EINVAL; goto out; } meson_canvas_hw_write(global_canvas, canvas_id, desc); out: spin_unlock_irqrestore(&global_canvas->lock, flags); return ret; } EXPORT_SYMBOL_GPL(meson_canvas_config); static int meson_canvas_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct meson_canvas *canvas; struct resource *res; canvas = devm_kzalloc(dev, sizeof(*canvas), GFP_KERNEL); if (!canvas) return -ENOMEM; canvas->dev = dev; spin_lock_init(&canvas->lock); res = platform_get_resource(pdev, IORESOURCE_MEM, 0); canvas->base = devm_ioremap_resource(dev, res); if (IS_ERR(canvas->base)) return PTR_ERR(canvas->base); global_canvas = canvas; /* 保存全局指针 */ /* debugfs 用于观察 canvas 占用 */ canvas->debugfs = debugfs_create_dir("meson-canvas", NULL); debugfs_create_bitmap("used", 0400, canvas->debugfs, canvas->used, MESON_CANVAS_NUM); dev_info(dev, "Amlogic Canvas driver probed, %d entries\n", MESON_CANVAS_NUM); return 0; } static const struct of_device_id meson_canvas_match[] = { { .compatible = "amlogic,canvas" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, meson_canvas_match); static struct platform_driver meson_canvas_drv = { .probe = meson_canvas_probe, .driver = { .name = "meson-canvas", .of_match_table = meson_canvas_match, }, }; module_platform_driver(meson_canvas_drv); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Amlogic Canvas Video Lookup Table Driver");1.4 场景调试
Canvas耗尽(-ENOSPC)查看
/sys/kernel/debug/meson-canvas/used位图,确认是否有泄漏。在解码器或显示驱动中,检查是否配对调用了alloc和free。使用trace_printk记录分配释放栈。显示花屏或解码错位可能Canvas配置的phyaddr未对齐,或宽高、包裹模式错误。通过debugfs导出当前活跃Canvas的配置寄存器值对比,可临时添加debugfs文件打印每个条目内容。
并发竞争导致内核崩溃确保所有对
canvas->used和硬件寄存器的访问都在自旋锁内。可使用lockdep检测。若发现死锁,检查是否在持有锁期间调用了可能睡眠的函数(应避免)。驱动加载顺序问题若Canvas驱动尚未加载,依赖模块会返回
-ENODEV。使用device_link_add或deferred probe确保正确顺序。在Canvas用户驱动中,可添加EPROBE_DEFER处理。
第二部分:Amlogic Clock Measurer —— 内部时钟测量模块
2.1 设计精髓
该IP提供了一种非侵入式测量内部时钟频率的方法,精度在MHz级别。它本身不是一个时钟源,而是时钟状态的调试工具。精髓在于:
硬件事先选通:通过选择器连接至待测时钟,测量其周期计数,再由软件换算频率。
简单寄存器接口:通常只有一个控制/状态寄存器,驱动去复杂化逻辑。
面向调试:输出结果不用于系统时钟框架,而是纯粹的信息展示,因此天然适合debugfs用户空间接口。
精髓分析:驱动不需要提供复杂的API,只需将测量值呈现给开发者,辅助验证DVFS、PLL配置是否正确。
2.2 从三大方向看编写规范
作为模块:标准
platform_driver,probe时映射寄存器,创建debugfs文件。模块卸载时清理debugfs。作为组件:非典型组件,因为它不向其他驱动提供服务,而是终端用户工具。若系统需要自动化时钟检查,可考虑导出
meson_clk_measure_read()符号供其他调试模块使用,但通常直接通过debugfs交互。作为中间件:若认为它可以向时钟驱动提供“测量当前频率”回调,需定义清晰接口,但通常测量功能需要停止时钟或干扰,不适合在线使用。因此保持轻量级模块即可。
2.3 驱动实现步骤精髓
步骤精髓:
映射寄存器。测量前需要选择时钟源(通过写入寄存器选择位),触发测量,等待完成标志,读取计数值。
由于测量可能耗时(微秒级),轮询时使用
readl_poll_timeout。将计数值转换为频率:
freq = (ref_clk_freq * count) / divider。参考时钟通常为固定的外部晶振,可从设备树或代码常量获取。将所有可测时钟ID收集在一张表中,通过debugfs的
measure文件触发单项测量,或measure_all循环输出。
/** * @file meson-clk-measure.c * @brief Amlogic Internal Clock Measurer Driver * * Exposes clock frequency measurements via debugfs for debugging purposes. */ /** * struct meson_clk_msr - device data * @dev: device * @base: registers * @debugfs_root: debugfs directory * @measure_lock: mutex to serialize measurements (hardware single channel) * @clk_list: array of known measurable clock names and selectors */ struct meson_clk_msr { struct device *dev; void __iomem *base; struct dentry *debugfs_root; struct mutex measure_lock; /* 测量必须串行 */ const struct meson_clk_msr_id *clks; unsigned int num_clks; u32 ref_clk_hz; /* 参考时钟频率,例如24MHz */ }; /** * struct meson_clk_msr_id - measurable clock descriptor * @name: clock name as shown in debugfs * @sel: value to write to selection register */ struct meson_clk_msr_id { const char *name; u8 sel; }; /** * @brief Perform measurement for a given clock selector * @param msr driver data * @param sel clock selector * @return frequency in Hz, or 0 on error * * Steps: write sel, start measure, wait for completion, read counter, * compute Hz. */ static unsigned long meson_clk_msr_measure(struct meson_clk_msr *msr, u8 sel) { u32 reg, count; int ret; /* 选择时钟并启动测量(具体位定义参照手册) */ writel((sel << 8) | BIT(0), msr->base); /* 等待测量完成(BIT(1) 表示完成) */ ret = readl_poll_timeout(msr->base, reg, reg & BIT(1), 10, 10000); if (ret) { dev_err(msr->dev, "Measurement timeout for sel %u\n", sel); return 0; } count = (reg >> 16) & 0xffff; /* 计数器偏移 */ /* 频率 = 参考频率 * 计数值 / 分频比(假设分频100) */ return (msr->ref_clk_hz * count) / 100; } /** * @brief debugfs write handler for "measure" file * @param file debugfs file * @param user_buf userspace string containing clock name * @param count length * @param ppos position * @return bytes consumed or error */ static ssize_t meson_clk_msr_debugfs_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos) { struct meson_clk_msr *msr = file->f_inode->i_private; char buf[32]; unsigned int i; unsigned long freq; if (count >= sizeof(buf)) return -EINVAL; if (copy_from_user(buf, user_buf, count)) return -EFAULT; buf[count] = 0; if (buf[count-1] == '\n') buf[count-1] = 0; for (i = 0; i < msr->num_clks; i++) { if (strcmp(msr->clks[i].name, buf) == 0) break; } if (i == msr->num_clks) { dev_err(msr->dev, "Unknown clock: %s\n", buf); return -EINVAL; } mutex_lock(&msr->measure_lock); freq = meson_clk_msr_measure(msr, msr->clks[i].sel); mutex_unlock(&msr->measure_lock); if (freq) dev_info(msr->dev, "%s: %lu Hz\n", buf, freq); else dev_info(msr->dev, "%s: measurement failed\n", buf); return count; } static const struct file_operations meson_clk_msr_fops = { .owner = THIS_MODULE, .write = meson_clk_msr_debugfs_write, }; static int meson_clk_msr_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct meson_clk_msr *msr; struct resource *res; msr = devm_kzalloc(dev, sizeof(*msr), GFP_KERNEL); if (!msr) return -ENOMEM; msr->dev = dev; mutex_init(&msr->measure_lock); res = platform_get_resource(pdev, IORESOURCE_MEM, 0); msr->base = devm_ioremap_resource(dev, res); if (IS_ERR(msr->base)) return PTR_ERR(msr->base); /* 参考时钟通常为24MHz,可从设备树获取或硬编码 */ msr->ref_clk_hz = 24000000; /* 填充可测时钟表(简化) */ /* 实际应基于compatible选取不同的表 */ msr->clks = NULL; /* 示例省略 */ msr->num_clks = 0; /* 创建debugfs接口 */ msr->debugfs_root = debugfs_create_dir("meson-clk-measure", NULL); debugfs_create_file("measure", 0200, msr->debugfs_root, msr, &meson_clk_msr_fops); /* 可添加measure_all文件触发循环测量 */ dev_info(dev, "Amlogic Clock Measurer probed\n"); return 0; } static const struct of_device_id meson_clk_msr_match[] = { { .compatible = "amlogic,meson-gx-clk-measure" }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(of, meson_clk_msr_match); static struct platform_driver meson_clk_msr_drv = { .probe = meson_clk_msr_probe, .driver = { .name = "meson-clk-measure", .of_match_table = meson_clk_msr_match, }, }; module_platform_driver(meson_clk_msr_drv); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Amlogic Clock Measurer Driver");2.4 场景调试
测量超时检查参考时钟是否正确。确认硬件提供的测量时钟门控是否打开(可能依赖于其他时钟域)。添加
dev_dbg打印寄存器值判断状态机是否正常。频率为0或明显错误确认选择的时钟线是否正确连接。部分SoC的测量通道可能被其他功能复用,需在设备树中正确配置引脚或存在特定的开关寄存器。
并发测量冲突由于硬件只有一个测量引擎,必须使用
mutex或自旋锁确保一次只有一个测量进行,否则读取的计数值会混乱。已在示例中使用measure_lock。用户空间无debugfs确认内核配置了
CONFIG_DEBUG_FS,挂载点/sys/kernel/debug已挂载。驱动加载后会自动创建目录。
总结
Canvas作为中间件提供全局资源抽象,通过导出的API服务于多个多媒体驱动,其精髓在于无锁化的资源池管理与简洁的硬件抽象。
Clock Measurer作为调试模块,直接面向用户空间输出测量数据,遵循最小化内核复杂度原则,完全依托debugfs实现人机交互。
两者在编写内核代码时均需严格遵守:
使用
devm_*资源管理减少泄漏。合理选择锁机制(spinlock用于短临界区,mutex用于可能阻塞的操作)。
充分利用重要注释提高代码可维护性并生成文档。
针对不同SoC通过
of_device_id灵活区分。
可更好地驾驭Amlogic平台的多媒体与调试基础设施。