编写客户端驱动程序

有关 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_device_alloc() 手动分配 SSAM 设备,随后必须通过 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 由 domaincategorytargetinstancefunction 组成。 domain 用于区分物理 SAM 设备(SSAM_DOMAIN_SERIALHUB),即可通过 Surface 串行集线器访问的设备,以及虚拟设备(SSAM_DOMAIN_VIRTUAL),例如客户端设备集线器,它们在 SAM EC 上没有真实的表示形式,仅在内核/驱动程序端使用。 对于物理设备,category 表示目标类别,target 表示目标 ID,instance 表示用于访问物理 SAM 设备的实例 ID。 此外,function 引用特定的设备功能,但对 SAM EC 没有意义。 客户端设备的(默认)名称是根据其 UID 生成的。

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

与 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 如何。 可以指示核心仅在事件的目标 ID 或实例 ID(或两者)与通知程序 ID(对于目标 ID,则为注册表的目标 ID)所暗示的 ID 匹配时才调用通知程序,方法是提供事件掩码(请参见 enum ssam_event_mask)。

通常,注册表的目标 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 的组合)当前注册了多少通知程序来负责启用和禁用事件。 这意味着当注册特定事件的第一个通知程序时将启用该事件,而当取消注册该事件的最后一个通知程序时将禁用该事件。 请注意,因此事件标志仅在第一个注册的通知程序上使用,但是应注意始终使用相同的标志注册特定事件的通知程序,否则将被视为错误。