文章目錄
Android的RIL驅動模組, 在hardware/ril目錄下,一共分rild,libril.so以及librefrence_ril.so三個部分,另有一 radiooptions可供自動或手動調試使用。都依賴於include目錄中ril.h標頭檔。目前cupcake分支上帶的是gsm的支援,另有一 cdma分支,這裡分析的是gsm驅動。
GSM模組,由於Modem的曆史原因,AP一直是通過基於串口的AT命令與BB互動。包括到了目前的一些edge或3g模組,或像omap這類ap,bp整合的晶片, 已經使用了USB或其他等高速匯流排通訊,但大多仍然使用類比串口機制來使用AT命令。這裡的RIL(Radio Interface Layer)層,主要也就是基於AT命令的操作,如發命令,response解析等。(gprs等傳輸會用到的MUX協議等在這裡並沒有包含,也暫不作介 紹。)
以下是詳細分析,本文主要涉及基本架構和初始化的內容:
首先介紹一下rild與libril.so以及librefrence_ril.so的關係:
1. rild:
僅實現一main函數作為整個ril層的進入點,負責完成初始化。
2. libril.so:
與rild結合相當緊密,是其共用庫,編譯時間就已經建立了這一關係。組成部分為ril.cpp,ril_event.cpp。libril.so駐留在 rild這一守護進程中,主要完成同上層通訊的工作,接受ril請求並傳遞給librefrence_ril.so, 同時把來自librefrence_ril.so的反饋回傳給調用進程。
3. librefrence_ril.so:
rild通過手動的dlopen方式載入,結合稍微鬆散,這也是因為librefrence.so主要負責跟Modem硬體通訊的緣故。這樣做更方便替 換或修改以適配更多的Modem種類。它轉換來自libril.so的請求為AT命令,同時監控Modem的反饋資訊,並傳遞迴libril.so。在初 始化時, rild通過符號RIL_Init擷取一組函數指標並以此與之建立聯絡。
4. radiooptions:
radiooptiongs通過擷取啟動參數, 利用socket與rild通訊,可供調試時配置Modem參數。
接下來分析初始化流程,主入口是rild.c中的main函數,主要完成三個任務:
1. 開啟libril.so中的event機制, 在RIL_startEventLoop中,是最核心的由多路I/O驅動的訊息迴圈。
2. 初始化librefrence_ril.so,也就是跟硬體或類比硬體modem通訊的部分(後面統一稱硬體), 通過RIL_Init函數完成。
3. 通過RIL_Init擷取一組函數指標RIL_RadioFunctions, 並通過RIL_register完成註冊,並開啟接受上層命令的socket通道。
首先看第一個任務,也就是RIL_startEventLoop函數。RIL_startEventLoop在ril.cpp中實現, 它的主要目的是通過pthread_create(&s_tid_dispatch, &attr, eventLoop, NULL)建立一個dispatch線程,進入點在eventLoop. 而eventLoop中,會調ril_event.cpp中的ril_event_loop()函數,建立起訊息(event)隊列機制。
我們來仔細看看這一訊息佇列的機制,這些代碼都在ril_event.cpp中。
void ril_event_init();
void ril_event_set(struct ril_event * ev, int fd, bool persist, ril_event_cb func, void * param);
void ril_event_add(struct ril_event * ev);
void ril_timer_add(struct ril_event * ev, struct timeval * tv);
void ril_event_del(struct ril_event * ev);
void ril_event_loop();
struct ril_event {
struct ril_event *next;
struct ril_event *prev;
int fd;
int index;
bool persist;
struct timeval timeout;
ril_event_cb func;
void *param;
};
每個ril_event結構,與一個fd控制代碼綁定(可以是檔案,socket,管道等),並且帶一個func指標去執行指定的操作。
具體流程是: ril_event_init完成後,通過ril_event_set來配置一新ril_event,並通過ril_event_add排入佇列之中(實 際通常用rilEventAddWakeup來添加),add會把隊列裡所有ril_event的fd,放入一個fd集合readFds中。這樣 ril_event_loop能通過一個多工I/O的機制(select)來等待這些fd, 如果任何一個fd有資料寫入,則進入分析流程processTimeouts(),processReadReadies(&rfds, n),firePending()。 後文會詳細分析這些流程。
另外我們可以看到, 在進入ril_event_loop之前, 已經掛入了一s_wakeupfd_event, 通過pipe的機制實現的, 這個event的目的是可以在一些情況下,能內部喚醒ril_event_loop的多工阻塞,比如一些帶timeout的命令timeout到期的 時候。
至此第一個任務分析完畢,這樣便建立起了基於event隊列的訊息迴圈,稍後便可以接受上層發來的的請求了(上層請求的event對象建立,在第三個任務中)。
接下來看第二個任務,這個任務的入口是RIL_Init, RIL_Init首先通過參數擷取硬體介面的裝置檔案或類比硬體介面的socket. 接下來便新開一個線程繼續初始化, 即mainLoop。
mainLoop的主要任務是建立起與硬體的通訊,然後通過read方法阻塞等待硬體的主動上報或響應。在註冊一些基礎回調 (timeout,readerclose)後,mainLoop首先開啟硬體裝置檔案,建立起與硬體的通訊,s_device_path和s_port 是前面擷取的裝置路徑參數,將其開啟(兩者可以同時開啟並擁有各自的reader,這裡也很容易添加雙卡雙待等支援)。
接下來通過 at_open函數建立起這一裝置檔案上的reader等待迴圈,這也是通過建立一個線程完成, ret = pthread_create(&s_tid_reader, &attr, readerLoop, &attr),進入點readerLoop。
AT命令都是以rn或nr的分行符號來作為分隔字元的,所以readerLoop是line驅動的,除非出錯,逾時等,否則會讀到一行完整的響應或主動上 報,才會返回。這個迴圈跑起來以後,我們基本的AT響應機制已經建立了起來。它的具體分析,包括at_open中掛接的ATUnsolHandler, 我們都放到後面分析response的連載文章裡去。
有了響應的機制(當然,能與硬體通訊也已經可以發請求了),通過 RIL_requestTimedCallback(initializeCallback, NULL, &TIMEVAL_0),跑到initializeCallback中,執行一些Modem的初始化命令,主要都是AT命令的方式。發AT命令的 流程,我們放到後面分析request的連載文章裡。這裡可以看到,主要是一些參數配置,以及網路狀態的檢查等。至此第二個任務分析完畢,硬體已經可以訪 問了。
最後是第三個任務。第三個任務是由RIL_Init的傳回值開始的,這是一個RIL_RadioFunctions結構的指標。
typedef struct {
int version; /* set to RIL_VERSION */
RIL_RequestFunc onRequest;
RIL_RadioStateRequest onStateRequest;
RIL_Supports supports;
RIL_Cancel onCancel;
RIL_GetVersion getVersion;
} RIL_RadioFunctions;
其中最重要的是onRequest域,上層來的請求都由這個函數進行映射後轉換成對應的AT命令發給硬體。
rild通過RIL_register註冊這一指標。
RIL_register中要完成的另外一個任務,就是開啟前面提到的跟上層通訊的socket介面(s_fdListen是主介面,s_fdDebug供調試時使用)。
然後將這兩個socket介面使用任務一中實現的機制進行註冊(僅列出s_fdListen)
ril_event_set (&s_listen_event, s_fdListen, false,
listenCallback, NULL);
rilEventAddWakeup (&s_listen_event);
這樣將兩個socket加到任務一中建立起來多工I/O的檢查控制代碼集合中,一旦有上層來的(調試)請求,event機制便能響應處理了。到這裡啟動流程已經分析完畢。
request流程
1. 多工I/O機制的運轉
上文說到request是接收,是通過ril_event_loop中的多工I/O,也對初始化做了分析.現在我們來仔細看看這個機制如何運轉.
ril_event_set負責配置一個event,主要有兩種event:
ril_event_add添加使用多路I/O的event,它負責將其掛到隊列,同時將event的通道控制代碼fd加入到watch_table,然後通過select等待.
ril_timer_add添加timer event,它將其掛在隊列,同時重新計算最短逾時時間.
無論哪種add,最後都會調用triggerEvLoop來重新整理隊列,更新逾時值或等待對象.
重新整理之後, ril_event_loop從阻塞的位置,select返回,只有兩種可能,一是逾時,二是等待到了某I/O操作.
逾時的處理在processTimeouts中,摘下逾時的event,加入pending_list.
檢查有I/O操作的通道的處理在processReadReadies中,將逾時的event加入pending_list.
最後在firePending中,檢索pending_list的event並依次執行event->func.
這些操作完之後,計算新逾時時間,並重新select阻塞於多路I/O.
前面的初始化流程已分析得知,初始化完成以後,隊列上掛了3個event對象,分別是:
s_listen_event: 名為rild的socket,主要requeset & response通道
s_debug_event: 名為rild-debug的socket,調試用requeset & response通道(流程與s_listen_event基本相同,後面僅分析s_listen_event)
s_wakeupfd_event: 無名管道,用於隊列主動喚醒(前面提到的隊列重新整理,就用它來實現,請參考使用它的相關地方)
2. request的傳入和dispatch
明白了event隊列的基本運行流程,我們可以來看看request是怎麼傳入和dispatch的了.
上 層的部分,核心代碼在frameworks/base/telephony/java/com/android/internal/telephony /gsm/RIL.java,這是android java架構處理radio(gsm)的核心組件.本文因為主要關注rild,也就是驅動部分,所以這裡只作簡單介紹.
我們看一個具體的例子,RIL.java中的dial函數:
public void
dial (String address, int clirMode, Message result)
{
RILRequest rr = RILRequest.obtain(RIL_REQUEST_DIAL, result);
rr.mp.writeString(address);
rr.mp.writeInt(clirMode);
if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
send(rr);
}
rr是以RIL_REQUEST_DIAL為request號而申請的一個RILRequest對象.這個request號在java架構和rild庫中共用(參考RILConstants.java中這些值的由來:))
RILRequest初始化的時候,會串連名為rild的socket(也就是rild中s_listen_event綁定的socket),初始化資料轉送的通道.
rr.mp 是Parcel對象,Parcel是一套簡單的序列化協議,用於將對象(或對象的成員)序列化成位元組流,以供傳遞參數之用.這裡可以看到String address和int clirMode都是將依次序列化的成員.在這之前,rr初始化的時候,request號跟request的序號(自動產生的遞增數),已經成為頭兩個 將被序列化的成員.這為後面的request解析打下了基礎.
接下來是send到handleMessage的流程,send將rr直接傳遞給另 一個線程的handleMessage,handleMessage執行data = rr.mp.marshall()執行序列化操作, 並將data位元組流寫入到rild socket.
接下來回到我們的rild,select發現rild socket有了請求連結的訊號,導致s_listen_event被掛入pending_list,執行event->func,即
static void listenCallback (int fd, short flags, void *param);
接下來,s_fdCommand = accept(s_fdListen, (sockaddr *) &peeraddr, &socklen),擷取傳入的socket描述符,也就是上層的java RIL傳入的串連.
然 後,通過record_stream_new建立起一個record_stream, 將其與s_fdCommand綁定, 這裡我們不關注record_stream 的具體流程, 我們來關注command event的回調, processCommandsCallback函數, 從前面的event機制分析, 一旦s_fdCommand上有資料, 此回呼函數就會被調用. (略過onNewCommandConnect的分析)
processCommandsCallback通過 record_stream_get_next阻塞讀取s_fdCommand上發來的 資料, 直到收到一完整的request(request包的完整性由record_stream的機制保證), 然後將其送達processCommandBuffer.
進入processCommandBuffer以後,我們就正式進入了命令的解析部分. 每個命令將以RequestInfo的形式存在.
typedef struct RequestInfo {
int32_t token; //this is not RIL_Token
CommandInfo *pCI;
struct RequestInfo *p_next;
char cancelled;
char local; // responses to local commands do not go back to command process
} RequestInfo;
這 裡的pRI就是一個RequestInfo結構指標, 從socket過來的資料流, 前面提到是Parcel處理過的序列化位元組流, 這裡會通過還原序列化的方法提取出來. 最前面的是request號, 以及token域(request的遞增序號). 我們更關注這個request號, 前面提到, 上層和rild之間, 這個號是統一的. 它的定義是一個包含ril_commands.h的枚舉, 在ril.cpp中
static CommandInfo s_commands[] = {
#include "ril_commands.h"
};
pRI直接存取這個數組, 來擷取自己的pCI.
這是一個CommandInfo結構:
typedef struct {
int requestNumber;
void (*dispatchFunction) (Parcel &p, struct RequestInfo *pRI);
int(*responseFunction) (Parcel &p, void *response, size_t responselen);
} CommandInfo;
基本解析到這裡就完成了, 接下來, pRI被掛入pending的request隊列, 執行具體的pCI->dispatchFunction, 進行詳細解析.
3. request的詳細解析
對dial而言, CommandInfo結構是這樣初始化的:
{RIL_REQUEST_DIAL, dispatchDial, responseVoid},
這 裡執行dispatchFunction, 也就是dispatchDial這一函數.我們可以看到其實有很多種類的dispatch function, 比如dispatchVoid, dispatchStrings, dispatchSIM_IO等等, 這些函數的區別, 在於Parcel傳入的參數形式,Void就是不帶參數的,Strings是以string[]做參數,又如Dial等,有自己的參數解析方式,以此類 推.
request號和參數現在都有了,那麼可以進行具體的request函數調用了.
s_callbacks.onRequest(pRI->pCI->requestNumber, xxx, len, pRI)完成這一操作.
s_callbacks 是上篇文章中提到的擷取自libreference-ril的RIL_RadioFunctions結構指標,request請求在這裡轉入底層的 libreference-ril處理,handler是reference-ril.c中的onRequest.
onRequest進行一個簡單的switch分發,我們依然來看RIL_REQUEST_DIAL
流程是 onRequest-->requestDial-->at_send_command-->at_send_command_full-->at_send_command_full_nolock-->writeline
requestDial中將命令和參數轉換成對應的AT命令,調用公用send command介面at_send_command.
除 了這個介面之外,還有 at_send_command_singleline,at_send_command_sms,at_send_command_multiline 等,這是根據at傳回值,以及發命令流程的類型來區別的.比如at+csq這類,需要at_send_command_singleline,而發送短 信,因為有prompt提示符">",傳裸資料,結束符等一系列操作,需要專門用at_send_command_sms來實現.
然後執行at_send_command_full,前面幾個介面都會最終到這裡,再通過一個互斥的at_send_command_full_nolock調用,然後完成最終的寫出操作,在writeline中,寫出到初始化時開啟的裝置中.
writeline返回之後,還有一些操作,如儲存type等資訊,供response回來時候使用,以及一些逾時處理. 不再詳述.
到這裡,request的詳細流程,就分析完畢了.
response流程
前文對request的分析, 終止在了at_send_command_full_nolock裡的writeline操作,因為這裡完成命令寫出到硬體裝置的操作,接下來就是等待硬體響應,也就是response的過程了。我們的分析也是從這裡開始。
response資訊的擷取,是在第一篇初始化分析中,提到的readerLoop中。由readline函數以‘行’為單位接收上來。
AT的response有兩種,一是主動上報的,比如網路狀態,簡訊,來電等都不需要經過請求,有一unsolicited詞語專門描述。另一種才是真正意義上的response,也就是命令的響應。
這 裡我們可以看到,所有的行,首先經過sms的自動上報篩選,因為簡訊的AT處理通常比較麻煩,無論收發都單獨列出。這裡是因為要即時處理這條簡訊訊息(兩 行,標誌+pdu),而不能拆開處理。處理函數為onUnsolicited(由s_unsolHandler指向),我們等下介紹。
除開sms的特例,所有的line都要經過processLine,我們來看看這個流程:
processLine
|----no cmd--->handleUnsolicited //主動上報
|----isFinalResponseSuccess--->handleFinalResponse //成功,標準響應
|----isFinalResponseError--->handleFinalResponse //失敗,標準響應
|----get '>'--->send sms pdu //收到>符號,發送sms資料再繼續等待響應
|----switch s_type--->具體響應 //命令有具體的響應資訊需要對應分析
我 們這裡主要關注handleUnsolicited自動上報(會調用到前面smsUnsolicite也調用的onUnsolicite),以及 switch s_type具體響應資訊,另外具體響應需要handleFinalResponse這樣的標準響應來最終完成。
1. onUnsolicite(主動上報響應)
static void onUnsolicited (const char *s, const char *sms_pdu);
簡訊的AT設計真是麻煩的主,以致這個函數的第二個參數完全就是為它準備的。
response 的主要的解析過程,由at_tok.c中的函數完成,其實就是字串按塊解析,具體的解析方式由每條命令或上報資訊自行決定。這裡不再詳 述,onUnsolicited只解析出頭部(一般是+XXXX的形式),然後按類型決定下一步操作,操作為 RIL_onUnsolicitedResponse和RIL_requestTimedCallback兩種。
a)RIL_onUnsolicitedResponse:
將 unsolicited的資訊直接返回給上層。通過Parcel傳遞,將 RESPONSE_UNSOLICITED,unsolResponse(request號)寫入Parcel先,然後通過 s_unsolResponses數組,尋找到對應的responseFunction完成進一步的的解析,存入Parcel中。最終通過 sendResponse將其傳遞迴原進程。流程:
sendResponse-->sendResponseRaw-->blockingWrite-->write to s_fdCommand(前面建立起來的和上層架構的socket串連)
這些步驟之後有一些喚醒系統等其他動作。不再詳述。
b)RIL_requestTimedCallback:
通 過event機制(參考文章二)實現的timer機制,回調對應的內部處理函數。通過internalRequestTimedCallback將回調添 加到event迴圈,最終完成callback上掛的函數的回調。比如pollSIMState,onPDPContextListChanged等回 調, 不用返回上層, 內部處理就可以。
2. switch s_type(命令的具體響應)及handleFinalResponse(標準響應)
命 令的類型(s_type)在send command的時候設定(參考文章二),有NO_RESULT,NUMERIC,SINGLELINE,MULTILINE幾種,供不同的AT使用。比 如AT+CSQ是singleline, 返回at+csq=xx,xx,再加一行OK,比如一些設定命令,就是no_result, 只有一行OK或ERROR。
這幾個類型的解析都很相仿,通過一定的判斷(比較AT頭標記等),如果是對應的響應,就通過 addIntermediate掛到一個臨時結果sp_response->p_intermediates隊列裡。如果不是對應響應,那它其實應 該是穿插其中的自動上報,用onUnsolicite來處理。
具體響應,只起一個擷取響應資訊到臨時結果,等待具體分析的作用。無論有無具體響應,最終都得以標準響應handleFinalResponse來完成,也就是接受到OK,ERROR等標準response來結束,這是大多數AT命令的規範。
handleFinalResponse 會設定s_commandcond這一object,也就是at_send_command_full_nolock等待的對象。到這裡,響應的完整資訊 已經完全獲得,send command可以進一步處理返回的資訊了(臨時結果,以及標準返回的成功或失敗,都在sp_response中)。
pp_outResponse參數將sp_response返回給調用at_send_command_full_nolock的函數。
繼續我們在文章二的分析的話,這個函數其實是requestDial,不過requestDial忽略了響應,所以我們另外看個例子,如requestSignalStrength,命令其實就是前面提到的at+csq:
可以看到確實是通過at_send_command_singleline來進行的操作,response在p_response中。
p_response如果返回失敗(也就是標準響應的ERROR等造成),則通過RIL_onRequestComplete發送返回資料給上層,結束命令。
如果成功,則進一步分析p_response->p_intermediates, 同樣是通過at_tok.c裡的函數進行分析。並同樣將結果通過RIL_onRequestComplete返回。
RIL_onRequestComplete:
RIL_onRequestComplete和RIL_onUnsolicitedResponse很相仿,功能也一致。
通 過Parcel來傳遞迴上層,同樣是先寫入RESPONSE_SOLICITED(區別於 RESPONSE_UNSOLICITED),pRI->token(上層傳下的request號),錯誤碼(send command的錯誤,不是AT響應)。如果有AT響應,通過訪問pRI->pCI->responseFunction來完成具體 response的解析,並寫入Parcel。
然後通過同樣的途徑:
sendResponse-->sendResponseRaw-->blockingWrite-->write to s_fdCommand
完成最終的響應傳遞。
到這裡,我們分析了自動上報與命令響應,其實response部分,也就告一段落了。