2

THE GOAL

In my Qt application, I need to control a GPIO pin, depending on data being sent over the serial bus. So, I need to set it to HIGH for as long as I transmit data, and to LOW, immediately after the transmission ends. Consider it as a serial communication flow control pin, which when set to 1 it enables transmission, and when set to 0 enables receive of data. The entire system is half-duplex and communicates in a master-slave fashion.

THE PROBLEM

I managed to come close to a solution, by setting it to HIGH immediately before any transmission, introducing some constant delay (I used QThread:usleep() ) depending on the baud rate and then setting it to low again, but I was getting random "stretchings" of the pulse (staying HIGH longer than it should) when I was visualizing it with an oscilloscope.

ATTEMPTED SOLUTIONS

Well, it seems that some "magic" is taking place, which adds some extra delay, on top of the one I have manually defined. In order to get rid of that possibility, I used the bytesWritten() signal, so I can fire my setPinLow() slot when we finish writing the actual data to the port. So my code now looks like this:

classTTY::classTTY(/*someStuff*/) : port(/*some other stuff*/)
{
    s_port = new QSerialPort();
    connect(s_port, SIGNAL(bytesWritten(qint64)), this, SLOT(setPinLow()));

    if(GPIOPin->open(QFile::ReadWrite | QFile::Truncate | QFile::Text | QFile::Unbuffered)) {
        qDebug() << "GPIO pin ready to switch.";
    } else {
        qDebug() << "Failed to access GPIO pin";
    }

bool classTTY::sendData(data, replyLength)
{
    directionPinEnable(true);

    if(m_port->isOpen()) {
        s_expectedReplyLength = replyLength;
        s_receivedData.clear();

        s_port->flush();
        s_port->write(data);

        return true;
    }

return false;
}

void classTTY::setPinLow()
{
    gpioPinEnable(false);
}

void classTTY::gpioPinEnable(bool enable){

    if(enable == true){
        GPIOPin->write("1");
    } else if (enable == false) {
        GPIOPin->write("0");
    }
}

After implementing it the pin started to give really short pulses, much more like "spikes", which implies (I think) that now it stays HIGH for as long as the Qt write() process lasts, and not while the actual propagation of the data lasts.

THE QUESTION(S)

  1. What is that extra delay being added when I use the naive, QThread::usleep approach, that causes the stretch of the pulse?
  2. Why the signal-slot approach is not working, since it is event-driven?
  3. In general, how can I instruct the pin to go active ONLY during the transmission of data and then drop again to zero, so I can receive the slave's reply?
Arkoudinos
  • 1,099
  • 12
  • 20
  • 2
    Propably the fact that you are on a normal operating system and scheduling gets in your way. If you need high timing accurracy, try a real time operating system – Felix Sep 27 '18 at 19:56
  • 1
    I don't know the workings of Qt, but my guess would be that using `QThread::usleep` allows other threads to run, so you're getting the sleep on your thread plus some runtime on another thread before you get back to yours. – Ed King Sep 27 '18 at 21:49
  • Because Linux is not an RTOS, and UART serial data can contain inter-character space, not only may your delay be stretched, but also the serial burst, so your method is seriously flawed. The GPIO control should be performed at the serial driver level, dropping it when the input buffer and UART FIFO/TX become empty. But that seems unnecessary; this is a master-slave relationship. The slave should not reply until asked, and the master will wait (with some appropriate timeout) for that reply. The data itself is already the flow control. – Clifford Sep 28 '18 at 06:54
  • @Clifford "But that seems unnecessary; this is a master-slave relationship." Exactly. This is also why I tried the second approach. When all of the bytes are written, set the direction pin back to 0 (so we can now receive the slave's reply). TBH, I cannot really see a reason why this doesn't work... – Arkoudinos Sep 28 '18 at 08:15
  • Using delays is problematic. Does Qt have something equivalent to the [**tcdrain()** function](https://www.ibm.com/support/knowledgecenter/en/SSLTBW_2.1.0/com.ibm.zos.v2r1.bpxbd00/rttcd.htm)? – sawdust Sep 28 '18 at 09:31

1 Answers1

1
  1. What is that extra delay being added when I use the naive, QThread::usleep approach, that causes the stretch of the pulse?

Linux is not a real-time operating system a thread sleep suspends the process fo no less than the time specified. During the sleep, other threads and processes may run and may not yield the processor for a longer time than your sleep period, or may not yield at all and consume their entire OS allocated time-slice. Beside that kernel driver interrupt handlers will always preempt a user-level process. Linus has a build option for real-time scheduling, but the guarantees remain less robust that a true RTOS and latencies typically worse.

Note also that not only can your thread be suspended for longer than the sleep period, but the transmission may be extended by more than the number of bits over baud-rate - the kernel driver can be preempted by other drivers and introduce inter-character gaps over which you have no control.

  1. Why the signal-slot approach is not working, since it is event-driven?

The documentation for QSerialPort::waitForBytesWritten() states:

This function blocks until at least one byte has been written to the serial port and the bytesWritten() signal has been emitted.

So it is clear that the semantics of this are that "some data has been written" rather than "all data has been written". It will return whenever a byte is written, then if you call it again, it will likely return immediatly if bytes are continuing to be written (because QSerialPort is buffered and will write data independently of you application).

  1. In general, how can I instruct the pin to go active ONLY during the transmission of data and then drop again to zero, so I can receive the slave's reply?

Qt is not unfortunately the answer; this behaviour needs to be implemented in the serial port kernel driver or at least at a lower-level that Qt. The Qt QSerialPort abstraction does not give you the level of control or insight into the actual occurrence "on the wire" that you need. It is somewhat arms-length from the hardware - for good reason.

However there is a simple solution - don't bother! it seems entirely unnecessary. It is a master-slave communication, and as such the data itself is flow control. The slave does not talk until spoken to, and the master must expect and wait for a reply after it has spoken. Why does the slave need any permission to speak other than that implied by being spoken to?

Clifford
  • 88,407
  • 13
  • 85
  • 165
  • *"the kernel driver can be preempted by other drivers and introduce inter-character gaps"* -- Unlikely when DMA is used. *"Why does the slave need any permission to speak other than that implied by being spoken to?"* -- Maybe this GPIO controls the (e.g. RS-485) transceiver? – sawdust Sep 28 '18 at 09:29
  • @sawdust : Not all embedded Linux implementations use DMA for serial I/O. I worked on a project on an Atmel ARM using Atmel's Linux distro - between versions the serial driver was changed inexplicably from DMA enabled, to interrupt driven and our application could no longer sustain the 230400 baud we were using. Point taken though; if DMA is enabled it is unlikely. The point remains that it is not an RTOS and therefore not entirely deterministic, so the application level delay is not likely to match the transmission burst length for _a number_ of reasons of varying likelihood and impact. – Clifford Sep 28 '18 at 09:41
  • *"I worked on a project on an Atmel ARM using Atmel's Linux"* -- I think I know what you're referring to. There was an omission in the device tree. *"The point remains that it is not an RTOS"* -- As long as the message to be transmitted is written in a single syscall, and a reasonable baudrate is involved, then intercharacter idle time is unlikely. The lack of an RTOS is not the root cause of the problem. Using delays or sleeping is simply the wrong method of syncing with the end of a transmission. – sawdust Sep 28 '18 at 09:58
  • @sawdust : I don't think we disagree - the mention of an RTOS was merely to point out that you cannot expect Linux to behave deterministically, not because using an RTOS is the solution. It was part of the explanation of why it does not work, not a suggested solution. And moreover, it was mentioned in the context of the sleep delay, not the inter-character space. Even in an RTOS it would be a poor method, but might be made to work given appropriate priority assignment. – Clifford Sep 28 '18 at 10:11
  • @sawdust "Maybe this GPIO controls the (e.g. RS-485) transceiver?" Bull's eye. The slave does not need permission to talk, but I need to set the GPIO pin to 0 in order to be able to listen to him. Clifford, you're right, I need to go closer to the hardware. Modifying the serial driver is the only way? – Arkoudinos Sep 28 '18 at 14:09
  • @Arkoudinos -- See [automatically changing RTS for RS-485 communication](https://stackoverflow.com/questions/25250731/automatically-changing-rts-for-rs-485-communication) – sawdust Sep 28 '18 at 20:21