【Android Vehicle Series】Chapter 6 Vehicle Communication - Serial Communication Principle

1 serial port

1.1 Introduction to Serial Port

  The serial interface is referred to as serial port for short, also known as serial communication interface or serial communication interface (commonly referred to as COM interface), which is an extended interface using serial communication. Serial interface (Serial Interface) refers to the sequential transmission of data bit by bit. Its characteristic is 通信线路简单that two-way communication can be realized as long as a pair of transmission lines (you can directly use the telephone line as the transmission line), thus 大大降低了成本, especially 适用于远距离通信, but 传送速度较慢.

1.2 Serial communication

  When two devices communicate using UART (Universal Asynchronous Receiver Receiver), they are connected by at least three wires: TXD serial port transmit, RXD serial port receive, GND . 串口设备通过改变TXD信号线上的电压来发送数据,接收端通过检测RXD信号线上的电压来读取数据
  Computers transmit information (data) one or more bits at a time. Serial refers to transferring data one bit at a time. Each word (ie, byte or character) sent or received when communicating over a serial port is sent one bit at a time. Each bit is a logic '1' or '0'. Also use Mark to represent logic 1, and Space to represent logic 0.
  The serial port data rate uses bits-per-second (“bps”) or baud rate (“baud”). This indicates how many logical 1s and 0s can be transmitted in one second. When the baud rate exceeds 1000, you will often see the rate expressed in Kbps. For rates over 1,000,000, it is generally expressed in Mbps.

1.3 Common serial ports

  TTL, RS-232, and RS-485 refer to the level standard (electrical signal) of the serial port. The level standard is simply: what voltage value represents 0, and what voltage value represents 1:

1. TTL level standard is low level is 0, high level is 1 (level signal)
2. RS-232 level standard is positive level is 0, negative level is 1 (level signal)
3.RS- 485 and RS-422 are similar to RS-232, but use differential signal logic, which is more suitable for long-distance and high-speed transmission.
4. Conversion between TTL level and RS232 level: commonly used MAX232 chip

Note: There are only three commonly used protocols for the DB9 interface: RS-232, RS-485 and RS-422. It will never be TTL level, 80% chance is RS-232.

There are two main hardware implementations of the serial port:
D-type 9-pin plug (DB9)
insert image description here
4-pin DuPont head
insert image description here

1.3.1 TTL serial port

  In practical applications, the use of TTL levels has gradually become a trend, which is more common in the serial ports provided by MCUs and serial port adapter chips. At this time, logic 1: voltage value: +5V or +3.3V, etc. Logic 0: Voltage value: 0V (logic ground).

1.3.2 RS-232 serial port

  RS-232 is a standard electrical interface for serial communication defined by the EIA. RS-232 actually has three different types (A, B, and C), each of which defines a different voltage range for logic 1 and logic 0 levels. The most common kind is RS-232C, which defines a logic 1. Voltage range: -3V~-12V and logic 0 voltage range: +3V~+12V.
The most common DB-9 interface distribution diagram is listed below, and the pin and function description are as follows:
insert image description here
Others include RS-422 and RS-485 serial port standards. RS-422 uses lower voltage and differential signaling allowing cables up to 1000 feet (300m).

1.3.3 Commonly used serial port signal definitions

GND - Logic Ground : Technically, logic ground is not a signal, but other signals will not work without it. Fundamentally, logic acts as a reference voltage so that it knows which voltages are positive or negative.
TXD - send data
RXD - receive data
RTS - request to send : RTS is set to logic 0 level to indicate that the party is ready to receive data. It is generally used together with CTS for serial port flow control, and is usually set to the default valid state. In addition to the flow control function, RTS can also be used as a general output signal, outputting high and low levels. Commonly used in microcontroller reset or serial port download circuit.
CTS - Clear to Send : The CTS signal is received from the other end of the serial cable. The logic 0 level of the signal line indicates that the own party can send data. It is generally used together with RTS for
serial port data flow control.
DTR - Data Terminal Ready : The DTR signal is used to inform the peer computer or device that it is ready (logic 0 level) or not ready (logic 1 level). DTR can also be used as a general-purpose output signal, outputting high and low levels. Commonly used in microcontroller reset or serial port download circuit.

1.4 Serial port baud rate

  The baud rate refers to the total number of bits transmitted by the serial port per second, such as: 9600 baud rate. Indicates that 1s transmits 9600 bits. The time required for 1 bit is: 1/9600 ≈ 0.104ms
insert image description here

