| /* Serial interface for local (hardwired) serial ports on Windows systems |
| |
| Copyright (C) 2006 |
| Free Software Foundation, Inc. |
| |
| This file is part of GDB. |
| |
| This program is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation; either version 2 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with this program; if not, write to the Free Software |
| Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| Boston, MA 02110-1301, USA. */ |
| |
| #include "defs.h" |
| #include "serial.h" |
| #include "ser-base.h" |
| #include "ser-tcp.h" |
| |
| #include <windows.h> |
| |
| #include <fcntl.h> |
| #include <unistd.h> |
| #include <sys/types.h> |
| |
| #include "gdb_assert.h" |
| #include "gdb_string.h" |
| |
| void _initialize_ser_windows (void); |
| |
| struct ser_windows_state |
| { |
| int in_progress; |
| OVERLAPPED ov; |
| DWORD lastCommMask; |
| HANDLE except_event; |
| }; |
| |
| /* Open up a real live device for serial I/O. */ |
| |
| static int |
| ser_windows_open (struct serial *scb, const char *name) |
| { |
| HANDLE h; |
| struct ser_windows_state *state; |
| COMMTIMEOUTS timeouts; |
| |
| /* Only allow COM ports. */ |
| if (strncmp (name, "COM", 3) != 0) |
| { |
| errno = ENOENT; |
| return -1; |
| } |
| |
| h = CreateFile (name, GENERIC_READ | GENERIC_WRITE, 0, NULL, |
| OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); |
| if (h == INVALID_HANDLE_VALUE) |
| { |
| errno = ENOENT; |
| return -1; |
| } |
| |
| scb->fd = _open_osfhandle ((long) h, O_RDWR); |
| if (scb->fd < 0) |
| { |
| errno = ENOENT; |
| return -1; |
| } |
| |
| if (!SetCommMask (h, EV_RXCHAR)) |
| { |
| errno = EINVAL; |
| return -1; |
| } |
| |
| timeouts.ReadIntervalTimeout = MAXDWORD; |
| timeouts.ReadTotalTimeoutConstant = 0; |
| timeouts.ReadTotalTimeoutMultiplier = 0; |
| timeouts.WriteTotalTimeoutConstant = 0; |
| timeouts.WriteTotalTimeoutMultiplier = 0; |
| if (!SetCommTimeouts (h, &timeouts)) |
| { |
| errno = EINVAL; |
| return -1; |
| } |
| |
| state = xmalloc (sizeof (struct ser_windows_state)); |
| memset (state, 0, sizeof (struct ser_windows_state)); |
| scb->state = state; |
| |
| /* Create a manual reset event to watch the input buffer. */ |
| state->ov.hEvent = CreateEvent (0, TRUE, FALSE, 0); |
| |
| /* Create a (currently unused) handle to record exceptions. */ |
| state->except_event = CreateEvent (0, TRUE, FALSE, 0); |
| |
| return 0; |
| } |
| |
| /* Wait for the output to drain away, as opposed to flushing (discarding) |
| it. */ |
| |
| static int |
| ser_windows_drain_output (struct serial *scb) |
| { |
| HANDLE h = (HANDLE) _get_osfhandle (scb->fd); |
| |
| return (FlushFileBuffers (h) != 0) ? 0 : -1; |
| } |
| |
| static int |
| ser_windows_flush_output (struct serial *scb) |
| { |
| HANDLE h = (HANDLE) _get_osfhandle (scb->fd); |
| |
| return (PurgeComm (h, PURGE_TXCLEAR) != 0) ? 0 : -1; |
| } |
| |
| static int |
| ser_windows_flush_input (struct serial *scb) |
| { |
| HANDLE h = (HANDLE) _get_osfhandle (scb->fd); |
| |
| return (PurgeComm (h, PURGE_RXCLEAR) != 0) ? 0 : -1; |
| } |
| |
| static int |
| ser_windows_send_break (struct serial *scb) |
| { |
| HANDLE h = (HANDLE) _get_osfhandle (scb->fd); |
| |
| if (SetCommBreak (h) == 0) |
| return -1; |
| |
| /* Delay for 250 milliseconds. */ |
| Sleep (250); |
| |
| if (ClearCommBreak (h)) |
| return -1; |
| |
| return 0; |
| } |
| |
| static void |
| ser_windows_raw (struct serial *scb) |
| { |
| HANDLE h = (HANDLE) _get_osfhandle (scb->fd); |
| DCB state; |
| |
| if (GetCommState (h, &state) == 0) |
| return; |
| |
| state.fParity = FALSE; |
| state.fOutxCtsFlow = FALSE; |
| state.fOutxDsrFlow = FALSE; |
| state.fDtrControl = DTR_CONTROL_ENABLE; |
| state.fDsrSensitivity = FALSE; |
| state.fOutX = FALSE; |
| state.fInX = FALSE; |
| state.fNull = FALSE; |
| state.fAbortOnError = FALSE; |
| state.ByteSize = 8; |
| state.Parity = NOPARITY; |
| |
| scb->current_timeout = 0; |
| |
| if (SetCommState (h, &state) == 0) |
| warning (_("SetCommState failed\n")); |
| } |
| |
| static int |
| ser_windows_setstopbits (struct serial *scb, int num) |
| { |
| HANDLE h = (HANDLE) _get_osfhandle (scb->fd); |
| DCB state; |
| |
| if (GetCommState (h, &state) == 0) |
| return -1; |
| |
| switch (num) |
| { |
| case SERIAL_1_STOPBITS: |
| state.StopBits = ONESTOPBIT; |
| break; |
| case SERIAL_1_AND_A_HALF_STOPBITS: |
| state.StopBits = ONE5STOPBITS; |
| break; |
| case SERIAL_2_STOPBITS: |
| state.StopBits = TWOSTOPBITS; |
| break; |
| default: |
| return 1; |
| } |
| |
| return (SetCommState (h, &state) != 0) ? 0 : -1; |
| } |
| |
| static int |
| ser_windows_setbaudrate (struct serial *scb, int rate) |
| { |
| HANDLE h = (HANDLE) _get_osfhandle (scb->fd); |
| DCB state; |
| |
| if (GetCommState (h, &state) == 0) |
| return -1; |
| |
| state.BaudRate = rate; |
| |
| return (SetCommState (h, &state) != 0) ? 0 : -1; |
| } |
| |
| static void |
| ser_windows_close (struct serial *scb) |
| { |
| struct ser_windows_state *state; |
| |
| /* Stop any pending selects. */ |
| CancelIo ((HANDLE) _get_osfhandle (scb->fd)); |
| state = scb->state; |
| CloseHandle (state->ov.hEvent); |
| CloseHandle (state->except_event); |
| |
| if (scb->fd < 0) |
| return; |
| |
| close (scb->fd); |
| scb->fd = -1; |
| |
| xfree (scb->state); |
| } |
| |
| static void |
| ser_windows_wait_handle (struct serial *scb, HANDLE *read, HANDLE *except) |
| { |
| struct ser_windows_state *state; |
| COMSTAT status; |
| DWORD errors; |
| HANDLE h = (HANDLE) _get_osfhandle (scb->fd); |
| |
| state = scb->state; |
| |
| *except = state->except_event; |
| *read = state->ov.hEvent; |
| |
| if (state->in_progress) |
| return; |
| |
| /* Reset the mask - we are only interested in any characters which |
| arrive after this point, not characters which might have arrived |
| and already been read. */ |
| |
| /* This really, really shouldn't be necessary - just the second one. |
| But otherwise an internal flag for EV_RXCHAR does not get |
| cleared, and we get a duplicated event, if the last batch |
| of characters included at least two arriving close together. */ |
| if (!SetCommMask (h, 0)) |
| warning (_("ser_windows_wait_handle: reseting mask failed")); |
| |
| if (!SetCommMask (h, EV_RXCHAR)) |
| warning (_("ser_windows_wait_handle: reseting mask failed (2)")); |
| |
| /* There's a potential race condition here; we must check cbInQue |
| and not wait if that's nonzero. */ |
| |
| ClearCommError (h, &errors, &status); |
| if (status.cbInQue > 0) |
| { |
| SetEvent (state->ov.hEvent); |
| return; |
| } |
| |
| state->in_progress = 1; |
| ResetEvent (state->ov.hEvent); |
| state->lastCommMask = -2; |
| if (WaitCommEvent (h, &state->lastCommMask, &state->ov)) |
| { |
| gdb_assert (state->lastCommMask & EV_RXCHAR); |
| SetEvent (state->ov.hEvent); |
| } |
| else |
| gdb_assert (GetLastError () == ERROR_IO_PENDING); |
| } |
| |
| static int |
| ser_windows_read_prim (struct serial *scb, size_t count) |
| { |
| struct ser_windows_state *state; |
| OVERLAPPED ov; |
| DWORD bytes_read, bytes_read_tmp; |
| HANDLE h; |
| gdb_byte *p; |
| |
| state = scb->state; |
| if (state->in_progress) |
| { |
| WaitForSingleObject (state->ov.hEvent, INFINITE); |
| state->in_progress = 0; |
| ResetEvent (state->ov.hEvent); |
| } |
| |
| memset (&ov, 0, sizeof (OVERLAPPED)); |
| ov.hEvent = CreateEvent (0, FALSE, FALSE, 0); |
| h = (HANDLE) _get_osfhandle (scb->fd); |
| |
| if (!ReadFile (h, scb->buf, /* count */ 1, &bytes_read, &ov)) |
| { |
| if (GetLastError () != ERROR_IO_PENDING |
| || !GetOverlappedResult (h, &ov, &bytes_read, TRUE)) |
| bytes_read = -1; |
| } |
| |
| CloseHandle (ov.hEvent); |
| return bytes_read; |
| } |
| |
| static int |
| ser_windows_write_prim (struct serial *scb, const void *buf, size_t len) |
| { |
| struct ser_windows_state *state; |
| OVERLAPPED ov; |
| DWORD bytes_written; |
| HANDLE h; |
| |
| memset (&ov, 0, sizeof (OVERLAPPED)); |
| ov.hEvent = CreateEvent (0, FALSE, FALSE, 0); |
| h = (HANDLE) _get_osfhandle (scb->fd); |
| if (!WriteFile (h, buf, len, &bytes_written, &ov)) |
| { |
| if (GetLastError () != ERROR_IO_PENDING |
| || !GetOverlappedResult (h, &ov, &bytes_written, TRUE)) |
| bytes_written = -1; |
| } |
| |
| CloseHandle (ov.hEvent); |
| return bytes_written; |
| } |
| |
| struct ser_console_state |
| { |
| HANDLE read_event; |
| HANDLE except_event; |
| |
| HANDLE start_select; |
| HANDLE stop_select; |
| }; |
| |
| static DWORD WINAPI |
| console_select_thread (void *arg) |
| { |
| struct serial *scb = arg; |
| struct ser_console_state *state, state_copy; |
| int event_index, fd; |
| HANDLE h; |
| |
| /* Copy useful information out of the control block, to make sure |
| that we do not race with freeing it. */ |
| state_copy = *(struct ser_console_state *) scb->state; |
| state = &state_copy; |
| fd = scb->fd; |
| |
| h = (HANDLE) _get_osfhandle (fd); |
| |
| while (1) |
| { |
| HANDLE wait_events[2]; |
| INPUT_RECORD record; |
| DWORD n_records; |
| |
| wait_events[0] = state->start_select; |
| wait_events[1] = state->stop_select; |
| |
| if (WaitForMultipleObjects (2, wait_events, FALSE, INFINITE) != WAIT_OBJECT_0) |
| { |
| CloseHandle (state->stop_select); |
| return 0; |
| } |
| |
| retry: |
| wait_events[0] = state->stop_select; |
| wait_events[1] = h; |
| |
| event_index = WaitForMultipleObjects (2, wait_events, FALSE, INFINITE); |
| |
| if (event_index == WAIT_OBJECT_0 |
| || WaitForSingleObject (state->stop_select, 0) == WAIT_OBJECT_0) |
| { |
| CloseHandle (state->stop_select); |
| return 0; |
| } |
| |
| if (event_index != WAIT_OBJECT_0 + 1) |
| { |
| /* Wait must have failed; assume an error has occured, e.g. |
| the handle has been closed. */ |
| SetEvent (state->except_event); |
| continue; |
| } |
| |
| /* We've got a pending event on the console. See if it's |
| of interest. */ |
| if (!PeekConsoleInput (h, &record, 1, &n_records) || n_records != 1) |
| { |
| /* Something went wrong. Maybe the console is gone. */ |
| SetEvent (state->except_event); |
| continue; |
| } |
| |
| if (record.EventType == KEY_EVENT && record.Event.KeyEvent.bKeyDown) |
| { |
| /* This is really a keypress. */ |
| SetEvent (state->read_event); |
| continue; |
| } |
| |
| /* Otherwise discard it and wait again. */ |
| ReadConsoleInput (h, &record, 1, &n_records); |
| goto retry; |
| } |
| } |
| |
| static int |
| fd_is_pipe (int fd) |
| { |
| if (PeekNamedPipe ((HANDLE) _get_osfhandle (fd), NULL, 0, NULL, NULL, NULL)) |
| return 1; |
| else |
| return 0; |
| } |
| |
| static DWORD WINAPI |
| pipe_select_thread (void *arg) |
| { |
| struct serial *scb = arg; |
| struct ser_console_state *state, state_copy; |
| int event_index, fd; |
| HANDLE h; |
| |
| /* Copy useful information out of the control block, to make sure |
| that we do not race with freeing it. */ |
| state_copy = *(struct ser_console_state *) scb->state; |
| state = &state_copy; |
| fd = scb->fd; |
| |
| h = (HANDLE) _get_osfhandle (fd); |
| |
| while (1) |
| { |
| HANDLE wait_events[2]; |
| DWORD n_avail; |
| |
| wait_events[0] = state->start_select; |
| wait_events[1] = state->stop_select; |
| |
| if (WaitForMultipleObjects (2, wait_events, FALSE, INFINITE) != WAIT_OBJECT_0) |
| { |
| CloseHandle (state->stop_select); |
| return 0; |
| } |
| |
| retry: |
| if (!PeekNamedPipe (h, NULL, 0, NULL, &n_avail, NULL)) |
| { |
| SetEvent (state->except_event); |
| continue; |
| } |
| |
| if (n_avail > 0) |
| { |
| SetEvent (state->read_event); |
| continue; |
| } |
| |
| if (WaitForSingleObject (state->stop_select, 0) == WAIT_OBJECT_0) |
| { |
| CloseHandle (state->stop_select); |
| return 0; |
| } |
| |
| Sleep (10); |
| goto retry; |
| } |
| } |
| |
| static void |
| ser_console_wait_handle (struct serial *scb, HANDLE *read, HANDLE *except) |
| { |
| struct ser_console_state *state = scb->state; |
| |
| if (state == NULL) |
| { |
| DWORD threadId; |
| int is_tty; |
| |
| is_tty = isatty (scb->fd); |
| if (!is_tty && !fd_is_pipe (scb->fd)) |
| { |
| *read = NULL; |
| *except = NULL; |
| return; |
| } |
| |
| state = xmalloc (sizeof (struct ser_console_state)); |
| memset (state, 0, sizeof (struct ser_console_state)); |
| scb->state = state; |
| |
| /* Create auto reset events to wake and terminate the select thread. */ |
| state->start_select = CreateEvent (0, FALSE, FALSE, 0); |
| state->stop_select = CreateEvent (0, FALSE, FALSE, 0); |
| |
| /* Create our own events to report read and exceptions separately. |
| The exception event is currently never used. */ |
| state->read_event = CreateEvent (0, FALSE, FALSE, 0); |
| state->except_event = CreateEvent (0, FALSE, FALSE, 0); |
| |
| /* And finally start the select thread. */ |
| if (is_tty) |
| CreateThread (NULL, 0, console_select_thread, scb, 0, &threadId); |
| else |
| CreateThread (NULL, 0, pipe_select_thread, scb, 0, &threadId); |
| } |
| |
| ResetEvent (state->read_event); |
| ResetEvent (state->except_event); |
| |
| SetEvent (state->start_select); |
| |
| *read = state->read_event; |
| *except = state->except_event; |
| } |
| |
| static void |
| ser_console_close (struct serial *scb) |
| { |
| struct ser_console_state *state = scb->state; |
| |
| if (scb->state) |
| { |
| SetEvent (state->stop_select); |
| |
| CloseHandle (state->read_event); |
| CloseHandle (state->except_event); |
| |
| xfree (scb->state); |
| } |
| } |
| |
| struct ser_console_ttystate |
| { |
| int is_a_tty; |
| }; |
| |
| static serial_ttystate |
| ser_console_get_tty_state (struct serial *scb) |
| { |
| if (isatty (scb->fd)) |
| { |
| struct ser_console_ttystate *state; |
| state = (struct ser_console_ttystate *) xmalloc (sizeof *state); |
| state->is_a_tty = 1; |
| return state; |
| } |
| else |
| return NULL; |
| } |
| |
| struct net_windows_state |
| { |
| HANDLE read_event; |
| HANDLE except_event; |
| |
| HANDLE start_select; |
| HANDLE stop_select; |
| HANDLE sock_event; |
| }; |
| |
| static DWORD WINAPI |
| net_windows_select_thread (void *arg) |
| { |
| struct serial *scb = arg; |
| struct net_windows_state *state, state_copy; |
| int event_index, fd; |
| |
| /* Copy useful information out of the control block, to make sure |
| that we do not race with freeing it. */ |
| state_copy = *(struct net_windows_state *) scb->state; |
| state = &state_copy; |
| fd = scb->fd; |
| |
| while (1) |
| { |
| HANDLE wait_events[2]; |
| WSANETWORKEVENTS events; |
| |
| wait_events[0] = state->start_select; |
| wait_events[1] = state->stop_select; |
| |
| if (WaitForMultipleObjects (2, wait_events, FALSE, INFINITE) != WAIT_OBJECT_0) |
| { |
| CloseHandle (state->stop_select); |
| return 0; |
| } |
| |
| wait_events[0] = state->stop_select; |
| wait_events[1] = state->sock_event; |
| |
| event_index = WaitForMultipleObjects (2, wait_events, FALSE, INFINITE); |
| |
| if (event_index == WAIT_OBJECT_0 |
| || WaitForSingleObject (state->stop_select, 0) == WAIT_OBJECT_0) |
| { |
| CloseHandle (state->stop_select); |
| return 0; |
| } |
| |
| if (event_index != WAIT_OBJECT_0 + 1) |
| { |
| /* Some error has occured. Assume that this is an error |
| condition. */ |
| SetEvent (state->except_event); |
| continue; |
| } |
| |
| /* Enumerate the internal network events, and reset the object that |
| signalled us to catch the next event. */ |
| WSAEnumNetworkEvents (fd, state->sock_event, &events); |
| |
| if (events.lNetworkEvents & FD_READ) |
| SetEvent (state->read_event); |
| |
| if (events.lNetworkEvents & FD_CLOSE) |
| SetEvent (state->except_event); |
| } |
| } |
| |
| static void |
| net_windows_wait_handle (struct serial *scb, HANDLE *read, HANDLE *except) |
| { |
| struct net_windows_state *state = scb->state; |
| |
| ResetEvent (state->read_event); |
| ResetEvent (state->except_event); |
| |
| SetEvent (state->start_select); |
| |
| *read = state->read_event; |
| *except = state->except_event; |
| } |
| |
| static int |
| net_windows_open (struct serial *scb, const char *name) |
| { |
| struct net_windows_state *state; |
| int ret; |
| DWORD threadId; |
| |
| ret = net_open (scb, name); |
| if (ret != 0) |
| return ret; |
| |
| state = xmalloc (sizeof (struct net_windows_state)); |
| memset (state, 0, sizeof (struct net_windows_state)); |
| scb->state = state; |
| |
| /* Create auto reset events to wake and terminate the select thread. */ |
| state->start_select = CreateEvent (0, FALSE, FALSE, 0); |
| state->stop_select = CreateEvent (0, FALSE, FALSE, 0); |
| |
| /* Associate an event with the socket. */ |
| state->sock_event = CreateEvent (0, TRUE, FALSE, 0); |
| WSAEventSelect (scb->fd, state->sock_event, FD_READ | FD_CLOSE); |
| |
| /* Create our own events to report read and close separately. */ |
| state->read_event = CreateEvent (0, FALSE, FALSE, 0); |
| state->except_event = CreateEvent (0, FALSE, FALSE, 0); |
| |
| /* And finally start the select thread. */ |
| CreateThread (NULL, 0, net_windows_select_thread, scb, 0, &threadId); |
| |
| return 0; |
| } |
| |
| |
| static void |
| net_windows_close (struct serial *scb) |
| { |
| struct net_windows_state *state = scb->state; |
| |
| SetEvent (state->stop_select); |
| |
| CloseHandle (state->read_event); |
| CloseHandle (state->except_event); |
| CloseHandle (state->start_select); |
| CloseHandle (state->sock_event); |
| |
| xfree (scb->state); |
| |
| net_close (scb); |
| } |
| |
| void |
| _initialize_ser_windows (void) |
| { |
| WSADATA wsa_data; |
| struct serial_ops *ops; |
| |
| /* First register the serial port driver. */ |
| |
| ops = XMALLOC (struct serial_ops); |
| memset (ops, 0, sizeof (struct serial_ops)); |
| ops->name = "hardwire"; |
| ops->next = 0; |
| ops->open = ser_windows_open; |
| ops->close = ser_windows_close; |
| |
| ops->flush_output = ser_windows_flush_output; |
| ops->flush_input = ser_windows_flush_input; |
| ops->send_break = ser_windows_send_break; |
| |
| /* These are only used for stdin; we do not need them for serial |
| ports, so supply the standard dummies. */ |
| ops->get_tty_state = ser_base_get_tty_state; |
| ops->set_tty_state = ser_base_set_tty_state; |
| ops->print_tty_state = ser_base_print_tty_state; |
| ops->noflush_set_tty_state = ser_base_noflush_set_tty_state; |
| |
| ops->go_raw = ser_windows_raw; |
| ops->setbaudrate = ser_windows_setbaudrate; |
| ops->setstopbits = ser_windows_setstopbits; |
| ops->drain_output = ser_windows_drain_output; |
| ops->readchar = ser_base_readchar; |
| ops->write = ser_base_write; |
| ops->async = ser_base_async; |
| ops->read_prim = ser_windows_read_prim; |
| ops->write_prim = ser_windows_write_prim; |
| ops->wait_handle = ser_windows_wait_handle; |
| |
| serial_add_interface (ops); |
| |
| /* Next create the dummy serial driver used for terminals. We only |
| provide the TTY-related methods. */ |
| |
| ops = XMALLOC (struct serial_ops); |
| memset (ops, 0, sizeof (struct serial_ops)); |
| |
| ops->name = "terminal"; |
| ops->next = 0; |
| |
| ops->close = ser_console_close; |
| ops->get_tty_state = ser_console_get_tty_state; |
| ops->set_tty_state = ser_base_set_tty_state; |
| ops->print_tty_state = ser_base_print_tty_state; |
| ops->noflush_set_tty_state = ser_base_noflush_set_tty_state; |
| ops->drain_output = ser_base_drain_output; |
| ops->wait_handle = ser_console_wait_handle; |
| |
| serial_add_interface (ops); |
| |
| /* If WinSock works, register the TCP/UDP socket driver. */ |
| |
| if (WSAStartup (MAKEWORD (1, 0), &wsa_data) != 0) |
| /* WinSock is unavailable. */ |
| return; |
| |
| ops = XMALLOC (struct serial_ops); |
| memset (ops, 0, sizeof (struct serial_ops)); |
| ops->name = "tcp"; |
| ops->next = 0; |
| ops->open = net_windows_open; |
| ops->close = net_windows_close; |
| ops->readchar = ser_base_readchar; |
| ops->write = ser_base_write; |
| ops->flush_output = ser_base_flush_output; |
| ops->flush_input = ser_base_flush_input; |
| ops->send_break = ser_base_send_break; |
| ops->go_raw = ser_base_raw; |
| ops->get_tty_state = ser_base_get_tty_state; |
| ops->set_tty_state = ser_base_set_tty_state; |
| ops->print_tty_state = ser_base_print_tty_state; |
| ops->noflush_set_tty_state = ser_base_noflush_set_tty_state; |
| ops->setbaudrate = ser_base_setbaudrate; |
| ops->setstopbits = ser_base_setstopbits; |
| ops->drain_output = ser_base_drain_output; |
| ops->async = ser_base_async; |
| ops->read_prim = net_read_prim; |
| ops->write_prim = net_write_prim; |
| ops->wait_handle = net_windows_wait_handle; |
| serial_add_interface (ops); |
| } |