36、Linux内核中的NVMEM与看门狗设备驱动详解

Linux内核中的NVMEM与看门狗设备驱动详解

1. NVMEM框架概述

NVMEM(Non-Volatile Memory)设备在Linux内核中扮演着重要角色,它可以从内核空间或用户空间进行访问。在这个框架中,存在生产者(提供者)和消费者两种角色。生产者驱动负责暴露设备存储,而消费者驱动则使用这些存储。

2. 编写NVMEM提供者驱动

提供者驱动的主要任务包括:
- 依据设备数据手册提供合适的NVMEM配置,并实现访问内存的例程。
- 向系统注册设备。
- 提供设备树绑定文档。

2.1 NVMEM设备的注册与注销

可以使用 nvmem_register() / nvmem_unregister() 函数,或者其托管版本 devm_nvmem_register() / devm_nvmem_unregister() 来进行设备的注册与注销。

struct nvmem_device *nvmem_register(const struct nvmem_config *config)
struct nvmem_device *devm_nvmem_register(struct device *dev, const struct nvmem_config *config)
int nvmem_unregister(struct nvmem_device *nvmem)
int devm_nvmem_unregister(struct device *dev, struct nvmem_device *nvmem)

注册成功后,会在 /sys/bus/nvmem/devices/dev-name/nvmem 创建二进制条目。

2.2 RTC设备中的NVMEM存储

许多实时时钟(RTC)设备嵌入了非易失性存储,如EEPROM或电池备份RAM。在 include/linux/rtc.h 中的 struct rtc_device 结构体包含了与NVMEM相关的字段:

struct rtc_device {
    [...]
    struct nvmem_device *nvmem;
    /* Old ABI support */
    bool nvram_old_abi;
    struct bin_attribute *nvram;
    [...]
}
  • nvmem :抽象底层硬件内存。
  • nvram_old_abi :布尔值,指示是否使用旧的(现已弃用)NVRAM ABI注册RTC的NVMEM。
  • nvram :仅用于旧ABI支持的底层内存二进制属性。

RTC相关的NVMEM框架API可以通过 RTC_NVMEM 内核配置选项启用,定义在 drivers/rtc/nvmem.c 中,提供了 rtc_nvmem_register() rtc_nvmem_unregister() 函数。

以下是DS1307 RTC驱动的 probe 函数示例:

static int ds1307_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    struct ds1307 *ds1307;
    int err = -ENODEV;
    int tmp;
    const struct chip_desc *chip;

    [...]
    ds1307->rtc->ops = chip->rtc_ops ? : &ds13xx_rtc_ops;
    err = rtc_register_device(ds1307->rtc);
    if (err)
        return err;
    if (chip->nvram_size) {
        struct nvmem_config nvmem_cfg = {
            .name = " ds1307_nvram",
            .word_size = 1,
            .stride = 1,
            .size = chip->nvram_size,
            .reg_read = ds1307_nvram_read,
            .reg_write = ds1307_nvram_write,
            .priv = ds1307,
        };
        ds1307->rtc->nvram_old_abi = true;
        rtc_nvmem_register(ds1307->rtc, &nvmem_cfg);
    }
    [...]
}
3. 实现NVMEM读写回调

为了让内核和其他框架能够对NVMEM设备及其单元进行读写操作,每个NVMEM提供者必须暴露一对回调函数:

typedef int (*nvmem_reg_read_t)(void *priv, unsigned int offset, void *val, size_t bytes);
typedef int (*nvmem_reg_write_t)(void *priv, unsigned int offset, void *val, size_t bytes);
  • nvmem_reg_read_t :从NVMEM设备读取数据。
  • nvmem_reg_write_t :向NVMEM设备写入数据。
4. NVMEM提供者的设备树绑定

NVMEM数据提供者本身没有特定的绑定,应根据其父总线的设备树绑定进行描述。例如,I2C设备应作为代表其所在I2C总线的节点的子节点进行描述。

以下是一个MMIO NVMEM设备及其子节点的示例:

