Hi,
We are using a Dual Tristate buffer to pull the Teensy Serial Output to 5V and to simultaneously create a one Wire communication line (Nexperie 74HC2G125).
This relies on the Tx Enable pin, much like the RS485 transceivers; the only difference is we stay on 0~5V levels with reference to GND.
What we're seeing on random occasions is that TX Enable is brought low, before the last character is transmitted. I attach a picture from our Logic Analyser.
- Channel 2 is Teensy Serial2 TX pin (that goes to the tri state buffer)
- Channel 3 is the Actual output after the Tri State buffer
- Channel 4 is the pin configured as Tx Enable using Serial2.transmitterEnable(...)
- Channel 5 can be ignored for the purpose of this post.
We've seen this happen frequently at 1Mbps, always with the last character; at lower speed (57k) this has not happened at all.
You can see the last character does not go through the Tri State buffer, which is consistent with the TX Enable being brought LOW too early, while the last byte is being transmitted by Teensy.
This should not be happening; as I mentioned, TX Enable is being managed by the Teensyduino library (set via the transmitterEnable pin) and as such was expected it to be held high during transmission.
After several hours of debugging and finally looking at the code in serial2.c we came across a "suspect" for a Race condition as explained:
- The code relies on checking the following, in the ISR, to determine of it's time to turn off the TX Enable pin.
UART_C2_TCIE interrupt is only enabled after we determined that we have no more characters to put on the TX buffer and thus we change UART1_C2 = C2_TX_COMPLETING (which enables TCIE)
In the following s:
- Assume that in last ISR call, we had nothing more to transmit and we enter TX_COMPLETING enabling the TCIE interrupt
1- If we then call serial2_putchar(..), to add a new character for transmission, before the next ISR call,
the serial2_putchar function will
a) re-assert the TX Enable,
b) check if our software buffer is full and if it is
b.1) wait on UART1_S1 & UART_S1_TDRE to transmit data so that it can free up space in the buffer
b.2) get the next character from the software buffer to be transmitted
b.3) assign the character to UART1_D, to effectively transmit.
c) save the new character we're passing as a parameter in the software buffer
d) Finally, it will reset the UART1_C2 to UART1_C2 = C2_TX_ACTIVE, which effectively disables the TCIE interrupt and enables the TDRE interrupt instead.
If we were in TX_COMPLETING state, with TCIE enabled, and the TC flag is raised while serial2_putchar is being executed, the TC flag appears to remain asserted in the S1 register, because we've done nothing clear it, despite the fact that we're adding more data to be sent.
(See datasheet page 1214: depending on whether the software buffer is full we may or may not read UART1_S1 but even if we do, there is still time between reading the UART1_S1 and writing to D; this is not atomic and as such won't implicitly reset TC.)
3- In the next ISR call, we have UART1_C2 = C2_TX_ACTIVE and it treats the TDRE flag. Because there are no other characters to transmit, it will change to UART1_C2 = C2_TX_COMPLETING, enabling TCIE again.
Because the ISR does not Return on each case it checks, after setting UART1_C2 = C2_TX_COMPLETING it will fall through to the final check of
The problem here is that we may have a leftover TC flag in UART1_S1 as expained above because S1 was not cleared when we added the new character.
It appears this condition may be evaluated as true which will deassert TxEnable too early, causing the last character to be not be transmitted by the Tri State buffer.
It is important to understand that TDRE firing means the buffer needs more data; however data may still be in the shift register. That's why we change to wait for TCIE after TDRE and wait for TCIE before disabling TXEnable.
There appear to be 2 ways to go about this:
- Either UART1_C2 needs to get set much earlier in serial2_putchar which disables the TCIE interrupt and prevents a the TC bit from being set. (maybe we also need to actively make sure we clear the TC flag anyway?)
- Or we don't fall through on the ISR. We actively RETURN from the function after treating each case, but it's not clear if this would work; there would still be a leftover (wrong) TC flag; also I'm not sure if this is a good policy because I'm not familiar enough with the way ISRs work in this architecture and we may miss something.
We're completely in the dark on this one, but even if this is not the cause of our problem, it is definitely worth looking into, because it does seem to be a potential race conditions.
Your thoughts would be greatly appreciated.
Thank you
We are using a Dual Tristate buffer to pull the Teensy Serial Output to 5V and to simultaneously create a one Wire communication line (Nexperie 74HC2G125).
This relies on the Tx Enable pin, much like the RS485 transceivers; the only difference is we stay on 0~5V levels with reference to GND.
What we're seeing on random occasions is that TX Enable is brought low, before the last character is transmitted. I attach a picture from our Logic Analyser.
- Channel 2 is Teensy Serial2 TX pin (that goes to the tri state buffer)
- Channel 3 is the Actual output after the Tri State buffer
- Channel 4 is the pin configured as Tx Enable using Serial2.transmitterEnable(...)
- Channel 5 can be ignored for the purpose of this post.
We've seen this happen frequently at 1Mbps, always with the last character; at lower speed (57k) this has not happened at all.
You can see the last character does not go through the Tri State buffer, which is consistent with the TX Enable being brought LOW too early, while the last byte is being transmitted by Teensy.
This should not be happening; as I mentioned, TX Enable is being managed by the Teensyduino library (set via the transmitterEnable pin) and as such was expected it to be held high during transmission.
After several hours of debugging and finally looking at the code in serial2.c we came across a "suspect" for a Race condition as explained:
- The code relies on checking the following, in the ISR, to determine of it's time to turn off the TX Enable pin.
Code:
if ((c & UART_C2_TCIE) && (UART1_S1 & UART_S1_TC)) {
UART_C2_TCIE interrupt is only enabled after we determined that we have no more characters to put on the TX buffer and thus we change UART1_C2 = C2_TX_COMPLETING (which enables TCIE)
In the following s:
- Assume that in last ISR call, we had nothing more to transmit and we enter TX_COMPLETING enabling the TCIE interrupt
1- If we then call serial2_putchar(..), to add a new character for transmission, before the next ISR call,
the serial2_putchar function will
a) re-assert the TX Enable,
b) check if our software buffer is full and if it is
b.1) wait on UART1_S1 & UART_S1_TDRE to transmit data so that it can free up space in the buffer
b.2) get the next character from the software buffer to be transmitted
b.3) assign the character to UART1_D, to effectively transmit.
c) save the new character we're passing as a parameter in the software buffer
d) Finally, it will reset the UART1_C2 to UART1_C2 = C2_TX_ACTIVE, which effectively disables the TCIE interrupt and enables the TDRE interrupt instead.
If we were in TX_COMPLETING state, with TCIE enabled, and the TC flag is raised while serial2_putchar is being executed, the TC flag appears to remain asserted in the S1 register, because we've done nothing clear it, despite the fact that we're adding more data to be sent.
(See datasheet page 1214: depending on whether the software buffer is full we may or may not read UART1_S1 but even if we do, there is still time between reading the UART1_S1 and writing to D; this is not atomic and as such won't implicitly reset TC.)
3- In the next ISR call, we have UART1_C2 = C2_TX_ACTIVE and it treats the TDRE flag. Because there are no other characters to transmit, it will change to UART1_C2 = C2_TX_COMPLETING, enabling TCIE again.
Because the ISR does not Return on each case it checks, after setting UART1_C2 = C2_TX_COMPLETING it will fall through to the final check of
Code:
if ((c & UART_C2_TCIE) && (UART1_S1 & UART_S1_TC)) {
The problem here is that we may have a leftover TC flag in UART1_S1 as expained above because S1 was not cleared when we added the new character.
It appears this condition may be evaluated as true which will deassert TxEnable too early, causing the last character to be not be transmitted by the Tri State buffer.
It is important to understand that TDRE firing means the buffer needs more data; however data may still be in the shift register. That's why we change to wait for TCIE after TDRE and wait for TCIE before disabling TXEnable.
There appear to be 2 ways to go about this:
- Either UART1_C2 needs to get set much earlier in serial2_putchar which disables the TCIE interrupt and prevents a the TC bit from being set. (maybe we also need to actively make sure we clear the TC flag anyway?)
- Or we don't fall through on the ISR. We actively RETURN from the function after treating each case, but it's not clear if this would work; there would still be a leftover (wrong) TC flag; also I'm not sure if this is a good policy because I'm not familiar enough with the way ISRs work in this architecture and we may miss something.
We're completely in the dark on this one, but even if this is not the cause of our problem, it is definitely worth looking into, because it does seem to be a potential race conditions.
Your thoughts would be greatly appreciated.
Thank you
Last edited: