Archive for the ‘Windows’ tag
Windows Serial Programming - Tips and Tricks
The last couple days I have been working on Serial Communications (RS-422 and RS-485) between a Windows PC and an Embedded FPGA Processor. From start to finish, synchronizing the data transfer between the two systems only took about half a day. When I first started using serial communications a few years ago, it took me a week to establish a good working link. Between then and now, I've noticed a few tips and tricks along the way which has helped get communication between two devices up-to-speed more quickly.
Not a Perpetual Motion Machine
What makes serial a bit difficult to work with is that it is (in general) not plug-and-play. The code used to communicate to one type of board, won't work straight-out-of-the-box with another type of board. Like snowflakes, no two serial-com links are alike. There is no perpetual motion machine. Each has individual timing constraints and quirks. Sometimes you can read about these timing constraints in the spec, but most times a good link is established only after a big battle of trial-and-error. This also makes it hard to do serial comm research on the Internet. Copying and pasting serial code from a website you find on google rarely works. It can help you get the gist of what is going on, but unfortunately, prepare to put in some legwork.
Know the baud-rate, parity, stop-bit, and byte-size
This is essential. Before even getting started make sure you know the baud-rate, parity, stop-bit, and byte-size information of the device you are trying to communicate to. If you have this information wrong, you are pretty much dead in the water from the get-go. Nothing will work. Check and double check this information. I don't know how many times I've ported serial code from one project to the other, ran it, then hit my head against a wall for a few hours before realizing that, duh, the last FPGA that I was communicating to was in 19200 the new FPGA is in 9600. These variables should be the first to be modified when programming serial in the windows environment.
DCB properties; GetCommState(hCom, &properties); properties.BaudRate = CBR_19200; properties.Parity = ODDPARITY; properties.StopBits = ONESTOPBIT; properties.ByteSize = 8; SetCommState(hCom, &properties);
Auto-Toggle or Manual? Know your serial card.
Oh Auto-Toggle. How we love thee. Let me count the ways. Auto-Toggle is a function that is built into certain serial cards that automatically toggles the transmit lines when bytes are sent then immediately toggles back to the recieve line to await bytes from the device to whom you were talking. The problem is that most serial cards don't have Auto-Toggle and sometimes even when they have it doesn't operate properly. Make sure you know to which camp you belong. Know your serial card. If your card has auto-toggle, verify that it works using an oscilloscope (we'll talk about that later). If it doesn't, know that you have to manually toggle the transmit and receive lines.
Enabling RTS_CONTROL toggles the transmit line and allows the user to transmit data over serial. When RTS_CONTROL has been enabled, the user cannot recieve any data over serial.
GetCommState(m_hCom, &properties); properties.fRtsControl = RTS_CONTROL_ENABLE; SetCommState(m_hCom, &properties);
Diabling RTS_Control toggles the recieve line and allows the user to recieve data over serial. When RTS_CONTROL has been disabled, the user cannot send any data over serial.
GetCommState(m_hCom, &properties); properties.fRtsControl = RTS_CONTROL_DISABLE; SetCommState(m_hCom, &properties);
If you are working with RS-485, and auto-toggle isn't an option, manually controlling the transmit/recieve lines is very necessary as RS-485 is half-duplex (which means that the transmit/receive lines are shared). If working with RS-422, which is full-duplex, transmit and receive are not being shared (both have separate lines) and toggling the transmit/receive lines should not be necessary. However, just this week I was working with a RS-422 and communication would not work unless I still toggled transmit/receive - go figure.
Give the WriteFile command time to breathe
After transmitting bytes using the WriteFile command, proper time must be given after executing WriteFile and before RTS_CONTROL is disabled. Unfortunately, the WriteFile command doesn't (properly) verify that all bytes are written to the transmit line. If RTS_CONTROL is disabled too quickly after WriteFile, bytes can actually be chopped off the end of your message. If you are losing bytes, simply insert something I like to call a 'for loop to nowhere' after the WriteFile command. It just wastes a little time before you disable RTS_CONTROL and makes sure all bytes are transmitted.
DCB properties; GetCommState(m_hCom, &properties); properties.fRtsControl = RTS_CONTROL_ENABLE; SetCommState(m_hCom, &properties); BOOL ok = ::WriteFile(m_hCom,(void *)buffer,bytes_to_send,&bytesSent,NULL); for(int i = 0; i < 200000; i++); //waste a little time GetCommState(m_hCom, &properties); properties.fRtsControl = RTS_CONTROL_DISABLE; SetCommState(m_hCom, &properties);
Use an Oscilloscope! ! !
Before I go any further, I'd like to urge you, no, gently prod you to use what I consider the number one best way to get serial communications up and running: the Oscilloscope. Using the Oscilloscope you can really nail down the timing of your sent and received messages over serial communications. You can measure the time it takes to send and receive one byte. You can measure the time between when the last byte is sent by the WriteFile command and when a response is seen on the receive line. You can actually SEE (I spy with my little eye) that you are disabling the RTS_CONTROL line too early and thus cutting off bytes. The Oscilloscope will save you time and thus head-banging hair-wrencing frustration. I guarantee it. I recommend setting two trigger points if using RS-422. One on the receive and one on the transmit line.
Free Serial Port Monitor
Another tool that I use constantly while working on Serial Programming is the Free Serial Port Monitor. This program allows you to monitor a specific COM port and shows you exactly what you have written and what you have read on that COM port. The tool is perfect when you are just getting started if you just want to know if you are writing anything at all. Sometimes, if you aren't using an Oscilloscope, it is difficult to tell if any bytes are being written correctly. Also, this tool shows you exactly how many bytes are being read and what bytes are being read with the ReadFile command. This helps you debug your serial read calls because you can see if you are chopping your message in half with the ReadFile command and adjust accordingly.
Reading when you know what to expect
If you are working on a command and response based serial system, one in which you issue and command and expect back a response of a certain length — reading on serial is a cinch. You just have to know three little bits (or should I say bytes?) of information. The first is the maximum time interval between the characters you are receiving in milliseconds. This can be found in your spec (if you are given a spec) or can be found with an Oscilloscope. The second is the number of bytes you expect back. The third is the time it takes to send one character (this can be found with a simple math equation). I'll tell you how to use this information below after I drop a big lump of code on you.
DWORD Serial_Controller_Class::Receive_Message(unsigned char * Msg_Ptr, int ExpectedNumberOfBytes, int MaxTimeBetweenChar, int MaxTimeToSendSingleChar) { DWORD bytesRead = 0; COMMTIMEOUTS comTimeOut; comTimeOut.ReadIntervalTimeout = MaxTimeBetweenChar; comTimeOut.ReadTotalTimeoutMultiplier = ExpectedNumberOfBytes; comTimeOut.ReadTotalTimeoutConstant = MaxTimeToSendSingleChar; SetCommTimeouts(m_hCom,&comTimeOut); BOOL ok = ::ReadFile(m_hCom,Msg_Ptr, ExpectedNumberOfBytes,&bytesRead,NULL); FlushFileBuffers(m_hCom); return bytesRead; }
The Receive Message method above has four elements passed to it. The first, MsgPtr, is used to hold the message received. ExpectedNumberOfBytes contains the number of bytes you expect to be returned based on the message you sent out on serial. The variables MaxTimeBetweenChar and MaxTimeToSendSingleChar are, in my opinion, pretty self explanatory. This passed data then can be used to set things called "Comm Timeouts." Comm Timeouts are used to control the amount of time that ReadFile will sit and wait for a message to be recieved. This is called "blocking" in the biz which means that all other processes are esentially "blocked" until ReadFile has finished executing. This is why setting CommTimeouts correctly are extreamly important. We don't want to "block" for longer then necessary. Each Comm Timeout is explained in detail below.
DWORD ReadIntervalTimeout - sets max period of time (in milliseconds) allowed between two sequential characters being read from the line of commutation. During the 'read' operation the time period countdown takes its start when the first character is received. When the interval between two sequential characters exceeds the given value the 'read' operation finishes and all the data accumulated in the buffer are transmitted to the programme. Zero value of this member indicates that this timeout isn't used.
DWORD ReadTotalTimeoutMultiplier- sets the multiplier (in milliseconds) used to calculate general timeout of the 'read' operation. In every case this value is multiplied by the quantity of the characters requested for reading.
DWORD ReadTotalTimeoutConstant - sets a constant (in milliseconds) used to calculate the general timeout of the operation. In case of every 'read' operation this value is added to the result of multiplying ReadTotalTimeoutMultiplier by the quantity of the characters requested for the reading. ReadTotalTimeoutMultiplier and ReadTotalTimeoutConstant mean that general timeout for 'read' operation isn't used.
Here's how to calculate MaxTimeToSendSingleCharacter. If we transfer one character at 19200 baud, 8 bits per byte, plus parity complement and one stop bit, then there will be 11 bits total per character.
MaxTimeToSendSingleCharacter = 11x(1/19200) = .057 milliseconds (which can be rounded up to 1 milliscond).
Imagine that we read 100 characters at a rate of 19200 bps. If 8 bits per character, parity complement and one stop bit are used, then there will be be 11 bits (including the start bit) per character in the physical line. So 100 characters at a rate 19200 bit/s will be received as
100×11x(1/19200)=0.0572916 sec
or approximately 57.2 milliseconds. If there is no interval between receiving multiple characters. If the interval between the characters is about a half time of one character transmission, i.e. 0.25 milliseconds, the reception time is calculated as
100×11x(1/19200)+(99×0.00025)=0.00820416 sec
or give or take 82 milliseconds. If the reading process has amounted to longer then 82 milliseconds, we can suppose there's been an external device error and stop the 'read' operation to anymore program blockage. Phew.
"No More Math! No More Math!" I hear you chant. Fear not, we are done.
Reading when you have no idea
When you have no idea how many bytes to expect back, the best thing to do is to follow the steps above with one exception. Hopefully you know the maximum number of bytes that can be sent at any given time. If you know that, just use that number for ExpectedNumberOfBytes.
Thats it!
These are my tricks. OK, maybe they are really tricks. Maybe they are just a better explanation of the dry-and-boring RS-232 specification that you've been starting at and scratching your head over for the past week or so. Regardless, I hope some of these tidbits helped.