ocotp: ocotp@21bc000 {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = " fsl,imx6sx-ocotp", " syscon";
    reg = <0x021bc000 0x4000>;
    [...]
    tempmon_calib: calib@38 {
        reg = <0x38 4>;
    };
    tempmon_temp_grade: temp-grade@20 {
        reg = <0x20 4>;
    };
    foo: foo@6 {
        reg = <0x6 0x2> bits = <7 2>
    };
    [...]
};

数据单元绑定的可能属性:
- reg :描述NVMEM设备内数据区域的偏移量和大小。
- bits :可选属性,指定 reg 属性指定地址范围内的位偏移和位数。

可以使用 nvmem-cells 属性将数据单元分配给消费者,并使用 nvmem-cell-names 属性为每个数据单元命名。

5. NVMEM消费者驱动API

消费者驱动可以通过包含 <linux/nvmem-consumer.h> 来使用NVMEM消费者API:

struct nvmem_cell *nvmem_cell_get(struct device *dev, const char *name);
struct nvmem_cell *devm_nvmem_cell_get(struct device *dev, const char *name);
void nvmem_cell_put(struct nvmem_cell *cell);
void devm_nvmem_cell_put(struct device *dev, struct nvmem_cell *cell);
void *nvmem_cell_read(struct nvmem_cell *cell, size_t *len);
int nvmem_cell_write(struct nvmem_cell *cell, void *buf, size_t len);
int nvmem_cell_read_u32(struct device *dev, const char *cell_id, u32 *val);

以下是读取 tempmon 节点分配的单元内容的代码示例:

static int imx_init_from_nvmem_cells(struct platform_device *pdev)
{
    int ret; u32 val;
    ret = nvmem_cell_read_u32(&pdev->dev, " calib", &val);
    if (ret)
        return ret;
    ret = imx_init_calib(pdev, val);
    if (ret)
        return ret;
    ret = nvmem_cell_read_u32(&pdev->dev, " temp_grade", &val);
    if (ret)
        return ret;
    imx_init_temp_grade(pdev, val);
    return 0;
}
6. 用户空间的NVMEM

NVMEM用户空间接口依赖于sysfs。每个注册的NVMEM设备在 /sys/bus/nvmem/devices 下有一个目录条目,其中包含一个 nvmem 二进制文件。

int rval;
rval = ida_simple_get(&nvmem_ida, 0, 0, GFP_KERNEL);
nvmem->id = rval;
if (config->id == -1 && config->name) {
    dev_set_name(&nvmem->dev, "%s", config->name);
} else {
    dev_set_name(&nvmem->dev, "%s%d", config->name ? : " nvmem", config->name ? config->id : nvmem->id);
}

无论定义了多少单元,NV MEM框架通过NV MEM二进制文件暴露完整的寄存器空间。可以使用 hexdump cat 命令在用户空间读取NVMEM内容。

例如,对于I2C EEPROM:

cat /sys/bus/nvmem/devices/2-00550/nvmem
echo " foo" > /sys/bus/nvmem/devices/2-00550/nvmem
cat /sys/bus/nvmem/devices/2-00550/nvmem
7. 看门狗设备驱动概述

看门狗是一种硬件(有时由软件模拟)设备,用于确保系统的可用性。当系统出现严重故障时,它会触发系统重启,有助于监控系统的正常运行。

8. 技术要求

在开始学习看门狗设备驱动之前,需要具备以下条件:
- C编程技能。
- 基本的电子知识。
- Linux内核v4.19.X源代码,可从 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags 获取。

9. 看门狗数据结构和API

看门狗子系统有几个重要的数据结构,其中主要的是 struct watchdog_device

struct watchdog_device {
    int id;
    struct device *parent;
    const struct watchdog_info *info;
    const struct watchdog_ops *ops;
    const struct watchdog_governor *gov;
    unsigned int bootstatus;
    unsigned int timeout;
    unsigned int pretimeout;
    unsigned int min_timeout;
    struct watchdog_core_data *wd_data;
    unsigned long status;
    [...]
};

