專注電子技術(shù)學(xué)習(xí)與研究
當(dāng)前位置:單片機(jī)教程網(wǎng) >> MCU設(shè)計(jì)實(shí)例 >> 瀏覽文章

Modbus協(xié)議完全資料與程序解析

作者:劉溫電   來(lái)源:本站原創(chuàng)   點(diǎn)擊數(shù):  更新時(shí)間:2013年11月25日   【字體:

 

1簡(jiǎn)述,modbus是一種工業(yè)用的多設(shè)備之間的主從通信協(xié)議。只要兩臺(tái)設(shè)備之間,是采用modbus協(xié)議的主從關(guān)系,并連接到相同網(wǎng)絡(luò),即可互相通信。因?yàn)镸odbus只是協(xié)議,而且只規(guī)定了數(shù)據(jù)幀,底層連接,可以是232,485或者以太網(wǎng)。設(shè)備一般采用232和485進(jìn)行通信,因?yàn)槌杀镜。?dāng)然要是考慮遠(yuǎn)距離傳輸和多賣錢的話,也會(huì)采用以太網(wǎng),不過(guò)應(yīng)該就會(huì)相應(yīng)復(fù)雜一些了。
2模式,modbus有兩種模式,一種叫RTU模式,另一種叫acsii模式,RTU模式是純二進(jìn)制的,而acsii模式,一個(gè)信息中的每8位字節(jié)作為2個(gè)ascii字符傳輸?shù)模@種模式的主要優(yōu)點(diǎn)時(shí)允許字符之間的時(shí)間間隔長(zhǎng)達(dá)1秒,也不會(huì)出現(xiàn)錯(cuò)誤。而較acsii模式,RTU模式的優(yōu)點(diǎn)是用最少的字節(jié),表達(dá)更多的內(nèi)容。但同時(shí)也要求設(shè)備必須連續(xù)傳輸。
3通訊,modbus屬于主從通訊,可以是一主一從或者一主多從。通訊的方式為主機(jī)向從機(jī)發(fā)送命令(或者叫請(qǐng)求)從機(jī)向主機(jī)發(fā)送響應(yīng)。主機(jī)不發(fā)送,從機(jī)不返回,一發(fā),一收,不發(fā)不收。而且一個(gè)時(shí)間,只有一個(gè)機(jī)器發(fā)送請(qǐng)求或者響應(yīng),否則的話,則會(huì)出錯(cuò)。
4信息幀,由于項(xiàng)目上沒(méi)有涉及到acsii模式,所以本文只討論RTU模式,不討論acsii模式,以后如果要是用的上,肯定會(huì)繼續(xù)討論。用不上,就不討論了。RTU幀,開(kāi)始時(shí),必須要有3.5個(gè)靜止的時(shí)間,也就是時(shí)間間隔,用來(lái)區(qū)分上一幀和下一幀,如果沒(méi)有時(shí)間間隔的話,則會(huì)分辨不出哪里是幀開(kāi)始,哪里是幀結(jié)束了。3.5個(gè)時(shí)間間隔依據(jù)波特率不同而不同。同樣,結(jié)束時(shí)也需要時(shí)間。除了時(shí)間以外,還有地址,功能碼,數(shù)據(jù),crc校驗(yàn)四個(gè)部分,每個(gè)部分的字節(jié)數(shù)不同,地址功能碼各1個(gè)字節(jié),crc是2個(gè)字節(jié)其完整表達(dá)如下:
      
開(kāi)始
地址
功能
數(shù)據(jù)
校驗(yàn)
結(jié)束
3.5t 
1字節(jié) 8b
1字節(jié) 8b
n字節(jié) n*8b
2字節(jié)16b
3.5t 
 
4.1、地址:主要用于區(qū)分從機(jī),在下位機(jī)程序中,的宏定義中設(shè)置不同的從機(jī)地址。
       #define Modbus_addr 0x01
設(shè)備響應(yīng)時(shí),第一位也是本機(jī)地址。地址的范圍是從0-247,地址0為廣播地址,所有機(jī)器均可以識(shí)別。
       4.2、功能碼:表示主機(jī)要命令這個(gè)設(shè)備的什么功能,執(zhí)行什么程序。我看了一下正規(guī)的modbus的功能碼多達(dá)24個(gè),不同廠家生產(chǎn)的不同型號(hào)的設(shè)備,可能會(huì)支持不同的功能碼,所以買之前需要注意一下。具體功能如下:
      
