Files
bacnet_stack/ports/win32/rs485.c
T

608 lines
18 KiB
C

/**************************************************************************
*
* 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 <stddef.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include "bacnet/datalink/mstp.h"
#include "bacnet/datalink/dlmstp.h"
#define WIN32_LEAN_AND_MEAN
#define STRICT 1
#include <windows.h>
#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