06-RTC与PWM驱动.md 35 KB


title: "Linux RTC与PWM驱动开发实战" tags:

  • embedded-linux
  • driver
  • rtc
  • pwm
  • imx6ull
  • platform-driver created: 2025-01-15 updated: 2025-01-15 pdf_ref:
  • "正点原子 I.MX6U嵌入式Linux驱动开发指南 V2.0.1 - 第六十章 Linux RTC驱动实验"
  • "正点原子 I.MX6U嵌入式Linux驱动开发指南 V2.0.1 - 第七十三章 Linux PWM驱动实验" ---

Linux RTC与PWM驱动开发实战


Part 1: RTC驱动


1. RTC硬件概述

1.1 RTC工作原理

RTC(Real Time Clock,实时时钟)是一种专用外设,用于在系统断电后继续维持时间计数。其核心是一个低功耗计数器,通常以秒为单位递增。

┌─────────────────────────────────────────────────┐
│                  RTC 工作原理                    │
├─────────────────────────────────────────────────┤
│                                                 │
│   晶振(32.768kHz) ──→ 分频器 ──→ 秒计数器      │
│                                                 │
│   秒计数器 ──→ 秒/分/时/日/月/年 寄存器         │
│                                                 │
│   CPU ──→ 总线接口 ──→ 读/写时间寄存器          │
│                                                 │
│   电池(VBAT) ──→ 持续供电(断电不丢失)           │
│                                                 │
└─────────────────────────────────────────────────┘

1.2 电池供电机制

RTC的典型供电方式:

供电模式 说明
VBAT引脚 外接纽扣电池(CR2032),系统断电后维持RTC运行
内部电源切换 SOC内置电源切换电路,主电源存在时充电,断电时自动切换到电池
超低功耗 典型功耗<1μA,一颗CR2032可维持数年

1.3 时间戳转换

RTC硬件通常以秒为单位计数(Unix时间戳),需要转换为人类可读时间:

// Unix时间戳 → rtc_time结构体
// 时间戳: 从1970-01-01 00:00:00 UTC起的秒数

struct rtc_time {
    int tm_sec;    // 秒 [0, 59]
    int tm_min;    // 分 [0, 59]
    int tm_hour;   // 时 [0, 23]
    int tm_mday;   // 日 [1, 31]
    int tm_mon;    // 月 [0, 11]  注意:0=一月
    int tm_year;   // 年 [从1900起算]
    int tm_wday;   // 星期 [0, 6]  0=周日
    int tm_yday;   // 一年中的第几天 [0, 365]
    int tm_isdst;  // 夏令时标志
};

转换函数:rtc_time_to_tm(unsigned long time, struct rtc_time *tm)rtc_tm_to_time(struct rtc_time *tm, unsigned long *time)


2. Linux RTC子系统

2.1 RTC子系统架构

┌──────────────────────────────────────────────────────┐
│                    应用层                              │
│         open() / read() / ioctl() / close()          │
├──────────────────────────────────────────────────────┤
│                  VFS层                                │
│              /dev/rtc0 字符设备                       │
├──────────────────────────────────────────────────────┤
│              RTC通用驱动层 (rtc-dev.c)                │
│    rtc_dev_fops → rtc_dev_ioctl → rtc_read_time      │
│                  → rtc_set_time → rtc_read_alarm     │
├──────────────────────────────────────────────────────┤
│              RTC核心层 (rtc-core.c)                   │
│    rtc_device_register() / rtc_device_unregister()   │
├──────────────────────────────────────────────────────┤
│              RTC设备驱动层                            │
│    rtc_class_ops: read_time / set_time / ...         │
├──────────────────────────────────────────────────────┤
│              RTC硬件                                  │
│    I.MX6ULL SNVS-RTC / DS1302 / RX8025T 等           │
└──────────────────────────────────────────────────────┘

2.2 rtc_class_ops结构体

驱动开发者需要实现的核心操作集,定义在include/linux/rtc.h