各字段含义如下:
| 字段 | 含义 |
| ---- | ---- |
| id | 内核在设备注册期间分配的看门狗ID。 |
| parent | 该设备的父设备。 |
| info | 指向 struct watchdog_info 结构体的指针,提供看门狗定时器的额外信息。 |
| ops | 指向看门狗操作列表的指针。 |
| gov | 指向看门狗预超时调节器的指针。 |
| bootstatus | 看门狗设备在启动时的状态。 |
| timeout | 看门狗设备的超时值(秒)。 |
| pretimeout | 预超时事件触发的时间间隔(秒)。 |
| min_timeout | 看门狗设备的最小超时值(秒)。 |
| wd_data | 指向看门狗核心内部数据的指针。 |
| status | 包含设备内部状态位的字段。 |

struct watchdog_info 结构体:

struct watchdog_info {
    u32 options;
    u32 firmware_version;
    u8 identity[32];
};
  • options :表示卡/驱动支持的功能。
  • firmware_version :卡的固件版本。
  • identity :描述设备的字符串。

struct watchdog_ops 结构体:

struct watchdog_ops {
    struct module *owner;
    /* mandatory operations */
    int (*start)(struct watchdog_device *);
    int (*stop)(struct watchdog_device *);
    /* optional operations */
    int (*ping)(struct watchdog_device *);
    unsigned int (*status)(struct watchdog_device *);
    int (*set_timeout)(struct watchdog_device *, unsigned int);
    int (*set_pretimeout)(struct watchdog_device *, unsigned int);
    unsigned int (*get_timeleft)(struct watchdog_device *);
    int (*restart)(struct watchdog_device *, unsigned long, void *);
    long (*ioctl)(struct watchdog_device *, unsigned int, unsigned long);
};

操作含义如下:
- start stop :分别启动和停止看门狗。
- ping :可选操作,用于向看门狗发送心跳信号。
- status :获取看门狗的状态。
- set_timeout :设置看门狗的超时时间。
- set_pretimeout :设置预超时时间。
- get_timeleft :获取剩余时间。
- restart :重启看门狗。
- ioctl :处理看门狗的I/O控制操作。

通过上述内容,我们详细了解了Linux内核中NVMEM框架和看门狗设备驱动的相关知识,包括数据结构、API以及如何在用户空间和内核空间进行操作。这些知识对于开发嵌入式系统和处理非易失性存储以及系统可靠性具有重要意义。

Linux内核中的NVMEM与看门狗设备驱动详解

10. 看门狗设备状态标志

struct watchdog_device 中的 status 字段包含设备的内部状态位,具体如下:
| 状态标志 | 含义 |
| ---- | ---- |
| WDOG_ACTIVE | 指示看门狗是否正在运行/激活。 |
| WDOG_NO_WAY_OUT | 指示是否设置了无退出功能。可使用 watchdog_set_nowayout() 函数设置。 |
| WDOG_STOP_ON_REBOOT | 表示在重启时应停止看门狗。 |
| WDOG_HW_RUNNING | 指示硬件看门狗正在运行。可使用 watchdog_hw_running() 函数检查。 |
| WDOG_STOP_ON_UNREGISTER | 指定在注销时应停止看门狗。可使用 watchdog_stop_on_unregister() 函数设置。 |

11. struct watchdog_info 结构体字段详解

