编写客户端驱动程序

有关 API 文档,请参阅

概述

客户端驱动程序可以通过两种主要方式进行设置,具体取决于如何向系统提供相应的设备。我们特别区分通过传统方式(例如通过 ACPI 作为平台设备)呈现给系统的设备,以及不可发现的设备,这些设备需要通过其他机制明确提供,如下文进一步讨论。

非 SSAM 客户端驱动程序

与 SAM EC 的所有通信都通过 struct ssam_controller 来处理,该结构表示内核的 EC。针对非 SSAM 设备(因此不是 struct ssam_device_driver)的驱动程序需要显式建立与该控制器的连接/关系。这可以通过 ssam_client_bind() 函数完成。该函数返回对 SSAM 控制器的引用,但更重要的是,还在客户端设备和控制器之间建立设备链接(这也可以通过 ssam_client_link() 单独完成)。这样做很重要,因为它首先保证返回的控制器在客户端驱动程序绑定到其设备时可用于客户端驱动程序,即驱动程序在控制器失效之前解除绑定,其次,它确保正确的挂起/恢复顺序。此设置应在驱动程序的 probe 函数中完成,并且可以用于在 SSAM 子系统尚未准备好时(例如)推迟探测。

static int client_driver_probe(struct platform_device *pdev)
{
        struct ssam_controller *ctrl;

        ctrl = ssam_client_bind(&pdev->dev);
        if (IS_ERR(ctrl))
                return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl);

        // ...

        return 0;
}

控制器可以通过 ssam_get_controller() 单独获取,并通过 ssam_controller_get()ssam_controller_put() 来保证其生命周期。请注意,这些函数都不能保证控制器不会关闭或挂起。这些函数本质上仅对引用进行操作,即仅保证最低限度的可访问性,而对实际可操作性没有任何保证。

添加 SSAM 设备

如果设备尚不存在/未通过传统方式提供,则应通过 SSAM 客户端设备集线器将其作为 struct ssam_device 提供。通过将设备的 UID 输入到相应的注册表中,可以将新设备添加到此集线器。SSAM 设备也可以通过 ssam_device_alloc() 手动分配,随后必须通过 ssam_device_add() 添加,并最终通过 ssam_device_remove() 删除。默认情况下,设备的父级设置为为分配提供的控制器设备,但是这可以在添加设备之前进行更改。请注意,在更改父设备时,必须注意确保控制器生命周期和挂起/恢复排序保证(在通过父子关系提供的默认设置中)得以保留。如有必要,可以通过使用 ssam_client_link()(就像对非 SSAM 客户端驱动程序所做的那样)并如上文更详细地描述的那样。

在控制器关闭之前,必须由添加相应设备的一方始终删除客户端设备。可以通过使用 ssam_client_link() 将提供 SSAM 设备的驱动程序链接到控制器来保证这种删除,从而导致它在控制器驱动程序解除绑定之前解除绑定。以控制器为父级注册的客户端设备在控制器关闭时会自动删除,但是不应依赖此功能,尤其是在不同的父级上,它不会扩展到客户端设备。

SSAM 客户端驱动程序

本质上,SSAM 客户端设备驱动程序与其他设备驱动程序类型没有区别。它们通过 struct ssam_device_driver 表示,并通过其 UID(struct ssam_device.uid)成员和匹配表(struct ssam_device_driver.match_table)绑定到 struct ssam_device,该匹配表应在声明驱动程序结构实例时设置。有关如何定义驱动程序匹配表的成员的更多详细信息,请参阅 SSAM_DEVICE() 宏文档。

SSAM 客户端设备的 UID 由 类别目标实例函数 组成。 用于区分物理 SAM 设备 (SSAM_DOMAIN_SERIALHUB),即可以通过 Surface 串行集线器访问的设备,以及虚拟设备 (SSAM_DOMAIN_VIRTUAL),例如客户端设备集线器,这些设备在 SAM EC 上没有实际表示形式,并且仅在内核/驱动程序端使用。对于物理设备,类别 表示目标类别,目标 表示目标 ID,实例 表示用于访问物理 SAM 设备的实例 ID。此外,函数 引用特定的设备功能,但对 SAM EC 没有意义。客户端设备(默认)名称是基于其 UID 生成的。