1.5 Serial port asynchronous communication

  Parsing serial data requires determining where one character ends and the next begins.
  When the serial data line is idle, it remains at a logic 1 state until a character is sent. The start bit of each byte is followed by each bit of the byte, an optional parity bit and one or two stop bits. The start bit is always logic 0, which notifies the other party that new serial port data is available. Data can be sent and received at the same time, hence the term "asynchronous".
insert image description here

Even parity : The number of data bits plus "1" in the parity bit remains even.
Odd parity : The number of data bits plus "1" in the parity bit remains odd.
Blank check : also called Space check, the check digit is always 0.
Mark check : also called Mark check, the check digit is always 1.
No Parity : No parity bit is present or transmitted.
Stop bits : There may be 1, 1.5 or 2 stop bits between characters and these bits are always 1. Asynchronous data formats are usually expressed as "8N1", "7E1", etc.

1.6 Full duplex and half duplex

Full-duplex means that the device can send and receive data at the same time, and has two independent data channels (one input and one output).
Half-duplex means that the device cannot send and receive data at the same time, which usually means that only one channel can communicate, such as RS485 serial port.

1.7 Serial port flow control

  Data flow control is often necessary when transferring data between two serial devices. This may be limited by an intermediate serial communication line, one of the devices, or another storage medium. There are two methods commonly used for asynchronous data flow control.
  The first method, often called "software" flow control, uses special characters to start (XON or DC1) or stop (XOFF or DC3) the flow of data. See the ASCII code table for definitions of these characters.
These code values ​​are useful for transmitting textual information, but cannot be used to transmit other types of information without special programming .
  The second method, called "hardware" flow control, uses the RTS and CTS signal lines instead of special characters. When the receiver is ready to receive data, it will set RTS to logic 0 to request the other party to send data, and when it is not ready, it will be set to logic 1, so the sender will judge whether it can send data by detecting the CTS level status.
  To use hardware flow control, at least the signal lines that need to be connected are GND, RXD, TXD, RTS, and CTS. Only GND, RXD, and TXD are required to use software flow control.

1.8 Linux system

  Use the shell command "lsusb" to confirm whether the usb serial device is recognized normally:
use "ls /dev" to confirm whether the serial device node ttyACM (in CDC drive mode) or ttyUSB (in VCP drive mode) is generated before and after insertion; you can also use "dmesg "Check the kernel message log, check the USB serial device enumeration process and the driver loading process.

2 USB

2.1 Introduction to USB

  USB, short for Universal Serial Bus (Universal Serial Bus), is an external bus standard used to regulate the connection and communication between computers and external devices. It is an interface technology applied in the PC field. That is, the U disk socket, mouse socket, and keyboard socket of common laptops.
insert image description here
  Both serial port and USB are serial communication, but they are completely different. The serial port was born in 1980, and the USB was born in 1995. USB communication speed and stability are better than traditional serial ports.

2.2 USB to serial port

  1. PL2303, CP2102, and FT232R chips are USB-to-serial* (TTL level output)* chips, and Windows drivers need to be installed
    .
  2. The picture below is a small board (TTL level) for USB to TTL serial port, the chip is PL2303HX.
    insert image description here
  3. If the target device is an RS-232 serial port (D-type 9-pin interface), it is enough to connect a MAX232 chip in series to convert it to RS-232 level, so a USB-to-RS-232 serial port product is produced, as shown in the figure below. Look carefully (from right to left), the USB is converted to a TTL serial port through PL2303 (the four holes in the middle can be drawn out), and then converted to RS-232 level through MAX232, and the 9-pin serial port is drawn out.
    insert image description here
  4. Now the common USB to RS-232 on the market is like this (as long as it is a D-type 9-pin serial port, it will not be TTL level, and the default is RS-232 if there is no special instruction.)
    insert image description here

3 Android implements serial port communication

To implement serial communication on Android devices, we need to complete the following steps:

  1. The first step is to find the serial port file
  2. The second step is to open the serial port file
  3. The third step is to send and read data
  4. The fourth step is to close the serial port

  Because of the serial communication, we need to adjust 波特率、数据位、校验位、停止位、流控these parameters. The Java layer directly creates FileDescriptor to access the serial file and cannot modify these parameters for serial communication. Therefore, the Native layer is used to create a FileDescriptor instance, adjust the corresponding parameters for serial communication, and provide the fd object to the Java layer. read and write operations.

The figure below shows the serial communication parameters:

insert image description here

3.1 The first step is to find the serial port file

  The Android serial port file has a separate directory: /dev/ttyS , we 先找到串口文件目录
