05-PWM与看门狗.md 30 KB


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%。

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 控制器:

ls /sys/class/pwm
# pwmchip0  pwmchip1  ...  pwmchip7

进入某个控制器目录(如 pwmchip0),重点关注 npwmexportunexport

属性文件 读写 说明
npwm 只读 该 PWM 控制器下共有几路 PWM 输出
export 只写 导出 PWM,写入编号(从 0 开始)
unexport 只写 删除已导出的 PWM,写入编号与 export 对应

I.MX6U 每个 PWM 控制器只有 1 路输出,因此 npwm 读出来是 1,导出时编号都用 0。

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 目录下的属性

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
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 已导出,再依次配置周期、占空比并使能。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

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 <id> <period> <duty>\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 需传入 idperiodduty 三个参数
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") 使能输出 配置完成后最后一步

编译:

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 启动方式,依次执行:

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 启动方式:

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 背光引脚),该引脚已通过扩展口引出,可用示波器测量。

./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 产生一个复位信号使系统重启(俗称"被狗咬");
  • 系统正常运行时,需在看门狗允许的时间间隔内对计数器清零(俗称"喂狗"),不让复位信号产生;
  • 若程序跑飞、没有按时喂狗,系统就"被咬"复位。

有些看门狗也可以只产生中断信号而不使系统复位。

flowchart LR
    START["看门狗启动/喂狗<br/>计数器清零"] --> COUNT["计数器递减/递增"]
    COUNT --> Q{"计时期内<br/>是否喂狗?"}
    Q -->|"是"| START
    Q -->|"否(超时溢出)"| RST["产生复位信号<br/>系统重启"]

I.MX6UL/I.MX6ULL SoC 集成了两个看门狗定时器(WDOG):WDOG1 和 WDOG2。其中 WDOG2 用于安全目的,WDOG1 则是一个普通看门狗,支持产生中断信号以及复位 CPU。


6. 看门狗设备节点

Linux 系统中注册的看门狗外设,都会在 /dev/ 下生成设备节点,名称通常为 watchdogX(X 为编号 0、1、2…),如 /dev/watchdog0/dev/watchdog1

ls /dev/watchdog*
# /dev/watchdog  /dev/watchdog0

watchdog0 就是 I.MX6U 的 WDOG1 对应的设备节点。而 /dev/watchdog 名称不带数字编号,它代表系统默认的看门狗设备,通常指的就是 watchdog0——即这两个节点代表同一个硬件外设。


7. 通过 ioctl 控制看门狗

应用层控制看门狗通过 ioctl() 实现。程序中需要包含头文件:


#include <linux/watchdog.h>

该头文件定义了各种 ioctl 指令宏,每个指令表示向设备请求不同操作:


#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 打开设备

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

ioctl(int fd, WDIOC_GETSUPPORT, struct watchdog_info *info);

需要传入一个 struct watchdog_info *ioctl 会把获取到的数据写入该对象:

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 字段可取以下任意一个值或多值的位或:

#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(设备支持"喂狗",即重置计时器)。

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 获取/设置超时时间

ioctl(int fd, WDIOC_GETTIMEOUT, int *timeout);   // 获取
ioctl(int fd, WDIOC_SETTIMEOUT, int *timeout);   // 设置

超时时间以为单位。设置时不可超过其最大值,否则 ioctl 调用会失败。

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

ioctl(int fd, WDIOC_SETOPTIONS, int *option);

option 指向一个 int 变量,可取:


#define WDIOS_DISABLECARD 0x0001 /* Turn off the watchdog timer */
#define WDIOS_ENABLECARD  0x0002 /* Turn on the watchdog timer */

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

ioctl(int fd, WDIOC_KEEPALIVE, NULL);

计时器启动后,必须在超时之前"喂狗",否则计时器溢出将导致系统复位或产生中断:

if (0 > ioctl(fd, WDIOC_KEEPALIVE, NULL)) {
    fprintf(stderr, "ioctl error: WDIOC_KEEPALIVE: %s\n", strerror(errno));
}


8. 看门狗应用程序实战(watchdog_test.c)

程序打开看门狗后先停止计时,设置超时时间,再开启计时并在超时前不断喂狗。


#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <string.h>
#include <linux/watchdog.h>

#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 <timeout>\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 循环喂狗 不断重置计时器,避免系统复位

编译:

arm-linux-gnueabihf-gcc -o testApp watchdog_test.c
scp testApp root@192.168.1.10:/home/root/


9. 在开发板上测试看门狗

把可执行文件拷贝到开发板 /home/root,执行测试程序,例如启动看门狗、设置超时时间为 2 秒:

./testApp 2

9.1 解决 "Device or resource busy"

第一次执行可能会报错:

open error: /dev/watchdog: Device or resource busy

这是因为看门狗默认已被其他功能(出厂系统的看门狗脚本)使用。解决方法是手动关闭它:打开 /etc/init.d/watchdog.sh 文件,在开头加一行 exit 0,把整个文件(后续逻辑)跳过。

vi /etc/init.d/watchdog.sh

# 在文件开头加入:
# exit 0

然后重启开发板,再执行程序,看门狗计时器就已启动,程序会不断喂狗、重置计时器,保证不重启:

./testApp 2

# timeout: 2s

9.2 验证不喂狗会复位

程序运行中按 Ctrl + C 结束程序。结束程序意味着停止喂狗了,但看门狗计时器并没有停止,于是计时器会溢出、发生复位重启:

./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/periodpwm0/duty_cycle 写入纳秒值,最后 echo 1 > pwm0/enable 使能。例如 period=500000duty_cycle=250000 表示 2kHz、50%。

Q2:sysfs 操作 PWM 的完整流程是怎样的?

:进入 /sys/class/pwm/pwmchipN,用 npwm 查看路数;向 export 写入编号(I.MX6U 为 0)导出,生成 pwm0 目录;在 pwm0 下配置 periodduty_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.c27_watchdog/watchdog_test.c