侯佩儒,曹炳堯,宋英雄
(上海大學(xué) 特種光纖與光接入網(wǎng)重點實驗室,上海 200444)
隨著現(xiàn)有的工程系統(tǒng)越發(fā)規(guī)模大型化、需求復(fù)雜化、模塊細分化[1],如:自動駕駛汽車的車載嵌入式系統(tǒng)平均具有1億行代碼,并分布在近100個嵌入式計算機中[2]。傳統(tǒng)的設(shè)計模式使用文檔,難以進行有效的模塊劃分與跨領(lǐng)域溝通,從而導(dǎo)致研制周期存在較高風(fēng)險。因此,出現(xiàn)了基于模型的系統(tǒng)工程(MBSE,model-based systems engineering)。其采用標(biāo)準化建模的方式來支持系統(tǒng)的完整設(shè)計,改變了復(fù)雜系統(tǒng)的開發(fā)模式,尤其是對智慧物聯(lián)網(wǎng)技術(shù)的高速發(fā)展催生出的復(fù)雜嵌入式系統(tǒng)開發(fā)具備重要主導(dǎo)意義。目前,MBSE已廣泛應(yīng)用在航空航天[3-5]、國防軍工[6-7]、智慧城市[8]、軌道交通[9]等領(lǐng)域。
基于MBSE的復(fù)雜嵌入式系統(tǒng)設(shè)計包括需求分析、系統(tǒng)設(shè)計、測試驗證和需求確認等多個環(huán)節(jié)[10-11]。其中,虛擬驗證在測試驗證中尤為重要,但卻一定程度上缺乏研究和探索[12]。由于嵌入式系統(tǒng)對硬件的依賴性強、場景專業(yè)性高、系統(tǒng)復(fù)雜性大,對嵌入式軟件開發(fā)進行虛擬驗證需要在實物集成前提前模擬異構(gòu)硬件系統(tǒng),并考慮復(fù)雜嵌入式系統(tǒng)中各子系統(tǒng)之間的關(guān)聯(lián)。但是,目前仍缺乏高效通用、考慮整體的嵌入式模擬驗證工具或平臺。
虛擬化技術(shù)的快速發(fā)展與廣泛應(yīng)用,解決了單個嵌入式開發(fā)板的模擬問題。在本文中,每個利用虛擬化方式模擬的異構(gòu)子系統(tǒng)稱為虛擬設(shè)備。虛擬設(shè)備是指在MBSE模擬系統(tǒng)中,對應(yīng)嵌入式系統(tǒng)中可獨立分離的物理單元,用于驗證開發(fā)的軟件模擬組件。例如,在汽車部分自動化嵌入式系統(tǒng)中,有Arduino 和樹莓派兩個主從開發(fā)板模塊,前者連接控制電機的驅(qū)動器,后者連接多種傳感器進行數(shù)據(jù)處理與計算。這兩個模塊使用串行端口進行通信[13-14],是互為可分離的物理單元,使用虛擬化技術(shù)模擬的這兩塊模塊單元即為虛擬設(shè)備。常用的虛擬化技術(shù)有操作系統(tǒng)層面虛擬化的Docker[15]、硬件開發(fā)板層面虛擬化的QEMU(Quick Emulator)[16-17]等。QEMU具有跨平臺、高速度、可移植等優(yōu)點,因此,本文選取QEMU來創(chuàng)建虛擬設(shè)備。
嵌入式系統(tǒng)中,各組件間最普遍的通信方式是基于網(wǎng)絡(luò)和串行端口的通信。關(guān)于虛擬設(shè)備間的通信調(diào)試,已有一些相關(guān)研究。例如,文獻[18]利用將QEMU中虛擬網(wǎng)卡與宿主機的tap虛擬網(wǎng)卡綁定的方式進行虛擬設(shè)備網(wǎng)絡(luò)通信;文獻[19]利用源定位技術(shù)、基于頻率發(fā)包與跟蹤技術(shù)等方法開發(fā)了可模擬CAN總線的網(wǎng)絡(luò)仿真開發(fā)平臺,可進行物理或虛擬控制設(shè)備的總線通信測試等。然而,關(guān)于串行端口通信調(diào)試的研究相對較少,主要是采用物理和虛擬兩種方式。物理方式是使用計算機上的真實物理串行端口進行測試,需要使用物理連接線完成端口間的連通,并將真實串行端口配置綁定到虛擬設(shè)備內(nèi)使用。這種方式測試簡單、可靠,但是僅支持小規(guī)模驗證,在大規(guī)模場景下時會面臨物理接線復(fù)雜、計算機的端口資源受限等問題。虛擬方式是采用模擬手段,在宿主機內(nèi)產(chǎn)生模擬串行端口供虛擬設(shè)備測試使用。相較于物理方式,該方式的優(yōu)點在于不依賴硬件、驗證成本低。在Windows系統(tǒng)中,已有成熟可用的虛擬串行端口工具VSPD(virtual serial port driver)。在Linux系統(tǒng)中,雖然沒有類似的工具,但可以借助pty(pseudo terminal)偽終端設(shè)備來實現(xiàn)虛擬設(shè)備間的通信。但是,不論是成熟工具VSPD還是系統(tǒng)自帶pty設(shè)備,都進行了嚴密的封裝,難以靈活接入統(tǒng)計或埋點接口,自定義調(diào)試難度較高,導(dǎo)致虛擬設(shè)備間的串行端口通信驗證較為困難。由上述論述可知,針對虛擬設(shè)備互聯(lián)問題,需要研究靈活可用的串行通信模擬技術(shù),以便實現(xiàn)多組件間的通信調(diào)試。
本文針對在MBSE虛擬驗證環(huán)節(jié),嵌入式虛擬設(shè)備間的串行端口通信問題展開研究,以實現(xiàn)在Linux系統(tǒng)環(huán)境下完成虛擬設(shè)備間串行通信的目標(biāo),并支持復(fù)雜嵌入式系統(tǒng)的全面數(shù)字化模擬,對MBSE工程化的最后一步實施與落地、和我國“十四五”規(guī)劃深入推進數(shù)字化發(fā)展都具有重大意義。
為了解決復(fù)雜嵌入式系統(tǒng)中子系統(tǒng)之間的串行端口通信模擬驗證問題,需要首先構(gòu)建虛擬設(shè)備,并創(chuàng)建串行端口,再進行多個虛擬設(shè)備之間的通信。如圖1所示,通信時主要涉及到兩類主體對象:虛擬設(shè)備、串行端口鏈路,多個虛擬設(shè)備通過串行端口鏈路實現(xiàn)串行數(shù)據(jù)傳輸。
圖1 基于串行端口的虛擬設(shè)備間通信方式
虛擬設(shè)備的主要作用是為嵌入式軟件提供接近硬件的運行環(huán)境,如:具備模擬的嵌入式外圍設(shè)備、能夠利用二進制翻譯等技術(shù)直接執(zhí)行異構(gòu)指令集代碼等。由于虛擬設(shè)備采用軟件邏輯構(gòu)建模擬,在驗證各單元的通信過程中,缺少物理串口介質(zhì)的鏈接。而在其中執(zhí)行串行通信時,必須依賴串行端口,才能保證模擬系統(tǒng)中的程序可以和物理設(shè)備無縫移植。那就必須要求虛擬設(shè)備中具有串行端口,才能進一步對串行端口設(shè)備進行開啟關(guān)閉、發(fā)送數(shù)據(jù)、接收數(shù)據(jù)等操作。
串行端口鏈路位于多個虛擬設(shè)備之外,由多個外部串行端口及之間的端口連接線構(gòu)成。其主要作用是接收虛擬設(shè)備中內(nèi)部虛擬串行端口發(fā)送的數(shù)據(jù),并將數(shù)據(jù)傳輸?shù)搅硪欢?,發(fā)送給另一個虛擬設(shè)備,從而為兩端虛擬設(shè)備的內(nèi)部虛擬串行端口之間建立可通信的串行鏈路通道,實現(xiàn)虛擬設(shè)備間的串行端口通信。為了使內(nèi)部虛擬串行端口使用該鏈路,還需要將內(nèi)外串行端口進行綁定或映射。
為實現(xiàn)總體方案設(shè)計,根據(jù)虛擬設(shè)備間進行串行端口通信的方式,提出了如圖2所示的功能需求。
圖2 基于串行端口的虛擬設(shè)備間通信功能需求
根據(jù)上述功能需求分析,本文提出基于模擬串行端口的虛擬設(shè)備間通信架構(gòu),如圖3所示。自下而上分為物理硬件層、虛擬設(shè)備層、串行通信層。
圖3 整體方案架構(gòu)
物理硬件層:為Linux系統(tǒng)的服務(wù)器或虛擬機,為上層實現(xiàn)提供必要的整體硬件環(huán)境;
虛擬設(shè)備層:為多個虛擬設(shè)備的模擬,包括內(nèi)部虛擬串行端口的創(chuàng)建與綁定,以及其他硬件模擬;
串行通信層:為最上層,負責(zé)創(chuàng)建、配置、連接外部串行端口,連接后外部串行端口對之間可以構(gòu)建出串行通信鏈路,通過內(nèi)外部串行端口綁定和該串行通信鏈路,多個虛擬設(shè)備間即可實現(xiàn)串行端口通信。
總體方案設(shè)計與實現(xiàn)時,主要考慮以下4個功能模塊的設(shè)計:
1)外部串行端口創(chuàng)建模塊:
該模塊負責(zé)在虛擬設(shè)備外部創(chuàng)建具備串行端口相關(guān)操作功能的設(shè)備,以供虛擬設(shè)備使用。鑒于已有虛擬串行端口工具具有強封裝的特點,難以在其中接入調(diào)試、監(jiān)控、埋點等功能,無法自定義配置,只能進行基本驗證。
本方案將自行編碼實現(xiàn)開放、可控的虛擬串行端口模擬。為了具備兼容性、可擴展性,以及支持測試代碼在無額外修改的情況下的可移植性,采用底層驅(qū)動開發(fā)的方式,分析Linux系統(tǒng)中的標(biāo)準串行端口驅(qū)動,在此基礎(chǔ)上類比開發(fā)虛擬的串行端口驅(qū)動。該種方案下,用戶在應(yīng)用層看到的虛擬串行端口與標(biāo)準串行端口是一致的,操作也相同,區(qū)別僅在于執(zhí)行底層操作時前者使用模擬方式,而后者使用物理方式。
2)外部串行端口配置:
為模擬創(chuàng)建的外部串行端口配置鏈路等相關(guān)參數(shù)。例如:配置該端口需要建立連接的另一個串行端口的索引信息,以便于后續(xù)鏈路構(gòu)建。
3)外部串行端口連接:
對于多個配置了對端信息的虛擬串行端口,連接對端的作用主要在于構(gòu)建接收、發(fā)送數(shù)據(jù)的通信鏈路??梢岳枚喾N方式傳遞兩端發(fā)送的消息,例如:管道、共享內(nèi)存、文件、消息隊列、網(wǎng)絡(luò)等。以文件交互方式為例,每個串行端口維護自己的屬性文件,并設(shè)置文件的寫入回調(diào)函數(shù)。在發(fā)送時將信息寫入對端串行端口對應(yīng)的文件中,對端文件觸發(fā)寫入回調(diào),通知對端端口讀取文件,從而實現(xiàn)完整的收發(fā)過程。
相較于文件方式,網(wǎng)絡(luò)傳遞信息可開發(fā)度更高,也更具有分布式擴展性,更適合大規(guī)模MBSE系統(tǒng)虛擬驗證環(huán)節(jié)的云服務(wù)化。因此,本方案采用網(wǎng)絡(luò)方式實現(xiàn)多個串行端口的連接與鏈路構(gòu)建。
4)內(nèi)部虛擬串行端口創(chuàng)建與綁定模塊:
該模塊借助成熟的虛擬化軟件QEMU來實現(xiàn)虛擬設(shè)備的整體硬件模擬。并將模擬時創(chuàng)建的內(nèi)部虛擬串行端口與虛擬設(shè)備外、宿主機內(nèi)的外部串行端口進行綁定,將后者作為前者的設(shè)備后端,即可在虛擬設(shè)備內(nèi)使用外部串行端口實現(xiàn)具體功能。
根據(jù)上述設(shè)計可知,內(nèi)外部串行端口綁定之后,在虛擬設(shè)備內(nèi)對內(nèi)部串行端口執(zhí)行發(fā)送、接收等操作都是依靠外部串行端口及其通信鏈路來完成的。因此,本文接下來的研究重點是基于Linux系統(tǒng)底層驅(qū)動開發(fā)的虛擬外部串行端口模擬和其鏈路的構(gòu)建與實現(xiàn)。
在進行模擬串行端口的底層驅(qū)動開發(fā)之前,需要先對Linux系統(tǒng)中標(biāo)準串行端口的驅(qū)動實現(xiàn)進行分析,以標(biāo)準驅(qū)動為參照類比設(shè)計與實現(xiàn),確保系統(tǒng)對虛擬串行端口的兼容性和后續(xù)擴展性、可移植性。
串行端口是嵌入式系統(tǒng)中常用的硬件端口,有RS232、RS422、RS485等多種電氣標(biāo)準[20]。在Linux系統(tǒng)中,串行端口是一種tty(Teletypes)設(shè)備,用于串行地輸入、輸出數(shù)據(jù)。其設(shè)備驅(qū)動位于操作系統(tǒng)的內(nèi)核空間,提供了計算機硬件與應(yīng)用程序的接口。在內(nèi)核空間實現(xiàn)虛擬串行端口的模擬,能真正做到上層軟件無感知。
Linux內(nèi)核中,使用cdev結(jié)構(gòu)體描述字符設(shè)備,該結(jié)構(gòu)體是所有字符設(shè)備的抽象[21]。串行端口的整體驅(qū)動框架如圖4所示。自右至左,驅(qū)動可分為3個層次,分別為字符設(shè)備層、tty核心層和串行端口硬件層。字符設(shè)備層將tty設(shè)備作為字符設(shè)備,提供系統(tǒng)調(diào)用的統(tǒng)一接口,即cdev結(jié)構(gòu)體的操作函數(shù)集。操作函數(shù)會進一步調(diào)用tty核心層的實現(xiàn)函數(shù)。tty核心層對底層硬件的具體形式進行解耦與抽象,負責(zé)將上層操作經(jīng)線路規(guī)程進行格式化協(xié)議轉(zhuǎn)換后,調(diào)用至下層。串行硬件核心層則真正地實現(xiàn)了對設(shè)備的管理和操作,直接與底層硬件交互,完成用戶請求。
圖4 串行端口驅(qū)動框架
自頂層向下,驅(qū)動主要包括uart_driver、tty_driver、uart_state、uart_port四大數(shù)據(jù)結(jié)構(gòu)。uart_driver是全局的根數(shù)據(jù)結(jié)構(gòu),進行了高度的軟件邏輯抽象,負責(zé)保存和控制其他所有結(jié)構(gòu)體信息。tty_driver是對tty層的具體實現(xiàn);uart_state和uart_port是底層驅(qū)動的具體實現(xiàn)。整體而言,設(shè)備硬件和uart_state、uart_port一一對應(yīng),而一個uart_driver對應(yīng)一個tty_driver和多個uart_state、uart_port,即多個同類型設(shè)備共用一種設(shè)備驅(qū)動。
完整的串行端口驅(qū)動主要包括驅(qū)動注冊、設(shè)備初始化、應(yīng)用操作3個部分。本章以Linux系統(tǒng)中8250串行端口為例,依次從這3個部分進行分析。
在注冊前,8250串行端口的最上層驅(qū)動uart_driver通過dev_name成員指定該設(shè)備在Linux系統(tǒng)中對應(yīng)的字符設(shè)備文件名稱前綴,major和minor成員分別指定設(shè)備的主設(shè)備號和最大支持設(shè)備數(shù)量等。
之后,按照標(biāo)準串行端口的驅(qū)動注冊流程注冊。通過uart_register_driver()函數(shù)向內(nèi)核注冊上述uart_driver定義,并進行一系列初始化操作。為每個uart_state申請空間,初始化state的tty_port成員,并為驅(qū)動分配與初始化tty_driver,設(shè)置其操作函數(shù)集。該函數(shù)集即為系統(tǒng)調(diào)用的接口,構(gòu)建起Linux內(nèi)核驅(qū)動和用戶空間交互的橋梁。最后進行tty_driver的注冊,將其掛載到全局tty驅(qū)動鏈表,并進行proc文件相關(guān)的注冊。此時,雖然在內(nèi)核上注冊了驅(qū)動,但還沒有對接真正的硬件端口。
當(dāng)硬件設(shè)備接入系統(tǒng)時,系統(tǒng)會根據(jù)圖4左上角所示的平臺驅(qū)動,調(diào)用設(shè)備獨有的dw8250_probe()硬件初始化函數(shù)。該部分與硬件端口緊密相關(guān),但也遵守標(biāo)準串行端口的設(shè)備初始化流程。
8250串行端口在uart_port之上,封裝了具體類型uart_8250_port。在初始化時,首先根據(jù)調(diào)用的硬件平臺設(shè)備,對其中uart_port結(jié)構(gòu)體成員進行相關(guān)初始化。設(shè)置中斷處理函數(shù)、線路規(guī)程設(shè)置函數(shù)、設(shè)備類型、寄存器地址等,并分配私有數(shù)據(jù)等。其次,按照標(biāo)準串行端口設(shè)備初始化流程進一步初始化,主要通過uart_add_one_port()函數(shù)實現(xiàn)。該函數(shù)根據(jù)串行端口編號將對應(yīng)uart_state和該uart_port進行雙向綁定,并將設(shè)備對應(yīng)的uart_port添加到uart_driver。由此,設(shè)備利用硬件初始化函數(shù),通過串行端口硬件層,與tty核心層和字符設(shè)備層建立了聯(lián)系,用戶空間可以對真實的硬件設(shè)備進行操作。
在進行串行端口讀寫操作之前,需要先打開串行端口。打開流程如圖4所示,自右至左為系統(tǒng)調(diào)用后,內(nèi)核由字符設(shè)備層的tty_open()函數(shù)開始逐層調(diào)用。直到串行端口硬件層,根據(jù)uart_port的操作集調(diào)用8250串行端口獨有的serial8250_startup()函數(shù),進行串行端口的硬件設(shè)置,如波特率、請求發(fā)送信號和硬件寄存器設(shè)置等,并初始化與使能串行端口的中斷。
串行端口打開之后,以讀取數(shù)據(jù)為例分析,主要涉及到兩個線程,分別稱為前臺線程和后臺線程。應(yīng)用程序在用戶空間使用read()函數(shù)讀取串行端口數(shù)據(jù),經(jīng)系統(tǒng)調(diào)用進入內(nèi)核空間時由前臺線程執(zhí)行并進行等待。后臺線程負責(zé)在中斷有數(shù)據(jù)時進行讀取,把讀取的數(shù)據(jù)填充至tty_buffer中,再調(diào)用flush_to_ldisc()函數(shù),將數(shù)據(jù)存放進線路規(guī)程的數(shù)據(jù)接收緩存中,喚醒前臺線程,使前臺線程讀取緩存中的數(shù)據(jù),并將數(shù)據(jù)從內(nèi)核空間拷貝進用戶空間中,即可完成接收。向串行端口寫入數(shù)據(jù)也是類似的,但數(shù)據(jù)流相反,在此不做贅述。
根據(jù)對標(biāo)準串行端口的驅(qū)動分析,能夠知道Linux系統(tǒng)為各種不同類型的串行端口提供了可復(fù)用的uart框架。例如,通過封裝的根數(shù)據(jù)結(jié)構(gòu)uart_driver,不同的串行端口可以定義自己的驅(qū)動信息。如圖5所示,自定義串行端口驅(qū)動在復(fù)用已有框架的基礎(chǔ)上,需要修改和定義3個主要部分。第一部分是每個串行端口驅(qū)動注冊時都需要定義的,包含設(shè)備驅(qū)動的名稱、設(shè)備號等信息;第二和第三部分涉及底層硬件的特定實現(xiàn),不同的串行端口需提供不同的操作函數(shù)以與硬件進行交互。這三部分恰好對應(yīng)于驅(qū)動注冊、設(shè)備初始化和功能操作。
圖5 自定義串行端口驅(qū)動設(shè)計框架
基于標(biāo)準串行端口的驅(qū)動框架,本章參照8250串行端口的定義和注冊方式,對總體方案中的外部虛擬串行端口創(chuàng)建、配置、連接模塊進行設(shè)計,并最終實現(xiàn)了一個完整可用的外部虛擬串行端口驅(qū)動。
創(chuàng)建虛擬串行端口需要進行驅(qū)動注冊和設(shè)備初始化。驅(qū)動注冊確保虛擬設(shè)備能在Linux系統(tǒng)中被識別,并提供內(nèi)核空間的接口供用戶空間使用;設(shè)備初始化類比設(shè)備接入的動作,在系統(tǒng)上產(chǎn)生虛擬設(shè)備,并進行一系列初始化操作。
1)驅(qū)動注冊:
參考8250串行端口,虛擬串行端口首先需要定義自身的驅(qū)動信息。設(shè)置驅(qū)動名為“virtual-uart”、字符設(shè)備文件名稱前綴為“vttyU”、最大支持串行端口個數(shù)等。并且,在未顯式地指定設(shè)備號的情況下,Linux系統(tǒng)會在驅(qū)動加載時為其動態(tài)分配設(shè)備號,以避免人為指定發(fā)生沖突。驅(qū)動信息定義完成后,通過uart_register_driver()函數(shù)將其注冊進內(nèi)核,并對uart_driver等數(shù)據(jù)結(jié)構(gòu)進行初始化。后續(xù)過程與標(biāo)準串行端口驅(qū)動注冊流程相同,進行復(fù)用。
2)設(shè)備初始化:
在設(shè)備創(chuàng)建與初始化之前,需要先定義虛擬串行端口整體的端口數(shù)據(jù)結(jié)構(gòu)。類比8250串行端口在uart_port結(jié)構(gòu)體之上,進一步封裝了與硬件相關(guān)的具體類型。在驅(qū)動設(shè)計時,也為虛擬串行端口封裝了具體數(shù)據(jù)結(jié)構(gòu)virtual_uart_port,包含了uart_port。由于虛擬串行端口不具有硬件設(shè)備,所以無法提供物理中斷和寄存器。為了真實模擬串行端口接收與發(fā)送數(shù)據(jù)的流程,該數(shù)據(jù)結(jié)構(gòu)內(nèi)提供接收和發(fā)送使能標(biāo)志位,用于控制虛擬串行端口能否收發(fā),并使用自旋鎖保護這兩個使能信號。此外,該數(shù)據(jù)結(jié)構(gòu)還提供了一個工作隊列,用于模擬中斷處理函數(shù),完成數(shù)據(jù)中斷發(fā)送操作。
在具備整體的端口數(shù)據(jù)結(jié)構(gòu)后,就可以進行具體設(shè)備的定義與注冊。由于不存在真實設(shè)備,無法通過系統(tǒng)自動識別與掃描設(shè)備的方式獲取設(shè)備。因此,需要自行定義多個平臺設(shè)備,并為每個設(shè)備指定不重復(fù)的串行端口索引,以便后續(xù)串行端口連接時互相識別。定義好設(shè)備后,利用platform_device_register()函數(shù)將其注冊進內(nèi)核中。但此時設(shè)備并不能進行初始化操作,因為內(nèi)核中沒有該設(shè)備對應(yīng)的平臺驅(qū)動。
為了保證設(shè)備進一步初始化并與整體的端口數(shù)據(jù)結(jié)構(gòu)建立聯(lián)系,還需要定義對應(yīng)的平臺驅(qū)動。其結(jié)構(gòu)體為platform_driver,具有probe、remove接口。其中,probe接口完成設(shè)備對應(yīng)uart_port的注冊,remove接口完成uart_port的注銷。并且,需要為平臺驅(qū)動設(shè)置與設(shè)備相同的名稱來實現(xiàn)設(shè)備和驅(qū)動間的匹配檢測。此外,還需要實現(xiàn)設(shè)備注冊函數(shù),該函數(shù)為uart_port申請內(nèi)存,設(shè)置端口類型、序號、具體的操作函數(shù)集等,以及初始化整體數(shù)據(jù)結(jié)構(gòu)中的自旋鎖和工作隊列,并指定模擬串行端口發(fā)送中斷功能的函數(shù)作為該工作隊列的回調(diào)函數(shù)。其中,操作函數(shù)集和模擬發(fā)送中斷功能的回調(diào)函數(shù)是后續(xù)實現(xiàn)虛擬串行端口連接的重點內(nèi)容,將在后續(xù)章節(jié)具體設(shè)計。最后調(diào)用uart_add_one_port()函數(shù)將初始化后的uart_port添加到第一步注冊的驅(qū)動中。
配置模塊主要負責(zé)向串行端口配置構(gòu)建鏈路相關(guān)信息,例如:與該串行端口相連接的另一個串行端口的設(shè)備索引,以便串行端口可以自由組合與連接。這也為驅(qū)動模塊加載進內(nèi)核后,留下可交互的方式。
在用戶空間,可以使用ioctl(input/output control)函數(shù)對設(shè)備進行一些特殊控制,實現(xiàn)用戶空間和內(nèi)核驅(qū)動之間的溝通。該函數(shù)指定設(shè)備的文件描述符、請求號和具體參數(shù),其中請求號代表交互協(xié)議,設(shè)備驅(qū)動會根據(jù)請求號執(zhí)行相應(yīng)操作。用戶空間的ioctl請求經(jīng)系統(tǒng)調(diào)用后,會調(diào)用字符設(shè)備層的tty_ioctl()函數(shù),函數(shù)內(nèi)部將進一步根據(jù)請求號調(diào)用相應(yīng)控制函數(shù)。因此,需要為配置事件預(yù)設(shè)未被占用、唯一的請求號。
進入內(nèi)核空間后,在設(shè)備初始化時,可以通過uart_port指定的操作函數(shù)集實現(xiàn)對應(yīng)ioctl()函數(shù)。該函數(shù)根據(jù)預(yù)設(shè)的請求號執(zhí)行期望的配置操作,例如:配置構(gòu)建鏈路所需對端信息。首先,獲取攜帶的請求參數(shù),即得到對端串行端口的索引,并將其保存在本設(shè)備端口信息中。之后在串行端口連接時,就可以知道連接的對象。如果需要更換連接對象,只需要重新配置即可,使驅(qū)動更加靈活。
真實的物理串行端口對是通過外部物理連接線來連通的,虛擬串行端口則只能使用軟件的方式進行實現(xiàn)。經(jīng)過總體方案中對文件、共享內(nèi)存、管道等實現(xiàn)手段的討論,為了具有更高可開發(fā)度和擴展性,決定采用網(wǎng)絡(luò)的連接方式。具體實現(xiàn)方式是在設(shè)備驅(qū)動中設(shè)置uart_port的操作函數(shù)集,使用網(wǎng)絡(luò)收發(fā)方式完成虛擬串行端口的發(fā)送和接收。
使用網(wǎng)絡(luò)來模擬虛擬串行端口間的通道連接有兩種方案,如圖6所示。
圖6 網(wǎng)絡(luò)模擬串行端口間通信的方案示意圖
第一種方案需要在每個串行端口的整體數(shù)據(jù)結(jié)構(gòu)中維護一個網(wǎng)絡(luò)Socket套接字成員,并且兩個要連接的串行端口要求一個具備服務(wù)端,另一個具備客戶端,根據(jù)配置信息來設(shè)置本設(shè)備的Socket句柄是服務(wù)端或是客戶端。在串行端口設(shè)備初始化時,通過對端索引信息獲取對端地址,連接對端Socket。連接后,將對端Socket句柄一并放入整體數(shù)據(jù)結(jié)構(gòu)中維護,以便于收發(fā)數(shù)據(jù)時直接使用。通過這種方式,可以模擬出串行端口之間的收發(fā)傳輸通道。
第二種方案需要在每個串行端口的整體數(shù)據(jù)結(jié)構(gòu)中維護兩個網(wǎng)絡(luò)Socket套接字成員,其中一個為服務(wù)端,用于模擬串行端口的接收數(shù)據(jù)引腳,另一個為客戶端,用于模擬串行端口的發(fā)送數(shù)據(jù)引腳。在這種方式下,兩個要連接的串行端口只需要將本端的客戶端連接到對端的服務(wù)端即可完成連接,并由客戶端發(fā)送數(shù)據(jù),服務(wù)端接收數(shù)據(jù)。通過這種方式,可以模擬出真實的串行端口物理傳輸方式,即兩個串行端口對之間建立的兩條網(wǎng)絡(luò)通道對應(yīng)了真實串行端口的發(fā)送與接收數(shù)據(jù)兩條物理線。
相較于第一種方案,第二種方案更為細化,每個串行端口都是相同且獨立的,串行端口之間耦合性更低,無需像第一種方案根據(jù)端口索引值設(shè)置本端與對端Socket套接字是客戶端還是服務(wù)端,再進行成對綁定。因此,本文選擇第二種方案來實現(xiàn)連接模塊。
在設(shè)備初始化時,根據(jù)設(shè)備索引值配置監(jiān)聽端口號并開啟服務(wù)端,等待客戶端連接請求。同時,開啟一個線程循環(huán)判斷服務(wù)端是否接收到消息。若有消息,則模擬接收中斷,將收取到的數(shù)據(jù)發(fā)送到tty_buffer的接收緩存中,并調(diào)用tty_flip_buffer_push()函數(shù),將接收緩存中的數(shù)據(jù)通過線路規(guī)程進行接收。在發(fā)送數(shù)據(jù)時,使用設(shè)備初始化時配置給工作隊列的回調(diào)函數(shù)來模擬發(fā)送中斷。在回調(diào)函數(shù)中,客戶端根據(jù)對端索引值獲取端口號,連接對端串行端口的服務(wù)端,并利用網(wǎng)絡(luò)連接發(fā)送數(shù)據(jù)。即可在沒有實際硬件的情況下,使用虛擬串行端口完成數(shù)據(jù)收發(fā)的功能。
本文使用12核CPU、32GB內(nèi)存、200GB硬盤的虛擬機作為宿主機進行實驗,該虛擬機位于搭載Intel(R)Xeon(R)Silver 4214R CPU @ 2.40GHz處理器、型號為UniServer R4900 G3的服務(wù)器上,并使用QEMU模擬器來模擬虛擬設(shè)備,完成整體方案驗證,具體實驗系統(tǒng)環(huán)境如表1所示。
表1 實驗系統(tǒng)環(huán)境參數(shù)
為了驗證方案設(shè)計與實現(xiàn)的有效性和正確性,分別對軟件模擬的串行端口進行功能和性能驗證、對基于模擬串行端口的虛擬設(shè)備互聯(lián)通信進行可行性驗證。
在本方案中,虛擬串行端口驅(qū)動被分為設(shè)備注冊和串行端口整體驅(qū)動兩部分。前者負責(zé)定義硬件設(shè)備并將其注冊進內(nèi)核中,以模擬硬件接入;后者負責(zé)實現(xiàn)串行端口相關(guān)結(jié)構(gòu)的初始化、注冊、端口連接等功能,是串行端口的整體設(shè)備驅(qū)動。這兩個驅(qū)動模塊均具備頭文件、模塊加載與卸載函數(shù)、模塊許可聲明等規(guī)范結(jié)構(gòu)。同時,為驅(qū)動模塊編寫編譯文件,內(nèi)容包括讀取內(nèi)核源碼中的編譯文件和指明模塊源碼中各文件的依賴關(guān)系等。
編譯執(zhí)行后,會生成對應(yīng).ko模塊文件,可以使用insmod命令加載模塊。以兩個虛擬串行端口為例,驅(qū)動模塊被加載到系統(tǒng)內(nèi)核后,將執(zhí)行初始化程序,開辟內(nèi)存、新建線程等。此時,如表2所示,在系統(tǒng)的/dev目錄下,將會出現(xiàn)兩個串行端口字符設(shè)備,分別為vttyU0和vttyU1,設(shè)備前綴由驅(qū)動定義的dev_name指定。
表2 驅(qū)動加載前后系統(tǒng)/dev目錄變化
用戶態(tài)通過執(zhí)行ioctl()函數(shù)來指定每個串行端口的對端索引信息。內(nèi)核態(tài)驅(qū)動完成相應(yīng)的設(shè)置后,本設(shè)備的Socket客戶端就會連接至對端設(shè)備的Socket服務(wù)端,實現(xiàn)串行端口之間的鏈路連通。在本實驗中,即連接了vttyU0和vttyU1。對于用戶態(tài)的應(yīng)用程序而言,這兩個字符設(shè)備與其他普通串行端口設(shè)備并無區(qū)別。此時,兩個串行端口的連接情況如圖7所示。編寫串行端口讀寫程序,對其進行功能和性能測試。
圖7 串行端口測試連接圖
1)功能測試。將vttyU1作為發(fā)送端,vttyU0作為接收端,進行數(shù)據(jù)持續(xù)收發(fā)操作,并將數(shù)據(jù)內(nèi)容及傳輸次數(shù)打印到終端。交換發(fā)送端與接收端后,結(jié)果仍然相同,結(jié)果如下所示:
(1)vttyU1發(fā)送數(shù)據(jù)
action@action-virtual-machine:~/uart-test sudo ./virtual_uart_test --dev=/dev/vttyU1 --type=write
Send Data: SEND TEST! Num = 1!
Send Data: SEND TEST! Num = 2!
Send Data: SEND TEST! Num = 3!
Send Data: SEND TEST! Num = 4!
Send Data: SEND TEST! Num = 5!
Send Data: SEND TEST! Num = 6!
Send Data: SEND TEST! Num = 7!
Send Data: SEND TEST! Num = 8!
Send Data: SEND TEST! Num = 9!
Send Data: SEND TEST! Num = 10!
(2)vttyU0接收數(shù)據(jù)
action@action-virtual-machine:~/uart-test sudo ./virtual_uart_test --dev=/dev/vttyU0 --type=read
Receive Data: SEND TEST! Num = 1!
Receive Data: SEND TEST! Num = 2!
Receive Data: SEND TEST! Num = 3!
Receive Data: SEND TEST! Num = 4!
Receive Data: SEND TEST! Num = 5!
Receive Data: SEND TEST! Num = 6!
Receive Data: SEND TEST! Num = 7!
Receive Data: SEND TEST! Num = 8!
Receive Data: SEND TEST! Num = 9!
Receive Data: SEND TEST! Num = 10!
實驗結(jié)果證明,串行端口模擬產(chǎn)生后,在用戶態(tài)的操作與物理串行端口的操作一致,接收到的數(shù)據(jù)和對端發(fā)送的數(shù)據(jù)內(nèi)容相同,因此通信功能可用。
2)性能測試。對串行端口的最大傳輸速率進行測試,測試方式為:將vttyU1作為發(fā)送端,以最小的時間間隔,持續(xù)發(fā)送1 024字節(jié)的數(shù)據(jù)給vttyU0,使得兩個串行端口驅(qū)動一直處于最大速率的發(fā)送與接收狀態(tài)。在接收端vttyU0設(shè)置流量計數(shù)模塊和時間計算模塊,得到接收一定數(shù)據(jù)量時所花費的時間,以計算單位時間內(nèi)傳輸?shù)谋忍財?shù),即數(shù)據(jù)傳輸速率。本實驗進行20組測試,每組測試發(fā)送和接收100 MB數(shù)據(jù),結(jié)果如圖8所示。
圖8 串行端口傳輸速率測試結(jié)果
實驗結(jié)果表明,本方案設(shè)計的虛擬串行端口驅(qū)動的數(shù)據(jù)傳輸速率快且較為穩(wěn)定,最大傳輸速率介于416.26 ~474.74 mbps之間,平均值為456.98 mbps,遠超過物理串行端口的傳輸速率。例如,RS232串行端口最大傳輸速率為20 kbps,RS422/RS485串行端口最大傳輸速率為10 mbps等。此外,串行端口也在不斷發(fā)展更高速的傳輸速率。該虛擬串行端口不僅能夠滿足基礎(chǔ)物理串行端口模擬的速率要求,也能夠應(yīng)對速率更高的增強型串行端口模擬的場景,符合設(shè)計需求。
對虛擬設(shè)備間互聯(lián)通信進行驗證,首先需要創(chuàng)建兩個虛擬設(shè)備,使用QEMU在宿主機內(nèi)模擬兩塊樹莓派3B開發(fā)板,包括CPU、內(nèi)存、外圍設(shè)備等硬件資源模擬。其次,每個虛擬設(shè)備內(nèi)都具有串行端口,將前述實驗中創(chuàng)建的兩個模擬串行端口分別綁定為虛擬設(shè)備內(nèi)串行端口的設(shè)備后端,使得兩個虛擬設(shè)備可以借助外部宿主機內(nèi)的虛擬串行端口對進行通信。模擬與綁定的具體命令如下所示:
指定模擬樹莓派3B 1024 MB內(nèi)存
sudo qemu-system-aarch64 -M raspi3b -m 1024
-dtb img/bcm2710-rpi-3-b.dtb
指定內(nèi)核
-kernel img/kernel8.img
指定加載鏡像與格式
-drive format=raw,file=img/2020-02-13-raspbian-buster.img
啟動附加命令
-append "rw earlycon=pl011,0x3f201000 console=ttyAMA0 loglevel=8 root=/dev/ mmcblk0p2 fsck.repair=yes net.ifnames=0 rootwait memtest=1 dwc_otg.fiq_fsm_enable=0 8250.nr_uarts=1"
指定標(biāo)準輸入輸出
-serial stdio
為樹莓派啟用USB鍵盤鼠標(biāo)等模擬
-usb -device usb-kbd -device usb-tablet
綁定外部串行端口/dev/vttyU0(另一個綁定/dev/vttyU1)
-serial /dev/vttyU0
綁定完成后,啟動虛擬設(shè)備,進入系統(tǒng)后可以看到在/dev目錄下都多出一個設(shè)備ttyS0。該設(shè)備是串行端口的設(shè)備前端,將消息在虛擬設(shè)備和設(shè)備后端之間進行轉(zhuǎn)發(fā),真實地與虛擬設(shè)備外進行數(shù)據(jù)收發(fā)交互的仍為宿主機內(nèi)創(chuàng)建的虛擬串行端口。由此,兩塊虛擬開發(fā)板借助外部虛擬串行端口對vttyU0和vttyU1建立了兩個內(nèi)部ttyS0的通信鏈路,連接情況如圖9所示。
圖9 虛擬設(shè)備串行通信測試連接圖
對其進行收發(fā)測試,測試方式為:在一塊虛擬樹莓派開發(fā)板內(nèi)部打開串行端口ttyS0,發(fā)送數(shù)據(jù)并打印發(fā)送次數(shù),另一塊虛擬樹莓派開發(fā)板內(nèi)部打開串行端口ttyS0,進行數(shù)據(jù)接收并輸出至終端。反過來通信也是相同的,測試結(jié)果如下所示:
(1)虛擬設(shè)備1發(fā)送數(shù)據(jù)
pi@raspberrypi:~/myTests/uart-test ./virtual-uart --dev=/dev/ttyS0 --type=write
Send Data: SEND TEST! Num = 1!
Send Data: SEND TEST! Num = 2!
Send Data: SEND TEST! Num = 3!
Send Data: SEND TEST! Num = 4!
Send Data: SEND TEST! Num = 5!
Send Data: SEND TEST! Num = 6!
Send Data: SEND TEST! Num = 7!
Send Data: SEND TEST! Num = 8!
Send Data: SEND TEST! Num = 9!
Send Data: SEND TEST! Num = 10!
(2)虛擬設(shè)備2接收數(shù)據(jù)
pi@raspberrypi:~/myTests/uart-test ./virtual-uart --dev=/dev/ttyS0 --type=read
Receive Data: SEND TEST! Num = 1!
Receive Data: SEND TEST! Num = 2!
Receive Data: SEND TEST! Num = 3!
Receive Data: SEND TEST! Num = 4!
Receive Data: SEND TEST! Num = 5!
Receive Data: SEND TEST! Num = 6!
Receive Data: SEND TEST! Num = 7!
Receive Data: SEND TEST! Num = 8!
Receive Data: SEND TEST! Num = 9!
Receive Data: SEND TEST! Num = 10!
測試結(jié)果表明,虛擬設(shè)備間可以利用本方案實現(xiàn)的虛擬串行端口,建立傳輸通道,進行雙向通信。該程序也可以直接移植到物理的嵌入式終端上,直接進行串行設(shè)備通信,可以確保模擬系統(tǒng)和程序具有較高的保真度和移植性。
本文針對在MBSE系統(tǒng)中的虛擬設(shè)備間進行串行互聯(lián)通信的需求,提出了一種針對Linux系統(tǒng)的軟件模擬串行端口方案,并基于模擬串行端口實現(xiàn)多個嵌入式虛擬設(shè)備之間的互聯(lián)通信。本文的串行端口模擬通過參照Linux系統(tǒng)的標(biāo)準串行端口驅(qū)動,復(fù)用標(biāo)準uart驅(qū)動框架,完成內(nèi)核驅(qū)動開發(fā),從內(nèi)核態(tài)產(chǎn)生虛擬串行端口,并實現(xiàn)了參數(shù)配置、通道連接等功能。
通過對兩個已連接的虛擬串行端口進行通信測試,驗證了本文提出的模擬串行端口的可用性和具備平均456.98Mbps的最大傳輸速率,能夠滿足物理串行端口模擬的傳輸速率要求。
同時,通過為QEMU構(gòu)建的虛擬設(shè)備綁定模擬串行端口,實現(xiàn)了虛擬設(shè)備之間的串行數(shù)據(jù)通信?;谔摂M設(shè)備開發(fā)的串行通信程序,可直接移植到物理硬件設(shè)備上,提高了嵌入式系統(tǒng)開發(fā)調(diào)試的效率,為復(fù)雜嵌入式系統(tǒng)的全面數(shù)字化模擬提供了支持。