struct rtc_class_ops {
    int (*open)(struct device *);
    void (*release)(struct device *);
    int (*ioctl)(struct device *, unsigned int, unsigned long);
    int (*read_time)(struct device *, struct rtc_time *);      // 读取时间
    int (*set_time)(struct device *, struct rtc_time *);       // 设置时间
    int (*read_alarm)(struct device *, struct rtc_wkalrm *);   // 读取闹钟
    int (*set_alarm)(struct device *, struct rtc_wkalrm *);    // 设置闹钟
    int (*proc)(struct device *, struct seq_file *);
    int (*set_mmss64)(struct device *, time64_t secs);         // 64位秒数设置
    int (*set_mmss)(struct device *, unsigned long secs);
    int (*read_callback)(struct device *, int data);
    int (*alarm_irq_enable)(struct device *, unsigned int enabled); // 闹钟中断使能
};

关键回调函数说明:

回调函数 功能 必须实现
read_time 从硬件读取当前时间
set_time 向硬件写入新时间
read_alarm 读取闹钟值 可选
set_alarm 设置闹钟值 可选
alarm_irq_enable 使能/关闭闹钟中断 可选

2.3 核心数据结构

// RTC设备结构体 (include/linux/rtc.h)
struct rtc_device {
    struct device dev;                              // 设备
    struct module *owner;
    int id;                                         // ID
    char name[RTC_DEVICE_NAME_SIZE];                // 名字
    const struct rtc_class_ops *ops;                // RTC底层操作函数
    struct mutex ops_lock;
    struct cdev char_dev;                           // 字符设备
    unsigned long flags;
    unsigned long irq_data;
    spinlock_t irq_lock;
    wait_queue_head_t irq_queue;
    struct fasync_struct *async_queue;
    struct rtc_task *irq_task;
    spinlock_t irq_task_lock;
    int irq_freq;
    int max_user_freq;
    struct timerqueue_head timerqueue;
    struct rtc_timer aie_timer;
    struct rtc_timer uie_rtctimer;
    struct hrtimer pie_timer;
    int pie_enabled;
    struct work_struct irqwork;
    int uie_unsupported;
};

2.4 RTC通用file_operations

Linux内核在drivers/rtc/rtc-dev.c中提供了所有RTC设备共用的字符设备操作集:

static const struct file_operations rtc_dev_fops = {
    .owner         = THIS_MODULE,
    .llseek        = no_llseek,
    .read          = rtc_dev_read,
    .poll          = rtc_dev_poll,
    .unlocked_ioctl = rtc_dev_ioctl,
    .open          = rtc_dev_open,
    .release       = rtc_dev_release,
    .fasync        = rtc_dev_fasync,
};

2.5 RTC注册与注销API

// 注册RTC设备
struct rtc_device *rtc_device_register(const char *name,
                                       struct device *dev,
                                       const struct rtc_class_ops *ops,
                                       struct module *owner);

// 注销RTC设备
void rtc_device_unregister(struct rtc_device *rtc);

// 设备管理版本(推荐,自动释放)
struct rtc_device *devm_rtc_device_register(struct device *dev,
                                            const char *name,
                                            const struct rtc_class_ops *ops,
                                            struct module *owner);
void devm_rtc_device_unregister(struct device *dev, struct rtc_device *rtc);

3. RTC设备树配置

3.1 设备树节点格式

RTC设备在设备树中通常作为平台设备节点存在,关键属性:

属性 说明
compatible 匹配字符串,用于匹配驱动
reg 寄存器地址和长度
interrupts 中断号和触发方式
clocks 时钟源

3.2 I.MX6ULL RTC设备树节点

I.MX6ULL内部RTC(SNVS-RTC)在imx6ull.dtsi中的定义:

snvs_rtc: snvs-rtc-lp {
    compatible = "fsl,sec-v4.0-mon-rtc-lp";
    regmap = <&snvs>;
    offset = <0x34>;
    interrupts = <GIC_SPI 19 IRQ_TYPE_LEVEL_HIGH>,
                 <GIC_SPI 20 IRQ_TYPE_LEVEL_HIGH>;
};

属性解析:

属性值 说明
compatible = "fsl,sec-v4.0-mon-rtc-lp" 匹配驱动rtc-snvs.c
regmap = <&snvs> 引用SNVS系统控制regmap
offset = <0x34> SNVS中RTC寄存器偏移
interrupts 两个中断:秒中断(19)和闹钟中断(20)

3.3 外部RTC芯片设备树示例(DS1302)

ds1302: rtc@68 {
    compatible = "maxim,ds1302";
    reg = <0x68>;
    #clock-cells = <0>;
};

4. RTC驱动源码分析

4.1 SNVS-RTC平台驱动框架

驱动文件:drivers/rtc/rtc-snvs.c