01 讀線圈狀態(tài)            02 讀輸入狀態(tài)            03 讀保持寄存器         04 讀輸入寄存器         05 強(qiáng)制單個(gè)線圈
06 預(yù)置單個(gè)寄存器     07 讀不正常狀態(tài)         08 診斷                    09 程序484                 10 查詢484
11 通訊事件控制         12 通訊事件記錄         13 程序控制器            14 查詢控制器            15 強(qiáng)制多個(gè)寄存器
16 預(yù)置多個(gè)寄存器     17 報(bào)告從機(jī)id            18 程序884/M84         19 通訊鏈路復(fù)位        20 讀通用參考值
21 寫通用參考值         22 Mask Write 4X Register           23 Read/Write 4X Registers          24 Read FIFO 隊(duì)列
 
雖然看著功能很多,但實(shí)際上有用的,只有01 02 03 04 05 06 15 和16功能碼。
       4.3、數(shù)據(jù)區(qū),根據(jù)功能碼的不同數(shù)據(jù)的長(zhǎng)度是不同的。
       4.4、crc校驗(yàn) 包含兩個(gè)字節(jié),發(fā)送端發(fā)送時(shí),一幀的所有數(shù)據(jù)統(tǒng)一計(jì)算出一個(gè)crc校驗(yàn)碼,然后加在一幀的最后兩位中,然后等到發(fā)送到接收端時(shí)接收端重新計(jì)算一次除最后兩位的一幀所有數(shù)據(jù),然后根據(jù)兩個(gè)數(shù)據(jù)的對(duì)比,來(lái)判斷接收到的數(shù)據(jù)是否正確。
       5、程序,以下位機(jī)為程序?qū)ο螅饕褂胏語(yǔ)言編寫,首先,先從變量入手,既然modbus接受以幀為單位,所以就要設(shè)置兩個(gè)緩沖區(qū),用來(lái)接收數(shù)據(jù),我們這里使用數(shù)組來(lái)存儲(chǔ)接收來(lái)的數(shù)據(jù)Modbus_send_buf[Modbus_max_send_buf];//數(shù)據(jù)發(fā)送緩沖 和 Modbus_recevie_buf[Modbus_max_recevie_buf];//數(shù)據(jù)接收緩沖 ,其中Modbus_max_send_buf,和Modbus_max_recevie_buf ,為宏定義,這樣可以方便的修改一幀最大的存儲(chǔ)數(shù)據(jù)。有了發(fā)送接收緩沖,就可以寫中斷函數(shù)了,進(jìn)入中斷后,首先做一些必要的工作,清ES ,判斷IR,清IR,做完后,就可以開(kāi)始接收數(shù)據(jù)了,但有個(gè)問(wèn)題?如果設(shè)備處于空閑狀態(tài),那么接收數(shù)據(jù)后按命令執(zhí)行,但如果當(dāng)設(shè)備正在執(zhí)行指令的時(shí)候,則不應(yīng)該再繼續(xù)的接收指令,那樣的話,會(huì)讓程序進(jìn)入混亂狀態(tài)。所以要在基礎(chǔ)工作做完后,增加一個(gè)判斷,來(lái)確定設(shè)備的忙閑。if((Modbus_cmd_flag == 0) && (Modbus_exe_flag == 0)),判斷完以后就可以繼續(xù)下面的工作了。如果通訊中包含奇偶校驗(yàn)的話,那么則判斷奇偶校驗(yàn)。下面就是接收數(shù)據(jù)。Modbus_recevie_buf[Modbus_recevie_count] = SBUF; ,將接收來(lái)的數(shù)據(jù)存入數(shù)組并記錄存入的數(shù)據(jù)個(gè)數(shù)Modbus_recevie_count,由于modbus是通過(guò)時(shí)間來(lái)判斷一幀的結(jié)束的,所以在程序中,必須要有一個(gè)定時(shí)器函數(shù),這個(gè)定時(shí)器用來(lái)判斷程序是正在接受,還是已經(jīng)接受完成了。所以中斷的最后所做的是計(jì)數(shù)器自加Modbus_recevie_count++;,定時(shí)器清0 Modbus_timeout_cnt = 0;   ,將設(shè)備狀態(tài)轉(zhuǎn)入接收狀態(tài)Modbus_recevie_flag = 1;。此時(shí),串口中斷的工作就完成了。
          下面開(kāi)始分析定時(shí)器,定時(shí)器的目的其實(shí)就1個(gè),判斷一幀是否接收完畢,如果完畢,則進(jìn)入下一步。在定時(shí)器中斷函數(shù)中,首先要對(duì)定時(shí)器值進(jìn)行初始化,這個(gè)就不多說(shuō)了,然后是判斷程序是否處于接受狀態(tài)if(Modbus_recevie_flag == 1),這個(gè)狀態(tài)只有在串口中斷函數(shù)中才會(huì)被置位,其他的情況不會(huì)被置位。若程序不是接收狀態(tài),則直接跳出定時(shí)器中斷,若程序處于接收狀態(tài),則定時(shí)計(jì)數(shù)自加Modbus_timeout_cnt++;,自加后進(jìn)入判斷if(Modbus_timeout_cnt >= Modbus_max_timeout_cnt),判斷的值即為modbus接收一幀傳輸完成所需要的時(shí)間間隔。至于是多少時(shí)間,可以通過(guò)修改Modbus_max_timeout_cnt來(lái)確定?梢詫⒍〞r(shí)器終端設(shè)置為1ms1次,在9600的情況下將超時(shí)時(shí)間設(shè)為4,#define Modbus_max_timeout_cnt   4,這樣如果串口中斷不在接收數(shù)據(jù)時(shí),定時(shí)計(jì)數(shù)將不會(huì)清0,當(dāng)?shù)竭_(dá)設(shè)定的超時(shí)時(shí)間后即判斷接收結(jié)束,轉(zhuǎn)向命令解析狀態(tài)。
       接收來(lái)的數(shù)據(jù)可以經(jīng)過(guò)一個(gè)函數(shù)來(lái)執(zhí)行,同時(shí)也可以經(jīng)過(guò)兩個(gè)函數(shù),解析與執(zhí)行兩步來(lái)分別執(zhí)行。我喜歡后者,因?yàn)檫@樣可以把解析的過(guò)程和執(zhí)行的過(guò)程分開(kāi)來(lái)寫。程序顯得更加清晰與明朗。
       在主函數(shù)中就執(zhí)行1個(gè)函數(shù),
