--- title: PWM与看门狗 tags: [嵌入式Linux, Linux应用编程, PWM, sysfs, 看门狗, watchdog, ioctl, IMX6ULL] created: 2026-09-18 updated: 2026-09-18 pdf_ref: "《I.MX6U嵌入式Linux C应用编程指南V1.6》第二十四章 PWM应用编程、第二十七章 看门狗应用编程" --- # PWM与看门狗 > 💡 **关联知识**:[[03-外设与高级IO编程/02-GPIO与LED应用编程]]、[[03-外设与高级IO编程/04-FrameBuffer与LCD应用编程]]、[[03-外设与高级IO编程/06-摄像头串口与音频]];延伸阅读:[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/06-RTC与PWM驱动]]、[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/01-LED与蜂鸣器驱动]] 本篇讲两个外设:**PWM**(脉冲宽度调制,用于背光调光、蜂鸣器发声、电机调速)和**看门狗**(Watchdog,用于系统异常时自动复位)。二者在应用层的操作方式很不一样——PWM 通过 **sysfs 属性文件**读写,看门狗通过 **ioctl** 控制,但都属于"把内核驱动暴露的接口用好"这一类应用编程。 --- ## 第一部分:PWM 应用编程 ## 1. PWM 基本原理 PWM(Pulse Width Modulation,脉冲宽度调制)是一种通过**数字信号占空比**来模拟不同输出的技术。一个 PWM 波形由三个要素描述: | 要素 | 含义 | 单位 | | ---- | ---- | ---- | | 周期 `period` | 一个完整波形的时间长度 | ns(纳秒) | | 频率 `f` | 周期的倒数,`f = 1 / period` | Hz | | 占空比 `duty_cycle` | 一个周期内高电平的时间长度 | ns(纳秒) | | 极性 `polarity` | 高电平有效(normal)或低电平有效(inversed) | — | 占空比比例 = `duty_cycle / period`。例如周期 `500000 ns = 500 us`,对应频率 `1 / 500us = 2 kHz`;占空比 `250000 ns` 占周期一半,即 50%。 ```mermaid flowchart LR subgraph 一个周期 period H["高电平 duty_cycle"] --> L["低电平 period - duty_cycle"] end P["period = 500000ns (500us)"] --> F["频率 f = 1/period = 2kHz"] H --> D["占空比 = duty_cycle/period"] ``` PWM 的典型用途:LCD 背光调光(改变占空比即改变平均亮度)、无源蜂鸣器发声(改变频率即改变音调)、电机调速等。I.MX6U 共有 **8 个 PWM 控制器(PWM1~PWM8)**,每个控制器只有 1 路 PWM 输出。 --- ## 2. 应用层如何操控 PWM:sysfs 与 LED 一样,PWM 通过 **sysfs** 操控。进入 `/sys/class/pwm` 目录,可以看到 8 个以 `pwmchipX`(X 为 0~7)命名的文件夹,分别对应 I.MX6U 的 8 个 PWM 控制器: ```bash ls /sys/class/pwm # pwmchip0 pwmchip1 ... pwmchip7 ``` 进入某个控制器目录(如 `pwmchip0`),重点关注 `npwm`、`export`、`unexport`: | 属性文件 | 读写 | 说明 | | -------- | ---- | ---- | | `npwm` | 只读 | 该 PWM 控制器下共有几路 PWM 输出 | | `export` | 只写 | 导出 PWM,写入编号(从 0 开始) | | `unexport` | 只写 | 删除已导出的 PWM,写入编号与 export 对应 | I.MX6U 每个 PWM 控制器只有 1 路输出,因此 `npwm` 读出来是 1,导出时编号都用 0。 ```bash echo 0 > /sys/class/pwm/pwmchip0/export # 导出 PWM1 echo 0 > /sys/class/pwm/pwmchip1/export # 导出 PWM2 # ... pwmchip2~pwmchip5 同理,编号都是 0 ``` 导出成功后会生成一个 `pwm0` 目录。 > 注意:开发板出厂系统中,**PWM1 已被用作 LCD 背光控制**,应用层不能直接控制;其它 PWM 因 I/O 资源不够也均不能使用。若要用 PWM1 测试,需要先禁用 LCD 与 backlight(见第 4 节)。 ### 2.1 控制 PWM:pwm0 目录下的属性 ```bash ls /sys/class/pwm/pwmchip0/pwm0 # duty_cycle enable period polarity ``` | 属性文件 | 读写 | 说明 | | -------- | ---- | ---- | | `enable` | 可读写 | 写 `1` 使能 PWM 输出,写 `0` 禁止;读可获取当前状态 | | `polarity` | 可读写 | `normal` 普通极性、`inversed` 反转极性 | | `period` | 可读写 | 配置 PWM 周期,单位 ns | | `duty_cycle` | 可读写 | 配置 PWM 占空比,单位 ns | ```bash echo 0 > enable # 禁止 PWM 输出 echo 1 > enable # 使能 PWM 输出(通常配置好后再使能) echo normal > polarity # 默认极性 echo inversed > polarity # 极性反转 echo 10000 > period # PWM 周期设置为 10us(10 * 1000ns) echo 5000 > duty_cycle # 占空比设置为 5us ``` > 很多 SoC 的 PWM 外设在硬件上并不支持极性配置,对应驱动未实现该接口,应用层自然无法通过 `polarity` 配置。ALPHA/Mini I.MX6U 出厂系统便如此。 --- ## 3. 编写 PWM 应用程序(pwm.c) 程序接收 3 个参数:PWM 编号、周期(ns)、占空比(ns)。先确保 PWM 已导出,再依次配置周期、占空比并使能。 ```c #include #include #include #include #include #include #include static char pwm_path[100]; static int pwm_config(const char *attr, const char *val) { char file_path[100]; int len; int fd; sprintf(file_path, "%s/%s", pwm_path, attr); if (0 > (fd = open(file_path, O_WRONLY))) { perror("open error"); return fd; } len = strlen(val); if (len != write(fd, val, len)) { perror("write error"); close(fd); return -1; } close(fd); //关闭文件 return 0; } int main(int argc, char *argv[]) { /* 校验传参 */ if (4 != argc) { fprintf(stderr, "usage: %s \n", argv[0]); exit(-1); } /* 打印配置信息 */ printf("PWM config: id<%s>, period<%s>, duty<%s>\n", argv[1], argv[2], argv[3]); /* 导出pwm */ sprintf(pwm_path, "/sys/class/pwm/pwmchip%s/pwm0", argv[1]); if (access(pwm_path, F_OK)) {//如果pwm0目录不存在, 则导出 char temp[100]; int fd; sprintf(temp, "/sys/class/pwm/pwmchip%s/export", argv[1]); if (0 > (fd = open(temp, O_WRONLY))) { perror("open error"); exit(-1); } if (1 != write(fd, "0", 1)) {//导出pwm perror("write error"); close(fd); exit(-1); } close(fd); //关闭文件 } /* 配置PWM周期 */ if (pwm_config("period", argv[2])) exit(-1); /* 配置占空比 */ if (pwm_config("duty_cycle", argv[3])) exit(-1); /* 使能pwm */ pwm_config("enable", "1"); /* 退出程序 */ exit(0); } ``` 逐段解释: | 位置 | 代码 | 说明 | | ---- | ---- | ---- | | `pwm_config()` | 通用属性写函数 | 拼接 `pwm_path/attr` 路径,以只写方式打开并写入值,是操作 sysfs 属性的标准写法 | | 传参校验 | `4 != argc` | 需传入 `id`、`period`、`duty` 三个参数 | | `sprintf(pwm_path, ...)` | 构造 `pwm0` 路径 | 如 `id=0` 时为 `/sys/class/pwm/pwmchip0/pwm0` | | `access(pwm_path, F_OK)` | 判断是否已导出 | 目录不存在说明未导出,需要写 `export` | | 写 `export` | `write(fd, "0", 1)` | 导出编号 0 的 PWM | | `pwm_config("period", ...)` | 配置周期 | 单位 ns | | `pwm_config("duty_cycle", ...)` | 配置占空比 | 单位 ns | | `pwm_config("enable", "1")` | 使能输出 | 配置完成后最后一步 | 编译: ```bash arm-linux-gnueabihf-gcc -o testApp pwm.c scp testApp root@192.168.1.10:/home/root/ ``` --- ## 4. 在开发板上测试 ### 4.1 先把 PWM1 腾出来 出厂系统无法直接使用 PWM,因为 PWM1 被 LCD 背光占用。要想测试,需要对设备树做配置、禁用 LCD 和 backlight 设备(`status` 属性设置为 `disabled`),再重新编译设备树、用生成的 dtb 替换开发板启动文件中的 dtb。也可以参考《I.MX6U 嵌入式 Linux 驱动开发指南》自行配置 PWM。 这里介绍一个**不用重新编译设备树**的简单方法:重启开发板进入 u-boot 命令行模式,u-boot 启动时会把内核设备树(dtb)拷贝到内存,我们直接查看、修改内存中的那份设备树,把 LCD 和 backlight 的 `status` 改为 `disabled`。 SD/eMMC 启动方式,依次执行: ```text setenv disable_lcd 'fdt addr ${fdt_addr}; fdt set /backlight status disable; fdt set /soc/aips-bus@02100000/lcdif@021c8000 status disable' setenv mmcboot 'echo Booting from mmc ...; run mmcargs; if test ${boot_fdt} = yes || test ${boot_fdt} = try; then if run loadfdt; then run disable_lcd; bootz ${loadaddr} - ${fdt_addr}; else if test ${boot_fdt} = try; then bootz; else echo WARN: Cannot load the DT; fi; fi; else bootz; fi;' ``` NAND 启动方式: ```text setenv disable_lcd 'fdt addr ${fdt_addr}; fdt set /backlight status disable; fdt set /soc/aips-bus@02100000/lcdif@021c8000 status disable' setenv bootcmd 'nand read ${loadaddr} 0x620000 0x800000;nand read ${fdt_addr} ${fdt_offset} 0x20000; run disable_lcd; bootz ${loadaddr} - ${fdt_addr}' ``` 然后执行 `boot` 启动内核。系统启动后 PWM1 就腾出来了,此时 LCD 被禁用。 > Tips:这种修改方式**只对本次启动生效**,因为我们改的是内存中的设备树;下一次重启会恢复原状。 ### 4.2 运行并验证波形 开发板出厂系统已将 PWM1 输出绑定到 **GPIO1_IO08** 引脚(即 LCD 背光引脚),该引脚已通过扩展口引出,可用示波器测量。 ```bash ./testApp 0 500000 250000 # PWM config: id<0>, period<500000>, duty<250000> ``` 本实验测试 PWM1。用示波器检测 GPIO1_IO08 引脚,应看到周期 `500us`(即 `500000ns`)、对应频率 `2kHz`、占空比 50% 的 PWM 波形,与配置一致。 ### 4.3 调试方法 | 现象 | 可能原因 | 排查手段 | | ---- | -------- | -------- | | `open error: No such file or directory` | 没进 u-boot 禁用 LCD/backlight,或路径/编号写错 | 确认 `ls /sys/class/pwm/pwmchip0/pwm0` | | 写属性报错 | 未先导出、或写入格式不对 | 先 `echo 0 > export`,写入的值不带单位 | | 引脚无波形 | PWM 未使能 / 被其它功能占用 | `cat enable` 应为 1;确认 PWM1 已腾出 | | 占空比大于周期 | 参数不合法 | `duty_cycle` 必须不超过 `period` | | 无 `pwm0` 目录 | 未导出 | 程序会自动导出,也可手动 `echo 0 > export` | ### 4.4 PWM 蜂鸣器/背光实验 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。 - **背光调光**:PWM1 原本就驱动 LCD 背光,改变 `duty_cycle / period` 即可改变平均亮度。运行 `./testApp 0 500000 100000`(20% 占空比)与 `./testApp 0 500000 400000`(80% 占空比),可对比亮度差异。 - **蜂鸣器发声**:无源蜂鸣器需要外部方波驱动,**频率决定音调、占空比影响响度**。可把 PWM 输出经扩展口接到无源蜂鸣器,用 `pwm.c` 改变 `period` 产生不同音调,例如 `period=1000000ns` 对应 1kHz。有源蜂鸣器自带振荡电路,只需电平驱动即可,不需要 PWM。 - 注意:实验前同样需要按第 4.1 节腾出 PWM 通道,并确认引脚连接正确,避免与 LCD 背光功能冲突。 --- ## 第二部分:看门狗应用编程 ## 5. 看门狗原理 在产品化的嵌入式系统中,为使系统在异常情况下能自动复位,一般都需要引入看门狗。看门狗其实就是一个**可以在一定时间内被复位(重置)的计数器**(看门狗计时器/定时器): - 看门狗启动后,计数器开始自动计数; - 经过一定时间若没有被复位,计数器溢出就会对 CPU 产生一个**复位信号使系统重启**(俗称"被狗咬"); - 系统正常运行时,需在看门狗允许的时间间隔内对计数器清零(俗称"喂狗"),不让复位信号产生; - 若程序跑飞、没有按时喂狗,系统就"被咬"复位。 有些看门狗也可以只产生中断信号而不使系统复位。 ```mermaid flowchart LR START["看门狗启动/喂狗
计数器清零"] --> COUNT["计数器递减/递增"] COUNT --> Q{"计时期内
是否喂狗?"} Q -->|"是"| START Q -->|"否(超时溢出)"| RST["产生复位信号
系统重启"] ``` I.MX6UL/I.MX6ULL SoC 集成了两个看门狗定时器(WDOG):**WDOG1 和 WDOG2**。其中 WDOG2 用于安全目的,WDOG1 则是一个普通看门狗,支持产生中断信号以及复位 CPU。 --- ## 6. 看门狗设备节点 Linux 系统中注册的看门狗外设,都会在 `/dev/` 下生成设备节点,名称通常为 `watchdogX`(X 为编号 0、1、2…),如 `/dev/watchdog0`、`/dev/watchdog1`。 ```bash ls /dev/watchdog* # /dev/watchdog /dev/watchdog0 ``` `watchdog0` 就是 I.MX6U 的 WDOG1 对应的设备节点。而 `/dev/watchdog` 名称不带数字编号,它代表**系统默认的看门狗设备**,通常指的就是 `watchdog0`——即这两个节点代表同一个硬件外设。 --- ## 7. 通过 ioctl 控制看门狗 应用层控制看门狗通过 `ioctl()` 实现。程序中需要包含头文件: ```c #include ``` 该头文件定义了各种 `ioctl` 指令宏,每个指令表示向设备请求不同操作: ```c #define WDIOC_GETSUPPORT _IOR(WATCHDOG_IOCTL_BASE, 0, struct watchdog_info) #define WDIOC_GETSTATUS _IOR(WATCHDOG_IOCTL_BASE, 1, int) #define WDIOC_GETBOOTSTATUS _IOR(WATCHDOG_IOCTL_BASE, 2, int) #define WDIOC_GETTEMP _IOR(WATCHDOG_IOCTL_BASE, 3, int) #define WDIOC_SETOPTIONS _IOR(WATCHDOG_IOCTL_BASE, 4, int) #define WDIOC_KEEPALIVE _IOR(WATCHDOG_IOCTL_BASE, 5, int) #define WDIOC_SETTIMEOUT _IOWR(WATCHDOG_IOCTL_BASE, 6, int) #define WDIOC_GETTIMEOUT _IOR(WATCHDOG_IOCTL_BASE, 7, int) #define WDIOC_SETPRETIMEOUT _IOWR(WATCHDOG_IOCTL_BASE, 8, int) #define WDIOC_GETPRETIMEOUT _IOR(WATCHDOG_IOCTL_BASE, 9, int) #define WDIOC_GETTIMELEFT _IOR(WATCHDOG_IOCTL_BASE, 10, int) ``` 常用指令: | ioctl 指令 | 作用 | | ---------- | ---- | | `WDIOC_GETSUPPORT` | 获取看门狗支持哪些功能 | | `WDIOC_SETOPTIONS` | 开启或关闭看门狗 | | `WDIOC_KEEPALIVE` | 喂狗操作 | | `WDIOC_SETTIMEOUT` | 设置看门狗超时时间 | | `WDIOC_GETTIMEOUT` | 获取看门狗超时时间 | ### 7.1 打开设备 ```c int fd; fd = open("/dev/watchdog", O_RDWR); if (0 > fd) fprintf(stderr, "open error: %s: %s\n", "/dev/watchdog", strerror(errno)); ``` ### 7.2 获取设备支持的功能:WDIOC_GETSUPPORT ```c ioctl(int fd, WDIOC_GETSUPPORT, struct watchdog_info *info); ``` 需要传入一个 `struct watchdog_info *`,`ioctl` 会把获取到的数据写入该对象: ```c struct watchdog_info { __u32 options; /* Options the card/driver supports */ __u32 firmware_version; /* Firmware version of the card */ __u8 identity[32]; /* Identity of the board */ }; ``` - `options`:设备支持哪些功能或选项; - `firmware_version`:固件版本号; - `identity`:描述性字符串。 `options` 字段可取以下任意一个值或多值的位或: ```c #define WDIOF_OVERHEAT 0x0001 /* Reset due to CPU overheat */ #define WDIOF_FANFAULT 0x0002 #define WDIOF_EXTERN1 0x0004 #define WDIOF_POWERUNDER 0x0010 #define WDIOF_CARDRESET 0x0020 #define WDIOF_POWEROVER 0x0040 #define WDIOF_SETTIMEOUT 0x0080 /* Set timeout (in seconds) */ #define WDIOF_MAGICCLOSE 0x0100 /* Supports magic close char */ #define WDIOF_PRETIMEOUT 0x0200 /* Pretimeout (in seconds), get/set */ #define WDIOF_ALARMONLY 0x0400 /* Watchdog triggers alarm, not a reboot */ #define WDIOF_KEEPALIVEPING 0x8000 /* Keep alive ping reply */ ``` 常见的值有 `WDIOF_SETTIMEOUT`(设备支持设置超时时间)与 `WDIOF_KEEPALIVEPING`(设备支持"喂狗",即重置计时器)。 ```c struct watchdog_info info; if (0 > ioctl(fd, WDIOC_GETSUPPORT, &info)) { fprintf(stderr, "ioctl error: WDIOC_GETSUPPORT: %s\n", strerror(errno)); return -1; } printf("identity: %s\n", info.identity); printf("version: %u\n", info.firmware_version); if (0 == (WDIOF_KEEPALIVEPING & info.options)) printf("设备不支持喂狗操作\n"); if (0 == (WDIOF_SETTIMEOUT & info.options)) printf("设备不支持设置超时时间\n"); ``` ### 7.3 获取/设置超时时间 ```c ioctl(int fd, WDIOC_GETTIMEOUT, int *timeout); // 获取 ioctl(int fd, WDIOC_SETTIMEOUT, int *timeout); // 设置 ``` 超时时间以**秒**为单位。设置时不可超过其最大值,否则 `ioctl` 调用会失败。 ```c int timeout; if (0 > ioctl(fd, WDIOC_GETTIMEOUT, &timeout)) { fprintf(stderr, "ioctl error: WDIOC_GETTIMEOUT: %s\n", strerror(errno)); return -1; } printf("current timeout: %ds\n", timeout); timeout = 10; //10 秒钟 if (0 > ioctl(fd, WDIOC_SETTIMEOUT, &timeout)) { fprintf(stderr, "ioctl error: WDIOC_SETTIMEOUT: %s\n", strerror(errno)); return -1; } ``` ### 7.4 开启/关闭看门狗:WDIOC_SETOPTIONS ```c ioctl(int fd, WDIOC_SETOPTIONS, int *option); ``` `option` 指向一个 `int` 变量,可取: ```c #define WDIOS_DISABLECARD 0x0001 /* Turn off the watchdog timer */ #define WDIOS_ENABLECARD 0x0002 /* Turn on the watchdog timer */ ``` ```c int option = WDIOS_ENABLECARD; //开启 //int option = WDIOS_DISABLECARD; //停止 if (0 > ioctl(fd, WDIOC_SETOPTIONS, &option)) { fprintf(stderr, "ioctl error: WDIOC_SETOPTIONS: %s\n", strerror(errno)); return -1; } ``` > **重要**:当调用 `open()` 打开看门狗设备时,即使程序中没有开启看门狗计时器,`close()` 关闭设备时看门狗也会**自动启动**。所以打开设备后,需要先用 `WDIOC_SETOPTIONS` 指令(`WDIOS_DISABLECARD`)停止计时,等所有设置完成之后再开启。 ### 7.5 喂狗:WDIOC_KEEPALIVE ```c ioctl(int fd, WDIOC_KEEPALIVE, NULL); ``` 计时器启动后,必须在超时之前"喂狗",否则计时器溢出将导致系统复位或产生中断: ```c if (0 > ioctl(fd, WDIOC_KEEPALIVE, NULL)) { fprintf(stderr, "ioctl error: WDIOC_KEEPALIVE: %s\n", strerror(errno)); } ``` --- ## 8. 看门狗应用程序实战(watchdog_test.c) 程序打开看门狗后先停止计时,设置超时时间,再开启计时并在超时前不断喂狗。 ```c #include #include #include #include #include #include #include #include #include #include #define WDOG_DEV "/dev/watchdog" int main(int argc, char *argv[]) { struct watchdog_info info; int timeout; int time; int fd; int op; if (2 != argc) { fprintf(stderr, "usage: %s \n", argv[0]); exit(EXIT_FAILURE); } /* 打开看门狗 */ fd = open(WDOG_DEV, O_RDWR); if (0 > fd) { fprintf(stderr, "open error: %s: %s\n", WDOG_DEV, strerror(errno)); exit(EXIT_FAILURE); } /* 打开之后看门狗计时器会开启、先停止它 */ op = WDIOS_DISABLECARD; if (0 > ioctl(fd, WDIOC_SETOPTIONS, &op)) { fprintf(stderr, "ioctl error: WDIOC_SETOPTIONS: %s\n", strerror(errno)); close(fd); exit(EXIT_FAILURE); } timeout = atoi(argv[1]); if (1 > timeout) timeout = 1; /* 设置超时时间 */ printf("timeout: %ds\n", timeout); if (0 > ioctl(fd, WDIOC_SETTIMEOUT, &timeout)) { fprintf(stderr, "ioctl error: WDIOC_SETTIMEOUT: %s\n", strerror(errno)); close(fd); exit(EXIT_FAILURE); } /* 开启看门狗计时器 */ op = WDIOS_ENABLECARD; if (0 > ioctl(fd, WDIOC_SETOPTIONS, &op)) { fprintf(stderr, "ioctl error: WDIOC_SETOPTIONS: %s\n", strerror(errno)); close(fd); exit(EXIT_FAILURE); } /* 喂狗 */ time = (timeout * 1000 - 100) * 1000;//喂狗时间设置us微秒、在超时时间到来前100ms喂狗 for ( ; ; ) { usleep(time); ioctl(fd, WDIOC_KEEPALIVE, NULL); } } ``` 逐段解释: | 位置 | 代码 | 说明 | | ---- | ---- | ---- | | 参数校验 | `2 != argc` | 需传入看门狗超时时间(秒) | | `open(WDOG_DEV, O_RDWR)` | 打开看门狗 | 以读写方式打开 `/dev/watchdog` | | `WDIOC_SETOPTIONS` + `WDIOS_DISABLECARD` | 先停止计时 | 因为打开设备后计时器会开启 | | `atoi(argv[1])` | 解析超时时间 | 小于 1 秒则强制为 1 | | `WDIOC_SETTIMEOUT` | 设置超时 | 单位秒,不能超过最大值 | | `WDIOC_SETOPTIONS` + `WDIOS_ENABLECARD` | 开启计时 | 开始看门狗工作 | | `time = (timeout*1000 - 100)*1000` | 计算喂狗间隔 | `usleep` 以微秒为单位,在超时前 100ms 喂狗 | | `WDIOC_KEEPALIVE` | 循环喂狗 | 不断重置计时器,避免系统复位 | 编译: ```bash arm-linux-gnueabihf-gcc -o testApp watchdog_test.c scp testApp root@192.168.1.10:/home/root/ ``` --- ## 9. 在开发板上测试看门狗 把可执行文件拷贝到开发板 `/home/root`,执行测试程序,例如启动看门狗、设置超时时间为 2 秒: ```bash ./testApp 2 ``` ### 9.1 解决 "Device or resource busy" 第一次执行可能会报错: ```text open error: /dev/watchdog: Device or resource busy ``` 这是因为**看门狗默认已被其他功能(出厂系统的看门狗脚本)使用**。解决方法是手动关闭它:打开 `/etc/init.d/watchdog.sh` 文件,在开头加一行 `exit 0`,把整个文件(后续逻辑)跳过。 ```bash vi /etc/init.d/watchdog.sh # 在文件开头加入: # exit 0 ``` 然后重启开发板,再执行程序,看门狗计时器就已启动,程序会不断喂狗、重置计时器,保证不重启: ```bash ./testApp 2 # timeout: 2s ``` ### 9.2 验证不喂狗会复位 程序运行中按 **Ctrl + C** 结束程序。结束程序意味着**停止喂狗了,但看门狗计时器并没有停止**,于是计时器会溢出、发生复位重启: ```text ./testApp 2 timeout: 2s ^C watchdog watchdog0: watchdog did not stop! <- 内核打印:计时器仍在计时、未停止 (随后系统复位重启) ``` 内核打印的 `watchdog watchdog0: watchdog did not stop!` 表示看门狗计时器还正在计时、未被停止。 测试完毕后,把 `/etc/init.d/watchdog.sh` 文件中的 `exit 0` 去掉,让系统重新使用看门狗。 ### 9.3 调试方法 | 现象 | 可能原因 | 排查手段 | | ---- | -------- | -------- | | `Device or resource busy` | 看门狗已被系统占用 | 在 `/etc/init.d/watchdog.sh` 开头加 `exit 0` 后重启 | | 设置超时失败 | 超过设备支持的最大值 | 用 `WDIOC_GETTIMEOUT` 读取,或改小参数 | | 程序退出后系统立即复位 | `close` 会自动启动看门狗,且退出后无人喂狗 | 正常现象,用于验证;测试完恢复 `watchdog.sh` | | 设备不支持喂狗/设置超时 | 硬件/驱动不支持 | 用 `WDIOC_GETSUPPORT` 检查 `options` 位 | | `open` 失败无权限 | 非 root 运行 | 用 `root` 执行 | --- ## 10. 跨平台对比:IMX6ULL vs STM32 vs RK3568 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。 | 维度 | I.MX6ULL(本教程) | STM32(裸机/RTOS) | RK3568 | | ---- | ------------------ | ------------------ | ------ | | PWM 接口 | sysfs `/sys/class/pwm/pwmchipN` | 定时器寄存器 / HAL_TIM_PWM | sysfs,接口一致 | | PWM 配置 | 写 `period`/`duty_cycle`/`enable`(ns) | 配置 ARR/PSC/CCR 寄存器 | 写 `period`/`duty_cycle`/`enable`(ns) | | 看门狗接口 | `/dev/watchdog` + `ioctl` | IWDG/WWDG 寄存器直接喂狗 | `/dev/watchdog` + `ioctl`,接口一致 | | 看门狗喂狗 | `WDIOC_KEEPALIVE` | 写 IWDG_KR 重装载键 | `WDIOC_KEEPALIVE` | | 交叉工具链 | `arm-linux-gnueabihf-gcc` | `arm-none-eabi-gcc` | `aarch64-linux-gnu-gcc` | I.MX6ULL 与 RK3568 的 sysfs PWM 与 ioctl 看门狗用法基本通用;STM32 裸机没有 sysfs 与设备节点,需直接操作寄存器。 --- ## 11. 面试精选 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux C应用编程指南》内容,为扩展知识。 ### Q1:PWM 的周期、频率、占空比是什么关系?如何用 sysfs 配置? **答**:周期是波形一个完整循环的时间长度,频率是周期的倒数(`f = 1/period`),占空比是一个周期内高电平的时间长度,占空比比例 = `duty_cycle / period`。在 sysfs 中,先 `echo 0 > /sys/class/pwm/pwmchipN/export` 导出,再向 `pwm0/period` 与 `pwm0/duty_cycle` 写入纳秒值,最后 `echo 1 > pwm0/enable` 使能。例如 `period=500000`、`duty_cycle=250000` 表示 2kHz、50%。 ### Q2:sysfs 操作 PWM 的完整流程是怎样的? **答**:进入 `/sys/class/pwm/pwmchipN`,用 `npwm` 查看路数;向 `export` 写入编号(I.MX6U 为 0)导出,生成 `pwm0` 目录;在 `pwm0` 下配置 `period`、`duty_cycle`(纳秒),可选配置 `polarity`;最后写 `enable=1` 使能;用完向 `unexport` 写入编号删除。程序中通常先 `access()` 判断是否已导出,未导出则先写 `export`。 ### Q3:看门狗的工作原理是什么?为什么 open 之后要先关闭它? **答**:看门狗是一个可在一定时间内被复位的计数器,启动后自动计数;若在超时前未被复位(喂狗),计数器溢出就会对 CPU 产生复位信号使系统重启。系统正常时程序按时喂狗,程序跑飞则不喂狗、系统自动复位,从而提高可靠性。至于 `open` 之后要先关闭:因为打开看门狗设备后即使程序没开启计时器,`close()` 关闭设备时看门狗也会自动启动,所以打开后应先用 `WDIOC_SETOPTIONS` + `WDIOS_DISABLECARD` 停止计时,待一切设置完成后再开启,避免在配置过程中意外触发。 ### Q4:看门狗常用的 ioctl 指令有哪些,各起什么作用? **答**:`WDIOC_GETSUPPORT` 获取设备支持的功能(如是否支持设置超时、是否支持喂狗);`WDIOC_SETOPTIONS` 配合 `WDIOS_ENABLECARD`/`WDIOS_DISABLECARD` 开启或关闭看门狗计时器;`WDIOC_SETTIMEOUT`/`WDIOC_GETTIMEOUT` 设置/获取超时时间(秒);`WDIOC_KEEPALIVE` 执行喂狗、重置计时器。这些指令都需要先用 `open("/dev/watchdog", O_RDWR)` 得到文件描述符。 ### Q5:如何验证看门狗确实会复位系统?为什么执行程序会提示 `Device or resource busy`? **答**:启动看门狗并设置超时(如 2 秒),程序正常运行时会不断喂狗。按 Ctrl+C 结束程序,即停止喂狗,但看门狗计时器仍在计时,超时后就会复位系统;内核会打印 `watchdog watchdog0: watchdog did not stop!`。提示 `Device or resource busy` 是因为看门狗默认已被系统功能(`/etc/init.d/watchdog.sh`)占用,可在该脚本开头加 `exit 0` 并重启开发板后再测试,测试完记得去掉。 --- ## 延伸阅读 - PWM 驱动原理:[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/06-RTC与PWM驱动]] - 蜂鸣器/LED 驱动:[[嵌入式Linux驱动开发实战/05-Linux外设驱动实战/01-LED与蜂鸣器驱动]] - 本库相关:[[03-外设与高级IO编程/02-GPIO与LED应用编程]]、[[03-外设与高级IO编程/04-FrameBuffer与LCD应用编程]]、[[03-外设与高级IO编程/06-摄像头串口与音频]] --- **内容来源**:《I.MX6U嵌入式Linux C应用编程指南》第二十四章 PWM应用编程、第二十七章 看门狗应用编程;例程源码 `24_pwm/pwm.c`、`27_watchdog/watchdog_test.c`