// 设备树ID表
static const struct of_device_id snvs_dt_ids[] = {
    { .compatible = "fsl,sec-v4.0-mon-rtc-lp", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, snvs_dt_ids);

// 平台驱动结构体
static struct platform_driver snvs_rtc_driver = {
    .driver = {
        .name    = "snvs_rtc",
        .pm      = SNVS_RTC_PM_OPS,
        .of_match_table = snvs_dt_ids,
    },
    .probe = snvs_rtc_probe,
};
module_platform_driver(snvs_rtc_driver);

4.2 snvs_rtc_probe函数

static int snvs_rtc_probe(struct platform_device *pdev)
{
    struct snvs_rtc_data *data;
    struct resource *res;
    int ret;
    void __iomem *mmio;

    // 1. 申请并初始化snvs_rtc_data结构体
    data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    // 2. 获取regmap(用于寄存器读写)
    data->regmap =
        syscon_regmap_lookup_by_phandle(pdev->dev.of_node, "regmap");

    if (IS_ERR(data->regmap)) {
        // 旧DTS兼容:直接映射寄存器
        res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
        mmio = devm_ioremap_resource(&pdev->dev, res);
        if (IS_ERR(mmio))
            return PTR_ERR(mmio);

        data->regmap = devm_regmap_init_mmio(&pdev->dev, mmio,
                                             &snvs_rtc_config);
    } else {
        // 新DTS:使用regmap偏移
        data->offset = SNVS_LPREGISTER_OFFSET;
        of_property_read_u32(pdev->dev.of_node, "offset",
                             &data->offset);
    }

    if (!data->regmap) {
        dev_err(&pdev->dev, "Can't find snvs syscon\n");
        return -ENODEV;
    }

    // 3. 获取中断号
    data->irq = platform_get_irq(pdev, 0);
    if (data->irq < 0)
        return data->irq;

    platform_set_drvdata(pdev, data);

    // 4. 初始化毛刺检测寄存器
    regmap_write(data->regmap, data->offset + SNVS_LPPGDR,
                 SNVS_LPPGDR_INIT);

    // 5. 清除中断状态
    regmap_write(data->regmap, data->offset + SNVS_LPSR, 0xffffffff);

    // 6. 使能RTC
    snvs_rtc_enable(data, true);

    // 7. 使能唤醒功能
    device_init_wakeup(&pdev->dev, true);

    // 8. 注册中断处理函数
    ret = devm_request_irq(&pdev->dev, data->irq,
                           snvs_rtc_irq_handler,
                           IRQF_SHARED, "rtc alarm", &pdev->dev);
    if (ret) {
        dev_err(&pdev->dev, "failed to request irq %d: %d\n",
                data->irq, ret);
        goto error_rtc_device_register;
    }

    // 9. 注册RTC设备
    data->rtc = devm_rtc_device_register(&pdev->dev, pdev->name,
                                          &snvs_rtc_ops, THIS_MODULE);
    if (IS_ERR(data->rtc)) {
        ret = PTR_ERR(data->rtc);
        dev_err(&pdev->dev, "failed to register rtc: %d\n", ret);
        goto error_rtc_device_register;
    }

    return 0;

error_rtc_device_register:
    if (data->clk)
        clk_disable_unprepare(data->clk);
    return ret;
}

4.3 snvs_rtc_ops操作集

static const struct rtc_class_ops snvs_rtc_ops = {
    .read_time = snvs_rtc_read_time,
    .set_time = snvs_rtc_set_time,
    .read_alarm = snvs_rtc_read_alarm,
    .set_alarm = snvs_rtc_set_alarm,
    .alarm_irq_enable = snvs_rtc_alarm_irq_enable,
};

4.4 snvs_rtc_read_time函数

static int snvs_rtc_read_time(struct device *dev,
                              struct rtc_time *tm)
{
    struct snvs_rtc_data *data = dev_get_drvdata(dev);
    unsigned long time = rtc_read_lp_counter(data);

    rtc_time_to_tm(time, tm);

    return 0;
}

4.5 rtc_read_lp_counter函数(读取RTC计数值)

static u32 rtc_read_lp_counter(struct snvs_rtc_data *data)
{
    u64 read1, read2;
    u32 val;

    do {
        // 读取高32位
        regmap_read(data->regmap, data->offset + SNVS_LPSRTCMR, &val);
        read1 = val;
        read1 <<= 32;
        // 读取低32位
        regmap_read(data->regmap, data->offset + SNVS_LPSRTCLR, &val);
        read1 |= val;

        // 再次读取,用于校验
        regmap_read(data->regmap, data->offset + SNVS_LPSRTCMR, &val);
        read2 = val;
        read2 <<= 32;
        regmap_read(data->regmap, data->offset + SNVS_LPSRTCLR, &val);
        read2 |= val;
    } while ((read1 >> CNTR_TO_SECS_SH) != (read2 >> CNTR_TO_SECS_SH));

    // 将47位计数器转换为32位秒数
    return (u32)(read1 >> CNTR_TO_SECS_SH);
}

读取策略说明:由于RTC是47位计数器,需要分两次读取高低32位。为防止读取过程中计数器更新导致高低位不匹配,采用连续读取两次并比较秒位的策略。


5. RTC实验验证

5.1 查看RTC设备

# 查看RTC设备信息
dmesg | grep rtc
# 输出: snvs-rtc-lp supply vsnvs not found, using dummy regulator
#       snvs-rtc-lp rtc0: setting system clock to 1970-01-01 00:06:11 UTC (371)

# 查看系统时间
date
# 输出: Wed Jan  1 00:06:11 UTC 1970

5.2 设置系统时间

# 设置系统时间(仅写入内存,重启丢失)
date -s "2025-01-15 14:30:00"

# 验证
date
# 输出: Wed Jan 15 14:30:00 CST 2025

5.3 将系统时间写入RTC硬件

# 将系统时间写入RTC(断电不丢失)
hwclock -w

# 从RTC读取时间
hwclock -r
# 输出: 2025-01-15 14:30:xx.123456+08:00

# 从RTC设置系统时间
hwclock -s

5.4 完整测试流程

# 1. 设置正确时间
date -s "2025-01-15 14:30:00"

# 2. 同步到RTC硬件
hwclock -w

# 3. 重启验证
reboot

# 4. 启动后查看时间是否保持
date
# 如果电池正常,时间应保持为设置值

5.5 RTC sysfs接口

# 查看RTC设备列表
ls /sys/class/rtc/
# 输出: rtc0

# 查看RTC详细信息
cat /sys/class/rtc/rtc0/name
# 输出: snvs-rtc-lp

cat /sys/class/rtc/rtc0/hctosys
# 输出: 1 (表示已作为系统时钟源)

cat /sys/class/rtc/rtc0/since_epoch
# 输出: 秒数(从1970-01-01起)

Part 2: PWM驱动


6. PWM硬件概述

6.1 PWM工作原理

PWM(Pulse Width Modulation,脉冲宽度调制)是一种通过数字信号模拟模拟信号的技术。

┌──────────────────────────────────────────────────────┐
│                 PWM 波形示意                          │
├──────────────────────────────────────────────────────┤
│                                                      │
│  周期 T = 1/f                                         │
│  ←─────────────────→                                 │
│                                                      │
│  高电平    ┌────────┐      ┌────────┐                 │
│           │        │      │        │                 │
│  ─────────┘        └──────┘        └──────           │
│                                                      │
│  ←── Ton ──→                                         │
│                                                      │
│  占空比 D = Ton / T × 100%                            │
│                                                      │
└──────────────────────────────────────────────────────┘

6.2 关键参数

参数 定义 公式
周期 T 一个完整PWM波形的时间 T = 1/f
频率 f 每秒脉冲数 f = 1/T
占空比 D 高电平时间占周期的比例 D = Ton / T × 100%
分辨率 占空比的最小调节步进 取决于计数器位数

6.3 应用场景

应用 说明
LED调光 改变占空比调节亮度
电机调速 控制直流电机转速
舵机控制 位置伺服(50Hz, 0.5-2.5ms脉宽)
LCD背光 调节屏幕亮度
音频输出 蜂鸣器发声
电源转换 开关电源PWM控制

7. Linux PWM子系统

7.1 PWM子系统架构

┌──────────────────────────────────────────────────────┐
│                    应用层                              │
│       sysfs: /sys/class/pwm/pwmchipX/pwmY/          │
│       ioctl: PWM_REQUEST / PWM_CONFIG / PWM_ENABLE   │
├──────────────────────────────────────────────────────┤
│              PWM核心层 (drivers/pwm/core.c)           │
│    pwm_request() / pwm_config() / pwm_enable()       │
│    pwmchip_add() / pwmchip_remove()                  │
├──────────────────────────────────────────────────────┤
│              PWM设备驱动层                            │
│    pwm_ops: request / free / config / enable / disable│
├──────────────────────────────────────────────────────┤
│              PWM硬件控制器                           │
│    I.MX6ULL PWM1~PWM8 / STM32 TIM / RK3568 PWM      │
└──────────────────────────────────────────────────────┘

7.2 pwm_chip结构体

PWM子系统的核心结构,定义在include/linux/pwm.h

struct pwm_chip {
    struct device *dev;                              // 设备
    struct list_head list;                           // 链表
    const struct pwm_ops *ops;                       // PWM操作函数集
    int base;                                        // PWM通道基编号
    unsigned int npwm;                               // PWM通道数量
    struct pwm_device *pwms;                         // PWM设备数组
    struct pwm_device * (*of_xlate)(struct pwm_chip *pc,
                                    const struct of_phandle_args *args);
    unsigned int of_pwm_n_cells;
    bool can_sleep;                                  // 是否可以睡眠
};

7.3 pwm_ops结构体

struct pwm_ops {
    int (*request)(struct pwm_chip *chip,             // 请求PWM
                   struct pwm_device *pwm);
    void (*free)(struct pwm_chip *chip,               // 释放PWM
                 struct pwm_device *pwm);
    int (*config)(struct pwm_chip *chip,              // 配置周期和占空比
                  struct pwm_device *pwm,
                  int duty_ns, int period_ns);
    int (*set_polarity)(struct pwm_chip *chip,        // 设置极性
                        struct pwm_device *pwm,
                        enum pwm_polarity polarity);
    int (*enable)(struct pwm_chip *chip,              // 使能PWM
                  struct pwm_device *pwm);
    void (*disable)(struct pwm_chip *chip,            // 关闭PWM
                    struct pwm_device *pwm);
    struct module *owner;
};

7.4 PWM内核API

// 注册/注销PWM芯片
int pwmchip_add(struct pwm_chip *chip);
int pwmchip_remove(struct pwm_chip *chip);

// PWM操作API
struct pwm_device *pwm_request(int pwm_num, const char *label);
void pwm_free(struct pwm_device *pwm);
int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns);
int pwm_enable(struct pwm_device *pwm);
void pwm_disable(struct pwm_device *pwm);
int pwm_set_polarity(struct pwm_device *pwm, enum pwm_polarity polarity);