while(1)
      {
             Modbus_proc();
       }
這個(gè)函數(shù)是經(jīng)過(guò)打包的兩個(gè)函數(shù),進(jìn)入這個(gè)函數(shù)
void Modbus_proc()
{
           Modbus_cmd();
           Modbus_exe();   
}
可以看到,程序分為cmd解析,exe執(zhí)行。
Cmd   命令解析函數(shù)
有這么幾個(gè)問(wèn)題是需要判斷的,命令解析狀態(tài),接收來(lái)的數(shù)據(jù)個(gè)數(shù),crc,地址,這幾個(gè)問(wèn)題是命令解析時(shí)需要注意的,順序可以稍做變化。但最好是這個(gè)順序。
首先判斷程序是否處于命令解析狀態(tài)if(Modbus_cmd_flag == 1)。命令解析狀態(tài)標(biāo)志只有在超時(shí)后置位,其他情況下不置位。之后是判斷接收數(shù)據(jù)是否大于4字節(jié),if(Modbus_recevie_count > 4)。當(dāng)程序接收數(shù)據(jù)小于4字節(jié)則說(shuō)明接收發(fā)生錯(cuò)誤,拋棄它。下一步則是判斷crc校驗(yàn),由于crc在一幀的最后兩位,所以crc應(yīng)該取緩沖的最后兩位
modbus_crc_h=Modbus_recevie_buf[Modbus_recevie_count-2];                    
modbus_crc_l = Modbus_recevie_buf[Modbus_recevie_count-1];
然后將取來(lái)的數(shù)據(jù)合并成一個(gè)16位數(shù)據(jù),得到接收的crc
modbus_crc = ((unsigned int)(modbus_crc_h) << 8) | modbus_crc_l;
重新計(jì)算1幀的crc,得到自己的crc
modbus_crc_b = crc16(Modbus_recevie_buf,Modbus_recevie_count - 2);
最后進(jìn)行對(duì)比,將自己算的crc和接收的crc進(jìn)行比較,來(lái)判斷接收的數(shù)據(jù)是否正確。
if( modbus_crc_b == modbus_crc )
在crc判斷正確后,就可以判斷地址了
if(Modbus_recevie_buf[0] == Modbus_addr)      // Modbus_addr為一個(gè)宏定義的本機(jī)地址,若多機(jī)可以在此處修改。
當(dāng)?shù)刂罚琧rc,等全判斷正確以后,就可以判斷最重要的功能碼了。由于功能碼很多,所以1可以用宏定義來(lái)定義功能碼增加程序的可讀性,2可以利用switch來(lái)命令的模式
 