insert image description here
insert image description here
insert image description here
use the following code 操作这个ttys开头的文件:

 private ArrayList<Driver> getDrivers() throws IOException {
    
    
        ArrayList<Driver> drivers = new ArrayList<>();
        LineNumberReader lineNumberReader = new LineNumberReader(new FileReader(DRIVERS_PATH));
        String readLine;
        while ((readLine = lineNumberReader.readLine()) != null) {
    
    
            String driverName = readLine.substring(0, 0x15).trim();
            String[] fields = readLine.split(" +");
            // driverName:/dev/tty 
            // driverName:/dev/console 
            // driverName:/dev/ptmx 
            // driverName:/dev/vc/0 
            // driverName:serial 
            // driverName:pty_slave 
            // driverName:pty_master 
            // driverName:unknown 
            Log.d(T.TAG, "SerialPortFinder getDrivers() driverName:" + driverName /*+ " readLine:" + readLine*/);
            if ((fields.length >= 5) && (fields[fields.length - 1].equals(SERIAL_FIELD))) {
    
    
                // 判断第四个等不等于serial
                // 找到了新串口驱动是:serial 此串口系列名是:/dev/ttyS 
                Log.d(T.TAG, "SerialPortFinder getDrivers() 找到了新串口驱动是:" + driverName + " 此串口系列名是:" + fields[fields.length - 4]);
                drivers.add(new Driver(driverName, fields[fields.length - 4]));
            }
        }
        return drivers;
    }

3.2 The second step is to open the serial port file

  然后是检验和获取权限权限, you usually need to have read and write permissions before operating the serial port

  /**
     * 检查权限
     */
    public void checkPermission() {
    
    
        if (!device.canRead() || !device.canWrite()) {
    
    
            if (!chmod777(device)) {
    
    
                Log.i(T.TAG, "SerialPortManager openSerialPort: 没有读写权限");
                if (null != mOnOpenSerialPortListener) {
    
    
                    mOnOpenSerialPortListener.onFail(device, OnOpenSerialPortListener.Status.NO_READ_WRITE_PERMISSION);
                }
                return false;
            }
        }
    }

    /*** 文件设置最高权限 777 可读 可写 可执行 * @param file 你要对那个文件,获取root权限 * @return 权限修改是否成功- 返回:成功 与 失败 结果 */
    private boolean chmod777(File file) {
    
    
        if (null == file || !file.exists()) {
    
    
            // 文件不存在 
            return false;
        }
        try {
    
    
            // 获取ROOT权限 
            Process su = Runtime.getRuntime().exec("/system/bin/su");
            // 修改文件属性为 [可读 可写 可执行] 
            String cmd = "chmod 777 " + file.getAbsolutePath() + "\n" + "exit\n";
            su.getOutputStream().write(cmd.getBytes());
            if (0 == su.waitFor() && file.canRead() && file.canWrite() && file.canExecute()) {
    
    
                return true;
            }
        } catch (IOException | InterruptedException e) {
    
    
            // 没有ROOT权限 
            e.printStackTrace();
        }
        return false;
    }

  After checking the permissions, we need to use the ndk code to open the serial port for operation. The connection between the Java layer and the Native layer is the file handle FileDescriptor, which is the fd in the code. The Native layer returns FileDescriptor, and the Java layer's FileInputStream, FileOutputStream and FileDescriptor are bound, so that the Java layer can read the data.

  public void start() {
    
    
        try {
    
    
            // 打开串口-native函数 
            mFd = openNative(device.getAbsolutePath(), baudRate, 0);
            // 读取的流 绑定了 (mFd文件句柄)-通 过文件句柄(mFd)包装出 输入流 
            mFileInputStream = new FileInputStream(mFd);
            // 写入的流 绑定了 (mFd文件句柄)- 通过文件句柄(mFd)包装出 输出流 
            mFileOutputStream = new FileOutputStream(mFd);
            Log.i(T.TAG, "SerialPortManager openSerialPort: 串口已经打开 " + mFd);
            // 串口已 经打开 FileDescriptor[35]
            if (null != mOnOpenSerialPortListener) {
    
    
                mOnOpenSerialPortListener.onSuccess(device);
            }
            // 开启发送消息的线程
            startSendThread();
            // 开启接收消息的线程
            startReadThread();
            return true;  
        } catch (Exception e) {
    
    
            e.printStackTrace();
            if (null != mOnOpenSerialPortListener) {
    
    
                mOnOpenSerialPortListener.onFail(device, OnOpenSerialPortListener.Status.OPEN_FAIL);
            }
        }
    }