8. PWM设备树配置

8.1 PWM控制器节点格式

pwm3: pwm@02088000 {
    compatible = "fsl,imx6ul-pwm", "fsl,imx27-pwm";
    reg = <0x02088000 0x4000>;
    interrupts = <GIC_SPI 85 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6UL_CLK_PWM3>,
             <&clks IMX6UL_CLK_PWM3>;
    clock-names = "ipg", "per";
    #pwm-cells = <2>;
};

8.2 pwm-cells说明

#pwm-cells = <2> 表示引用此PWM控制器时需要2个参数:

参数 说明
第1个 PWM通道索引(I.MX6ULL只有1个通道,填0)
第2个 PWM标志位(极性等)

引用示例:

// 引用pwm1,通道0,周期5000000ns(5ms, 200Hz)
pwms = <&pwm1 0 5000000>;

8.3 引脚复用配置

// 在iomuxc节点下添加引脚定义
pinctrl_pwm3: pwm3grp {
    fsl,pins = <
        MX6UL_PAD_GPIO1_IO04__PWM3_OUT  0x110b0
    >;
};

// 在pwm3节点中引用
&pwm3 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_pwm3>;
    clocks = <&clks IMX6UL_CLK_PWM3>,
             <&clks IMX6UL_CLK_PWM3>;
    status = "okay";
};

