如何使用 I2C - Part 2

總覽

本文件將會說明在 MediaTek LinkIt 7687 開發板上如何使用 I2C 的功能。除了解釋相關的 API 使用方式之外,同時也會透過移植其他常用的 I2C 函式庫來示範如何使用 LinkIt SDK v3.3.1。 這是本教學的第二部,您將學習如何移植 mbed 上 ADXL345 加速度器的驅動程式來展示如何使用 Polling mode 以及 DMA mode 來進行 I2C 讀取的範例。

簡介

此教學文件將會介紹以下內容:

前置作業

請先安裝 LinkIt SDK v3.3.1 所需的開發環境以進行後續操作。

開始之前

若您之前不曾使用 7687 HDK 來開發專案,請先安裝 LinkIt SDK v3.3.1 並進行開發環境設定,相關細節請參考 LinkIt development platform for RTOS 入門手冊。 此外,亦可透過閱讀數位時鐘教學範例 Part1 以獲得初始化 I2C 功能的基礎知識。 

簡述 I2C API

此平台支援兩種 I2C 資料傳輸模式:Polling mode 和 DMA mode。 在 Polling mode 當中,所有讀寫的 API 呼叫都是同步呼叫。 也就是說,當呼叫了一個 API 且該 API 結束返回的時候,底下硬體實際的讀寫動作也已經完成了。相反地,在 DMA mode 中 API 呼叫都是非同步呼叫,意思是當呼叫 API 並且結束返回時,實際讀寫動作尚未發生,它只代表送出了指令給底層硬體,使之知道將要進行後續的讀寫。 而之後讀寫的狀態結束與否則是透過事先註冊的 callback 函式來進行通知。

LinkIt SDK 裡提供了以下的 I2C API 供開發者使用,依功能類型區分為三大類:

 FunctionDescription
系統設定
  • hal_i2c_master_init()
  • hal_i2c_master_deinit()
  • hal_i2c_master_set_frequecy()
  • hal_i2c_master_register_callback()
  • 在開始資料傳輸前,使用此函式來初始化 I2C 功能。
  • 當資料傳輸結束後,使用此函式來釋出 I2C 相關資源。
  • 使用此函式來設定 I2C 的工作頻率。
  • 使用此函式註冊 DMA mode 中所需的 callback 函式。
資料傳輸
  • hal_i2c_master_send_polling()
  • hal_i2c_master_send_dma()
  • hal_i2c_master_receive_polling()
  • hal_i2c_master_receive_dma()
  • 使用此函式以 Polling mode 傳送資料給 I2C 裝置。
  • 使用此函式以 DMA mode 傳送資料給 I2C 裝置。
  • 使用此函式以 Polling mode 從 I2C 裝置接收資料。
  • 使用此函式以 DMA mode 從 I2C 裝置接收資料。
狀態查詢
  • hal_i2c_master_get_running_status()
  • 透過此函式來得知目前系統 I2C 的狀態。

特別注意:受限於硬體設計,每筆資料傳輸長度有其上限。在 Polling mode 和 DMA mode 當中,該限制分別是 8 位元組與 65,535 位元組。更多細節請參考 API 線上文件。 

硬體設置

本範例使用與數位時鐘教學範例 Part1 相同的硬體設置。

軟體實作

Polling mode

如同之前的 I2C 寫入範例,一樣我們先從 Polling mode 開始介紹,本範例將會說明如何以 Polling mode 來移植 mbed ADXL345 驅動程式 (Grove Accelerometer 內使用的加速度計晶片)。下面的內容將以 I2C 的部分為主,其他的移植細節請參考此範例的原始碼

