1. I/O 裝置介紹
RT-Thread 提供了一套簡單的 I/O 裝置模型架構,如下圖所示,它位于硬體和應用程式之間,共分成三層,從上到下分别是 I/O 裝置管理層、裝置驅動架構層、裝置驅動層。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5CNxEGO3czM2IGZ4kTZzgDN0QDMmdDZjdzY5MTNyQTY28CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
應用程式通過 I/O 裝置管理接口獲得正确的裝置驅動,然後通過這個裝置驅動與底層 I/O 硬體裝置進行資料(或控制)互動。
I/O 裝置管理層實作了對裝置驅動程式的封裝。應用程式通過圖中的"I/O裝置管理層"提供的标準接口通路底層裝置,裝置驅動程式的更新、更替不會對上層應用産生影響。這種方式使得裝置的硬體操作相關的代碼能夠獨立于應用程式而存在,雙方隻需關注各自的功能實作,進而降低了代碼的耦合性、複雜性,提高了系統的可靠性。
裝置驅動架構層是對同類硬體裝置驅動的抽象,将不同廠家的同類硬體裝置驅動中相同的部分抽取出來,将不同部分留出接口,由驅動程式實作。
裝置驅動層是一組驅使硬體裝置工作的程式,實作通路硬體裝置的功能。它負責建立和注冊 I/O 裝置,對于操作邏輯簡單的裝置,可以不經過裝置驅動架構層,直接将裝置注冊到 I/O 裝置管理器中,使用序列圖如下圖所示,主要有以下 2 點:
- 裝置驅動根據裝置模型定義,建立出具備硬體通路能力的裝置執行個體,将該裝置通過
接口注冊到 I/O 裝置管理器中。rt_device_register()
- 應用程式通過
接口查找到裝置,然後使用 I/O 裝置管理接口來通路硬體。rt_device_find()
對于另一些裝置,如看門狗等,則會将建立的裝置執行個體先注冊到對應的裝置驅動架構中,再由裝置驅動架構向 I/O 裝置管理器進行注冊,主要有以下幾點:
- 看門狗裝置驅動程式根據看門狗裝置模型定義,建立出具備硬體通路能力的看門狗裝置執行個體,并将該看門狗裝置通過
接口注冊到看門狗裝置驅動架構中。rt_hw_watchdog_register()
- 看門狗裝置驅動架構通過
接口将看門狗裝置注冊到 I/O 裝置管理器中。rt_device_register()
- 應用程式通過 I/O 裝置管理接口來通路看門狗裝置硬體。
看門狗裝置使用序列圖:
2. I/O 裝置模型
RT-Thread 的裝置模型是建立在核心對象模型基礎之上的,裝置被認為是一類對象,被納入對象管理器的範疇。每個裝置對象都是由基對象派生而來,每個具體裝置都可以繼承其父類對象的屬性,并派生出其私有屬性,下圖是裝置對象的繼承和派生關系示意圖。
裝置對象具體定義如下所示:
struct rt_device
{
struct rt_object parent; /* 核心對象基類 */
enum rt_device_class_type type; /* 裝置類型 */
rt_uint16_t flag; /* 裝置參數 */
rt_uint16_t open_flag; /* 裝置打開标志 */
rt_uint8_t ref_count; /* 裝置被引用次數 */
rt_uint8_t device_id; /* 裝置 ID,0 - 255 */
/* 資料收發回調函數 */
rt_err_t (*rx_indicate)(rt_device_t dev, rt_size_t size);
rt_err_t (*tx_complete)(rt_device_t dev, void *buffer);
const struct rt_device_ops *ops; /* 裝置操作方法 */
/* 裝置的私有資料 */
void *user_data;
};
typedef struct rt_device *rt_device_t;
3. I/O 裝置類型
RT-Thread 支援多種 I/O 裝置類型,主要裝置類型如下所示:
RT_Device_Class_Char /* 字元裝置 */
RT_Device_Class_Block /* 塊裝置 */
RT_Device_Class_NetIf /* 網絡接口裝置 */
RT_Device_Class_MTD /* 記憶體裝置 */
RT_Device_Class_RTC /* RTC 裝置 */
RT_Device_Class_Sound /* 聲音裝置 */
RT_Device_Class_Graphic /* 圖形裝置 */
RT_Device_Class_I2CBUS /* I2C 總線裝置 */
RT_Device_Class_USBDevice /* USB device 裝置 */
RT_Device_Class_USBHost /* USB host 裝置 */
RT_Device_Class_SPIBUS /* SPI 總線裝置 */
RT_Device_Class_SPIDevice /* SPI 裝置 */
RT_Device_Class_SDIO /* SDIO 裝置 */
RT_Device_Class_Miscellaneous /* 雜類裝置 */
其中字元裝置、塊裝置是常用的裝置類型,它們的分類依據是裝置資料與系統之間的傳輸處理方式。字元模式裝置允許非結構的資料傳輸,即通常資料傳輸采用串行的形式,每次一個位元組。字元裝置通常是一些簡單裝置,如序列槽、按鍵。
塊裝置每次傳輸一個資料塊,例如每次傳輸 512 個位元組資料。這個資料塊是硬體強制性的,資料塊可能使用某類資料接口或某些強制性的傳輸協定,否則就可能發生錯誤。是以,有時塊裝置驅動程式對讀或寫操作必須執行附加的工作,如下圖所示:
當系統服務于一個具有大量資料的寫操作時,裝置驅動程式必須首先将資料劃分為多個包,每個包采用裝置指定的資料尺寸。而在實際過程中,最後一部分資料尺寸有可能小于正常的裝置塊尺寸。如上圖中每個塊使用單獨的寫請求寫入到裝置中,頭 3 個直接進行寫操作。但最後一個資料塊尺寸小于裝置塊尺寸,裝置驅動程式必須使用不同于前 3 個塊的方式處理最後的資料塊。通常情況下,裝置驅動程式需要首先執行相對應的裝置塊的讀操作,然後把寫入資料覆寫到讀出資料上,然後再把這個 “合成” 的資料塊作為一整個塊寫回到裝置中。例如上圖中的塊 4,驅動程式需要先把塊 4 所對應的裝置塊讀出來,然後将需要寫入的資料覆寫至從裝置塊讀出的資料上,使其合并成一個新的塊,最後再寫回到塊裝置中。
4. 建立和注冊 I/O 裝置
驅動層負責建立裝置執行個體,并注冊到 I/O 裝置管理器中,可以通過靜态申明的方式建立裝置執行個體,也可以用下面的接口進行動态建立:
rt_device_t rt_device_create(int type, int attach_size); 複制錯誤複制成功
參數 | 描述 |
---|---|
type | 裝置類型,可取前面小節列出的裝置類型值 |
attach_size | 使用者資料大小 |
傳回 | —— |
裝置句柄 | 建立成功 |
RT_NULL | 建立失敗,動态記憶體配置設定失敗 |
調用該接口時,系統會從動态堆記憶體中配置設定一個裝置控制塊,大小為 struct rt_device 和 attach_size 的和,裝置的類型由參數 type 設定。裝置被建立後,需要實作它通路硬體的操作方法。
struct rt_device_ops
{
/* common device interface */
rt_err_t (*init) (rt_device_t dev);
rt_err_t (*open) (rt_device_t dev, rt_uint16_t oflag);
rt_err_t (*close) (rt_device_t dev);
rt_size_t (*read) (rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size);
rt_size_t (*write) (rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size);
rt_err_t (*control)(rt_device_t dev, int cmd, void *args);
};
複制錯誤複制成功
各個操作方法的描述如下表所示:
方法名稱 | 方法描述 |
---|---|
init | 初始化裝置。裝置初始化完成後,裝置控制塊的 flag 會被置成已激活狀态 (RT_DEVICE_FLAG_ACTIVATED)。如果裝置控制塊中的 flag 标志已經設定成激活狀态,那麼再運作初始化接口時會立刻傳回,而不會重新進行初始化。 |
open | 打開裝置。有些裝置并不是系統一啟動就已經打開開始運作,或者裝置需要進行資料收發,但如果上層應用還未準備好,裝置也不應預設已經使能并開始接收資料。是以建議在寫底層驅動程式時,在調用 open 接口時才使能裝置。 |
close | 關閉裝置。在打開裝置時,裝置控制塊會維護一個打開計數,在打開裝置時進行 + 1 操作,在關閉裝置時進行 - 1 操作,當計數器變為 0 時,才會進行真正的關閉操作。 |
read | 從裝置讀取資料。參數 pos 是讀取資料的偏移量,但是有些裝置并不一定需要指定偏移量,例如序列槽裝置,裝置驅動應忽略這個參數。而對于塊裝置來說,pos 以及 size 都是以塊裝置的資料塊大小為機關的。例如塊裝置的資料塊大小是 512,而參數中 pos = 10, size = 2,那麼驅動應該傳回裝置中第 10 個塊 (從第 0 個塊做為起始),共計 2 個塊的資料。這個接口傳回的類型是 rt_size_t,即讀到的位元組數或塊數目。正常情況下應該會傳回參數中 size 的數值,如果傳回零請設定對應的 errno 值。 |
write | 向裝置寫入資料。參數 pos 是寫入資料的偏移量。與讀操作類似,對于塊裝置來說,pos 以及 size 都是以塊裝置的資料塊大小為機關的。這個接口傳回的類型是 rt_size_t,即真實寫入資料的位元組數或塊數目。正常情況下應該會傳回參數中 size 的數值,如果傳回零請設定對應的 errno 值。 |
control | 根據 cmd 指令控制裝置。指令往往是由底層各類裝置驅動自定義實作。例如參數 RT_DEVICE_CTRL_BLK_GETGEOME,意思是擷取塊裝置的大小資訊。 |
當一個動态建立的裝置不再需要使用時可以通過如下函數來銷毀:
void rt_device_destroy(rt_device_t device); 複制錯誤複制成功
參數 | 描述 |
---|---|
device | 裝置句柄 |
傳回 | 無 |
裝置被建立後,需要注冊到 I/O 裝置管理器中,應用程式才能夠通路,注冊裝置的函數如下所示:
rt_err_t rt_device_register(rt_device_t dev, const char* name, rt_uint8_t flags); 複制錯誤複制成功
參數 | 描述 |
---|---|
dev | 裝置句柄 |
name | 裝置名稱,裝置名稱的最大長度由 rtconfig.h 中定義的宏 RT_NAME_MAX 指定,多餘部分會被自動截掉 |
flags | 裝置模式标志 |
傳回 | —— |
RT_EOK | 注冊成功 |
-RT_ERROR | 注冊失敗,dev 為空或者 name 已經存在 |
Note
注:應當避免重複注冊已經注冊的裝置,以及注冊相同名字的裝置。
flags 參數支援下列參數 (可以采用或的方式支援多種參數):
#define RT_DEVICE_FLAG_RDONLY 0x001 /* 隻讀 */
#define RT_DEVICE_FLAG_WRONLY 0x002 /* 隻寫 */
#define RT_DEVICE_FLAG_RDWR 0x003 /* 讀寫 */
#define RT_DEVICE_FLAG_REMOVABLE 0x004 /* 可移除 */
#define RT_DEVICE_FLAG_STANDALONE 0x008 /* 獨立 */
#define RT_DEVICE_FLAG_SUSPENDED 0x020 /* 挂起 */
#define RT_DEVICE_FLAG_STREAM 0x040 /* 流模式 */
#define RT_DEVICE_FLAG_INT_RX 0x100 /* 中斷接收 */
#define RT_DEVICE_FLAG_DMA_RX 0x200 /* DMA 接收 */
#define RT_DEVICE_FLAG_INT_TX 0x400 /* 中斷發送 */
#define RT_DEVICE_FLAG_DMA_TX 0x800 /* DMA 發送 */ 複制錯誤複制成功
裝置流模式 RT_DEVICE_FLAG_STREAM 參數用于向序列槽終端輸出字元串:當輸出的字元是
“\n”
時,自動在前面補一個
“\r”
做分行。
注冊成功的裝置可以在 FinSH 指令行使用
list_device
指令檢視系統中所有的裝置資訊,包括裝置名稱、裝置類型和裝置被打開次數:
msh />list_device
device type ref count
-------- -------------------- ----------
e0 Network Interface 0
sd0 Block Device 1
rtc RTC 0
uart1 Character Device 0
uart0 Character Device 2
msh /> 複制錯誤複制成功
當裝置登出後的,裝置将從裝置管理器中移除,也就不能再通過裝置查找搜尋到該裝置。登出裝置不會釋放裝置控制塊占用的記憶體。登出裝置的函數如下所示:
rt_err_t rt_device_unregister(rt_device_t dev); 複制錯誤複制成功
參數 | 描述 |
---|---|
dev | 裝置句柄 |
傳回 | —— |
RT_EOK | 成功 |
下面代碼為看門狗裝置的注冊示例,調用
rt_hw_watchdog_register()
接口後,裝置通過
rt_device_register()
接口被注冊到 I/O 裝置管理器中。
const static struct rt_device_ops wdt_ops =
{
rt_watchdog_init,
rt_watchdog_open,
rt_watchdog_close,
RT_NULL,
RT_NULL,
rt_watchdog_control,
};
rt_err_t rt_hw_watchdog_register(struct rt_watchdog_device *wtd,
const char *name,
rt_uint32_t flag,
void *data)
{
struct rt_device *device;
RT_ASSERT(wtd != RT_NULL);
device = &(wtd->parent);
device->type = RT_Device_Class_Miscellaneous;
device->rx_indicate = RT_NULL;
device->tx_complete = RT_NULL;
device->ops = &wdt_ops;
device->user_data = data;
/* register a character device */
return rt_device_register(device, name, flag);
}
複制錯誤複制成功
5. 通路 I/O 裝置
應用程式通過 I/O 裝置管理接口來通路硬體裝置,當裝置驅動實作後,應用程式就可以通路該硬體。I/O 裝置管理接口與 I/O 裝置的操作方法的映射關系下圖所示:
查找裝置
應用程式根據裝置名稱擷取裝置句柄,進而可以操作裝置。查找裝置函數如下所示:
rt_device_t rt_device_find(const char* name); 複制錯誤複制成功
參數 | 描述 |
---|---|
name | 裝置名稱 |
傳回 | —— |
裝置句柄 | 查找到對應裝置将傳回相應的裝置句柄 |
RT_NULL | 沒有找到相應的裝置對象 |
初始化裝置
獲得裝置句柄後,應用程式可使用如下函數對裝置進行初始化操作:
rt_err_t rt_device_init(rt_device_t dev);
參數 | 描述 |
---|---|
dev | 裝置句柄 |
傳回 | —— |
RT_EOK | 裝置初始化成功 |
錯誤碼 | 裝置初始化失敗 |
Note
注:當一個裝置已經初始化成功後,調用這個接口将不再重複做初始化 0。
打開和關閉裝置
通過裝置句柄,應用程式可以打開和關閉裝置,打開裝置時,會檢測裝置是否已經初始化,沒有初始化則會預設調用初始化接口初始化裝置。通過如下函數打開裝置:
rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflags); 複制錯誤複制成功
參數 | 描述 |
---|---|
dev | 裝置句柄 |
oflags | 裝置打開模式标志 |
傳回 | —— |
RT_EOK | 裝置打開成功 |
-RT_EBUSY | 如果裝置注冊時指定的參數中包括 RT_DEVICE_FLAG_STANDALONE 參數,此裝置将不允許重複打開 |
其他錯誤碼 | 裝置打開失敗 |
oflags 支援以下的參數:
#define RT_DEVICE_OFLAG_CLOSE 0x000 /* 裝置已經關閉(内部使用)*/
#define RT_DEVICE_OFLAG_RDONLY 0x001 /* 以隻讀方式打開裝置 */
#define RT_DEVICE_OFLAG_WRONLY 0x002 /* 以隻寫方式打開裝置 */
#define RT_DEVICE_OFLAG_RDWR 0x003 /* 以讀寫方式打開裝置 */
#define RT_DEVICE_OFLAG_OPEN 0x008 /* 裝置已經打開(内部使用)*/
#define RT_DEVICE_FLAG_STREAM 0x040 /* 裝置以流模式打開 */
#define RT_DEVICE_FLAG_INT_RX 0x100 /* 裝置以中斷接收模式打開 */
#define RT_DEVICE_FLAG_DMA_RX 0x200 /* 裝置以 DMA 接收模式打開 */
#define RT_DEVICE_FLAG_INT_TX 0x400 /* 裝置以中斷發送模式打開 */
#define RT_DEVICE_FLAG_DMA_TX 0x800 /* 裝置以 DMA 發送模式打開 */
Note
注:如果上層應用程式需要設定裝置的接收回調函數,則必須以 RT_DEVICE_FLAG_INT_RX 或者 RT_DEVICE_FLAG_DMA_RX 的方式打開裝置,否則不會回調函數。
應用程式打開裝置完成讀寫等操作後,如果不需要再對裝置進行操作則可以關閉裝置,通過如下函數完成:
rt_err_t rt_device_close(rt_device_t dev); 複制錯誤複制成功
參數 | 描述 |
---|---|
dev | 裝置句柄 |
傳回 | —— |
RT_EOK | 關閉裝置成功 |
-RT_ERROR | 裝置已經完全關閉,不能重複關閉裝置 |
其他錯誤碼 | 關閉裝置失敗 |
Note
注:關閉裝置接口和打開裝置接口需配對使用,打開一次裝置對應要關閉一次裝置,這樣裝置才會被完全關閉,否則裝置仍處于未關閉狀态。
控制裝置
通過指令控制字,應用程式也可以對裝置進行控制,通過如下函數完成:
rt_err_t rt_device_control(rt_device_t dev, rt_uint8_t cmd, void* arg); 複制錯誤複制成功
參數 | 描述 |
---|---|
dev | 裝置句柄 |
cmd | 指令控制字,這個參數通常與裝置驅動程式相關 |
arg | 控制的參數 |
傳回 | —— |
RT_EOK | 函數執行成功 |
-RT_ENOSYS | 執行失敗,dev 為空 |
其他錯誤碼 | 執行失敗 |
參數 cmd 的通用裝置指令可取如下宏定義:
#define RT_DEVICE_CTRL_RESUME 0x01 /* 恢複裝置 */
#define RT_DEVICE_CTRL_SUSPEND 0x02 /* 挂起裝置 */
#define RT_DEVICE_CTRL_CONFIG 0x03 /* 配置裝置 */
#define RT_DEVICE_CTRL_SET_INT 0x10 /* 設定中斷 */
#define RT_DEVICE_CTRL_CLR_INT 0x11 /* 清中斷 */
#define RT_DEVICE_CTRL_GET_INT 0x12 /* 擷取中斷狀态 */ 複制錯誤複制成功
讀寫裝置
應用程式從裝置中讀取資料可以通過如下函數完成:
rt_size_t rt_device_read(rt_device_t dev, rt_off_t pos,void* buffer, rt_size_t size); 複制錯誤複制成功
參數 | 描述 |
---|---|
dev | 裝置句柄 |
pos | 讀取資料偏移量 |
buffer | 記憶體緩沖區指針,讀取的資料将會被儲存在緩沖區中 |
size | 讀取資料的大小 |
傳回 | —— |
讀到資料的實際大小 | 如果是字元裝置,傳回大小以位元組為機關,如果是塊裝置,傳回的大小以塊為機關 |
需要讀取目前線程的 errno 來判斷錯誤狀态 |
調用這個函數,會從 dev 裝置中讀取資料,并存放在 buffer 緩沖區中,這個緩沖區的最大長度是 size,pos 根據不同的裝置類别有不同的意義。
向裝置中寫入資料,可以通過如下函數完成:
rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos,const void* buffer, rt_size_t size); 複制錯誤複制成功
參數 | 描述 |
---|---|
dev | 裝置句柄 |
pos | 寫入資料偏移量 |
buffer | 記憶體緩沖區指針,放置要寫入的資料 |
size | 寫入資料的大小 |
傳回 | —— |
寫入資料的實際大小 | 如果是字元裝置,傳回大小以位元組為機關;如果是塊裝置,傳回的大小以塊為機關 |
需要讀取目前線程的 errno 來判斷錯誤狀态 |
調用這個函數,會把緩沖區 buffer 中的資料寫入到裝置 dev 中,寫入資料的最大長度是 size,pos 根據不同的裝置類别存在不同的意義。
資料收發回調
當硬體裝置收到資料時,可以通過如下函數回調另一個函數來設定資料接收訓示,通知上層應用線程有資料到達:
rt_err_t rt_device_set_rx_indicate(rt_device_t dev, rt_err_t (*rx_ind)(rt_device_t dev,rt_size_t size));
複制錯誤複制成功
參數 | 描述 |
---|---|
dev | 裝置句柄 |
rx_ind | 回調函數指針 |
傳回 | —— |
RT_EOK | 設定成功 |
該函數的回調函數由調用者提供。當硬體裝置接收到資料時,會回調這個函數并把收到的資料長度放在 size 參數中傳遞給上層應用。上層應用線程應在收到訓示後,立刻從裝置中讀取資料。
在應用程式調用
rt_device_write()
寫入資料時,如果底層硬體能夠支援自動發送,那麼上層應用可以設定一個回調函數。這個回調函數會在底層硬體資料發送完成後 (例如 DMA 傳送完成或 FIFO 已經寫入完畢産生完成中斷時) 調用。可以通過如下函數設定裝置發送完成訓示,函數參數及傳回值見:
rt_err_t rt_device_set_tx_complete(rt_device_t dev, rt_err_t (*tx_done)(rt_device_t dev,void *buffer)); 複制錯誤複制成功
參數 | 描述 |
---|---|
dev | 裝置句柄 |
tx_done | 回調函數指針 |
傳回 | —— |
RT_EOK | 設定成功 |
調用這個函數時,回調函數由調用者提供,當硬體裝置發送完資料時,由驅動程式回調這個函數并把發送完成的資料塊位址 buffer 作為參數傳遞給上層應用。上層應用(線程)在收到訓示時會根據發送 buffer 的情況,釋放 buffer 記憶體塊或将其作為下一個寫資料的緩存。
裝置通路示例
下面代碼為用程式通路裝置的示例,首先通過
rt_device_find()
口查找到看門狗裝置,獲得裝置句柄,然後通過
rt_device_init()
口初始化裝置,通過
rt_device_control()
口設定看門狗裝置溢出時間。
#include <rtthread.h>
#include <rtdevice.h>
#define IWDG_DEVICE_NAME "iwg"
static rt_device_t wdg_dev;
static void idle_hook(void)
{
/* 在空閑線程的回調函數裡喂狗 */
rt_device_control(wdg_dev, RT_DEVICE_CTRL_WDT_KEEPALIVE, NULL);
rt_kprintf("feed the dog!\n ");
}
int main(void)
{
rt_err_t res = RT_EOK;
rt_uint32_t timeout = 1000; /* 溢出時間 */
/* 根據裝置名稱查找看門狗裝置,擷取裝置句柄 */
wdg_dev = rt_device_find(IWDG_DEVICE_NAME);
if (!wdg_dev)
{
rt_kprintf("find %s failed!\n", IWDG_DEVICE_NAME);
return RT_ERROR;
}
/* 初始化裝置 */
res = rt_device_init(wdg_dev);
if (res != RT_EOK)
{
rt_kprintf("initialize %s failed!\n", IWDG_DEVICE_NAME);
return res;
}
/* 設定看門狗溢出時間 */
res = rt_device_control(wdg_dev, RT_DEVICE_CTRL_WDT_SET_TIMEOUT, &timeout);
if (res != RT_EOK)
{
rt_kprintf("set %s timeout failed!\n", IWDG_DEVICE_NAME);
return res;
}
/* 設定空閑線程回調函數 */
rt_thread_idle_sethook(idle_hook);
return res;
}