8.4 LCD背光PWM节点示例

backlight {
    compatible = "pwm-backlight";
    pwms = <&pwm1 0 5000000>;           // PWM1, 通道0, 200Hz
    brightness-levels = <0 4 8 16 32 64 128 255>;
    default-brightness-level = <7>;     // 默认最大亮度
    status = "okay";
};

属性解析:

属性 说明
compatible 必须为"pwm-backlight"
pwms 指定PWM通道和周期
brightness-levels 背光等级数组(0~255)
default-brightness-level 默认等级索引

9. PWM驱动源码分析

9.1 I.MX6ULL PWM平台驱动

驱动文件:drivers/pwm/pwm-imx.c

// 设备树ID表
static const struct of_device_id imx_pwm_dt_ids[] = {
    { .compatible = "fsl,imx1-pwm", .data = &imx_pwm_data_v1, },
    { .compatible = "fsl,imx27-pwm", .data = &imx_pwm_data_v2, },
    { /* sentinel */ }
};

// 平台驱动结构体
static struct platform_driver imx_pwm_driver = {
    .driver = {
        .name    = "imx-pwm",
        .of_match_table = imx_pwm_dt_ids,
    },
    .probe  = imx_pwm_probe,
    .remove = imx_pwm_remove,
};

module_platform_driver(imx_pwm_driver);