首先先來看 mbed 裡面是如何做初始化的 (參考 ADXL345_I2C.cpp):

  • 初始化 

    ADXL345_I2C() constructor 裡面:

    ADXL345_I2C::ADXL345_I2C(PinName sda, PinName scl) : i2c_(sda, scl) {
    //400kHz, allowing us to use the fastest data rates.
    i2c_.frequency(400000); 
    ...

    可以看到接腳與工作頻率的設定是透過 mbed 的 I2C 介面 (即 i2c_ 變數) 完成。對應到使用 LinkIt SDK v3.3 的 i2c_read_polling 範例裡,在 main.cgsensor_init() 需要進行如下的初始化:

    // pinmux initialization
    hal_gpio_init(HAL_GPIO_27);
    hal_gpio_init(HAL_GPIO_28);
    hal_pinmux_set_function(HAL_GPIO_27,HAL_GPIO_27_I2C1_CLK);
    hal_pinmux_set_function(HAL_GPIO_28,HAL_GPIO_28_I2C1_DATA);
    hal_gpio_pull_up(HAL_GPIO_27);
    hal_gpio_pull_up(HAL_GPIO_28);
    // setup I2C
    i2c_init.frequency = HAL_I2C_FREQUENCY_100K;
    hal_i2c_master_init(HAL_I2C_MASTER_0, &i2c_init);
  • 資料傳輸 

    在 mbed 裡 I2C 的存取是透過 I2C.write() 和 I2C.read() 這兩個 API 來完成。而它們可以直接用 LinkIt SDK v3.3 API 轉換為 (參考 main.c):

    static int _i2c_write(uint8_t addr, uint8_t* data, uint8_t dataLen)
    {
        ...
        // Write in Polling mode
        ret = hal_i2c_master_send_polling(HAL_I2C_MASTER_0, ADXL345_I2C_ADDRESS, data, dataLen);
        ...
    }
     
    static int _i2c_read(uint8_t addr, uint8_t* data, uint8_t dataLen)
    {
        ...
        // Read in Polling mode
        ret = hal_i2c_master_receive_polling(HAL_I2C_MASTER_0, ADXL345_I2C_ADDRESS, data, dataLen);
      
        ...
    }

    在移植 mbed 驅動程式的過程中,有個重要的差異是兩個平台之間 (LinkIt 和 mbed) 對 I2C 裝置位址解讀方式的不同。以此範例來說,原本會使用 ADXL345_I2C_READ (0xA7) 和 ADXL345_I2C_WRITE (0xA6) (定義在 ADXL345_I2C.h) 當做傳進 read()write() API 的位址參數。但是 LinkIt SDK 只需要 I2C 的真實位址 (即最高 7 位元的資訊) 當做輸入參數,因此這就是為何 ADXL345_I2C_ADDRESS (0x53) 會被直接使用在 LinkIt SDK APIs 當中的原因。

DMA mode

開始之前, 我們先回顧一下前面 I2C 使用 DMA mode 寫入的範例中,有哪些重要的步驟:

  • 註冊 callback 函式以獲得系統 I2C 傳輸結果的相關通知。
  • 使用 _dma 的 APIs 來取代掉 _polling 的 APIs。

而且如同 I2C 使用 DMA mode 寫入的範例,我們需要使用 semaphore 來模擬同步函式呼叫,與確保程式和 Polling mode 時有一樣的執行流程。於是需要做的是:

  • 建立一個 binary semaphore 並將它的狀態設定為 taken。
  • 每當讀取或寫入的指令發出時,程式就將此 semaphore 鎖住。
  • 當讀寫的動作完成時,在 I2C ISR 裡透過 callback 將此 semaphore 放開。

把上述的要點結合起來轉換為如下的實作 (參考 i2c_read_dma 範例裡的 main.c):

  • 初始化

    在 gsensor_init() 裡:

    i2c_init.frequency = HAL_I2C_FREQUENCY_100K;
    hal_i2c_master_init(HAL_I2C_MASTER_0, &i2c_init);
    // register the callback function here
    hal_i2c_master_register_callback(HAL_I2C_MASTER_0, _i2c_callback, NULL);

    同時也在 main() 裡新增一個 semaphore 並將它的初始狀態設定為 taken:

    // create the binary semaphore
    gh_lock = xSemaphoreCreateBinary();
    // setup the initial state of the semaphore
    if (xSemaphoreGive(gh_lock) != pdTRUE)
    {
        ...
    // lock the semaphore to block the subsequent take
    if (xSemaphoreTake(gh_lock, portMAX_DELAY) != pdTRUE)
    {
        ...
  • 資料傳輸

    在 _i2c_write() 裡,使用 DMA mode 版本的 API 來發出傳送資料的指令,並且於函式呼叫返回後,立即去鎖住 semaphore:

    // the DMA mode I2C write API
    ret = hal_i2c_master_send_dma(HAL_I2C_MASTER_0, addr, data, dataLen);
    if (HAL_I2C_STATUS_OK != ret)
    {
        ...
    // lock the semaphore to wait the transaction being done
    if (xSemaphoreTake(gh_lock, portMAX_DELAY) != pdTRUE)
    {
        ...

    _i2c_read() 裡亦使用類似的概念實作:

    // DMA mode I2C read API
    ret = hal_i2c_master_receive_dma(HAL_I2C_MASTER_0, ADXL345_I2C_ADDRESS, data, dataLen);
    if(HAL_I2C_STATUS_OK != ret)
    { 
        ...
    // lock the semaphore to wait for the transaction being done
    if (xSemaphoreTake(gh_lock, portMAX_DELAY) != pdTRUE)
    {
        ...

    最後,在 _i2c_callbak() 裡面放掉 semaphore:

    static void _i2c_callback(uint8_t slave_address, hal_i2c_callback_event_t event, void *user_data)
    {
        // transaction done
        if (event == HAL_I2C_EVENT_SUCCESS)
        {
            // release the semaphore inside I2C ISR
            if (xSemaphoreGiveFromISR(gh_lock, NULL) != pdTRUE)
            {
                ...
        // error
        ...
    }

    如同之前提醒要小心的地方,因為 callback 是在 I2C ISR 內被呼叫,因此不能使用一般的 xSemaphoreGive() API 來放開 semaphore,而是要用 xSemaphoreGiveFromISR() 的版本才能正確執行。

執行您的程式

本文件相關的程式碼皆可自行下載,並依照 LinkIt 7687 HDK 入門手冊的步驟進行程式的上傳與執行。

結語

透過此教學,您可以學到如何在 LinkIt 7687 開發板上使用 I2C 的功能以及如何使用 LinkIt SDK v3 來移植其他不同種類的 I2C 函式庫。為了讓開發者有更多的參考資料,將來也會提供公開的驅動程式資料庫用來存放各式使用 LinkIt SDK v3 移植的裝置驅動程式,以方便開發或移植相關功能的進行。欲瞭解更多關於 MediaTek LinkIt 7687 開發板的資訊,請參考 LinkIt development platform for RTOS 開發者指南