协议基础笔记

发布时间:2024年01月02日

Android串口使用方法_android-serialport的使用-CSDN博客

安卓与串口通信-基础篇_安卓串口通信-CSDN博客

串口简介

串口通信是Android智能硬件开发所必须具备的能力,市面上类型众多的外设基本都是通过串口进行数据传输的,所以说不会串口通信根本就做不了智能硬件开发。

串口通信(Serial Communications)的概念非常简单,串口按位(bit)发送和接收字节。串口可以在使用一根线(Tx)发送数据的同时用另一根线(Rx)接收数据。

串口参数

**波特率:**串口传输速率,用来衡量数据传输的快慢,即单位时间内载波参数变化的次数,如每秒钟传送240个字符,而每个字符格式包含10位(1个起始位,1个停止位,8个数据位),这时的波特率为240Bd,

比特率为10位*240个/秒=2400bps。波特率与距离成反比,波特率越大传输距离相应的就越短。

**数据位:**这是衡量通信中实际数据位的参数。当计算机发送一个信息包,实际的数据往往不会是8位的,标准的值是6、7和8位。如何设置取决于你想传送的信息。

**停止位:**用于表示单个包的最后一位。典型的值为1,1.5和2位。

由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。

**校验位:**在串口通信中一种简单的检错方式。有四种检错方式:偶、奇、高和低。当然没有校验位也是可以的。对于偶和奇校验的情况,串口会设置校验位(数据位后面的一位),用一个值确保传输的数据有偶个或者奇个逻辑高位。

串口地址

如下表不同操作系统的串口地址,Android是基于Linux的所以一般情况下使用Android系统的设备串口地址为/dev/ttyS0...

? ?

SystemPort 1Port 2
IRIX?/dev/ttyf1/dev/ttyf2
Linux?/dev/ttyS0/dev/ttyS1
Digital UNIX?/dev/tty01/dev/tty02

Android串口实现

?在Android上使用串口比较快速的方式就是直接套用google官方的串口demo代码(android-serialport-api),基本上能够应付很多在Android设备使用串口的场景。

在收发数据频率很快的情况下,实际测试这种方式接收数据会有延迟。比如:发送一个命令之后,设备会同时响应两条命令,一条是结果一条是校验且两条命令间隔时间仅1ms,按理两条命令会几乎同时收到,但是实际使用该方式会出现10ms的延迟。所以只能着手优化,尝试使用C/C++的方式进行串口数据的读写。

一番查阅下来,使用C/C++实现其实和上面的demo差别不大,同样是那几个步骤,设置串口参数,通过调用open方法开启串口,再进行数据的读写操作。出现数据读取延迟很可能的原因,就是因为官方demo是通过Java层的文件流(FileInputStream,FileOutputStream)进行读写操作引起的。如果有大神懂这块的可以说明这种方式导致延迟的原因。

?

比如设置波特率代码:

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;
}

打开串口 函数,设置相关读写参数

int SerialPort::openSerialPort(SerialPortConfig config) {
    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;
}

串口数据读取涉及两个函数 select和read ,函数相关的含义暂且没去深究,属于C/C++范凑了,读取数据代码如下:?

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);  //把要检测的句柄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;
}

?

?串口写数据就是调用write函数了,代码如下:

int SerialPort::writeData(BYTE *data, int len) {
    int result;
    result = static_cast<int>(write(fd, data, static_cast<size_t>(len)));
    return TRUE;
}

阻塞与非阻塞

在项目初期使用google官方的串口demo代码调试设备串口是否能正常通信的时候,遇到在串口读数据的线程中会卡死在inputStream.read(buffer);这个时候就让人疑惑了,不知道问题是出在硬件还是在串口读取上,在没有了解串口相关知识前,希望的场景是读数据的线程能够不阻塞,一直轮询读取数据。

出现读取数据线程卡死的情况是因为在 fd = open(path_utf, O_RDWR | flags); 设置相关参数,读取默认为阻塞模式,若在open操作中设置O_NONBLOCK则是非阻塞模式。在阻塞模式中,read没有读到数据会阻塞住,直到收到数据;非阻塞模式read没有读到数据会返回-1不会阻塞。

修改open方法:

fd = open(path_utf, O_RDWR | flags | O_NONBLOCK | O_NOCTTY | O_NDELAY);

读取线程就不会再出现卡死了,这个时候仍然接收不到串口设备反馈的数据,就可以断定是串口设备的问题了。
?