#define Modbus_read_coil 0x01 //功能碼01 讀可讀寫數(shù)字量寄存器(線圈狀態(tài)):
switch (Modbus_recevie_buf[1])
{
case Modbus_read_coil:
Modbus_mode = Modbus_read_coil;
break;
       ……
     default:                //非法命令準(zhǔn)備報(bào)異常                               
    return ;
    break;
}
       Modbus_exe_flag = 1;
       解析后,將執(zhí)行標(biāo)志置位即可。
Exe 執(zhí)行函數(shù),
執(zhí)行函數(shù)在解析函數(shù)后面,而不是在里面,所以,若沒(méi)有解析,照樣可以進(jìn)入執(zhí)行函數(shù),但由于執(zhí)行函數(shù)中有判斷執(zhí)行標(biāo)志位if( modbus_crc_b == modbus_crc ),所以若標(biāo)志為0,則直接退出函數(shù)。若標(biāo)志為1,則執(zhí)行Modbus_mode中對(duì)應(yīng)的函數(shù)函數(shù)中依然用switch來(lái)選擇具體功能函數(shù)
 
switch(Modbus_mode)      //通過(guò)判斷模式來(lái)進(jìn)行對(duì)響應(yīng)的發(fā)送
{
case Modbus_read_coil:
read_coil_proc();   
break;
       ……
default:
return;
break;
}
這樣的做的話,就可以吧解析函數(shù),執(zhí)行函數(shù)和具體的實(shí)施函數(shù)分開(kāi)來(lái)弄,層次多多少少要清晰一些
下面就是針對(duì)01,02,03,04,05,06,15,16幾個(gè)功能碼的執(zhí)行及返回進(jìn)行說(shuō)明
在說(shuō)明各功能函數(shù)之前,先說(shuō)說(shuō)響應(yīng)。
上面說(shuō)的那兩個(gè)函數(shù)只不過(guò)是對(duì)一幀的外圍進(jìn)行解析與判斷,至于具體的參數(shù),還需要功能函數(shù)去解析與返回,功能函數(shù)要做的事情有3個(gè),1個(gè)是參數(shù)的解析,2是執(zhí)行,3是返回響應(yīng)。
先說(shuō)響應(yīng),響應(yīng)是有特點(diǎn)的,第一個(gè)字節(jié)肯定是自己的本機(jī)地址,第二個(gè)字節(jié)肯定是功能碼,最后兩個(gè)字節(jié)肯定是crc校驗(yàn),所以說(shuō),在發(fā)送緩沖中,基本上4個(gè)字節(jié)已經(jīng)定死了
 
Modbus_send_buf[0] = Modbus_addr;        
Modbus_send_buf[1] = Modbus_read_input_reg;  //相應(yīng)的功能碼,每個(gè)功能寒暑都不一樣
再經(jīng)過(guò)執(zhí)行函數(shù)最后算crc
modbus_crc = crc16(Modbus_send_buf,temp);    //計(jì)算發(fā)送crc數(shù)據(jù)
Modbus_send_buf[temp] = modbus_crc >> 8;     //計(jì)算
temp++;                                                         
Modbus_send_buf[temp] = modbus_crc & 0xff;   //return num 高位
      
5.1 01 讀線圈狀態(tài)
#define Modbus_read_coil 0x01 
       其實(shí)表面上挺難理解的,啥線圈啥的,但你仔細(xì)看看就可以了解,就是讀輸出數(shù)字量,如果你寫下位機(jī)的話,其實(shí)就是控制讀取輸出io,說(shuō)白了,就是把目前的io輸出狀態(tài)返回給主機(jī)。這些io連接的可能是繼電器,也可能是一些開(kāi)關(guān)之類的東西,也就是些數(shù)字信號(hào)。讀數(shù)字輸出信號(hào)。