struct watchdog_info 结构体中的 options 字段是一个位掩码,代表看门狗设备/驱动支持的功能,具体如下:
| 标志 | 含义 |
| ---- | ---- |
| WDIOF_SETTIMEOUT | 表示看门狗设备可以设置超时时间。若设置此标志,则必须定义 set_timeout 回调函数。 |
| WDIOF_MAGICCLOSE | 表示驱动支持魔术关闭字符功能。写入 V 字符序列可使下次关闭操作关闭看门狗(若 nowayout 未设置)。 |
| WDIOF_POWERUNDER | 表示设备可以监测/检测电源故障。设置在 watchdog_device.bootstatus 中时,表示机器因欠压触发复位。 |
| WDIOF_POWEROVER | 表示设备可以监测工作电压。设置在 watchdog_device.bootstatus 中时,表示系统复位可能是由于过压状态。 |
| WDIOF_OVERHEAT | 表示看门狗设备可以监测芯片/SoC 温度。设置在 watchdog_device.bootstatus 中时,表示上次机器通过看门狗重启是由于超过热限制。 |
| WDIOF_FANFAULT | 表示此看门狗设备可以监测风扇。设置时,表示看门狗卡监测的系统风扇出现故障。 |
| WDIOF_EXTERN1 WDIOF_EXTERN2 | 表示看门狗设备有单独的事件输入。设置在 watchdog_device.bootstatus 中时,表示机器上次重启是由于外部继电器/源 1 或 2。 |
| WDIOF_PRETIMEOUT | 表示此看门狗设备支持预超时功能。 |
| WDIOF_KEEPALIVEPING | 表示驱动支持 WDIOC_KEEPALIVE ioctl。设置在 watchdog_device.bootstatus 中时,表示看门狗自上次查询以来收到了保活 ping。 |
| WDIOF_CARDRESET | 仅出现在 watchdog_device.bootstatus 中,表示上次重启是由看门狗本身(其超时)引起的。 |

12. 看门狗设备驱动工作流程

下面是一个简单的 mermaid 流程图,展示了看门狗设备驱动的基本工作流程:

graph TD;
    A[初始化看门狗设备] --> B[注册看门狗设备];
    B --> C{是否启动看门狗};
    C -- 是 --> D[启动看门狗];
    C -- 否 --> E[等待启动信号];
    D --> F{是否收到保活信号};
    F -- 是 --> G[重置看门狗定时器];
    F -- 否 --> H{定时器是否超时};
    H -- 是 --> I[触发系统复位];
    H -- 否 --> F;
    G --> F;
    E --> C;
13. 示例代码:简单的看门狗驱动实现

以下是一个简单的看门狗驱动示例,展示了如何初始化和使用看门狗设备:

#include <linux/module.h>
#include <linux/watchdog.h>

static struct watchdog_device wdd;

static int my_watchdog_start(struct watchdog_device *wdd)
{
    // 启动看门狗定时器的代码
    printk(KERN_INFO "Watchdog started\n");
    return 0;
}

static int my_watchdog_stop(struct watchdog_device *wdd)
{
    // 停止看门狗定时器的代码
    printk(KERN_INFO "Watchdog stopped\n");
    return 0;
}

static const struct watchdog_ops my_watchdog_ops = {
    .owner = THIS_MODULE,
    .start = my_watchdog_start,
    .stop = my_watchdog_stop,
};

static int __init my_watchdog_init(void)
{
    wdd.info = &watchdog_info;
    wdd.ops = &my_watchdog_ops;
    wdd.timeout = 60; // 设置超时时间为 60 秒
    wdd.min_timeout = 10;
    wdd.max_timeout = 120;

    return watchdog_register_device(&wdd);
}

static void __exit my_watchdog_exit(void)
{
    watchdog_unregister_device(&wdd);
}

module_init(my_watchdog_init);
module_exit(my_watchdog_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple Watchdog Driver");
14. 总结

通过对 NVMEM 框架和看门狗设备驱动的详细介绍,我们了解了以下重要内容:
- NVMEM 框架 :涵盖了提供者驱动和消费者驱动的编写,包括设备的注册与注销、读写回调的实现、设备树绑定以及用户空间的访问。
- 看门狗设备驱动 :介绍了看门狗的基本概念、数据结构、API 以及工作原理。了解了如何初始化、启动和停止看门狗,以及如何处理超时事件。

这些知识对于开发嵌入式系统和确保系统的可靠性至关重要。在实际应用中,可以根据具体需求对 NVMEM 和看门狗设备进行配置和使用,以满足不同的系统要求。无论是处理非易失性存储还是监控系统的正常运行,这些技术都能发挥重要作用。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值