/************************************************************************** * * Copyright (C) 2004 Steve Karg * * SPDX-License-Identifier: GPL-2.0-or-later WITH GCC-exception-2.0 * *********************************************************************/ /** @file win32/rs485.c Provides Windows-specific functions for RS-485 */ /* Suggested USB to RS485 devices: B&B Electronics USOPTL4 SerialGear USB-COMi-SI-M USB-RS485-WE-1800-BT */ #include #include #include #include #include #include #include #include "bacnet/datalink/mstp.h" #include "bacnet/datalink/dlmstp.h" #define WIN32_LEAN_AND_MEAN #define STRICT 1 #include #include "rs485.h" #include "bacnet/basic/sys/fifo.h" /* details from Serial Communications in Win32 at MSDN */ /* Win32 handle for the port */ HANDLE RS485_Handle; /* Original COM Timeouts */ static COMMTIMEOUTS RS485_Timeouts; /* COM port name COM1, COM2, etc */ static char RS485_Port_Name[256] = "COM4"; /* baud rate - MS enumerated CBR_110, CBR_300, CBR_600, CBR_1200, CBR_2400, CBR_4800, CBR_9600, CBR_14400, CBR_19200, CBR_38400, CBR_56000, CBR_57600, CBR_115200, CBR_128000, CBR_256000 */ static DWORD RS485_Baud = CBR_38400; /* ByteSize in bits: 5, 6, 7, 8 are valid */ static DWORD RS485_ByteSize = 8; /* Parity - MS enumerated: NOPARITY, EVENPARITY, ODDPARITY, MARKPARITY, SPACEPARITY */ static DWORD RS485_Parity = NOPARITY; /* StopBits - MS enumerated: ONESTOPBIT, ONE5STOPBITS, TWOSTOPBITS */ static DWORD RS485_StopBits = ONESTOPBIT; /* DTRControl - MS enumerated: DTR_CONTROL_ENABLE, DTR_CONTROL_DISABLE, DTR_CONTROL_HANDSHAKE */ static DWORD RS485_DTRControl = DTR_CONTROL_DISABLE; /* RTSControl - MS enumerated: RTS_CONTROL_ENABLE, RTS_CONTROL_DISABLE, RTS_CONTROL_HANDSHAKE, RTS_CONTROL_TOGGLE */ static DWORD RS485_RTSControl = RTS_CONTROL_DISABLE; /**************************************************************************** * DESCRIPTION: Change the characters in a string to uppercase * RETURN: nothing * ALGORITHM: none * NOTES: none *****************************************************************************/ static void strupper(char *str) { char *p; for (p = str; *p != '\0'; ++p) { *p = (char)toupper(*p); } } /**************************************************************************** * DESCRIPTION: Initializes the RS485 hardware and variables, and starts in * receive mode. * RETURN: none * ALGORITHM: none * NOTES: expects a constant char ifname, or char from the heap *****************************************************************************/ void RS485_Set_Interface(char *ifname) { /* For COM ports greater than 9 you have to use a special syntax for CreateFileA. The syntax also works for COM ports 1-9. */ /* http://support.microsoft.com/kb/115831 */ if (ifname) { strupper(ifname); if (strncmp("COM", ifname, 3) == 0) { if (strlen(ifname) > 3) { snprintf( RS485_Port_Name, sizeof(RS485_Port_Name), "\\\\.\\COM%i", atoi(ifname + 3)); fprintf( stdout, "Adjusted interface name to %s\r\n", RS485_Port_Name); } } } } /**************************************************************************** * DESCRIPTION: Check the serial port to see if port exists * RETURN: true if port exists * ALGORITHM: none * NOTES: none *****************************************************************************/ bool RS485_Interface_Valid(unsigned port_number) { HANDLE h = 0; DWORD err = 0; bool status = false; char ifname[255] = ""; snprintf(ifname, sizeof(ifname), "\\\\.\\COM%u", port_number); h = CreateFileA( ifname, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (h == INVALID_HANDLE_VALUE) { err = GetLastError(); if ((err == ERROR_ACCESS_DENIED) || (err == ERROR_GEN_FAILURE) || (err == ERROR_SHARING_VIOLATION) || (err == ERROR_SEM_TIMEOUT)) { status = true; } } else { status = true; CloseHandle(h); } return status; } const char *RS485_Interface(void) { return RS485_Port_Name; } void RS485_Print_Error(void) { LPVOID lpMsgBuf; FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpMsgBuf, 0, NULL); MessageBox(NULL, lpMsgBuf, "GetLastError", MB_OK | MB_ICONINFORMATION); LocalFree(lpMsgBuf); return; } static void RS485_Configure_Status(void) { DCB dcb = { 0 }; COMMTIMEOUTS ctNew; dcb.DCBlength = sizeof(dcb); /* get current DCB settings */ if (!GetCommState(RS485_Handle, &dcb)) { fprintf(stderr, "Unable to get status from %s\n", RS485_Port_Name); RS485_Print_Error(); exit(1); } /* update DCB rate, byte size, parity, and stop bits size */ dcb.BaudRate = RS485_Baud; dcb.ByteSize = (unsigned char)RS485_ByteSize; dcb.Parity = (unsigned char)RS485_Parity; dcb.StopBits = (unsigned char)RS485_StopBits; /* update flow control settings */ dcb.fDtrControl = RS485_DTRControl; dcb.fRtsControl = RS485_RTSControl; /* dcb.fOutxCtsFlow = CTSOUTFLOW(TTYInfo); dcb.fOutxDsrFlow = DSROUTFLOW(TTYInfo); dcb.fDsrSensitivity = DSRINFLOW(TTYInfo); dcb.fOutX = XONXOFFOUTFLOW(TTYInfo); dcb.fInX = XONXOFFINFLOW(TTYInfo); dcb.fTXContinueOnXoff = TXAFTERXOFFSENT(TTYInfo); dcb.XonChar = XONCHAR(TTYInfo); dcb.XoffChar = XOFFCHAR(TTYInfo); dcb.XonLim = XONLIMIT(TTYInfo); dcb.XoffLim = XOFFLIMIT(TTYInfo); // DCB settings not in the user's control dcb.fParity = TRUE; */ if (!SetCommState(RS485_Handle, &dcb)) { fprintf(stderr, "Unable to set status on %s\n", RS485_Port_Name); RS485_Print_Error(); } /* configure the time-out parameters for a communications device. */ /* If an application sets ReadIntervalTimeout and ReadTotalTimeoutMultiplier to MAXDWORD and sets ReadTotalTimeoutConstant to a value greater than zero and less than MAXDWORD, one of the following occurs when the ReadFile function is called: * If there are any bytes in the input buffer, ReadFile returns immediately with the bytes in the buffer. * If there are no bytes in the input buffer, ReadFile waits until a byte arrives and then returns immediately. * If no bytes arrive within the time specified by ReadTotalTimeoutConstant, ReadFile times out. Constant values are in milliseconds */ ctNew.ReadIntervalTimeout = MAXDWORD; ctNew.ReadTotalTimeoutMultiplier = MAXDWORD; ctNew.ReadTotalTimeoutConstant = 1; ctNew.WriteTotalTimeoutMultiplier = 0; ctNew.WriteTotalTimeoutConstant = 0; if (!SetCommTimeouts(RS485_Handle, &ctNew)) { RS485_Print_Error(); } /* Get rid of any stray characters */ if (!PurgeComm(RS485_Handle, PURGE_TXABORT | PURGE_RXABORT)) { fprintf(stderr, "Unable to purge %s\n", RS485_Port_Name); RS485_Print_Error(); } /* Set the Comm buffer size */ SetupComm(RS485_Handle, DLMSTP_MPDU_MAX, DLMSTP_MPDU_MAX); /* raise DTR */ if (!EscapeCommFunction(RS485_Handle, SETDTR)) { fprintf(stderr, "Unable to set DTR on %s\n", RS485_Port_Name); RS485_Print_Error(); } } /**************************************************************************** * DESCRIPTION: Cleans up any handles that were created at startup. * RETURN: none * ALGORITHM: none * NOTES: none *****************************************************************************/ static void RS485_Cleanup(void) { if (!EscapeCommFunction(RS485_Handle, CLRDTR)) { RS485_Print_Error(); } if (!SetCommTimeouts(RS485_Handle, &RS485_Timeouts)) { RS485_Print_Error(); } CloseHandle(RS485_Handle); } /**************************************************************************** * DESCRIPTION: Initializes the RS485 hardware and variables, and starts in * receive mode. * RETURN: none * ALGORITHM: none * NOTES: none *****************************************************************************/ void RS485_Initialize(void) { RS485_Handle = CreateFileA( RS485_Port_Name, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, /*FILE_FLAG_OVERLAPPED */ 0, 0); if (RS485_Handle == INVALID_HANDLE_VALUE) { DWORD err = GetLastError(); fprintf( stderr, "RS485 unable to open %s (Error %lu)\n", RS485_Port_Name, err); RS485_Print_Error(); exit(1); } if (!GetCommTimeouts(RS485_Handle, &RS485_Timeouts)) { RS485_Print_Error(); } RS485_Configure_Status(); #if PRINT_ENABLED fprintf(stdout, "RS485 Interface: %s\n", RS485_Port_Name); fprintf(stdout, "RS485 Baud Rate %u\n", RS485_Get_Baud_Rate()); fflush(stdout); #endif atexit(RS485_Cleanup); return; } /**************************************************************************** * DESCRIPTION: Returns the baud rate that we are currently running at * RETURN: none * ALGORITHM: none * NOTES: none *****************************************************************************/ uint32_t RS485_Get_Baud_Rate(void) { switch (RS485_Baud) { case CBR_19200: return 19200; case CBR_38400: return 38400; case CBR_57600: return 57600; case CBR_115200: return 115200; case CBR_110: return 110; case CBR_300: return 300; case CBR_600: return 600; case CBR_1200: return 1200; case CBR_2400: return 2400; case CBR_4800: return 4800; case CBR_14400: return 14400; case CBR_56000: return 56000; case CBR_128000: return 128000; case CBR_256000: return 256000; case 76800: /* See comments in RS485_Set_Baud_Rate() below * also look at definition of CBR_xx in winbase.h * some serial drivers will only support the defined * baud rates but others will try and configure the * requested baud rate (or as close as they can get) */ return 76800; case CBR_9600: default: return 9600; } } /**************************************************************************** * DESCRIPTION: Sets the baud rate for the chip USART * RETURN: none * ALGORITHM: none * NOTES: none *****************************************************************************/ bool RS485_Set_Baud_Rate(uint32_t baud) { bool valid = true; switch (baud) { case 9600: RS485_Baud = CBR_9600; break; case 19200: RS485_Baud = CBR_19200; break; case 38400: RS485_Baud = CBR_38400; break; case 57600: RS485_Baud = CBR_57600; break; case 115200: RS485_Baud = CBR_115200; break; case 110: RS485_Baud = CBR_110; break; case 300: RS485_Baud = CBR_300; break; case 600: RS485_Baud = CBR_600; break; case 1200: RS485_Baud = CBR_1200; break; case 2400: RS485_Baud = CBR_2400; break; case 4800: RS485_Baud = CBR_4800; break; case 14400: RS485_Baud = CBR_14400; break; case 56000: RS485_Baud = CBR_56000; break; case 128000: RS485_Baud = CBR_128000; break; case 256000: RS485_Baud = CBR_256000; break; case 76800: /* I'm using the B&B Electronics USOPTL4 USB RS485 adapter * on Win 7 and building with VS2008 Express Edition and it * seems to work for the most part if I use the following. * I get the occasional data errors especially if the devices * are transmitting with 1 stop bit (some devices receive with * 1 stop bit but effectivly end up transmitting with 2 stop * bits, usually because of synchroisation issues in some UARTs * which mean that if you wait until the serialiser has finished * with the current character and then load the TX buffer it has * to wait until the next bit boundary to start transmitting. * PMcS */ RS485_Baud = 76800; break; default: valid = false; break; } if (valid) { /* FIXME: store the baud rate */ } return valid; } /* Transmits a Frame on the wire */ void RS485_Send_Frame( struct mstp_port_struct_t *mstp_port, /* port specific data */ const uint8_t *buffer, /* frame to send (up to 501 bytes of data) */ uint16_t nbytes) { /* number of bytes of data (up to 501) */ DWORD dwWritten = 0; if (mstp_port) { uint32_t baud; uint8_t turnaround_time; baud = RS485_Get_Baud_Rate(); /* wait about 40 bit times since reception */ if (baud == 9600) { turnaround_time = 4; } else if (baud == 19200) { turnaround_time = 2; } else { turnaround_time = 2; } while (mstp_port->SilenceTimer(NULL) < turnaround_time) { /* do nothing - wait for timer to increment */ }; } WriteFile(RS485_Handle, buffer, nbytes, &dwWritten, NULL); /* per MSTP spec, reset SilenceTimer after each byte is sent */ if (mstp_port) { mstp_port->SilenceTimerReset(NULL); } return; } /* called by timer, interrupt(?) or other thread */ void RS485_Check_UART_Data(struct mstp_port_struct_t *mstp_port) { char lpBuf[1]; DWORD dwRead = 0; if (mstp_port->ReceiveError == true) { /* wait for state machine to clear this */ } /* wait for state machine to read from the DataRegister */ else if (mstp_port->DataAvailable == false) { /* check for data */ if (!ReadFile(RS485_Handle, lpBuf, sizeof(lpBuf), &dwRead, NULL)) { if (GetLastError() != ERROR_IO_PENDING) { mstp_port->ReceiveError = TRUE; } } else { if (dwRead) { mstp_port->DataRegister = lpBuf[0]; mstp_port->DataAvailable = TRUE; } } } } /************************************************************************* * Description: print available COM ports * Returns: none * Notes: none **************************************************************************/ void RS485_Print_Ports(void) { unsigned i = 0; /* try to open all 255 COM ports */ for (i = 1; i < 256; i++) { if (RS485_Interface_Valid(i)) { /* note: format for Wireshark ExtCap */ printf( "interface {value=COM%u}" "{display=BACnet MS/TP on COM%u}\n", i, i); } } } #ifdef TEST_RS485 #include "bacnet/datalink/mstpdef.h" static void test_transmit_task(void *pArg) { char *TxBuf = "BACnet MS/TP"; size_t len = strlen(TxBuf) + 1; while (TRUE) { Sleep(1000); RS485_Send_Frame(NULL, &TxBuf[0], len); } } #if defined(_WIN32) static BOOL WINAPI CtrlCHandler(DWORD dwCtrlType) { dwCtrlType = dwCtrlType; exit(0); return TRUE; } #endif static int ascii_hex_to_int(char ch) { int rv = -1; if ((ch >= '0') && (ch <= '9')) { rv = ch - '0'; } else if ((ch >= 'a') && (ch <= 'f')) { rv = 10 + ch - 'a'; } else if ((ch >= 'A') && (ch <= 'F')) { rv = 10 + ch - 'a'; } return rv; } int main(int argc, char *argv[]) { unsigned long hThread = 0; uint32_t arg_value = 0; char lpBuf[1]; DWORD dwRead = 0; unsigned i = 0, len = 0, count = 0; char hex_pair[5] = "0xff"; char ch = ' '; int lsb = 0, msb = 0; long my_baud = 38400; uint8_t buffer[501] = { 0 }; if (argc > 1) { RS485_Set_Interface(argv[1]); } if (argc > 2) { my_baud = strtol(argv[2], NULL, 0); } RS485_Set_Baud_Rate(my_baud); RS485_Initialize(); #if defined(_WIN32) SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), ENABLE_PROCESSED_INPUT); SetConsoleCtrlHandler((PHANDLER_ROUTINE)CtrlCHandler, TRUE); #endif #ifdef TEST_RS485_TRANSMIT /* read a stream of characters from stdin or argument */ if (argc > 3) { len = strlen(argv[3]); for (i = 0; i < len; i++) { /* grab pairs of hex characters, skip spaces */ ch = argv[3][i]; if (ch == ' ') { continue; } msb = ascii_hex_to_int(ch); if (msb >= 0) { i++; ch = argv[3][i]; lsb = ascii_hex_to_int(ch); if (lsb >= 0) { buffer[count] = msb << 4 | lsb; } else { buffer[count] = msb; } count++; if (count >= sizeof(buffer)) { break; } } } RS485_Send_Frame(NULL, buffer, count); } #endif #ifdef TEST_RS485_RECEIVE /* receive task */ for (;;) { if (!ReadFile(RS485_Handle, lpBuf, sizeof(lpBuf), &dwRead, NULL)) { if (GetLastError() != ERROR_IO_PENDING) { RS485_Print_Error(); } } else { /* print any characters received */ if (dwRead) { for (i = 0; i < dwRead; i++) { fprintf(stderr, "%02X ", lpBuf[i]); } } dwRead = 0; } } #endif } #endif