9.2 imx_pwm_data版本结构

// v2版本(I.MX6ULL使用)
static struct imx_pwm_data imx_pwm_data_v2 = {
    .config = imx_pwm_config_v2,
    .set_enable = imx_pwm_set_enable_v2,
};

9.3 imx_pwm_probe函数

static int imx_pwm_probe(struct platform_device *pdev)
{
    const struct of_device_id *of_id =
        of_match_device(imx_pwm_dt_ids, &pdev->dev);
    const struct imx_pwm_data *data;
    struct imx_chip *imx;
    struct resource *r;
    int ret = 0;

    if (!of_id)
        return -ENODEV;

    // 1. 申请imx_chip结构体内存
    imx = devm_kzalloc(&pdev->dev, sizeof(*imx), GFP_KERNEL);
    if (imx == NULL)
        return -ENOMEM;

    // 2. 初始化pwm_chip
    imx->chip.ops = &imx_pwm_ops;
    imx->chip.dev = &pdev->dev;
    imx->chip.base = -1;
    imx->chip.npwm = 1;                     // I.MX6ULL每个控制器1个通道
    imx->chip.can_sleep = true;

    // 3. 获取并映射PWM寄存器基地址
    r = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    imx->mmio_base = devm_ioremap_resource(&pdev->dev, r);
    if (IS_ERR(imx->mmio_base))
        return PTR_ERR(imx->mmio_base);

    // 4. 获取版本特定的操作函数
    data = of_id->data;
    imx->config = data->config;
    imx->set_enable = data->set_enable;

    // 5. 向内核注册PWM芯片
    ret = pwmchip_add(&imx->chip);
    if (ret < 0)
        return ret;

    platform_set_drvdata(pdev, imx);
    return 0;
}

9.4 imx_pwm_ops操作集

static struct pwm_ops imx_pwm_ops = {
    .enable  = imx_pwm_enable,
    .disable = imx_pwm_disable,
    .config  = imx_pwm_config,
    .owner   = THIS_MODULE,
};

9.5 imx_pwm_set_enable_v2函数(使能/关闭PWM)

static void imx_pwm_set_enable_v2(struct pwm_chip *chip, bool enable)
{
    struct imx_chip *imx = to_imx_chip(chip);
    u32 val;

    // 读取PWMCR寄存器
    val = readl(imx->mmio_base + MX3_PWMCR);

    if (enable)
        val |= MX3_PWMCR_EN;      // bit0置1,使能PWM
    else
        val &= ~MX3_PWMCR_EN;     // bit0清0,关闭PWM

    // 写回PWMCR寄存器
    writel(val, imx->mmio_base + MX3_PWMCR);
}

9.6 imx_pwm_config_v2函数(配置频率和占空比)

static int imx_pwm_config_v2(struct pwm_chip *chip,
                              struct pwm_device *pwm,
                              int duty_ns, int period_ns)
{
    struct imx_chip *imx = to_imx_chip(chip);
    struct device *dev = chip->dev;
    unsigned long long c;
    unsigned long period_cycles, duty_cycles, prescale;
    bool enable = test_bit(PWMF_ENABLED, &pwm->flags);
    u32 cr;

    // 1. 计算周期计数值
    c = clk_get_rate(imx->clk_per);     // 获取外设时钟频率
    c = c * period_ns;                   // 时钟周期数 = 频率 × 周期(ns)
    do_div(c, 1000000000);              // 转换为计数值
    period_cycles = c;

    // 2. 计算预分频值
    prescale = period_cycles / 0x10000 + 1;

    // 3. 计算占空比计数值
    period_cycles /= prescale;
    c = (unsigned long long)period_cycles * duty_ns;
    do_div(c, period_ns);
    duty_cycles = c;

    // 4. 周期值减2(硬件要求)
    if (period_cycles > 2)
        period_cycles -= 2;
    else
        period_cycles = 0;

    // 5. 写入寄存器
    writel(duty_cycles, imx->mmio_base + MX3_PWMSAR);    // 占空比
    writel(period_cycles, imx->mmio_base + MX3_PWMPR);   // 周期

    // 6. 配置PWMCR寄存器
    cr = MX3_PWMCR_PRESCALER(prescale) |
         MX3_PWMCR_DOZEEN | MX3_PWMCR_WAITEN |
         MX3_PWMCR_DBGEN | MX3_PWMCR_CLKSRC_IPG_HIGH;

    if (enable)
        cr |= MX3_PWMCR_EN;

    writel(cr, imx->mmio_base + MX3_PWMCR);

    return 0;
}