驱动程序实例可以通过 ssam_device_driver_register() 注册,并通过 ssam_device_driver_unregister() 取消注册。为方便起见,可以使用 module_ssam_device_driver() 宏来定义注册驱动程序的模块初始化和退出函数。

与 SSAM 客户端设备关联的控制器可以在其 struct ssam_device.ctrl 成员中找到。保证此引用至少在客户端驱动程序绑定期间有效,但也应在客户端设备存在期间有效。但是,请注意,在绑定的客户端驱动程序之外访问时,必须确保在发出任何请求或(取消)注册事件通知程序时,控制器设备不会被挂起(因此通常应避免)。当从绑定的客户端驱动程序内部访问控制器时,可以保证这一点。

发起同步请求

同步请求是(目前)主机与 EC 进行通信的主要形式。定义和执行此类请求有几种方法,但是,它们中的大多数都归结为类似于下面示例中所示的内容。此示例定义了一个写-读请求,这意味着调用者向 SAM EC 提供一个参数并接收一个响应。调用者需要知道响应有效负载的(最大)长度并为其提供缓冲区。

必须注意确保传递给 SAM EC 的任何命令有效负载数据都以小端格式提供,并且类似地,从其接收的任何响应有效负载数据都从小端转换为主机字节序。

int perform_request(struct ssam_controller *ctrl, u32 arg, u32 *ret)
{
        struct ssam_request rqst;
        struct ssam_response resp;
        int status;

        /* Convert request argument to little-endian. */
        __le32 arg_le = cpu_to_le32(arg);
        __le32 ret_le = cpu_to_le32(0);

        /*
         * Initialize request specification. Replace this with your values.
         * The rqst.payload field may be NULL if rqst.length is zero,
         * indicating that the request does not have any argument.
         *
         * Note: The request parameters used here are not valid, i.e.
         *       they do not correspond to an actual SAM/EC request.
         */
        rqst.target_category = SSAM_SSH_TC_SAM;
        rqst.target_id = SSAM_SSH_TID_SAM;
        rqst.command_id = 0x02;
        rqst.instance_id = 0x03;
        rqst.flags = SSAM_REQUEST_HAS_RESPONSE;
        rqst.length = sizeof(arg_le);
        rqst.payload = (u8 *)&arg_le;

        /* Initialize request response. */
        resp.capacity = sizeof(ret_le);
        resp.length = 0;
        resp.pointer = (u8 *)&ret_le;

        /*
         * Perform actual request. The response pointer may be null in case
         * the request does not have any response. This must be consistent
         * with the SSAM_REQUEST_HAS_RESPONSE flag set in the specification
         * above.
         */
        status = ssam_request_do_sync(ctrl, &rqst, &resp);

        /*
         * Alternatively use
         *
         *   ssam_request_do_sync_onstack(ctrl, &rqst, &resp, sizeof(arg_le));
         *
         * to perform the request, allocating the message buffer directly
         * on the stack as opposed to allocation via kzalloc().
         */

        /*
         * Convert request response back to native format. Note that in the
         * error case, this value is not touched by the SSAM core, i.e.
         * 'ret_le' will be zero as specified in its initialization.
         */
        *ret = le32_to_cpu(ret_le);

        return status;
}

请注意,ssam_request_do_sync() 本质上是较低级别请求原语的包装器,也可以使用这些原语来执行请求。有关更多详细信息,请参阅其实现和文档。

一种更方便用户的方式来定义此类函数是通过使用生成器宏之一,例如通过

SSAM_DEFINE_SYNC_REQUEST_W(__ssam_tmp_perf_mode_set, __le32, {
        .target_category = SSAM_SSH_TC_TMP,
        .target_id       = SSAM_SSH_TID_SAM,
        .command_id      = 0x03,
        .instance_id     = 0x00,
});

此示例定义了一个函数

static int __ssam_tmp_perf_mode_set(struct ssam_controller *ctrl, const __le32 *arg);

执行指定的请求,并在调用该函数时传入控制器。在此示例中,参数通过 arg 指针提供。请注意,生成的函数会在堆栈上分配消息缓冲区。因此,如果通过请求提供的参数很大,则应避免使用此类宏。另请注意,与之前的非宏示例相比,此函数不进行任何字节序转换,必须由调用者处理。除了这些差异之外,宏生成的函数与上面非宏示例中提供的函数类似。

