這一段時(shí)間做的項(xiàng)目自動(dòng)售貨機(jī)和無(wú)線(xiàn)終端設(shè)備的通訊,都是通過(guò)串口進(jìn)行對(duì)接和通訊。在Android中進(jìn)行串口通信方式可以用Google官方提供的demo代碼(android-serialport-api),也可以通過(guò)NDK的方式使用C/C++進(jìn)行實(shí)現(xiàn)(Android串口助手,C++實(shí)現(xiàn)),其底層原理都是通過(guò)調(diào)用open函數(shù)打開(kāi)設(shè)備文件來(lái)進(jìn)行讀寫(xiě)操作。對(duì)串口接觸下來(lái),發(fā)現(xiàn)真的可以做很多有意思的東西,很多硬件設(shè)備都可以通過(guò)串口進(jìn)行通訊,比如:打印機(jī)、ATM吐卡機(jī)、IC/ID卡讀卡等,以及物聯(lián)網(wǎng)相關(guān)的設(shè)備。所以有必有對(duì)相關(guān)知識(shí)進(jìn)行下梳理和總結(jié)。
串口簡(jiǎn)介
串口通信(Serial Communications)的概念非常簡(jiǎn)單,串口按位(bit)發(fā)送和接收字節(jié)。串口可以在使用一根線(xiàn)(Tx)發(fā)送數(shù)據(jù)的同時(shí)用另一根線(xiàn)(Rx)接收數(shù)據(jù)。
串口參數(shù)
波特率:串口傳輸速率,用來(lái)衡量數(shù)據(jù)傳輸?shù)目炻?,即單位時(shí)間內(nèi)載波參數(shù)變化的次數(shù),如每秒鐘傳送240個(gè)字符,而每個(gè)字符格式包含10位(1個(gè)起始位,1個(gè)停止位,8個(gè)數(shù)據(jù)位),這時(shí)的波特率為240Bd,比特率為10位*240個(gè)/秒=2400bps。波特率與距離成反比,波特率越大傳輸距離相應(yīng)的就越短。
數(shù)據(jù)位:這是衡量通信中實(shí)際數(shù)據(jù)位的參數(shù)。當(dāng)計(jì)算機(jī)發(fā)送一個(gè)信息包,實(shí)際的數(shù)據(jù)往往不會(huì)是8位的,標(biāo)準(zhǔn)的值是6、7和8位。如何設(shè)置取決于你想傳送的信息。
停止位:用于表示單個(gè)包的最后一位。典型的值為1,1.5和2位。由于數(shù)據(jù)是在傳輸線(xiàn)上定時(shí)的,并且每一個(gè)設(shè)備有其自己的時(shí)鐘,很可能在通信中兩臺(tái)設(shè)備間出現(xiàn)了小小的不同步。因此停止位不僅僅是表示傳輸?shù)慕Y(jié)束,并且提供計(jì)算機(jī)校正時(shí)鐘同步的機(jī)會(huì)。適用于停止位的位數(shù)越多,不同時(shí)鐘同步的容忍程度越大,但是數(shù)據(jù)傳輸率同時(shí)也越慢。
校驗(yàn)位:在串口通信中一種簡(jiǎn)單的檢錯(cuò)方式。有四種檢錯(cuò)方式:偶、奇、高和低。當(dāng)然沒(méi)有校驗(yàn)位也是可以的。對(duì)于偶和奇校驗(yàn)的情況,串口會(huì)設(shè)置校驗(yàn)位(數(shù)據(jù)位后面的一位),用一個(gè)值確保傳輸?shù)臄?shù)據(jù)有偶個(gè)或者奇?zhèn)€邏輯高位。
串口地址
如下表不同操作系統(tǒng)的串口地址,Android是基于Linux的所以一般情況下使用Android系統(tǒng)的設(shè)備串口地址為/dev/ttyS0...
| System | Port 1 | Port 2 |
|---|---|---|
| IRIX? | /dev/ttyf1 | /dev/ttyf2 |
| HP-UX | /dev/tty1p0 | /dev/tty2p0 |
| Solaris?/SunOS? | /dev/ttya | /dev/ttyb |
| Linux? | /dev/ttyS0 | /dev/ttyS1 |
| Digital UNIX? | /dev/tty01 | /dev/tty02 |
Android串口實(shí)現(xiàn)
在Android上使用串口比較快速的方式就是直接套用google官方的串口demo代碼(android-serialport-api),基本上能夠應(yīng)付很多在Android設(shè)備使用串口的場(chǎng)景。比如簡(jiǎn)單的讀卡號(hào)。
但是問(wèn)題來(lái)了!
在收發(fā)數(shù)據(jù)頻率很快的情況下,實(shí)際測(cè)試這種方式接收數(shù)據(jù)會(huì)有延遲。比如:發(fā)送一個(gè)命令之后,設(shè)備會(huì)同時(shí)響應(yīng)兩條命令,一條是結(jié)果一條是校驗(yàn)且兩條命令間隔時(shí)間僅1ms,按理兩條命令會(huì)幾乎同時(shí)收到,但是實(shí)際使用該方式會(huì)出現(xiàn)10ms的延遲。所以只能著手優(yōu)化,嘗試使用C/C++的方式進(jìn)行串口數(shù)據(jù)的讀寫(xiě)。
一番查閱下來(lái),使用C/C++實(shí)現(xiàn)其實(shí)和上面的demo差別不大,同樣是那幾個(gè)步驟,設(shè)置串口參數(shù),通過(guò)調(diào)用open方法開(kāi)啟串口,再進(jìn)行數(shù)據(jù)的讀寫(xiě)操作。出現(xiàn)數(shù)據(jù)讀取延遲很可能的原因,就是因?yàn)楣俜絛emo是通過(guò)Java層的文件流(FileInputStream,F(xiàn)ileOutputStream)進(jìn)行讀寫(xiě)操作引起的。如果有大神懂這塊的可以說(shuō)明這種方式導(dǎo)致延遲的原因。
關(guān)于使用C、C++在Android上實(shí)現(xiàn)串口通訊的源代碼有很多,沒(méi)有實(shí)際做過(guò)C/C++開(kāi)發(fā),但是也容易看懂。
設(shè)置串口波特率、數(shù)據(jù)位、停止位、校驗(yàn)位主要操作的就是termios 結(jié)構(gòu)體,對(duì)應(yīng)的頭文件是termios.h。
比如設(shè)置波特率代碼:
int SerialPort::setSpeed(int fd, int speed) {
speed_t b_speed;
struct termios cfg;
b_speed = getBaudrate(speed);
if (tcgetattr(fd, &cfg)) {
LOGE("tcgetattr invocation method failed!");
close(fd);
return FALSE;
}
?
cfmakeraw(&cfg);
cfsetispeed(&cfg, b_speed);
cfsetospeed(&cfg, b_speed);
?
if (tcsetattr(fd, TCSANOW, &cfg)) {
LOGE("tcsetattr invocation method failed!");
close(fd);
return FALSE;
}
return TRUE;
}
打開(kāi)串口就是簡(jiǎn)單的調(diào)用open函數(shù),設(shè)置相關(guān)讀寫(xiě)參數(shù),這個(gè)和官方推薦的demo一致,代碼如下:
LOGD("Open device!");
isClose = false;
fd = open(path, O_RDWR);
if (fd < 0) {
LOGE("Error to read %s port file!", path);
return FALSE;
}
?
if (!setSpeed(fd, config.baudrate)) {
LOGE("Set Speed Error!");
return FALSE;
}
if (!setParity(fd, config.databits, config.stopbits, config.parity)) {
LOGE("Set Parity Error!");
return FALSE;
}
LOGD("Open Success!");
return TRUE;
}
串口數(shù)據(jù)讀取涉及兩個(gè)函數(shù) select和read ,函數(shù)相關(guān)的含義暫且沒(méi)去深究,屬于C/C++范湊了,讀取數(shù)據(jù)代碼如下:
int SerialPort::readData(BYTE *data, int size) {
?
int ret, retval;
fd_set rfds;
ret = 0;
?
if (isClose) return 0;
for (int i = 0; i < size; i++) {
data[i] = static_cast<char>(0xFF);
}
FD_ZERO(&rfds); //清空集合
FD_SET(fd, &rfds); //把要檢測(cè)的句柄fd加入到集合里
// TODO Async operation. Thread blocking.
if (FD_ISSET(fd, &rfds)) {
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
retval = select(fd + 1, &rfds, NULL, NULL, NULL);
if (retval == -1) {
LOGE("Select error!");
} else if (retval) {
LOGD("This device has data!");
ret = static_cast<int>(read(fd, data, static_cast<size_t>(size)));
} else {
LOGE("Select timeout!");
}
}
if (isClose) close(fd);
return ret;
}
串口寫(xiě)數(shù)據(jù)就是調(diào)用write函數(shù)了,代碼如下:
int SerialPort::writeData(BYTE *data, int len) {
int result;
result = static_cast<int>(write(fd, data, static_cast<size_t>(len)));
return TRUE;
}
因?yàn)椴皇煜/C++,所以就參考網(wǎng)上相關(guān)源代碼,依葫蘆畫(huà)瓢實(shí)現(xiàn)了一個(gè)基于C++的Android串口通訊庫(kù),并對(duì)相關(guān)串口控制做了優(yōu)化,詳細(xì)見(jiàn)gayhub,地址:https://github.com/freyskill/SerialPortHelper,歡迎star。
通過(guò)該庫(kù),完美解決串口數(shù)據(jù)讀取延遲的問(wèn)題。
阻塞與非阻塞
在項(xiàng)目初期使用google官方的串口demo代碼調(diào)試設(shè)備串口是否能正常通信的時(shí)候,遇到在串口讀數(shù)據(jù)的線(xiàn)程中會(huì)卡死在inputStream.read(buffer);這個(gè)時(shí)候就讓人疑惑了,不知道問(wèn)題是出在硬件還是在串口讀取上,在沒(méi)有了解串口相關(guān)知識(shí)前,希望的場(chǎng)景是讀數(shù)據(jù)的線(xiàn)程能夠不阻塞,一直輪詢(xún)讀取數(shù)據(jù)。
出現(xiàn)讀取數(shù)據(jù)線(xiàn)程卡死的情況是因?yàn)樵?fd = open(path_utf, O_RDWR | flags); 設(shè)置相關(guān)參數(shù),讀取默認(rèn)為阻塞模式,若在open操作中設(shè)置O_NONBLOCK則是非阻塞模式。在阻塞模式中,read沒(méi)有讀到數(shù)據(jù)會(huì)阻塞住,直到收到數(shù)據(jù);非阻塞模式read沒(méi)有讀到數(shù)據(jù)會(huì)返回-1不會(huì)阻塞。
修改open方法:
fd = open(path_utf, O_RDWR | flags | O_NONBLOCK | O_NOCTTY | O_NDELAY);
讀取線(xiàn)程就不會(huì)再出現(xiàn)卡死了,這個(gè)時(shí)候仍然接收不到串口設(shè)備反饋的數(shù)據(jù),就可以斷定是串口設(shè)備的問(wèn)題了。
關(guān)于串口文件打開(kāi)方式,可采用下面的文件打開(kāi)模式,具體說(shuō)明如下:
O_RDONLY:以只讀方式打開(kāi)文件
O_WRONLY:以只寫(xiě)方式打開(kāi)文件
O_RDWR:以讀寫(xiě)方式打開(kāi)文件
O_APPEND:寫(xiě)入數(shù)據(jù)時(shí)添加到文件末尾
O_CREATE:如果文件不存在則產(chǎn)生該文件,使用該標(biāo)志需要設(shè)置訪問(wèn)權(quán)限位mode_t
O_EXCL:指定該標(biāo)志,并且指定了O_CREATE標(biāo)志,如果打開(kāi)的文件存在則會(huì)產(chǎn)生一個(gè)錯(cuò)誤
O_TRUNC:如果文件存在并且成功以寫(xiě)或者只寫(xiě)方式打開(kāi),則清除文件所有內(nèi)容,使得文件長(zhǎng)度變?yōu)?
O_NOCTTY:如果打開(kāi)的是一個(gè)終端設(shè)備,這個(gè)程序不會(huì)成為對(duì)應(yīng)這個(gè)端口的控制終端,如果沒(méi)有該標(biāo)志,任何一個(gè)輸入,例如鍵盤(pán)中止信號(hào)等,都將影響進(jìn)程。
O_NONBLOCK:該標(biāo)志與早期使用的O_NDELAY標(biāo)志作用差不多。程序不關(guān)心DCD信號(hào)線(xiàn)的狀態(tài),如果指定該標(biāo)志,進(jìn)程將一直在休眠狀態(tài),直到DCD信號(hào)線(xiàn)為0。
實(shí)際應(yīng)用中,都會(huì)選擇阻塞模式,這樣更節(jié)省資源。但是如果希望在一個(gè)線(xiàn)程中同時(shí)進(jìn)行讀寫(xiě)操作,沒(méi)數(shù)據(jù)反饋時(shí),線(xiàn)程就會(huì)阻塞等待,就無(wú)法進(jìn)行寫(xiě)數(shù)據(jù)了。
串口數(shù)據(jù)校驗(yàn)方式
一般情況下串口通訊協(xié)議都會(huì)在數(shù)據(jù)幀或者說(shuō)命令格式里定義一個(gè)校驗(yàn)方式,常用的有異或校驗(yàn)、和校驗(yàn)、CRC校驗(yàn)和LRC校驗(yàn)。
注意:這里說(shuō)的校驗(yàn)和上面說(shuō)的校驗(yàn)位是不同的,校驗(yàn)位針對(duì)的是單個(gè)字節(jié),校驗(yàn)類(lèi)型針對(duì)的是單個(gè)數(shù)據(jù)幀。
校驗(yàn)方式一般放在命令最后,可以是一個(gè)byte,也可以是兩個(gè)byte或者其他,具體看協(xié)議設(shè)計(jì)。
比如命令格式如下,采用和校驗(yàn):
| addr | command | data_length | data1 | data2 | datan | checksum |
|---|---|---|---|---|---|---|
| 0x01 | 0x52 | 0x05 | 0x11 | 0xBA | ... | 8E |
其中,獲取校驗(yàn)碼(checksum)就是將命令中的數(shù)據(jù)進(jìn)行相加生成,Checksum=256-(data1+data2+datan)算出校驗(yàn)碼為:8E。具體計(jì)算方式就是通過(guò)將十六進(jìn)制進(jìn)行相加算出校驗(yàn)碼的十進(jìn)制字符,詳細(xì)代碼如下:
/**
* 獲取校驗(yàn)碼(計(jì)算方式如下:cs= 256-(data1+data2+data3+data4+datan))
*/
public static String getCheckSum(String data){
Integer in = Integer.valueOf(makeChecksum(data),16);
String st = Integer.toHexString(256 -in).toUpperCase();
st = String.format("%2s",st);
return st.replaceAll(" ","0");
}
十六進(jìn)制進(jìn)行相加代碼:
/**
* 生成校驗(yàn)碼,十六進(jìn)制相加
* @param data
* @return
*/
public static String makeChecksum(String data) {
if (data == null || data.equals("")) {
return "00";
}
int iTotal = 0;
int iLen = data.length();
int iNum = 0;
?
while (iNum < iLen){
String s = data.substring(iNum, iNum + 2);
System.out.println(s);
iTotal += Integer.parseInt(s, 16);
iNum = iNum + 2;
}
?
/**
* 用256求余最大是255,即16進(jìn)制的FF
*/
int iMod = iTotal % 256;
String sHex = Integer.toHexString(iMod);
iLen = sHex.length();
//如果不夠校驗(yàn)位的長(zhǎng)度,補(bǔ)0,這里用的是兩位校驗(yàn)
if (iLen < 2){
sHex = "0" + sHex;
}
return sHex;
}
再比如使用CRC校驗(yàn)(有CRC8,CRC16,CRC32),關(guān)于CRC校驗(yàn)的原理可以參考:https://blog.csdn.net/u011854789/article/details/80206676
/**
* 獲取CRC檢驗(yàn)
* @param command 命令集
* @param len 命令長(zhǎng)度
* @return
*/
public static int CalCrc(byte[] command,int len){
long MSBInfo;
int i,j ;
int nCRCData;
nCRCData = 0xffff;
for(i = 0; i < len ;i++) {
int temp = (int)(command[i]&0xff);
nCRCData = nCRCData ^ temp ;
for(j= 0 ; j < 8 ;j ++){
MSBInfo = nCRCData & 0x0001;
nCRCData = nCRCData >> 1;
if(MSBInfo != 0 )
nCRCData = nCRCData ^ 0xa001;
}
}
return nCRCData;
}
串口設(shè)備問(wèn)題排查
在對(duì)接串口設(shè)備的過(guò)程中,負(fù)責(zé)硬件的同事說(shuō)在PC上通過(guò)串口助手收發(fā)數(shù)據(jù)沒(méi)有問(wèn)題,然鵝我在Android設(shè)備上,通過(guò)串口就是無(wú)法接收到數(shù)據(jù),于是乎雙方僵持,對(duì)方就差說(shuō):“如果我硬件有問(wèn)題我吃xiang...” 堅(jiān)稱(chēng)是Android板子串口問(wèn)題或者是我讀寫(xiě)數(shù)據(jù)的代碼有問(wèn)題。在沒(méi)有示波器的情況下,如何定位問(wèn)題呢?各方打聽(tīng)嘗試了如下方式:
-
直接短路Tx 與 Rx 兩條線(xiàn)
不接設(shè)備,先確定Android設(shè)備(開(kāi)發(fā)板)上的串口是否可通,檢查方式:直接短路板子上的Tx和Rx兩個(gè)針腳,然后通過(guò)Android的串口demo或者相關(guān)串口助手進(jìn)行命令發(fā)送,看串口是否能夠接收響應(yīng)。也就是檢查板子串口是否可以自發(fā)自收。
-
直接與PC對(duì)接
操作方式是將Android板子上的串口通過(guò)USB轉(zhuǎn)接頭直接插入PC,然后在PC和Android設(shè)備上同時(shí)打開(kāi)串口助手,波特率等參數(shù)保持一致。對(duì)接之后打開(kāi)串口,PC發(fā)命令看Android端是否能接收到,反之Android端發(fā)看PC端是否能接收到。
在嘗試了上面方法之后,發(fā)現(xiàn)Android端的串口是通的,那原因就只能出在要使用串口的設(shè)備(無(wú)線(xiàn)通訊模塊)上了,又是一段時(shí)間僵持之后,我說(shuō)這東西是不是要接電才行?結(jié)果一試,果然是沒(méi)有接電的原因,崩潰。為什么PC上不需要接電能通,然道是因?yàn)閁SB已經(jīng)帶電?不得而知。
以上,只是提供一種在沒(méi)有示波器情況下,檢查串口是否正常的方式,僅做參考。
數(shù)據(jù)轉(zhuǎn)換工具類(lèi)
串口開(kāi)發(fā)中比較常見(jiàn)進(jìn)制與進(jìn)制,進(jìn)制與字節(jié)間的轉(zhuǎn)換,比如:十六進(jìn)制轉(zhuǎn)十進(jìn)制,字節(jié)數(shù)組轉(zhuǎn)十六進(jìn)制字符串等。
相關(guān)代碼如下:
package top.keepempty.serialdemo;
/**
* 數(shù)據(jù)轉(zhuǎn)換工具類(lèi)
* @author frey
*/
public class DataConversion {
?
/**
* 判斷奇數(shù)或偶數(shù),位運(yùn)算,最后一位是1則為奇數(shù),為0是偶數(shù)
* @param num
* @return
*/
public static int isOdd(int num) {
return num & 0x1;
}
?
/**
* 將int轉(zhuǎn)成byte
* @param number
* @return
*/
public static byte intToByte(int number){
return hexToByte(intToHex(number));
}
?
/**
* 將int轉(zhuǎn)成hex字符串
* @param number
* @return
*/
public static String intToHex(int number){
String st = Integer.toHexString(number).toUpperCase();
return String.format("%2s",st).replaceAll(" ","0");
}
?
/**
* 字節(jié)轉(zhuǎn)十進(jìn)制
* @param b
* @return
*/
public static int byteToDec(byte b){
String s = byteToHex(b);
return (int) hexToDec(s);
}
?
/**
* 字節(jié)數(shù)組轉(zhuǎn)十進(jìn)制
* @param bytes
* @return
*/
public static int bytesToDec(byte[] bytes){
String s = encodeHexString(bytes);
return (int) hexToDec(s);
}
?
/**
* Hex字符串轉(zhuǎn)int
*
* @param inHex
* @return
*/
public static int hexToInt(String inHex) {
return Integer.parseInt(inHex, 16);
}
?
/**
* 字節(jié)轉(zhuǎn)十六進(jìn)制字符串
* @param num
* @return
*/
public static String byteToHex(byte num) {
char[] hexDigits = new char[2];
hexDigits[0] = Character.forDigit((num >> 4) & 0xF, 16);
hexDigits[1] = Character.forDigit((num & 0xF), 16);
return new String(hexDigits).toUpperCase();
}
?
/**
* 十六進(jìn)制轉(zhuǎn)byte字節(jié)
* @param hexString
* @return
*/
public static byte hexToByte(String hexString) {
int firstDigit = toDigit(hexString.charAt(0));
int secondDigit = toDigit(hexString.charAt(1));
return (byte) ((firstDigit << 4) + secondDigit);
}
?
private static int toDigit(char hexChar) {
int digit = Character.digit(hexChar, 16);
if(digit == -1) {
throw new IllegalArgumentException(
"Invalid Hexadecimal Character: "+ hexChar);
}
return digit;
}
?
/**
* 字節(jié)數(shù)組轉(zhuǎn)十六進(jìn)制
* @param byteArray
* @return
*/
public static String encodeHexString(byte[] byteArray) {
StringBuffer hexStringBuffer = new StringBuffer();
for (int i = 0; i < byteArray.length; i++) {
hexStringBuffer.append(byteToHex(byteArray[i]));
}
return hexStringBuffer.toString().toUpperCase();
}
?
/**
* 十六進(jìn)制轉(zhuǎn)字節(jié)數(shù)組
* @param hexString
* @return
*/
public static byte[] decodeHexString(String hexString) {
if (hexString.length() % 2 == 1) {
throw new IllegalArgumentException(
"Invalid hexadecimal String supplied.");
}
byte[] bytes = new byte[hexString.length() / 2];
for (int i = 0; i < hexString.length(); i += 2) {
bytes[i / 2] = hexToByte(hexString.substring(i, i + 2));
}
return bytes;
}
?
/**
* 十進(jìn)制轉(zhuǎn)十六進(jìn)制
* @param dec
* @return
*/
public static String decToHex(int dec){
String hex = Integer.toHexString(dec);
if (hex.length() == 1) {
hex = '0' + hex;
}
return hex.toLowerCase();
}
?
/**
* 十六進(jìn)制轉(zhuǎn)十進(jìn)制
* @param hex
* @return
*/
public static long hexToDec(String hex){
return Long.parseLong(hex, 16);
}
?
/**
* 十六進(jìn)制轉(zhuǎn)十進(jìn)制,并對(duì)卡號(hào)補(bǔ)位
*/
public static String setCardNum(String cardNun){
String cardNo1= cardNun;
String cardNo=null;
if(cardNo1!=null){
Long cardNo2=Long.parseLong(cardNo1,16);
//cardNo=String.format("%015d", cardNo2);
cardNo = String.valueOf(cardNo2);
}
return cardNo;
}
}
其他
串口中相關(guān)引腳說(shuō)明如下表,一般在開(kāi)發(fā)板子上可以看到Tx,Rx這兩個(gè)針腳,分別標(biāo)識(shí)串口的發(fā)送和接收。
| 序號(hào) | 信號(hào)名稱(chēng) | 符號(hào) | 流向 | 功能 |
|---|---|---|---|---|
| 2 | 發(fā)送數(shù)據(jù) | TXD | DTE→DCE | DTE 發(fā)送串行數(shù)據(jù) |
| 3 | 接收數(shù)據(jù) | RXD | DTE←DCE | DTE 接收串行數(shù)據(jù) |
| 4 | 請(qǐng)求發(fā)送 | RTS | DTE→DCE | DTE 請(qǐng)求 DCE 將線(xiàn)路切換到發(fā)送方式 |
| 5 | 允許發(fā)送 | CTS | DTE←DCE | DCE 告訴 DTE 線(xiàn)路已接通可以發(fā)送數(shù)據(jù) |
| 6 | 數(shù)據(jù)設(shè)備準(zhǔn)備好 | DSR | DTE←DCE | DCE 準(zhǔn)備好 |
| 7 | 信號(hào)地 | 信號(hào)公共地 | ||
| 8 | 載波檢測(cè) | DCD | DTE←DCE | 表示 DCE 接收到遠(yuǎn)程載波 |
| 20 | 數(shù)據(jù)終端準(zhǔn)備好 | DTR | DTE→DCE | DTE 準(zhǔn)備好 |
| 22 | 振鈴指示 | RI | DTE←DCE | 表示 DCE 與線(xiàn)路接通,出現(xiàn)振鈴 |
關(guān)于串口的相關(guān)知識(shí)可以參考這篇文章
參考:
https://www.cnblogs.com/hackfun/p/7612617.html
https://blog.csdn.net/tianruxishui/article/details/37592903
以上,只是個(gè)人學(xué)習(xí)整理,歡迎學(xué)習(xí)交流,如有紕漏歡迎指出,大神略過(guò)。