计算流程:

period_cycles = clk_rate × period_ns / 1e9
prescale = period_cycles / 65536 + 1
period_cycles = period_cycles / prescale
duty_cycles = period_cycles × duty_ns / period_ns
最终写入: PWMSAR = duty_cycles, PWMPR = period_cycles - 2

10. PWM实验验证

10.1 使能PWM驱动(内核配置)

Device Drivers
  -> Pulse-Width Modulation (PWM) Support
    -> <*> i.MX PWM support

10.2 sysfs接口操作

# 1. 查看可用的PWM芯片
ls /sys/class/pwm/
# 输出: pwmchip0 pwmchip1 pwmchip2 ... pwmchip7
# pwmchip2 对应 PWM3

# 2. 导出PWM通道
echo 0 > /sys/class/pwm/pwmchip2/export
# 导出后生成 pwm0 子目录

# 3. 设置周期(频率20KHz = 周期50000ns)
echo 50000 > /sys/class/pwm/pwmchip2/pwm0/period

# 4. 设置占空比(10000ns = 20%占空比)
echo 10000 > /sys/class/pwm/pwmchip2/pwm0/duty_cycle

# 5. 使能PWM
echo 1 > /sys/class/pwm/pwmchip2/pwm0/enable

# 6. 查看当前配置
cat /sys/class/pwm/pwmchip2/pwm0/period
cat /sys/class/pwm/pwmchip2/pwm0/duty_cycle
cat /sys/class/pwm/pwmchip2/pwm0/enable

# 7. 关闭PWM
echo 0 > /sys/class/pwm/pwmchip2/pwm0/enable

# 8. 取消导出
echo 0 > /sys/class/pwm/pwmchip2/unexport

10.3 波形验证

使用示波器连接GPIO1_IO04引脚,验证输出波形:

设置值 预期波形
period=50000ns, duty_cycle=10000ns 20KHz, 20%占空比
period=50000ns, duty_cycle=25000ns 20KHz, 50%占空比
period=100000ns, duty_cycle=50000ns 10KHz, 50%占空比

10.4 PWM测试脚本

#!/bin/sh
# PWM测试脚本 - 验证不同频率和占空比

PWM_PATH="/sys/class/pwm/pwmchip2"

# 导出PWM
echo 0 > ${PWM_PATH}/export
sleep 1

# 测试1: 20KHz, 50%占空比
echo 50000 > ${PWM_PATH}/pwm0/period
echo 25000 > ${PWM_PATH}/pwm0/duty_cycle
echo 1 > ${PWM_PATH}/pwm0/enable
echo "Test 1: 20KHz, 50% - check scope"
sleep 3

# 测试2: 1KHz, 30%占空比
echo 1000000 > ${PWM_PATH}/pwm0/period
echo 300000 > ${PWM_PATH}/pwm0/duty_cycle
echo "Test 2: 1KHz, 30% - check scope"
sleep 3

# 测试3: 10KHz, 80%占空比
echo 100000 > ${PWM_PATH}/pwm0/period
echo 80000 > ${PWM_PATH}/pwm0/duty_cycle
echo "Test 3: 10KHz, 80% - check scope"
sleep 3

# 清理
echo 0 > ${PWM_PATH}/pwm0/enable
echo 0 > ${PWM_PATH}/unexport
echo "Tests complete"

11. 跨平台对比

11.1 RTC对比

