如何使用 I2C - Part 1

總覽

本文件將會說明在 MediaTek LinkIt 7687 開發板上如何使用 I2C 的功能。除了解釋相關的 API 使用方式之外,同時也會透過移植其他常用的 I2C 函式庫來示範如何使用 LinkIt SDK v3.3.1。 這是本教學的第一部,您將學習如何移植 LCD 模組所使用的 Wire library 來展示如何使用 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

首先先介紹如何在 LinkIt 7687 開發板上以 Polling mode 模式移植 Grove LCD 模組使用的 Wire 函式庫,進而達到控制 Grove LCD 的目的。 由於本教學文件的主題是 I2C,所以以下範例將會著重在 I2C 相關的部分。 其他移植細節例如:C++ 到 C 的改寫、system call 的轉換與橋接、 以及函式的封裝等,請參考此範例的原始碼以獲得相關資訊。

使用 Wire 函式庫進行資料傳輸時,會使用到以下的 APIs (參考 Grove LCD 模組使用的 rgb_lcd.cpp):

  • 初始化 

    在 rgb_lcd::begin() 裡:

    Wire.begin();
    
  • 資料傳輸 

    在 i2c_send_byte() 裡:

    // transmit to I2C device
    Wire.beginTransmission(LCD_ADDRESS);
    // sends bytes 
    Wire.write(dta);
    // stop transmitting                     
    Wire.endTransmission();
    

而這些 I2C 相關的 APIs 對應到 LinkIt SDK v3 的版本則是 (i2c_write_polling 範例中的 main.c):

  • 初始化 

     lcd_init()裡: 

    // setup I2C frequency
    i2c_init.frequency = HAL_I2C_FREQUENCY_100K; 
    // initialization     
    hal_i2c_master_init(HAL_I2C_MASTER_0, &i2c_init); 
    
  • 資料傳輸 

    在 _i2c_write()裡: 

    hal_i2c_status_t ret;
    // sends bytes
    ret = hal_i2c_master_send_polling(HAL_I2C_MASTER_0, addr, data, dataLen); 
    // transaction done and check return code
    if (HAL_I2C_STATUS_OK != ret)                                             
    {
        ...
    }
    

藉由這樣的轉換,就可以用 Polling mode 的方式將 Grove LCD 模組所使用的 Wire 函式庫透過 LinkIt SDK v3 進行移植。 完整的移植實作請參考相關原始碼

DMA mode

以上面的範例為基礎,這裡將示範如何使用 DMA mode 達成同樣的行為。由於接下來的內容會牽涉到非同步的函式呼叫,因此使用了 binary semaphore 以維持原本的程式流程,相關的細節也會在下面的範例中一併說明。

如同 Polling mode 的範例,我們先從初始化開始。在 i2c_write_dma 範例的 main.c 裡,可以看到除了原本的 I2C 初始化函式,還多了一步註冊 callback 函式的動作。透過這個 callback 函式,程式才能知道相關動作的傳輸狀態 (為了方便說明,下面的程式碼將省略錯誤處理的部分)。

  • 初始化

    在 lcd_init()裡:

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

並且在 main() 裡新增一個 binary 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)
{
    ...

  • 資料傳輸

    相較於 Polling mode,這裡需要換成呼叫 DMA mode 的 API。同時為了維持原本程式流程,在呼叫 I2C 寫入的 API 之後,得去鎖住 semaphore 以暫停下原本的執行流程。參考 _i2c_write() 裡:

    // 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)
    {
        ...

    當 send API 被呼叫之後,它會立刻返回,而不是等底層硬體做完才返回。藉由透過鎖住 semaphore,程式才會停在該處直到 semaphore 變為 given 的狀態 (被解開的狀態) 程式才會繼續執行下去。

    至於 semaphore 被解開的地方,請參考 _i2c_callback()。當底層硬體做完 I2C 相關動作後 (有可能是成功, 也有可能是失敗,需要透過 callback 中的 event 變數做判斷), _i2c_callback() 會被呼叫,進而將 semaphore 設為 given 狀態:

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 中被呼叫,因此不能呼叫一般常用的 FreeRTOS xSemaphoreGive() API 來解開 semaphore,而是要使用 xSemaphoreGiveFromISR() 來進行相關操作。

藉由利用 DMA mode 的 APIs 與 binary semaphore,我們可以從上述的範例看到如何將 Polling mode 的用法改寫為 DMA mode。請參考本範例的程式碼以瞭解更多細節。

執行您的程式

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

結語

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

接下來,我們會使用 Grove Accelerometer 模組來示範另一種 I2C 函式庫的移植方式並以此展示 I2C 讀取的功能。