方寸山

Android RSR232 串口通信

由于工作需要,需要在工控机上实现Android对RSR232串口读写功能。Android 官方串口通信文档中已经对串口通信的核心功能有所实现,然而实际应用时会发现还有需要额外的参数需要设置。

参考资料

Android官方串口通信
Android串口通讯源代码
Building C++ in Android Studio with CMake or ndk-build

走过的坑

起初接到需求的时候便简单查了一些串口通信相关的现成实现,发现已经有很多现成的代码,不禁窃喜,以为可以偷下懒拿别人封装好的代码直接用了。殊不知,我们的硬件合作厂商对RSR-232协议遵循的非常到位,其中的参数必须完全设置正常才能进行有效的通信,而网络上已经开源的类库中并未发现适合项目的,正因此才有了本文。

RSR-232

串口通信相关的电气信息此处不做介绍,如果需要可以参考RSR-232这篇wiki。以下是进行串口通信开发时常用的设置项:

  • 波特率(Baud),从一设备发到另一设备的波特率,即每秒钟多少符号。
  • 奇偶校验(Parity),用来验证数据的正确性。
  • 停止位(Stop Bit),在每个字节传输之后发送的,它用来帮助接受信号方硬件重同步。
  • 流量控制,当需要发送握手信号或数据完整性检测时需要制定其他设置。

以上四个设置项,我们开发时按照硬件厂商的要求进行正确配置即可。

JNI

本文主要介绍纯串口通信模式下的编码,如果你使用的是基于USB-Host模式的串口通信,请参考其他文章。

纯串口通信方式需要借助于JNI,关于JNI的开发配置本文不做过多介绍,参考上述资料自行配置即可。需要有一点注意的是,我在进行基于Android Studio进行NDK的开发时,发现Java代码与native代码在同一个Module时无法编译通过,如果你也遇到了这个问题,可以暂时剥离Java与native代码。

Native代码

  1. 检测参数是否合法

    /* Check arguments */
    {
        speed = getBaudrate(baudrate);
        if (speed == -1) {
            /* TODO: throw an exception */
            LOGE("Invalid baudrate");
            return NULL;
        }
    }
    
  2. 打开TTY设备

    /* Opening device */
    {
        jboolean iscopy;
        const char *path_utf = (*env)->GetStringUTFChars(env, path, &iscopy);
        LOGD("Opening serial port %s with flags 0x%x", path_utf, O_RDWR);
        fd = open(path_utf, O_RDWR);
        LOGD("open() fd = %d", fd);
        (*env)->ReleaseStringUTFChars(env, path, path_utf);
        if (fd == -1) {
            /* Throw an exception */
            LOGE("Cannot open port");
            /* TODO: throw an exception */
            return NULL;
        }
    }
    
  3. 配置设备

    • 配置波特率

      struct termios cfg;
      LOGD("Configuring serial port");
      if (tcgetattr(fd, &cfg)) {
          LOGE("tcgetattr() failed");
          close(fd);
          /* TODO: throw an exception */
          return NULL;
      }
      
      cfmakeraw(&cfg);
      cfsetispeed(&cfg, speed);
      cfsetospeed(&cfg, speed);
      
    • 配置数据位

      cfg.c_cflag &= ~CSIZE;
      switch (dataBits) {
          case 5:
              cfg.c_cflag |= CS5;    //使用5位数据位
              break;
          case 6:
              cfg.c_cflag |= CS6;    //使用6位数据位
              break;
          case 7:
              cfg.c_cflag |= CS7;    //使用7位数据位
              break;
          case 8:
              cfg.c_cflag |= CS8;    //使用8位数据位
              break;
          default:
              cfg.c_cflag |= CS8;
              break;
      }
      
    • 配置校验位

      switch (parity) {
      case 0:
          cfg.c_cflag &= ~PARENB;    //无奇偶校验
          break;
      case 1:
          cfg.c_cflag |= (PARODD | PARENB);   //奇校验
          break;
      case 2:
          cfg.c_iflag &= ~(IGNPAR | PARMRK); // 偶校验
          cfg.c_iflag |= INPCK;
          cfg.c_cflag |= PARENB;
          cfg.c_cflag &= ~PARODD;
          break;
      default:
          cfg.c_cflag &= ~PARENB;
          break;
      }
      
    • 配置停止位

      switch (stopBits) {
          case 1:
              cfg.c_cflag &= ~CSTOPB;    //1位停止位
              break;
          case 2:
              cfg.c_cflag |= CSTOPB;    //2位停止位
              break;
          default:
              break;
      }
      
    • 配置流控

      // hardware flow control
      switch (flowCon) {
          case 0:
              cfg.c_cflag &= ~CRTSCTS;    //不使用流控
              break;
          case 1:
              cfg.c_cflag |= CRTSCTS;    //硬件流控
              break;
          case 2:
              cfg.c_cflag |= IXON | IXOFF | IXANY;    //软件流控
              break;
          default:
              cfg.c_cflag &= ~CRTSCTS;
              break;
      }
      