特性 I.MX6ULL (SNVS-RTC) STM32F103 (RTC) RK3568 (RTC)
RTC位置 SNVS域,独立供电 备份域(BDR) 独立PMU域
电池接口 内置电源切换 VBAT引脚 VBAT引脚
计数精度 32.768kHz晶振 32.768kHz晶振 32.768kHz晶振
驱动文件 rtc-snvs.c rtc-stm32.c rtc-rk808.c
compatible fsl,sec-v4.0-mon-rtc-lp st,stm32-rtc rockchip,rk808-rtc
特殊机制 47位计数器,两次读取校验 预分频器 外部PMIC RTC
寄存器访问 regmap机制 直接MMIO I2C/SPI总线

11.2 PWM对比

特性 I.MX6ULL STM32F103 RK3568
PWM数量 8路 多路(TIM) 多路
驱动文件 pwm-imx.c pwm-stm32.c pwm-rk.c
compatible fsl,imx6ul-pwm st,stm32-pwm rockchip,rk3568-pwm
最小分辨率 取决于时钟源 取决于ARR 取决于时钟源
配置方式 sysfs + API sysfs + API sysfs + API
特殊功能 背光控制 高级定时器 多路独立控制

11.3 驱动框架共性

三个平台的RTC和PWM驱动都遵循相同的Linux子系统框架:

  1. 设备树描述硬件:通过compatible属性匹配驱动
  2. 平台驱动模型platform_driver + probe函数
  3. 子系统注册:通过核心层API注册设备
  4. 操作集抽象:底层操作通过函数指针结构体实现

12. 面试精选

Q1: RTC子系统中rtc_class_opsfile_operations的关系是什么?

答案file_operations是字符设备的通用操作集,提供给用户空间的open/read/write/ioctl接口。rtc_class_ops是RTC硬件的底层操作集,包括read_time/set_time等。

RTC驱动采用两层封装:用户空间调用ioctl(RTC_RD_TIME)rtc-dev.c中的rtc_dev_ioctl → 核心层rtc_read_time → 驱动的rtc_class_ops.read_time。这种设计使得所有RTC设备共享同一套用户空间接口,驱动只需实现硬件操作。

Q2: 为什么I.MX6ULL的SNVS-RTC读取时间需要连续读两次?

答案:SNVS-RTC是47位计数器,需要分两次读取高低32位寄存器(LPSRTCMR和LPSRTCLR)。由于总线读取存在延迟,读取高位后到读取低位之间,低位可能已经进位(例如从0x0000FFFF变为0x00010000),导致高低位不匹配。

解决方案是连续读取两次,比较秒位(>> CNTR_TO_SECS_SH后的值)是否一致。如果两次秒值相同,说明读取期间没有发生进位,数据有效。

Q3: PWM子系统中#pwm-cells属性的含义是什么?如何在设备树中引用PWM?

答案#pwm-cells定义了引用PWM控制器时需要的参数个数。例如#pwm-cells = <2>表示需要2个参数:第1个是通道索引,第2个是标志位。

引用示例:pwms = <&pwm1 0 5000000> 表示使用pwm1控制器的通道0,周期为5000000ns。核心层会根据#pwm-cells解析参数,调用驱动的of_xlate回调转换为具体的PWM设备。

Q4: RTC断电后时间如何保持?如果电池没电会怎样?

答案:RTC通过VBAT引脚连接纽扣电池(如CR2032),主电源断电时自动切换到电池供电。SNVS-RTC功耗极低(<1μA),一颗CR2032可维持数年。

如果电池没电,RTC计数器停止工作,时间丢失。重启后RTC从默认值(通常是0,即1970-01-01)开始计数。驱动会通过rtc_valid_tm检查时间有效性,无效时间会返回错误。此时需要通过NTP同步或手动设置时间。

Q5: PWM配置中周期和占空比的关系是什么?如何计算寄存器值?

答案:周期(period)决定PWM频率,占空比(duty_cycle)决定高电平时间占比。占空比必须小于等于周期。

寄存器值计算:I.MX6ULL使用IPG时钟,period_cycles = clk_rate × period_ns / 1e9duty_cycles = period_cycles × duty_ns / period_ns。由于硬件要求周期值需减2(PWMPR寄存器),最终写入PWMPR = period_cycles - 2。预分频器用于将大周期值分频到16位计数器范围内。


相关页面

  • [[05-GPIO与LED驱动]]
  • [[07-按键输入驱动]]
  • [[08-I2C驱动]]
  • [[09-SPI驱动]]
  • [[10-网络设备驱动]]