关于串口文件打开方式,可采用下面的文件打开模式,具体说明如下:

? O_RDONLY:以只读方式打开文件

?O_WRONLY:以只写方式打开文件

O_RDWR:以读写方式打开文件

O_APPEND:写入数据时添加到文件末尾

? O_CREATE:如果文件不存在则产生该文件,使用该标志需要设置访问权限位mode_t

O_EXCL:指定该标志,并且指定了O_CREATE标志,如果打开的文件存在则会产生一个错误

O_TRUNC:如果文件存在并且成功以写或者只写方式打开,则清除文件所有内容,使得文件长度变为0

O_NOCTTY:如果打开的是一个终端设备,这个程序不会成为对应这个端口的控制终端,如果没有该标志,任何一个输入,例如键盘中止信号等,都将影响进程。

O_NONBLOCK:该标志与早期使用的O_NDELAY标志作用差不多。程序不关心DCD信号线的状态,如果指定该标志,进程将一直在休眠状态,直到DCD信号线为0。

实际应用中,都会选择阻塞模式,这样更节省资源。但是如果希望在一个线程中同时进行读写操作,没数据反馈时,线程就会阻塞等待,就无法进行写数据了。

串口数据校验方式

一般情况下串口通讯协议都会在数据帧或者说命令格式里定义一个校验方式,常用的有或校验、和校验、CRC校验LRC校验

**注意:**这里说的校验和上面说的校验位是不同的,校验位针对的是单个字节,校验类型针对的是单个数据帧。

校验方式一般放在命令最后,可以是一个byte,也可以是两个byte或者其他,具体看协议设计。

比如命令格式如下,采用和校验(类似穿山甲):

addrcommanddata_lengthdata1data2datanchecksum
0x010x520x050x110xBA...8E

其中,获取校验码(checksum)就是将命令中的数据进行相加生成,Checksum=256-(data1+data2+datan)算出校验码为:8E。具体计算方式就是通过将十六进制进行相加算出校验码的十进制字符,详细代码如下:

/**
 *  获取校验码(计算方式如下: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");
}

十六进制进行相加代码:

/**
* 生成校验码,十六进制相加
* @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进制的FF
     */
    int iMod = iTotal % 256;
    String sHex = Integer.toHexString(iMod);
    iLen = sHex.length();
    //如果不够校验位的长度,补0,这里用的是两位校验
    if (iLen < 2){
        sHex = "0" + sHex;
    }
    return sHex;
}

再比如使用CRC校验(有CRC8,CRC16,CRC32),关于CRC校验的原理可以参考:blog.csdn.net/u011854789/…

/**
  * 获取CRC检验
  * @param command  命令集
  * @param len      命令长度 
  * @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;
}

串口设备问题排查

?在对接串口设备的过程中,负责硬件的同事说在PC上通过串口助手收发数据没有问题,然鹅我在Android设备上,通过串口就是无法接收到数据,于是乎双方僵持,对方就差说:“如果我硬件有问题我吃xiang...” 坚称是Android板子串口问题或者是我读写数据的代码有问题。在没有示波器的情况下,如何定位问题呢?各方打听尝试了如下方式:

1.直接短路Tx 与 Rx 两条线

不接设备,先确定Android设备(开发板)上的串口是否可通,检查方式:直接短路板子上的Tx和Rx两个针脚,然后通过Android的串口demo或者相关串口助手进行命令发送,看串口是否能够接收响应。也就是检查板子串口是否可以自发自收。

2.直接与PC对接

操作方式是将Android板子上的串口通过USB转接头直接插入PC,然后在PC和Android设备上同时打开串口助手,波特率等参数保持一致。对接之后打开串口,PC发命令看Android端是否能接收到,反之Android端发看PC端是否能接收到。

?在尝试了上面方法之后,发现Android端的串口是通的,那原因就只能出在要使用串口的设备(无线通讯模块)上了,又是一段时间僵持之后,我说这东西是不是要接电才行?结果一试,果然是没有接电的原因,崩溃。为什么PC上不需要接电能通,然道是因为USB已经带电?不得而知。

以上,只是提供一种在没有示波器情况下,检查串口是否正常的方式,仅做参考。

?

数据转换工具类

串口开发中比较常见进制与进制,进制与字节间的转换,比如:十六进制转十进制,字节数组转十六进制字符串等。

?

package top.keepempty.serialdemo;
/**
 *  数据转换工具类
 *  @author frey
 */
public class DataConversion {

