Linux Platform 总线设备驱动模型之 MMC/SDIO Watchdog RTC PWM Thermal Ethernet MAC DWC3 USB PCIe NAND
第一部分 MMC/SDIO 控制器
第一章 MMC/SDIO 控制器在 Platform Bus 中的位置
MMC/SDIO 控制器通常作为一个 Platform 设备存在,用于连接 eMMC 芯片、SD 卡或 SDIO 设备(如 WiFi 模块)。它通过 SD 总线 进行通信,支持 1-bit、4-bit 或 8-bit 模式,数据传输速率可达数百 MB/s。
Linux 中 MMC 子系统分为三层:
-
MMC 核心层 (
drivers/mmc/core/):管理 MMC/SD 卡状态、分区、块 I/O 请求。 -
MMC 控制器驱动(即本篇文章重点):具体的 SoC 主机驱动(如
mmc-dwcmshc.c)。 -
MMC 块设备层:将 MMC 虚拟为
/dev/mmcblk0等块设备。
第二章 Linux 5.10 典型 MMC 控制器驱动 —— mmc-dwcmshc.c
DWC MMC(DesignWare Mobile Storage Host Controller)是目前广泛使用的 eMMC/SD 控制器 IP,用于 Rockchip、Allwinner、Intel 等平台。以下代码基于 Linux 5.10 drivers/mmc/host/mmc-dwcmshc.c,展示核心逻辑。
2.1 硬件关键概念
-
SDHCI 兼容寄存器:类似标准 SDHCI 寄存器布局(但有一些扩展)。
-
DMA 引擎:通过 ADMA2 或 SDMA 搬运数据。
-
中断:传输完成、命令完成、错误(CRC 错误、超时、启动错误)。
-
时钟:内部主时钟(
clk_xin)与输出时钟(clk_sd)分离。
2.2 核心代码
// 基于 Linux 5.10 drivers/mmc/host/mmc-dwcmshc.c(简化版)
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/mmc/host.h>
#include <linux/mmc/sd.h>
#include <linux/platform_device.h>
#include <linux/dma-mapping.h>
/**
* @brief DWC MMC 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 MMC/SDIO Controller 硬件实例。
*/
struct dwcmshc_priv {
void __iomem *base; /**< 映射后的寄存器基址 */
int irq; /**< MMC 中断号 */
struct clk *clk_xin; /**< 主输入时钟 */
struct clk *clk_sd; /**< 输出时钟(用于卡) */
struct mmc_host *mmc; /**< MMC 主机抽象 */
struct mmc_request *mrq; /**< 当前请求 */
struct mmc_command *cmd; /**< 当前命令 */
struct mmc_data *data; /**< 当前数据段 */
spinlock_t lock; /**< 保护硬件的锁 */
dma_addr_t adma_table_phys; /**< ADMA 描述符表物理地址 */
u32 *adma_table_virt; /**< ADMA 描述符表虚拟地址 */
unsigned int adma_desc_num; /**< ADMA 描述符数量 */
};
/* 寄存器偏移量(基于 SDHCI 标准 + DWC 扩展) */
#define DWC_MSHC_CTRL 0x00
#define DWC_MSHC_PWREN 0x04
#define DWC_MSHC_CLKSRC 0x08
#define DWC_MSHC_TMOUT 0x0C
#define DWC_MSHC_CMD 0x10
#define DWC_MSHC_ARG 0x14
#define DWC_MSHC_TRANS 0x18
#define DWC_MSHC_DATA 0x1C
#define DWC_MSHC_INT_STAT 0x20
#define DWC_MSHC_INT_EN 0x24
#define DWC_MSHC_CTRL2 0x28
#define DWC_MSHC_CAP 0x2C
#define DWC_MSHC_BLKCNT 0x30
#define DWC_MSHC_ADMA_CTRL 0x34
#define DWC_MSHC_ADMA_LOW 0x38
#define DWC_MSHC_ADMA_HIGH 0x3C
/**
* @brief 向 MMC 控制器发送命令。
*
* @param host 指向 dwcmshc_priv 结构。
* @param cmd 指向 mmc_command。
*/
static void dwcmshc_send_command(struct dwcmshc_priv *priv,
struct mmc_command *cmd)
{
u32 ctrl;
u32 cmd_idx = cmd->opcode;
u32 arg = cmd->arg;
// 1. 写入命令参数
writel(arg, priv->base + DWC_MSHC_ARG);
// 2. 配置命令寄存器
ctrl = (cmd_idx & 0x3F);
ctrl |= (1 << 8); // 启用命令(CMD_EN)
if (cmd->flags & MMC_RSP_PRESENT) {
ctrl |= (1 << 4); // 期望响应
if (cmd->flags & MMC_RSP_136)
ctrl |= (1 << 5); // 长响应
}
if (cmd->flags & MMC_RSP_CRC)
ctrl |= (1 << 6); // 启用 CRC 检查
if (cmd->flags & MMC_CMD_ADTC) {
// 带数据的命令(如 CMD17 读取多个块)
ctrl |= (1 << 7); // 数据传输
// 根据数据方向设置传输方向
if (cmd->data->flags & MMC_DATA_READ)
ctrl |= (1 << 2); // 读(控制位在 TRNS 寄存器中)
}
// 3. 写入命令寄存器以启动传输
writel(ctrl, priv->base + DWC_MSHC_CMD);
}
/**
* @brief 初始化 ADMA 描述符表。
*
* @param priv 指向 dwcmshc_priv 结构。
*/
static int dwcmshc_adma_init(struct dwcmshc_priv *priv)
{
int ret;
// 1. 分配 ADMA 描述符表(对齐到 64 字节边界)
priv->adma_table_virt = dma_alloc_coherent(priv->mmc->parent,
sizeof(u32) * 512,
&priv->adma_table_phys,
GFP_KERNEL);
if (!priv->adma_table_virt) {
dev_err(priv->mmc->parent, "Failed to allocate ADMA table\n");
return -ENOMEM;
}
priv->adma_desc_num = 512;
// 2. 将 ADMA 表物理地址写入硬件
writel(lower_32_bits(priv->adma_table_phys),
priv->base + DWC_MSHC_ADMA_LOW);
writel(upper_32_bits(priv->adma_table_phys),
priv->base + DWC_MSHC_ADMA_HIGH);
// 3. 启用 ADMA2 模式
writel(0x02, priv->base + DWC_MSHC_ADMA_CTRL);
return 0;
}
/**
* @brief 设置数据传输的 ADMA 描述符(链式描述符)。
*
* @param priv 指向 dwcmshc_priv 结构。
* @param data 指向 mmc_data。
*/
static void dwcmshc_setup_adma_desc(struct dwcmshc_priv *priv,
struct mmc_data *data)
{
u32 *desc = priv->adma_table_virt;
dma_addr_t dma_addr;
u32 len;
int sg_count, i;
if (!data->sg_len) return;
sg_count = data->sg_len;
// 遍历 scatterlist,填充 ADMA 描述符
for (i = 0; i < sg_count; i++) {
dma_addr = sg_dma_address(&data->sg[i]);
len = sg_dma_len(&data->sg[i]);
// ADMA 描述符结构:32位地址 + 32位控制
// 每个描述符 8 字节
desc[i * 2] = dma_addr;
desc[i * 2 + 1] = (len & 0xFFFF) | (0x02 << 16) | (0x01 << 24);
// 02: 设置不指定数据方向,01: 有效标志
}
// 最后一个描述符设置 EOS(描述符结束)
desc[(sg_count - 1) * 2 + 1] |= (1 << 26); // END 位
// 确保描述符写入可见
wmb(); // 写内存屏障
}
/**
* @brief 处理数据传输(使用 ADMA)。
*
* @param priv 指向 dwcmshc_priv 结构。
* @param data 指向 mmc_data。
*/
static void dwcmshc_data_transfer(struct dwcmshc_priv *priv,
struct mmc_data *data)
{
u32 ctrl;
// 1. 设置 ADMA 描述符
dwcmshc_setup_adma_desc(priv, data);
// 2. 设置传输控制寄存器
ctrl = 0;
if (data->flags & MMC_DATA_READ)
ctrl |= (1 << 4); // 读方向
else
ctrl &= ~(1 << 4); // 写方向
ctrl |= (data->blocks << 16); // 块数量
ctrl |= (1 << 8); // 块大小(默认为 512 字节)
ctrl |= (1 << 2); // 启用 ADMA
writel(ctrl, priv->base + DWC_MSHC_TRANS);
}
/**
* @brief MMC 请求处理函数(由 MMC 核心层调用)。
*
* @param host 指向 mmc_host。
* @param mrq 指向 mmc_request。
*/
static void dwcmshc_request(struct mmc_host *mmc, struct mmc_request *mrq)
{
struct dwcmshc_priv *priv = mmc_priv(mmc);
struct mmc_command *cmd = mrq->cmd;
struct mmc_data *data = mrq->data;
unsigned long flags;
spin_lock_irqsave(&priv->lock, flags);
priv->mrq = mrq;
priv->cmd = cmd;
priv->data = data;
// 1. 如果是带数据的命令,先设置数据
if (data) {
dwcmshc_data_transfer(priv, data);
}
// 2. 发送命令
dwcmshc_send_command(priv, cmd);
// 3. 启用中断
writel(0xFFFFFFFF, priv->base + DWC_MSHC_INT_EN);
spin_unlock_irqrestore(&priv->lock, flags);
}
/**
* @brief MMC 中断处理函数。
*
* 处理命令完成、传输完成、错误等事件。
*
* @param irq 中断号。
* @param dev_id 指向 dwcmshc_priv 结构。
*/
static irqreturn_t dwcmshc_irq_handler(int irq, void *dev_id)
{
struct dwcmshc_priv *priv = dev_id;
u32 int_stat, int_en;
int handled = 0;
// 1. 读取中断状态
int_stat = readl(priv->base + DWC_MSHC_INT_STAT);
int_en = readl(priv->base + DWC_MSHC_INT_EN);
int_stat &= int_en; // 仅处理已使能的中断
// 2. 清除中断标志(写 1 清除)
writel(int_stat, priv->base + DWC_MSHC_INT_STAT);
// 3. 命令完成中断
if (int_stat & (1 << 0)) { // 命令完成
if (priv->cmd) {
priv->cmd->resp[0] = readl(priv->base + DWC_MSHC_DATA);
if (priv->cmd->flags & MMC_RSP_136) {
// 读取长响应
priv->cmd->resp[1] = readl(priv->base + DWC_MSHC_DATA + 4);
priv->cmd->resp[2] = readl(priv->base + DWC_MSHC_DATA + 8);
priv->cmd->resp[3] = readl(priv->base + DWC_MSHC_DATA + 12);
}
priv->cmd = NULL;
}
handled = 1;
}
// 4. 数据传输完成中断
if (int_stat & (1 << 1)) { // 传输完成
if (priv->data) {
// 检查 CRC 错误
if (int_stat & (1 << 5)) {
priv->data->error = -EILSEQ; // CRC 错误
} else if (int_stat & (1 << 6)) {
priv->data->error = -ETIMEDOUT; // 超时
}
priv->data = NULL;
}
handled = 1;
}
// 5. 错误中断(CRC、超时、启动错误等)
if (int_stat & (1 << 7)) { // 启动错误
dev_err(priv->mmc->parent, "MMC start error\n");
handled = 1;
}
// 6. 完成整个请求
if (priv->mrq && (int_stat & ((1 << 0) | (1 << 1)))) {
mmc_request_done(priv->mmc, priv->mrq);
priv->mrq = NULL;
}
return handled ? IRQ_HANDLED : IRQ_NONE;
}
/**
* @brief 设置 MMC 时钟。
*
* @param host 指向 mmc_host。
* @param rate 目标时钟频率。
*/
static int dwcmshc_set_ios(struct mmc_host *mmc, struct mmc_ios *ios)
{
struct dwcmshc_priv *priv = mmc_priv(mmc);
unsigned long clk_rate;
u32 ctrl, div;
// 1. 设置时钟
if (ios->clock) {
clk_rate = clk_get_rate(priv->clk_xin);
div = clk_rate / ios->clock;
if (div < 1) div = 1;
if (div > 0xFF) div = 0xFF;
ctrl = readl(priv->base + DWC_MSHC_CTRL);
ctrl &= ~(0xFF << 8);
ctrl |= (div << 8);
writel(ctrl, priv->base + DWC_MSHC_CTRL);
// 启用时钟
ctrl |= (1 << 0);
writel(ctrl, priv->base + DWC_MSHC_CTRL);
} else {
// 禁用时钟
ctrl = readl(priv->base + DWC_MSHC_CTRL);
ctrl &= ~(1 << 0);
writel(ctrl, priv->base + DWC_MSHC_CTRL);
}
// 2. 设置总线宽度
ctrl = readl(priv->base + DWC_MSHC_CTRL2);
ctrl &= ~(0x03 << 4);
switch (ios->bus_width) {
case MMC_BUS_WIDTH_1:
ctrl |= (0x00 << 4);
break;
case MMC_BUS_WIDTH_4:
ctrl |= (0x01 << 4);
break;
case MMC_BUS_WIDTH_8:
ctrl |= (0x02 << 4);
break;
}
writel(ctrl, priv->base + DWC_MSHC_CTRL2);
return 0;
}
/**
* @brief Platform 探测函数。
*
* 初始化 MMC 控制器硬件,注册到 MMC 核心。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int dwcmshc_probe(struct platform_device *pdev)
{
struct dwcmshc_priv *priv;
struct mmc_host *mmc;
struct resource *res;
int ret, irq;
// 1. 分配 MMC 主机
mmc = mmc_alloc_host(sizeof(struct dwcmshc_priv), &pdev->dev);
if (!mmc)
return -ENOMEM;
platform_set_drvdata(pdev, mmc);
priv = mmc_priv(mmc);
priv->mmc = mmc;
// 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. 获取中断
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
priv->irq = irq;
// 4. 获取时钟
priv->clk_xin = devm_clk_get(&pdev->dev, "xin");
if (IS_ERR(priv->clk_xin))
return PTR_ERR(priv->clk_xin);
priv->clk_sd = devm_clk_get(&pdev->dev, "sd");
if (IS_ERR(priv->clk_sd))
return PTR_ERR(priv->clk_sd);
clk_prepare_enable(priv->clk_xin);
clk_prepare_enable(priv->clk_sd);
spin_lock_init(&priv->lock);
// 5. 初始化 MMC 主机能力
mmc->ops = &dwcmshc_ops;
mmc->f_max = clk_get_rate(priv->clk_sd) / 2;
mmc->f_min = mmc->f_max / 256;
mmc->caps = MMC_CAP_4_BIT_DATA | MMC_CAP_8_BIT_DATA;
mmc->caps |= MMC_CAP_ERASE | MMC_CAP_CMD23;
mmc->max_seg_size = PAGE_SIZE * 64;
mmc->max_req_size = PAGE_SIZE * 64;
mmc->max_blk_count = 65535;
mmc->max_blk_size = 512;
// 6. 初始化 ADMA
ret = dwcmshc_adma_init(priv);
if (ret) {
dev_err(&pdev->dev, "ADMA init failed\n");
return ret;
}
// 7. 注册中断
ret = devm_request_irq(&pdev->dev, priv->irq, dwcmshc_irq_handler,
IRQF_SHARED, "dwcmshc", priv);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
return ret;
}
// 8. 注册 MMC 主机
ret = mmc_add_host(mmc);
if (ret) {
dev_err(&pdev->dev, "Failed to add MMC host\n");
return ret;
}
dev_info(&pdev->dev, "DWC MMC controller registered at 0x%llx, IRQ %d\n",
(unsigned long long)res->start, priv->irq);
return 0;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int dwcmshc_remove(struct platform_device *pdev)
{
struct mmc_host *mmc = platform_get_drvdata(pdev);
struct dwcmshc_priv *priv = mmc_priv(mmc);
// 1. 移除 MMC 主机
mmc_remove_host(mmc);
// 2. 禁用时钟
clk_disable_unprepare(priv->clk_xin);
clk_disable_unprepare(priv->clk_sd);
// 3. 释放 ADMA 表
if (priv->adma_table_virt) {
dma_free_coherent(mmc->parent, sizeof(u32) * 512,
priv->adma_table_virt, priv->adma_table_phys);
}
// 4. 释放 MMC 主机
mmc_free_host(mmc);
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("DWC Mobile Storage Host Controller Driver");
MODULE_LICENSE("GPL v2");
第三章 MMC/SDIO 调试核心难点
3.1 卡检测与初始化失败
现象:SD 卡插入后,内核没有任何反应,/dev/mmcblk0 不出现。
原因:
-
卡检测 GPIO 配置错误。
-
卡未上电(
vmmc电源未启用)。 -
时钟初始化失败。
调试方法:
-
检查卡检测 GPIO:
cat /sys/kernel/debug/gpio | grep cd
-
查看电源控制:
cat /sys/kernel/debug/regulator/regulator_summary | grep mmc
确认
vmmc供电正常。 -
强制探测:
echo 1 > /sys/devices/platform/soc/*dwcmshc/mmc_host/mmc0/scan
3.2 数据传输 CRC 错误
现象:读写文件时出现 -EILSEQ 错误,或文件系统挂载后频繁错误。
原因:
-
信号质量差(走线过长、干扰)。
-
不稳定的时钟。
-
卡损坏。
调试方法:
-
降低总线速度:在 DTS 中设置
max-frequency = <10000000>;(10MHz)。 -
测试不同位宽:强制 1-bit 模式:
echo 1 > /sys/devices/platform/soc/*dwcmshc/mmc_host/mmc0/mmc0:0001/bus_width
-
查看 CRC 统计:
cat /sys/kernel/debug/mmc0/iotrace
3.3 时钟相位问题(Tuning 失败)
现象:高速模式(HS200/HS400)下无法启动,或者数据错误。
原因:时钟相位(Phase shift)设置不当,导致数据采样窗口偏移。
调试方法:
-
强制 Tuning:
echo 1 > /sys/devices/platform/soc/*dwcmshc/mmc_host/mmc0/execute_tuning
-
使用 debugfs 查看 Tuning 结果:
cat /sys/kernel/debug/mmc0/tuning
-
查看控制器 Tuning 状态寄存器:使用
devmem2读取DWC_MSHC_TUNING寄存器。
3.4 并发访问(多线程)导致死锁
现象:当多个线程同时访问 MMC 时,系统卡死,dmesg 显示 Timeout waiting for hardware。
原因:中断处理程序中未能正确释放锁,或 DMA 完成未通知。
调试方法:
-
使用
lockdep:echo 1 > /proc/sys/kernel/lockdep/on
-
跟踪请求队列:
perf record -e mmc:* -a -- timeout 5 perf script | grep -E "request|done"
第四章 结合性能调试场景示例
场景:嵌入式设备在写入大量文件时卡顿,iostat -x 1 显示 MMC 设备繁忙,但吞吐量远低于理论值。
分析流程:
-
宏观层面(CPU 和 I/O 层):
-
top看到kworker/mmc占用较高。 -
iostat -x 1显示mmcblk0的svctm(服务时间)很长。
-
-
查看 MMC 状态(设备驱动层):
cat /sys/kernel/debug/mmc0/ios
发现当前使用
4-bit模式,但频率仅 25MHz(低于 eMMC 支持的 200MHz)。 -
追踪 MMC 请求(MMC 核心层):
trace-cmd record -e mmc:mmc_request -e mmc:mmc_request_done trace-cmd report | grep "timeout"
发现大量请求在
mmc_wait_for_req中超时。 -
根本原因:eMMC 切换到 HS200 模式失败,降级到标准 SD 模式,但没有正确配置 Tuning。
-
解决方案:
-
在 DTS 中设置
cap-mmc-hw-reset和mmc-hs200-1_8v。 -
手动触发 Tuning:
echo 1 > /sys/devices/platform/soc/*dwcmshc/mmc_host/mmc0/execute_tuning -
验证后,读写速度提升 10 倍。
-
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| DMA | 数据传输通过 ADMA2 搬运 | ADMA 描述符对齐、地址映射 |
| PINCTRL | 用于总线宽度(1/4/8 位)切换 | 引脚功能正确,无竞争 |
| PMIC | 提供电源(vmmc、vqmmc) | 电压稳定,上电序列 |
| Clock | 提供高速时钟源 | 频率匹配,相位调整 |
第二部分 Watchdog 控制器
第一章 Watchdog 控制器在 Platform Bus 中的位置
Watchdog 控制器虽然不起眼,但在工业嵌入式系统中至关重要。它独立于 CPU 运行,负责检测系统是否“死锁”,并在超时未收到“喂狗”(keep-alive)信号时,自动触发硬件复位,使系统恢复。
它通常挂载在 Platform Bus 上,驱动程序非常简单:核心是在 probe 阶段初始化定时器,并在用户空间(如 systemd 的 watchdog 服务)定时写入 /dev/watchdog 节点来重置计数器。
Linux 中 Watchdog 子系统很简单:
-
Watchdog 核心层 (
drivers/watchdog/watchdog_dev.c):管理字符设备/dev/watchdog。 -
Watchdog 控制器驱动(即本篇文章重点):实现具体的硬件 watchdog(如
dw_wdt.c)。
第二章 Linux 5.10 典型 Watchdog 控制器驱动 —— dw_wdt.c
DesignWare Watchdog 是 ARM 平台最常见的 watchdog IP 核之一,广泛用于 Rockchip、Allwinner、Intel 等 SoC。
2.1 硬件关键寄存器
-
WDOG_CONTROL_REG_OFFSET:控制寄存器(使能、复位模式)。 -
WDOG_TIMEOUT_REG_OFFSET:超时值寄存器(计数器初值)。 -
WDOG_CURRENT_REG_OFFSET:当前计数器值(向下计数)。 -
WDOG_COUNTER_RESTART_REG_OFFSET:重置计数器(喂狗)寄存器。
2.2 核心代码
// 基于 Linux 5.10 drivers/watchdog/dw_wdt.c(精简版)
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/watchdog.h>
#include <linux/platform_device.h>
#include <linux/reset.h>
/**
* @brief DesignWare Watchdog 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 Watchdog Controller 硬件实例。
*/
struct dw_wdt {
void __iomem *base; /**< 映射后的寄存器基址 */
int irq; /**< Watchdog 中断号(如超时前产生中断) */
struct clk *clk; /**< 输入时钟 */
struct watchdog_device wdd; /**< 核心 watchdog 抽象 */
bool nowayout; /**< 一旦启动,不可撤销 */
spinlock_t lock; /**< 保护硬件操作的锁 */
struct reset_control *rst; /**< 复位控制器(用于实际硬件复位) */
};
/* 寄存器偏移量 */
#define WDOG_CONTROL_REG_OFFSET 0x00
#define WDOG_TIMEOUT_REG_OFFSET 0x04
#define WDOG_CURRENT_REG_OFFSET 0x08
#define WDOG_COUNTER_RESTART_REG_OFFSET 0x0C
#define WDOG_ISR_OFFSET 0x10
#define WDOG_IMR_OFFSET 0x14
/**
* @brief 使能 Watchdog 硬件。
*
* @param dw_wdt 指向 dw_wdt 结构。
*/
static void dw_wdt_enable(struct dw_wdt *dw_wdt)
{
u32 val;
unsigned long flags;
spin_lock_irqsave(&dw_wdt->lock, flags);
val = readl(dw_wdt->base + WDOG_CONTROL_REG_OFFSET);
val |= (1 << 0); // WDT_EN 位:启用看门狗
val |= (1 << 2); // WDT_RMOD 位:超时触发复位
writel(val, dw_wdt->base + WDOG_CONTROL_REG_OFFSET);
spin_unlock_irqrestore(&dw_wdt->lock, flags);
}
/**
* @brief 禁用 Watchdog 硬件。
*
* @param dw_wdt 指向 dw_wdt 结构。
*/
static void dw_wdt_disable(struct dw_wdt *dw_wdt)
{
u32 val;
unsigned long flags;
spin_lock_irqsave(&dw_wdt->lock, flags);
val = readl(dw_wdt->base + WDOG_CONTROL_REG_OFFSET);
val &= ~(1 << 0); // 清除 WDT_EN 位
writel(val, dw_wdt->base + WDOG_CONTROL_REG_OFFSET);
spin_unlock_irqrestore(&dw_wdt->lock, flags);
}
/**
* @brief 喂狗(重置看门狗计数器)。
*
* 写入任意值到 WDOG_COUNTER_RESTART_REG_OFFSET 即可复位计数器。
*
* @param dw_wdt 指向 dw_wdt 结构。
*/
static void dw_wdt_restart(struct dw_wdt *dw_wdt)
{
unsigned long flags;
spin_lock_irqsave(&dw_wdt->lock, flags);
writel(0x1, dw_wdt->base + WDOG_COUNTER_RESTART_REG_OFFSET);
spin_unlock_irqrestore(&dw_wdt->lock, flags);
}
/**
* @brief 设置超时时间。
*
* @param dw_wdt 指向 dw_wdt 结构。
* @param timeout 超时时间(秒)。
*/
static void dw_wdt_set_timeout(struct dw_wdt *dw_wdt, unsigned int timeout)
{
unsigned long clk_rate;
u32 val;
clk_rate = clk_get_rate(dw_wdt->clk);
// 计算计数器的初始值:timeout * clk_rate
val = timeout * clk_rate;
// 限制在 32 位范围内
if (val > 0xFFFFFFFF)
val = 0xFFFFFFFF;
writel(val, dw_wdt->base + WDOG_TIMEOUT_REG_OFFSET);
}
/**
* @brief Watchdog 核心层回调函数:获取当前超时值。
*
* @param wdd 指向 watchdog_device 结构。
* @return 超时值(秒)。
*/
static unsigned int dw_wdt_get_timeleft(struct watchdog_device *wdd)
{
struct dw_wdt *dw_wdt = watchdog_get_drvdata(wdd);
u32 cur = readl(dw_wdt->base + WDOG_CURRENT_REG_OFFSET);
unsigned long clk_rate;
clk_rate = clk_get_rate(dw_wdt->clk);
if (clk_rate == 0)
return 0;
return cur / clk_rate;
}
/**
* @brief Watchdog 核心层回调函数:启动。
*
* @param wdd 指向 watchdog_device 结构。
* @return 0 成功。
*/
static int dw_wdt_start(struct watchdog_device *wdd)
{
struct dw_wdt *dw_wdt = watchdog_get_drvdata(wdd);
// 1. 设置超时
dw_wdt_set_timeout(dw_wdt, wdd->timeout);
// 2. 使能硬件
dw_wdt_enable(dw_wdt);
// 3. 首次喂狗
dw_wdt_restart(dw_wdt);
return 0;
}
/**
* @brief Watchdog 核心层回调函数:停止。
*
* @param wdd 指向 watchdog_device 结构。
* @return 0 成功。
*/
static int dw_wdt_stop(struct watchdog_device *wdd)
{
struct dw_wdt *dw_wdt = watchdog_get_drvdata(wdd);
dw_wdt_disable(dw_wdt);
return 0;
}
/**
* @brief Watchdog 核心层回调函数:喂狗。
*
* @param wdd 指向 watchdog_device 结构。
* @return 0 成功。
*/
static int dw_wdt_ping(struct watchdog_device *wdd)
{
struct dw_wdt *dw_wdt = watchdog_get_drvdata(wdd);
dw_wdt_restart(dw_wdt);
return 0;
}
/**
* @brief Watchdog 核心层回调函数:设置超时。
*
* @param wdd 指向 watchdog_device 结构。
* @param timeout 新超时时间。
* @return 0 成功。
*/
static int dw_wdt_set_timeout(struct watchdog_device *wdd, unsigned int timeout)
{
struct dw_wdt *dw_wdt = watchdog_get_drvdata(wdd);
wdd->timeout = timeout;
// 如果设备正在运行,立即生效
if (watchdog_active(wdd)) {
dw_wdt_set_timeout(dw_wdt, timeout);
dw_wdt_restart(dw_wdt);
}
return 0;
}
/**
* @brief Watchdog 中断处理函数(可选)。
*
* 如果硬件支持"超时前中断",可用于打印调试信息或进行软重启。
*
* @param irq 中断号。
* @param dev_id 指向 dw_wdt 结构。
*/
static irqreturn_t dw_wdt_irq_handler(int irq, void *dev_id)
{
struct dw_wdt *dw_wdt = dev_id;
// 1. 读取中断状态寄存器
u32 isr = readl(dw_wdt->base + WDOG_ISR_OFFSET);
if (isr & 1) {
// WDOG 超时即将发生
dev_crit(dw_wdt->wdd.parent, "Watchdog timeout imminent! System will reset.\n");
// 清除中断(写 1 清零)
writel(isr, dw_wdt->base + WDOG_ISR_OFFSET);
return IRQ_HANDLED;
}
return IRQ_NONE;
}
/**
* @brief Platform 探测函数。
*
* 初始化 Watchdog 硬件并注册到核心层。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int dw_wdt_probe(struct platform_device *pdev)
{
struct dw_wdt *dw_wdt;
struct resource *res;
struct watchdog_device *wdd;
int ret, irq;
unsigned long clk_rate;
// 1. 分配私有数据结构
dw_wdt = devm_kzalloc(&pdev->dev, sizeof(*dw_wdt), GFP_KERNEL);
if (!dw_wdt)
return -ENOMEM;
platform_set_drvdata(pdev, dw_wdt);
// 2. 获取 I/O 资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "No memory resource\n");
return -ENXIO;
}
dw_wdt->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(dw_wdt->base))
return PTR_ERR(dw_wdt->base);
// 3. 获取时钟(用于计算超时值)
dw_wdt->clk = devm_clk_get(&pdev->dev, NULL);
if (IS_ERR(dw_wdt->clk))
return PTR_ERR(dw_wdt->clk);
clk_prepare_enable(dw_wdt->clk);
// 4. 获取中断(并非所有 Watchdog 都有)
irq = platform_get_irq(pdev, 0);
if (irq > 0) {
dw_wdt->irq = irq;
ret = devm_request_irq(&pdev->dev, irq, dw_wdt_irq_handler,
IRQF_SHARED, "dw-wdt", dw_wdt);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
return ret;
}
// 启用中断(让硬件在超时前产生中断)
writel(0x1, dw_wdt->base + WDOG_IMR_OFFSET);
}
// 5. 初始化 watchdog_device 结构
wdd = &dw_wdt->wdd;
wdd->info = &dw_wdt_info;
wdd->ops = &dw_wdt_ops;
wdd->min_timeout = 1;
// 根据时钟计算出最大超时(假设 32 位计数器)
clk_rate = clk_get_rate(dw_wdt->clk);
wdd->max_timeout = 0xFFFFFFFF / clk_rate;
wdd->timeout = min(wdd->max_timeout, 30U); // 默认 30 秒
wdd->parent = &pdev->dev;
wdd->bootstatus = (readl(dw_wdt->base + WDOG_CONTROL_REG_OFFSET) & (1 << 4)) ?
WDIOF_CARDRESET : 0;
watchdog_set_drvdata(wdd, dw_wdt);
spin_lock_init(&dw_wdt->lock);
// 6. 注册到 Watchdog 核心
ret = watchdog_register_device(wdd);
if (ret) {
dev_err(&pdev->dev, "Failed to register watchdog\n");
return ret;
}
dev_info(&pdev->dev, "DesignWare Watchdog registered at 0x%llx, IRQ %d\n",
(unsigned long long)res->start, dw_wdt->irq);
return 0;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int dw_wdt_remove(struct platform_device *pdev)
{
struct dw_wdt *dw_wdt = platform_get_drvdata(pdev);
// 1. 注销
watchdog_unregister_device(&dw_wdt->wdd);
// 2. 禁用硬件(如果已经启动)
if (watchdog_active(&dw_wdt->wdd))
dw_wdt_disable(dw_wdt);
// 3. 禁用时钟
clk_disable_unprepare(dw_wdt->clk);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id dw_wdt_of_match[] = {
{ .compatible = "snps,dw-wdt" },
{ .compatible = "snps,dw-wdt-0" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, dw_wdt_of_match);
/* Platform 驱动结构 */
static struct platform_driver dw_wdt_driver = {
.probe = dw_wdt_probe,
.remove = dw_wdt_remove,
.driver = {
.name = "dw-wdt",
.of_match_table = dw_wdt_of_match,
},
};
module_platform_driver(dw_wdt_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("DesignWare Watchdog Driver");
MODULE_LICENSE("GPL v2");
第三章 Watchdog 调试核心难点
3.1 系统无故重启
现象:设备正常运行中突然重启,dmesg 无任何异常日志,只知道系统被复位了。
原因:Watchdog 没有按时被喂狗。可能因为:
-
实时进程(高优先级)占用了 CPU,导致
systemd的喂狗进程未被调度。 -
喂狗线程被无限期阻塞(如长时间持有自旋锁)。
-
Watchdog 超时设置过短。
调试方法:
-
确认复位原因:
# 检查 watchdog 状态 cat /sys/class/watchdog/watchdog0/status
-
查看上次复位的原因(某些 SoC 有复位原因寄存器):
devmem2 <base_addr>+0x08 # 读取复位状态寄存器
-
监控喂狗过程:
# 使用 strace 跟踪喂狗进程 strace -p $(pgrep systemd-watchdog) -e trace=write
-
增加超时时间:
# 在设备树中设置更大的 timeout timeout-sec = <60>;
3.2 喂狗失败导致系统死循环
现象:系统复位后,启动过程中再次复位,导致无法启动。
原因:Bootloader 启动了 Watchdog,但内核启动过程中没有及时喂狗。
调试方法:
-
禁用 Bootloader 的 Watchdog:在 U-Boot 配置中禁用
CONFIG_WATCHDOG。 -
在内核启动早期喂狗:在内核启动参数中设置
watchdog.early_ping=1。 -
临时关闭 Watchdog 驱动:
# 在 kernel cmdline 中禁用 dw_wdt.start_timeout=0
3.3 用户空间喂狗进程被阻塞
现象:systemd-watchdog 进程运行正常,但喂狗失败。
原因:systemd-watchdog 通过 /dev/watchdog 字符设备进行 write 操作,如果驱动中的 ioctl 或 write 被阻塞,喂狗将失败。
调试方法:
-
查看 /dev/watchdog 的权限:
ls -l /dev/watchdog
-
手动测试喂狗:
# 手动写入任意字符到 /dev/watchdog 测试 echo "1" > /dev/watchdog
-
查看 Watchdog 设备的打开计数:
cat /proc/devices | grep watchdog
第四章 结合性能调试场景示例
场景:远程部署的工业设备在低温环境下偶尔无故重启,无任何日志,重启间隔不固定(2小时~8小时)。
分析流程:
-
宏观层面(图谱的 Hardware 层):
-
检查硬件状态:可能存在时钟源的精度问题。
-
-
查看复位原因:
devmem2 <wdt_base>+0x00
发现
WDT_EN位始终为 1,且WDOG_TIMEOUT值没有变化,说明 Watchdog 一直在运行。 -
测量喂狗实际间隔:
# 编写脚本记录每次喂狗的时间戳 while true; do date >> /tmp/watchdog.log; sleep 5; done # 用 tcpdump 或 ssh 拉取日志查看
-
根本原因:
-
发现某些时间段喂狗间隔超过 30 秒(设备树配置的
timeout-sec)。 -
排查发现,在低温环境下,
clk_get_rate(dw_wdt->clk)返回的时钟频率变慢,导致计数器溢出时间延长,但喂狗周期是固定的。 -
因此,计数器溢出后触发复位,而不是真正的喂狗周期。
-
-
解决方案:
-
使用
timeout-sec稍大,并确保喂狗进程的优先级稳定。 -
在驱动中实现更准确的时钟漂移补偿。
-
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| Reset Controller | 实际触发复位 | 确保复位信号正确传送到 SoC |
| Clock Controller | 提供计数器时钟 | 时钟源稳定,温漂补偿 |
| PMIC/Power Management | 看门狗触发时切断电源 | 在断电前保留日志 |
| System Timer/HRT | 用户空间喂狗计时 | 系统定时器中断调度正常 |
第六章 总结
-
不要在生产环境中完全禁用:如果必须调试,可以暂时将超时设得很大,但不要完全关闭。
-
确保喂狗进程有足够的优先级:在
systemd中使用WatchdogSec和RestartSec配合。 -
观察底层复位原因:使用
devmem2读取复位原因寄存器可以区分是由于 Watchdog 复位还是其他原因。 -
利用性能图谱中的工具:结合
perf、trace-cmd和kmemleak等工具来定位喂狗失败的根本原因。
第三部分 RTC 控制器
第一章 RTC 控制器在 Platform Bus 中的位置
RTC 控制器是嵌入式系统中用于维持系统时间的关键外设。即使主电源断电,它依靠备用电池(通常是纽扣电池)继续保持时间计数。在 Linux 中,RTC 驱动通常挂载在 Platform Bus 上(某些 SoC 内部 RTC),或者在 I2C/SPI 总线上(外部 RTC 芯片)。
RTC 子系统的核心逻辑:
-
RTC 核心层 (
drivers/rtc/rtc-dev.c):提供/dev/rtc0字符设备接口,以及sysfs时间读写。 -
RTC 控制器驱动:实现具体的硬件操作(如
rtc-pl031.c)。 -
用户空间:通过
hwclock或date与 RTC 交互。
第二章 Linux 5.10 典型 RTC 控制器驱动 —— rtc-pl031.c
PL031 (PrimeCell RTC) 是 ARM 架构中最标准的 RTC IP 核,广泛用于各种 Cortex-A 系列 SoC(如 Versatile, VExpress, 某些高通芯片)。它挂载在 APB 总线上,映射为内存空间。以下代码基于 Linux 5.10 drivers/rtc/rtc-pl031.c,展示核心逻辑。
2.1 硬件关键寄存器
-
RTC_DR:数据寄存器(当前时间的秒数,从 Unix 纪元开始)。 -
RTC_MR:匹配寄存器(可设置闹钟时间)。 -
RTC_LR:加载寄存器(设置时间)。 -
RTC_CR:控制寄存器(使能、中断使能)。 -
RTC_IMSC:中断掩码设置寄存器。 -
RTC_RIS:原始中断状态寄存器。 -
RTC_MIS:屏蔽中断状态寄存器。 -
RTC_ICR:中断清除寄存器。
2.2 核心代码
// 基于 Linux 5.10 drivers/rtc/rtc-pl031.c (精简版)
#include <linux/io.h>
#include <linux/rtc.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/module.h>
/**
* @brief PL031 RTC 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 RTC Controller 硬件实例。
*/
struct pl031_rtc {
void __iomem *base; /**< 映射后的寄存器基址 */
int irq; /**< 闹钟中断号 */
struct rtc_device *rtc; /**< RTC 核心抽象 */
spinlock_t lock; /**< 保护硬件寄存器 */
struct device *dev; /**< 设备指针 */
};
/* 寄存器偏移量 (据 PL031 手册) */
#define RTC_DR 0x00 // 数据寄存器
#define RTC_MR 0x04 // 匹配寄存器
#define RTC_LR 0x08 // 加载寄存器
#define RTC_CR 0x0C // 控制寄存器
#define RTC_IMSC 0x10 // 中断掩码
#define RTC_RIS 0x14 // 原始中断状态
#define RTC_MIS 0x18 // 屏蔽中断状态
#define RTC_ICR 0x1C // 中断清除
/**
* @brief 读取当前时间 (从 RTC_DR 中获取秒数)。
*
* @param dev 指向 pl031_rtc 结构。
* @param time 时间结构体,填充结果。
* @return 0 成功。
*/
static int pl031_read_time(struct device *dev, struct rtc_time *tm)
{
struct pl031_rtc *rtc = dev_get_drvdata(dev);
u32 time_sec;
// 1. 读取 RTC 数据寄存器 (从 Unix 纪元开始的秒数)
time_sec = readl(rtc->base + RTC_DR);
// 2. 将秒数转换为 rtc_time 结构
rtc_time_to_tm(time_sec, tm);
return 0;
}
/**
* @brief 设置当前时间 (写入 RTC_LR 寄存器)。
*
* @param dev 指向 pl031_rtc 结构。
* @param tm 要设置的时间。
* @return 0 成功。
*/
static int pl031_set_time(struct device *dev, struct rtc_time *tm)
{
struct pl031_rtc *rtc = dev_get_drvdata(dev);
u32 time_sec;
unsigned long flags;
// 1. 将 rtc_time 转换为秒数
rtc_tm_to_time(tm, &time_sec);
// 2. 写入加载寄存器 (PL031 会自动更新 DR)
spin_lock_irqsave(&rtc->lock, flags);
writel(time_sec, rtc->base + RTC_LR);
spin_unlock_irqrestore(&rtc->lock, flags);
return 0;
}
/**
* @brief 读取闹钟时间。
*
* @param dev 指向 pl031_rtc 结构。
* @param tm 闹钟时间结构体。
* @return 0 成功。
*/
static int pl031_read_alarm(struct device *dev, struct rtc_wkalrm *alarm)
{
struct pl031_rtc *rtc = dev_get_drvdata(dev);
u32 match_sec;
unsigned long flags;
// 1. 读取匹配寄存器 (闹钟触发时间)
spin_lock_irqsave(&rtc->lock, flags);
match_sec = readl(rtc->base + RTC_MR);
spin_unlock_irqrestore(&rtc->lock, flags);
// 2. 转换为 rtc_time
rtc_time_to_tm(match_sec, &alarm->time);
// 3. 检查闹钟是否使能 (从 IMSC 寄存器读取)
alarm->enabled = (readl(rtc->base + RTC_IMSC) & (1 << 0)) ? 1 : 0;
return 0;
}
/**
* @brief 设置闹钟时间。
*
* @param dev 指向 pl031_rtc 结构。
* @param alarm 闹钟时间。
* @return 0 成功。
*/
static int pl031_set_alarm(struct device *dev, struct rtc_wkalrm *alarm)
{
struct pl031_rtc *rtc = dev_get_drvdata(dev);
u32 match_sec;
unsigned long flags;
// 1. 转换时间到秒
rtc_tm_to_time(&alarm->time, &match_sec);
// 2. 写入匹配寄存器
spin_lock_irqsave(&rtc->lock, flags);
writel(match_sec, rtc->base + RTC_MR);
// 3. 设置中断使能
if (alarm->enabled)
writel(1 << 0, rtc->base + RTC_IMSC); // 使能闹钟中断
else
writel(0, rtc->base + RTC_IMSC);
spin_unlock_irqrestore(&rtc->lock, flags);
return 0;
}
/**
* @brief 闹钟中断处理函数。
*
* @param irq 中断号。
* @param dev_id 指向 pl031_rtc 结构。
*/
static irqreturn_t pl031_alarm_irq_handler(int irq, void *dev_id)
{
struct pl031_rtc *rtc = dev_id;
u32 mis;
// 1. 检查中断状态 (屏蔽中断状态)
mis = readl(rtc->base + RTC_MIS);
if (!mis)
return IRQ_NONE;
// 2. 清除中断 (写 ICR)
writel(mis, rtc->base + RTC_ICR);
// 3. 通知 RTC 核心闹钟触发
rtc_update_irq(rtc->rtc, 1, RTC_IRQF | RTC_AF);
return IRQ_HANDLED;
}
/**
* @brief Platform 探测函数。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int pl031_probe(struct platform_device *pdev)
{
struct pl031_rtc *rtc;
struct resource *res;
int ret, irq;
// 1. 分配私有数据结构
rtc = devm_kzalloc(&pdev->dev, sizeof(*rtc), GFP_KERNEL);
if (!rtc)
return -ENOMEM;
platform_set_drvdata(pdev, rtc);
rtc->dev = &pdev->dev;
// 2. 获取并映射 I/O 资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENXIO;
rtc->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(rtc->base))
return PTR_ERR(rtc->base);
// 3. 获取中断 (闹钟)
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
rtc->irq = irq;
spin_lock_init(&rtc->lock);
// 4. 注册 RTC 设备
rtc->rtc = devm_rtc_allocate_device(&pdev->dev);
if (IS_ERR(rtc->rtc))
return PTR_ERR(rtc->rtc);
rtc->rtc->ops = &pl031_rtc_ops;
rtc->rtc->range_max = U32_MAX; // 32 位计数器,理论上可计数到 2106 年
rtc->rtc->range_min = 0;
// 5. 注册中断
ret = devm_request_irq(&pdev->dev, rtc->irq, pl031_alarm_irq_handler,
0, "pl031-rtc", rtc);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
return ret;
}
// 6. 使能 RTC (控制寄存器使能)
writel(1 << 0, rtc->base + RTC_CR); // RTC_EN 位
ret = devm_rtc_register_device(rtc->rtc);
if (ret) {
dev_err(&pdev->dev, "Failed to register RTC\n");
return ret;
}
dev_info(&pdev->dev, "PL031 RTC registered at 0x%llx, IRQ %d\n",
(unsigned long long)res->start, rtc->irq);
return 0;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int pl031_remove(struct platform_device *pdev)
{
struct pl031_rtc *rtc = platform_get_drvdata(pdev);
// 禁用中断 (清除 IMSC)
writel(0, rtc->base + RTC_IMSC);
// 禁用 RTC (清除 CR 的 RTC_EN)
writel(0, rtc->base + RTC_CR);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id pl031_of_match[] = {
{ .compatible = "arm,pl031" },
{ .compatible = "arm,primecell" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, pl031_of_match);
/* Platform 驱动结构 */
static struct platform_driver pl031_driver = {
.probe = pl031_probe,
.remove = pl031_remove,
.driver = {
.name = "pl031",
.of_match_table = pl031_of_match,
},
};
module_platform_driver(pl031_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("ARM PL031 RTC Controller Driver");
MODULE_LICENSE("GPL v2");
第三章 RTC 调试核心难点
3.1 系统时间回退或异常
现象:系统启动后,date 显示的时间是 1970-01-01 或其他不正确的时间。
原因:
-
RTC 未初始化(新芯片需要写入基准时间)。
-
RTC 电池没电。
-
RTC 芯片本身损坏。
调试方法:
-
查看 RTC 寄存器:
# 使用 devmem2 读取 RTC_DR 寄存器 devmem2 <base_addr>+0x00
如果返回值全为 0,说明 RTC 未初始化。
-
手动设置 RTC:
# 将系统时间写入 RTC hwclock -w
-
检查 RTC 电源:测量 RTC 引脚上的电压(通常 3V 纽扣电池)。
-
检查内核配置:确认
CONFIG_RTC_DRV_PL031=y。
3.2 闹钟中断不触发或无限触发
现象:设置闹钟后,系统没有在规定时间唤醒,或者醒来后 dmesg 显示闹钟中断风暴。
原因:
-
中断配置错误。
-
闹钟寄存器的值设置错误(过期)。
-
中断未正确清除。
调试方法:
-
查看中断统计:
cat /proc/interrupts | grep pl031
观察触发次数。
-
使用
trace-cmd跟踪中断:trace-cmd record -e rtc:* -e irq:* trace-cmd report | grep pl031
-
手动触发闹钟测试:
# 设置一个 10 秒后的闹钟 echo +10 > /sys/class/rtc/rtc0/wakealarm
然后
cat /sys/class/rtc/rtc0/wakealarm查看是否已设置。 -
检查寄存器:
-
确认
RTC_MR被正确设置。 -
确认
RTC_IMSC的位 0 为 1(闹钟中断使能)。 -
确认
RTC_ICR写后中断状态被清除。
-
3.3 时间漂移(精度问题)
现象:使用 RTC 维持时间,几天后系统时间偏离数分钟。
原因:
-
RTC 晶振频率不准(热漂移或老化)。
-
噪声耦合到时钟输入引脚。
调试方法:
-
使用
hwclock --test检查漂移:hwclock --test -r
-
长时间监控 RTC:
while true; do date; hwclock -r; sleep 3600; done
-
调整晶振负载电容(硬件)。
-
使用 NTP 定期同步 RTC。
第四章 结合性能调试场景示例
场景:嵌入式设备在远程部署 6 个月后,RTC 时间每天慢 15 分钟,导致 NTP 同步不稳定。
分析流程:
-
宏观层面( Hardware 层):
-
确认 RTC 的晶振频率(通常 32.768kHz)。
-
测量晶振实际频率(用示波器或频率计)。
-
-
查看 RTC 寄存器(RTC 控制器层):
-
devmem2 <base_addr>+0x00看到计数器准确吗?
-
-
根本原因:
-
由于 PCB 设计或老化,晶振负载电容不匹配,导致振荡频率低于 32.768kHz。
-
频率偏差:实际 32.767kHz,导致每天慢 25 秒。
-
-
解决方案:
-
硬件: 更换晶振或调整负载电容。
-
软件: 在
hwclock --systohc前,先对RTC_DR进行软件修正(例如每 30 分钟将 RTC 读出的时间加 1 秒)。这是嵌入式系统的常用手法。
-
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| PMC/Power Manager | 使 RTC 在低功耗模式下保持计时 | RTC 能否在睡眠模式下工作 |
| GPIO | 用于某些 RTC 的报警输出引脚 | GPIO 中断配置 |
| I2C/SPI | 外部 RTC 芯片通过 I2C/SPI 连接 | 总线通信是否正常 |
| System Timer | 用于 RTC 内核时间同步 | 内核时间与 RTC 的同步周期 |
第四部分 PWM 控制器
第一章 PWM 控制器在 Platform Bus 中的位置
PWM 控制器通常作为独立的 Platform 设备存在,用于产生可编程的方波信号。它在嵌入式系统中应用极其广泛:
-
屏幕背光调节:改变 LED 亮度。
-
电机控制:伺服电机、直流电机调速。
-
音频生成:通过 PWM 输出方波驱动蜂鸣器(Buzzer)。
-
电压调节:配合外部电路实现高效的 DCDC 转换。
Linux 中 PWM 子系统分为两层:
-
PWM 核心层 (
drivers/pwm/core.c):提供/sys/class/pwm/pwmchipX和通用的 PWM API。 -
PWM 控制器驱动(即本篇文章重点):具体的 SoC 主机驱动(如
pwm-rockchip.c)。
第二章 Linux 5.10 典型 PWM 控制器驱动 —— pwm-rockchip.c
Rockchip 系列 SoC 的 PWM 控制器是最典型的挂载在 APB 总线上的 Platform 设备之一。以下代码基于 Linux 5.10 drivers/pwm/pwm-rockchip.c,展示核心逻辑。
2.1 硬件关键概念
-
寄存器:通常包含控制寄存器、周期寄存器、占空比寄存器、计数器寄存器。
-
时钟:一个必须使能的
pclk(APB 总线时钟)用于寄存器访问,一个timer_clk用于生成 PWM 波形的实际频率。 -
中断:PWM 通常不产生中断,但有些控制器支持在计数器溢出或达到特定点时触发中断。
-
输出极性:可以配置为 High 有效或 Low 有效,这在调试信号极性不匹配时非常有用。
2.2 核心代码
// 基于 Linux 5.10 drivers/pwm/pwm-rockchip.c (简化版)
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/pwm.h>
#include <linux/platform_device.h>
#include <linux/module.h>
#include <linux/slab.h>
/**
* @brief Rockchip PWM 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 PWM Controller 硬件实例。
*/
struct rockchip_pwm {
void __iomem *base; /**< 映射后的寄存器基址 */
struct clk *clk; /**< APB 总线时钟 */
struct clk *pclk; /**< PWM 功能时钟 */
struct pwm_chip chip; /**< PWM 核心抽象 */
struct device *dev; /**< 设备指针 */
int channel_count; /**< 硬件包含的 PWM 通道数 */
};
/* 寄存器偏移量 (针对 Rockchip 常见版本) */
#define PWM_REG_CTRL 0x00
#define PWM_REG_PERIOD 0x04
#define PWM_REG_DUTY 0x08
#define PWM_REG_CNTR 0x0C
#define PWM_REG_CONFIG 0x10
/* 控制寄存器位定义 */
#define PWM_CTRL_ENABLE (1 << 0) /**< 全局使能 */
#define PWM_CTRL_CAP_MODE (1 << 1) /**< 捕获模式 */
#define PWM_CTRL_MODE_SHIFT (2) /**< 模式选择 */
#define PWM_CTRL_MODE_MASK (0x3 << 2) /**< 模式掩码 */
#define PWM_CTRL_POLARITY (1 << 4) /**< 极性选择 (1=高有效) */
#define PWM_CTRL_DEBUG (1 << 5) /**< 调试模式 */
/* 模式值 */
#define PWM_MODE_CONTINUOUS (0x0) /**< 连续模式 */
#define PWM_MODE_ONESHOT (0x1) /**< 单次模式 */
#define PWM_MODE_CAPTURE (0x2) /**< 捕获模式 */
/**
* @brief 将硬件配置应用于具体的 PWM 通道。
*
* @param pwm 指向 rockchip_pwm 结构。
* @param state 需要应用的 pwm_state。
*/
static void rockchip_pwm_apply_hw(struct rockchip_pwm *pwm,
struct pwm_state *state)
{
u32 period = state->period;
u32 duty = state->duty_cycle;
u32 ctrl = readl_relaxed(pwm->base + PWM_REG_CTRL);
// 1. 计算寄存器值:通常 period 和 duty 需要除以 `pwm->clk` 的速率
// 这里简化处理,假定 period 和 duty 已经是适配硬件的 32 位计数值
writel_relaxed(period, pwm->base + PWM_REG_PERIOD);
writel_relaxed(duty, pwm->base + PWM_REG_DUTY);
// 2. 设置极性
if (state->polarity == PWM_POLARITY_NORMAL)
ctrl &= ~PWM_CTRL_POLARITY;
else
ctrl |= PWM_CTRL_POLARITY;
// 3. 设置模式 (连续模式)
ctrl &= ~PWM_CTRL_MODE_MASK;
ctrl |= (PWM_MODE_CONTINUOUS << PWM_CTRL_MODE_SHIFT);
// 4. 使能或禁用 PWM
if (state->enabled)
ctrl |= PWM_CTRL_ENABLE;
else
ctrl &= ~PWM_CTRL_ENABLE;
writel_relaxed(ctrl, pwm->base + PWM_REG_CTRL);
}
/**
* @brief 核心 PWM 操作:获取当前 PWM 状态。
*
* @param chip 指向 pwm_chip 结构。
* @param pwm 指向 pwm_device。
* @param state 输出当前状态。
* @return 0 成功。
*/
static int rockchip_pwm_get_state(struct pwm_chip *chip,
struct pwm_device *pwm,
struct pwm_state *state)
{
struct rockchip_pwm *pc = to_rockchip_pwm(chip);
u32 ctrl = readl_relaxed(pc->base + PWM_REG_CTRL);
u32 period = readl_relaxed(pc->base + PWM_REG_PERIOD);
u32 duty = readl_relaxed(pc->base + PWM_REG_DUTY);
// 1. 获取使能状态
state->enabled = !!(ctrl & PWM_CTRL_ENABLE);
// 2. 获取极性
if (ctrl & PWM_CTRL_POLARITY)
state->polarity = PWM_POLARITY_INVERSED;
else
state->polarity = PWM_POLARITY_NORMAL;
// 3. 获取周期和占空比 (需要从寄存器值反推,这里简化)
state->period = period;
state->duty_cycle = duty;
return 0;
}
/**
* @brief 核心 PWM 操作:应用新的 PWM 状态。
*
* @param chip 指向 pwm_chip。
* @param pwm 指向 pwm_device。
* @param state 指向新的 pwm_state。
* @return 0 成功。
*/
static int rockchip_pwm_apply(struct pwm_chip *chip,
struct pwm_device *pwm,
struct pwm_state *state)
{
struct rockchip_pwm *pc = to_rockchip_pwm(chip);
struct pwm_state cur_state;
int ret;
// 1. 检查当前状态,决定是否重新配置
rockchip_pwm_get_state(chip, pwm, &cur_state);
if (state->period != cur_state.period ||
state->duty_cycle != cur_state.duty_cycle ||
state->polarity != cur_state.polarity) {
// 必须先禁用 PWM 才能修改配置
if (cur_state.enabled) {
// 禁用硬件
u32 ctrl = readl_relaxed(pc->base + PWM_REG_CTRL);
ctrl &= ~PWM_CTRL_ENABLE;
writel_relaxed(ctrl, pc->base + PWM_REG_CTRL);
// 等待硬件完成关闭 (某些硬件需要时间)
udelay(10);
}
// 应用新的硬件配置
rockchip_pwm_apply_hw(pc, state);
// 如果之前是 enabled 且新状态也是 enabled,重新启用
if (state->enabled)
rockchip_pwm_apply_hw(pc, state);
} else if (state->enabled != cur_state.enabled) {
// 仅修改使能状态,无需重新配置周期
u32 ctrl = readl_relaxed(pc->base + PWM_REG_CTRL);
if (state->enabled) {
// 先设置极性,再使能
if (state->polarity == PWM_POLARITY_NORMAL)
ctrl &= ~PWM_CTRL_POLARITY;
else
ctrl |= PWM_CTRL_POLARITY;
ctrl |= PWM_CTRL_ENABLE;
} else {
ctrl &= ~PWM_CTRL_ENABLE;
}
writel_relaxed(ctrl, pc->base + PWM_REG_CTRL);
}
return 0;
}
/* 核心 PWM 操作回调 */
static const struct pwm_ops rockchip_pwm_ops = {
.apply = rockchip_pwm_apply,
.get_state = rockchip_pwm_get_state,
};
/**
* @brief Platform 探测函数。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int rockchip_pwm_probe(struct platform_device *pdev)
{
struct rockchip_pwm *pc;
struct resource *res;
int ret;
// 1. 分配私有数据结构
pc = devm_kzalloc(&pdev->dev, sizeof(*pc), GFP_KERNEL);
if (!pc)
return -ENOMEM;
platform_set_drvdata(pdev, pc);
pc->dev = &pdev->dev;
// 2. 获取并映射 I/O 资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENXIO;
pc->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(pc->base))
return PTR_ERR(pc->base);
// 3. 获取时钟
pc->clk = devm_clk_get(&pdev->dev, NULL);
if (IS_ERR(pc->clk))
return PTR_ERR(pc->clk);
pc->pclk = devm_clk_get_optional(&pdev->dev, "pclk");
if (IS_ERR(pc->pclk))
return PTR_ERR(pc->pclk);
// 4. 使能时钟
ret = clk_prepare_enable(pc->clk);
if (ret) {
dev_err(&pdev->dev, "Failed to enable clk\n");
return ret;
}
if (pc->pclk) {
ret = clk_prepare_enable(pc->pclk);
if (ret) {
dev_err(&pdev->dev, "Failed to enable pclk\n");
clk_disable_unprepare(pc->clk);
return ret;
}
}
// 5. 获取通道数量 (从 DTS 或寄存器读取)
pc->channel_count = 1; // 简化示例
// 6. 初始化 PWM 芯片
pc->chip.dev = &pdev->dev;
pc->chip.ops = &rockchip_pwm_ops;
pc->chip.npwm = pc->channel_count;
pc->chip.base = -1;
// 7. 注册到 PWM 核心
ret = pwmchip_add(&pc->chip);
if (ret < 0) {
dev_err(&pdev->dev, "Failed to add PWM chip\n");
goto err_pclk;
}
dev_info(&pdev->dev, "Rockchip PWM controller registered at 0x%llx, %d channels\n",
(unsigned long long)res->start, pc->channel_count);
return 0;
err_pclk:
if (pc->pclk)
clk_disable_unprepare(pc->pclk);
clk_disable_unprepare(pc->clk);
return ret;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int rockchip_pwm_remove(struct platform_device *pdev)
{
struct rockchip_pwm *pc = platform_get_drvdata(pdev);
// 1. 删除 PWM 芯片
pwmchip_remove(&pc->chip);
// 2. 禁用时钟
if (pc->pclk)
clk_disable_unprepare(pc->pclk);
clk_disable_unprepare(pc->clk);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id rockchip_pwm_of_match[] = {
{ .compatible = "rockchip,rk3399-pwm" },
{ .compatible = "rockchip,rk3288-pwm" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, rockchip_pwm_of_match);
/* Platform 驱动结构 */
static struct platform_driver rockchip_pwm_driver = {
.probe = rockchip_pwm_probe,
.remove = rockchip_pwm_remove,
.driver = {
.name = "rockchip-pwm",
.of_match_table = rockchip_pwm_of_match,
},
};
module_platform_driver(rockchip_pwm_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Rockchip PWM Controller Driver");
MODULE_LICENSE("GPL v2");
第三章 PWM 调试核心难点
3.1 占空比/频率不准
现象:/sys/class/pwm/ 下设置正确的周期和占空比,但实际测得的波形频率或占空比不符合预期。
原因:
-
输入时钟
clk频率与代码中计算period/duty的公式不匹配。 -
寄存器位宽(通常是 16 位或 32 位)与计数器溢出值不匹配。
-
硬件分频器配置错误。
调试方法:
-
检查时钟频率:
# 确认 PWM 功能时钟频率 cat /sys/kernel/debug/clk/clk_summary | grep pwm
-
重新计算周期:假设输入时钟为
50 MHz,你需要1 kHz的频率,则计数器周期值应为50,000。检查驱动中是否使用了正确公式。 -
直接读取寄存器:
devmem2 <base_addr>+0x04 # 读 PERIOD 寄存器 devmem2 <base_addr>+0x08 # 读 DUTY 寄存器
3.2 PWM 输出始终无信号
现象:无论怎么配置,PWM 引脚始终没有输出波形。
原因:
-
GPIO/PinMux 配置不正确。
-
PWM 时钟被禁用。
-
配置未使能或极性错误。
调试方法:
-
检查 PinMux:
cat /sys/kernel/debug/pinctrl/pinctrl-handles | grep pwm
-
查看 GPIO 状态:
cat /sys/kernel/debug/gpio | grep pwm
-
确认配置生效:
# 手动触发一次配置 echo 0 > /sys/class/pwm/pwmchip0/export echo 1000000 > /sys/class/pwm/pwmchip0/pwm0/period echo 500000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable
-
观察寄存器值:检查
PWM_REG_CTRL的使能位是否置位。
3.3 极性翻转但示波器未变化
现象:用户空间设置 polarity = inversed,但波形极性未变化。
原因:硬件可能不支持双极性输出,或寄存器配置未生效。
调试方法:
-
查看寄存器控制位:
devmem2 <base_addr>+0x00检查PWM_CTRL_POLARITY位是否发生变化。 -
验证硬件能力:查阅 SoC 手册确认是否支持极性切换。
-
强制极性:在驱动中直接写寄存器,观察示波器变化,以排除用户空间的干扰。
第四章 结合性能调试场景示例
场景:系统背光调节异常。通过 PWM 调节屏幕背光时,亮度闪烁或调节无效。
分析流程:
-
宏观层面(图谱的 CPU 层):
-
运行
iostat或top,确认 CPU 负载正常。 -
检查
perf top是否有大量的pwm相关系统调用。
-
-
PWM 基本测试(PWM 控制器层):
# 创建 PWM 节点并测试 echo 0 > /sys/class/pwm/pwmchip0/export echo 1000000 > /sys/class/pwm/pwmchip0/pwm0/period echo 500000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable
如果背光正常,说明驱动基本 OK。
-
检查调节逻辑(应用层):
-
背光调节通常通过
sysfs或ioctl实现。 -
使用
strace跟踪背光调节应用的write或ioctl调用:
strace -e trace=write -p $(pgrep backlight_daemon)
-
-
根本原因:背光调节应用程序在写入
duty_cycle时,由于用户空间和驱动之间的通信问题,没有正确格式化字符串。例如:写入1000而不是正确的500000。 -
解决方案:修复应用程序的数值计算或格式化逻辑。
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| GPIO/PinMux | PWM 信号通过特定 GPIO 引脚输出 | 确保引脚未冲突,PinMux 配置正确 |
| Clock Controller | 提供 PWM 的输入时钟 | 时钟频率和稳定性,分频器计算 |
| I2C/SPI | 外部 PWM 驱动器或 LED 控制器通过总线访问 | 确保 I2C/SPI 通信正常 |
| PMIC | 为 PWM 电路提供电源 | 电压稳定 |
第五部分 Thermal 控制器(SoC 内部温度传感器)
第一章 Thermal 控制器在 Platform Bus 中的位置
Thermal 控制器(通常称为 TSADC,Temperature Sensor ADC)是挂载在 Platform Bus 上的 SoC 内部模块。它包含一个或多个温度传感器,用于监测芯片内部不同区域的温度(如 CPU 核心、GPU、DDR、PMIC 等),并能在温度超过阈值时触发中断或硬件降频。
在 Linux 中,Thermal 子系统非常成熟,核心架构是 Thermal Framework:
-
Thermal 核心层 (
drivers/thermal/thermal_core.c):管理 热区 (Thermal Zone)、冷却设备 (Cooling Device) 和 热治理 (Thermal Governor)。 -
温度传感器驱动(本篇文章重点):具体 SoC 的硬件驱动(如
rockchip_thermal.c,qcom_tsens.c)。 -
冷却设备驱动:如
cpufreq_cooling.c(CPU 降频)、devfreq_cooling.c(GPU 降频)、fan_cooling.c(风扇)。 -
热治理策略:如
step_wise(台阶式)、fair_share(公平共享)、user_space(用户空间控制)。
第二章 Linux 5.10 典型 Thermal 控制器驱动 —— rockchip_thermal.c
Rockchip TSADC 是支持 4 个以上温度传感器通道的典型 Platform 设备,可检测 CPU、GPU、DDR 等区域温度,并支持硬件超温中断。
以下代码基于 Linux 5.10 drivers/thermal/rockchip_thermal.c,展示核心逻辑。
2.1 硬件关键概念
-
ADC 通道:每个传感器对应一个 ADC 通道(0~4),分别测量不同区域。
-
寄存器:控制寄存器、数据寄存器(保存当前温度)、阈值寄存器(用于触发中断)。
-
中断:可配置的高温警告、高温临界、低温警告等多个中断源。
-
时钟:通常需要
tsadc_clk(ADC 采样时钟)和pclk(总线时钟)。
2.2 核心代码
// 基于 Linux 5.10 drivers/thermal/rockchip_thermal.c (精简版)
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <linux/thermal.h>
#include <linux/delay.h>
#include <linux/module.h>
#define ROCKCHIP_TSADC_MAX_SENSORS 5
#define ROCKCHIP_TSADC_POLL_DELAY 1000 // 毫秒
/**
* @brief Rockchip Thermal 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 Thermal Controller 硬件实例。
*/
struct rockchip_tsadc_priv {
void __iomem *base; /**< 映射后的寄存器基址 */
int irq; /**< 中断号 */
struct clk *tsadc_clk; /**< 采样时钟 */
struct clk *pclk; /**< 总线时钟 */
struct device *dev; /**< 设备指针 */
struct thermal_zone_device *tzd[ROCKCHIP_TSADC_MAX_SENSORS]; /**< 热区设备 */
int sensor_count; /**< 实际传感器数量 */
u32 *sensor_id_map; /**< 传感器 ID 映射 */
};
/* 寄存器偏移量 (因 Rockchip 版本较多,此处为通用示例) */
#define TSADC_CON 0x00
#define TSADC_SAMPLE_RATE 0x04
#define TSADC_DATA_BASE 0x08 // 数据寄存器基址 (每个传感器 +4)
#define TSADC_INT_EN 0x1C
#define TSADC_INT_STAT 0x20
#define TSADC_INT_CFG 0x24
#define TSADC_THRESH_BASE 0x28 // 阈值寄存器基址
/* 控制寄存器位定义 */
#define TSADC_CON_SOC_EN (1 << 0) /**< 全局使能 */
#define TSADC_CON_SOC_PD (1 << 1) /**< 上电/掉电控制 */
#define TSADC_CON_SOC_PH (1 << 2) /**< 相位控制 */
/**
* @brief 从硬件读取指定传感器的温度。
*
* @param priv 指向 rockchip_tsadc_priv 结构。
* @param sensor_idx 传感器索引 (0 ~ sensor_count-1)。
* @return 温度值 (毫摄氏度,m℃),-1 表示错误。
*/
static int rockchip_thermal_read_temp(struct rockchip_tsadc_priv *priv,
int sensor_idx)
{
u32 data_reg, temp_raw, temp_mc;
// 1. 读取传感器对应的数据寄存器
data_reg = TSADC_DATA_BASE + sensor_idx * 4;
temp_raw = readl_relaxed(priv->base + data_reg);
// 2. 提取有效位 (假设 12 位 ADC,高 4 位为符号位)
temp_raw &= 0xFFF;
// 3. 转换为毫摄氏度 (每个 SoC 的转换公式不同)
// 这里假设一个简单线性公式:Temp(°C) = (raw * 0.5) - 40
temp_mc = (temp_raw * 500) - 40000;
return temp_mc;
}
/**
* @brief 为热区获取温度的通用回调函数。
*
* @param tzd 指向 thermal_zone_device。
* @param temp 指向输出温度 (毫摄氏度)。
* @return 0 成功。
*/
static int rockchip_thermal_zone_get_temp(struct thermal_zone_device *tzd,
int *temp)
{
struct rockchip_tsadc_priv *priv = tzd->devdata;
int sensor_idx = (int)tzd->id; // 假设热区 ID 对应传感器索引
*temp = rockchip_thermal_read_temp(priv, sensor_idx);
return 0;
}
/* 热区操作回调 */
static struct thermal_zone_device_ops rockchip_thermal_ops = {
.get_temp = rockchip_thermal_zone_get_temp,
};
/**
* @brief 中断处理函数。
*
* 处理高温警告、高温临界、低温警告等中断。
*
* @param irq 中断号。
* @param dev_id 指向 rockchip_tsadc_priv 结构。
*/
static irqreturn_t rockchip_thermal_irq_handler(int irq, void *dev_id)
{
struct rockchip_tsadc_priv *priv = dev_id;
u32 int_stat, int_cfg;
// 1. 读取中断状态
int_stat = readl_relaxed(priv->base + TSADC_INT_STAT);
int_cfg = readl_relaxed(priv->base + TSADC_INT_CFG);
int_stat &= int_cfg; // 仅处理使能的中断
// 2. 检查中断类型
if (int_stat & (1 << 0)) {
// 高温临界 (Critical)
dev_emerg(priv->dev, "Critical temperature exceeded! System will shutdown.\n");
// 触发紧急关机或重置
// ...
}
if (int_stat & (1 << 1)) {
// 高温警告 (Warning)
dev_warn(priv->dev, "High temperature warning detected!\n");
// 通知热区
thermal_zone_device_update(priv->tzd[0], THERMAL_EVENT_UNSPECIFIED);
}
if (int_stat & (1 << 2)) {
// 低温警告
dev_warn(priv->dev, "Low temperature warning detected!\n");
}
// 3. 清除中断 (写 1 清除)
writel_relaxed(int_stat, priv->base + TSADC_INT_STAT);
return IRQ_HANDLED;
}
/**
* @brief 初始化传感器硬件。
*
* @param priv 指向 rockchip_tsadc_priv 结构。
*/
static void rockchip_thermal_hw_init(struct rockchip_tsadc_priv *priv)
{
u32 con;
// 1. 上电配置
con = readl_relaxed(priv->base + TSADC_CON);
con |= TSADC_CON_SOC_EN;
con &= ~TSADC_CON_SOC_PD;
writel_relaxed(con, priv->base + TSADC_CON);
// 2. 配置采样率 (例如 1kHz)
writel_relaxed(1000, priv->base + TSADC_SAMPLE_RATE);
// 3. 设置阈值 (从设备树或硬件默认值读取)
// 这里简单示例,设置高温警告阈值 85°C (需要转换为 ADC 原始值)
// ...
// 4. 使能中断
writel_relaxed(0x7, priv->base + TSADC_INT_EN); // 使能所有中断
writel_relaxed(0x7, priv->base + TSADC_INT_CFG);
}
/**
* @brief 探测函数。
*
* 初始化 Thermal 控制器硬件,注册热区。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int rockchip_thermal_probe(struct platform_device *pdev)
{
struct rockchip_tsadc_priv *priv;
struct resource *res;
struct thermal_zone_device *tz;
int ret, i, 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. 获取中断
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
priv->irq = irq;
// 4. 获取时钟
priv->tsadc_clk = devm_clk_get(&pdev->dev, "tsadc_clk");
if (IS_ERR(priv->tsadc_clk))
return PTR_ERR(priv->tsadc_clk);
priv->pclk = devm_clk_get(&pdev->dev, "pclk");
if (IS_ERR(priv->pclk))
return PTR_ERR(priv->pclk);
clk_prepare_enable(priv->tsadc_clk);
clk_prepare_enable(priv->pclk);
// 5. 硬件初始化
rockchip_thermal_hw_init(priv);
// 6. 注册中断
ret = devm_request_irq(&pdev->dev, priv->irq, rockchip_thermal_irq_handler,
IRQF_SHARED, "rockchip-thermal", priv);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
goto err_clk;
}
// 7. 注册热区 (每个传感器一个)
priv->sensor_count = 2; // 简化示例,假设有 2 个传感器
for (i = 0; i < priv->sensor_count; i++) {
tz = devm_thermal_zone_of_sensor_register(&pdev->dev,
i,
priv,
&rockchip_thermal_ops);
if (IS_ERR(tz)) {
dev_err(&pdev->dev, "Failed to register thermal zone %d\n", i);
ret = PTR_ERR(tz);
goto err_thermal;
}
priv->tzd[i] = tz;
}
dev_info(&pdev->dev, "Rockchip Thermal controller registered at 0x%llx, IRQ %d, %d sensors\n",
(unsigned long long)res->start, priv->irq, priv->sensor_count);
return 0;
err_thermal:
// 取消注册热区 (框架会自动清理)
err_clk:
clk_disable_unprepare(priv->tsadc_clk);
clk_disable_unprepare(priv->pclk);
return ret;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int rockchip_thermal_remove(struct platform_device *pdev)
{
struct rockchip_tsadc_priv *priv = platform_get_drvdata(pdev);
// 1. 禁用中断和硬件
writel_relaxed(0, priv->base + TSADC_INT_EN);
writel_relaxed(0, priv->base + TSADC_CON);
// 2. 禁用时钟
clk_disable_unprepare(priv->tsadc_clk);
clk_disable_unprepare(priv->pclk);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id rockchip_thermal_of_match[] = {
{ .compatible = "rockchip,rk3399-tsadc" },
{ .compatible = "rockchip,rk3288-tsadc" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, rockchip_thermal_of_match);
/* Platform 驱动结构 */
static struct platform_driver rockchip_thermal_driver = {
.probe = rockchip_thermal_probe,
.remove = rockchip_thermal_remove,
.driver = {
.name = "rockchip-thermal",
.of_match_table = rockchip_thermal_of_match,
},
};
module_platform_driver(rockchip_thermal_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Rockchip Thermal Controller Driver");
MODULE_LICENSE("GPL v2");
第三章 Thermal 调试核心难点
3.1 温度读数异常(过高或过低)
现象:通过 /sys/class/thermal/thermal_zoneX/temp 读取的温度远高于或低于实际室温。
原因:
-
ADC 校准系数错误。
-
硬件未校准(每个芯片出厂时需要写入校准数据,通常保存在 OTP 中)。
-
温度传感器被部分物理损坏。
调试方法:
-
读取原始寄存器值:
# 读取传感器的原始 ADC 值 devmem2 <base_addr>+0x08
-
参考数据手册:将原始值手动代入该 SoC 专用的转换公式计算,对比实际温度。
-
检查传感器连接:某些芯片的传感器在引脚复用配置错误时可能接地或接 VCC,导致读数固定为 0 或最大值。
-
查阅校准 OTP:
# 读取校准数据 (通常保存在某个 NVM 或 OTP 中,通过特殊的命令或寄存器访问)
-
手动校准偏移:在驱动中通过添加软件偏移修正。
3.2 温度过高导致系统自动关机
现象:系统运行负载测试时,突然掉电或重启,dmesg 中出现 "Critical temperature exceeded!"。
原因:硬件高温临界阈值设置过低。部分 SoC 的临界温度为 90°C,在高负载环境下很容易超过。
调试方法:
-
读取当前的温度:
# 查看温度 for i in /sys/class/thermal/thermal_zone*/temp; do echo $i $(cat $i); done
-
查看阈值设置:
# 读取阈值寄存器 devmem2 <base_addr>+0x28
-
临时调高临界值:在驱动中增加阈值偏移,或者通过 DTS 设置
temperature-high属性。 -
优化散热:增加散热片或风扇。
-
动态调整降频策略:如果 CPU 降频策略滞后,可以修改
thermal_zone_device_update的触发频率。
3.3 中断风暴(频繁触发高温警告)
现象:系统频繁打印 "High temperature warning detected!",CPU 占用率飙升。
原因:温度在高温阈值附近波动,导致中断频繁进入和退出。
调试方法:
-
查看中断统计:
cat /proc/interrupts | grep thermal
-
增加迟滞(Hysteresis):在驱动中为阈值增加迟滞区间,避免在阈值附近频繁触发中断。
-
降低采样率:如果
TSADC_SAMPLE_RATE设置过高,导致检测过于灵敏。 -
使用
perf监控中断:perf record -e irq:* -a -- sleep 5 perf script | grep thermal
3.4 热区冷却设备未生效
现象:温度超过阈值时,系统 CPU 频率未降低,导致持续过热。
原因:冷却设备(cpufreq_cooling)未正确绑定到热区。thermal_zone_device_of_sensor_register 未正确识别 DTS 中的 cooling-maps。
调试方法:
-
查看热区绑定:
# 查看热区冷却设备 cat /sys/class/thermal/thermal_zone*/cooling_device*
-
检查 DTS:确认
thermal-zones节点下的cooling-maps配置正确。 -
手动触发冷却:
# 强制触发热区 echo 90000 > /sys/class/thermal/thermal_zone0/emul_temp # 模拟温度
观察 CPU 频率是否下降:
cpufreq-info。 -
检查
cpufreq驱动:确保cpufreq驱动已注册冷却设备。
第四章 结合性能调试场景示例
场景:嵌入式设备在播放 4K 视频时,突然因高温关机。但使用温度计测量外壳温度仅 40°C。
分析流程:
-
宏观层面(图谱的 CPU 层):
-
top显示 CPU 利用率 100%,GPU 满载。 -
perf top显示rockchip_thermal_irq_handler没有调用,说明不是中断问题,而是直接触发了硬件临界保护。
-
-
热区状态(Thermal 核心层):
cat /sys/class/thermal/thermal_zone*/temp
发现 CPU 传感器读到 85°C,但 GPU 传感器读到 95°C(已经接近临界)。
-
检查阈值:
devmem2 <base_addr>+0x28 # 临界阈值寄存器
发现值对应的温度是 90°C,GPU 超过并触发了硬件复位。
-
根本原因:
-
GPU 散热设计不良,局部热点达到 95°C。
-
系统没有为 GPU 配置独立的降频冷却设备。
-
临界阈值设定太低(90°C),而在 4K 视频下 GPU 温度经常高于 90°C。
-
-
解决方案:
-
在 DTS 中为 GPU 热区绑定
devfreq_cooling设备。 -
调整热区治理策略为
step_wise并增加降温台阶。 -
适当提高临界阈值到 100°C(结合芯片的耐受温度)。
-
改进 PCB 散热设计。
-
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| CPU (CPUFreq) | 高温时通过 cpufreq_cooling 降低 CPU 频率 |
降频策略响应是否及时 |
| GPU (DevFreq) | 高温时通过 devfreq_cooling 降低 GPU 频率 |
降频策略响应是否及时 |
| PMIC | 读取 PMIC 温度,以及热插拔控制 | PMIC 与 SoC 之间通信正常 |
| Clock Controller | 提供 TSADC 采样时钟 | 时钟频率影响采样精度 |
| Watchdog | 高温临界时可能触发硬件复位 | 在复位前记录日志 |
第六部分 Ethernet MAC 控制器
第一章 Ethernet MAC 在 Platform Bus 中的位置
Ethernet MAC(Media Access Controller)是 SoC 内部负责网络数据收发的核心模块。它将数据封装成以太网帧,通过 MII/RMII/RGMII 接口连接到外部 PHY 芯片(物理层收发器),实现网络通信。
在 Linux 中,网络子系统非常庞大,但核心分为三层:
-
网络核心层 (
net/core/):提供统一的net_device抽象和套接字接口。 -
MAC 控制器驱动(本篇文章重点):具体的 SoC 以太网控制器驱动(如
stmmac,dwmac,gmac)。 -
PHY 驱动:管理外部 PHY 芯片,通过 MDIO 总线配置。
第二章 Linux 5.10 典型 Ethernet MAC 驱动 —— dwmac-rockchip.c
Rockchip GMAC(Gigabit Ethernet MAC)是典型的基于 Synopsys DesignWare 内核的以太网控制器,广泛应用于 RK3399、RK3568 等平台。以下代码基于 Linux 5.10 drivers/net/ethernet/stmicro/stmmac/dwmac-rockchip.c,展示核心逻辑。
2.1 硬件关键概念
-
MAC 核心寄存器:控制发送/接收引擎、MAC 地址过滤、流控、VLAN 等。
-
DMA 引擎:支持双通道(发送 DMA 和接收 DMA),通过描述符链搬运数据。
-
外部接口:支持 MII/RMII/RGMII/GMII 多种 PHY 接口模式。
-
时钟:需要
stmmac_clk(MAC 主时钟)、pclk(总线时钟)、ptp_ref_clk(PTP 时钟)。 -
MDIO 接口:用于管理和配置外部 PHY 芯片。
2.2 核心代码
// 基于 Linux 5.10 drivers/net/ethernet/stmicro/stmmac/dwmac-rockchip.c
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/phy.h>
#include <linux/netdevice.h>
#include <linux/platform_device.h>
#include <linux/stmmac.h>
#include <linux/delay.h>
#include <linux/of_gpio.h>
#include <linux/of_net.h>
/**
* @brief Rockchip GMAC 控制器的私有数据结构。
*
* 对应图中 Platform Bus 上的 Ethernet MAC 控制器硬件实例。
*/
struct rockchip_dwmac {
void __iomem *base; /**< 映射后的寄存器基址 */
struct device *dev; /**< 设备指针 */
struct clk *stmmac_clk; /**< MAC 主时钟 */
struct clk *pclk; /**< 总线时钟 */
struct clk *ptp_ref_clk; /**< PTP 时钟 */
struct clk *clk_mac_speed; /**< RGMII 速度时钟 */
struct clk *clk_tx; /**< 发送时钟 */
struct clk *clk_rx; /**< 接收时钟 */
int phy_interface; /**< PHY 接口模式 (RGMII/RMII/MII) */
u32 tx_delay; /**< 发送延迟 (用于 RGMII 相位调整) */
u32 rx_delay; /**< 接收延迟 (用于 RGMII 相位调整) */
struct device_node *phy_node; /**< PHY 设备树节点 */
struct stmmac_resources res; /**< STMMAC 通用资源 */
};
/* 部分寄存器偏移量 (Rockchip 扩展) */
#define GMAC_GRP_CLK_CTRL 0x30
#define GMAC_GRP_RGMII_CTRL 0x34
#define GMAC_GRP_MDIO_CTRL 0x38
/**
* @brief 配置 RGMII 相位。
*
* 调整发送和接收时钟的相位,用于满足 RGMII 接口的时序要求。
*
* @param priv 指向 rockchip_dwmac 结构。
*/
static void rockchip_dwmac_set_rgmii_phase(struct rockchip_dwmac *priv)
{
u32 ctrl = readl(priv->base + GMAC_GRP_RGMII_CTRL);
// 清除旧的延迟配置
ctrl &= ~0x7F;
// 配置发送延迟 (tx_delay, 0~31)
ctrl |= (priv->tx_delay & 0x1F) << 2;
// 配置接收延迟 (rx_delay, 0~31)
ctrl |= (priv->rx_delay & 0x1F) << 7;
writel(ctrl, priv->base + GMAC_GRP_RGMII_CTRL);
}
/**
* @brief 初始化 PHY 接口。
*
* 根据 DTS 中的 phy-interface 配置 MAC 的接口模式。
*
* @param priv 指向 rockchip_dwmac 结构。
* @return 0 成功。
*/
static int rockchip_dwmac_init_phy(struct rockchip_dwmac *priv)
{
struct phy_device *phydev;
struct net_device *ndev;
int ret;
ndev = alloc_etherdev_mqs(sizeof(struct stmmac_priv), 1, 1);
if (!ndev)
return -ENOMEM;
// 1. 查找 PHY 设备 (通过 DTS 中的 phy-handle 或 phy-mode)
phydev = of_phy_connect(ndev, priv->phy_node, NULL, 0, priv->phy_interface);
if (!phydev) {
dev_err(priv->dev, "Failed to connect PHY\n");
free_netdev(ndev);
return -ENODEV;
}
// 2. 配置 PHY 速度 (全双工/自适应)
phy_set_max_speed(phydev, SPEED_1000);
phy_start_aneg(phydev);
priv->res.phy_node = priv->phy_node;
return 0;
}
/**
* @brief 设置网络设备接口参数。
*
* @param dev 指向 net_device。
* @param cmd 命令码。
* @param data 参数指针。
* @return 0 成功。
*/
static int rockchip_dwmac_ioctl(struct net_device *dev, struct ifreq *ifr, int cmd)
{
// 通常调用 STMMAC 核心的 ioctl
// ...
return 0;
}
/**
* @brief Platform 探测函数。
*
* 初始化 MAC 硬件,注册网络设备。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int rockchip_dwmac_probe(struct platform_device *pdev)
{
struct rockchip_dwmac *priv;
struct stmmac_resources *res;
struct resource *mem_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 资源
mem_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!mem_res) {
dev_err(&pdev->dev, "No memory resource\n");
return -ENXIO;
}
priv->base = devm_ioremap_resource(&pdev->dev, mem_res);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
// 3. 获取中断
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
// 4. 获取时钟
priv->stmmac_clk = devm_clk_get(&pdev->dev, "stmmac_clk");
if (IS_ERR(priv->stmmac_clk)) {
dev_err(&pdev->dev, "Failed to get stmmac_clk\n");
return PTR_ERR(priv->stmmac_clk);
}
priv->pclk = devm_clk_get(&pdev->dev, "pclk");
if (IS_ERR(priv->pclk)) {
dev_err(&pdev->dev, "Failed to get pclk\n");
return PTR_ERR(priv->pclk);
}
priv->ptp_ref_clk = devm_clk_get_optional(&pdev->dev, "ptp_ref_clk");
if (IS_ERR(priv->ptp_ref_clk))
return PTR_ERR(priv->ptp_ref_clk);
// 5. 使能时钟
clk_prepare_enable(priv->stmmac_clk);
clk_prepare_enable(priv->pclk);
if (priv->ptp_ref_clk)
clk_prepare_enable(priv->ptp_ref_clk);
// 6. 读取 DTS 配置 (phy-interface, tx/rx delay)
priv->phy_node = of_parse_phandle(pdev->dev.of_node, "phy-handle", 0);
if (!priv->phy_node) {
dev_err(&pdev->dev, "No phy-handle in DT\n");
ret = -ENODEV;
goto err_clk;
}
ret = of_get_phy_mode(pdev->dev.of_node, &priv->phy_interface);
if (ret) {
dev_err(&pdev->dev, "Failed to get phy-mode\n");
goto err_clk;
}
if (priv->phy_interface == PHY_INTERFACE_MODE_RGMII ||
priv->phy_interface == PHY_INTERFACE_MODE_RGMII_ID ||
priv->phy_interface == PHY_INTERFACE_MODE_RGMII_RXID ||
priv->phy_interface == PHY_INTERFACE_MODE_RGMII_TXID) {
// 读取 RGMII 延迟配置
of_property_read_u32(pdev->dev.of_node, "tx_delay", &priv->tx_delay);
of_property_read_u32(pdev->dev.of_node, "rx_delay", &priv->rx_delay);
// 配置 RGMII 相位
rockchip_dwmac_set_rgmii_phase(priv);
}
// 7. 设置通用的 STMMAC 资源
res = &priv->res;
res->addr = priv->base;
res->irq = irq;
res->pdev = pdev;
res->phy_node = priv->phy_node;
res->phy_interface = priv->phy_interface;
// 8. 调用 STMMAC 核心初始化网络设备
ret = stmmac_dwmac_probe(pdev, res);
if (ret) {
dev_err(&pdev->dev, "STMMAC probe failed\n");
goto err_clk;
}
dev_info(&pdev->dev, "Rockchip GMAC registered at 0x%llx, IRQ %d, %s\n",
(unsigned long long)mem_res->start, irq,
phy_modes(priv->phy_interface));
return 0;
err_clk:
clk_disable_unprepare(priv->stmmac_clk);
clk_disable_unprepare(priv->pclk);
if (priv->ptp_ref_clk)
clk_disable_unprepare(priv->ptp_ref_clk);
return ret;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int rockchip_dwmac_remove(struct platform_device *pdev)
{
struct rockchip_dwmac *priv = platform_get_drvdata(pdev);
// 1. 调用 STMMAC 核心移除
stmmac_dwmac_remove(pdev);
// 2. 禁用时钟
clk_disable_unprepare(priv->stmmac_clk);
clk_disable_unprepare(priv->pclk);
if (priv->ptp_ref_clk)
clk_disable_unprepare(priv->ptp_ref_clk);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id rockchip_dwmac_of_match[] = {
{ .compatible = "rockchip,rk3399-gmac" },
{ .compatible = "rockchip,rk3568-gmac" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, rockchip_dwmac_of_match);
/* Platform 驱动结构 */
static struct platform_driver rockchip_dwmac_driver = {
.probe = rockchip_dwmac_probe,
.remove = rockchip_dwmac_remove,
.driver = {
.name = "rockchip-dwmac",
.of_match_table = rockchip_dwmac_of_match,
},
};
module_platform_driver(rockchip_dwmac_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Rockchip GMAC Ethernet Controller Driver");
MODULE_LICENSE("GPL v2");
第三章 Ethernet MAC 调试核心难点
3.1 网络不通(无法连接)
现象:ip link set eth0 up 后,ifconfig 显示 Link 状态 DOWN,无法获取 IP。
原因:
-
PHY 芯片未被正确初始化和连接。
-
MAC 与 PHY 的接口模式(RGMII/RMII/MII)不匹配。
-
时钟未使能或频率错误。
调试方法:
-
检查 PHY 状态:
# 查看 MDIO 总线上的 PHY 设备 mdio-tool dump <bus> <addr> # 或使用 ethtool ethtool eth0
-
检查 PHY 接口模式:
ethtool eth0 | grep "Speed"
-
验证时钟频率:
cat /sys/kernel/debug/clk/clk_summary | grep gmac
-
检查 Reset 引脚:确保 PHY 的复位引脚被正确拉高。
3.2 丢包率高,网络卡顿
现象:iperf 测试带宽远低于理论值,ifconfig 显示大量的 rx dropped 或 tx dropped。
原因:
-
中断处理不及时。
-
内核缓冲区被占满。
-
DMA 描述符不足。
-
RGMII 相位没有正确调整。
调试方法:
-
查看网络统计:
netstat -i ethtool -S eth0
-
检查中断触发频率:
cat /proc/interrupts | grep gmac
-
使用
perf监控网络中断:perf record -e irq:* -p $(pgrep -f "stmmac") perf report
-
调整 RGMII 延迟:尝试通过 DTS 中的
tx_delay/rx_delay调整,通常 RGMII 对时序敏感。
3.3 PTP 无法同步(时间戳丢失)
现象:ptp4l 报告 No timestamp 或 bad timestamp。
原因:
-
PTP 时钟未使能或频率不正确。
-
硬件时间戳功能未启用。
-
中断处理中未读取时间戳寄存器。
调试方法:
-
检查 PTP 内核支持:
# 检查硬件时间戳能力 ethtool -T eth0
-
查看 PTP 中断:
cat /proc/interrupts | grep ptp
-
手动测试时间戳:
# 使用 phc2sys 查看时钟偏移 phc2sys -s eth0 -c CLOCK_REALTIME -m
3.4 网络唤醒(Wake-on-LAN)失败
现象:系统进入睡眠模式后,无法通过魔术包唤醒。
原因:
-
WOL 在 PHY 层未使能。
-
电源管理未正确配置。
-
MAC 或 PHY 进入了低功耗状态后无法响应。
调试方法:
-
检查 WOL 状态:
ethtool eth0 | grep Wake-on
-
使能 WOL:
ethtool -s eth0 wol g
-
检查电源管理:
# 确保 CONFIG_PM 配置正确 cat /sys/kernel/debug/pm_genpd/pm_genpd_summary
第四章 结合性能调试场景示例
场景:使用 iperf3 测试千兆以太网吞吐量,从 PC 发送到开发板,只能达到 400Mbps,实际硬件能力为 1Gbps。
分析流程:
-
宏观层面(图谱的 CPU 层):
-
top显示irq/gmac中断处理占用 CPU 约 20%。 -
perf top -G显示stmmac_napi_poll和dma_sync_single_for_cpu占用 CPU 超过 30%。
-
-
网络层查看(Ethernet MAC 层):
ethtool -S eth0
发现
rx_framework很高,说明接收 DMA 处理效率低下。 -
跟踪 DMA 事件(图谱的 DMA 控制器层):
trace-cmd record -e dma:* -e net:* -a -- timeout 5 trace-cmd report | grep stmmac
发现在
dma_sync_single_for_cpu上的耗时几乎占一次 NAPI 轮询的一半。 -
根本原因:
-
STMMAC 驱动默认使用
dma_map_single来映射每个接收数据包。每次 NAPI 轮询中,调用dma_sync_single_for_cpu将 DMA 缓冲区刷新到 CPU 地址空间,这会触发昂贵的缓存无效化操作。 -
流量越大,这种同步操作就越频繁。
-
-
解决方案:
-
切换使用
dma_alloc_coherent分配 RX 缓冲区,避免每次轮询都进行同步。 -
调整 NAPI 预算,使一次轮询处理更多数据包,减少每帧的中断开销。
-
将
ethtool -C eth0 rx-usecs的聚合时间调大,允许 DMA 在中断前收集更多数据包。 -
优化后,吞吐量可以提升到 900Mbps 以上。
-
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| DMA | 网络数据通过 DMA 传输到内存 | DMA 描述符链完整,中断延迟低 |
| PINCTRL | 配置 RGMII/RMII 引脚 | 引脚复用不冲突,信号完整性 |
| Clock | 提供 MAC 和 PHY 时钟 | 时钟频率和相位调整(RGMII 时序) |
| PHY Driver | 通过 MDIO 配置外 PHY 芯片 | PHY 上电和初始化时序 |
| PMIC | 提供 PHY 电源 | 电压正常,无噪声 |
第七部分 DWC3 USB 控制器
第一章 DWC3 USB 控制器在 Platform Bus 中的位置
DWC3 (DesignWare USB 3.0) 是 Synopsys 公司提供的 USB 控制器 IP,广泛用于 SoC 中,支持 USB 2.0 和 USB 3.0 (SuperSpeed)。它挂载在 Platform Bus 上,作为USB主机控制器,管理物理端口、设备连接、枚举和传输。
在 Linux 中,USB 子系统非常成熟,核心层次分为:
-
USB 核心层 (
drivers/usb/core/):提供通用的 USB API、hcd、urb、总线抽象。 -
DWC3 控制器驱动(本篇文章重点):基于
dwc3的 SoC 特定驱动。 -
PHY 驱动:负责 USB 物理层收发器(例如
usb3-phy-...)。 -
DMA 引擎:DWC3 自带 DMA 控制器。
第二章 Linux 5.10 典型 DWC3 USB 控制器驱动 —— dwc3-rockchip.c
以下代码基于 Linux 5.10 drivers/usb/dwc3/dwc3-rockchip.c,展示核心逻辑。
2.1 硬件关键概念
-
核心寄存器:配置 USB 速度、模式、电源管理。
-
事件缓冲区 (Event Buffer):DWC3 通过中断通知主机事件。
-
DMA 描述符:支持链式传输,用于批量数据传输。
-
PHY 接口:需要获取 USB 物理层(hs_phy, ss_phy, hs_phy_utmi, ss_phy_pipe 等)。
2.2 核心代码
// 基于 Linux 5.10 drivers/usb/dwc3/dwc3-rockchip.c (精简版)
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/platform_device.h>
#include <linux/usb/dwc3.h>
#include <linux/phy/phy.h>
/**
* @brief Rockchip DWC3 USB 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 USB Controller 硬件实例。
*/
struct rockchip_dwc3 {
struct device *dev; /**< 设备指针 */
struct clk *clk; /**< USB 核心时钟 */
struct clk *bus_clk; /**< 总线时钟(用于注册访问) */
struct phy *hs_phy; /**< USB 2.0 PHY */
struct phy *ss_phy; /**< USB 3.0 PHY */
struct usb_phy *usb2_phy; /**< USB 2.0 PHY(另一类抽象) */
struct dwc3 *dwc3; /**< 核心 DWC3 实例指针 */
u32 mode; /**< 模式:主机、设备、OTG */
u32 *event_buffer; /**< 事件缓冲区(用于中断处理) */
int irq; /**< DWC3 中断号 */
};
/**
* @brief 初始化 USB 控制器硬件。
*
* @param dwc3_rockchip 指向 rockchip_dwc3 结构。
* @return 0 成功。
*/
static int rockchip_dwc3_probe(struct platform_device *pdev)
{
struct rockchip_dwc3 *dwc3_rockchip;
struct resource *res;
int ret, irq;
// 1. 分配私有数据结构
dwc3_rockchip = devm_kzalloc(&pdev->dev, sizeof(*dwc3_rockchip), GFP_KERNEL);
if (!dwc3_rockchip)
return -ENOMEM;
platform_set_drvdata(pdev, dwc3_rockchip);
dwc3_rockchip->dev = &pdev->dev;
// 2. 获取时钟
dwc3_rockchip->clk = devm_clk_get(&pdev->dev, "ref_clk");
if (IS_ERR(dwc3_rockchip->clk))
return PTR_ERR(dwc3_rockchip->clk);
dwc3_rockchip->bus_clk = devm_clk_get_optional(&pdev->dev, "bus_clk");
if (IS_ERR(dwc3_rockchip->bus_clk))
return PTR_ERR(dwc3_rockchip->bus_clk);
clk_prepare_enable(dwc3_rockchip->clk);
if (dwc3_rockchip->bus_clk)
clk_prepare_enable(dwc3_rockchip->bus_clk);
// 3. 获取 PHY (USB 2.0 和 USB 3.0 物理层)
dwc3_rockchip->hs_phy = devm_phy_get(&pdev->dev, "usb2-phy");
if (IS_ERR(dwc3_rockchip->hs_phy)) {
dev_err(&pdev->dev, "Failed to get usb2-phy\n");
ret = PTR_ERR(dwc3_rockchip->hs_phy);
goto err_clk;
}
dwc3_rockchip->ss_phy = devm_phy_get_optional(&pdev->dev, "usb3-phy");
if (IS_ERR(dwc3_rockchip->ss_phy)) {
dev_err(&pdev->dev, "Failed to get usb3-phy\n");
ret = PTR_ERR(dwc3_rockchip->ss_phy);
goto err_clk;
}
// 4. 初始化 PHY
phy_init(dwc3_rockchip->hs_phy);
phy_power_on(dwc3_rockchip->hs_phy);
if (dwc3_rockchip->ss_phy) {
phy_init(dwc3_rockchip->ss_phy);
phy_power_on(dwc3_rockchip->ss_phy);
}
// 5. 读取 DTS 中的模式配置 (dr_mode)
dwc3_rockchip->mode = usb_get_dr_mode(&pdev->dev);
if (dwc3_rockchip->mode < 0) {
dev_err(&pdev->dev, "Failed to get dr_mode\n");
ret = -EINVAL;
goto err_phy;
}
// 6. 获取中断
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
ret = irq;
goto err_phy;
}
dwc3_rockchip->irq = irq;
// 7. 分配事件缓冲区 (中断处理时用于存放事件数据)
dwc3_rockchip->event_buffer = devm_kzalloc(&pdev->dev,
DWC3_EVENT_BUFFER_SIZE,
GFP_KERNEL);
if (!dwc3_rockchip->event_buffer) {
ret = -ENOMEM;
goto err_phy;
}
// 8. 分配核心 DWC3 结构
dwc3_rockchip->dwc3 = devm_dwc3_allocate(&pdev->dev);
if (IS_ERR(dwc3_rockchip->dwc3)) {
ret = PTR_ERR(dwc3_rockchip->dwc3);
goto err_phy;
}
// 9. 设置 DWC3 核心参数
dwc3_rockchip->dwc3->dev = &pdev->dev;
dwc3_rockchip->dwc3->mode = dwc3_rockchip->mode;
dwc3_rockchip->dwc3->hs_phy = dwc3_rockchip->hs_phy;
dwc3_rockchip->dwc3->ss_phy = dwc3_rockchip->ss_phy;
dwc3_rockchip->dwc3->event_buffer = dwc3_rockchip->event_buffer;
// 10. 注册 DWC3 核心
ret = dwc3_core_init(dwc3_rockchip->dwc3);
if (ret) {
dev_err(&pdev->dev, "Failed to initialize DWC3 core\n");
goto err_phy;
}
// 11. 注册 USB 控制器
ret = dwc3_core_register(dwc3_rockchip->dwc3);
if (ret) {
dev_err(&pdev->dev, "Failed to register DWC3 core\n");
goto err_phy;
}
dev_info(&pdev->dev, "Rockchip DWC3 USB controller registered\n");
return 0;
err_phy:
if (dwc3_rockchip->ss_phy) {
phy_power_off(dwc3_rockchip->ss_phy);
phy_exit(dwc3_rockchip->ss_phy);
}
phy_power_off(dwc3_rockchip->hs_phy);
phy_exit(dwc3_rockchip->hs_phy);
err_clk:
if (dwc3_rockchip->bus_clk)
clk_disable_unprepare(dwc3_rockchip->bus_clk);
clk_disable_unprepare(dwc3_rockchip->clk);
return ret;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int rockchip_dwc3_remove(struct platform_device *pdev)
{
struct rockchip_dwc3 *dwc3_rockchip = platform_get_drvdata(pdev);
// 1. 注销 USB 控制器
dwc3_core_unregister(dwc3_rockchip->dwc3);
// 2. 关闭 PHY
if (dwc3_rockchip->ss_phy) {
phy_power_off(dwc3_rockchip->ss_phy);
phy_exit(dwc3_rockchip->ss_phy);
}
phy_power_off(dwc3_rockchip->hs_phy);
phy_exit(dwc3_rockchip->hs_phy);
// 3. 禁用时钟
if (dwc3_rockchip->bus_clk)
clk_disable_unprepare(dwc3_rockchip->bus_clk);
clk_disable_unprepare(dwc3_rockchip->clk);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id rockchip_dwc3_of_match[] = {
{ .compatible = "rockchip,rk3399-dwc3" },
{ .compatible = "rockchip,rk3568-dwc3" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, rockchip_dwc3_of_match);
/* Platform 驱动结构 */
static struct platform_driver rockchip_dwc3_driver = {
.probe = rockchip_dwc3_probe,
.remove = rockchip_dwc3_remove,
.driver = {
.name = "rockchip-dwc3",
.of_match_table = rockchip_dwc3_of_match,
},
};
module_platform_driver(rockchip_dwc3_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Rockchip DWC3 USB Controller Driver");
MODULE_LICENSE("GPL v2");
第三章 USB 控制器调试核心难点
3.1 USB 设备无法识别
现象:插入 USB 设备,lsusb 显示无设备连接,内核无 usb 1-1: new high-speed USB device 日志。
原因:
-
PHY 供电或时钟未使能。
-
设备树中
dr_mode错误(应设为host或peripheral)。 -
VBUS 电源未开启。
调试方法:
-
检查 USB 核心状态:
cat /sys/kernel/debug/usb/devices
-
检查 PHY 状态:
# 查看 PHY 设备 ls /sys/class/phy/phy*
-
检查 VBUS 电源:
# 手动启用 VBUS 电源 echo 1 > /sys/devices/platform/soc/*usb-vbus/gpio/value
-
查看中断状态:
cat /proc/interrupts | grep dwc3
-
检查
dmesg日志:dmesg | grep dwc3 dmesg | grep usb
3.2 USB 3.0 降级为 2.0
现象:连接 USB 3.0 设备,速度显示为 480Mbps 而非 5Gbps。
原因:
-
外部 USB 3.0 PHY 初始化失败。
-
信号完整性差,无法建立超高速链路。
-
内核配置中未启用 USB 3.0 支持。
调试方法:
-
检查 USB 速度:
lsusb -t
-
查看 PHY 注册:
dmesg | grep usb3-phy
-
检查 USB 3.0 内核配置:
zcat /proc/config.gz | grep CONFIG_USB_XHCI_HCD
-
在
dwc3调试节点中查看:cat /sys/kernel/debug/dwc3/dwc3.0/regdump
3.3 通过 USB 传输数据丢包
现象:dd if=/dev/sda of=/dev/null bs=1M 出现大量 I/O errors。
原因:
-
DMA 描述符溢出。
-
USB 总线上的带宽不足。
-
中断处理被高优先级任务阻塞。
调试方法:
-
查看 USB 统计:
cat /sys/kernel/debug/usb/ehci/0000:00:14.0/registers
-
使用
perf监控中断处理延迟:perf record -e irq:irq_handler_exit -a -- timeout 10 perf script | grep dwc3
-
检查 DMA 描述符池:
cat /proc/dwc3/dwc3.0/descriptors
第四章 结合性能调试场景示例
场景:通过 USB 3.0 连接 U 盘拷贝大量文件时,速度仅在 10MB/s 左右,远低于理论值 400MB/s。
分析流程:
-
宏观层面(CPU 层):
-
iostat -x 1显示磁盘设备繁忙,但%util很高。 -
perf top -G显示dwc3_complete_trb和dwc3_ep_dequeue占用 CPU 较多。
-
-
USB 层(USB 控制器层):
cat /sys/kernel/debug/usb/devices
显示处于
SuperSpeed模式,说明 PHY 工作正常。 -
检查 DMA 配置(DMA 控制器层):
trace-cmd record -e dma:* -e usb:* -a -- timeout 5 trace-cmd report | grep dwc3
发现
dma_sync_single_for_cpu被频繁调用,导致大量缓存刷新。 -
根本原因:
-
DWC3 默认使用
dma_map_single映射每个 URB,对每个 TRB 都会进行dma_sync_single_for_cpu。 -
对于批量传输(bulk transfer),频繁同步操作严重影响性能。
-
同时发现中断频率很高,导致 CPU 忙于 ISR。
-
-
解决方案:
-
在
dwc3_ep_dequeue中优化同步逻辑,避免不必要的dma_sync_single_for_cpu。 -
增加
dwc3的 interrupt coalescing 设置,减少中断触发频率。 -
使用
dwc3-queue-buffer-size增加每 URB 的缓冲区大小。 -
验证后,拷贝速度提升到 300MB/s 以上。
-
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| PHY | 物理层收发器 | USB 2.0/3.0 PHY 正确初始化和上电 |
| DMA | 数据传输搬运 | DMA 描述符链和中断延迟 |
| GPIO | VBUS 电源控制 | VBUS 使能,电压检测 |
| Pinctrl | 控制 USB 引脚复用 | 确保引脚不被其他外设占用 |
| PMIC | 提供 USB 电源和电池充电检测 | 电源稳定,充电检测功能正确 |
第八部分 PCIe 控制器
第一章 PCIe 控制器在 Platform Bus 中的位置
PCIe (Peripheral Component Interconnect Express) 是一种高速串行总线,广泛用于连接高性能外设(如 WiFi 模块、SSD、GPU 等)。在嵌入式 SoC(如 Rockchip、Qualcomm、NXP 等)中,PCIe 控制器通常作为 Platform 设备挂载在内部总线上,并通过外部 PHY 与 PCIe 设备通信。
Linux 中 PCIe 子系统的核心层次:
-
PCI 核心层 (
drivers/pci/):管理 PCI 总线、设备、配置空间、电源管理、中断(MSI/MSI-X)。 -
PCIe 主机控制器驱动(本篇文章重点):具体的 SoC PCIe 控制器驱动(如
pcie-rockchip.c,pcie-designware.c)。 -
PCIe PHY 驱动:负责物理层信号调理和链路训练。
-
PCIe 设备驱动:具体 PCIe 设备(如 NVMe SSD、WiFi 卡)的驱动。
第二章 Linux 5.10 典型 PCIe 控制器驱动 —— pcie-rockchip.c
Rockchip PCIe 控制器基于 DesignWare PCIe 核心 IP,支持 PCIe 2.0/3.0,通常与外部 PHY 配合工作。以下代码基于 Linux 5.10 drivers/pci/controller/dwc/pcie-rockchip.c,展示核心逻辑。
2.1 硬件关键概念
-
DWC PCIe 核心:处理 PCIe 协议层、配置空间访问、链路管理。
-
应用程序层 (App Layer):Rockchip 特有的寄存器扩展,用于控制链路状态、复位、中断。
-
PHY 接口:通过 PIPE 接口与外部 PCIe PHY 芯片连接。
-
DMA 引擎:支持 TLP 数据传输。
-
中断:支持 MSI/MSI-X 和常规中断。
2.2 核心代码
// 基于 Linux 5.10 drivers/pci/controller/dwc/pcie-rockchip.c (精简版)
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/pci.h>
#include <linux/platform_device.h>
#include <linux/phy/phy.h>
#include <linux/reset.h>
#include <linux/delay.h>
#include <linux/of.h>
/**
* @brief Rockchip PCIe 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 PCIe Controller 硬件实例。
*/
struct rockchip_pcie {
struct dw_pcie pci; /**< DesignWare PCIe 核心结构 */
void __iomem *app_base; /**< 应用层寄存器基址 */
int irq; /**< PCIe 中断号 */
struct clk *aclk; /**< 总线时钟 (AXI) */
struct clk *aclk_perf; /**< 性能时钟 */
struct clk *hclk; /**< 主机时钟 */
struct clk *pmclk; /**< 电源管理时钟 */
struct phy *phy; /**< PCIe PHY 设备 */
struct reset_control *rst; /**< 复位控制器 */
struct device *dev; /**< 设备指针 */
struct pci_host_bridge *bridge; /**< PCI 主机桥 */
u32 link_gen; /**< 链路速度 (Gen1/Gen2/Gen3) */
u32 lanes; /**< 链路通道数 (1/2/4) */
};
/* Rockchip 应用层寄存器偏移 */
#define PCIE_APP_CTRL 0x00
#define PCIE_APP_CFG 0x04
#define PCIE_APP_INT_STAT 0x08
#define PCIE_APP_INT_EN 0x0C
#define PCIE_APP_LINK_STAT 0x10
#define PCIE_APP_AXI_CTRL 0x14
/**
* @brief PCIe 链路状态读取函数。
*
* @param pci 指向 rockchip_pcie 结构。
* @return 链路状态 (0=down, 1=up)
*/
static int rockchip_pcie_link_up(struct dw_pcie *pci)
{
struct rockchip_pcie *rockchip = to_rockchip_pcie(pci);
u32 stat;
// 读取应用层链路状态寄存器
stat = readl(rockchip->app_base + PCIE_APP_LINK_STAT);
// 检查链路状态位 (假设 bit 0 为链路状态)
return !!(stat & 0x1);
}
/**
* @brief 开始 PCIe 链路训练。
*
* @param pci 指向 dw_pcie 结构。
*/
static void rockchip_pcie_start_link(struct dw_pcie *pci)
{
struct rockchip_pcie *rockchip = to_rockchip_pcie(pci);
u32 ctrl;
// 1. 启用核心 PLL 和 PHY
phy_init(rockchip->phy);
phy_power_on(rockchip->phy);
// 2. 等待 PHY 稳定
msleep(100);
// 3. 配置应用层寄存器,启动链路训练
ctrl = readl(rockchip->app_base + PCIE_APP_CTRL);
ctrl |= (1 << 0); // 启用链路训练
writel(ctrl, rockchip->app_base + PCIE_APP_CTRL);
// 4. 等待链路建立 (最多 3 秒)
int i;
for (i = 0; i < 30; i++) {
if (rockchip_pcie_link_up(pci))
break;
msleep(100);
}
if (!rockchip_pcie_link_up(pci))
dev_err(rockchip->dev, "PCIe link training timeout\n");
}
/**
* @brief 停止 PCIe 链路。
*
* @param pci 指向 dw_pcie 结构。
*/
static void rockchip_pcie_stop_link(struct dw_pcie *pci)
{
struct rockchip_pcie *rockchip = to_rockchip_pcie(pci);
phy_power_off(rockchip->phy);
phy_exit(rockchip->phy);
}
/**
* @brief 初始化 PCIe 控制器硬件。
*
* @param rockchip 指向 rockchip_pcie 结构。
*/
static int rockchip_pcie_hw_init(struct rockchip_pcie *rockchip)
{
// 1. 启用时钟
clk_prepare_enable(rockchip->aclk);
clk_prepare_enable(rockchip->aclk_perf);
clk_prepare_enable(rockchip->hclk);
clk_prepare_enable(rockchip->pmclk);
// 2. 复位 PCIe 控制器
reset_control_assert(rockchip->rst);
udelay(10);
reset_control_deassert(rockchip->rst);
msleep(100);
// 3. 配置链路速度 (Gen2/Gen3)
// 在 DWC 核心配置寄存器中设置
dw_pcie_setup_rc(&rockchip->pci);
// 4. 初始化 PHY
phy_init(rockchip->phy);
phy_power_on(rockchip->phy);
// 5. 启动链路训练
rockchip_pcie_start_link(&rockchip->pci);
return 0;
}
/**
* @brief PCIe 中断处理函数。
*
* 处理链路错误、PME (电源管理事件)、MSI 等。
*
* @param irq 中断号。
* @param dev_id 指向 rockchip_pcie 结构。
*/
static irqreturn_t rockchip_pcie_irq_handler(int irq, void *dev_id)
{
struct rockchip_pcie *rockchip = dev_id;
u32 int_stat, int_en;
// 1. 读取中断状态
int_stat = readl(rockchip->app_base + PCIE_APP_INT_STAT);
int_en = readl(rockchip->app_base + PCIE_APP_INT_EN);
int_stat &= int_en;
// 2. 处理链路错误 (Link error)
if (int_stat & (1 << 0)) {
dev_err(rockchip->dev, "PCIe link error detected\n");
// 清除中断
writel(1 << 0, rockchip->app_base + PCIE_APP_INT_STAT);
// 可能需要重新训练链路
rockchip_pcie_start_link(&rockchip->pci);
}
// 3. 处理电源管理事件 (PME)
if (int_stat & (1 << 1)) {
dev_info(rockchip->dev, "PCIe PME event received\n");
// 唤醒可能处于睡眠状态的设备
writel(1 << 1, rockchip->app_base + PCIE_APP_INT_STAT);
}
// 4. 处理其他中断 (MSI 等)
// DWC 核心会处理 MSI/MSI-X 中断
return IRQ_HANDLED;
}
/**
* @brief Platform 探测函数。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int rockchip_pcie_probe(struct platform_device *pdev)
{
struct rockchip_pcie *rockchip;
struct dw_pcie *pci;
struct resource *res;
int ret, irq;
// 1. 分配私有数据结构
rockchip = devm_kzalloc(&pdev->dev, sizeof(*rockchip), GFP_KERNEL);
if (!rockchip)
return -ENOMEM;
platform_set_drvdata(pdev, rockchip);
rockchip->dev = &pdev->dev;
pci = &rockchip->pci;
pci->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;
}
pci->dbi_base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(pci->dbi_base))
return PTR_ERR(pci->dbi_base);
// 3. 获取应用层寄存器 (Rockchip 扩展)
res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
if (!res) {
dev_err(&pdev->dev, "No app memory resource\n");
return -ENXIO;
}
rockchip->app_base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(rockchip->app_base))
return PTR_ERR(rockchip->app_base);
// 4. 获取中断
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
rockchip->irq = irq;
// 5. 获取时钟
rockchip->aclk = devm_clk_get(&pdev->dev, "aclk");
if (IS_ERR(rockchip->aclk))
return PTR_ERR(rockchip->aclk);
rockchip->aclk_perf = devm_clk_get(&pdev->dev, "aclk_perf");
if (IS_ERR(rockchip->aclk_perf))
return PTR_ERR(rockchip->aclk_perf);
rockchip->hclk = devm_clk_get(&pdev->dev, "hclk");
if (IS_ERR(rockchip->hclk))
return PTR_ERR(rockchip->hclk);
rockchip->pmclk = devm_clk_get_optional(&pdev->dev, "pmclk");
if (IS_ERR(rockchip->pmclk))
return PTR_ERR(rockchip->pmclk);
// 6. 获取 PHY
rockchip->phy = devm_phy_get(&pdev->dev, "pcie-phy");
if (IS_ERR(rockchip->phy))
return PTR_ERR(rockchip->phy);
// 7. 获取复位
rockchip->rst = devm_reset_control_get(&pdev->dev, "pcie");
if (IS_ERR(rockchip->rst))
return PTR_ERR(rockchip->rst);
// 8. 读取 DTS 参数 (链路速度、通道数)
of_property_read_u32(pdev->dev.of_node, "link-gen", &rockchip->link_gen);
of_property_read_u32(pdev->dev.of_node, "lanes", &rockchip->lanes);
// 9. 初始化 DWC PCIe 核心
pci->ops = &rockchip_pcie_ops;
pci->link_up = rockchip_pcie_link_up;
pci->start_link = rockchip_pcie_start_link;
pci->stop_link = rockchip_pcie_stop_link;
pci->num_lanes = rockchip->lanes;
// 10. 硬件初始化
ret = rockchip_pcie_hw_init(rockchip);
if (ret) {
dev_err(&pdev->dev, "Failed to initialize hardware\n");
return ret;
}
// 11. 注册中断
ret = devm_request_irq(&pdev->dev, rockchip->irq, rockchip_pcie_irq_handler,
IRQF_SHARED, "rockchip-pcie", rockchip);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
return ret;
}
// 12. 建立 PCIe 总线并注册主机桥
rockchip->bridge = devm_pci_alloc_host_bridge(&pdev->dev, sizeof(*rockchip));
if (!rockchip->bridge)
return -ENOMEM;
// 填充主机桥参数
rockchip->bridge->ops = &rockchip_pcie_host_ops;
rockchip->bridge->busnr = 0;
rockchip->bridge->dev.parent = &pdev->dev;
ret = pci_host_bridge_register(rockchip->bridge);
if (ret) {
dev_err(&pdev->dev, "Failed to register host bridge\n");
return ret;
}
dev_info(&pdev->dev, "Rockchip PCIe controller registered at 0x%llx, IRQ %d, Gen%d, %d lanes\n",
(unsigned long long)res->start, rockchip->irq, rockchip->link_gen, rockchip->lanes);
return 0;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int rockchip_pcie_remove(struct platform_device *pdev)
{
struct rockchip_pcie *rockchip = platform_get_drvdata(pdev);
// 1. 停止链路
rockchip_pcie_stop_link(&rockchip->pci);
// 2. 禁用时钟
if (rockchip->pmclk)
clk_disable_unprepare(rockchip->pmclk);
clk_disable_unprepare(rockchip->hclk);
clk_disable_unprepare(rockchip->aclk_perf);
clk_disable_unprepare(rockchip->aclk);
// 3. 复位控制器
reset_control_assert(rockchip->rst);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id rockchip_pcie_of_match[] = {
{ .compatible = "rockchip,rk3399-pcie" },
{ .compatible = "rockchip,rk3568-pcie" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, rockchip_pcie_of_match);
/* Platform 驱动结构 */
static struct platform_driver rockchip_pcie_driver = {
.probe = rockchip_pcie_probe,
.remove = rockchip_pcie_remove,
.driver = {
.name = "rockchip-pcie",
.of_match_table = rockchip_pcie_of_match,
},
};
module_platform_driver(rockchip_pcie_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Rockchip PCIe Controller Driver");
MODULE_LICENSE("GPL v2");
第三章 PCIe 调试核心难点
3.1 链路训练失败 (Link training failed)
现象:PCIe 设备无法识别,lspci 无输出,dmesg 显示 "PCIe link training timeout"。
原因:
-
PHY 初始化失败或信号质量差。
-
时钟不稳定 (REFCLK 缺失或漂移)。
-
电源电压不稳。
-
设备树配置错误 (lane 数量、速度不匹配)。
调试方法:
-
检查 PHY 状态:
# 查看 PHY 寄存器 devmem2 <phy_base_addr> # 检查 PHY 锁定状态 (通常有状态寄存器)
-
检查时钟:
cat /sys/kernel/debug/clk/clk_summary | grep pcie
-
强制链路训练:
# 使用 devmem2 强制启用链路训练 devmem2 <app_base+0x00> 0x01
-
查看链路状态寄存器:
devmem2 <app_base+0x10> # 读取链路状态
-
降低链路速度:在 DTS 中设置
link-gen = <1>(Gen1),排除高速信号问题。
3.2 设备枚举后无法访问配置空间
现象:lspci 能列出设备,但访问设备时 (lspci -v) 报告 "Configuration space access failed"。
原因:
-
配置空间映射错误。
-
设备没有正确响应配置请求。
-
链路状态不稳定。
调试方法:
-
测试配置空间访问:
# 使用 setpci 手动读取配置空间 setpci -s 00:00.0 COMMAND
-
检查总线设备映射:
cat /sys/bus/pci/devices/0000:00:00.0/device
-
检查 PCIe 核心配置:
cat /sys/kernel/debug/pci/0000:00:00.0/config
3.3 MSI/MSI-X 中断不工作
现象:设备驱动能加载,但中断处理函数从不被调用。
原因:
-
MSI/MSI-X 使能未设置。
-
中断控制器 (GIC) 未正确映射 MSI 中断。
-
设备未配置 MSI/MSI-X 能力。
调试方法:
-
检查 MSI 使能:
# 查看设备的 MSI 能力 setpci -s 00:00.0 MSI_CTRL
-
查看 MSI 中断映射:
cat /proc/interrupts | grep pci
-
强制使用传统中断:
# 在驱动中禁用 MSI pci_disable_msi(pdev)
3.4 PCIe 链路不稳定性 (PCIe Link flapping)
现象:PCIe 设备反复断开重连,dmesg 显示 "PCIe link down" 然后 "PCIe link up"。
原因:
-
信号完整性差。
-
电源噪声。
-
温度变化导致链路不稳定。
调试方法:
-
查看链路状态统计:
cat /sys/kernel/debug/dwc3/dwc3.0/regdump
-
降低链路速度:尝试使用 Gen1。
-
检查 PHY 信号质量:使用示波器查看 PCIe 差分信号。
第四章 结合性能调试场景示例
场景:通过 NVMe SSD 读取大文件时,性能只有 200MB/s,预期达到 3GB/s。
分析流程:
-
宏观层面(图谱的 CPU 和 I/O 层):
-
iostat -x 1显示nvme0n1的%util一直 100%,但吞吐量远低于预期。 -
perf top -G显示rockchip_pcie_irq_handler和dma_sync_single_for_cpu占用大量 CPU。
-
-
PCIe 层(图谱的 Device Drivers -> PCIe Controller 层):
lspci -vv -s 01:00.0
发现设备支持 PCIe Gen3 x4,但当前只工作在 Gen2 x2。
-
跟踪 DMA 事件(DMA 控制器层):
trace-cmd record -e pci:* -e dma:* -a -- timeout 5 trace-cmd report | grep -E "pcie|dma"
发现 DMA 中断频繁,中断处理延迟高。
-
根本原因:
-
链路工作在 Gen2 x2 模式,理论带宽约 1000MB/s,实际带宽远低于理论值。
-
NVMe 驱动使用大量的小块 IO,每次 IO 都触发 PCIe 中断,中断处理占用了 CPU 资源。
-
同时
dma_sync_single_for_cpu被频繁调用,造成缓存刷新开销。
-
-
解决方案:
-
在 DTS 中强制链路速度为 Gen3 x4。
-
调整 IO 合并策略,合并小 IO 为大块 IO。
-
增加 IO 队列深度 (nr_requests) 和请求合并 (merge).
-
使用
nvme_core.io_timeout=100设置更长的超时。 -
优化后,吞吐量提升到 2.8GB/s。
-
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| PHY | 物理层收发器 | 信号质量、链路训练、速度协商 |
| DMA | 数据传输搬运 | DMA 描述符链、中断聚合 |
| GPIO | 设备复位、电源使能 | 复位时序,电平电压 |
| PMC | 电源管理、链路状态 | 链路休眠恢复、PME 唤醒 |
| Clock Controller | 提供 REFCLK 和总线时钟 | 时钟稳定性与链路速率匹配 |
第九部分 NAND 控制器
第一章 NAND 控制器在 Platform Bus 中的位置
NAND 控制器(NAND Flash Controller)是 SoC 内部用于管理 NAND Flash 存储芯片的硬件模块。它负责处理 NAND Flash 的命令序列、地址控制、数据读写(包括 ECC 校验)、坏块管理等复杂逻辑。NAND 控制器通常通过 Platform Bus 挂载在 SoC 内部总线上。
Linux 中 NAND 子系统的核心架构分为三层:
-
NAND 核心层 (
drivers/mtd/nand/): 提供通用的 NAND 芯片抽象 (struct nand_chip),处理 ECC、坏块管理、BBT (Bad Block Table) 等。 -
NAND 控制器驱动(本篇文章重点):具体的 SoC NAND 控制器驱动(如
sunxi_nand.c、gpmi-nand.c、mxc_nand.c)。 -
MTD 层 (
drivers/mtd/): 将 NAND Flash 抽象为/dev/mtdX字符设备和/dev/mtdblockX块设备,供文件系统(UBIFS、JFFS2)使用。
第二章 Linux 5.10 典型 NAND 控制器驱动 —— sunxi_nand.c
Allwinner (SUNXI) NAND 控制器是典型的挂载在 Platform Bus 上的 NAND Flash 控制器,广泛用于 H3、A64、H6 等 SoC。它支持 SLC 和 MLC NAND,内置硬件 ECC (BCH 或 Hamming),并支持 DMA 数据传输。以下代码基于 Linux 5.10 drivers/mtd/nand/raw/sunxi_nand.c,展示核心逻辑。
2.1 硬件关键概念
-
NAND 命令寄存器:用于发送命令、地址、数据。
-
DMA 引擎:支持通过 DMA 进行数据传输,提高性能。
-
ECC 引擎:内置 BCH 或 Hamming ECC 硬件,用于错误检测和纠正。
-
时钟:通常需要
ahb_clk(总线时钟)、mod_clk(NAND 模块时钟)。 -
中断:处理 DMA 完成、ECC 错误等事件。
2.2 核心代码
// 基于 Linux 5.10 drivers/mtd/nand/raw/sunxi_nand.c (精简版)
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/dmaengine.h>
#include <linux/mtd/mtd.h>
#include <linux/mtd/nand.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
#include <linux/delay.h>
/**
* @brief Allwinner NAND 控制器的私有数据结构。
*
* 对应于图中 Platform Bus 上的 NAND Controller 硬件实例。
*/
struct sunxi_nand {
void __iomem *base; /**< 映射后的寄存器基址 */
int irq; /**< NAND 中断号 */
struct clk *ahb_clk; /**< AHB 总线时钟 */
struct clk *mod_clk; /**< 模块时钟 */
struct dma_chan *dma_tx; /**< DMA 发送通道 */
struct dma_chan *dma_rx; /**< DMA 接收通道 */
struct nand_chip nand; /**< NAND 芯片抽象 */
struct mtd_info mtd; /**< MTD 设备抽象 */
struct device *dev; /**< 设备指针 */
spinlock_t lock; /**< 硬件保护锁 */
u8 *buffer; /**< 数据缓存 */
dma_addr_t buffer_dma; /**< 缓存 DMA 地址 */
int ecc_strength; /**< ECC 强度 (4/8/16/24/32/40/48/56/60/64) */
int ecc_size; /**< ECC 步长 (512/1024) */
};
/* 寄存器偏移量 (Allwinner NAND) */
#define NAND_CMD_REG 0x00
#define NAND_ADDR_REG 0x04
#define NAND_DATA_REG 0x08
#define NAND_CTL_REG 0x0C
#define NAND_STAT_REG 0x10
#define NAND_INT_STAT_REG 0x14
#define NAND_INT_EN_REG 0x18
#define NAND_DMA_CTL_REG 0x1C
#define NAND_ECC_CTL_REG 0x20
#define NAND_ECC_STAT_REG 0x24
#define NAND_DMA_ADDR_REG 0x28
/**
* @brief 发送 NAND 命令。
*
* @param nand 指向 nand_chip 结构。
* @param cmd 命令码。
*/
static void sunxi_nand_cmd(struct nand_chip *nand, u8 cmd)
{
struct sunxi_nand *sunxi_nand = container_of(nand, struct sunxi_nand, nand);
unsigned long flags;
spin_lock_irqsave(&sunxi_nand->lock, flags);
// 写入命令寄存器
writel(cmd, sunxi_nand->base + NAND_CMD_REG);
// 等待命令完成 (状态寄存器相应位)
while (!(readl(sunxi_nand->base + NAND_STAT_REG) & (1 << 0)))
;
spin_unlock_irqrestore(&sunxi_nand->lock, flags);
}
/**
* @brief 发送 NAND 地址。
*
* @param nand 指向 nand_chip 结构。
* @param addr 地址值。
*/
static void sunxi_nand_addr(struct nand_chip *nand, u8 addr)
{
struct sunxi_nand *sunxi_nand = container_of(nand, struct sunxi_nand, nand);
writel(addr, sunxi_nand->base + NAND_ADDR_REG);
}
/**
* @brief 从 NAND 读取一个字节。
*
* @param nand 指向 nand_chip 结构。
* @return 读取的字节。
*/
static u8 sunxi_nand_read_byte(struct nand_chip *nand)
{
struct sunxi_nand *sunxi_nand = container_of(nand, struct sunxi_nand, nand);
return readl(sunxi_nand->base + NAND_DATA_REG) & 0xFF;
}
/**
* @brief 向 NAND 写入一个字节。
*
* @param nand 指向 nand_chip 结构。
* @param data 要写入的字节。
*/
static void sunxi_nand_write_byte(struct nand_chip *nand, u8 data)
{
struct sunxi_nand *sunxi_nand = container_of(nand, struct sunxi_nand, nand);
writel(data, sunxi_nand->base + NAND_DATA_REG);
}
/**
* @brief 读取 NAND 状态。
*
* @param nand 指向 nand_chip 结构。
* @return 状态字节。
*/
static u8 sunxi_nand_status(struct nand_chip *nand)
{
struct sunxi_nand *sunxi_nand = container_of(nand, struct sunxi_nand, nand);
return readl(sunxi_nand->base + NAND_STAT_REG) & 0xFF;
}
/**
* @brief 读取 NAND 芯片 ID。
*
* @param nand 指向 nand_chip 结构。
* @param id 输出 ID 缓冲区。
* @param len 读取的长度。
*/
static void sunxi_nand_read_id(struct nand_chip *nand, u8 *id, int len)
{
struct sunxi_nand *sunxi_nand = container_of(nand, struct sunxi_nand, nand);
int i;
for (i = 0; i < len; i++) {
id[i] = readl(sunxi_nand->base + NAND_DATA_REG) & 0xFF;
}
}
/**
* @brief 通过 DMA 读取数据页。
*
* @param nand 指向 nand_chip 结构。
* @param data 数据缓冲区。
* @param len 数据长度。
*/
static int sunxi_nand_dma_read(struct nand_chip *nand, u8 *data, int len)
{
struct sunxi_nand *sunxi_nand = container_of(nand, struct sunxi_nand, nand);
struct dma_async_tx_descriptor *tx;
dma_addr_t dma_addr;
int ret = 0;
// 1. 映射用户缓冲区到 DMA 地址
dma_addr = dma_map_single(sunxi_nand->dev, data, len, DMA_FROM_DEVICE);
if (dma_mapping_error(sunxi_nand->dev, dma_addr)) {
dev_err(sunxi_nand->dev, "Failed to map DMA buffer\n");
return -ENOMEM;
}
// 2. 配置 DMA 接收通道
struct dma_slave_config config;
memset(&config, 0, sizeof(config));
config.direction = DMA_DEV_TO_MEM;
config.src_addr = (dma_addr_t)sunxi_nand->base + NAND_DATA_REG;
config.src_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
config.src_maxburst = 32;
dmaengine_slave_config(sunxi_nand->dma_rx, &config);
// 3. 创建 DMA 描述符
tx = dmaengine_prep_slave_single(sunxi_nand->dma_rx, dma_addr, len,
DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT);
if (!tx) {
dev_err(sunxi_nand->dev, "Failed to prepare DMA descriptor\n");
dma_unmap_single(sunxi_nand->dev, dma_addr, len, DMA_FROM_DEVICE);
return -ENOMEM;
}
// 4. 启动 DMA
dmaengine_submit(tx);
dma_async_issue_pending(sunxi_nand->dma_rx);
// 5. 等待 DMA 完成
wait_for_completion(&sunxi_nand->dma_complete);
// 6. 取消映射
dma_unmap_single(sunxi_nand->dev, dma_addr, len, DMA_FROM_DEVICE);
return ret;
}
/**
* @brief 通过 DMA 写入数据页。
*
* @param nand 指向 nand_chip 结构。
* @param data 数据缓冲区。
* @param len 数据长度。
*/
static int sunxi_nand_dma_write(struct nand_chip *nand, u8 *data, int len)
{
struct sunxi_nand *sunxi_nand = container_of(nand, struct sunxi_nand, nand);
struct dma_async_tx_descriptor *tx;
dma_addr_t dma_addr;
int ret = 0;
// 1. 映射用户缓冲区到 DMA 地址
dma_addr = dma_map_single(sunxi_nand->dev, data, len, DMA_TO_DEVICE);
if (dma_mapping_error(sunxi_nand->dev, dma_addr)) {
dev_err(sunxi_nand->dev, "Failed to map DMA buffer\n");
return -ENOMEM;
}
// 2. 配置 DMA 发送通道
struct dma_slave_config config;
memset(&config, 0, sizeof(config));
config.direction = DMA_MEM_TO_DEV;
config.dst_addr = (dma_addr_t)sunxi_nand->base + NAND_DATA_REG;
config.dst_addr_width = DMA_SLAVE_BUSWIDTH_1_BYTE;
config.dst_maxburst = 32;
dmaengine_slave_config(sunxi_nand->dma_tx, &config);
// 3. 创建 DMA 描述符
tx = dmaengine_prep_slave_single(sunxi_nand->dma_tx, dma_addr, len,
DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT);
if (!tx) {
dev_err(sunxi_nand->dev, "Failed to prepare DMA descriptor\n");
dma_unmap_single(sunxi_nand->dev, dma_addr, len, DMA_TO_DEVICE);
return -ENOMEM;
}
// 4. 启动 DMA
dmaengine_submit(tx);
dma_async_issue_pending(sunxi_nand->dma_tx);
// 5. 等待 DMA 完成
wait_for_completion(&sunxi_nand->dma_complete);
// 6. 取消映射
dma_unmap_single(sunxi_nand->dev, dma_addr, len, DMA_TO_DEVICE);
return ret;
}
/**
* @brief 中断处理函数。
*
* 处理 DMA 完成、ECC 错误等中断事件。
*
* @param irq 中断号。
* @param dev_id 指向 sunxi_nand 结构。
*/
static irqreturn_t sunxi_nand_irq_handler(int irq, void *dev_id)
{
struct sunxi_nand *sunxi_nand = dev_id;
u32 int_stat;
// 1. 读取中断状态
int_stat = readl(sunxi_nand->base + NAND_INT_STAT_REG);
// 2. 处理 DMA 完成中断
if (int_stat & (1 << 0)) {
// 清除中断
writel(1 << 0, sunxi_nand->base + NAND_INT_STAT_REG);
// 通知等待 DMA 完成的线程
complete(&sunxi_nand->dma_complete);
}
// 3. 处理 ECC 错误中断
if (int_stat & (1 << 1)) {
// 读取 ECC 状态
u32 ecc_stat = readl(sunxi_nand->base + NAND_ECC_STAT_REG);
dev_err(sunxi_nand->dev, "ECC error detected: 0x%x\n", ecc_stat);
// 清除中断
writel(1 << 1, sunxi_nand->base + NAND_INT_STAT_REG);
}
// 4. 处理其他错误中断
if (int_stat & (1 << 2)) {
dev_err(sunxi_nand->dev, "NAND controller error\n");
writel(1 << 2, sunxi_nand->base + NAND_INT_STAT_REG);
}
return IRQ_HANDLED;
}
/**
* @brief 初始化 ECC 硬件。
*
* @param sunxi_nand 指向 sunxi_nand 结构。
*/
static void sunxi_nand_ecc_init(struct sunxi_nand *sunxi_nand)
{
u32 ecc_ctl;
// 1. 配置 ECC 强度 (从 DTS 读取或根据 NAND 类型自动选择)
sunxi_nand->ecc_strength = 8;
sunxi_nand->ecc_size = 512;
// 2. 配置 ECC 控制寄存器
ecc_ctl = (sunxi_nand->ecc_strength << 8) | (sunxi_nand->ecc_size << 0);
writel(ecc_ctl, sunxi_nand->base + NAND_ECC_CTL_REG);
}
/**
* @brief Platform 探测函数。
*
* 初始化 NAND 控制器硬件,注册 NAND 设备。
*
* @param pdev Platform 设备指针。
* @return 0 成功,负数错误。
*/
static int sunxi_nand_probe(struct platform_device *pdev)
{
struct sunxi_nand *sunxi_nand;
struct nand_chip *nand;
struct mtd_info *mtd;
struct resource *res;
int ret, irq;
// 1. 分配私有数据结构
sunxi_nand = devm_kzalloc(&pdev->dev, sizeof(*sunxi_nand), GFP_KERNEL);
if (!sunxi_nand)
return -ENOMEM;
platform_set_drvdata(pdev, sunxi_nand);
sunxi_nand->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;
}
sunxi_nand->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(sunxi_nand->base))
return PTR_ERR(sunxi_nand->base);
// 3. 获取中断
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
sunxi_nand->irq = irq;
// 4. 获取时钟
sunxi_nand->ahb_clk = devm_clk_get(&pdev->dev, "ahb");
if (IS_ERR(sunxi_nand->ahb_clk))
return PTR_ERR(sunxi_nand->ahb_clk);
sunxi_nand->mod_clk = devm_clk_get(&pdev->dev, "mod");
if (IS_ERR(sunxi_nand->mod_clk))
return PTR_ERR(sunxi_nand->mod_clk);
clk_prepare_enable(sunxi_nand->ahb_clk);
clk_prepare_enable(sunxi_nand->mod_clk);
// 5. 获取 DMA 通道
sunxi_nand->dma_tx = dma_request_chan(sunxi_nand->dev, "tx");
if (IS_ERR(sunxi_nand->dma_tx)) {
dev_err(sunxi_nand->dev, "Failed to get TX DMA channel\n");
ret = PTR_ERR(sunxi_nand->dma_tx);
goto err_clk;
}
sunxi_nand->dma_rx = dma_request_chan(sunxi_nand->dev, "rx");
if (IS_ERR(sunxi_nand->dma_rx)) {
dev_err(sunxi_nand->dev, "Failed to get RX DMA channel\n");
ret = PTR_ERR(sunxi_nand->dma_rx);
goto err_dma_tx;
}
// 6. 初始化 DMA 完成量
init_completion(&sunxi_nand->dma_complete);
spin_lock_init(&sunxi_nand->lock);
// 7. 注册中断
ret = devm_request_irq(&pdev->dev, sunxi_nand->irq, sunxi_nand_irq_handler,
IRQF_SHARED, "sunxi-nand", sunxi_nand);
if (ret) {
dev_err(&pdev->dev, "Failed to request IRQ\n");
goto err_dma_rx;
}
// 8. 初始化 NAND 芯片结构
nand = &sunxi_nand->nand;
mtd = nand_to_mtd(nand);
mtd->dev.parent = &pdev->dev;
mtd->name = "sunxi-nand";
mtd->type = MTD_NANDFLASH;
// 设置 NAND 操作回调
nand->cmd = sunxi_nand_cmd;
nand->addr = sunxi_nand_addr;
nand->read_byte = sunxi_nand_read_byte;
nand->write_byte = sunxi_nand_write_byte;
nand->read_id = sunxi_nand_read_id;
nand->status = sunxi_nand_status;
// 9. 配置 ECC
sunxi_nand_ecc_init(sunxi_nand);
// 10. 扫描 NAND 芯片
ret = nand_scan(nand, 1);
if (ret) {
dev_err(&pdev->dev, "Failed to scan NAND chip\n");
goto err_irq;
}
// 11. 注册 MTD 设备
ret = mtd_device_register(mtd, NULL, 0);
if (ret) {
dev_err(&pdev->dev, "Failed to register MTD device\n");
goto err_nand_cleanup;
}
dev_info(&pdev->dev, "Sunxi NAND controller registered at 0x%llx, IRQ %d\n",
(unsigned long long)res->start, sunxi_nand->irq);
return 0;
err_nand_cleanup:
nand_cleanup(nand);
err_irq:
devm_free_irq(&pdev->dev, sunxi_nand->irq, sunxi_nand);
err_dma_rx:
dma_release_channel(sunxi_nand->dma_rx);
err_dma_tx:
dma_release_channel(sunxi_nand->dma_tx);
err_clk:
clk_disable_unprepare(sunxi_nand->mod_clk);
clk_disable_unprepare(sunxi_nand->ahb_clk);
return ret;
}
/**
* @brief 移除函数。
*
* @param pdev Platform 设备指针。
*/
static int sunxi_nand_remove(struct platform_device *pdev)
{
struct sunxi_nand *sunxi_nand = platform_get_drvdata(pdev);
struct nand_chip *nand = &sunxi_nand->nand;
// 1. 注销 MTD 设备
mtd_device_unregister(nand_to_mtd(nand));
// 2. 清理 NAND 芯片
nand_cleanup(nand);
// 3. 释放 DMA 通道
dma_release_channel(sunxi_nand->dma_rx);
dma_release_channel(sunxi_nand->dma_tx);
// 4. 禁用时钟
clk_disable_unprepare(sunxi_nand->mod_clk);
clk_disable_unprepare(sunxi_nand->ahb_clk);
return 0;
}
/* 设备树匹配表 */
static const struct of_device_id sunxi_nand_of_match[] = {
{ .compatible = "allwinner,sun8i-a23-nand-controller" },
{ .compatible = "allwinner,sun8i-h3-nand-controller" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, sunxi_nand_of_match);
/* Platform 驱动结构 */
static struct platform_driver sunxi_nand_driver = {
.probe = sunxi_nand_probe,
.remove = sunxi_nand_remove,
.driver = {
.name = "sunxi-nand",
.of_match_table = sunxi_nand_of_match,
},
};
module_platform_driver(sunxi_nand_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Allwinner NAND Controller Driver");
MODULE_LICENSE("GPL v2");
第三章 NAND 调试核心难点
3.1 NAND 芯片无法识别
现象:cat /proc/mtd 显示为空,dmesg 无 NAND 设备输出。
原因:
-
NAND 时序配置错误(不同厂商的 NAND 需要不同的时序)。
-
电源未使能。
-
控制器初始化失败。
调试方法:
-
检查 NAND 芯片 ID:
# 手动读取 NAND ID devmem2 <base_addr>+0x08 # 读取 ID 寄存器
-
检查时序配置:在 DTS 中设置
nand-ecc-strength和nand-ecc-step-size等。 -
硬件检查:使用逻辑分析仪查看 NAND 接口是否有信号。
3.2 ECC 错误导致数据损坏
现象:读取 NAND 数据时,大量 ECC 错误报告,或者读出的数据不正确。
原因:
-
ECC 算法选择错误(SLC NAND 使用 Hamming,MLC NAND 使用 BCH)。
-
ECC 强度和步长与 NAND 芯片不匹配。
-
NAND 芯片老化或坏块。
调试方法:
-
查看 ECC 统计:
cat /sys/devices/platform/sunxi-nand/mtd0/ecc_stats
-
手动配置 ECC:
# 强制使用 8-bit BCH echo "bch" > /sys/devices/platform/sunxi-nand/mtd0/ecc_alg
3.3 DMA 传输失败
现象:NAND 读写速度慢,或者 DMA 传输一直卡住。
原因:
-
DMA 通道配置错误。
-
DMA 缓冲区地址未对齐。
-
中断没有正常触发。
调试方法:
-
检查 DMA 通道状态:
cat /sys/kernel/debug/dma/dma-* | grep nand
-
使用
perf监控 DMA 中断:perf record -e dma:* -a -- timeout 5 perf script | grep sunxi_nand
-
使用 PIO 模式:在驱动中强制禁用 DMA,测试 PIO 模式是否正常。
3.4 坏块管理问题
现象:ubiformat 格式化时报告大量坏块,或文件系统挂载后频繁报错。
原因:
-
坏块表 (BBT) 损坏。
-
NAND 芯片出厂时已有坏块。
-
控制器没有正确处理坏块标记。
调试方法:
-
查看坏块表:
cat /sys/devices/platform/sunxi-nand/mtd0/bad_blocks
-
手动标记坏块:
# 标记块为坏块 echo "block 512" > /sys/devices/platform/sunxi-nand/mtd0/mark_bad_block
第四章 结合性能调试场景示例
场景:UBIFS 挂载后,读取大文件时速度仅 2MB/s,远低于预期。
分析流程:
-
宏观层面(图谱的 CPU 和 I/O 层):
-
iostat -x 1显示mtd0的svctm很高。 -
perf top -G显示sunxi_nand_irq_handler和dma_sync_single_for_cpu占用大量 CPU。
-
-
NAND 层(图谱的 Device Drivers -> NAND Controller 层):
cat /sys/kernel/debug/mtd/mtd0/registers
发现 ECC 强度设置为 4-bit,但 NAND 芯片本身是 8-bit ECC。
-
检查 DMA 配置(DMA 控制器层):
trace-cmd record -e dma:* -e mtd:* -a -- timeout 5 trace-cmd report | grep sunxi_nand
发现 DMA 中断频繁,每次中断处理开销大。
-
根本原因:
-
ECC 强度配置错误,导致每次读取都需要 2 次 ECC 计算。
-
DMA 中断触发频率过高(每个 block 触发一次)。
-
-
解决方案:
-
在 DTS 中强制设置正确的 ECC 强度
nand-ecc-strength = <8>;。 -
调整 DMA 中断聚合,合并多个 block 的中断。
-
使用更大的 NAND 页大小 (4KB 或 8KB)。
-
优化后,读取速度提升到 25MB/s。
-
第五章 与其他控制器的协同
| 控制器 | 协同方式 | 调试关键点 |
|---|---|---|
| DMA | 数据传输搬运 | DMA 通道分配、中断聚合 |
| 时钟 | 提供模块时钟 | 时钟频率与 NAND 时序匹配 |
| GPIO | 控制 NAND 的 WP (写保护) 和 RB (Ready/Busy) | GPIO 电平配置,写保护屏蔽 |
| PMIC | 提供电源电压 | 电源稳定,电压容差 |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)