* Copyright (c) 2007-2008 by Michael Lotz
* Heavily based on the original usb_serial driver which is:
*
* Copyright (c) 2003 by Siarzhuk Zharski <imker@gmx.li>
* Distributed under the terms of the MIT License.
*
* Authors:
* Alexander von Gluck IV, kallisti5@unixzen.com
*/
#include <new>
#include "SerialDevice.h"
#include "USB3.h"
#include "ACM.h"
#include "FTDI.h"
#include "KLSI.h"
#include "Option.h"
#include "Prolific.h"
#include "Silicon.h"
#include "WinChipHead.h"
#include <sys/ioctl.h>
SerialDevice::SerialDevice(usb_device device, uint16 vendorID,
uint16 productID, const char *description)
: fDevice(device),
fVendorID(vendorID),
fProductID(productID),
fDescription(description),
fDeviceOpen(false),
fDeviceRemoved(false),
fControlPipe(0),
fReadPipe(0),
fWritePipe(0),
fBufferArea(-1),
fReadBuffer(NULL),
fReadBufferSize(ROUNDUP(DEF_BUFFER_SIZE, 16)),
fOutputBuffer(NULL),
fOutputBufferSize(ROUNDUP(DEF_BUFFER_SIZE, 16)),
fWriteBuffer(NULL),
fWriteBufferSize(ROUNDUP(DEF_BUFFER_SIZE, 16)),
fInterruptBuffer(NULL),
fInterruptBufferSize(16),
fDoneRead(-1),
fDoneWrite(-1),
fControlOut(0),
fInputStopped(false),
fMasterTTY(NULL),
fSlaveTTY(NULL),
fSystemTTYCookie(NULL),
fDeviceTTYCookie(NULL),
fInputThread(-1),
fStopThreads(false)
{
memset(&fTTYConfig, 0, sizeof(termios));
fTTYConfig.c_cflag = B9600 | CS8 | CREAD;
}
SerialDevice::~SerialDevice()
{
Removed();
if (fDoneRead >= 0)
delete_sem(fDoneRead);
if (fDoneWrite >= 0)
delete_sem(fDoneWrite);
if (fBufferArea >= 0)
delete_area(fBufferArea);
}
status_t
SerialDevice::Init()
{
fDoneRead = create_sem(0, "usb_serial:done_read");
if (fDoneRead < 0)
return fDoneRead;
fDoneWrite = create_sem(0, "usb_serial:done_write");
if (fDoneWrite < 0)
return fDoneWrite;
size_t totalBuffers = fReadBufferSize + fOutputBufferSize + fWriteBufferSize
+ fInterruptBufferSize;
fBufferArea = create_area("usb_serial:buffers_area", (void **)&fReadBuffer,
B_ANY_KERNEL_ADDRESS, ROUNDUP(totalBuffers, B_PAGE_SIZE), B_CONTIGUOUS,
B_KERNEL_READ_AREA | B_KERNEL_WRITE_AREA);
if (fBufferArea < 0)
return fBufferArea;
fOutputBuffer = fReadBuffer + fReadBufferSize;
fWriteBuffer = fOutputBuffer + fOutputBufferSize;
fInterruptBuffer = fWriteBuffer + fWriteBufferSize;
return B_OK;
}
void
SerialDevice::SetControlPipe(usb_pipe handle)
{
fControlPipe = handle;
}
void
SerialDevice::SetReadPipe(usb_pipe handle)
{
fReadPipe = handle;
}
void
SerialDevice::SetWritePipe(usb_pipe handle)
{
fWritePipe = handle;
}
inline int32
baud_index_to_speed(int index)
{
switch (index) {
case B0: return 0;
case B50: return 50;
case B75: return 75;
case B110: return 110;
case B134: return 134;
case B150: return 150;
case B200: return 200;
case B300: return 300;
case B600: return 600;
case B1200: return 1200;
case B1800: return 1800;
case B2400: return 2400;
case B4800: return 4800;
case B9600: return 9600;
case B19200: return 19200;
case B31250: return 31250;
case B38400: return 38400;
case B57600: return 57600;
case B115200: return 115200;
case B230400: return 230400;
}
TRACE_ALWAYS("invalid baud index %d\n", index);
return -1;
}
void
SerialDevice::SetModes(struct termios *tios)
{
TRACE_FUNCRES(trace_termios, tios);
uint8 baud = tios->c_cflag & CBAUD;
int32 speed;
if (baud == CBAUD) {
speed = tios->c_ospeed + (tios->c_ospeed_high << 16);
} else {
speed = baud_index_to_speed(baud);
}
memcpy(&fTTYConfig, tios, sizeof(termios));
fTTYConfig.c_cflag &= ~CBAUD;
fTTYConfig.c_cflag |= baud;
termios config;
memset(&config, 0, sizeof(termios));
config.c_cflag = tios->c_cflag;
config.c_cflag &= ~CBAUD;
config.c_cflag |= baud;
gTTYModule->tty_control(fDeviceTTYCookie, TCSETA, &config, sizeof(termios));
SetHardwareFlowControl((tios->c_cflag & CRTSCTS) != 0);
usb_cdc_line_coding lineCoding;
lineCoding.speed = speed;
lineCoding.stopbits = (tios->c_cflag & CSTOPB)
? USB_CDC_LINE_CODING_2_STOPBITS : USB_CDC_LINE_CODING_1_STOPBIT;
if (tios->c_cflag & PARENB) {
lineCoding.parity = USB_CDC_LINE_CODING_EVEN_PARITY;
if (tios->c_cflag & PARODD)
lineCoding.parity = USB_CDC_LINE_CODING_ODD_PARITY;
} else
lineCoding.parity = USB_CDC_LINE_CODING_NO_PARITY;
lineCoding.databits = (tios->c_cflag & CS8) ? 8 : 7;
if (memcmp(&lineCoding, &fLineCoding, sizeof(usb_cdc_line_coding)) != 0) {
fLineCoding.speed = lineCoding.speed;
fLineCoding.stopbits = lineCoding.stopbits;
fLineCoding.databits = lineCoding.databits;
fLineCoding.parity = lineCoding.parity;
TRACE("send to modem: speed %d sb: 0x%08x db: 0x%08x parity: 0x%08x\n",
fLineCoding.speed, fLineCoding.stopbits, fLineCoding.databits,
fLineCoding.parity);
SetLineCoding(&fLineCoding);
}
}
bool
SerialDevice::Service(struct tty *tty, uint32 op, void *buffer, size_t length)
{
if (!fDeviceOpen)
return false;
if (tty != fMasterTTY)
return false;
switch (op) {
case TTYENABLE:
{
bool enable = *(bool *)buffer;
TRACE("TTYENABLE: %sable\n", enable ? "en" : "dis");
gTTYModule->tty_hardware_signal(fSystemTTYCookie, TTYHWDCD, enable);
gTTYModule->tty_hardware_signal(fSystemTTYCookie, TTYHWCTS, enable);
fControlOut = enable ? USB_CDC_CONTROL_SIGNAL_STATE_DTR
| USB_CDC_CONTROL_SIGNAL_STATE_RTS : 0;
SetControlLineState(fControlOut);
return true;
}
case TTYISTOP:
fInputStopped = *(bool *)buffer;
TRACE("TTYISTOP: %sstopped\n", fInputStopped ? "" : "not ");
gTTYModule->tty_hardware_signal(fSystemTTYCookie, TTYHWCTS,
!fInputStopped);
return true;
case TTYGETSIGNALS:
TRACE("TTYGETSIGNALS\n");
gTTYModule->tty_hardware_signal(fSystemTTYCookie, TTYHWDCD,
(fControlOut & (USB_CDC_CONTROL_SIGNAL_STATE_DTR
| USB_CDC_CONTROL_SIGNAL_STATE_RTS)) != 0);
gTTYModule->tty_hardware_signal(fSystemTTYCookie, TTYHWCTS,
!fInputStopped);
gTTYModule->tty_hardware_signal(fSystemTTYCookie, TTYHWDSR, false);
gTTYModule->tty_hardware_signal(fSystemTTYCookie, TTYHWRI, false);
return true;
case TTYSETMODES:
TRACE("TTYSETMODES\n");
SetModes((struct termios *)buffer);
return true;
case TTYSETDTR:
case TTYSETRTS:
{
bool set = *(bool *)buffer;
uint8 bit = op == TTYSETDTR ? USB_CDC_CONTROL_SIGNAL_STATE_DTR
: USB_CDC_CONTROL_SIGNAL_STATE_RTS;
if (set)
fControlOut |= bit;
else
fControlOut &= ~bit;
SetControlLineState(fControlOut);
return true;
}
case TTYOSTART:
case TTYOSYNC:
case TTYSETBREAK:
case TTYFLUSH:
TRACE("TTY other\n");
return true;
}
return false;
}
status_t
SerialDevice::Open(uint32 flags)
{
status_t status = B_OK;
if (fDeviceOpen)
return B_BUSY;
if (fDeviceRemoved)
return B_DEV_NOT_READY;
status = gTTYModule->tty_create(usb_serial_service, NULL, &fMasterTTY);
if (status != B_OK) {
TRACE_ALWAYS("open: failed to init master tty\n");
return status;
}
status = gTTYModule->tty_create(usb_serial_service, fMasterTTY, &fSlaveTTY);
if (status != B_OK) {
TRACE_ALWAYS("open: failed to init slave tty\n");
gTTYModule->tty_destroy(fMasterTTY);
return status;
}
status = gTTYModule->tty_create_cookie(fMasterTTY, fSlaveTTY, O_RDWR, &fSystemTTYCookie);
if (status != B_OK) {
TRACE_ALWAYS("open: failed to init system tty cookie\n");
gTTYModule->tty_destroy(fMasterTTY);
gTTYModule->tty_destroy(fSlaveTTY);
return status;
}
status = gTTYModule->tty_create_cookie(fSlaveTTY, fMasterTTY, O_RDWR, &fDeviceTTYCookie);
if (status != B_OK) {
TRACE_ALWAYS("open: failed to init device tty cookie\n");
gTTYModule->tty_destroy_cookie(fSystemTTYCookie);
gTTYModule->tty_destroy(fMasterTTY);
gTTYModule->tty_destroy(fSlaveTTY);
return status;
}
ResetDevice();
fStopThreads = false;
fInputThread = spawn_kernel_thread(_InputThread,
"usb_serial input thread", B_NORMAL_PRIORITY, this);
if (fInputThread < 0) {
TRACE_ALWAYS("open: failed to spawn input thread\n");
return fInputThread;
}
resume_thread(fInputThread);
fControlOut = USB_CDC_CONTROL_SIGNAL_STATE_DTR
| USB_CDC_CONTROL_SIGNAL_STATE_RTS;
SetControlLineState(fControlOut);
status = gUSBModule->queue_interrupt(fControlPipe, fInterruptBuffer, fInterruptBufferSize,
_InterruptCallbackFunction, this);
if (status < B_OK)
TRACE_ALWAYS("failed to queue initial interrupt\n");
gTTYModule->tty_control(fSystemTTYCookie, TCSETA, &fTTYConfig,
sizeof(termios));
fDeviceOpen = true;
return B_OK;
}
status_t
SerialDevice::Read(char *buffer, size_t *numBytes)
{
if (fDeviceRemoved) {
*numBytes = 0;
return B_DEV_NOT_READY;
}
return gTTYModule->tty_read(fSystemTTYCookie, buffer, numBytes);
}
status_t
SerialDevice::Write(const char *buffer, size_t *numBytes)
{
if (fDeviceRemoved) {
*numBytes = 0;
return B_DEV_NOT_READY;
}
size_t bytesLeft = *numBytes;
*numBytes = 0;
while (bytesLeft > 0) {
size_t length = MIN(bytesLeft, 256);
status_t result = gTTYModule->tty_write(fSystemTTYCookie, buffer,
&length);
if (result != B_OK) {
TRACE_ALWAYS("failed to write to tty: %s\n", strerror(result));
return result;
}
buffer += length;
*numBytes += length;
bytesLeft -= length;
while (true) {
int readable = 0;
gTTYModule->tty_control(fDeviceTTYCookie, FIONREAD, &readable,
sizeof(readable));
if (readable == 0)
break;
result = _WriteToDevice();
if (result != B_OK) {
TRACE_ALWAYS("failed to write to device: %s\n",
strerror(result));
return result;
}
}
}
if (*numBytes > 0)
return B_OK;
return B_ERROR;
}
status_t
SerialDevice::Control(uint32 op, void *arg, size_t length)
{
if (fDeviceRemoved)
return B_DEV_NOT_READY;
return gTTYModule->tty_control(fSystemTTYCookie, op, arg, length);
}
status_t
SerialDevice::Select(uint8 event, uint32 ref, selectsync *sync)
{
if (fDeviceRemoved)
return B_DEV_NOT_READY;
return gTTYModule->tty_select(fSystemTTYCookie, event, ref, sync);
}
status_t
SerialDevice::DeSelect(uint8 event, selectsync *sync)
{
if (fDeviceRemoved)
return B_DEV_NOT_READY;
return gTTYModule->tty_deselect(fSystemTTYCookie, event, sync);
}
status_t
SerialDevice::Close()
{
OnClose();
fStopThreads = true;
fInputStopped = false;
fDeviceOpen = false;
if (!fDeviceRemoved) {
gUSBModule->cancel_queued_transfers(fReadPipe);
gUSBModule->cancel_queued_transfers(fWritePipe);
gUSBModule->cancel_queued_transfers(fControlPipe);
}
gTTYModule->tty_close_cookie(fSystemTTYCookie);
gTTYModule->tty_close_cookie(fDeviceTTYCookie);
int32 result = B_OK;
wait_for_thread(fInputThread, &result);
fInputThread = -1;
gTTYModule->tty_destroy_cookie(fSystemTTYCookie);
gTTYModule->tty_destroy_cookie(fDeviceTTYCookie);
gTTYModule->tty_destroy(fMasterTTY);
gTTYModule->tty_destroy(fSlaveTTY);
fMasterTTY = NULL;
fSlaveTTY = NULL;
fSystemTTYCookie = NULL;
fDeviceTTYCookie = NULL;
return B_OK;
}
status_t
SerialDevice::Free()
{
return B_OK;
}
void
SerialDevice::Removed()
{
if (fDeviceRemoved)
return;
fDeviceRemoved = true;
fStopThreads = true;
fInputStopped = false;
gUSBModule->cancel_queued_transfers(fReadPipe);
gUSBModule->cancel_queued_transfers(fWritePipe);
gUSBModule->cancel_queued_transfers(fControlPipe);
}
status_t
SerialDevice::AddDevice(const usb_configuration_info *config)
{
return B_ERROR;
}
status_t
SerialDevice::ResetDevice()
{
return B_OK;
}
status_t
SerialDevice::SetLineCoding(usb_cdc_line_coding *coding)
{
return B_NOT_SUPPORTED;
}
status_t
SerialDevice::SetControlLineState(uint16 state)
{
return B_NOT_SUPPORTED;
}
status_t
SerialDevice::SetHardwareFlowControl(bool enable)
{
return B_NOT_SUPPORTED;
}
void
SerialDevice::OnRead(char **buffer, size_t *numBytes)
{
}
void
SerialDevice::OnWrite(const char *buffer, size_t *numBytes, size_t *packetBytes)
{
memcpy(fWriteBuffer, buffer, *numBytes);
}
void
SerialDevice::OnClose()
{
}
int32
SerialDevice::_InputThread(void *data)
{
SerialDevice *device = (SerialDevice *)data;
while (!device->fStopThreads) {
status_t status = gUSBModule->queue_bulk(device->fReadPipe,
device->fReadBuffer, device->fReadBufferSize,
device->_ReadCallbackFunction, data);
if (status < B_OK) {
TRACE_ALWAYS("input thread: queueing failed with error: 0x%08x\n",
status);
return status;
}
status = acquire_sem_etc(device->fDoneRead, 1, B_CAN_INTERRUPT, 0);
if (status < B_OK) {
TRACE_ALWAYS("input thread: failed to get read done sem 0x%08x\n",
status);
return status;
}
if (device->fStatusRead != B_OK) {
TRACE("input thread: device status error 0x%08x\n",
device->fStatusRead);
if (device->fStatusRead == B_DEV_STALLED
&& gUSBModule->clear_feature(device->fReadPipe,
USB_FEATURE_ENDPOINT_HALT) != B_OK) {
TRACE_ALWAYS("input thread: failed to clear halt feature\n");
return B_ERROR;
}
continue;
}
char *buffer = device->fReadBuffer;
size_t readLength = device->fActualLengthRead;
device->OnRead(&buffer, &readLength);
if (readLength == 0)
continue;
while (device->fInputStopped)
snooze(100);
status = gTTYModule->tty_write(device->fDeviceTTYCookie, buffer,
&readLength);
if (status != B_OK) {
TRACE_ALWAYS("input thread: failed to write into TTY\n");
return status;
}
}
return B_OK;
}
status_t
SerialDevice::_WriteToDevice()
{
char *buffer = fOutputBuffer;
size_t bytesLeft = fOutputBufferSize;
status_t status = gTTYModule->tty_read(fDeviceTTYCookie, buffer,
&bytesLeft);
if (status != B_OK) {
TRACE_ALWAYS("write to device: failed to read from TTY: %s\n",
strerror(status));
return status;
}
while (!fDeviceRemoved && bytesLeft > 0) {
size_t length = MIN(bytesLeft, fWriteBufferSize);
size_t packetLength = length;
OnWrite(buffer, &length, &packetLength);
status = gUSBModule->queue_bulk(fWritePipe, fWriteBuffer, packetLength,
_WriteCallbackFunction, this);
if (status != B_OK) {
TRACE_ALWAYS("write to device: queueing failed with status "
"0x%08x\n", status);
return status;
}
status = acquire_sem_etc(fDoneWrite, 1, B_CAN_INTERRUPT, 0);
if (status != B_OK) {
TRACE_ALWAYS("write to device: failed to get write done sem "
"0x%08x\n", status);
return status;
}
if (fStatusWrite != B_OK) {
TRACE("write to device: device status error 0x%08x\n",
fStatusWrite);
if (fStatusWrite == B_DEV_STALLED) {
status = gUSBModule->clear_feature(fWritePipe,
USB_FEATURE_ENDPOINT_HALT);
if (status != B_OK) {
TRACE_ALWAYS("write to device: failed to clear device "
"halt\n");
return B_ERROR;
}
}
continue;
}
buffer += length;
bytesLeft -= length;
}
return B_OK;
}
void
SerialDevice::_ReadCallbackFunction(void *cookie, status_t status, void *data,
size_t actualLength)
{
TRACE_FUNCALLS("read callback: cookie: 0x%08x status: 0x%08x data: 0x%08x "
"length: %lu\n", cookie, status, data, actualLength);
SerialDevice *device = (SerialDevice *)cookie;
device->fActualLengthRead = actualLength;
device->fStatusRead = status;
release_sem_etc(device->fDoneRead, 1, B_DO_NOT_RESCHEDULE);
}
void
SerialDevice::_WriteCallbackFunction(void *cookie, status_t status, void *data,
size_t actualLength)
{
TRACE_FUNCALLS("write callback: cookie: 0x%08x status: 0x%08x data: 0x%08x "
"length: %lu\n", cookie, status, data, actualLength);
SerialDevice *device = (SerialDevice *)cookie;
device->fActualLengthWrite = actualLength;
device->fStatusWrite = status;
release_sem_etc(device->fDoneWrite, 1, B_DO_NOT_RESCHEDULE);
}
void
SerialDevice::_InterruptCallbackFunction(void *cookie, status_t status,
void *data, size_t actualLength)
{
TRACE_FUNCALLS("interrupt callback: cookie: 0x%08x status: 0x%08x data: "
"0x%08x len: %lu\n", cookie, status, data, actualLength);
SerialDevice *device = (SerialDevice *)cookie;
device->fActualLengthInterrupt = actualLength;
device->fStatusInterrupt = status;
if (status == B_OK && !device->fDeviceRemoved) {
status = gUSBModule->queue_interrupt(device->fControlPipe,
device->fInterruptBuffer, device->fInterruptBufferSize,
device->_InterruptCallbackFunction, device);
}
}
SerialDevice *
SerialDevice::MakeDevice(usb_device device, uint16 vendorID,
uint16 productID)
{
for (uint32 i = 0; i < sizeof(kFTDIDevices)
/ sizeof(kFTDIDevices[0]); i++) {
if (vendorID == kFTDIDevices[i].vendorID
&& productID == kFTDIDevices[i].productID) {
return new(std::nothrow) FTDIDevice(device, vendorID, productID,
kFTDIDevices[i].deviceName);
}
}
for (uint32 i = 0; i < sizeof(kKLSIDevices)
/ sizeof(kKLSIDevices[0]); i++) {
if (vendorID == kKLSIDevices[i].vendorID
&& productID == kKLSIDevices[i].productID) {
return new(std::nothrow) KLSIDevice(device, vendorID, productID,
kKLSIDevices[i].deviceName);
}
}
for (uint32 i = 0; i < sizeof(kProlificDevices)
/ sizeof(kProlificDevices[0]); i++) {
if (vendorID == kProlificDevices[i].vendorID
&& productID == kProlificDevices[i].productID) {
return new(std::nothrow) ProlificDevice(device, vendorID, productID,
kProlificDevices[i].deviceName);
}
}
for (uint32 i = 0; i < sizeof(kSiliconDevices)
/ sizeof(kSiliconDevices[0]); i++) {
if (vendorID == kSiliconDevices[i].vendorID
&& productID == kSiliconDevices[i].productID) {
return new(std::nothrow) SiliconDevice(device, vendorID, productID,
kSiliconDevices[i].deviceName);
}
}
for (uint32 i = 0; i < sizeof(kWCHDevices)
/ sizeof(kWCHDevices[0]); i++) {
if (vendorID == kWCHDevices[i].vendorID
&& productID == kWCHDevices[i].productID) {
return new(std::nothrow) WCHDevice(device, vendorID, productID,
kWCHDevices[i].deviceName);
}
}
for (uint32 i = 0; i < sizeof(kOptionDevices)
/ sizeof(kOptionDevices[0]); i++) {
if (vendorID == kOptionDevices[i].vendorID
&& productID == kOptionDevices[i].productID) {
return new(std::nothrow) OptionDevice(device, vendorID, productID,
kOptionDevices[i].deviceName);
}
}
return new(std::nothrow) ACMDevice(device, vendorID, productID,
"CDC ACM compatible device");
}