此类函数生成宏的完整列表是

有关更多详细信息,请参阅各自的文档。对于每个这些宏,都提供了一个特殊变体,该变体针对适用于同一设备类型的多个实例的请求类型

这些宏与前面提到的版本之间的区别在于,设备目标和实例 ID 不是为生成的函数固定的,而是必须由该函数的调用者提供。

此外,还提供了直接用于客户端设备的变体,即 struct ssam_device。例如,这些可以按如下方式使用

SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_get_sta, __le32, {
        .target_category = SSAM_SSH_TC_BAT,
        .command_id      = 0x01,
});

此宏调用定义了一个函数

static int ssam_bat_get_sta(struct ssam_device *sdev, __le32 *ret);

使用客户端设备中给定的设备 ID 和控制器执行指定的请求。客户端设备的此类宏的完整列表是

处理事件

要接收来自 SAM EC 的事件,必须通过 ssam_notifier_register() 为所需的事件注册事件通知程序。一旦不再需要,必须通过 ssam_notifier_unregister() 取消注册该通知程序。对于 struct ssam_device 类型客户端,应首选 ssam_device_notifier_register()ssam_device_notifier_unregister() 包装器,因为它们可以正确处理客户端设备的热插拔。

注册事件通知程序需要(至少)提供一个回调,以便在收到事件时调用;注册表指定应如何启用事件;一个事件 ID,用于指定为哪个目标类别以及(可选)根据使用的注册表,为哪个实例 ID 启用事件;最后,描述 EC 将如何发送这些事件的标志。如果特定注册表不按实例 ID 启用事件,则实例 ID 必须设置为零。此外,可以为相应的通知程序指定优先级,该优先级确定其相对于为同一目标类别注册的任何其他通知程序的顺序。

默认情况下,事件通知程序将接收特定目标类别的所有事件,而不管注册通知程序时指定的实例 ID 如何。可以通过提供事件掩码(请参阅 enum ssam_event_mask),指示核心仅在事件的目标 ID 或实例 ID(或两者)与通知程序 ID(在目标 ID 的情况下,注册表的目标 ID)所暗示的 ID 匹配时才调用通知程序。

通常,注册表的目标 ID 也是启用事件的目标 ID(值得注意的例外是 Surface Laptop 1 和 2 上的键盘输入事件,这些事件通过目标 ID 为 1 的注册表启用,但提供目标 ID 为 2 的事件)。

下面提供了注册事件通知程序和处理接收到的事件的完整示例

u32 notifier_callback(struct ssam_event_notifier *nf,
                      const struct ssam_event *event)
{
        int status = ...

        /* Handle the event here ... */

        /* Convert return value and indicate that we handled the event. */
        return ssam_notifier_from_errno(status) | SSAM_NOTIF_HANDLED;
}

int setup_notifier(struct ssam_device *sdev,
                   struct ssam_event_notifier *nf)
{
        /* Set priority wrt. other handlers of same target category. */
        nf->base.priority = 1;

        /* Set event/notifier callback. */
        nf->base.fn = notifier_callback;

        /* Specify event registry, i.e. how events get enabled/disabled. */
        nf->event.reg = SSAM_EVENT_REGISTRY_KIP;

        /* Specify which event to enable/disable */
        nf->event.id.target_category = sdev->uid.category;
        nf->event.id.instance = sdev->uid.instance;

        /*
         * Specify for which events the notifier callback gets executed.
         * This essentially tells the core if it can skip notifiers that
         * don't have target or instance IDs matching those of the event.
         */
        nf->event.mask = SSAM_EVENT_MASK_STRICT;

        /* Specify event flags. */
        nf->event.flags = SSAM_EVENT_SEQUENCED;

        return ssam_notifier_register(sdev->ctrl, nf);
}

可以为同一事件注册多个事件通知程序。事件处理程序核心负责在注册和取消注册通知程序时启用和禁用事件,方法是跟踪当前注册了多少个特定事件(注册表、事件目标类别和事件实例 ID 的组合)的通知程序。这意味着当注册第一个事件通知程序时将启用特定事件,并在取消注册最后一个事件通知程序时禁用该事件。请注意,事件标志因此仅在第一个注册的通知程序上使用,但是,应注意特定事件的通知程序始终使用相同的标志注册,否则将其视为错误。