The stupid question is the question not asked | |
PerlMonks |
Marrying Tk, threads and SerialPort - a COM port monitorby spurperl (Priest) |
on Dec 20, 2004 at 08:42 UTC ( [id://416132]=perlmeditation: print w/replies, xml ) | Need Help?? |
The serial port (or "COM" port, present in all PCs,
at least until recently) is a very important tool when
developing embedded devices. It implements the lowest
network levels in a simple and well known way (usually
RS232 for the physical layer, and a UART to transmit
single bytes). It is quite simple to implement a UART
in hardware, so the serial port has become one of the
favorite ways to debug embedded devices.
Here I will present a complete solution in Perl, from requirements to implementation - a sophisticated serial port monitor with ability to monitor full-duplex communications with time stamping.
RequirementsTwo embedded devides talk using a serial UART connection. On top of the UART, the whole network protocol is implented in the devices' hardware/software. However, something goes wrong and a need is arised to thoroughly debug the protocol between these devices. From this, the following requirements arise:
Believe it or not, I found no such program in the Internet (I'll be delighted if someone has...). There are several COM monitors around the web, but almost none use time stamping, and almost none monitor more than one port simultaneously. Therefore, there was a need to implement such a program. The language of choice (as is always the case for me :-) - Perl. Initial implementation considerationsFrom the requirements we can immediately derive the tools needed to complete the task.
What makes the task difficult is the time-stamping requirement. For it to happen, characters should be read from the port one-at-a-time, which imposes severe timing restrictions and we'll soon see.
It won't go without threadsThe accepted way to do "background processing" in Tk is use "after" and "repeat" for timed events that execute "from time to time" in parallel with the GUI. However, a quick analysis of the timing involved proves that this approach is not feasible.Even at the measly 2400 baud, in a continuous data stream a new character arrives each 4 ms. Even using Time::HiRes I couldn't achieve a reliable sleep time of less than 30 ms, and that's in idial conditions (lets remember that Windows is not even close to being a RTOS). So, a background thread is necessary. The Tk GUI does its job (that is, sleeps most of the time), sending messages to a background monitoring thread when needed. The background thread is live all the time and not only on "wakeup" intervals, so it will be possible to achieve the harsh timing constraints.
Tk and threads - not the best friends...The Perl Tk implementation is not thread save, and the Perl threads implementation is not especially Tk-aware. There you go for a nice friendship.Fortunately, there is a solution. What we should do is create the background thread *before* the Tk GUI goes live. This way, its creation won't have to copy all the Tk data in, and Tk won't die when the thread exits - they are as independent as possible, the only thing connecting between them is a message queue. When the application exits, it kills this monitoring thread.
Note the communication queues. I couldn't think of a better way than two, one for each direction. Blocking vs. non-blocking serial port readWin32::SerialPort provides us with two options for reading the serial port: blocking read() and non-blocking read_bg().I first tried to non-blocking alternative, in an attempt to do all the monitoring in that single background thread. The idea: loop over all the open ports and ask them (non blockingly) if they have data for us, if they do, log it. This approach worked - but had a serious flaw. The logging thread would take up 100% CPU and seriously clogged the PC - naturally, after all it ran an endless loop. The solution: wait/sleep/whatever between iterations. However, as I already said in the case of Tk's "after", we can't reliably wait for short periods (at least 30ms), and that caused missed characters and stuck communication. So, the only solution left is blocking read(). When SerialPort::read(1) is called, it blocks until the monitored port receives a character, and then returns that character. This block is what I call "sleepy" - it keeps the CPU free (most likely that it's implemented with an interrupt deep down in the Windows API). Hurray ! We can monitor ports and keep the CPU free. Not so quick... It would be fine if we only had one port to monitor. But when it's two or more, this approach won't work. While we block on one port, the other may receive data and we'll lose it after its buffer overflows (which happens quickly, especially in continuous data streams). Looks like a more complicated solution is due... The manager-workers modelOne of the best known methods of multi-threaded programming is the manager-workers model. A single manager thread creates worker threads to do jobs, and manages them. It keeps an eye on free workers and on incoming jobs. From the worker's point of view, it can either work on some job or wait for the manager to assign it one. From the manager's point of view, it receives jobs and sends them to workers. This is the approach that finally allowed me to fulfill the requirements:As we saw, blocking read() is required to make the program's performance reasonable (installing a CPU clogging utility is very rude...). But a logging thread can't block on more than one port at the same time. SO... what we need is one (worker) thread per port, and a single (manager) thread to manage these workers. The manager thread will keep a queue (actually two queues, for bidirectional communications) for each worker. It will send the worker "init", "run" and "stop" commands, and the worker will act according to those. To demonstrate, here is the complete "worker" thread function that is called (with threads->create) by the manager for each port it's told to monitor:
As you can see, each worker has a state - it's either idle or running (monitoring). When idle, it just waits for 10 ms using 'select' (this will probably result in a 30 ms wait). We don't care waiting here, because it starts running only after the manager is being asked so by a GUI events. The non-blocking dequeue_nb() call checks if it has new messages from the manager on each iteration. When a command to open a port is received, the worker dutifully opens a Win32::SerialPort. When a command to run is received, the worker is in "running" state, where it no longer sleeps on each iteration, but reads data from the port with a blocking read, and logs it to the dump file when received. To see the manager's side of the communication, here's the manager's response to the "open" and "run" messages from the GUI: When an "open" command arrives, the manager creates a worker thread to handle the port. When the "run" command is received, the manager sends a "run" command to all the workers. What about CPU clogging ? There's none. The manager thread sleeps on each iteration - it doesn't mind, it only responds to GUI requests. The workers sleep when not running, and block on read() when running, so no "naked loops" and the program hardly bothers the CPU. ConclusionThis implementation answers all the requirements: multiple ports can be monitored simultaneously and logged into a single file with timestamping. The output is terrific for protocol debugging, we can see exactly what was going on the line and when. The program provides reasonable performance and doesn't block the CPU.Some problems still remain:
All in all, however, this task proves that Perl is good for just about anything. This is a serious industrial-level application, very useful for debugging serial protocols. I'll be happy to hear insights on the problems I ran into. Tk/threads/SerialPort is a rare combination, and as you saw many interesting issues arise. The full code can be downloaded from here
Back to
Meditations
|
|