編寫客戶端驅動程式

有關 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 的組合)當前註冊了多少通知程式來負責啟用和停用事件。 這意味著當註冊特定事件的第一個通知程式時將啟用該事件,而當取消註冊該事件的最後一個通知程式時將停用該事件。 請注意,因此事件標誌僅在第一個註冊的通知程式上使用,但是應注意始終使用相同的標誌註冊特定事件的通知程式,否則將被視為錯誤。