views:

189

answers:

5

Hi everyone,

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::update( 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.

+2  A: 

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.

Nils Pipenbrinck
i'm pushing the UART output straight into an RS485 line driver, so there's no chance of that..
damian
+1  A: 

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.

Emile Cormier
+1. With your solution, the actual baud rate is irrelevant, unless it is _too_ slow. And the OP assumes the _serial port_ is the problem--it could well be the _animation frame rate_. Another possibility is the UART clock: the first Google hit for 'rs232 clock accuracy' indicates clocks can drift by +/- 0.5% over temperature and life, which is not too different from the OP's 1% drift.
Joseph Quinsey
The animation frame rate is not fixed. I am sometimes flickering all LEDs and sometimes pulsing one with a nice fade - lower framerate is fine for flickering (and necessary since the serial packets are larger), but for the single pulse i'd prefer a higher framerate for a nicer visual fade.
damian
+2  A: 

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.

Ben Voigt
huh. nice thinking, thanks! i'm pushing the data through an RS485 line driver (half-duplex) and although at the moment there's no need for ACK messages from the slave devices; but if that changes (if i need to start doing error checking for example) this method won't work..
damian
My printer actually did have a reverse channel with status information (not precisely ACK/NACK but I don't see how that changes anything). If you're writing the other end then you shouldn't have any difficulty implementing ACK/NACK at the same time as permitting padding.
Ben Voigt
A: 

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. :-)

Emile Cormier
+2  A: 

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);
}
caf
that's what i was looking for! thank you so much.
damian