計(jì)算機(jī)發(fā)送命令:[設(shè)備地址] [命令號(hào)01] [起始寄存器地址高8位] [低8位] [讀取的寄存器數(shù)高8位] [低8位]
設(shè)備響應(yīng):[設(shè)備地址] [命令號(hào)01] [返回的字節(jié)個(gè)數(shù)][數(shù)據(jù)1][數(shù)據(jù)2]...[數(shù)據(jù)n][CRC校驗(yàn)的低8位] [CRC校驗(yàn)的高8位]
簡(jiǎn)單的說(shuō)就是返回所有的輸出io的值,放在一個(gè)或者幾個(gè)字節(jié)里,可以用判斷的方法來(lái)實(shí)現(xiàn),當(dāng)然,也可以用與或的方式實(shí)現(xiàn)。
    if(P1_0 == 1)
    {
        temp |= (1<<8);
    }
    else
    {
        temp &= (1<<8);
    }
temp的值放入第四個(gè)緩沖區(qū),當(dāng)然這根據(jù)設(shè)備的io口,編程時(shí)就已經(jīng)確定了的。接下來(lái)就可以進(jìn)行crc計(jì)算了。最后發(fā)送即可。
Modbus_send_buf[3] = temp;
modbus_crc = crc16(Modbus_send_buf,4);
Modbus_send_buf[4] = modbus_crc >> 8;   
Modbus_send_buf[5] = modbus_crc & 0xff;      //return num 高位 
 
 
5.2 02 讀只可讀數(shù)字量寄存器(輸入狀態(tài))
基本上和01意思差不多,只不過(guò)這個(gè)功能碼返回的數(shù)據(jù)是輸入io的數(shù)據(jù),和01的區(qū)別是01可讀可改,而02只可讀不可改。也就是輸入的狀態(tài)。數(shù)據(jù)不可由設(shè)備本身控制。程序方面和01程序一樣。
 
5.3 03讀可讀寫模擬量寄存器(保持寄存器)
說(shuō)簡(jiǎn)單點(diǎn)就是讀da,da屬于模擬量,也可以輸出,但是以模擬量的方式來(lái)進(jìn)行傳輸?shù)?/div>
計(jì)算機(jī)發(fā)送命令:[設(shè)備地址] [命令號(hào)03] [起始寄存器地址高8位] [低8位] [讀取的寄存器數(shù)高8位] [低8位] [CRC校驗(yàn)的低8位] [CRC校驗(yàn)的高8位]
設(shè)備響應(yīng):[設(shè)備地址] [命令號(hào)03] [返回的字節(jié)個(gè)數(shù)][數(shù)據(jù)1][數(shù)據(jù)2]...[數(shù)據(jù)n][CRC校驗(yàn)的低8位] [CRC校驗(yàn)的高8位]
其中返回字節(jié)個(gè)數(shù),為讀取寄存器數(shù)乘2
寫程序時(shí),首先要注意數(shù)據(jù)個(gè)數(shù),temp = Modbus_recevie_buf[5];一般寄存器個(gè)數(shù)不會(huì)超過(guò)255,個(gè)數(shù)取讀取寄存器個(gè)數(shù)的低八位即可。返回即乘2,temp = temp << 1;,下面要做的就是一個(gè)循環(huán)for(i = 0;i < temp ; i += 2),把需要的數(shù)據(jù)放入發(fā)送數(shù)組。其內(nèi)容是
Modbus_send_buf[i+3]=(data_v&0xff00)>>8;
Modbus_send_buf[i+4]=data_v&0x0ff;
由于幀的前面3個(gè)是地址,功能碼,和返回字節(jié)個(gè)數(shù),所以循環(huán)從第四個(gè)數(shù)據(jù)開(kāi)始存放。data_v為讀取的數(shù)據(jù),在程序中還需要其他語(yǔ)句配合。比如:data_v = updateValue();
    循環(huán)后就可以進(jìn)入crc校驗(yàn)了可以利用返回字節(jié)數(shù)來(lái)確定crc的校驗(yàn)個(gè)數(shù)temp = temp + 3;,最后計(jì)算發(fā)送字節(jié)的個(gè)數(shù)
    send_cnt = Modbus_recevie_buf[5]*2 + 5 ; //數(shù)據(jù)發(fā)送個(gè)數(shù)   數(shù)據(jù)+地址+命令+返回?cái)?shù)據(jù)個(gè)數(shù)+crc低+crc高
       最后將數(shù)據(jù)發(fā)送出去即可。
   
    5.4 04讀只可讀模擬量寄存器(輸入寄存器)
       和03的區(qū)別是04就是讀ad,ad輸入輸入模擬兩,只能讀,不能改,同樣也是以模擬兩的方式來(lái)進(jìn)行傳輸?shù)。其程?span>       與03類似
      
       5.5 05寫數(shù)字量(線圈狀態(tài))
       05則是修改io口輸出狀態(tài),數(shù)字量輸出。
