串口:write() 函数被限流了?

6
我正在开发一个项目,通过串行数据控制LED灯的动画,并需要与动画引擎保持同步。这种情况下,由于有一个大的串行写缓冲区(OSX(POSIX)+ FTDI芯片组USB串行设备),如果不手动限制write()调用的速度,软件可能会领先几秒钟。目前,我手动限制串行写速度为波特率(8N1 = 10个字节的串行帧每8个字节的数据,19200 bps串行 - >最大每秒1920个字节),但是我遇到了一个问题:随着时间的推移,动画与灯光之间开始脱离同步-一开始还好,但是在10分钟后,动画和灯光之间出现明显的延迟(100ms +)。以下是限制串行写速度的代码(每个动画帧调用一次,“elapsed”是当前帧的持续时间,“baudrate”是波特率(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);
        }
    }
}   

显然,在某个地方出了问题。

更好的方法是能够确定当前在传输队列中的字节数,并尝试将其保持在固定的阈值以下,但我无法弄清楚如何在OSX(POSIX)系统上实现此目标。

欢迎任何建议。


1
跨缓冲区的同步是 POSIX 通常不适合处理的事情;早期在所有消费者操作系统上进行同步音频和视频的尝试就是一个很好的例子。您可能需要直接编写串行 UART,或查找或编写驱动程序,使得 plesiochronous ioctls(如“不早于时间 n 发出此字节”)成为可能。 - msw
你对动画引擎有控制权吗? - Emile Cormier
动画帧率是固定的吗? - Emile Cormier
不,动画帧速率是可变的。基本上它尽可能地快速运行,但当有大量串行数据需要传输(许多LED更改值)时,帧速率需要减慢以匹配传输速度到LED,以便它们不会失去同步。因此出现了“经过”的浮点变量。 - damian
5个回答

3

如果您想减慢动画速度以匹配最大写入LED的速度,只需使用tcdrain()即可,就像这样:

while (1)
{
    write(serial_fd, led_command);
    animate_frame();
    tcdrain(serial_fd);
}

2
你可以使用硬件流控制。
我不知道串行链接的另一端有什么样的硬件,但是两端都可以通过 RTS/CTS 握手线同步和调节速度。
毕竟这就是它们的用途。

我将UART输出直接推入RS485线路驱动器,因此没有任何机会出现问题。 - damian

2

曾经需要将数据提供给一台串行热敏条形图记录仪(非常类似于收据打印机),遇到了同样的问题。任何数据延迟都会导致打印输出跳过,这是不可接受的。

解决方案非常简单:如果您始终在内核串行缓冲区中保留数据,则输出将恰好为(波特率/(1 + 数据位数+ 停止位数))每秒字符数。因此,只需添加足够的NUL字节填充以间隔您的数据即可。

我想有些设备看到数据中的NUL字节会出现非常糟糕的情况,在这种情况下,这种方法不起作用。但是,许多设备只会忽略消息之间的额外NUL字节,这使您可以使用串口内部的非常准确的硬件定时器来控制时间。


哦,不错的想法,谢谢!我正在通过RS485线驱动器(半双工)传输数据,虽然目前从从设备中没有需要ACK消息的需求;但如果情况改变了(例如我需要开始进行错误检查),这种方法就行不通了。 - damian
我的打印机实际上有一个反向通道,可以传输状态信息(不完全是ACK/NACK,但我不认为这会改变任何事情)。如果你正在编写另一端,那么在允许填充的同时实现ACK/NACK不应该有任何困难。 - Ben Voigt

1

只需保持一个略快于所需速率的固定波特率,并在每个N个动画帧块中将LED与动画同步:

for each block
{
    writeBlockSerialData();
    for each frame in block
    {
         animateFrame();
    }
}

稍微更快的波特率将确保串行缓冲区不会逐渐溢出。

在串行数据块之间会有一个小暂停(毫秒),但这应该是不可感知的。

编辑:这是假设您有固定的动画速率。


+1. 通过您的解决方案,实际波特率无关紧要,除非它太慢了。而且 OP 假设串口是问题所在——它可能是动画帧速率。另一个可能性是 UART 时钟:第一个谷歌搜索结果“rs232 时钟精度”表明时钟可以因温度和寿命而漂移 +/- 0.5%,这与 OP 的 1% 漂移并没有太大区别。 - Joseph Quinsey
动画帧速率不是固定的。有时我会闪烁所有LED,有时会用漂亮的淡入淡出脉冲一个-低帧速率对于闪烁来说很好(并且由于串行数据包较大而必要),但对于单个脉冲,我更喜欢更高的帧速率以获得更好的视觉效果。 - damian

0

这里有一种使用多线程的方法,与我的其他答案不同:

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);
    }
}

在上面的伪代码中,队列是一个容量为1的阻塞生产者-消费者队列。换句话说,在队列不为空时,生产者将在queue.push()处阻塞。除了使用阻塞队列外,您还可以使用带有条件变量或信号量的“乒乓缓冲区”。
每个动画帧都在相应的LED数据传输后显示。串口传输数据包所需的时间用于计算下一个动画帧。
拥有两个线程的优点是,您可以在等待串行数据传输时使用CPU进行动画制作(传输串行数据几乎不使用任何CPU)。
仅用文字很难描述这种多线程技术。我希望我有一个白板可以涂鸦。 :-)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接