Java层代码

为了便于调用,Java层代码也做了一定的封装。

  1. 数据封装

    在封装代码时,应该尽可能的为调用者减少出错机会,如:对固定波特率的封装,并且被调用方法只接受波特率的封装类,将使调用更加明确和简单。

    • 波特率

      同Native层,Java层对波特率进行了封装,如下:

      /** 串口波特率定义 */
      public enum BAUDRATE {
          B0(0),
          B50(50),
          B75(75),
          B110(110),
          B134(134),
          B150(150),
          B200(200),
          B300(300),
          B600(600),
          B1200(1200),
          B1800(1800),
          B2400(2400),
          B4800(4800),
          B9600(9600),
          B19200(19200),
          B38400(38400),
          B57600(57600),
          B115200(115200),
          B230400(230400),
          B460800(460800),
          B500000(500000),
          B576000(576000),
          B921600(921600),
          B1000000(1000000),
          B1152000(1152000),
          B1500000(1500000),
          B2000000(2000000),
          B2500000(2500000),
          B3000000(3000000),
          B3500000(3500000),
          B4000000(4000000);
      
          int baudrate;
      
          BAUDRATE(int baudrate) {
              this.baudrate = baudrate;
          }
      
          int getBaudrate() {
              return this.baudrate;
          }
      
      }
      
    • 停止位

      /** 串口停止位定义 */
      public enum STOPB {
          /** 1位停止位 */
          B1(1),
          /** 2位停止位 */
          B2(2);
      
          int stopBit;
      
          STOPB(int stopBit) {
              this.stopBit = stopBit;
          }
      
          public int getStopBit() {
              return this.stopBit;
          }
      
      }
      
    • 数据位

      /** 串口数据位定义 */
      public enum DATAB {
          /** 5位数据位 */
          CS5(5),
          /** 6位数据位 */
          CS6(6),
          /** 7位数据位 */
          CS7(7),
          /** 8位数据位 */
          CS8(8);
      
          int dataBit;
      
          DATAB(int dataBit) {
              this.dataBit = dataBit;
          }
      
          public int getDataBit() {
              return this.dataBit;
          }
      }
      
    • 校验位

      /** 串口校验位定义 */
      public enum PARITY {
          /** 无奇偶校验 */
          NONE(0),
          /** 奇校验 */
          ODD(1),
          /** 偶校验 */
          EVEN(2);
      
          int parity;
      
          PARITY(int parity) {
              this.parity = parity;
          }
      
          public int getParity() {
              return this.parity;
          }
      }
      
    • 流控

      /** 串口流控定义 */
      public enum FLOWCON {
          /** 不使用流控 */
          NONE(0),
          /** 硬件流控 */
          HARD(1),
          /** 软件流控 */
          SOFT(2);
      
          int flowCon;
      
          FLOWCON(int flowCon) {
              this.flowCon = flowCon;
          }
      
          public int getFlowCon() {
              return this.flowCon;
          }
      }
      
  2. 打开设备

    /**
     * 打开串口设备
     *
     * @param device   设备文件
     * @param baudrate {@link BAUDRATE} 串口波特率
     * @param stopbit  {@link STOPB} 串口停止位
     * @param databit  {@link DATAB} 串口数据位
     * @param parity   {@link PARITY} 串口奇偶检验
     * @param flowCon  {@link FLOWCON} 串口流控
     *
     * @return true, 串口设备打开成功。false,打开失败
     */
    public boolean open(File device,
                        BAUDRATE baudrate,
                        STOPB stopbit,
                        DATAB databit,
                        PARITY parity,
                        FLOWCON flowCon) {
        /* Check access permission */
        if (!device.canRead() || !device.canWrite()) {
            try {
                /* Missing read/write permission, trying to chmod the file */
                Process su;
                su = Runtime.getRuntime().exec("/system/bin/su");
                String cmd = "chmod 666 " + device.getAbsolutePath() + "\n"
                             + "exit\n";
                su.getOutputStream().write(cmd.getBytes());
                if ((su.waitFor() != 0) || !device.canRead()
                    || !device.canWrite()) {
                    Log.e(TAG, "native open device error!");
                    return false;
                }
            } catch (Exception e) {
                Log.e(TAG, "native open device cause exception: " + e.getLocalizedMessage());
                return false;
            }
        }
    
        mFd = open(device.getAbsolutePath(),
                   baudrate.getBaudrate(),
                   stopbit.getStopBit(),
                   databit.getDataBit(),
                   parity.getParity(),
                   flowCon.getFlowCon());
        if (mFd == null) {
            Log.e(TAG, "native open returns null fd");
            return false;
        }
    
        mFileInputStream = new FileInputStream(mFd);
        mFileOutputStream = new FileOutputStream(mFd);
        return true;
    }
    

    open方法接受6个参数并返回一个结果值。设备打开成功,open方法将返回true。

源码下载

上述实现已经开源android_serialport

坚持原创技术分享,您的支持将鼓励我继续创作!