The following is the Native code:

 JNIEXPORT jobject JNICALL Java_com_test_openNative(JNIEnv *env, jclass thiz, jstring path, jint baudrate, jint flags) {
    
    
        int fd; // Linux串口文件句柄(本次整个函数最终的关键成果) 
        speed_t speed; // 波特率类型的值 
        jobject mFileDescriptor; // 文件句柄(最终返回的成果) //检查参数,获取波特率参数信息 [先确定好波特率] 
        {
    
    
            speed = getBaudrate(baudrate);
            if (speed == -1) {
    
    
                LOGE("无效的波特率,证明用户选择的波特率 是错误的");
                return NULL;
            }
        }
        // TODO 第一步:打开串口 
        {
    
    
            jboolean iscopy; const char *path_utf = ( * env)->GetStringUTFChars(env, path, & iscopy)
            ;
            LOGD("打开串口 路径是:%s", path_utf); // 打开串口 路径是:/dev/ttyS0 
            fd = open(path_utf, O_RDWR /*| flags*/); // 打开串口的函数,O_RDWR(读 和 写) 
            LOGD("打开串口 open() fd = %d", fd); // open() fd = 44
            ( * env)->ReleaseStringUTFChars(env, path, path_utf);
            // 释放操作 
            if (fd == -1) {
    
    
                LOGE("无法打开端口");
                return NULL;
            }
        } 
        LOGD("第一步:打开串口,成功了√√√");
        // TODO 第二步:获取和设置终端属性-配置串口设备 /* TCSANOW:不等数据传输完毕就立即改变属性。 TCSADRAIN:等待所有数据传输结束才改变属性。 TCSAFLUSH:清空输入输出缓冲区才改变属性。 注意:当进行多重修改时,应当在这个函数之后再次调用 tcgetattr() 来检测是否所有修改都成 功实现。*/ 
        {
    
    
            struct termios cfg;
            LOGD("执行配置串口中...");
            if (tcgetattr(fd, & cfg)){
    
    
                // 获取串口属性 
                LOGE("配置串口tcgetattr() 失败");
                close(fd); // 关闭串口 return NULL; 
            }
            cfmakeraw( & cfg); // 将串口设置成原始模式,并且让fd(文件句柄 对串口可读可 写) 
            cfsetispeed( & cfg, speed); // 设置串口读取波特率 
            cfsetospeed( & cfg, speed); // 设置串口写入波特率 
            if (tcsetattr(fd, TCSANOW, & cfg)){
    
     // 根据上面的配置,再次获取串口属性 
                LOGE("再配置串口tcgetattr() 失败");
                close(fd); // 关闭串口 
                return NULL;
            }
        }
        
        LOGD("第二步:获取和设置终端属性-配置串口设备,成功了√√√");
        // TODO 第三步:构建FileDescriptor.java对象,并赋予丰富串口相关的值 
        {
    
    
            jclass cFileDescriptor = ( * env)->FindClass(env, "java/io/FileDescriptor");
            jmethodID iFileDescriptor = ( * env)->
            GetMethodID(env, cFileDescriptor, " <init>", "()V");
            jfieldID descriptorID = ( * env)->GetFieldID(env, cFileDescriptor, "descriptor", "I");
// 反射生成FileDescriptor对象,并赋值 (fd==Linux串口文件句柄) FileDescriptor的构造函数实 例化 
            mFileDescriptor = ( * env)->NewObject(env, cFileDescriptor, iFileDescriptor);
            ( * env)->SetIntField(env, mFileDescriptor, descriptorID, (jint) fd);
// 这里的 fd,就是打开串口的关键成果 }
            LOGD("第三步:构建FileDescriptor.java对象,并赋予丰富串口相关的值,成功了√√√");
            return mFileDescriptor; // 把最终的成果,返回会Java层 
        }
    }

In this way, we have completed the entire operation of opening the serial port.
insert image description here

