--- 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硬件概述 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第六十章内容,为扩展知识。原书指出 RTC 硬件原理已在“裸机篇”第二十五章讲解,本章直接从 Linux 内核 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时间戳),需要转换为人类可读时间: ```c // 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`: ```c 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 核心数据结构 ```c // 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设备共用的字符设备操作集: ```c 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, }; ``` 应用层通过 `ioctl` 下发 `RTC_RD_TIME`(读取时间)/`RTC_SET_TIME`(设置时间)/`RTC_ALM_READ`(读取闹钟)等命令,最终由 `rtc_dev_ioctl` 逐层调用到 `rtc_class_ops` 中的 `read_time`/`set_time`/`read_alarm`。核心层 `__rtc_read_time` 的调用关系如下(原书示例代码 60.1.5): ```c static int __rtc_read_time(struct rtc_device *rtc, struct rtc_time *tm) { int err; if (!rtc->ops) err = -ENODEV; else if (!rtc->ops->read_time) err = -EINVAL; else { memset(tm, 0, sizeof(struct rtc_time)); err = rtc->ops->read_time(rtc->dev.parent, tm); if (err < 0) { dev_dbg(&rtc->dev, "read_time: fail to read: %d\n", err); return err; } err = rtc_valid_tm(tm); /* 校验时间有效性 */ if (err < 0) dev_dbg(&rtc->dev, "read_time: rtc_time isn't valid\n"); } return err; } ``` ### 2.5 RTC注册与注销API ```c // 注册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`中的定义: ```dts snvs_rtc: snvs-rtc-lp { compatible = "fsl,sec-v4.0-mon-rtc-lp"; regmap = <&snvs>; offset = <0x34>; interrupts = , ; }; ``` 属性解析: | 属性值 | 说明 | | ---------------------------------------- | ---------------------------------- | | `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) ```dts ds1302: rtc@68 { compatible = "maxim,ds1302"; reg = <0x68>; #clock-cells = <0>; }; ``` --- ## 4. RTC驱动源码分析 ### 4.1 SNVS-RTC平台驱动框架 驱动文件:`drivers/rtc/rtc-snvs.c` ```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函数 ```c 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. 初始化毛刺检测寄存器,SNVS_LPPGDR_INIT = 0x41736166 regmap_write(data->regmap, data->offset + SNVS_LPPGDR, SNVS_LPPGDR_INIT); // 5. 清除中断状态(LPSR 写1清零) 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操作集 ```c 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函数 ```c 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计数值) ```c 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)); // Convert 47-bit counter to 32-bit raw second count // CNTR_TO_SECS_SH = 15,即右移15位得到秒数 return (u32)(read1 >> CNTR_TO_SECS_SH); } ``` 读取策略说明:由于RTC是47位计数器(SNVS_LPSRTCMR/LPSRTCLR),需要分两次读取高低32位。为防止读取过程中计数器更新导致高低位不匹配,采用连续读取两次并比较秒位(`>> CNTR_TO_SECS_SH`)的策略;代码注释也指出在 CPU/总线低速运行时两次读取可能始终不同,因此只比较第二次的值。 --- ## 5. RTC实验验证 ### 5.1 查看RTC设备 ```bash # 查看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 设置系统时间 ```bash # 设置系统时间(仅写入内存,重启丢失) date -s "2025-01-15 14:30:00" # 验证 date # 输出: Wed Jan 15 14:30:00 CST 2025 ``` ### 5.3 将系统时间写入RTC硬件 ```bash # 将系统时间写入RTC(断电不丢失) hwclock -w # 从RTC读取时间 hwclock -r # 输出: 2025-01-15 14:30:xx.123456+08:00 # 从RTC设置系统时间 hwclock -s ``` ### 5.4 完整测试流程 ```bash # 1. 设置正确时间 date -s "2025-01-15 14:30:00" # 2. 同步到RTC硬件 hwclock -w # 3. 重启验证 reboot # 4. 启动后查看时间是否保持 date # 如果电池正常,时间应保持为设置值 ``` ### 5.5 RTC sysfs接口 > ⚠️ **来源说明**:本小节不属于《I.MX6U嵌入式Linux驱动开发指南》第六十章内容,为扩展知识(原书只演示了 `date` 与 `hwclock -w`)。 ```bash # 查看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硬件概述 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第七十三章内容,为扩展知识。原书指出 PWM 原理与 I.MX6ULL PWM 外设已在“裸机篇”讲解,本章直接分析内核自带的 `pwm-imx.c` 驱动。 ### 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`: ```c 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结构体 ```c 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 ```c // 注册/注销PWM芯片(原书 73.1.2 讲解) int pwmchip_add(struct pwm_chip *chip); int pwmchip_remove(struct pwm_chip *chip); // 以下是“消费者(consumer)”侧的传统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); ``` > ⚠️ **来源说明**:`pwmchip_add`/`pwmchip_remove` 来自原书 73.1.2;`pwm_request`/`pwm_config`/`pwm_enable`/`pwm_disable`/`pwm_set_polarity` 等消费者 API 原书第七十三章未讲解,为扩展知识。较新内核推荐使用 `devm_pwm_get()` + `pwm_apply_state()`。 --- ## 8. PWM设备树配置 ### 8.1 PWM控制器节点格式 ```dts pwm3: pwm@02088000 { compatible = "fsl,imx6ul-pwm", "fsl,imx27-pwm"; reg = <0x02088000 0x4000>; interrupts = ; clocks = <&clks IMX6UL_CLK_PWM3>, <&clks IMX6UL_CLK_PWM3>; clock-names = "ipg", "per"; #pwm-cells = <2>; }; ``` 说明(原书 73.1.1):I.MX6ULL 共有 8 路 PWM(PWM1~PWM8),对应 8 个控制器节点,同属 AIPS-1 域,但在 `imx6ull.dtsi` 中分为 PWM1~PWM4 与 PWM5~PWM8 两部分,8 个节点除 `reg` 外内容相同。本章实验使用 `GPIO1_IO04`,它对应 PWM3 输出。绑定文档:`Documentation/devicetree/bindings/pwm/imx-pwm.txt`。 ### 8.2 pwm-cells说明 `#pwm-cells = <2>` 表示引用此PWM控制器时需要2个参数(以原书 73.4.1.1 的 `pwms = <&pwm1 0 5000000>` 为例): | 参数 | 说明 | | ----- | ------------------------------------------------- | | 第1个 | PWM通道索引(I.MX6ULL每个控制器只有1个通道,填0) | | 第2个 | PWM周期,单位ns(如5000000ns即5ms,对应200Hz) | 引用示例: ```dts // 引用pwm1,通道0,周期5000000ns(5ms, 200Hz) pwms = <&pwm1 0 5000000>; ``` ### 8.3 引脚复用配置 ```dts // 在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"; }; ``` 注意(原书 73.2.1):若设备树中其他外设也用到 `GPIO1_IO04`,必须屏蔽掉,且不能只屏蔽 pinctrl 配置,还要搜索 `"gpio1 4"` 检查是否有其他地方引用。PWM1~PWM8 分别对应 `IMX6UL_CLK_PWM1`~`IMX6UL_CLK_PWM8`,默认时钟源可能是 `IMX6UL_CLK_DUMMY`,需改为实际时钟。 ### 8.4 LCD背光PWM节点示例 ```dts 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"; }; ``` 属性解析(原书 73.4): | 属性 | 说明 | | -------------------------- | -------------------------------------------------------------------- | | `compatible` | 必须为`"pwm-backlight"`,匹配驱动 `drivers/video/backlight/pwm_bl.c` | | `pwms` | 指定PWM通道和周期,`<&pwm1 0 5000000>` 即200Hz | | `brightness-levels` | 背光等级数组,范围0~255对应0%~100%,必须从0开始、以255结尾 | | `default-brightness-level` | 默认背光**等级索引**(不是数值),这里7对应255,即100%占空比 | | `power-supply` | 支持的电压,可选 | 绑定文档:`Documentation/devicetree/bindings/video/backlight/pwm-backlight.txt`。 --- ## 9. PWM驱动源码分析 ### 9.1 I.MX6ULL PWM平台驱动 驱动文件:`drivers/pwm/pwm-imx.c` ```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版本结构 ```c // 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函数 ```c 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操作集 ```c 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) ```c 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函数(配置频率和占空比) ```c 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接口操作 ```bash # 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测试脚本 ```bash #!/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. 跨平台对比 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。 ### 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. 面试精选 > ⚠️ **来源说明**:本节面试题不属于《I.MX6U嵌入式Linux驱动开发指南》正文内容,为扩展知识。 ### Q1: RTC子系统中`rtc_class_ops`和`file_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个是PWM周期(单位ns)。 引用示例:`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)决定高电平时间占比。占空比必须小于等于周期。 寄存器值计算:`imx_pwm_config_v2` 用 `clk_get_rate(imx->clk_per)` 取 per 时钟频率,`period_cycles = clk_rate × period_ns / 1e9`,`prescale = period_cycles / 0x10000 + 1`,`period_cycles /= prescale`,`duty_cycles = period_cycles × duty_ns / period_ns`。由于硬件要求 PWMPR 中存放的值应比实际周期小 2(即实际周期 = PWMPR 值 + 2),最终写入 `PWMPR = period_cycles - 2`(若结果 ≤2 则写 0),占空比写入 `PWMSAR = duty_cycles`。 --- ## 相关页面 - [[05-GPIO与LED驱动]] - [[07-按键输入驱动]] - [[08-I2C驱动]] - [[09-SPI驱动]] - [[10-网络设备驱动]] --- **内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第六十章 Linux RTC驱动实验 **内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第七十三章 Linux PWM驱动实验