計(jì)算機(jī)發(fā)送命令:[設(shè)備地址] [命令號(hào)05] [需下置的寄存器地址高8位] [低8位] [下置的數(shù)據(jù)高8位] [低8位] [CRC校驗(yàn)的低8位] [CRC校驗(yàn)的高8位]
設(shè)備響應(yīng):若執(zhí)行成功,則原樣返回
寫程序時(shí),首先確定需要修改的io口,然后根據(jù)0xff00或0x0000來(lái)置位或清零該數(shù)據(jù)位。執(zhí)行完成后,將接收到的數(shù)據(jù)重新發(fā)送即可 Uart0_senddata(Modbus_recevie_buf,8);
 
5.6 06寫單個(gè)模擬量寄存器(保持寄存器)
06為修改設(shè)備da數(shù)據(jù),模擬量傳輸數(shù)據(jù)。
計(jì)算機(jī)發(fā)送命令:[設(shè)備地址] [命令號(hào)06] [需下置的寄存器地址高8位] [低8位] [下置的數(shù)據(jù)高8位] [低8位] [CRC校驗(yàn)的低8位] [CRC校驗(yàn)的高8位]
設(shè)備響應(yīng):若執(zhí)行成功,原樣返回即可
 
5.7 16主機(jī)設(shè)置寄存器
簡(jiǎn)單的說(shuō),就是一次設(shè)置多個(gè)da,以一個(gè)偏移量為準(zhǔn),一次設(shè)置多個(gè)輸出模擬里量
計(jì)算機(jī)發(fā)送命令:[設(shè)備地址] [命令號(hào)10] [開(kāi)始地址高8位] [低8位] [寄存器個(gè)數(shù)高8位] [低8位] [第一個(gè)寄存器數(shù)據(jù)高][第一個(gè)寄存器數(shù)據(jù)低][第二個(gè)寄存器數(shù)據(jù)高][第二個(gè)寄存器數(shù)據(jù)低]……[CRC校驗(yàn)的低8位] [CRC校驗(yàn)的高8位]
命令響應(yīng):功能碼[0x10],寄存器起始地址高字節(jié),低字節(jié),要寫的寄存器數(shù)量的高字節(jié),低字節(jié),CRC校驗(yàn)低字節(jié),高字節(jié)
在程序中,首先要獲取寄存器個(gè)數(shù)
num = Modbus_recevie_buf[6] - 2;
然后進(jìn)入循環(huán),一次把寄存器數(shù)據(jù)提取出來(lái)for(i = 0; i < num; i = i + 2)
在循環(huán)的內(nèi)部提取數(shù)據(jù)temp = (((unsigned int)(Modbus_recevie_buf[i+7])<<8)|(Modbus_recevie_buf[i+8]));
 
以上就是我在項(xiàng)目中涉及到的一點(diǎn)modbus的通訊的下位機(jī)程序,不全,但總體的思路,接收數(shù)據(jù)并解析,解析后提取數(shù)據(jù)在設(shè)備上加載或采集,然后再按照響應(yīng)的方式發(fā)送回去。
下回改進(jìn)的方向,1,增加功能碼2,增加宏定義及編譯定義,3增加單片主機(jī)的程序,和pc主從機(jī)的程序。4,增加ascii的程序,和rtu同時(shí)設(shè)置。Pc機(jī)程序,采用c#號(hào)編寫。

 

完整的程序請(qǐng)參考:http://www.torrancerestoration.com/bbs/dpj-23230-1.html

 

相關(guān)文章