3.3 Sending and reading data

  Reading and sending data is to operate on file IO, we must do it in sub-threads.

  private void startReadThread() {
    
    
        mSerialPortReadThread = new SerialPortReadThread(mFileInputStream) {
    
    
            @Override
            public void onDataReceived(byte[] bytes) {
    
    
                if (null != mOnSerialPortDataListener) {
    
    
                    mOnSerialPortDataListener.onDataReceived(bytes);
                }
            }
        };
        mSerialPortReadThread.start();
    }

    /*** 串口消息读取线程 * 开启接收消息的线程 * 读取 串口数据 需要用到线程 */
    public abstract class SerialPortReadThread extends Thread {
    
    
        public abstract void onDataReceived(byte[] bytes);

        private static final String TAG = SerialPortReadThread.class.getSimpleName();
        private InputStream mInputStream; // 此输入流==mFileInputStream(关联mFd文件句柄)
        private byte[] mReadBuffer; // 用于装载读取到的串口数据

        public SerialPortReadThread(InputStream inputStream) {
    
    
            mInputStream = inputStream;
            mReadBuffer = new byte[1024]; // 缓冲区
        }

        @Override
        public void run() {
    
    
            super.run(); // 相当于是一直执行?为什么要一直执行?因为只要App存活,就需要读取 底层发过来的串口数据
            while (!isInterrupted()) {
    
    
                try {
    
    
                    if (null == mInputStream) {
    
    
                        return;
                    }
                    Log.i(TAG, "run: ");
                    int size = mInputStream.read(mReadBuffer);
                    if (-1 == size || 0 >= size) {
    
    
                        return;
                    }
                    byte[] readBytes = new byte[size]; // 拷贝到缓冲区
                    System.arraycopy(mReadBuffer, 0, readBytes, 0, size);
                    Log.i(TAG, "run: readBytes = " + new String(readBytes));
                    onDataReceived(readBytes); // 发出去-(间接的发到SerialPortActivity中 去打印显示)
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                    return;
                }
            }
        }

        @Override
        public synchronized void start() {
    
    
            super.start();
        }

        /**
         * 关闭线程 释放资源
         */
        public void release() {
    
    
            interrupt();
            if (null != mInputStream) {
    
    
                try {
    
    
                    mInputStream.close();
                    mInputStream = null;
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    }

    private void startSendThread() {
    
    
        // 开启发送消息的线程
        mSendingHandlerThread = new HandlerThread("mSendingHandlerThread");
        mSendingHandlerThread.start(); // Handler
        mSendingHandler = new Handler(mSendingHandlerThread.getLooper()) {
    
    
            @Override
            public void handleMessage(Message msg) {
    
    
                byte[] sendBytes = (byte[]) msg.obj;
                if (null != mFileOutputStream && null != sendBytes && 0 < sendBytes.length) {
    
    
                    try {
    
    
                        mFileOutputStream.write(sendBytes);
                        if (null != mOnSerialPortDataListener) {
    
    
                            mOnSerialPortDataListener.onDataSent(sendBytes); // 【发送 1】
                        }
                    } catch (IOException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        };
    }

  Reading and writing data is actually to operate on the two read-in and read-out streams, and in this way we have completed sending and receiving data to the serial port.

3.4 Close the serial port

  After we have finished using the serial port, we will definitely close the serial port. When we close the serial port, we will close the started reading and writing threads, and also close the serial port at the Native layer, and close the two streams bound to the file handle. .

 /*** 关闭串口 */
    public void closeSerialPort() {
    
    
        if (null != mFd) {
    
    
            closeNative(); // 关闭串口-native函数 
            mFd = null;
        }
        stopSendThread(); // 停止发送消息的线程 
        stopReadThread(); // 停止接收消息的线程
        if (null != mFileInputStream) {
    
    
            try {
    
    
                mFileInputStream.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
            mFileInputStream = null;
        }
        if (null != mFileOutputStream) {
    
    
            try {
    
    
                mFileOutputStream.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
            mFileOutputStream = null;
        }
        mOnOpenSerialPortListener = null;
        mOnSerialPortDataListener = null;
    }

The following is the Native code:

    /** 关闭串口 * Class: cedric_serial_SerialPort * Method: close * Signature: ()V */
    JNIEXPORT void JNICALL Java_com_test_closeNative(JNIEnv *env, jobject thiz) {
    
    
        jclass SerialPortClass = ( * env)->GetObjectClass(env, thiz);
        jclass FileDescriptorClass = ( * env)->FindClass(env, "java/io/FileDescriptor");
        jfieldID mFdID = ( * env)->
        GetFieldID(env, SerialPortClass, "mFd", "Ljava/io/FileDescriptor;");
        jfieldID descriptorID = ( * env)->GetFieldID(env, FileDescriptorClass, "descriptor", "I");
        jobject mFd = ( * env)->GetObjectField(env, thiz, mFdID);
        jint descriptor = ( * env)->GetIntField(env, mFd, descriptorID);
        LOGD("关闭串口 close(fd = %d)", descriptor);
        close(descriptor); // 把此串口文件句柄关闭掉-文件读写流(文件句柄) InputStream/OutputStream=串口 发/收
    }

3.5 Summary

串口通信,其实就是对文件进行操作,一边读一边写, Just like passing a note with your deskmate when you were in school, the above code refers to Google's open source code, from finding the serial port to closing the serial port, I have sorted out the basic process of serial port communication! hope its good for U.S..

Guess you like

Origin blog.csdn.net/u010687761/article/details/129804225