    /**
     * 判断奇数或偶数,位运算,最后一位是1则为奇数,为0是偶数
     * @param num
     * @return
     */
    public static int isOdd(int num) {
        return num & 0x1;
    }

    /**
     * 将int转成byte
     * @param number
     * @return
     */
    public static byte intToByte(int number){
        return hexToByte(intToHex(number));
    }

    /**
     * 将int转成hex字符串
     * @param number
     * @return
     */
    public static String intToHex(int number){
        String st = Integer.toHexString(number).toUpperCase();
        return String.format("%2s",st).replaceAll(" ","0");
    }

    /**
     * 字节转十进制
     * @param b
     * @return
     */
    public static int byteToDec(byte b){
        String s = byteToHex(b);
        return (int) hexToDec(s);
    }

    /**
     * 字节数组转十进制
     * @param bytes
     * @return
     */
    public static int bytesToDec(byte[] bytes){
        String s = encodeHexString(bytes);
        return (int)  hexToDec(s);
    }

    /**
     * Hex字符串转int
     *
     * @param inHex
     * @return
     */
    public static int hexToInt(String inHex) {
        return Integer.parseInt(inHex, 16);
    }

    /**
     * 字节转十六进制字符串
     * @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();
    }

    /**
     * 十六进制转byte字节
     * @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;
    }

    /**
     * 字节数组转十六进制
     * @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();
    }

    /**
     * 十六进制转字节数组
     * @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;
    }

    /**
     * 十进制转十六进制
     * @param dec
     * @return
     */
    public static String decToHex(int dec){
        String hex = Integer.toHexString(dec);
        if (hex.length() == 1) {
            hex = '0' + hex;
        }
        return hex.toLowerCase();
    }

    /**
     * 十六进制转十进制
     * @param hex
     * @return
     */
    public static long hexToDec(String hex){
        return Long.parseLong(hex, 16);
    }

    /**
     * 十六进制转十进制,并对卡号补位
     */
    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;
    }
}

?其他

串口中相关引脚说明如下表,一般在开发板子上可以看到Tx,Rx这两个针脚,分别标识串口的发送和接收。

串口基础知识

串行与并行区别

如果我们想让两台设备(单片机、电脑等)相互通信,一般可以有两种方式:串行与并行。

例如我们想从设备A中发送一条 8 bit 的数据到设备 B。

如果是串行的方式则会从第1位到第8位依次排队从一根线(注意,这里的一根线不是物理意义上的一根线,可以理解成一根完整的数据线)上发送过去。

如果使用并行的方式,则可能设备A和设备B之间有8条线相连接,发送时这 8 bit 数据会同时从8根线上发送到设备B。

?从上面的图也不难看出,并行传输相比于串行传输速度会更快,但是相应的线路成本也更高,而且抗干扰能力也没有串行强,所以并行的通信距离没有串行的距离长。

同时,由于串行协议的外设简单,成本低通信距离远,所以我们在单片机或者说工控设备之间通常使用的都是串行传输。

而串行传输又可以分为同步串行和异步串行。

同步串行与异步串行

?同步串行:指的是通信双方的时钟频率保持一致,接收方时刻准备接收数据,并且只需要在接受到开始传输的信号后即可连续传输数据,数据之间没有间隙,不需要起始位和结束位,传输效率高,可以进行一对多通信。常用的同步串行有 I2C 和 SPI。

异步串行:指的是双方时钟频率不一致,传输数据时采用帧格式传输,发送方发送每一帧数据时需要附加起始位和结束位,接收方通过读取起始位和结束位来实现与发送方的信息同步,传输效率低,只能1对1通信。通常一帧数据由起始位、数据位、校验位、停止位组成。常用的异步串行有 UART ,即我们俗称的串口通信。
?

串口通信(UART)

?为了实现串口通信,我们需要在设备之间连接三条线:TX(发送)、RX(接收)、GND(接地)。

由于每个设备之间都同时有发送和接收端,所以串口通信是全双工通信,即支持同时发送和接收数据。

设备A的TX和设备B的RX相连;

设备A的RX和设备B的TX相连;

设备A、B的GND相连。

假如我们想要由设备A向设备B发送一个字符 “E”,则发送效果如图:

通过设备A的发送端(TX)向设备B的接收端(RX)发送一段数据。

此时如果测量发送数据时的这条线上的电平,将得到这么一个波形图:

文章来源:https://blog.csdn.net/qq_33552379/article/details/135259126
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。