Linux Platform 总线设备驱动模型之 ISP SDHCI GPU PMIC SARADC NPU STMMAC/Rockchip MIPI D-PHY MDIO
第一部分 ISP 图像信号处理器
第一章 ISP 控制器在 Platform Bus 中的位置
ISP (Image Signal Processor) 是 SoC 中专门用于处理摄像头图像数据的硬件模块。它接收来自 CSI 控制器的原始 Bayer 数据(RAW),经过黑电平校正、去马赛克、白平衡、色彩校正、降噪、锐化等处理,输出高质量的 YUV 或 RGB 图像供 CPU 或编码器使用。
在 Linux 5.10 中,ISP 子系统通常集成在 V4L2 框架下,位于 drivers/media/platform/,典型驱动包括 rockchip-isp.c、omap3isp.c 等。
1.1 硬件关键概念
-
ISP 流水线:多个硬件处理模块串联(黑电平、去马赛克、增益、色彩转换等)。
-
统计引擎:收集曝光、白平衡统计数据,供 3A 算法使用。
-
DMA 引擎:支持多路输出(RAW、YUV、统计信息)。
-
中断:帧开始/结束、统计完成、错误等。
-
V4L2 子设备:通过 V4L2 框架暴露控制接口。
1.2 核心代码
// 基于 Linux 5.10 drivers/media/platform/rockchip/isp/rockchip_isp.c (精简版)
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <media/v4l2-device.h>
#include <media/v4l2-ioctl.h>
#include <media/videobuf2-dma-contig.h>
/**
* @brief Rockchip ISP 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 ISP 硬件实例。
*/
struct rkisp_device {
void __iomem *base; /**< 映射后的寄存器基址 */
int irq; /**< 中断号 */
struct clk *aclk; /**< 总线时钟 */
struct clk *pclk; /**< 总线时钟 (APB) */
struct clk *cclk; /**< 核心时钟 */
struct device *dev; /**< 设备指针 */
struct v4l2_device v4l2_dev; /**< V4L2 设备抽象 */
struct video_device *vdev; /**< 视频设备节点 */
struct vb2_queue queue; /**< 视频缓冲区队列 */
spinlock_t lock; /**< 硬件保护锁 */
struct rkisp_buffer *curr_buf; /**< 当前正在处理的缓冲区 */
struct list_head buffers; /**< 缓冲队列 */
u32 frame_count; /**< 帧计数 */
u32 isp_version; /**< ISP 硬件版本 */
};
/* 寄存器偏移量 (Rockchip ISP) */
#define ISP_CTRL 0x00
#define ISP_STATUS 0x04
#define ISP_INT_EN 0x08
#define ISP_INT_STAT 0x0C
#define ISP_RAW_CFG 0x10
#define ISP_DEMOSAIC_CFG 0x14
#define ISP_GAIN_CFG 0x18
#define ISP_CCM_CFG 0x1C
#define ISP_DMA_CTRL 0x20
#define ISP_DMA_ADDR_Y 0x24
#define ISP_DMA_ADDR_CB 0x28
#define ISP_DMA_ADDR_CR 0x2C
#define ISP_STATS_BASE 0x30
#define ISP_STATS_LEN 0x34
/**
* @brief 配置 ISP 黑电平校正。
*
* @param isp 指向 rkisp_device 结构。
* @param r 红色通道黑电平。
* @param gr 绿色-红色通道黑电平。
* @param gb 绿色-蓝色通道黑电平。
* @param b 蓝色通道黑电平。
*/
static void rkisp_set_black_level(struct rkisp_device *isp,
u16 r, u16 gr, u16 gb, u16 b)
{
u32 reg = (r << 0) | (gr << 8) | (gb << 16) | (b << 24);
writel(reg, isp->base + ISP_RAW_CFG);
}
/**
* @brief 配置 ISP 增益。
*
* @param isp 指向 rkisp_device 结构。
* @param gain_r 红色增益。
* @param gain_g 绿色增益。
* @param gain_b 蓝色增益。
*/
static void rkisp_set_gain(struct rkisp_device *isp,
u16 gain_r, u16 gain_g, u16 gain_b)
{
u32 reg = (gain_r << 0) | (gain_g << 8) | (gain_b << 16);
writel(reg, isp->base + ISP_GAIN_CFG);
}
/**
* @brief 配置 ISP 颜色校正矩阵 (CCM)。
*
* @param isp 指向 rkisp_device 结构。
* @param ccm 3x3 矩阵系数。
*/
static void rkisp_set_ccm(struct rkisp_device *isp, u32 ccm[3][3])
{
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
writel(ccm[i][j], isp->base + ISP_CCM_CFG + (i * 3 + j) * 4);
}
}
}
/**
* @brief 启动 ISP 视频流。
*
* @param isp 指向 rkisp_device 结构。
*/
static void rkisp_start_streaming(struct rkisp_device *isp)
{
u32 ctrl;
// 1. 检查是否有缓冲区可用
if (list_empty(&isp->buffers)) {
dev_err(isp->dev, "No buffers available\n");
return;
}
// 2. 设置 DMA 地址
isp->curr_buf = list_first_entry(&isp->buffers,
struct rkisp_buffer, list);
list_del_init(&isp->curr_buf->list);
dma_addr_t dma_addr = vb2_dma_contig_plane_dma_addr(&isp->curr_buf->vbuf.vb2_buf, 0);
writel(dma_addr, isp->base + ISP_DMA_ADDR_Y);
// 3. 启用 ISP 流水线
ctrl = (1 << 0) | (1 << 1) | (1 << 2); // 启用 RAW、去马赛克、CCM
writel(ctrl, isp->base + ISP_CTRL);
// 4. 启用中断
writel(0x1F, isp->base + ISP_INT_EN);
}
/**
* @brief 停止 ISP 视频流。
*
* @param isp 指向 rkisp_device 结构。
*/
static void rkisp_stop_streaming(struct rkisp_device *isp)
{
// 1. 禁用中断
writel(0x0, isp->base + ISP_INT_EN);
// 2. 禁用 ISP
writel(0x0, isp->base + ISP_CTRL);
// 3. 等待硬件停止
while (readl(isp->base + ISP_STATUS) & 0x1)
;
}
/**
* @brief ISP 中断处理函数。
*
* 处理帧完成、统计完成、错误等。
*
* @param irq 中断号。
* @param dev_id 指向 rkisp_device 结构。
*/
static irqreturn_t rkisp_irq_handler(int irq, void *dev_id)
{
struct rkisp_device *isp = dev_id;
u32 int_stat;
// 1. 读取中断状态
int_stat = readl(isp->base + ISP_INT_STAT);
// 2. 处理帧完成中断
if (int_stat & (1 << 0)) {
writel(1 << 0, isp->base + ISP_INT_STAT);
// 完成当前缓冲区
if (isp->curr_buf) {
isp->curr_buf->vbuf.sequence = isp->frame_count++;
vb2_buffer_done(&isp->curr_buf->vbuf.vb2_buf, VB2_BUF_STATE_DONE);
}
// 取出下一个缓冲区
if (!list_empty(&isp->buffers)) {
isp->curr_buf = list_first_entry(&isp->buffers,
struct rkisp_buffer, list);
list_del_init(&isp->curr_buf->list);
dma_addr_t dma_addr = vb2_dma_contig_plane_dma_addr(&isp->curr_buf->vbuf.vb2_buf, 0);
writel(dma_addr, isp->base + ISP_DMA_ADDR_Y);
} else {
isp->curr_buf = NULL;
}
}
// 3. 处理统计完成中断
if (int_stat & (1 << 1)) {
writel(1 << 1, isp->base + ISP_INT_STAT);
// 读取统计数据
}
// 4. 处理错误中断
if (int_stat & (1 << 2)) {
dev_err(isp->dev, "ISP error detected\n");
writel(1 << 2, isp->base + ISP_INT_STAT);
}
return IRQ_HANDLED;
}
/**
* @brief Platform 探测函数。
*
* 初始化 ISP 硬件,注册 V4L2 设备。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int rkisp_probe(struct platform_device *pdev)
{
struct rkisp_device *isp;
struct resource *res;
int ret, irq;
// 1. 分配私有数据结构
isp = devm_kzalloc(&pdev->dev, sizeof(*isp), GFP_KERNEL);
if (!isp)
return -ENOMEM;
platform_set_drvdata(pdev, isp);
isp->dev = &pdev->dev;
// 2. 获取 I/O 资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "No memory resource\n");
return -ENXIO;
}
isp->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(isp->base))
return PTR_ERR(isp->base);
// 3. 获取时钟
isp->aclk = devm_clk_get(&pdev->dev, "aclk");
if (IS_ERR(isp->aclk))
return PTR_ERR(isp->aclk);
isp->pclk = devm_clk_get(&pdev->dev, "pclk");
if (IS_ERR(isp->pclk))
return PTR_ERR(isp->pclk);
isp->cclk = devm_clk_get(&pdev->dev, "cclk");
if (IS_ERR(isp->cclk))
return PTR_ERR(isp->cclk);
clk_prepare_enable(isp->aclk);
clk_prepare_enable(isp->pclk);
clk_prepare_enable(isp->cclk);
// 4. 获取中断
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
ret = irq;
goto err_clk;
}
isp->irq = irq;
// 5. 初始化 V4L2
ret = v4l2_device_register(&pdev->dev, &isp->v4l2_dev);
if (ret) {
dev_err(&pdev->dev, "Failed to register V4L2 device\n");
goto err_clk;
}
spin_lock_init(&isp->lock);
INIT_LIST_HEAD(&isp->buffers);
// 6. 注册中断
ret = devm_request_irq(&pdev->dev, isp->irq, rkisp_irq_handler,
IRQF_SHARED, "rkisp", isp);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
goto err_v4l2;
}
// 7. 初始化 VB2 队列
isp->queue.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
isp->queue.io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
isp->queue.ops = &rkisp_qops;
isp->queue.mem_ops = &vb2_dma_contig_memops;
isp->queue.drv_priv = isp;
ret = vb2_queue_init(&isp->queue);
if (ret) {
dev_err(&pdev->dev, "Failed to init vb2 queue\n");
goto err_v4l2;
}
// 8. 注册视频设备
isp->vdev = video_device_alloc();
if (!isp->vdev) {
dev_err(&pdev->dev, "Failed to alloc video device\n");
ret = -ENOMEM;
goto err_v4l2;
}
isp->vdev->v4l2_dev = &isp->v4l2_dev;
isp->vdev->queue = &isp->queue;
isp->vdev->fops = &rkisp_fops;
isp->vdev->ioctl_ops = &rkisp_ioctl_ops;
ret = video_register_device(isp->vdev, VFL_TYPE_VIDEO, -1);
if (ret) {
dev_err(&pdev->dev, "Failed to register video device\n");
goto err_video;
}
dev_info(&pdev->dev, "Rockchip ISP registered at 0x%llx, IRQ %d\n",
(unsigned long long)res->start, isp->irq);
return 0;
err_video:
video_device_release(isp->vdev);
err_v4l2:
v4l2_device_unregister(&isp->v4l2_dev);
err_clk:
clk_disable_unprepare(isp->cclk);
clk_disable_unprepare(isp->pclk);
clk_disable_unprepare(isp->aclk);
return ret;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int rkisp_remove(struct platform_device *pdev)
{
struct rkisp_device *isp = platform_get_drvdata(pdev);
// 1. 停止 ISP
rkisp_stop_streaming(isp);
// 2. 注销视频设备
video_unregister_device(isp->vdev);
// 3. 注销 V4L2 设备
v4l2_device_unregister(&isp->v4l2_dev);
// 4. 禁用时钟
clk_disable_unprepare(isp->cclk);
clk_disable_unprepare(isp->pclk);
clk_disable_unprepare(isp->aclk);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id rkisp_of_match[] = {
{ .compatible = "rockchip,rk3399-isp" },
{ .compatible = "rockchip,rk3568-isp" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, rkisp_of_match);
/* Platform 驱动结构 */
static struct platform_driver rkisp_driver = {
.probe = rkisp_probe,
.remove = rkisp_remove,
.driver = {
.name = "rkisp",
.of_match_table = rkisp_of_match,
},
};
module_platform_driver(rkisp_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Rockchip ISP Controller Driver");
MODULE_LICENSE("GPL v2");
第三章 ISP 调试核心难点
3.1 图像颜色异常
现象:输出的图像整体偏色,或某些区域颜色错误。
原因:
-
白平衡参数错误。
-
颜色校正矩阵 (CCM) 配置错误。
-
黑电平校正值不正确。
调试方法:
-
检查黑电平:
devmem2 <base>+0x10 # ISP_RAW_CFG
-
检查 CCM 矩阵:
# 读取 CCM 寄存器 devmem2 <base>+0x1C devmem2 <base>+0x20
-
获取原始数据:
# 直接捕获 RAW 数据,用工具查看 v4l2-ctl --device /dev/video0 --stream-mmap --stream-to=/tmp/raw.raw
3.2 图像过曝或欠曝
现象:图像整体太亮或太暗,细节丢失。
原因:
-
自动曝光算法未正确收敛。
-
增益设置错误。
-
曝光时间设置错误。
调试方法:
-
检查增益:
devmem2 <base>+0x18 # ISP_GAIN_CFG
-
检查曝光时间:
# 从传感器驱动读取曝光时间 cat /sys/class/video4linux/video0/controls
-
手动设置曝光:
v4l2-ctl -d /dev/v4l-subdev0 -c exposure=1000
3.3 统计数据读取失败
现象:3A 算法无法获取统计数据,自动对焦/曝光不工作。
原因:
-
统计 DMA 地址未设置。
-
统计缓冲区未准备好。
-
统计中断未触发。
调试方法:
-
检查统计状态:
devmem2 <base>+0x30 # ISP_STATS_BASE
-
检查统计中断:
cat /proc/interrupts | grep isp
3.4 ISP 处理延时过高
现象:V4L2 缓冲区长时间未返回,perf 显示处理延迟。
原因:
-
缓冲区不足。
-
硬件处理流水线被阻塞。
-
DMA 传输效率低。
调试方法:
-
检查缓冲区队列:
# 查看 VB2 队列状态 cat /sys/kernel/debug/v4l2/dev0/queue
-
减少处理负载:
# 降低输出分辨率 v4l2-ctl -d /dev/video0 --set-fmt-video=width=1920,height=1080
第四章 结合性能调试场景示例
场景:ISP 处理 4K 视频时,帧率只能达到 15fps,预期 30fps。
分析流程:
-
宏观层面(CPU 层):
-
perf top显示rkisp_irq_handler占用 CPU 较低。 -
iostat显示内存带宽接近饱和。
-
-
ISP 层( Device Drivers -> ISP Controller 层):
devmem2 <base>+0x04 # ISP_STATUS
显示 ISP 流水线处于忙碌状态。
-
DMA 层(DMA 控制器层):
trace-cmd record -e dma:* -a -- timeout 5 trace-cmd report | grep isp
显示 DMA 传输延迟较高。
-
根本原因:
-
4K 分辨率需要大量内存带宽 (4096216016bit*30fps ≈ 4.2GB/s)。
-
嵌入式 DDR 带宽有限,与 GPU 和 CPU 竞争。
-
ISP DMA 请求被其他高优先级设备抢占。
-
-
解决方案:
-
降低帧率到 30fps 或降低输出分辨率。
-
使用 YUV422 格式代替 RAW 格式减少带宽。
-
调整 ISP 时钟频率,提高处理速度。
-
使用 DMA 缓冲池减少分配开销。
-
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| CSI | 接收传感器原始数据 | 分辨率匹配、格式转换 |
| DMA | 传输处理后的图像数据 | 带宽分配、地址对齐 |
| Memory Controller | 提供高带宽内存 | 频率设置、QoS 控制 |
| Video Encoder | 处理 ISP 输出数据 | 编码质量、帧率同步 |
第二部分 SDHCI 控制器 (Synopsys DWC MSHC)
第一章 SDHCI 控制器在 Platform Bus 中的位置
SDHCI (Secure Digital Host Controller Interface) 是 SD 卡和 eMMC 存储设备的标准化主机接口。在嵌入式 SoC 中,SDHCI 控制器通常作为 Platform 设备挂载在内部总线上,通过 SD 总线协议与存储设备通信。
在 Linux 5.10 中,SDHCI 子系统是 MMC 子系统下的一个标准框架,位于 drivers/mmc/host/,核心层次分为三层:
-
MMC 核心层 (
drivers/mmc/core/):管理 MMC/SD 设备状态、分区、块 I/O 请求。 -
SDHCI 控制器驱动(本篇文章重点):具体的 SoC SDHCI 主机驱动(如
sdhci-of-dwcmshc.c、sdhci-pltfm.c)。 -
SDHCI 标准层 (
drivers/mmc/host/sdhci.c):提供通用的 SDHCI 操作、中断处理、DMA 管理。
第二章 Linux 5.10 典型 SDHCI 控制器驱动 —— sdhci-of-dwcmshc.c
DesignWare DWC MSHC (Mobile Storage Host Controller) 是最广泛使用的 eMMC/SD 控制器 IP 核之一。以下代码基于 Linux 5.10 drivers/mmc/host/sdhci-of-dwcmshc.c,展示核心逻辑。
2.1 硬件关键概念
-
SDHCI 标准寄存器:控制寄存器、状态寄存器、命令寄存器、数据寄存器。
-
SDHCI 扩展寄存器:DWC 特有的寄存器,用于配置时钟、DMA、中断等。
-
ADMA2 引擎:支持高级 DMA 传输,使用描述符链。
-
时钟:需要
ahb_clk(总线时钟)和core_clk(SD 核心时钟)。 -
中断:处理命令完成、数据传输、错误等事件。
2.2 核心代码
// 基于 Linux 5.10 drivers/mmc/host/sdhci-of-dwcmshc.c (精简版)
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <linux/mmc/host.h>
#include <linux/dma-mapping.h>
#include "sdhci-pltfm.h"
/**
* @brief DesignWare MSHC SDHCI 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 SDHCI Controller 硬件实例。
*/
struct dwcmshc_priv {
void __iomem *base; /**< 映射后的寄存器基址 */
int irq; /**< 中断号 */
struct clk *ahb_clk; /**< AHB 总线时钟 */
struct clk *core_clk; /**< SD 核心时钟 */
struct clk *bus_clk; /**< 总线时钟 */
struct sdhci_host *host; /**< SDHCI 主机抽象 */
struct device *dev; /**< 设备指针 */
spinlock_t lock; /**< 硬件保护锁 */
u32 version; /**< 控制器版本 */
struct sdhci_pltfm_data *pdata; /**< 平台特定数据 */
};
/* 寄存器偏移量 (DWC MSHC 扩展) */
#define DWC_MSHC_CTRL 0x30
#define DWC_MSHC_CLK_SRC 0x34
#define DWC_MSHC_PWR_EN 0x38
#define DWC_MSHC_TMOUT 0x3C
#define DWC_MSHC_BLK_CNT 0x40
#define DWC_MSHC_AXI_CTRL 0x44
#define DWC_MSHC_CAP 0x48
#define DWC_MSHC_ADMA_CTRL 0x4C
#define DWC_MSHC_ADMA_LOW 0x50
#define DWC_MSHC_ADMA_HIGH 0x54
/**
* @brief 初始化 DWC MSHC 硬件。
*
* @param priv 指向 dwcmshc_priv 结构。
*/
static void dwcmshc_hw_init(struct dwcmshc_priv *priv)
{
u32 ctrl;
// 1. 启用 ADMA2 模式
ctrl = readl(priv->base + DWC_MSHC_ADMA_CTRL);
ctrl |= (1 << 0);
writel(ctrl, priv->base + DWC_MSHC_ADMA_CTRL);
// 2. 设置 ADMA 描述符表地址 (由 sdhci 核心分配)
dma_addr_t adma_addr = sdhci_get_adma_addr(priv->host);
writel(lower_32_bits(adma_addr), priv->base + DWC_MSHC_ADMA_LOW);
writel(upper_32_bits(adma_addr), priv->base + DWC_MSHC_ADMA_HIGH);
// 3. 配置 AXI 总线控制 (提升性能)
ctrl = readl(priv->base + DWC_MSHC_AXI_CTRL);
ctrl |= (0x03 << 16); // 增加 AXI 突发长度
writel(ctrl, priv->base + DWC_MSHC_AXI_CTRL);
}
/**
* @brief 设置 SD 时钟频率。
*
* @param host 指向 sdhci_host 结构。
* @param clock 目标时钟频率 (Hz)。
*/
static void dwcmshc_set_clock(struct sdhci_host *host, unsigned int clock)
{
struct dwcmshc_priv *priv = sdhci_priv(host);
unsigned long clk_rate;
u32 div, ctrl;
if (clock == 0) {
// 关闭时钟
writel(0, priv->base + DWC_MSHC_CLK_SRC);
return;
}
clk_rate = clk_get_rate(priv->core_clk);
div = clk_rate / clock;
if (div < 1) div = 1;
if (div > 0xFF) div = 0xFF;
// 设置分频器
ctrl = readl(priv->base + DWC_MSHC_CLK_SRC);
ctrl &= ~0xFF;
ctrl |= div;
writel(ctrl, priv->base + DWC_MSHC_CLK_SRC);
// 启用时钟
ctrl |= (1 << 8);
writel(ctrl, priv->base + DWC_MSHC_CLK_SRC);
}
/**
* @brief SDHCI 平台探测函数。
*
* 初始化 SDHCI 控制器硬件,注册到 MMC 核心。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int dwcmshc_probe(struct platform_device *pdev)
{
struct dwcmshc_priv *priv;
struct sdhci_host *host;
struct resource *res;
int ret, irq;
// 1. 分配私有数据结构
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
platform_set_drvdata(pdev, priv);
priv->dev = &pdev->dev;
// 2. 获取 I/O 资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "No memory resource\n");
return -ENXIO;
}
priv->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
// 3. 获取时钟
priv->ahb_clk = devm_clk_get(&pdev->dev, "ahb");
if (IS_ERR(priv->ahb_clk))
return PTR_ERR(priv->ahb_clk);
priv->core_clk = devm_clk_get(&pdev->dev, "core");
if (IS_ERR(priv->core_clk))
return PTR_ERR(priv->core_clk);
priv->bus_clk = devm_clk_get_optional(&pdev->dev, "bus");
if (IS_ERR(priv->bus_clk))
return PTR_ERR(priv->bus_clk);
clk_prepare_enable(priv->ahb_clk);
clk_prepare_enable(priv->core_clk);
if (priv->bus_clk)
clk_prepare_enable(priv->bus_clk);
// 4. 获取中断
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
ret = irq;
goto err_clk;
}
priv->irq = irq;
// 5. 初始化 SDHCI 主机
host = sdhci_pltfm_init(pdev, &priv->pdata, sizeof(*priv));
if (IS_ERR(host)) {
ret = PTR_ERR(host);
goto err_clk;
}
priv->host = host;
host->mmc->parent = &pdev->dev;
// 6. 设置 SDHCI 操作回调
host->ops = &dwcmshc_ops;
host->mmc->caps = MMC_CAP_4_BIT_DATA | MMC_CAP_8_BIT_DATA;
host->mmc->caps |= MMC_CAP_ERASE | MMC_CAP_CMD23;
host->mmc->max_seg_size = PAGE_SIZE * 64;
host->mmc->max_req_size = PAGE_SIZE * 64;
spin_lock_init(&priv->lock);
// 7. 硬件初始化
dwcmshc_hw_init(priv);
// 8. 注册中断
ret = devm_request_irq(&pdev->dev, priv->irq, sdhci_irq_handler,
IRQF_SHARED, "dwcmshc", host);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
goto err_sdhci;
}
// 9. 注册 MMC 主机
ret = sdhci_add_host(host);
if (ret) {
dev_err(&pdev->dev, "Failed to add MMC host\n");
goto err_sdhci;
}
dev_info(&pdev->dev, "DWC MSHC SDHCI controller registered at 0x%llx, IRQ %d\n",
(unsigned long long)res->start, priv->irq);
return 0;
err_sdhci:
sdhci_pltfm_free(pdev);
err_clk:
if (priv->bus_clk)
clk_disable_unprepare(priv->bus_clk);
clk_disable_unprepare(priv->core_clk);
clk_disable_unprepare(priv->ahb_clk);
return ret;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int dwcmshc_remove(struct platform_device *pdev)
{
struct dwcmshc_priv *priv = platform_get_drvdata(pdev);
// 1. 移除 SDHCI 主机
sdhci_remove_host(priv->host, 0);
// 2. 释放 SDHCI 平台资源
sdhci_pltfm_free(pdev);
// 3. 禁用时钟
if (priv->bus_clk)
clk_disable_unprepare(priv->bus_clk);
clk_disable_unprepare(priv->core_clk);
clk_disable_unprepare(priv->ahb_clk);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id dwcmshc_of_match[] = {
{ .compatible = "rockchip,rk3399-dwcmshc" },
{ .compatible = "snps,dwcmshc" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, dwcmshc_of_match);
/* Platform 驱动结构 */
static struct platform_driver dwcmshc_driver = {
.probe = dwcmshc_probe,
.remove = dwcmshc_remove,
.driver = {
.name = "dwcmshc",
.of_match_table = dwcmshc_of_match,
},
};
module_platform_driver(dwcmshc_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("DesignWare DWC MSHC SDHCI Controller Driver");
MODULE_LICENSE("GPL v2");
第三章 SDHCI 调试核心难点
3.1 卡检测失败
现象:插入 SD 卡后,系统无任何响应,lsblk 不显示设备。
原因:
-
卡检测 GPIO 未正确配置。
-
卡供电未使能。
-
卡检测中断未触发。
调试方法:
-
检查卡检测 GPIO:
cat /sys/kernel/debug/gpio | grep cd
-
强制检测卡:
echo 1 > /sys/devices/platform/*dwcmshc/mmc_host/mmc0/scan
-
检查卡供电:
cat /sys/kernel/debug/regulator/regulator_summary | grep mmc
3.2 数据传输 CRC 错误
现象:dmesg 频繁出现 "CRC error",读写速度慢。
原因:
-
SD 总线信号质量差。
-
时钟频率过高。
-
布线问题。
调试方法:
-
降低时钟频率:
# 在 DTS 中设置 max-frequency = <10000000>;
-
降低总线宽度:
echo 1 > /sys/devices/platform/*dwcmshc/mmc_host/mmc0/mmc0:0001/bus_width
-
检查信号完整性:用示波器查看 SD 时钟和数据线。
3.3 DMA 传输失败
现象:大量数据传输时出错,dmesg 显示 "DMA error"。
原因:
-
ADMA 描述符表对齐错误。
-
DMA 地址超过 32 位范围。
-
DMA 中断处理超时。
调试方法:
-
检查 ADMA 描述符:
devmem2 <base>+0x50 # ADMA_LOW
-
使用 PIO 模式测试:在驱动中强制禁用 DMA。
-
调试 DMA 中断:
trace-cmd record -e dma:* -a -- timeout 5 trace-cmd report | grep dwcmshc
第四章 结合性能调试场景示例
场景:使用 eMMC 读写文件时,速度只有 50MB/s,理论上应达 300MB/s。
分析流程:
-
宏观层面(图谱的 CPU 和 I/O 层):
-
perf top显示sdhci_irq_handler占用 CPU 约 30%。 -
iostat -x 1显示 mmcblk0 的r/s和w/s较高,但吞吐量低。
-
-
SDHCI 层(图谱的 Device Drivers -> SDHCI 层):
cat /sys/kernel/debug/mmc0/ios
发现当前频率为 50MHz,总线宽度为 4-bit,理论带宽约 200MB/s。
-
时钟配置(Clock 层):
cat /sys/kernel/debug/clk/clk_summary | grep core
发现
core_clk频率仅为 100MHz。 -
根本原因:
-
eMMC 芯片支持 HS200 模式(200MHz),但实际时钟被限制在 50MHz。
-
SDHCI 驱动中未正确启用 HS200 模式。
-
未执行调优 (Tuning) 流程。
-
-
解决方案:
-
在 DTS 中设置
cap-mmc-hw-reset和mmc-hs200-1_8v。 -
手动触发调优:
echo 1 > /sys/kernel/debug/mmc0/execute_tuning。 -
调整时钟频率到 200MHz。
-
优化后,读写速度提升到 280MB/s。
-
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| PINCTRL | 配置 SD 总线引脚功能 | 引脚复用、驱动强度 |
| Clock Controller | 提供 SD 核心时钟 | 频率匹配、电压调整 |
| DMA | 数据传输搬运 | 描述符链、中断聚合 |
| Regulator | 提供卡电源 (vmmc/vqmmc) | 电压稳定、上电时序 |
第三部分 GPU 控制器 (Mali)
第一章 GPU 在 Platform Bus 中的位置
GPU 是现代 SoC 中最重要的协处理器之一,负责图形渲染、计算加速和多媒体处理。在嵌入式设备中,Mali 系列 GPU 是最广泛使用的 IP 核之一,通常作为 Platform 设备挂载在内部总线上,通过专用 DMA 和中断机制与 CPU 协同工作。
在 Linux 5.10 中,Mali GPU 驱动位于 drivers/gpu/drm/panfrost/(Panfrost 是 Mali 的开源驱动),支持 Mali T860、T880、G52、G76 等型号。
1.1 硬件关键概念
-
GPU 核心寄存器:管理 GPU 电源、时钟、工作状态。
-
MMU (Memory Management Unit):GPU 专用的内存管理单元,支持虚拟地址。
-
Shader 引擎:执行图形和计算任务。
-
调度器:管理任务队列、优先级、上下文切换。
-
中断:处理作业完成、MMU 缺页、错误等事件。
-
DVFS (Dynamic Voltage and Frequency Scaling):动态电源管理。
1.2 核心代码
// 基于 Linux 5.10 drivers/gpu/drm/panfrost/panfrost_device.c (精简版)
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <linux/pm_runtime.h>
#include <drm/drm_device.h>
#include <drm/drm_file.h>
#include <drm/drm_ioctl.h>
#include <drm/drm_gem_shmem_helper.h>
/**
* @brief Mali GPU 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 GPU 硬件实例。
*/
struct panfrost_device {
void __iomem *base; /**< 映射后的寄存器基址 */
void __iomem *mmu_base; /**< MMU 寄存器基址 */
void __iomem *pm_base; /**< 电源管理寄存器基址 */
int irq; /**< GPU 中断号 */
int irq_mmu; /**< MMU 中断号 */
int irq_job; /**< 作业中断号 */
struct clk *clk; /**< 核心时钟 */
struct clk *bus_clk; /**< 总线时钟 */
struct device *dev; /**< 设备指针 */
struct drm_device *drm; /**< DRM 设备指针 */
struct panfrost_job *curr_job; /**< 当前运行的作业 */
spinlock_t lock; /**< 硬件保护锁 */
u32 gpu_version; /**< GPU 版本号 */
u32 num_cores; /**< Shader 核心数量 */
u32 num_tiler; /**< Tiler 单元数量 */
atomic_t job_count; /**< 作业计数 */
};
/* 寄存器偏移量 (Mali GPU) */
#define GPU_CTRL 0x00
#define GPU_STATUS 0x04
#define GPU_INT_EN 0x08
#define GPU_INT_STAT 0x0C
#define GPU_SHADER_CONFIG 0x10
#define GPU_TILER_CONFIG 0x14
#define GPU_MMU_CONFIG 0x18
#define GPU_PWR_CTRL 0x20
#define GPU_PWR_STATUS 0x24
#define GPU_PWR_CTRL2 0x28
#define GPU_MMU_PAGE_TABLE 0x30
#define GPU_MMU_STATUS 0x34
/**
* @brief 初始化 GPU 电源管理。
*
* @param pfdev 指向 panfrost_device 结构。
* @return 0 成功。
*/
static int panfrost_gpu_power_init(struct panfrost_device *pfdev)
{
u32 pwr_ctrl;
// 1. 读取电源状态
pwr_ctrl = readl(pfdev->pm_base + GPU_PWR_STATUS);
if (!(pwr_ctrl & 0x1)) {
dev_err(pfdev->dev, "GPU power domain not ready\n");
return -EIO;
}
// 2. 启用所有 Shader 核心
pwr_ctrl = readl(pfdev->pm_base + GPU_PWR_CTRL);
pwr_ctrl |= 0xFFFF;
writel(pwr_ctrl, pfdev->pm_base + GPU_PWR_CTRL);
// 3. 等待电源稳定
msleep(100);
return 0;
}
/**
* @brief 初始化 GPU MMU。
*
* @param pfdev 指向 panfrost_device 结构。
* @param pgd 页表基地址。
*/
static void panfrost_mmu_init(struct panfrost_device *pfdev, dma_addr_t pgd)
{
// 1. 设置页表地址
writel(lower_32_bits(pgd), pfdev->mmu_base + GPU_MMU_PAGE_TABLE);
writel(upper_32_bits(pgd), pfdev->mmu_base + GPU_MMU_PAGE_TABLE + 4);
// 2. 启用 MMU
u32 mmu_cfg = readl(pfdev->mmu_base + GPU_MMU_CONFIG);
mmu_cfg |= (1 << 0);
writel(mmu_cfg, pfdev->mmu_base + GPU_MMU_CONFIG);
// 3. 检查 MMU 状态
u32 status = readl(pfdev->mmu_base + GPU_MMU_STATUS);
if (!(status & (1 << 0))) {
dev_err(pfdev->dev, "MMU not ready\n");
}
}
/**
* @brief 提交 GPU 作业。
*
* @param pfdev 指向 panfrost_device 结构。
* @param job 指向 panfrost_job 结构。
* @return 0 成功。
*/
static int panfrost_gpu_submit_job(struct panfrost_device *pfdev,
struct panfrost_job *job)
{
// 1. 检查 GPU 是否空闲
if (atomic_read(&pfdev->job_count) > 0) {
// 需要排队
return -EBUSY;
}
// 2. 设置作业上下文
// 将作业参数写入 GPU 寄存器
writel(job->fragment_shader, pfdev->base + 0x100);
writel(job->vertex_shader, pfdev->base + 0x104);
writel(job->fragment_begin, pfdev->base + 0x108);
writel(job->vertex_begin, pfdev->base + 0x10C);
writel(job->frame_buffer, pfdev->base + 0x110);
writel(job->render_target, pfdev->base + 0x114);
// 3. 启用 GPU 中断
writel(0x1F, pfdev->base + GPU_INT_EN);
// 4. 启动 GPU 作业
u32 ctrl = readl(pfdev->base + GPU_CTRL);
ctrl |= (1 << 0); // 启用 GPU
ctrl |= (1 << 1); // 启用作业
writel(ctrl, pfdev->base + GPU_CTRL);
pfdev->curr_job = job;
atomic_inc(&pfdev->job_count);
return 0;
}
/**
* @brief GPU 中断处理函数。
*
* 处理作业完成、MMU 缺页、电源错误等事件。
*
* @param irq 中断号。
* @param dev_id 指向 panfrost_device 结构。
*/
static irqreturn_t panfrost_gpu_irq_handler(int irq, void *dev_id)
{
struct panfrost_device *pfdev = dev_id;
u32 int_stat;
// 1. 读取中断状态
int_stat = readl(pfdev->base + GPU_INT_STAT);
// 2. 处理作业完成中断
if (int_stat & (1 << 0)) {
writel(1 << 0, pfdev->base + GPU_INT_STAT);
// 通知上层作业完成
if (pfdev->curr_job) {
pfdev->curr_job->cb(pfdev->curr_job);
pfdev->curr_job = NULL;
atomic_dec(&pfdev->job_count);
}
// 检查队列中是否有待处理作业
}
// 3. 处理 MMU 缺页中断
if (int_stat & (1 << 1)) {
dev_err(pfdev->dev, "GPU MMU fault detected\n");
writel(1 << 1, pfdev->base + GPU_INT_STAT);
// 读取 MMU 状态寄存器定位错误地址
u32 fault_addr = readl(pfdev->mmu_base + 0x38);
dev_err(pfdev->dev, "Fault address: 0x%08x\n", fault_addr);
}
// 4. 处理电源错误
if (int_stat & (1 << 2)) {
dev_err(pfdev->dev, "GPU power error\n");
writel(1 << 2, pfdev->base + GPU_INT_STAT);
}
return IRQ_HANDLED;
}
/**
* @brief Platform 探测函数。
*
* 初始化 GPU 控制器硬件,注册到 DRM。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int panfrost_probe(struct platform_device *pdev)
{
struct panfrost_device *pfdev;
struct resource *res;
struct drm_device *drm;
int ret, irq, irq_mmu, irq_job;
// 1. 分配私有数据结构
pfdev = devm_kzalloc(&pdev->dev, sizeof(*pfdev), GFP_KERNEL);
if (!pfdev)
return -ENOMEM;
platform_set_drvdata(pdev, pfdev);
pfdev->dev = &pdev->dev;
// 2. 获取 I/O 资源 (核心寄存器)
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "No memory resource for core\n");
return -ENXIO;
}
pfdev->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(pfdev->base))
return PTR_ERR(pfdev->base);
// 3. 获取 MMU 资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
if (res) {
pfdev->mmu_base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(pfdev->mmu_base))
return PTR_ERR(pfdev->mmu_base);
} else {
pfdev->mmu_base = pfdev->base + 0x2000;
}
// 4. 获取电源管理资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 2);
if (res) {
pfdev->pm_base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(pfdev->pm_base))
return PTR_ERR(pfdev->pm_base);
} else {
pfdev->pm_base = pfdev->base + 0x1000;
}
// 5. 获取时钟
pfdev->clk = devm_clk_get(&pdev->dev, "core");
if (IS_ERR(pfdev->clk))
return PTR_ERR(pfdev->clk);
pfdev->bus_clk = devm_clk_get(&pdev->dev, "bus");
if (IS_ERR(pfdev->bus_clk))
return PTR_ERR(pfdev->bus_clk);
clk_prepare_enable(pfdev->clk);
clk_prepare_enable(pfdev->bus_clk);
// 6. 获取中断
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
ret = irq;
goto err_clk;
}
pfdev->irq = irq;
irq_mmu = platform_get_irq(pdev, 1);
if (irq_mmu < 0) {
ret = irq_mmu;
goto err_clk;
}
pfdev->irq_mmu = irq_mmu;
irq_job = platform_get_irq(pdev, 2);
if (irq_job < 0) {
ret = irq_job;
goto err_clk;
}
pfdev->irq_job = irq_job;
// 7. 初始化硬件
ret = panfrost_gpu_power_init(pfdev);
if (ret) {
dev_err(&pdev->dev, "Failed to initialize GPU power\n");
goto err_clk;
}
// 8. 读取 GPU 信息
pfdev->gpu_version = readl(pfdev->base + 0x00);
pfdev->num_cores = readl(pfdev->base + GPU_SHADER_CONFIG);
pfdev->num_tiler = readl(pfdev->base + GPU_TILER_CONFIG);
spin_lock_init(&pfdev->lock);
atomic_set(&pfdev->job_count, 0);
// 9. 注册中断
ret = devm_request_irq(&pdev->dev, pfdev->irq, panfrost_gpu_irq_handler,
IRQF_SHARED, "panfrost-gpu", pfdev);
if (ret) {
dev_err(&pdev->dev, "Failed to request GPU IRQ\n");
goto err_clk;
}
ret = devm_request_irq(&pdev->dev, pfdev->irq_mmu, panfrost_mmu_irq_handler,
IRQF_SHARED, "panfrost-mmu", pfdev);
if (ret) {
dev_err(&pdev->dev, "Failed to request MMU IRQ\n");
goto err_clk;
}
ret = devm_request_irq(&pdev->dev, pfdev->irq_job, panfrost_job_irq_handler,
IRQF_SHARED, "panfrost-job", pfdev);
if (ret) {
dev_err(&pdev->dev, "Failed to request job IRQ\n");
goto err_clk;
}
// 10. 注册 DRM 设备
drm = drm_device_alloc(&panfrost_driver, &pdev->dev);
if (IS_ERR(drm)) {
ret = PTR_ERR(drm);
goto err_clk;
}
pfdev->drm = drm;
drm->dev_private = pfdev;
ret = drm_dev_register(drm, 0);
if (ret) {
dev_err(&pdev->dev, "Failed to register DRM device\n");
goto err_drm;
}
dev_info(&pdev->dev, "Mali GPU registered (version %d, cores %d, tiler %d)\n",
pfdev->gpu_version, pfdev->num_cores, pfdev->num_tiler);
return 0;
err_drm:
drm_dev_put(drm);
err_clk:
clk_disable_unprepare(pfdev->bus_clk);
clk_disable_unprepare(pfdev->clk);
return ret;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int panfrost_remove(struct platform_device *pdev)
{
struct panfrost_device *pfdev = platform_get_drvdata(pdev);
// 1. 注销 DRM 设备
drm_dev_unregister(pfdev->drm);
// 2. 禁用 GPU 硬件
writel(0, pfdev->base + GPU_CTRL);
// 3. 禁用时钟
clk_disable_unprepare(pfdev->bus_clk);
clk_disable_unprepare(pfdev->clk);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id panfrost_of_match[] = {
{ .compatible = "arm,mali-t860" },
{ .compatible = "arm,mali-t880" },
{ .compatible = "arm,mali-g52" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, panfrost_of_match);
/* Platform 驱动结构 */
static struct platform_driver panfrost_driver = {
.probe = panfrost_probe,
.remove = panfrost_remove,
.driver = {
.name = "panfrost",
.of_match_table = panfrost_of_match,
},
};
module_platform_driver(panfrost_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Mali GPU Controller Driver");
MODULE_LICENSE("GPL v2");
第三章 GPU 调试核心难点
3.1 GPU 挂死 (GPU Hang)
现象:图形界面响应缓慢或完全无响应,dmesg 显示 "GPU hang detected"。
原因:
-
Shader 作业超时。
-
内存访问越界。
-
电源管理问题。
调试方法:
-
检查 GPU 状态:
devmem2 <base>+0x04 # GPU_STATUS
-
强制复位 GPU:
# 通过 debugfs 复位 echo 1 > /sys/kernel/debug/dri/0/panfrost/gpu_reset
-
使用
perf监控作业:perf record -e panfrost:panfrost_job_submit -a -- sleep 10 perf script | grep panfrost
3.2 MMU 缺页错误
现象:dmesg 出现 "GPU MMU fault detected",图形程序崩溃。
原因:
-
分配的内存被释放但 GPU 仍在访问。
-
页表映射不正确。
-
内存分配对齐问题。
调试方法:
-
读取 MMU 状态:
devmem2 <mmu_base>+0x04 # MMU_STATUS
-
定位错误地址:
devmem2 <mmu_base>+0x38 # 错误地址寄存器
-
分析 GPU 作业:
# 跟踪最近提交的作业 trace-cmd record -e panfrost:* -a -- timeout 5 trace-cmd report | grep fault
3.3 GPU 性能异常 (Low FPS)
现象:OpenGL 游戏帧率远低于预期,GPU 利用率高但性能差。
原因:
-
GPU 时钟频率过低。
-
内存带宽不足。
-
作业调度配置错误。
调试方法:
-
检查 GPU 频率:
cat /sys/kernel/debug/clk/clk_summary | grep gpu
-
查看 GPU 频率上限:
# 检查频率上限 cat /sys/devices/platform/*gpu*/devfreq/devfreq0/cur_freq
-
增加作业并发:
# 增加调度队列深度 echo 8 > /sys/kernel/debug/dri/0/panfrost/job_queue_depth
3.4 电源管理异常 (GPU 不掉电)
现象:系统空闲时,GPU 仍然处于工作状态,功耗较高。
原因:
-
电源管理驱动未启用。
-
有持续运行的作业。
-
GPU 无法进入休眠状态。
调试方法:
-
检查 GPU 电源状态:
devmem2 <pm_base>+0x24 # PWR_STATUS
-
强制 GPU 进入空闲:
# 终止所有作业 echo 1 > /sys/kernel/debug/dri/0/panfrost/gpu_force_idle
-
检查 PM 脚本:
cat /sys/power/pm_debug
第四部分:结合性能调试场景示例
场景:3D 游戏帧率在 20-25fps 之间波动,而 GPU 峰值性能应达到 60fps。
分析流程:
-
宏观层面( CPU 和 Memory 层):
-
perf top显示panfrost_job_irq_handler占用 CPU 约 20%。 -
iostat显示内存带宽接近饱和。
-
-
GPU 层(Device Drivers -> GPU 层):
cat /sys/devices/platform/*gpu*/devfreq/devfreq0/cur_freq
发现 GPU 频率稳定在 650MHz,而理论最高为 800MHz。
-
硬件监控( Thermal 层):
cat /sys/class/thermal/thermal_zone*/temp
发现 GPU 温度达到 85°C。
-
根本原因:
-
GPU 温度达到温度阈值,导致热降频(Thermal Throttling)。
-
散热设计不足,无法长期维持高性能。
-
热管理策略过于保守。
-
-
解决方案:
-
改善散热(增加散热片或风扇)。
-
在 DTS 中提高温度阈值
temperature-high = <90000>;。 -
降低渲染分辨率或画质。
-
增加 GPU 主动冷却周期。
-
优化后,帧率稳定在 50fps 以上。
-
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| MMU | 为 GPU 提供虚拟地址映射 | 页表错误、内存访问权限 |
| DMA | 通过 DMA 传输纹理数据 | 带宽分配、描述符链 |
| Thermal | 监测 GPU 温度并进行降频 | 温度阈值、降频策略 |
| Power | 提供电源动态管理 | 空闲降频、休眠唤醒 |
| Interrupt Controller | 管理 GPU 中断 | 中断优先级、延迟 |
第四部分 PMIC 控制器 (RK808)
第一章 PMIC 控制器在 Platform Bus 中的位置
PMIC (Power Management Integrated Circuit) 是现代嵌入式系统中最重要的组件之一,负责为 SoC 和所有外设提供稳定的电压和电流,并管理电池充电、热保护、电源按键等功能。PMIC 通常通过 I2C 或 SPI 总线与主 SoC 通信,在 Linux 中作为 Platform 设备注册。
在 Linux 5.10 中,PMIC 驱动位于 drivers/mfd/(多功能设备驱动),核心层次分为三层:
-
MFD 核心层:将 PMIC 注册为 MFD 设备,暴露子设备(Regulator、RTC、PWM、ADC 等)。
-
PMIC 主驱动(本篇文章重点):负责 I2C 通信、中断处理、电源管理。
-
子设备驱动:如
rk808-regulator.c(电压调节器)、rk808-rtc.c(RTC)、rk808-clk.c(时钟输出)。
第二章 Linux 5.10 典型 PMIC 驱动 —— rk808.c
RK808 是 Rockchip 系列 SoC 中最常用的 PMIC 之一,支持 4 路 DCDC 转换器、8 路 LDO、RTC、电源按键、电池充电等功能。以下代码基于 Linux 5.10 drivers/mfd/rk808.c,展示核心逻辑。
2.1 硬件关键概念
-
I2C 寄存器:通过 I2C 访问内部寄存器,配置电压、使能、中断。
-
DCDC 转换器:提供大电流、高效率的电压输出(用于 CPU、GPU、DDR)。
-
LDO:提供低噪声、低电流的电压输出(用于模拟电路、音频)。
-
RTC:实时时钟,保持系统时间。
-
中断控制器:处理电源按键、过压、过温、充电状态等事件。
-
充电器:支持锂电池充电管理。
2.2 核心代码
// 基于 Linux 5.10 drivers/mfd/rk808.c (精简版)
#include <linux/io.h>
#include <linux/i2c.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <linux/regmap.h>
#include <linux/mfd/core.h>
#include <linux/mfd/rk808.h>
/**
* @brief RK808 PMIC 的私有数据结构。
*
* 对应于图中 Platform Bus 上的 PMIC 硬件实例。
*/
struct rk808 {
struct i2c_client *i2c; /**< I2C 客户端指针 */
struct regmap *regmap; /**< 寄存器映射 */
int irq; /**< 中断号 */
struct device *dev; /**< 设备指针 */
struct regmap_irq_chip_data *irq_data; /**< 中断控制器数据 */
struct i2c_client *i2c_rtc; /**< RTC I2C 客户端 */
struct i2c_client *i2c_gpio; /**< GPIO I2C 客户端 */
bool battery_present; /**< 电池是否在位 */
};
/* 寄存器偏移量 (RK808) */
#define RK808_REG_PWRON 0x00
#define RK808_REG_PWRON_EN 0x01
#define RK808_REG_INT_STS 0x02
#define RK808_REG_INT_MASK 0x03
#define RK808_REG_DCDC1_CTRL 0x10
#define RK808_REG_DCDC2_CTRL 0x11
#define RK808_REG_DCDC3_CTRL 0x12
#define RK808_REG_LDO1_CTRL 0x20
#define RK808_REG_LDO2_CTRL 0x21
#define RK808_REG_LDO3_CTRL 0x22
#define RK808_REG_BATTERY_CTRL 0x30
#define RK808_REG_CHARGE_CTRL 0x31
#define RK808_REG_RTC_CTRL 0x40
#define RK808_REG_RTC_SEC 0x41
/* 中断位定义 */
#define RK808_IRQ_PWRON (1 << 0) /**< 电源按键中断 */
#define RK808_IRQ_OVP (1 << 1) /**< 过压中断 */
#define RK808_IRQ_OTP (1 << 2) /**< 过温中断 */
#define RK808_IRQ_BAT_INSERT (1 << 3) /**< 电池插入中断 */
#define RK808_IRQ_BAT_REMOVE (1 << 4) /**< 电池移除中断 */
#define RK808_IRQ_BAT_OK (1 << 5) /**< 电池状态良好中断 */
#define RK808_IRQ_CHARGE (1 << 6) /**< 充电状态中断 */
/**
* @brief 读取 PMIC 寄存器。
*
* @param rk808 指向 rk808 结构。
* @param reg 寄存器地址。
* @param val 输出值。
* @return 0 成功。
*/
static int rk808_read_reg(struct rk808 *rk808, u8 reg, u8 *val)
{
return regmap_read(rk808->regmap, reg, (unsigned int *)val);
}
/**
* @brief 写入 PMIC 寄存器。
*
* @param rk808 指向 rk808 结构。
* @param reg 寄存器地址。
* @param val 要写入的值。
* @return 0 成功。
*/
static int rk808_write_reg(struct rk808 *rk808, u8 reg, u8 val)
{
return regmap_write(rk808->regmap, reg, val);
}
/**
* @brief 初始化 PMIC 硬件配置。
*
* @param rk808 指向 rk808 结构。
* @return 0 成功。
*/
static int rk808_hw_init(struct rk808 *rk808)
{
u8 val;
int ret;
// 1. 检查 PMIC 是否正常工作 (读取设备 ID)
ret = rk808_read_reg(rk808, RK808_REG_PWRON, &val);
if (ret) {
dev_err(rk808->dev, "Failed to read PMIC ID\n");
return ret;
}
dev_info(rk808->dev, "RK808 PMIC ID: 0x%02x\n", val);
// 2. 使能电源按键中断
ret = rk808_write_reg(rk808, RK808_REG_INT_MASK, ~RK808_IRQ_PWRON);
if (ret) {
dev_err(rk808->dev, "Failed to enable power key interrupt\n");
return ret;
}
// 3. 设置 DCDC1 输出电压 (CPU 核心电压)
// RK808 的 DCDC1 默认输出 1.0V,通过寄存器设置
// 此处简化为配置使能
ret = rk808_write_reg(rk808, RK808_REG_DCDC1_CTRL, 0x1F);
if (ret) {
dev_err(rk808->dev, "Failed to configure DCDC1\n");
return ret;
}
// 4. 设置 LDO1 输出电压 (用于某些外设)
ret = rk808_write_reg(rk808, RK808_REG_LDO1_CTRL, 0x0F);
if (ret) {
dev_err(rk808->dev, "Failed to configure LDO1\n");
return ret;
}
// 5. 检查电池状态
ret = rk808_read_reg(rk808, RK808_REG_BATTERY_CTRL, &val);
if (ret == 0) {
rk808->battery_present = !!(val & (1 << 0));
}
return 0;
}
/**
* @brief 处理电源按键中断。
*
* @param rk808 指向 rk808 结构。
*/
static void rk808_handle_power_key(struct rk808 *rk808)
{
u8 val;
// 1. 读取电源按键状态
rk808_read_reg(rk808, RK808_REG_PWRON, &val);
// 2. 判断按键按下还是释放
if (val & (1 << 0)) {
dev_info(rk808->dev, "Power key pressed\n");
// 触发系统关机或休眠
// 需要根据按键时长决定 (例如长按关机)
// 此处简化为直接关机
pm_power_off();
} else {
dev_info(rk808->dev, "Power key released\n");
}
}
/**
* @brief 处理电池插入/移除中断。
*
* @param rk808 指向 rk808 结构。
*/
static void rk808_handle_battery(struct rk808 *rk808)
{
u8 val;
// 1. 读取电池状态
rk808_read_reg(rk808, RK808_REG_BATTERY_CTRL, &val);
// 2. 更新电池状态
rk808->battery_present = !!(val & (1 << 0));
if (rk808->battery_present) {
dev_info(rk808->dev, "Battery inserted\n");
// 通知电池子系统
power_supply_changed(NULL);
} else {
dev_info(rk808->dev, "Battery removed\n");
}
}
/**
* @brief PMIC 中断处理函数。
*
* 处理电源按键、电池插入/移除、过压/过温等事件。
*
* @param irq 中断号。
* @param dev_id 指向 rk808 结构。
*/
static irqreturn_t rk808_irq_handler(int irq, void *dev_id)
{
struct rk808 *rk808 = dev_id;
u8 int_stat;
// 1. 读取中断状态
rk808_read_reg(rk808, RK808_REG_INT_STS, &int_stat);
// 2. 处理电源按键中断
if (int_stat & RK808_IRQ_PWRON) {
rk808_handle_power_key(rk808);
// 清除中断
rk808_write_reg(rk808, RK808_REG_INT_STS, RK808_IRQ_PWRON);
}
// 3. 处理电池插入/移除中断
if (int_stat & RK808_IRQ_BAT_INSERT) {
rk808_handle_battery(rk808);
rk808_write_reg(rk808, RK808_REG_INT_STS, RK808_IRQ_BAT_INSERT);
}
if (int_stat & RK808_IRQ_BAT_REMOVE) {
rk808_handle_battery(rk808);
rk808_write_reg(rk808, RK808_REG_INT_STS, RK808_IRQ_BAT_REMOVE);
}
// 4. 处理过压/过温中断
if (int_stat & RK808_IRQ_OVP) {
dev_err(rk808->dev, "Over-voltage detected!\n");
rk808_write_reg(rk808, RK808_REG_INT_STS, RK808_IRQ_OVP);
// 可能需要紧急关机
}
if (int_stat & RK808_IRQ_OTP) {
dev_err(rk808->dev, "Over-temperature detected!\n");
rk808_write_reg(rk808, RK808_REG_INT_STS, RK808_IRQ_OTP);
// 可能需要启用热保护
}
// 5. 处理充电状态变化
if (int_stat & RK808_IRQ_CHARGE) {
dev_info(rk808->dev, "Charge status changed\n");
rk808_write_reg(rk808, RK808_REG_INT_STS, RK808_IRQ_CHARGE);
power_supply_changed(NULL);
}
return IRQ_HANDLED;
}
/**
* @brief Platform 探测函数。
*
* 初始化 PMIC 硬件,注册子设备。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int rk808_probe(struct platform_device *pdev)
{
struct rk808 *rk808;
struct i2c_client *i2c;
struct regmap *regmap;
int ret, irq;
// 1. 分配私有数据结构
rk808 = devm_kzalloc(&pdev->dev, sizeof(*rk808), GFP_KERNEL);
if (!rk808)
return -ENOMEM;
platform_set_drvdata(pdev, rk808);
rk808->dev = &pdev->dev;
// 2. 从平台数据中获取 I2C 客户端
i2c = dev_get_drvdata(pdev->dev.parent);
if (!i2c) {
dev_err(&pdev->dev, "No I2C client provided\n");
return -EINVAL;
}
rk808->i2c = i2c;
// 3. 初始化 regmap
regmap = devm_regmap_init_i2c(i2c, &rk808_regmap_config);
if (IS_ERR(regmap)) {
dev_err(&pdev->dev, "Failed to init regmap\n");
return PTR_ERR(regmap);
}
rk808->regmap = regmap;
// 4. 获取中断
irq = i2c->irq;
if (irq < 0) {
dev_err(&pdev->dev, "No IRQ provided\n");
return irq;
}
rk808->irq = irq;
// 5. 硬件初始化
ret = rk808_hw_init(rk808);
if (ret) {
dev_err(&pdev->dev, "Failed to initialize hardware\n");
return ret;
}
// 6. 注册中断
ret = devm_request_irq(&pdev->dev, rk808->irq, rk808_irq_handler,
IRQF_SHARED, "rk808", rk808);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
return ret;
}
// 7. 注册 MFD 子设备 (Regulator, RTC, GPIO, ADC 等)
ret = devm_mfd_add_devices(&pdev->dev, PLATFORM_DEVID_NONE,
rk808_devs, ARRAY_SIZE(rk808_devs),
NULL, 0, NULL);
if (ret) {
dev_err(&pdev->dev, "Failed to add MFD devices\n");
return ret;
}
dev_info(&pdev->dev, "RK808 PMIC registered\n");
return 0;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int rk808_remove(struct platform_device *pdev)
{
// 1. 移除 MFD 子设备
// MFD 核心会自动清理
// 2. 禁用中断
// devm 会自动释放
return 0;
}
/* MFD 子设备列表 */
static const struct mfd_cell rk808_devs[] = {
{
.name = "rk808-regulator",
.of_compatible = "rockchip,rk808-regulator",
},
{
.name = "rk808-rtc",
.of_compatible = "rockchip,rk808-rtc",
},
{
.name = "rk808-gpio",
.of_compatible = "rockchip,rk808-gpio",
},
{
.name = "rk808-adc",
.of_compatible = "rockchip,rk808-adc",
},
};
/* 设备树匹配表 */
static const struct of_device_id rk808_of_match[] = {
{ .compatible = "rockchip,rk808" },
{ .compatible = "rockchip,rk809" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, rk808_of_match);
/* Platform 驱动结构 */
static struct platform_driver rk808_driver = {
.probe = rk808_probe,
.remove = rk808_remove,
.driver = {
.name = "rk808",
.of_match_table = rk808_of_match,
},
};
module_platform_driver(rk808_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Rockchip RK808 PMIC Driver");
MODULE_LICENSE("GPL v2");
第三章 PMIC 调试核心难点
3.1 PMIC 无法与 SoC 通信
现象:dmesg 显示 "Failed to read PMIC ID",系统启动后 PMIC 未初始化。
原因:
-
I2C 总线通信失败。
-
PMIC 地址错误。
-
PMIC 未正确上电。
调试方法:
-
检查 I2C 总线:
i2cdetect -y <bus>
-
手动读取 PMIC 寄存器:
i2ctransfer -y <bus> w1@<addr> 0x00 r1
-
检查电源输入:用万用表测量 PMIC 的输入电压。
3.2 电源按键无反应
现象:按下板上的电源按键,系统没有任何响应。
原因:
-
电源按键中断未使能。
-
电源按键 GPIO 配置错误。
-
PMIC 中断处理失败。
调试方法:
-
查看中断统计:
cat /proc/interrupts | grep rk808
-
测试中断触发:
# 手动触发中断 devmem2 <base>+0x02 0x01 # 写入中断状态寄存器
-
检查电源按键 GPIO:
cat /sys/kernel/debug/gpio | grep power
3.3 电压调节器无法控制
现象:cat /sys/class/regulator/regulator0/status 显示 "disabled",无法启用。
原因:
-
电压调节器驱动未加载。
-
电压上限/下限配置错误。
-
硬件保护触发。
调试方法:
-
查看调节器状态:
cat /sys/class/regulator/regulator*/status
-
尝试手动启用:
echo "enabled" > /sys/class/regulator/regulator0/enable
-
检查硬件保护:
devmem2 <base>+0x02 # 读取中断状态
3.4 电池充电不工作
现象:插入充电器后,系统不显示充电状态,电池电量不增加。
原因:
-
充电控制寄存器未配置。
-
电池检测失效。
-
充电器插入检测失败。
调试方法:
-
检查充电状态:
cat /sys/class/power_supply/charger/status
-
读取充电寄存器:
devmem2 <base>+0x31 # CHARGE_CTRL
-
手动启动充电:
echo "1" > /sys/class/power_supply/charger/charge_control
第四章 结合性能调试场景示例
场景:系统在插入充电器时,PMIC 频繁触发中断,CPU 占用率升高,导致系统响应缓慢。
分析流程:
-
宏观层面(图谱的 CPU 层):
-
perf top显示rk808_irq_handler占用 CPU 约 30%。 -
cat /proc/interrupts | grep rk808显示中断触发频率极高(每秒 50 次)。
-
-
PMIC 层(图谱的 Device Drivers -> PMIC 层):
devmem2 <base>+0x02 # INT_STS
发现
RK808_IRQ_CHARGE持续置位。 -
充电状态(电池充电层):
cat /sys/class/power_supply/charger/status
显示充电状态在 "charging" 和 "charged" 之间快速切换。
-
根本原因:
-
电池接近满电,充电芯片在 "恒压充电" 阶段。
-
充电电流临界触发中断阈值。
-
PMIC 在充电完成和充电进行之间频繁切换。
-
-
解决方案:
-
增加充电中断的迟滞 (hysteresis) 值。
-
在驱动中添加 "充电完成" 状态锁定机制。
-
调整充电终止电流阈值。
-
优化后,中断频率降低至正常水平。
-
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| I2C | PMIC 通过 I2C 与 SoC 通信 | I2C 频率、地址冲突 |
| Regulator | 管理电压输出 | 电压稳定、上电时序 |
| RTC | 维护系统时间 | 电池备份、时间同步 |
| Power Supply | 管理电池充电 | 充电状态、电量计算 |
| Thermal | 监测 PMIC 温度 | 过热保护、降频策略 |
第五部分 SARADC 控制器
第一章 SARADC 控制器在 Platform Bus 中的位置
SARADC (逐次逼近模数转换器) 是 SoC 中用于将模拟信号转换为数字信号的硬件模块。它广泛应用于嵌入式系统,用于读取触摸屏坐标、电池电压、传感器数据、环境温度等。SARADC 控制器通常作为 Platform 设备挂载在 SoC 内部总线上。
在 Linux 5.10 中,SARADC 驱动通常位于 drivers/iio/adc/,通过 IIO (Industrial I/O) 子系统暴露控制接口。典型驱动包括 rockchip-saradc.c、sun4i-gpadc.c 等。
1.1 硬件关键概念
-
ADC 核心寄存器:控制寄存器、数据寄存器、状态寄存器、中断使能。
-
时钟:需要
clk(ADC 核心时钟) 和pclk(总线时钟)。 -
中断:转换完成中断,用于通知 CPU 读取结果。
-
采样率:可由软件配置(如 1kHz ~ 1MHz)。
-
通道:支持多路模拟输入(通常 4~8 个通道)。
-
参考电压:通常为 SoC 的 VCC 或内部 1.8V 基准。
1.2 核心代码
// 基于 Linux 5.10 drivers/iio/adc/rockchip-saradc.c (精简版)
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>
#include <linux/delay.h>
/**
* @brief Rockchip SARADC 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 SARADC 硬件实例。
*/
struct rockchip_saradc {
void __iomem *base; /**< 映射后的寄存器基址 */
int irq; /**< 中断号 */
struct clk *clk; /**< ADC 核心时钟 */
struct clk *pclk; /**< 总线时钟 */
struct device *dev; /**< 设备指针 */
struct iio_dev *indio_dev; /**< IIO 设备抽象 */
spinlock_t lock; /**< 硬件保护锁 */
u32 version; /**< 控制器版本 */
u32 sample_rate; /**< 采样率 (Hz) */
u32 channel_count; /**< 通道数量 */
struct completion completion; /**< 转换完成同步 */
u32 last_result; /**< 上一次转换结果 */
};
/* 寄存器偏移量 (Rockchip SARADC) */
#define SARADC_CTRL 0x00
#define SARADC_DATA 0x04
#define SARADC_STATUS 0x08
#define SARADC_INT_EN 0x0C
#define SARADC_INT_STAT 0x10
#define SARADC_CLK_DIV 0x14
#define SARADC_PWR_CTRL 0x18
#define SARADC_CHANNEL_SEL 0x1C
/* 控制寄存器位定义 */
#define SARADC_CTRL_ENABLE (1 << 0) /**< ADC 使能 */
#define SARADC_CTRL_START (1 << 1) /**< 启动转换 */
#define SARADC_CTRL_MODE_CONTINUOUS (0 << 2) /**< 连续模式 */
#define SARADC_CTRL_MODE_SINGLE (1 << 2) /**< 单次模式 */
/**
* @brief 初始化 SARADC 硬件。
*
* @param saradc 指向 rockchip_saradc 结构。
* @return 0 成功。
*/
static int rockchip_saradc_hw_init(struct rockchip_saradc *saradc)
{
u32 ctrl;
// 1. 启用电源
writel(0x1, saradc->base + SARADC_PWR_CTRL);
msleep(1);
// 2. 配置时钟分频 (根据采样率计算)
u32 clk_rate = clk_get_rate(saradc->clk);
u32 div = clk_rate / saradc->sample_rate;
if (div < 2) div = 2;
if (div > 0xFFFF) div = 0xFFFF;
writel(div, saradc->base + SARADC_CLK_DIV);
// 3. 配置为单次转换模式
ctrl = SARADC_CTRL_ENABLE | SARADC_CTRL_MODE_SINGLE;
writel(ctrl, saradc->base + SARADC_CTRL);
// 4. 启用中断
writel(0x1, saradc->base + SARADC_INT_EN);
return 0;
}
/**
* @brief 启动 SARADC 单次转换。
*
* @param saradc 指向 rockchip_saradc 结构。
* @param channel 通道号 (0 ~ channel_count-1)。
* @return 0 成功。
*/
static int rockchip_saradc_start_single(struct rockchip_saradc *saradc,
int channel)
{
u32 ctrl;
unsigned long flags;
if (channel >= saradc->channel_count) {
dev_err(saradc->dev, "Invalid channel %d\n", channel);
return -EINVAL;
}
spin_lock_irqsave(&saradc->lock, flags);
// 1. 选择通道
writel(channel, saradc->base + SARADC_CHANNEL_SEL);
// 2. 启动转换
ctrl = readl(saradc->base + SARADC_CTRL);
ctrl |= SARADC_CTRL_START;
writel(ctrl, saradc->base + SARADC_CTRL);
spin_unlock_irqrestore(&saradc->lock, flags);
// 3. 等待转换完成
if (!wait_for_completion_timeout(&saradc->completion, 100 * HZ)) {
dev_err(saradc->dev, "ADC conversion timeout\n");
return -ETIMEDOUT;
}
return 0;
}
/**
* @brief 读取 SARADC 转换结果。
*
* @param saradc 指向 rockchip_saradc 结构。
* @return 转换结果 (0 ~ 1023 或 0 ~ 4095 取决于分辨率)。
*/
static u32 rockchip_saradc_read_result(struct rockchip_saradc *saradc)
{
return readl(saradc->base + SARADC_DATA) & 0xFFF;
}
/**
* @brief IIO 读取原始值回调。
*
* @param indio_dev 指向 iio_dev。
* @param chan 指向 iio_chan_spec。
* @param val 输出原始值。
* @return 0 成功。
*/
static int rockchip_saradc_read_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int *val, int *val2, long mask)
{
struct rockchip_saradc *saradc = iio_priv(indio_dev);
int ret;
switch (mask) {
case IIO_CHAN_INFO_RAW:
// 启动单次转换
ret = rockchip_saradc_start_single(saradc, chan->channel);
if (ret < 0)
return ret;
// 读取结果
*val = rockchip_saradc_read_result(saradc);
return IIO_VAL_INT;
case IIO_CHAN_INFO_SCALE:
// 计算实际电压值 = raw * 参考电压 / 分辨率
// 假设 1.8V 参考电压, 12 位分辨率
*val = 1800;
*val2 = 12; // 12 位分辨率
return IIO_VAL_FRACTIONAL_LOG2;
default:
return -EINVAL;
}
}
/**
* @brief SARADC 中断处理函数。
*
* 处理转换完成中断,唤醒等待的线程。
*
* @param irq 中断号。
* @param dev_id 指向 rockchip_saradc 结构。
*/
static irqreturn_t rockchip_saradc_irq_handler(int irq, void *dev_id)
{
struct rockchip_saradc *saradc = dev_id;
u32 int_stat;
// 1. 读取中断状态
int_stat = readl(saradc->base + SARADC_INT_STAT);
// 2. 处理转换完成中断
if (int_stat & (1 << 0)) {
// 清除中断
writel(1 << 0, saradc->base + SARADC_INT_STAT);
// 唤醒等待的线程
complete(&saradc->completion);
}
// 3. 处理错误中断
if (int_stat & (1 << 1)) {
dev_err(saradc->dev, "ADC error\n");
writel(1 << 1, saradc->base + SARADC_INT_STAT);
}
return IRQ_HANDLED;
}
/**
* @brief Platform 探测函数。
*
* 初始化 SARADC 硬件,注册 IIO 设备。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int rockchip_saradc_probe(struct platform_device *pdev)
{
struct rockchip_saradc *saradc;
struct iio_dev *indio_dev;
struct resource *res;
int ret, irq;
// 1. 分配 IIO 设备
indio_dev = devm_iio_device_alloc(&pdev->dev, sizeof(*saradc));
if (!indio_dev)
return -ENOMEM;
saradc = iio_priv(indio_dev);
saradc->indio_dev = indio_dev;
saradc->dev = &pdev->dev;
platform_set_drvdata(pdev, saradc);
// 2. 获取 I/O 资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "No memory resource\n");
return -ENXIO;
}
saradc->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(saradc->base))
return PTR_ERR(saradc->base);
// 3. 获取时钟
saradc->clk = devm_clk_get(&pdev->dev, "clk");
if (IS_ERR(saradc->clk))
return PTR_ERR(saradc->clk);
saradc->pclk = devm_clk_get(&pdev->dev, "pclk");
if (IS_ERR(saradc->pclk))
return PTR_ERR(saradc->pclk);
clk_prepare_enable(saradc->clk);
clk_prepare_enable(saradc->pclk);
// 4. 获取中断
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
ret = irq;
goto err_clk;
}
saradc->irq = irq;
// 5. 读取配置
saradc->channel_count = 4; // 假设 4 通道
saradc->sample_rate = 1000; // 1kHz
saradc->version = 0x100;
spin_lock_init(&saradc->lock);
init_completion(&saradc->completion);
// 6. 硬件初始化
ret = rockchip_saradc_hw_init(saradc);
if (ret) {
dev_err(&pdev->dev, "Failed to init hardware\n");
goto err_clk;
}
// 7. 注册中断
ret = devm_request_irq(&pdev->dev, saradc->irq, rockchip_saradc_irq_handler,
IRQF_SHARED, "rockchip-saradc", saradc);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
goto err_clk;
}
// 8. 设置 IIO 设备
indio_dev->name = "rockchip-saradc";
indio_dev->info = &rockchip_saradc_info;
indio_dev->modes = INDIO_DIRECT_MODE;
indio_dev->channels = rockchip_saradc_channels;
indio_dev->num_channels = saradc->channel_count;
// 9. 注册 IIO 设备
ret = devm_iio_device_register(&pdev->dev, indio_dev);
if (ret) {
dev_err(&pdev->dev, "Failed to register IIO device\n");
goto err_clk;
}
dev_info(&pdev->dev, "Rockchip SARADC registered at 0x%llx, IRQ %d\n",
(unsigned long long)res->start, saradc->irq);
return 0;
err_clk:
clk_disable_unprepare(saradc->pclk);
clk_disable_unprepare(saradc->clk);
return ret;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int rockchip_saradc_remove(struct platform_device *pdev)
{
struct rockchip_saradc *saradc = platform_get_drvdata(pdev);
// 1. 禁用 ADC
writel(0, saradc->base + SARADC_CTRL);
// 2. 禁用时钟
clk_disable_unprepare(saradc->pclk);
clk_disable_unprepare(saradc->clk);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id rockchip_saradc_of_match[] = {
{ .compatible = "rockchip,rk3399-saradc" },
{ .compatible = "rockchip,rk3568-saradc" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, rockchip_saradc_of_match);
/* Platform 驱动结构 */
static struct platform_driver rockchip_saradc_driver = {
.probe = rockchip_saradc_probe,
.remove = rockchip_saradc_remove,
.driver = {
.name = "rockchip-saradc",
.of_match_table = rockchip_saradc_of_match,
},
};
module_platform_driver(rockchip_saradc_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Rockchip SARADC Controller Driver");
MODULE_LICENSE("GPL v2");
第三章 SARADC 调试核心难点
3.1 ADC 读数异常(噪声或跳变)
现象:cat /sys/bus/iio/devices/iio:device0/in_voltage0_raw 显示的数值在多个值之间频繁跳变。
原因:
-
模拟输入信号存在噪声。
-
采样率过高导致转换不稳定。
-
参考电压不稳定。
调试方法:
-
降低采样率:
echo 100 > /sys/bus/iio/devices/iio:device0/sampling_frequency
-
添加软件滤波:
# 使用 iio 滤波工具 iio-filter --average 5 /sys/bus/iio/devices/iio:device0/in_voltage0_raw
-
检查参考电压:用万用表测量 ADC 参考引脚。
3.2 ADC 读数始终为 0
现象:无论输入电压如何,ADC 读数始终为 0。
原因:
-
输入通道未正确选择。
-
ADC 电源未启用。
-
参考电压为 0。
调试方法:
-
检查 ADC 状态:
devmem2 <base>+0x08 # SARADC_STATUS
-
检查电源控制:
devmem2 <base>+0x18 # SARADC_PWR_CTRL
-
检查时钟:
cat /sys/kernel/debug/clk/clk_summary | grep saradc
3.3 ADC 读数始终为最大值
现象:输入电压变化,ADC 读数始终为 1023(10 位)或 4095(12 位)。
原因:
-
输入电压超过参考电压。
-
输入引脚被上拉。
-
通道选择错误。
调试方法:
-
测量输入电压:用万用表测量 ADC 输入引脚电压。
-
检查参考电压:确认参考电压值。
-
检查通道选择:
devmem2 <base>+0x1C # SARADC_CHANNEL_SEL
3.4 ADC 转换超时
现象:读取 ADC 值时,程序卡住,dmesg 显示 "ADC conversion timeout"。
原因:
-
中断未触发。
-
硬件时钟停止。
-
中断被其他中断阻塞。
调试方法:
-
检查中断统计:
cat /proc/interrupts | grep saradc
-
检查中断使能:
devmem2 <base>+0x0C # SARADC_INT_EN
-
手动强制转换:
devmem2 <base>+0x00 0x03 # 强制启动
第四章 结合性能调试场景示例
场景:系统使用 ADC 读取电池电压,当 CPU 负载高时,ADC 读取结果出现波动。
分析流程:
-
宏观层面(图谱的 CPU 层):
-
perf top显示rockchip_saradc_irq_handler占用 CPU 约 10%。 -
中断触发频率较高。
-
-
ADC 层(图谱的 Device Drivers -> SARADC 层):
cat /sys/kernel/debug/iio/device0/registers
发现采样率为 1000Hz。
-
中断延迟(Interrupt Controller 层):
perf record -e irq:irq_handler_entry -a -- timeout 5 perf script | grep saradc
发现中断处理延迟在 CPU 负载高时达到 500us。
-
根本原因:
-
ADC 中断优先级较低,被其他高优先级中断(如网络、USB)抢占。
-
采样率 1000Hz 需要每 1ms 处理一次中断。
-
高 CPU 负载导致中断处理延迟超过 1ms,转换结果被丢弃或读取时已过期。
-
-
解决方案:
-
降低采样率到 100Hz。
-
提高 ADC 中断优先级。
-
使用 DMA 传输数据(如果硬件支持)。
-
在 ADC 驱动中添加硬件 FIFO 缓冲。
-
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| Clock Controller | 提供 ADC 核心时钟 | 频率稳定、分频计算 |
| GPIO | 配置 ADC 输入引脚 | 引脚模式(模拟输入) |
| Interrupt Controller | 触发转换完成中断 | 优先级、延迟 |
| Thermal | 读取内部温度传感器 | 温度校准、偏移补偿 |
| Power Supply | 读取电池电压 | 电压分压比、充电状态 |
第六部分 NPU 控制器 (Rockchip RKNPU2)
第一章 NPU 控制器在 Platform Bus 中的位置
NPU (Neural Processing Unit) 是专门为神经网络推理设计的协处理器。在嵌入式 AI 设备中,NPU 负责加速卷积神经网络 (CNN)、循环神经网络 (RNN) 等深度学习模型的推理计算。它通常作为 Platform 设备挂载在 SoC 内部总线上,通过专用 DMA 和中断机制与 CPU 协同工作。
在 Linux 5.10 中,Rockchip 的 NPU 驱动位于 drivers/soc/rockchip/rknpu/ (RKNPU2),通过 IOCTL 接口暴露给用户空间,支持 TensorFlow Lite、ONNX、RKNN 等模型格式。
1.1 硬件关键概念
-
NPU 核心寄存器:管理 NPU 电源、时钟、工作状态。
-
DMA 引擎:从内存直接搬运模型权重和输入数据。
-
指令队列:接收 CPU 提交的推理指令(卷积、池化、全连接等)。
-
中断:处理推理完成、指令执行错误、电源异常等。
-
DVFS:动态调频调压,平衡性能与功耗。
1.2 核心代码与 Doxygen 注解
// 基于 Linux 5.10 drivers/soc/rockchip/rknpu/rknpu2.c (精简版)
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <linux/dma-mapping.h>
#include <linux/mm.h>
#include <linux/sched.h>
/**
* @brief Rockchip NPU 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 NPU 硬件实例。
*/
struct rknpu_device {
void __iomem *base; /**< 映射后的寄存器基址 */
int irq; /**< NPU 中断号 */
struct clk *aclk; /**< 总线时钟 */
struct clk *hclk; /**< 核心时钟 */
struct clk *pclk; /**< APB 总线时钟 */
struct device *dev; /**< 设备指针 */
struct miscdevice misc; /**< 字符设备抽象 */
spinlock_t lock; /**< 硬件保护锁 */
struct work_struct power_work; /**< 电源管理工作队列 */
u32 version; /**< NPU 版本号 (2.0/2.5) */
u32 core_count; /**< 核心数量 (1/2/3) */
u32 max_freq; /**< 最大频率 */
struct rknpu_regs *regs; /**< 寄存器缓存 */
};
/* 寄存器偏移量 (Rockchip NPU2) */
#define NPU_REG_CTRL 0x000
#define NPU_REG_STATUS 0x004
#define NPU_REG_INT_EN 0x008
#define NPU_REG_INT_STAT 0x00C
#define NPU_REG_CORE_CTRL 0x100
#define NPU_REG_CORE_STATUS 0x104
#define NPU_REG_INSTR_QUEUE 0x200
#define NPU_REG_INSTR_STAT 0x204
#define NPU_REG_WEIGHT_BASE 0x300
#define NPU_REG_DATA_BASE 0x304
#define NPU_REG_RESULT_BASE 0x308
#define NPU_REG_DMA_CTRL 0x400
#define NPU_REG_DMA_STATUS 0x404
#define NPU_REG_DVFS_CTRL 0x500
#define NPU_REG_DVFS_STATUS 0x504
/**
* @brief 初始化 NPU 电源管理。
*
* @param npu 指向 rknpu_device 结构。
* @return 0 成功。
*/
static int rknpu_power_init(struct rknpu_device *npu)
{
u32 status;
// 1. 检查电源域状态
status = readl(npu->base + NPU_REG_STATUS);
if (!(status & 0x1)) {
dev_err(npu->dev, "NPU power domain not ready\n");
return -EIO;
}
// 2. 启用核心
writel(0x1, npu->base + NPU_REG_CORE_CTRL);
msleep(10);
// 3. 检查核心状态
status = readl(npu->base + NPU_REG_CORE_STATUS);
if (!(status & 0x1)) {
dev_err(npu->dev, "NPU core not ready\n");
return -EIO;
}
return 0;
}
/**
* @brief 初始化 NPU DMA。
*
* @param npu 指向 rknpu_device 结构。
* @param weight_dma 权重数据 DMA 地址。
* @param input_dma 输入数据 DMA 地址。
* @param output_dma 输出数据 DMA 地址。
*/
static void rknpu_dma_init(struct rknpu_device *npu,
dma_addr_t weight_dma,
dma_addr_t input_dma,
dma_addr_t output_dma)
{
// 1. 设置权重地址
writel(lower_32_bits(weight_dma), npu->base + NPU_REG_WEIGHT_BASE);
writel(upper_32_bits(weight_dma), npu->base + NPU_REG_WEIGHT_BASE + 4);
// 2. 设置输入数据地址
writel(lower_32_bits(input_dma), npu->base + NPU_REG_DATA_BASE);
writel(upper_32_bits(input_dma), npu->base + NPU_REG_DATA_BASE + 4);
// 3. 设置输出数据地址
writel(lower_32_bits(output_dma), npu->base + NPU_REG_RESULT_BASE);
writel(upper_32_bits(output_dma), npu->base + NPU_REG_RESULT_BASE + 4);
// 4. 启用 DMA
writel(0x1, npu->base + NPU_REG_DMA_CTRL);
}
/**
* @brief 提交 NPU 推理任务。
*
* @param npu 指向 rknpu_device 结构。
* @param instr_ptr 指令序列指针。
* @param instr_len 指令序列长度。
* @return 0 成功。
*/
static int rknpu_submit_task(struct rknpu_device *npu,
u32 *instr_ptr, u32 instr_len)
{
int i;
// 1. 检查 NPU 是否空闲
u32 status = readl(npu->base + NPU_REG_STATUS);
if (status & 0x2) {
dev_err(npu->dev, "NPU busy\n");
return -EBUSY;
}
// 2. 将指令写入 NPU 指令队列
for (i = 0; i < instr_len; i++) {
writel(instr_ptr[i], npu->base + NPU_REG_INSTR_QUEUE + i * 4);
}
// 3. 启动推理
writel(0x1, npu->base + NPU_REG_CTRL);
// 4. 启用中断
writel(0x1, npu->base + NPU_REG_INT_EN);
return 0;
}
/**
* @brief 读取 NPU 推理结果。
*
* @param npu 指向 rknpu_device 结构。
* @param output_dma 输出数据地址。
* @param output_len 输出数据长度。
*/
static void rknpu_get_result(struct rknpu_device *npu,
dma_addr_t output_dma,
u32 output_len)
{
// 结果已通过 DMA 直接写入内存
// 只需要通知用户空间读取即可
}
/**
* @brief 配置 NPU 动态电压频率调整。
*
* @param npu 指向 rknpu_device 结构。
* @param freq 目标频率 (MHz)。
*/
static void rknpu_dvfs_config(struct rknpu_device *npu, u32 freq)
{
u32 div = clk_get_rate(npu->aclk) / (freq * 1000000);
if (div < 1) div = 1;
if (div > 0xFF) div = 0xFF;
writel(div, npu->base + NPU_REG_DVFS_CTRL);
}
/**
* @brief NPU 中断处理函数。
*
* 处理推理完成、指令错误、DMA 错误等事件。
*
* @param irq 中断号。
* @param dev_id 指向 rknpu_device 结构。
*/
static irqreturn_t rknpu_irq_handler(int irq, void *dev_id)
{
struct rknpu_device *npu = dev_id;
u32 int_stat;
// 1. 读取中断状态
int_stat = readl(npu->base + NPU_REG_INT_STAT);
// 2. 处理推理完成中断
if (int_stat & (1 << 0)) {
writel(1 << 0, npu->base + NPU_REG_INT_STAT);
// 通知用户空间推理完成
wake_up_interruptible(&npu->wait_queue);
}
// 3. 处理指令错误中断
if (int_stat & (1 << 1)) {
dev_err(npu->dev, "NPU instruction error\n");
// 读取错误指令地址
u32 err_addr = readl(npu->base + NPU_REG_INSTR_STAT);
dev_err(npu->dev, "Error instruction address: 0x%08x\n", err_addr);
writel(1 << 1, npu->base + NPU_REG_INT_STAT);
}
// 4. 处理 DMA 错误中断
if (int_stat & (1 << 2)) {
dev_err(npu->dev, "NPU DMA error\n");
writel(1 << 2, npu->base + NPU_REG_INT_STAT);
// 可能需要重置 DMA
}
return IRQ_HANDLED;
}
/**
* @brief IOCTL 接口:提交推理任务。
*
* @param npu 指向 rknpu_device 结构。
* @param arg 用户空间参数。
* @return 0 成功。
*/
static long rknpu_ioctl_submit(struct rknpu_device *npu, void __user *arg)
{
struct rknpu_task task;
dma_addr_t weight_dma, input_dma, output_dma;
int ret;
// 1. 从用户空间复制参数
if (copy_from_user(&task, arg, sizeof(task))) {
dev_err(npu->dev, "Failed to copy task from user\n");
return -EFAULT;
}
// 2. 映射用户空间 DMA 缓冲区
weight_dma = dma_map_single(npu->dev, task.weight, task.weight_len,
DMA_TO_DEVICE);
if (dma_mapping_error(npu->dev, weight_dma)) {
dev_err(npu->dev, "Failed to map weight DMA\n");
return -ENOMEM;
}
input_dma = dma_map_single(npu->dev, task.input, task.input_len,
DMA_TO_DEVICE);
if (dma_mapping_error(npu->dev, input_dma)) {
dev_err(npu->dev, "Failed to map input DMA\n");
dma_unmap_single(npu->dev, weight_dma, task.weight_len, DMA_TO_DEVICE);
return -ENOMEM;
}
output_dma = dma_map_single(npu->dev, task.output, task.output_len,
DMA_FROM_DEVICE);
if (dma_mapping_error(npu->dev, output_dma)) {
dev_err(npu->dev, "Failed to map output DMA\n");
dma_unmap_single(npu->dev, input_dma, task.input_len, DMA_TO_DEVICE);
dma_unmap_single(npu->dev, weight_dma, task.weight_len, DMA_TO_DEVICE);
return -ENOMEM;
}
// 3. 初始化 DMA
rknpu_dma_init(npu, weight_dma, input_dma, output_dma);
// 4. 提交推理任务
ret = rknpu_submit_task(npu, task.instr_ptr, task.instr_len);
if (ret < 0) {
dev_err(npu->dev, "Failed to submit task\n");
dma_unmap_single(npu->dev, output_dma, task.output_len, DMA_FROM_DEVICE);
dma_unmap_single(npu->dev, input_dma, task.input_len, DMA_TO_DEVICE);
dma_unmap_single(npu->dev, weight_dma, task.weight_len, DMA_TO_DEVICE);
return ret;
}
// 5. 等待推理完成
wait_event_interruptible(npu->wait_queue,
!(readl(npu->base + NPU_REG_STATUS) & 0x2));
// 6. 取消 DMA 映射
dma_unmap_single(npu->dev, output_dma, task.output_len, DMA_FROM_DEVICE);
dma_unmap_single(npu->dev, input_dma, task.input_len, DMA_TO_DEVICE);
dma_unmap_single(npu->dev, weight_dma, task.weight_len, DMA_TO_DEVICE);
return 0;
}
/**
* @brief 字符设备 IOCTL 接口。
*
* @param file 指向 file 结构。
* @param cmd IOCTL 命令。
* @param arg 参数。
* @return 0 成功。
*/
static long rknpu_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct rknpu_device *npu = container_of(file->private_data,
struct rknpu_device, misc);
long ret = -ENOTTY;
switch (cmd) {
case RKNPU_IOCTL_SUBMIT:
ret = rknpu_ioctl_submit(npu, (void __user *)arg);
break;
case RKNPU_IOCTL_GET_VERSION:
ret = put_user(npu->version, (u32 __user *)arg);
break;
case RKNPU_IOCTL_GET_CORE_COUNT:
ret = put_user(npu->core_count, (u32 __user *)arg);
break;
default:
break;
}
return ret;
}
/**
* @brief Platform 探测函数。
*
* 初始化 NPU 控制器硬件,注册字符设备。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int rknpu_probe(struct platform_device *pdev)
{
struct rknpu_device *npu;
struct resource *res;
int ret, irq;
// 1. 分配私有数据结构
npu = devm_kzalloc(&pdev->dev, sizeof(*npu), GFP_KERNEL);
if (!npu)
return -ENOMEM;
platform_set_drvdata(pdev, npu);
npu->dev = &pdev->dev;
// 2. 获取 I/O 资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "No memory resource\n");
return -ENXIO;
}
npu->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(npu->base))
return PTR_ERR(npu->base);
// 3. 获取时钟
npu->aclk = devm_clk_get(&pdev->dev, "aclk");
if (IS_ERR(npu->aclk))
return PTR_ERR(npu->aclk);
npu->hclk = devm_clk_get(&pdev->dev, "hclk");
if (IS_ERR(npu->hclk))
return PTR_ERR(npu->hclk);
npu->pclk = devm_clk_get(&pdev->dev, "pclk");
if (IS_ERR(npu->pclk))
return PTR_ERR(npu->pclk);
clk_prepare_enable(npu->aclk);
clk_prepare_enable(npu->hclk);
clk_prepare_enable(npu->pclk);
// 4. 获取中断
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
ret = irq;
goto err_clk;
}
npu->irq = irq;
// 5. 读取 NPU 配置
npu->version = readl(npu->base + 0x00);
npu->core_count = readl(npu->base + 0x04);
npu->max_freq = readl(npu->base + 0x08);
spin_lock_init(&npu->lock);
init_waitqueue_head(&npu->wait_queue);
// 6. 硬件初始化
ret = rknpu_power_init(npu);
if (ret) {
dev_err(&pdev->dev, "Failed to init NPU power\n");
goto err_clk;
}
// 7. 注册中断
ret = devm_request_irq(&pdev->dev, npu->irq, rknpu_irq_handler,
IRQF_SHARED, "rknpu", npu);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
goto err_clk;
}
// 8. 注册字符设备
npu->misc.minor = MISC_DYNAMIC_MINOR;
npu->misc.name = "rknpu";
npu->misc.fops = &rknpu_fops;
ret = misc_register(&npu->misc);
if (ret) {
dev_err(&pdev->dev, "Failed to register misc device\n");
goto err_clk;
}
// 9. 注册到设备树
dev_info(&pdev->dev, "Rockchip NPU registered at 0x%llx, IRQ %d, version %d, cores %d\n",
(unsigned long long)res->start, npu->irq, npu->version, npu->core_count);
return 0;
err_clk:
clk_disable_unprepare(npu->pclk);
clk_disable_unprepare(npu->hclk);
clk_disable_unprepare(npu->aclk);
return ret;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int rknpu_remove(struct platform_device *pdev)
{
struct rknpu_device *npu = platform_get_drvdata(pdev);
// 1. 注销字符设备
misc_deregister(&npu->misc);
// 2. 关闭 NPU
writel(0, npu->base + NPU_REG_CTRL);
// 3. 禁用时钟
clk_disable_unprepare(npu->pclk);
clk_disable_unprepare(npu->hclk);
clk_disable_unprepare(npu->aclk);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id rknpu_of_match[] = {
{ .compatible = "rockchip,rk3568-npu" },
{ .compatible = "rockchip,rk3588-npu" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, rknpu_of_match);
/* Platform 驱动结构 */
static struct platform_driver rknpu_driver = {
.probe = rknpu_probe,
.remove = rknpu_remove,
.driver = {
.name = "rknpu",
.of_match_table = rknpu_of_match,
},
};
module_platform_driver(rknpu_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Rockchip NPU Controller Driver");
MODULE_LICENSE("GPL v2");
第三章 NPU 调试核心难点
3.1 模型推理失败
现象:用户空间调用 IOCTL 提交推理任务后,系统卡住或返回错误码。
原因:
-
模型权重数据格式错误。
-
DMA 地址对齐错误。
-
指令序列不完整。
调试方法:
-
检查 NPU 状态:
devmem2 <base>+0x004 # NPU_REG_STATUS
-
检查中断统计:
cat /proc/interrupts | grep rknpu
-
使用调试工具:
# 使用 rknn 测试工具 rknn-test -m model.rknn -i input.bin
3.2 内存分配失败
现象:DMA 映射时失败,dmesg 显示 "Failed to map weight DMA"。
原因:
-
内存申请超过连续内存大小。
-
用户空间缓冲区未对齐到 64 字节。
-
DMA 地址超过 32 位范围。
调试方法:
-
检查 CMA 大小:
cat /sys/kernel/debug/dma/cma/cma-0/used
-
增加 CMA 大小:
# 在 kernel cmdline 中设置 cma=256M
-
检查 DMA 对齐:
# 检查用户空间分配 cat /proc/sys/kernel/dma_alignment
3.3 推理速度慢
现象:模型推理速度远低于理论值。
原因:
-
NPU 频率过低。
-
数据未在 DDR 中连续存放。
-
模型量化精度低。
调试方法:
-
检查 NPU 频率:
cat /sys/kernel/debug/clk/clk_summary | grep npu
-
增加频率:
# 设置最大频率 echo 800000000 > /sys/devices/platform/*npu/devfreq/devfreq0/max_freq
-
使用 DMA 缓冲池:预分配 DMA 缓冲区,减少映射开销。
3.4 多任务并发问题
现象:多个应用程序同时使用 NPU 时,任务冲突或死锁。
原因:
-
无并发控制机制。
-
中断冲突。
-
资源未同步。
调试方法:
-
检查锁状态:
cat /proc/lockdep
-
优化并发控制:
-
增加硬件队列深度。
-
使用任务优先级管理。
-
第四章 结合性能调试场景示例
场景:4K 图像进行目标检测时,推理时间 500ms,远高于要求的 50ms,无法满足实时需求。
分析流程:
-
宏观层面(图谱的 CPU 和 Memory 层):
-
perf top显示rknpu_ioctl_submit占用 CPU 约 15%。 -
iostat显示内存带宽高,但主要不是 NPU 消耗。
-
-
NPU 层(图谱的 Device Drivers -> NPU 层):
devmem2 <base>+0x004 # NPU_REG_STATUS
显示 NPU 频率为 200MHz。
-
DVFS 层(Power 层):
cat /sys/devices/platform/*npu/devfreq/devfreq0/cur_freq
发现 NPU 频率被限制在 200MHz,低于最高 800MHz。
-
根本原因:
-
热降频 (Thermal Throttling) 导致 NPU 频率无法提升。
-
温度达到 85°C 阈值。
-
散热设计不足。
-
-
解决方案:
-
提高温度阈值
temperature-high = <90000>;。 -
增加散热片或主动散热。
-
降低输入图像分辨率 (从 4K 到 1080p)。
-
优化模型,使用更低精度的量化。
-
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| DMA | 模型权重和输入数据传输 | 地址对齐、带宽分配 |
| Thermal | 监测 NPU 温度 | 降频阈值、散热设计 |
| Power | 动态电压频率调整 | 频率上限、功耗优化 |
| Clock Controller | 提供 NPU 时钟 | 时钟源稳定 |
| Interrupt Controller | 推理完成中断 | 优先级、延迟 |
第七部分 Ethernet MAC 控制器 (STMMAC/Rockchip)
第一章 控制器在 Platform Bus 中的位置
Ethernet MAC 控制器 是挂载在 Platform Bus 上的一个独立外设。它负责数据包处理(封装成帧、地址过滤、CRC 校验)和DMA 传输(将数据从内存搬运到硬件 FIFO,反之亦然)。它通过 RGMII/RMII/MII 接口连接到 PHY 芯片(负责物理层信号转换),通过 MDIO 总线管理 PHY。
Linux 5.10 中,STMMAC 驱动的核心层次:
-
STMMAC 核心层 (
drivers/net/ethernet/stmicro/stmmac/):管理 MAC 硬件、DMA 通道、中断、NAPI 机制。 -
SoC 特定逻辑层(如
dwmac-rockchip.c):负责获取特定的时钟、PHY 接口配置、RGMII 延迟调整。 -
网络核心层 (
net/core/):net_device、sk_buff、NAPI、中断。 -
PHY 层 (
drivers/net/phy/):通过 MDIO 管理外部 PHY 状态。
第二部分:核心数据结构与关键操作
2.1 核心数据结构
// 基于 Linux 5.10 drivers/net/ethernet/stmicro/stmmac/stmmac.h (简化)
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/netdevice.h>
#include <linux/phy.h>
#include <linux/ethtool.h>
#include <linux/skbuff.h>
#include <linux/interrupt.h>
/**
* @brief STMMAC 核心的私有数据结构。
*
* 包含 DMA 配置、NAPI 状态、中断配置、硬件寄存器映射等核心内容。
*/
struct stmmac_priv {
struct net_device *dev; /**< 网络设备指针 */
void __iomem *ioaddr; /**< 寄存器基址 */
struct stmmac_dma_cfg dma_cfg; /**< DMA 配置 (突发长度、阈值等) */
struct stmmac_chain *tx_chain; /**< TX 描述符链 */
struct stmmac_chain *rx_chain; /**< RX 描述符链 */
struct napi_struct napi; /**< NAPI 结构 (用于轮询) */
spinlock_t lock; /**< 自旋锁 */
u32 dma_rx_size; /**< RX 描述符数量 */
u32 dma_tx_size; /**< TX 描述符数量 */
bool rx_coalesce; /**< 是否启用 RX 中断聚合 */
u32 rx_coalesce_usecs; /**< RX 聚合超时 (微秒) */
u32 rx_max_coalesced_frames; /**< RX 最大聚合帧数 */
struct stmmac_packet_desc *rx_pool; /**< RX 缓冲区池 */
struct stmmac_packet_desc *tx_pool; /**< TX 缓冲区池 */
struct device *device; /**< 设备指针 */
};
/**
* @brief Rockchip 特定扩展数据。
*/
struct rockchip_dwmac {
struct stmmac_priv *stmmac; /**< 指向核心私有数据 */
struct clk *stmmac_clk; /**< MAC 核心时钟 */
struct clk *pclk; /**< 总线时钟 */
struct clk *ptp_ref_clk; /**< PTP 参考时钟 */
struct phy_device *phydev; /**< 关联的 PHY 设备 */
u32 tx_delay; /**< RGMII 发送延迟 (相位调整) */
u32 rx_delay; /**< RGMII 接收延迟 */
int phy_interface; /**< PHY 接口模式 (RGMII/RMII) */
};
2.2 核心操作:NAPI 轮询 (数据包接收)
这是以太网驱动最关键的路径。它利用 NAPI(New API)避免在高速流量下发生中断风暴。
/**
* @brief NAPI 轮询函数 (替代中断处理进行接收)。
*
* 当数据包到达时,NAPI 会禁用 RX 中断并轮询 RX 描述符。这极大地减少了中断开销。
*
* @param napi 指向 napi_struct。
* @param budget 本次轮询允许处理的最大帧数。
* @return 实际处理的帧数。
*/
static int stmmac_napi_poll(struct napi_struct *napi, int budget)
{
struct stmmac_priv *priv = container_of(napi, struct stmmac_priv, napi);
struct net_device *dev = priv->dev;
struct stmmac_packet_desc *rx_desc;
struct sk_buff *skb;
int work_done = 0;
// 1. 检查 RX 描述符环是否已处理完所有帧
while (work_done < budget) {
// 获取当前 RX 描述符
rx_desc = &priv->rx_chain[priv->rx_cur_desc];
if (!(rx_desc->status & RX_DESC_STATUS_OWN)) {
// 硬件已标记完成(DMA 将数据写入内存)
dma_unmap_single(priv->device, rx_desc->addr,
rx_desc->len, DMA_FROM_DEVICE);
// 2. 创建 SKB 并复制数据
skb = napi_alloc_skb(napi, rx_desc->len);
if (likely(skb)) {
memcpy(skb_put(skb, rx_desc->len),
rx_desc->virt_addr, rx_desc->len);
skb->protocol = eth_type_trans(skb, dev);
netif_receive_skb(skb); // 送入网络协议栈
}
// 3. 清理描述符,重新填充 DMA 缓冲区
priv->rx_cur_desc = (priv->rx_cur_desc + 1) % priv->dma_rx_size;
work_done++;
} else {
break; // 无更多数据
}
}
// 4. 如果未达到预算,重新启用中断
if (work_done < budget) {
napi_complete(napi);
stmmac_enable_rx_interrupt(priv); // 重新开启中断
}
return work_done;
}
2.3 核心操作:TX 传输
/**
* @brief 网络设备 TX 回调函数。
*
* @param skb 指向 sk_buff。
* @param dev 指向 net_device。
* @return NETDEV_TX_OK 成功。
*/
static netdev_tx_t stmmac_tx(struct sk_buff *skb, struct net_device *dev)
{
struct stmmac_priv *priv = netdev_priv(dev);
struct stmmac_packet_desc *tx_desc;
dma_addr_t dma_addr;
// 1. 获取当前 TX 描述符
tx_desc = &priv->tx_chain[priv->tx_cur_desc];
// 2. 映射 SKB 数据到 DMA 地址
dma_addr = dma_map_single(priv->device, skb->data, skb->len, DMA_TO_DEVICE);
if (dma_mapping_error(priv->device, dma_addr)) {
netdev_err(dev, "Failed to map TX DMA\n");
dev_kfree_skb(skb);
return NETDEV_TX_OK;
}
tx_desc->addr = dma_addr;
tx_desc->len = skb->len;
tx_desc->status = TX_DESC_STATUS_OWN; // 将所有权交给 DMA
// 3. 触发 DMA 发送
stmmac_tx_dma_start(priv);
priv->tx_cur_desc = (priv->tx_cur_desc + 1) % priv->dma_tx_size;
// 4. 如果 TX 环已满,停止队列
if (priv->tx_cur_desc == priv->tx_next_desc) {
netif_stop_queue(dev);
}
dev_kfree_skb(skb); // SKB 数据已被 DMA 映射,交给硬件后释放
return NETDEV_TX_OK;
}
第三部分:资深视角 —— STMMAC 调试与性能优化
3.1 丢包调试 (Dropped Packets)
现象:ifconfig eth0 显示 rx_dropped 或 tx_dropped 不断上升。
原因:
-
RX 掉包:RX 描述符不足,DMA 缓冲区无法及时返回给硬件。
-
TX 掉包:TX 队列满,上层无法入队。
-
中断风暴:中断处理未及时完成,导致新帧被丢弃。
调试方法:
-
查看系统统计:
ethtool -S eth0 # 查看硬件统计(丢包、CRC 错误) netstat -i # 查看接口统计
-
查看 RX 环:
# 查看 DMA RX 环大小(默认 256) cat /sys/class/net/eth0/rings/rx_pending
-
使用 trace-cmd 跟踪 NAPI:
# 跟踪 NAPI 轮询过程 trace-cmd record -e net:netif_receive_skb -e net:netif_napi_add -e net:napi_poll trace-cmd report | grep -E "NAPI|drop"
如果在
napi_poll中看到大量WORK_DONE等于 0,表示 NAPI 未及时处理数据。 -
增加 NAPI budget 或环大小:
# 增加 RX 描述符数量 ethtool -G eth0 rx 1024 tx 512 # 增加 NAPI 预算 echo 300 > /sys/module/stmmac/parameters/napi_budget
3.2 吞吐量低 (Low Throughput)
现象:iperf3 -c host 只能跑到 200Mbps,而硬件支持 1Gbps。
原因:
-
PHY 协商到低速(如 100Mbps)。
-
中断聚合设置不当。
-
DMA 突发长度过小。
调试方法:
-
检查链路速度:
ethtool eth0
-
检查中断聚合:
ethtool -c eth0 # 查看聚合参数 ethtool -C eth0 rx-usecs 20 rx-frames 32 # 增加聚合
-
检查 DMA 突发长度:
# 查看驱动中的 DMA 配置 devmem2 <base_addr>+0x200 # 读取 DMA 控制寄存器
如果突发长度是 64 字节,尝试增加到 256 或 512。
-
使用
perf监控中断处理时间:perf record -e irq:irq_handler_entry -e irq:irq_handler_exit -a -- sleep 10 perf script | grep -E "stmmac|napi"
如果处理时间超过 100us,可能 NAPI 未及时完成。
3.3 中断风暴 (Interrupt Storm)
现象:cat /proc/interrupts | grep eth0 显示每秒数千次中断,CPU 占用高。
原因:
-
中断聚合未启用或设置过小。
-
RX 描述符环过小,导致硬件频繁触发 RX 中断。
-
驱动未正确清除中断标志。
调试方法:
-
启用中断聚合:
ethtool -C eth0 rx-usecs 100 tx-usecs 100
-
使用
bpftrace动态插桩 ISR:bpftrace -e 'kprobe:stmmac_interrupt /arg1 == <irq_num>/ { @count[tid] = count(); @time[tid] = nsecs; } kretprobe:stmmac_interrupt { @us[tid] = (nsecs - @time[tid]) / 1000; }' -
强制进入 NAPI 轮询模式:
# 关闭硬件中断,仅通过 NAPI 轮询 echo 1 > /sys/class/net/eth0/napi_mode
3.4 RGMII 时序问题 (Phantom Issues)
现象:链路正常,但丢包率非常高,CRC 错误多。RGMII 接口对相位非常敏感。
根本原因:TX/RX 时钟与数据线的相位偏移。
调试方法:
-
检查当前延迟:
# 读取硬件延迟配置 devmem2 <base_addr>+0x34 # RGMII_CTRL 寄存器
-
调整延迟:在 DTS 中修改
tx_delay和rx_delay(从 0 到 31)。 -
测试不同相位:
# 逐步调整,每次 1 步,测试吞吐量 for i in {0..31}; do echo $i > /sys/class/net/eth0/rx_delay; done
第四章 结合性能调试场景示例
场景:服务器通过千兆网卡发送大文件时,偶尔出现持续 100ms 的卡顿,iperf3 测试间歇性降速到 100Mbps。
分析流程:
-
宏观层面(图谱的 CPU 层):
-
perf top显示stmmac_napi_poll占用 CPU 较高。 -
cat /proc/interrupts显示网卡中断触发频繁。
-
-
网络层(图谱的 Device Drivers -> Ethernet MAC 层):
ethtool -S eth0
发现
rx_errors和rx_missed在卡顿期间急剧增加。 -
DMA 层(DMA 控制器层):
trace-cmd record -e dma:* -e net:netdev_xmit -a -- sleep 5 trace-cmd report | grep -E "dma|stmmac"
发现
dma_sync_single_for_cpu被频繁调用,且耗时较长。 -
根本原因:
-
TX DMA 描述符环太小(仅 64 个)。
-
当突发流量到来时,描述符环迅速被填满,驱动进入
netif_stop_queue()。 -
内核
dma_sync_single_for_cpu对每个 TX 描述符进行缓存刷新,造成延迟。
-
-
解决方案:
-
增加 DMA 描述符环大小:
ethtool -G eth0 tx 1024
-
启用
tx_napi模式,减少中断频率。 -
使用
dma_alloc_coherent分配 TX 缓冲区,避免sync开销。 -
增加
tx_max_coalesced_frames到 64。
-
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| PHY | 通过 MDIO 总线管理物理层芯片 | 链路协商、信号质量 |
| DMA | 数据包搬运 | 描述符环大小、对齐、带宽 |
| Interrupt Controller (GIC) | 将中断路由到特定 CPU | 中断亲和性、优先级 |
| Power Management | WOL (Wake-on-LAN)、链路休眠 | WOL 配置、PME 信号 |
| PTP Hardware Clock (PHC) | 高精度时间同步 | PTP 事件中断 |
第八部分 MIPI D-PHY 控制器
第一章 MIPI D-PHY 控制器在 Platform Bus 中的位置
MIPI D-PHY 是 MIPI 联盟定义的物理层接口标准,用于连接 SoC 与 MIPI DSI(显示接口)或 MIPI CSI(摄像头接口)设备。它通过高速差分信号传输数据,通常作为 Platform 设备挂载在 SoC 内部总线上,是 CSI 和 DSI 控制器的物理层基础。
在 Linux 5.10 中,MIPI D-PHY 驱动通常位于 drivers/phy/ 或 drivers/phy/mipi/,通过 PHY 框架 暴露控制接口。典型驱动包括 phy-rockchip-mipi-dphy.c、phy-mipi-dphy-standalone.c 等。
1.1 硬件关键概念
-
D-PHY 核心寄存器:控制寄存器、状态寄存器、中断使能、链路配置。
-
高速模式 (HS Mode):用于传输视频/图像数据,速率可达 1Gbps 以上。
-
低功耗模式 (LP Mode):用于控制信号和低功耗状态。
-
时钟通道 (Clock Lane):提供 DDR 时钟信号。
-
数据通道 (Data Lane):传输数据,支持 1-4 条通道。
-
ESD 保护:内置静电防护电路。
-
自动校准:支持自动补偿信号质量。
1.2 核心代码
// 基于 Linux 5.10 drivers/phy/rockchip/phy-rockchip-mipi-dphy.c (精简版)
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <linux/phy/phy.h>
#include <linux/delay.h>
/**
* @brief Rockchip MIPI D-PHY 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 MIPI D-PHY 硬件实例。
*/
struct rockchip_mipi_dphy {
void __iomem *base; /**< 映射后的寄存器基址 */
int irq; /**< 中断号 */
struct clk *pclk; /**< 总线时钟 */
struct clk *ref_clk; /**< 参考时钟 */
struct phy *phy; /**< PHY 抽象 */
struct device *dev; /**< 设备指针 */
spinlock_t lock; /**< 硬件保护锁 */
u32 version; /**< PHY 版本 */
u32 lanes; /**< 数据通道数 */
u32 max_speed; /**< 最大速度 (Mbps) */
u32 current_speed; /**< 当前速度 */
struct completion completion; /**< 完成同步 */
};
/* 寄存器偏移量 (Rockchip MIPI D-PHY) */
#define DPHY_CTRL 0x00
#define DPHY_STATUS 0x04
#define DPHY_INT_EN 0x08
#define DPHY_INT_STAT 0x0C
#define DPHY_CLK_CTRL 0x10
#define DPHY_CLK_STATUS 0x14
#define DPHY_LANE_CTRL 0x20
#define DPHY_LANE_STATUS 0x24
#define DPHY_CALIBRATION 0x30
#define DPHY_CALIBRATION_STATUS 0x34
#define DPHY_POWER_CTRL 0x40
#define DPHY_POWER_STATUS 0x44
/**
* @brief 初始化 MIPI D-PHY 硬件。
*
* @param dphy 指向 rockchip_mipi_dphy 结构。
* @return 0 成功。
*/
static int rockchip_mipi_dphy_hw_init(struct rockchip_mipi_dphy *dphy)
{
u32 ctrl;
// 1. 上电 D-PHY
writel(0x1, dphy->base + DPHY_POWER_CTRL);
msleep(10);
// 2. 检查电源状态
if (!(readl(dphy->base + DPHY_POWER_STATUS) & 0x1)) {
dev_err(dphy->dev, "D-PHY power on failed\n");
return -EIO;
}
// 3. 配置时钟通道
ctrl = readl(dphy->base + DPHY_CLK_CTRL);
ctrl |= (1 << 0); // 启用时钟通道
writel(ctrl, dphy->base + DPHY_CLK_CTRL);
// 4. 配置数据通道
ctrl = readl(dphy->base + DPHY_LANE_CTRL);
ctrl &= ~0x0F;
ctrl |= dphy->lanes; // 设置通道数
writel(ctrl, dphy->base + DPHY_LANE_CTRL);
// 5. 执行自动校准
writel(0x1, dphy->base + DPHY_CALIBRATION);
msleep(50);
// 6. 检查校准状态
if (!(readl(dphy->base + DPHY_CALIBRATION_STATUS) & 0x1)) {
dev_err(dphy->dev, "D-PHY calibration failed\n");
return -EIO;
}
return 0;
}
/**
* @brief 配置 D-PHY 速度。
*
* @param dphy 指向 rockchip_mipi_dphy 结构。
* @param speed 目标速度 (Mbps)。
*/
static int rockchip_mipi_dphy_set_speed(struct rockchip_mipi_dphy *dphy,
u32 speed)
{
u32 ctrl, div;
if (speed > dphy->max_speed) {
dev_err(dphy->dev, "Speed %d exceeds max %d\n", speed, dphy->max_speed);
return -EINVAL;
}
// 1. 计算分频系数 (基于参考时钟)
u32 ref_clk_rate = clk_get_rate(dphy->ref_clk);
div = ref_clk_rate / (speed * 1000000);
if (div < 1) div = 1;
if (div > 0xFF) div = 0xFF;
// 2. 配置时钟分频
ctrl = readl(dphy->base + DPHY_CLK_CTRL);
ctrl &= ~0xFF00;
ctrl |= (div << 8);
writel(ctrl, dphy->base + DPHY_CLK_CTRL);
dphy->current_speed = speed;
return 0;
}
/**
* @brief 启动 MIPI D-PHY 传输。
*
* @param dphy 指向 rockchip_mipi_dphy 结构。
* @return 0 成功。
*/
static int rockchip_mipi_dphy_start(struct rockchip_mipi_dphy *dphy)
{
u32 ctrl;
// 1. 启用高速模式
ctrl = readl(dphy->base + DPHY_CTRL);
ctrl |= (1 << 0); // 启用高速模式
ctrl |= (1 << 1); // 启用数据通道
writel(ctrl, dphy->base + DPHY_CTRL);
// 2. 启用中断
writel(0x0F, dphy->base + DPHY_INT_EN);
return 0;
}
/**
* @brief 停止 MIPI D-PHY 传输。
*
* @param dphy 指向 rockchip_mipi_dphy 结构。
*/
static void rockchip_mipi_dphy_stop(struct rockchip_mipi_dphy *dphy)
{
// 1. 禁用中断
writel(0x00, dphy->base + DPHY_INT_EN);
// 2. 禁用高速模式
u32 ctrl = readl(dphy->base + DPHY_CTRL);
ctrl &= ~(1 << 0);
ctrl &= ~(1 << 1);
writel(ctrl, dphy->base + DPHY_CTRL);
}
/**
* @brief MIPI D-PHY 中断处理函数。
*
* 处理链路错误、自动校准完成等事件。
*
* @param irq 中断号。
* @param dev_id 指向 rockchip_mipi_dphy 结构。
*/
static irqreturn_t rockchip_mipi_dphy_irq_handler(int irq, void *dev_id)
{
struct rockchip_mipi_dphy *dphy = dev_id;
u32 int_stat;
// 1. 读取中断状态
int_stat = readl(dphy->base + DPHY_INT_STAT);
// 2. 处理校准完成中断
if (int_stat & (1 << 0)) {
writel(1 << 0, dphy->base + DPHY_INT_STAT);
complete(&dphy->completion);
}
// 3. 处理时钟锁定丢失
if (int_stat & (1 << 1)) {
dev_err(dphy->dev, "D-PHY clock lock lost\n");
writel(1 << 1, dphy->base + DPHY_INT_STAT);
// 可能需要重新校准
}
// 4. 处理数据通道错误
if (int_stat & (1 << 2)) {
dev_err(dphy->dev, "D-PHY data lane error\n");
writel(1 << 2, dphy->base + DPHY_INT_STAT);
}
return IRQ_HANDLED;
}
/**
* @brief PHY 框架操作:初始化。
*
* @param phy 指向 phy 结构。
* @return 0 成功。
*/
static int rockchip_mipi_dphy_init(struct phy *phy)
{
struct rockchip_mipi_dphy *dphy = phy_get_drvdata(phy);
return rockchip_mipi_dphy_hw_init(dphy);
}
/**
* @brief PHY 框架操作:上电。
*
* @param phy 指向 phy 结构。
* @return 0 成功。
*/
static int rockchip_mipi_dphy_power_on(struct phy *phy)
{
struct rockchip_mipi_dphy *dphy = phy_get_drvdata(phy);
return rockchip_mipi_dphy_start(dphy);
}
/**
* @brief PHY 框架操作:断电。
*
* @param phy 指向 phy 结构。
* @return 0 成功。
*/
static int rockchip_mipi_dphy_power_off(struct phy *phy)
{
struct rockchip_mipi_dphy *dphy = phy_get_drvdata(phy);
rockchip_mipi_dphy_stop(dphy);
return 0;
}
/**
* @brief PHY 框架操作:配置。
*
* @param phy 指向 phy 结构。
* @param params 指向 phy_ops_args。
* @return 0 成功。
*/
static int rockchip_mipi_dphy_configure(struct phy *phy,
struct phy_ops_args *params)
{
struct rockchip_mipi_dphy *dphy = phy_get_drvdata(phy);
if (params->speed) {
return rockchip_mipi_dphy_set_speed(dphy, params->speed);
}
return 0;
}
/**
* @brief Platform 探测函数。
*
* 初始化 MIPI D-PHY 硬件,注册 PHY 设备。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int rockchip_mipi_dphy_probe(struct platform_device *pdev)
{
struct rockchip_mipi_dphy *dphy;
struct resource *res;
int ret, irq;
// 1. 分配私有数据结构
dphy = devm_kzalloc(&pdev->dev, sizeof(*dphy), GFP_KERNEL);
if (!dphy)
return -ENOMEM;
platform_set_drvdata(pdev, dphy);
dphy->dev = &pdev->dev;
// 2. 获取 I/O 资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "No memory resource\n");
return -ENXIO;
}
dphy->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(dphy->base))
return PTR_ERR(dphy->base);
// 3. 获取时钟
dphy->pclk = devm_clk_get(&pdev->dev, "pclk");
if (IS_ERR(dphy->pclk))
return PTR_ERR(dphy->pclk);
dphy->ref_clk = devm_clk_get(&pdev->dev, "ref_clk");
if (IS_ERR(dphy->ref_clk))
return PTR_ERR(dphy->ref_clk);
clk_prepare_enable(dphy->pclk);
clk_prepare_enable(dphy->ref_clk);
// 4. 获取中断
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
ret = irq;
goto err_clk;
}
dphy->irq = irq;
// 5. 读取 D-PHY 配置
dphy->lanes = 4; // 假设 4 通道
dphy->max_speed = 1500; // 1.5 Gbps
dphy->current_speed = 1000; // 1 Gbps
spin_lock_init(&dphy->lock);
init_completion(&dphy->completion);
// 6. 注册中断
ret = devm_request_irq(&pdev->dev, dphy->irq, rockchip_mipi_dphy_irq_handler,
IRQF_SHARED, "rockchip-dphy", dphy);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
goto err_clk;
}
// 7. 创建 PHY 设备
dphy->phy = devm_phy_create(&pdev->dev, NULL, &rockchip_mipi_dphy_ops);
if (IS_ERR(dphy->phy)) {
dev_err(&pdev->dev, "Failed to create PHY\n");
ret = PTR_ERR(dphy->phy);
goto err_clk;
}
phy_set_drvdata(dphy->phy, dphy);
// 8. 注册 PHY 设备
ret = devm_phy_register(&pdev->dev, dphy->phy);
if (ret) {
dev_err(&pdev->dev, "Failed to register PHY\n");
goto err_clk;
}
dev_info(&pdev->dev, "Rockchip MIPI D-PHY registered at 0x%llx, IRQ %d\n",
(unsigned long long)res->start, dphy->irq);
return 0;
err_clk:
clk_disable_unprepare(dphy->ref_clk);
clk_disable_unprepare(dphy->pclk);
return ret;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int rockchip_mipi_dphy_remove(struct platform_device *pdev)
{
struct rockchip_mipi_dphy *dphy = platform_get_drvdata(pdev);
// 1. 停止 D-PHY
rockchip_mipi_dphy_stop(dphy);
// 2. 禁用时钟
clk_disable_unprepare(dphy->ref_clk);
clk_disable_unprepare(dphy->pclk);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id rockchip_mipi_dphy_of_match[] = {
{ .compatible = "rockchip,rk3399-mipi-dphy" },
{ .compatible = "rockchip,rk3568-mipi-dphy" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, rockchip_mipi_dphy_of_match);
/* Platform 驱动结构 */
static struct platform_driver rockchip_mipi_dphy_driver = {
.probe = rockchip_mipi_dphy_probe,
.remove = rockchip_mipi_dphy_remove,
.driver = {
.name = "rockchip-mipi-dphy",
.of_match_table = rockchip_mipi_dphy_of_match,
},
};
module_platform_driver(rockchip_mipi_dphy_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Rockchip MIPI D-PHY Controller Driver");
MODULE_LICENSE("GPL v2");
第三章 MIPI D-PHY 调试核心难点
3.1 信号质量问题
现象:DSI/CSI 输出图像出现花屏、条纹、闪烁,降低速度后正常。
原因:
-
信号完整性差(走线过长、阻抗不匹配)。
-
时钟通道抖动过大。
-
D-PHY 驱动强度不足。
调试方法:
-
查看 D-PHY 状态:
devmem2 <base>+0x04 # DPHY_STATUS
-
检查时钟锁定状态:
devmem2 <base>+0x14 # DPHY_CLK_STATUS
-
调整驱动强度:
# 调整驱动强度寄存器 (假设存在) devmem2 <base>+0x40 0x03
3.2 链路建立失败
现象:CSI 或 DSI 设备无法启动,dmesg 显示 "D-PHY calibration failed"。
原因:
-
参考时钟频率不正确。
-
电源电压不足。
-
ESD 保护电路触发。
调试方法:
-
检查参考时钟:
cat /sys/kernel/debug/clk/clk_summary | grep ref_clk
-
增加校准重试次数:
# 在驱动中增加重试逻辑 devmem2 <base>+0x30 0x01 # 重启校准
-
检查电源电压:用万用表测量 MIPI 电源引脚。
3.3 高速模式无法进入
现象:D-PHY 只能工作在低功耗模式,无法切换到高速模式。
原因:
-
PLL 未能锁定。
-
通道数配置错误。
-
速度设置过高。
调试方法:
-
检查 PLL 状态:
devmem2 <base>+0x04 # DPHY_STATUS
-
降低速度:
# 设置速度为 500 Mbps devmem2 <base>+0x10 0x12345678
第四章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| CSI Controller | 通过 D-PHY 接收图像数据 | 信号质量、校准 |
| DSI Controller | 通过 D-PHY 发送显示数据 | 链路稳定、速度匹配 |
| PLL | 提供高速时钟 | 锁定状态、频率稳定 |
| Power Management | 省电模式切换 | 低功耗唤醒 |
| Clock Controller | 提供参考时钟 | 频率精度、抖动 |
第九部分 MDIO 总线控制器 (Rockchip MDIO)
第一章 MDIO 控制器在 Platform Bus 中的位置
MDIO (Management Data Input/Output) 总线是 IEEE 802.3 定义的用于管理 PHY 芯片的串行接口,包含时钟线(MDC)和数据线(MDIO)。在 SoC 中,MDIO 控制器通常作为 Platform 设备挂载在内部总线上,为以太网 MAC 提供访问 PHY 寄存器的通道。
在 Linux 5.10 中,MDIO 控制器驱动位于 drivers/net/ethernet/ 或 drivers/net/mdio/,通过 MDIO 总线框架 暴露管理接口。典型驱动包括 mdio-rockchip.c、mdio-mux.c 等。
1.1 硬件关键概念
-
MDIO 控制器寄存器:控制寄存器、状态寄存器、数据寄存器、时钟分频。
-
管理帧格式:包含 PHY 地址、寄存器地址、读/写位、数据。
-
时钟分频:根据总线速度计算 MDC 时钟分频系数(通常 MDC 为 2.5MHz)。
-
中断:有些控制器支持 MDIO 传输完成中断。
-
PHY 扫描:自动检测总线上的 PHY 地址。
-
共享总线:支持多个 PHY 挂载在同一 MDIO 总线。
1.2 核心代码
// 基于 Linux 5.10 drivers/net/ethernet/stmicro/stmmac/dwmac-mdio.c (精简版)
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <linux/mdio.h>
#include <linux/phy.h>
/**
* @brief Rockchip MDIO 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 MDIO 控制器硬件实例。
*/
struct rockchip_mdio_priv {
void __iomem *base; /**< 映射后的寄存器基址 */
int irq; /**< 中断号 (某些版本支持) */
struct clk *mdio_clk; /**< MDIO 时钟 */
struct clk *pclk; /**< 总线时钟 */
struct device *dev; /**< 设备指针 */
struct mii_bus *bus; /**< MII 总线抽象 */
spinlock_t lock; /**< 硬件保护锁 */
u32 bus_freq; /**< MDC 时钟频率 (Hz) */
u32 phy_mask; /**< PHY 扫描掩码 */
struct completion complete; /**< 传输完成同步 */
u32 last_data; /**< 上一次读写数据 */
};
/* 寄存器偏移量 (Rockchip MDIO) */
#define MDIO_CTRL 0x00
#define MDIO_STATUS 0x04
#define MDIO_DATA 0x08
#define MDIO_INT_EN 0x0C
#define MDIO_INT_STAT 0x10
#define MDIO_CLK_DIV 0x14
#define MDIO_DEV_ADDR 0x18
#define MDIO_OP_CODE 0x1C
/* 控制寄存器位定义 */
#define MDIO_CTRL_ENABLE (1 << 0) /**< MDIO 使能 */
#define MDIO_CTRL_READ (1 << 1) /**< 读操作 */
#define MDIO_CTRL_WRITE (0 << 1) /**< 写操作 */
#define MDIO_CTRL_START (1 << 2) /**< 启动传输 */
#define MDIO_CTRL_BUSY (1 << 3) /**< MDIO 忙 */
/**
* @brief 初始化 MDIO 硬件。
*
* @param mdio 指向 rockchip_mdio_priv 结构。
* @return 0 成功。
*/
static int rockchip_mdio_hw_init(struct rockchip_mdio_priv *mdio)
{
u32 ctrl;
// 1. 启用 MDIO 控制器
ctrl = MDIO_CTRL_ENABLE;
writel(ctrl, mdio->base + MDIO_CTRL);
// 2. 设置时钟分频 (使 MDC 达到 2.5MHz)
// 假设输入时钟为 50MHz,则需要分频 20
u32 div = clk_get_rate(mdio->mdio_clk) / (mdio->bus_freq * 2);
if (div < 1) div = 1;
if (div > 0xFF) div = 0xFF;
writel(div, mdio->base + MDIO_CLK_DIV);
// 3. 检查状态
if (!(readl(mdio->base + MDIO_STATUS) & 0x1)) {
dev_err(mdio->dev, "MDIO controller not ready\n");
return -EIO;
}
return 0;
}
/**
* @brief 执行 MDIO 读操作。
*
* @param mdio 指向 rockchip_mdio_priv 结构。
* @param phy_id PHY 地址 (0-31)。
* @param reg 寄存器地址 (0-31)。
* @return 16 位数据,-1 错误。
*/
static int rockchip_mdio_read(struct rockchip_mdio_priv *mdio,
int phy_id, int reg)
{
u32 ctrl;
int ret;
// 1. 等待 MDIO 空闲
while (readl(mdio->base + MDIO_STATUS) & MDIO_CTRL_BUSY)
;
// 2. 设置 PHY 地址和寄存器地址
writel((phy_id << 5) | reg, mdio->base + MDIO_DEV_ADDR);
// 3. 启动读操作
ctrl = MDIO_CTRL_ENABLE | MDIO_CTRL_READ | MDIO_CTRL_START;
writel(ctrl, mdio->base + MDIO_CTRL);
// 4. 等待传输完成
if (!wait_for_completion_timeout(&mdio->complete, 100 * HZ)) {
dev_err(mdio->dev, "MDIO read timeout\n");
return -ETIMEDOUT;
}
// 5. 读取数据
ret = readl(mdio->base + MDIO_DATA) & 0xFFFF;
return ret;
}
/**
* @brief 执行 MDIO 写操作。
*
* @param mdio 指向 rockchip_mdio_priv 结构。
* @param phy_id PHY 地址。
* @param reg 寄存器地址。
* @param data 16 位数据。
* @return 0 成功。
*/
static int rockchip_mdio_write(struct rockchip_mdio_priv *mdio,
int phy_id, int reg, u16 data)
{
u32 ctrl;
// 1. 等待 MDIO 空闲
while (readl(mdio->base + MDIO_STATUS) & MDIO_CTRL_BUSY)
;
// 2. 设置 PHY 地址和寄存器地址
writel((phy_id << 5) | reg, mdio->base + MDIO_DEV_ADDR);
// 3. 写入数据
writel(data, mdio->base + MDIO_DATA);
// 4. 启动写操作
ctrl = MDIO_CTRL_ENABLE | MDIO_CTRL_WRITE | MDIO_CTRL_START;
writel(ctrl, mdio->base + MDIO_CTRL);
// 5. 等待传输完成
if (!wait_for_completion_timeout(&mdio->complete, 100 * HZ)) {
dev_err(mdio->dev, "MDIO write timeout\n");
return -ETIMEDOUT;
}
return 0;
}
/**
* @brief MII 总线读取回调。
*
* @param bus 指向 mii_bus。
* @param phy_id PHY 地址。
* @param reg 寄存器地址。
* @return 16 位数据。
*/
static int rockchip_mdio_bus_read(struct mii_bus *bus, int phy_id, int reg)
{
struct rockchip_mdio_priv *mdio = bus->priv;
return rockchip_mdio_read(mdio, phy_id, reg);
}
/**
* @brief MII 总线写入回调。
*
* @param bus 指向 mii_bus。
* @param phy_id PHY 地址。
* @param reg 寄存器地址。
* @param data 16 位数据。
* @return 0 成功。
*/
static int rockchip_mdio_bus_write(struct mii_bus *bus, int phy_id,
int reg, u16 data)
{
struct rockchip_mdio_priv *mdio = bus->priv;
return rockchip_mdio_write(mdio, phy_id, reg, data);
}
/**
* @brief MDIO 中断处理函数。
*
* 处理传输完成中断。
*
* @param irq 中断号。
* @param dev_id 指向 rockchip_mdio_priv 结构。
*/
static irqreturn_t rockchip_mdio_irq_handler(int irq, void *dev_id)
{
struct rockchip_mdio_priv *mdio = dev_id;
u32 int_stat;
// 1. 读取中断状态
int_stat = readl(mdio->base + MDIO_INT_STAT);
// 2. 处理传输完成中断
if (int_stat & (1 << 0)) {
writel(1 << 0, mdio->base + MDIO_INT_STAT);
complete(&mdio->complete);
}
// 3. 处理错误中断
if (int_stat & (1 << 1)) {
dev_err(mdio->dev, "MDIO error\n");
writel(1 << 1, mdio->base + MDIO_INT_STAT);
}
return IRQ_HANDLED;
}
/**
* @brief 扫描并注册所有 PHY 设备。
*
* @param mdio 指向 rockchip_mdio_priv 结构。
* @param bus 指向 mii_bus。
*/
static void rockchip_mdio_scan_phys(struct rockchip_mdio_priv *mdio,
struct mii_bus *bus)
{
int i;
for (i = 0; i < PHY_MAX_ADDR; i++) {
// 跳过掩码指定的地址
if (mdio->phy_mask & (1 << i))
continue;
// 尝试读取 PHY ID
if (mdiobus_get_phy(bus, i)) {
dev_info(mdio->dev, "PHY found at address %d\n", i);
}
}
}
/**
* @brief Platform 探测函数。
*
* 初始化 MDIO 控制器硬件,注册 MII 总线。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int rockchip_mdio_probe(struct platform_device *pdev)
{
struct rockchip_mdio_priv *mdio;
struct mii_bus *bus;
struct resource *res;
int ret, irq;
// 1. 分配私有数据结构
mdio = devm_kzalloc(&pdev->dev, sizeof(*mdio), GFP_KERNEL);
if (!mdio)
return -ENOMEM;
platform_set_drvdata(pdev, mdio);
mdio->dev = &pdev->dev;
// 2. 获取 I/O 资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "No memory resource\n");
return -ENXIO;
}
mdio->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(mdio->base))
return PTR_ERR(mdio->base);
// 3. 获取时钟
mdio->mdio_clk = devm_clk_get(&pdev->dev, "mdio_clk");
if (IS_ERR(mdio->mdio_clk))
return PTR_ERR(mdio->mdio_clk);
mdio->pclk = devm_clk_get(&pdev->dev, "pclk");
if (IS_ERR(mdio->pclk))
return PTR_ERR(mdio->pclk);
clk_prepare_enable(mdio->mdio_clk);
clk_prepare_enable(mdio->pclk);
// 4. 获取中断 (可选)
irq = platform_get_irq(pdev, 0);
if (irq > 0) {
mdio->irq = irq;
ret = devm_request_irq(&pdev->dev, mdio->irq,
rockchip_mdio_irq_handler,
IRQF_SHARED, "rockchip-mdio", mdio);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
goto err_clk;
}
} else {
// 没有中断,使用轮询模式
mdio->irq = -1;
}
// 5. 硬件初始化
mdio->bus_freq = 2500000; // 2.5 MHz
mdio->phy_mask = 0; // 扫描所有地址
spin_lock_init(&mdio->lock);
init_completion(&mdio->complete);
ret = rockchip_mdio_hw_init(mdio);
if (ret) {
dev_err(&pdev->dev, "Failed to init hardware\n");
goto err_clk;
}
// 6. 分配并注册 MII 总线
bus = devm_mdiobus_alloc(&pdev->dev);
if (!bus) {
ret = -ENOMEM;
goto err_clk;
}
mdio->bus = bus;
bus->name = "rockchip-mdio";
bus->read = rockchip_mdio_bus_read;
bus->write = rockchip_mdio_bus_write;
bus->priv = mdio;
snprintf(bus->id, MII_BUS_ID_SIZE, "rockchip-mdio-%d", pdev->id);
ret = devm_mdiobus_register(&pdev->dev, bus);
if (ret) {
dev_err(&pdev->dev, "Failed to register MDIO bus\n");
goto err_clk;
}
// 7. 扫描 PHY
rockchip_mdio_scan_phys(mdio, bus);
dev_info(&pdev->dev, "Rockchip MDIO controller registered at 0x%llx, IRQ %d\n",
(unsigned long long)res->start, mdio->irq);
return 0;
err_clk:
clk_disable_unprepare(mdio->pclk);
clk_disable_unprepare(mdio->mdio_clk);
return ret;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int rockchip_mdio_remove(struct platform_device *pdev)
{
struct rockchip_mdio_priv *mdio = platform_get_drvdata(pdev);
// 1. 禁用 MDIO 控制器
writel(0, mdio->base + MDIO_CTRL);
// 2. 禁用时钟
clk_disable_unprepare(mdio->pclk);
clk_disable_unprepare(mdio->mdio_clk);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id rockchip_mdio_of_match[] = {
{ .compatible = "rockchip,rk3399-mdio" },
{ .compatible = "rockchip,rk3568-mdio" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, rockchip_mdio_of_match);
/* Platform 驱动结构 */
static struct platform_driver rockchip_mdio_driver = {
.probe = rockchip_mdio_probe,
.remove = rockchip_mdio_remove,
.driver = {
.name = "rockchip-mdio",
.of_match_table = rockchip_mdio_of_match,
},
};
module_platform_driver(rockchip_mdio_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Rockchip MDIO Controller Driver");
MODULE_LICENSE("GPL v2");
第三章 MDIO 调试核心难点
3.1 MDIO 总线无法访问 PHY
现象:mii-tool eth0 显示 No PHY found 或 PHY ID reads failed。
原因:
-
MDIO 时钟频率过高(应 ≤ 2.5MHz)。
-
PHY 地址错误(DTS 中的
phy-address与实际不符)。 -
MDIO 引脚复用配置错误。
调试方法:
-
检查 MDIO 时钟:
devmem2 <base>+0x14 # MDIO_CLK_DIV
-
手动读取 PHY ID:
# 使用 mdio-tool 或 devmem2 直接操作 devmem2 <base>+0x08 0x00 # 尝试读 PHY 地址 0
-
检查 PHY 地址:
ethtool -s eth0 phyaddr <addr>
3.2 MDIO 传输超时
现象:dmesg 出现 "MDIO write timeout" 或 "MDIO read timeout"。
原因:
-
中断未配置(使用轮询模式时超时值过小)。
-
MDIO 总线被拉死(PHY 芯片故障或线路短路)。
-
时钟频率过低。
调试方法:
-
增加超时值:
# 在驱动中增加 timeout = 500 * HZ
-
检查 MDIO 状态:
devmem2 <base>+0x04 # MDIO_STATUS
-
禁用中断使用轮询:
# 在 driver 中设置 polling = true
3.3 PHY 扫描失败
现象:ethtool -s eth0 phyaddr 0 后 PHY 仍然无法工作。
原因:
-
PHY 扫描掩码 (
phy_mask) 配置错误,导致有效 PHY 被跳过。 -
PHY 芯片要求特定上电序列。
调试方法:
-
检查
phy_mask:devmem2 <base>+0x20 # 读取掩码寄存器
-
全扫描:
# 在驱动中设置 phy_mask = 0
第四章 结合性能调试场景示例
场景:系统启动后,PHY 无法链接,ethtool -s eth0 设置后仍无法工作,MAC 与 PHY 通信正常,但 PHY 状态寄存器不变。
分析流程:
-
宏观层面(图谱的 MAC 层):
-
perf top显示stmmac_mdio_read频繁调用且返回错误。 -
dmesg显示 "MDIO read failed"。
-
-
MDIO 层(Device Drivers -> MDIO 层):
devmem2 <base>+0x04 # MDIO_STATUS
发现状态寄存器
BUSY位一直为 1。 -
硬件检查(PHY 层):
-
用示波器测量 MDC 和 MDIO 信号,发现 MDC 频率为 50MHz(过高)。
-
MDIO 数据线受干扰。
-
-
根本原因:
-
MDIO 时钟分频计算错误,导致 MDC 频率高达 50MHz(规范要求 ≤ 2.5MHz)。
-
PHY 芯片无法识别过高的时钟频率。
-
-
解决方案:
-
调整
clk_div使 MDC = 2.5MHz。 -
增加滤波电容。
-
优化后,MDIO 通信正常,PHY 链接建立。
-
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| Ethernet MAC | 通过 MDIO 管理 PHY | MDC 频率、PHY 地址 |
| GPIO | 控制 PHY 复位引脚 | 复位时序、电平配置 |
| Clock Controller | 提供 MDIO 时钟 | 频率计算、分频设置 |
| PHY | 被管理的物理层芯片 | 寄存器读写、链路状态 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)