I'm working on a project sending serial data to control animation of LED lights, which need to stay in sync with an animation engine. There seems to be a large serial write buffer (OSX (POSIX) + FTDI chipset usb serial device), so without manually throttling calls to write(), the software can get several seconds ahead of the lights.
Currently I'm manually restricting the serial write speed to the baudrate (8N1 = 10 bytes serial frame per 8 bytes data, 19200 bps serial -> 1920 bytes per second max), but I am having a problem with the animation drifting out of sync with the lights over time - it starts fine, but after 10 minutes there's a noticeable (100ms+) lag between the animation and the lights.
This is the code that's restricting the serial write speed (called once per animation frame, 'elapsed' is the duration of the current frame, 'baudrate' is the bps (19200)):
void BufferedSerial::u开发者_如何学Cpdate( float elapsed )
{
baud_timer += elapsed;
if ( bytes_written > 1024 )
{
// maintain baudrate
float time_should_have_taken = (float(bytes_written)*10)/float(baudrate);
float time_actually_took = baud_timer;
// sleep if we have > 20ms lag between serial transmit and our write calls
if ( time_should_have_taken-time_actually_took > 0.02f )
{
float sleep_time = time_should_have_taken - time_actually_took;
int sleep_time_us = sleep_time*1000.0f*1000.0f;
//printf("BufferedSerial::update sleeping %i ms\n", sleep_time_us/1000 );
delayUs( sleep_time_us );
// subtract 128 bytes
bytes_written -= 128;
// subtract the time it should have taken to write 128 bytes
baud_timer -= (float(128)*10)/float(baudrate);
}
}
}
Clearly there's something wrong, somewhere.
A much better approach would be to be able to determine the number of bytes currently in the transmit queue, and try and keep that below a fixed threshold, but I can't figure out how to do this on an OSX (POSIX) system.
Any advice appreciated.
If you want to slow your animation down to match the maximum speed that you can write to the LEDs, you can just use tcdrain()
; something like this:
while (1)
{
write(serial_fd, led_command);
animate_frame();
tcdrain(serial_fd);
}
You could use hardware flow control.
I don't know what kind of hardware do you have on the other side of the serial link, but booth sides could synchronize and throttle themselves via the RTS/CTS handshake lines.
That's what they are intended for after all.
Had to feed data to a serial thermal strip chart recorder once (very much like a receipt printer) and had the same kind of issues. Any delays in the data caused skips in the printed output which are unacceptable.
The solution is very simple: if you keep data in the kernel serial buffer at all times, then the output will be exactly (baud rate / (1 + data bits + stop bits)) characters per second. So just add enough NUL byte padding to space out your data.
Some devices could do very bad things if they see NUL bytes in the data I suppose, in which case this won't work. But many just ignore extra NUL bytes in between messages, which lets you use the very accurate hardware timer inside the serial port to control your timing.
Just keep a fixed baudrate that is slightly faster than what it needs to be, and sync the LEDs with the animation for every block of N animation frames:
for each block
{
writeBlockSerialData();
for each frame in block
{
animateFrame();
}
}
The slightly faster baudrate will ensure that the serial buffer doesn't gradually overflow.
There will be a small pause between the blocks of serial data (milliseconds) but this should not be perceptible.
EDIT: This is assuming you have a fixed animation rate.
Here's an approach, using multi-threading, that is different than my other answer:
ledThread()
{
while(animating)
{
queue.pop(packet);
writeSerialPacket(packet);
flushSerialPacket(); // Blocks until serial buffer is empty
}
}
animationThread()
{
time lastFrameTime = now();
time_duration elapsed = 0;
while(animating)
{
buildLedPacket(packet);
queue.push(packet);
elapsed = lastFrameTime - now();
lastFrameTime = now();
animateNextFrame(elapsed);
}
}
In the above pseudocode, queue is a blocking producer-consumer queue, with a capacity of one. In other words, the producer will block during queue.push() while the queue is not empty. Instead of a blocking queue, you could also use a "ping-pong" buffer with a condition variable or semaphore.
Each animation frame is shown after its corresponding LED data was transmitted. The elapsed time the serial port takes to transmit a packet is used to compute the next animation frame.
The advantage of having two threads is that you can use the CPU for animating while waiting for the serial data to transmit (transmitting serial data hardly uses any CPU at all).
It's hard to describe this multi-threading stuff with only words. I wish I had a whiteboard to scribble on. :-)
精彩评论