/* Copyright 2011-2013 Google Inc. * Copyright 2013 mike wakerly * Copyright 2020 kai morich * * Project home page: https://github.com/mik3y/usb-serial-for-android */ package com.hoho.android.usbserial.driver; import android.hardware.usb.UsbConstants; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; import android.util.Log; import com.hoho.android.usbserial.util.MonotonicClock; import java.io.IOException; import java.util.ArrayList; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /* * driver is implemented from various information scattered over FTDI documentation * * baud rate calculation https://www.ftdichip.com/Support/Documents/AppNotes/AN232B-05_BaudRates.pdf * control bits https://www.ftdichip.com/Firmware/Precompiled/UM_VinculumFirmware_V205.pdf * device type https://www.ftdichip.com/Support/Documents/AppNotes/AN_233_Java_D2XX_for_Android_API_User_Manual.pdf -> bvdDevice * */ public class FtdiSerialDriver implements UsbSerialDriver { private static final String TAG = FtdiSerialPort.class.getSimpleName(); private final UsbDevice mDevice; private final List mPorts; public FtdiSerialDriver(UsbDevice device) { mDevice = device; mPorts = new ArrayList<>(); for( int port = 0; port < device.getInterfaceCount(); port++) { mPorts.add(new FtdiSerialPort(mDevice, port)); } } @Override public UsbDevice getDevice() { return mDevice; } @Override public List getPorts() { return mPorts; } public class FtdiSerialPort extends CommonUsbSerialPort { private static final int USB_WRITE_TIMEOUT_MILLIS = 5000; private static final int READ_HEADER_LENGTH = 2; // contains MODEM_STATUS private static final int REQTYPE_HOST_TO_DEVICE = UsbConstants.USB_TYPE_VENDOR | UsbConstants.USB_DIR_OUT; private static final int REQTYPE_DEVICE_TO_HOST = UsbConstants.USB_TYPE_VENDOR | UsbConstants.USB_DIR_IN; private static final int RESET_REQUEST = 0; private static final int MODEM_CONTROL_REQUEST = 1; private static final int SET_BAUD_RATE_REQUEST = 3; private static final int SET_DATA_REQUEST = 4; private static final int GET_MODEM_STATUS_REQUEST = 5; private static final int SET_LATENCY_TIMER_REQUEST = 9; private static final int GET_LATENCY_TIMER_REQUEST = 10; private static final int MODEM_CONTROL_DTR_ENABLE = 0x0101; private static final int MODEM_CONTROL_DTR_DISABLE = 0x0100; private static final int MODEM_CONTROL_RTS_ENABLE = 0x0202; private static final int MODEM_CONTROL_RTS_DISABLE = 0x0200; private static final int MODEM_STATUS_CTS = 0x10; private static final int MODEM_STATUS_DSR = 0x20; private static final int MODEM_STATUS_RI = 0x40; private static final int MODEM_STATUS_CD = 0x80; private static final int RESET_ALL = 0; private static final int RESET_PURGE_RX = 1; private static final int RESET_PURGE_TX = 2; private boolean baudRateWithPort = false; private boolean dtr = false; private boolean rts = false; private int breakConfig = 0; public FtdiSerialPort(UsbDevice device, int portNumber) { super(device, portNumber); } @Override public UsbSerialDriver getDriver() { return FtdiSerialDriver.this; } @Override protected void openInt(UsbDeviceConnection connection) throws IOException { if (!connection.claimInterface(mDevice.getInterface(mPortNumber), true)) { throw new IOException("Could not claim interface " + mPortNumber); } if (mDevice.getInterface(mPortNumber).getEndpointCount() < 2) { throw new IOException("Not enough endpoints"); } mReadEndpoint = mDevice.getInterface(mPortNumber).getEndpoint(0); mWriteEndpoint = mDevice.getInterface(mPortNumber).getEndpoint(1); int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, RESET_REQUEST, RESET_ALL, mPortNumber+1, null, 0, USB_WRITE_TIMEOUT_MILLIS); if (result != 0) { throw new IOException("Reset failed: result=" + result); } result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, MODEM_CONTROL_REQUEST, (dtr ? MODEM_CONTROL_DTR_ENABLE : MODEM_CONTROL_DTR_DISABLE) | (rts ? MODEM_CONTROL_RTS_ENABLE : MODEM_CONTROL_RTS_DISABLE), mPortNumber+1, null, 0, USB_WRITE_TIMEOUT_MILLIS); if (result != 0) { throw new IOException("Init RTS,DTR failed: result=" + result); } // mDevice.getVersion() would require API 23 byte[] rawDescriptors = connection.getRawDescriptors(); if(rawDescriptors == null || rawDescriptors.length < 14) { throw new IOException("Could not get device descriptors"); } int deviceType = rawDescriptors[13]; baudRateWithPort = deviceType == 7 || deviceType == 8 || deviceType == 9; // ...H devices } @Override protected void closeInt() { try { mConnection.releaseInterface(mDevice.getInterface(mPortNumber)); } catch(Exception ignored) {} } @Override public int read(final byte[] dest, final int timeout) throws IOException { if(dest.length <= READ_HEADER_LENGTH) { throw new IllegalArgumentException("Read buffer to small"); // could allocate larger buffer, including space for 2 header bytes, but this would // result in buffers not being 64 byte aligned any more, causing data loss at continuous // data transfer at high baud rates when buffers are fully filled. } int nread; if (timeout != 0) { long endTime = MonotonicClock.millis() + timeout; do { nread = super.read(dest, Math.max(1, (int)(endTime - MonotonicClock.millis())), false); } while (nread == READ_HEADER_LENGTH && MonotonicClock.millis() < endTime); if(nread <= 0 && MonotonicClock.millis() < endTime) testConnection(); } else { do { nread = super.read(dest, timeout, false); } while (nread == READ_HEADER_LENGTH); } return readFilter(dest, nread); } protected int readFilter(byte[] buffer, int totalBytesRead) throws IOException { final int maxPacketSize = mReadEndpoint.getMaxPacketSize(); int destPos = 0; for(int srcPos = 0; srcPos < totalBytesRead; srcPos += maxPacketSize) { int length = Math.min(srcPos + maxPacketSize, totalBytesRead) - (srcPos + READ_HEADER_LENGTH); if (length < 0) throw new IOException("Expected at least " + READ_HEADER_LENGTH + " bytes"); System.arraycopy(buffer, srcPos + READ_HEADER_LENGTH, buffer, destPos, length); destPos += length; } //Log.d(TAG, "read filter " + totalBytesRead + " -> " + destPos); return destPos; } private void setBaudrate(int baudRate) throws IOException { int divisor, subdivisor, effectiveBaudRate; if (baudRate > 3500000) { throw new UnsupportedOperationException("Baud rate to high"); } else if(baudRate >= 2500000) { divisor = 0; subdivisor = 0; effectiveBaudRate = 3000000; } else if(baudRate >= 1750000) { divisor = 1; subdivisor = 0; effectiveBaudRate = 2000000; } else { divisor = (24000000 << 1) / baudRate; divisor = (divisor + 1) >> 1; // round subdivisor = divisor & 0x07; divisor >>= 3; if (divisor > 0x3fff) // exceeds bit 13 at 183 baud throw new UnsupportedOperationException("Baud rate to low"); effectiveBaudRate = (24000000 << 1) / ((divisor << 3) + subdivisor); effectiveBaudRate = (effectiveBaudRate +1) >> 1; } double baudRateError = Math.abs(1.0 - (effectiveBaudRate / (double)baudRate)); if(baudRateError >= 0.031) // can happen only > 1.5Mbaud throw new UnsupportedOperationException(String.format("Baud rate deviation %.1f%% is higher than allowed 3%%", baudRateError*100)); int value = divisor; int index = 0; switch(subdivisor) { case 0: break; // 16,15,14 = 000 - sub-integer divisor = 0 case 4: value |= 0x4000; break; // 16,15,14 = 001 - sub-integer divisor = 0.5 case 2: value |= 0x8000; break; // 16,15,14 = 010 - sub-integer divisor = 0.25 case 1: value |= 0xc000; break; // 16,15,14 = 011 - sub-integer divisor = 0.125 case 3: value |= 0x0000; index |= 1; break; // 16,15,14 = 100 - sub-integer divisor = 0.375 case 5: value |= 0x4000; index |= 1; break; // 16,15,14 = 101 - sub-integer divisor = 0.625 case 6: value |= 0x8000; index |= 1; break; // 16,15,14 = 110 - sub-integer divisor = 0.75 case 7: value |= 0xc000; index |= 1; break; // 16,15,14 = 111 - sub-integer divisor = 0.875 } if(baudRateWithPort) { index <<= 8; index |= mPortNumber+1; } Log.d(TAG, String.format("baud rate=%d, effective=%d, error=%.1f%%, value=0x%04x, index=0x%04x, divisor=%d, subdivisor=%d", baudRate, effectiveBaudRate, baudRateError*100, value, index, divisor, subdivisor)); int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, SET_BAUD_RATE_REQUEST, value, index, null, 0, USB_WRITE_TIMEOUT_MILLIS); if (result != 0) { throw new IOException("Setting baudrate failed: result=" + result); } } @Override public void setParameters(int baudRate, int dataBits, int stopBits, @Parity int parity) throws IOException { if(baudRate <= 0) { throw new IllegalArgumentException("Invalid baud rate: " + baudRate); } setBaudrate(baudRate); int config = 0; switch (dataBits) { case DATABITS_5: case DATABITS_6: throw new UnsupportedOperationException("Unsupported data bits: " + dataBits); case DATABITS_7: case DATABITS_8: config |= dataBits; break; default: throw new IllegalArgumentException("Invalid data bits: " + dataBits); } switch (parity) { case PARITY_NONE: break; case PARITY_ODD: config |= 0x100; break; case PARITY_EVEN: config |= 0x200; break; case PARITY_MARK: config |= 0x300; break; case PARITY_SPACE: config |= 0x400; break; default: throw new IllegalArgumentException("Invalid parity: " + parity); } switch (stopBits) { case STOPBITS_1: break; case STOPBITS_1_5: throw new UnsupportedOperationException("Unsupported stop bits: 1.5"); case STOPBITS_2: config |= 0x1000; break; default: throw new IllegalArgumentException("Invalid stop bits: " + stopBits); } int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, SET_DATA_REQUEST, config, mPortNumber+1,null, 0, USB_WRITE_TIMEOUT_MILLIS); if (result != 0) { throw new IOException("Setting parameters failed: result=" + result); } breakConfig = config; } private int getStatus() throws IOException { byte[] data = new byte[2]; int result = mConnection.controlTransfer(REQTYPE_DEVICE_TO_HOST, GET_MODEM_STATUS_REQUEST, 0, mPortNumber+1, data, data.length, USB_WRITE_TIMEOUT_MILLIS); if (result != 2) { throw new IOException("Get modem status failed: result=" + result); } return data[0]; } @Override public boolean getCD() throws IOException { return (getStatus() & MODEM_STATUS_CD) != 0; } @Override public boolean getCTS() throws IOException { return (getStatus() & MODEM_STATUS_CTS) != 0; } @Override public boolean getDSR() throws IOException { return (getStatus() & MODEM_STATUS_DSR) != 0; } @Override public boolean getDTR() throws IOException { return dtr; } @Override public void setDTR(boolean value) throws IOException { int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, MODEM_CONTROL_REQUEST, value ? MODEM_CONTROL_DTR_ENABLE : MODEM_CONTROL_DTR_DISABLE, mPortNumber+1, null, 0, USB_WRITE_TIMEOUT_MILLIS); if (result != 0) { throw new IOException("Set DTR failed: result=" + result); } dtr = value; } @Override public boolean getRI() throws IOException { return (getStatus() & MODEM_STATUS_RI) != 0; } @Override public boolean getRTS() throws IOException { return rts; } @Override public void setRTS(boolean value) throws IOException { int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, MODEM_CONTROL_REQUEST, value ? MODEM_CONTROL_RTS_ENABLE : MODEM_CONTROL_RTS_DISABLE, mPortNumber+1, null, 0, USB_WRITE_TIMEOUT_MILLIS); if (result != 0) { throw new IOException("Set DTR failed: result=" + result); } rts = value; } @Override public EnumSet getControlLines() throws IOException { int status = getStatus(); EnumSet set = EnumSet.noneOf(ControlLine.class); if(rts) set.add(ControlLine.RTS); if((status & MODEM_STATUS_CTS) != 0) set.add(ControlLine.CTS); if(dtr) set.add(ControlLine.DTR); if((status & MODEM_STATUS_DSR) != 0) set.add(ControlLine.DSR); if((status & MODEM_STATUS_CD) != 0) set.add(ControlLine.CD); if((status & MODEM_STATUS_RI) != 0) set.add(ControlLine.RI); return set; } @Override public EnumSet getSupportedControlLines() throws IOException { return EnumSet.allOf(ControlLine.class); } @Override public void purgeHwBuffers(boolean purgeWriteBuffers, boolean purgeReadBuffers) throws IOException { if (purgeWriteBuffers) { int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, RESET_REQUEST, RESET_PURGE_RX, mPortNumber+1, null, 0, USB_WRITE_TIMEOUT_MILLIS); if (result != 0) { throw new IOException("Purge write buffer failed: result=" + result); } } if (purgeReadBuffers) { int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, RESET_REQUEST, RESET_PURGE_TX, mPortNumber+1, null, 0, USB_WRITE_TIMEOUT_MILLIS); if (result != 0) { throw new IOException("Purge read buffer failed: result=" + result); } } } @Override public void setBreak(boolean value) throws IOException { int config = breakConfig; if(value) config |= 0x4000; int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, SET_DATA_REQUEST, config, mPortNumber+1,null, 0, USB_WRITE_TIMEOUT_MILLIS); if (result != 0) { throw new IOException("Setting BREAK failed: result=" + result); } } public void setLatencyTimer(int latencyTime) throws IOException { int result = mConnection.controlTransfer(REQTYPE_HOST_TO_DEVICE, SET_LATENCY_TIMER_REQUEST, latencyTime, mPortNumber+1, null, 0, USB_WRITE_TIMEOUT_MILLIS); if (result != 0) { throw new IOException("Set latency timer failed: result=" + result); } } public int getLatencyTimer() throws IOException { byte[] data = new byte[1]; int result = mConnection.controlTransfer(REQTYPE_DEVICE_TO_HOST, GET_LATENCY_TIMER_REQUEST, 0, mPortNumber+1, data, data.length, USB_WRITE_TIMEOUT_MILLIS); if (result != 1) { throw new IOException("Get latency timer failed: result=" + result); } return data[0]; } } public static Map getSupportedDevices() { final Map supportedDevices = new LinkedHashMap(); supportedDevices.put(UsbId.VENDOR_FTDI, new int[] { UsbId.FTDI_FT232R, UsbId.FTDI_FT232H, UsbId.FTDI_FT2232H, UsbId.FTDI_FT4232H, UsbId.FTDI_FT231X, // same ID for FT230X, FT231X, FT234XD }); return supportedDevices; } }