3. Camera硬件接口
摄像头驱动跑起来的第一步,就是初始化。说白了,就是让内核知道「嘿,我在这儿,这是我的资源,给我干活」。这一步要是没做好,后面调得再好也白搭。我刚开始接触R-Car平台时,就吃过设备树写错的亏,折腾了两天才发现是时钟节点名拼错了。
这一节,我们拆成四个核心环节来讲:
- platform driver注册 —— 驱动怎么挂到内核里
- 设备树解析 —— 硬件信息怎么读进来
- 时钟与电源管理 —— 让摄像头芯片「活」过来
- 中断注册 —— 让CPU能响应摄像头的事件
核心思路: 初始化不是一股脑把所有事都做完,而是按依赖关系分层推进。先注册驱动,再解析资源,然后上电配时钟,最后注册中断。顺序错了,系统可能直接崩。
3.1 platform driver注册:驱动与设备的「相亲」
在R-Car平台上,摄像头控制器通常挂载在ISP或VIN总线上,用platform driver模型来管理。你想想看,内核怎么知道哪个驱动对应哪个硬件?靠的就是compatible字符串。
我个人习惯把驱动结构体写成这样:
static const struct of_device_id rcar_vin_of_table[] = {
{ .compatible = "renesas,vin-r8a7795", .data = &rcar_vin_info_r8a7795 },
{ .compatible = "renesas,vin-r8a7796", .data = &rcar_vin_info_r8a7796 },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, rcar_vin_of_table);
static struct platform_driver rcar_vin_driver = {
.driver = {
.name = "rcar-vin",
.of_match_table = rcar_vin_of_table,
.pm = &rcar_vin_pm_ops,
},
.probe = rcar_vin_probe,
.remove = rcar_vin_remove,
};
module_platform_driver(rcar_vin_driver);
这里有个细节:module_platform_driver 是个宏,它帮你把 module_init 和 module_exit 都包好了。我早期手写过 platform_driver_register,后来发现用这个宏更干净,少写两行代码,也少犯低级错误。
小技巧: 如果你的驱动支持多个SoC版本(比如R-Car H3、M3、V3U),of_device_id 里的 .data 字段可以指向一个结构体,里面放每个版本的差异参数。probe时用 of_device_get_match_data() 取出来,避免写一堆if-else。
3.2 设备树解析:把硬件信息「读」出来
设备树(DT)是R-Car平台描述硬件的主要方式。摄像头节点里通常包含:寄存器地址、中断号、时钟源、GPIO引脚、电源域等信息。
probe函数里,我一般这样解析:
static int rcar_vin_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct rcar_vin_priv *priv;
struct resource *res;
int ret;
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
platform_set_drvdata(pdev, priv);
// 1. 解析寄存器地址
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
priv->base = devm_ioremap_resource(dev, res);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
// 2. 解析中断
priv->irq = platform_get_irq(pdev, 0);
if (priv->irq < 0)
return priv->irq;
// 3. 解析时钟
priv->clk = devm_clk_get(dev, "vin_clk");
if (IS_ERR(priv->clk))
return PTR_ERR(priv->clk);
// 4. 解析电源
priv->rstc = devm_reset_control_get_exclusive(dev, NULL);
if (IS_ERR(priv->rstc))
return PTR_ERR(priv->rstc);
// 5. 解析GPIO(如果有)
priv->pwdn_gpio = devm_gpiod_get_optional(dev, "powerdown", GPIOD_OUT_HIGH);
// ... 后续初始化
}
嗯,这里要注意:devm_ 系列函数是「托管资源」,probe失败或remove时,内核会自动释放。我建议能用 devm_ 就用,省得自己写一堆错误处理。
我曾经踩过的坑: 设备树里 clocks 属性配了两个时钟,但代码里只 devm_clk_get 了一个。结果第二个时钟没使能,摄像头初始化到一半就卡死了。后来我养成了习惯:设备树里配了几个时钟,代码里就一定要全部获取并检查。
3.3 时钟与电源管理:让摄像头「活」过来
摄像头芯片要工作,离不开三样东西:时钟、电源、复位。顺序很重要——先上电,再给时钟,最后释放复位。
R-Car平台上的时钟管理,我一般这样处理:
static int rcar_vin_power_on(struct rcar_vin_priv *priv)
{
int ret;
// 1. 先使能时钟
ret = clk_prepare_enable(priv->clk);
if (ret)
return ret;
// 2. 释放复位
ret = reset_control_deassert(priv->rstc);
if (ret) {
clk_disable_unprepare(priv->clk);
return ret;
}
// 3. 如果有外部电源控制,拉高GPIO
if (priv->pwdn_gpio)
gpiod_set_value(priv->pwdn_gpio, 0); // 0 = 正常模式
return 0;
}
static void rcar_vin_power_off(struct rcar_vin_priv *priv)
{
if (priv->pwdn_gpio)
gpiod_set_value(priv->pwdn_gpio, 1);
reset_control_assert(priv->rstc);
clk_disable_unprepare(priv->clk);
}
你想想看,如果先释放复位再给时钟,芯片内部逻辑可能处于不确定状态,轻则功能异常,重则锁死总线。我在调试一个OV5640模组时就遇到过,时序反了导致I2C通信一直NACK。
另外,R-Car的电源管理还涉及Runtime PM。我个人习惯在probe里注册 pm_runtime 回调:
pm_runtime_enable(dev);
if (!pm_runtime_enabled(dev)) {
ret = rcar_vin_power_on(priv);
if (ret)
goto err_power;
}
这样,系统可以在摄像头空闲时自动关掉时钟和电源,省电又降温。车载场景下,这个功能特别实用。
3.4 中断注册:让CPU「听」到摄像头的声音
摄像头驱动里,中断是用来通知CPU「帧准备好了」、「出错啦」、「同步信号来了」的。R-Car VIN模块的中断类型挺多,我一般只注册最关键的几个:
| 中断类型 | 触发条件 | 处理动作 |
|---|---|---|
| 帧开始 (VS) | 检测到垂直同步信号 | 清DMA缓冲区,准备接收 |
| 帧结束 (FE) | 一帧数据采集完成 | 唤醒等待队列,通知应用层 |
| 错误 (ERR) | 数据溢出、CRC错误等 | 统计错误计数,尝试恢复 |
| 行结束 (LE) | 一行数据采集完成 | 可选,用于调试或逐行处理 |
注册中断的代码很简单:
ret = devm_request_irq(dev, priv->irq, rcar_vin_irq_handler,
IRQF_TRIGGER_RISING, "rcar-vin", priv);
if (ret) {
dev_err(dev, "failed to request irq %d\n", priv->irq);
return ret;
}
中断处理函数里,我习惯先读状态寄存器,判断是哪种中断,然后清标志位,最后做对应处理:
static irqreturn_t rcar_vin_irq_handler(int irq, void *data)
{
struct rcar_vin_priv *priv = data;
u32 status;
status = readl(priv->base + VIN_INTSTS);
writel(status, priv->base + VIN_INTSTS); // 写1清中断
if (status & VIN_INT_FE) {
// 帧结束:通知DMA完成
complete(&priv->frame_completion);
}
if (status & VIN_INT_ERR) {
// 错误:记录并尝试恢复
priv->err_count++;
dev_warn_ratelimited(priv->dev, "VIN error, count=%d\n", priv->err_count);
}
return IRQ_HANDLED;
}
避坑指南: 中断标志位一定要先读再写清,而且清的时候只写已读到的位。我曾经图省事直接写 0xFFFFFFFF,结果把不该清的中断也清了,导致丢帧。另外,错误中断建议用 dev_warn_ratelimited,防止中断风暴把日志刷爆。
3.5 初始化流程总览
把上面四个环节串起来,整个初始化的流程就清晰了。我画了一张图,方便你对照理解:
从图上你能看到,整个初始化是线性推进的。每一步都依赖上一步的结果。我个人建议在probe函数里,每一步都加 dev_dbg 打印,方便调试时定位问题。