Teensy 4.1 - single wire half duplex UART

Hi all,
does anyone know how single wire half duplex works on a teensy 4.1?
I couldn't find anything about that.

My project is a diy RC transmitter using a TBS crossfire transmitter module + receiver.
The communication to the transmitter works over single wire half duplex uart with a specified protocol.

Thank you
Best regards from Austria
 
Yes - There are a variety of ways to do it depending on your needs.

a) Extra hardware typically using 3 IO pins. RX, TX and and TX-Enable pin. There are a variety of ways to do it, with different chip(s) and you then yo more or less emulate the functionality of RS485 like module. There is code in HardwareSerial that if you set the TX_Enable pin you can have it automatically switch to TX whenever you try to write something out and it will then switch back to RX mode when the TX buffer is empty and the last bits have been shifted out the hardware.

b) Software. You can setup to do TX pin as bidirectional with the Uarts. I have examples of this up in my BioloidSerial code base that is an older library I updated to support Dyamixel TTL Servos which is half duplex.
You can see some of it up in my library: https://github.com/KurtE/BioloidSerial/blob/master/dxlSerial.cpp
Search for IMXRT...
But in the init code I do:
Code:
#elif defined(__IMXRT1052__) || defined(__IMXRT1062__)
// Teensy4
static IMXRT_LPUART_t *s_pkuart = nullptr;
void dxlInit(long baud, HardwareSerial* pserial, int direction_pin, int tx_pin, int rx_pin ) {
    // Need to enable the PU resistor on the TX pin
    s_paxStream = pserial;
    s_direction_pin = direction_pin;    // save away.
    if (pserial == &Serial1) s_pkuart = &IMXRT_LPUART6;
    else if (pserial == &Serial2) s_pkuart = &IMXRT_LPUART4;
    else if (pserial == &Serial3) s_pkuart = &IMXRT_LPUART2;
    else if (pserial == &Serial4) s_pkuart = &IMXRT_LPUART3;
    else if (pserial == &Serial5) s_pkuart = &IMXRT_LPUART8;
    else if (pserial == &Serial6) {s_pkuart = &IMXRT_LPUART1; Serial.println("dxlInit Serial6"); }
    else if (pserial == &Serial7) s_pkuart = &IMXRT_LPUART7;
//    else if (pserial == &Serial8) s_pkuart = &IMXRT_LPUART5;
    pserial->begin(baud);
    if (tx_pin != -1) {
        pserial->setTX(tx_pin);
    } else {
        tx_pin = 1; // default Serial 1 TX
    }
    if (rx_pin != -1) {
        pserial->setRX(rx_pin);
    }
    if (s_direction_pin == -1) {
        s_pkuart->CTRL |= LPUART_CTRL_LOOPS | LPUART_CTRL_RSRC;
/*
        s_pkuart->C1 |= UART_C1_LOOPS | UART_C1_RSRC;
        volatile uint32_t *reg = portConfigRegister(tx_pin);
        *reg = PORT_PCR_DSE | PORT_PCR_SRE | PORT_PCR_MUX(3) | PORT_PCR_PE | PORT_PCR_PS; // pullup on output pin;
*/        
    } else {
        Serial.printf("Direction Pin:%d\n", s_direction_pin);
        pserial->transmitterEnable(s_direction_pin);
    }
    setRX(0);
}
Note: I have played as much with this as I did with T3.x... So not sure yet if it might still want additional stuff to configure the TX pins port or not...

And then you need the code to switch to TX mode and RX mode, which looks something like:
Code:
void setTX(int id){
    if (s_direction_pin != -1) {
#if !defined(TEENSYDUINO)
        digitalWrite(s_direction_pin, HIGH);
#endif
        return;
    }

    if (s_pkuart) s_pkuart->CTRL |= LPUART_CTRL_TXDIR;

}



void flushAX12InputBuffer(void)  {
    // First lets clear out any RX bytes that may be lingering in our queue
    while (s_paxStream->available()) {
        s_paxStream->read();
    }
}

void setRX(int id){

    // First clear our input buffer
	flushAX12InputBuffer();
    //digitalWriteFast(4, HIGH);
    // Now setup to enable the RX and disable the TX
    // If we are using hardware direction pin, can bypass the rest...
    if (s_direction_pin != -1) {
#if !defined(TEENSYDUINO)
        // Make sure all of the output has happened before we switch the direction pin.
        s_paxStream->flush();
        digitalWrite(s_direction_pin, LOW);
#endif
        return;
    }

    // Make sure everything is output before switching.
    s_paxStream->flush();
    if (s_pkuart) s_pkuart->CTRL &= ~LPUART_CTRL_TXDIR;
}
And knowing when you wish to switch...
 
Yes - There are a variety of ways to do it depending on your needs.


Indeed - the simplest extra hardware is a wired-AND circuit. TX pin to 1N4148 cathode, RX pin to the bus wire
and to 1N4148 anode. 4k7 resistor to Vcc somewhere on the bus.

Since normal UART mode is inactive HIGH, this just works as open-drain bus.

Downsides are there's local echo that you'll have to explicitly ignore, and no explicit collision detection. If
either side powers down it jams the bus low(-ish)...

You can defeat the local echo with something like this:
half_duplex.jpg
where the "TXenable" isn't really the best name, but the same code/pinout can then work on an RS485 chip,
assuming you tristate the TXenable and use appropriate pull-down.

Many other ways to do the hardware.
 
Robotis does it a couple of different ways. The one in the OpenCM9.04 is similar to what I have done for some of my own boards...

screenshot.jpg
 
Hi,
thank you for all of your replies!

You described different ways to do it, but what do you think would be the easiest and suitable for my needs (talk to a TBS crossfire module over their specified CRSF protocol using single wire UART) ?

Sorry for my English - I am from Austria....

Best regards!
Al3xand3r05
 
Sorry I don't really much (anything) about these modules. It might help to know what unit you are trying to setup to use.

For example with one unit I did a quick look, sounded like you could configure the pins, in BRIDGE mode, and then it has a TX pin, RX pin and a RTS pin...

But assuming this is not the setup, than need to know things like Baud Rate, is 3.3v signal sufficient... If so I would probably start off with the Software version stuff I mentioned, as it takes no additional hardware. Just connect up to the Serial port TX pin and a common gnd.
 
Hi it's a tbs crossfire transmitter module + receiver. The transmitter received data from the RC (the teensy 4.1 in this case) and sends data to it (telemetry). It's a single wire half duplex UART.
The module gets +3,3V.
Baud rate is 416k
Best regards!
Al3xand3r05
 
I am not sure there is much more for me to suggest. Especially if we have not details on the protocol.

At one point earlier this year I suggested that we add half duplex support directly with the Hardware Serial classes and did an implementation of it in the Pull Request:
https://github.com/PaulStoffregen/cores/pull/419

So far this has not gone anywhere.

But my suggestion are, try a few things and see what works for you.

There is hopefully enough details in the BioloidSerial code I linked to earlier where it shows how to configure the UART to allow Half duplex. Plus code that then switches the TX pin into TX mode or RX Mode...

Alternatively when I was doing some quick and dirty hacking, I have Jerry rigged a setup using hardware that has a half duplex circuit on it, like I have done it with an OpenCM 485 expansion board. and setup to get a half duplex setup. However some of these are setup for the half duplex line be driven at 5v. Which I am not sure if your radio can handle 5v TTL levels?
 
Hi, no this module just supports 3,3V and no TTL.
There is a master and a slave.

What do you think about the onewire library - could this maybe the right for me?

Best regards!
 
Hi, no this module just supports 3,3V and no TTL.
There is a master and a slave.

What do you think about the onewire library - could this maybe the right for me?

Best regards!
 
Sorry I have no idea if it would work or not. My Impression is that 1-wire protocol: https://en.wikipedia.org/wiki/1-Wire is a slower 16.3K protocol and you says yours is at 416K and that it is a specific protocol, which is probably different than yours.

Best thing I would suggest is try it and see if it works for you.

And again if it were me, and I had the stuff to experiment with, I would probably simply try a few things to see if they work. For example lets say try hooking up one of the TX pins to your hardware, like TX3(pin 14)

Then write a quick and dirty sketch to see if you can talk to your hardware. Again I have clues of your protocol and how much is needed to start it up. But suppose there is some simple packet, like query a register, maybe there is something like Query product ID... And I would start off seeing if I could read it. Note: Again I know nothing here, like maybe you have to send out a bunch of initialization data or reset or ??? And maybe that would be first test...

Side note: when implementing something like a new protocol or the like it always helps to have a scope or logic analyzer to watch the data to see states of things and see what the actual data is...

And again I am only doing an outline of code, that probably has code issues and the like and simply to experiment with. Your final code may/probably would be very different...

But hopefully you can extract some information from this...

Code:
#define DEBUG_PIN 2
#define SERIAL_TX_PIN 14

uint8_t some_message[] = {0xff, 0xff}; // ******* need to setup message...
elapsedMillis  em_last_recv;

IMXRT_LPUART_t *s_pkuart = &IMXRT_LPUART2;  // underlying hardware UART for Serial3
void setup() {
  while (!Serial && millis() < 5000) ; // wait up to 5 seconds for terminal monitor
  pinMode(DEBUG_PIN, OUTPUT);
  // Setup Serial3 for Half duplex:
  Serial3.begin(416000); // would probably want to see how close we are
  // Lets setup that IO pin to be in half duplex
  s_pkuart->CTRL |= LPUART_CTRL_LOOPS | LPUART_CTRL_RSRC;

  // Lets try to enable PU resistor on that pin...
  *(portControlRegister(SERIAL_TX_PIN)) = IOMUXC_PAD_DSE(7) | IOMUXC_PAD_PKE | IOMUXC_PAD_PUE | IOMUXC_PAD_PUS(3) | IOMUXC_PAD_HYS;
  IOMUXC_LPUART2_TX_SELECT_INPUT = 1; // need to get to right one...
  setPortRX();
  em_last_recv = 0;
}

void setPortTX() {
  digitalWrite(DEBUG_PIN, HIGH);
  s_pkuart->CTRL |= LPUART_CTRL_TXDIR;  // Set in to TX Mode...
}

void setPortRX() {
  Serial3.flush();  // Make sure we output everything first before changing state.
  s_pkuart->CTRL &= ~LPUART_CTRL_TXDIR;  // Set in to RX Mode...
  digitalWrite(DEBUG_PIN, LOW);
}


void loop() {
  int ch;
  int received_count = 0;

  // Try receiving any data
  while (em_last_recv < 500) {
    ch = Serial3.read();
    if (ch >= 0) {
      Serial.printf("%02x ", ch);
      received_count++;
      if (!(received_count & 0x1f)) Serial.println();
      em_last_recv = 0; // reset our timeout (maybe?)
    }
  }
  if (received_count) Serial.println();

  // output new packet
  setPortTX(); // put port in TX mode
  Serial.write(some_message, sizeof(some_message));
  setPortRX();  // put back into RX mode.
  em_last_recv = 0;
}
 
Hi, thank you for your explanations! The problem is that I don't own such a transmitter module for testing and I would only buy it if I am sure that it works - it's quiet expensive for a student^^
I have a scope (100mhz analog Tektronix) but no logic analyzer.

Thank you for your example code!
I will try to understand all what's in the protocol specs and then maybe write again here.

Best regards!
Al3xand3r05
 
For the fun fo it I extended the code above to setup two Serial ports in half duplex mode (Serial3 and Serial4) and it is quick and dirty code, It is setup to have you jumper Pin 14 to pin 17. And when Serial3 outputs something Serial4 will receive it. When it get the \n character it echoes the whole string back which then Serial3 will the when it receives it will write it out to the Serial port...

Code:
#define DEBUG_PIN 8
#define LOOP_DELAY_MS 25

// Serial3 information - Say this is our Main Serial port
#define SERIALM Serial3
#define SERIALM_TX_PIN 14
IMXRT_LPUART_t *s_pkuartM = &IMXRT_LPUART2;  // underlying hardware UART for Serial3

// Lets define this as our Echo Serial port...
#define SERIALE Serial4
#define SERIALE_TX_PIN 17
IMXRT_LPUART_t *s_pkuartE = &IMXRT_LPUART3;  // underlying hardware UART for Serial3

elapsedMillis  em_last_recv;
uint8_t receive_buffer[80];
uint8_t receive_index = 0;

void setup() {
  while (!Serial && millis() < 5000) ; // wait up to 5 seconds for terminal monitor
  pinMode(DEBUG_PIN, OUTPUT);
  pinMode(13, OUTPUT);

  // Setup Serial3 for Half duplex:
  SERIALM.begin(416000); // would probably want to see how close we are
  // Lets setup that IO pin to be in half duplex
  s_pkuartM->CTRL |= LPUART_CTRL_LOOPS | LPUART_CTRL_RSRC;

  // Lets try to enable PU resistor on that pin...
  *(portControlRegister(SERIALM_TX_PIN)) = IOMUXC_PAD_DSE(7) | IOMUXC_PAD_PKE | IOMUXC_PAD_PUE | IOMUXC_PAD_PUS(3) | IOMUXC_PAD_HYS;
  IOMUXC_LPUART2_TX_SELECT_INPUT = 1; // need to get to right one...
  setPortRX();

  // Also Setup Serial4 for Half duplex:
  SERIALE.begin(416000); // would probably want to see how close we are
  // Lets setup that IO pin to be in half duplex
  s_pkuartE->CTRL |= LPUART_CTRL_LOOPS | LPUART_CTRL_RSRC;

  // Lets try to enable PU resistor on that pin...
  *(portControlRegister(SERIALE_TX_PIN)) = IOMUXC_PAD_DSE(7) | IOMUXC_PAD_PKE | IOMUXC_PAD_PUE | IOMUXC_PAD_PUS(3) | IOMUXC_PAD_HYS;
  IOMUXC_LPUART3_TX_SELECT_INPUT = 0; // need to get to right one...
  s_pkuartE->CTRL &= ~LPUART_CTRL_TXDIR;  // Set in to RX Mode...


  em_last_recv = 0;
}

void setPortTX() {
  digitalWrite(DEBUG_PIN, HIGH);
  s_pkuartM->CTRL |= LPUART_CTRL_TXDIR;  // Set in to TX Mode...
}

void setPortRX() {
  SERIALM.flush();  // Make sure we output everything first before changing state.
  s_pkuartM->CTRL &= ~LPUART_CTRL_TXDIR;  // Set in to RX Mode...
  digitalWrite(DEBUG_PIN, LOW);
}

uint32_t loop_count = 0;

void loop() {
  // Lets see if we have anything coming in on the Echo PORT
  while (SERIALE.available()) {
    uint8_t ch = SERIALE.read();
    receive_buffer[receive_index++] = ch;
    if (ch == '\n') {
      receive_buffer[receive_index] = 0;  // Null terminate.
      // BUGBUG:: Put in line instead of calls
      s_pkuartE->CTRL |= LPUART_CTRL_TXDIR;  // Set in to TX Mode...
      SERIALE.write((char*)receive_buffer);
      SERIALE.flush();
      s_pkuartE->CTRL &= ~LPUART_CTRL_TXDIR;  // Set in to RX Mode...
      receive_index = 0;
    }
    em_last_recv = 0;
  }

  // Lets see if we have anything coming in on the main port
  while (SERIALM.available()) {
    uint8_t ch = SERIALM.read();
    Serial.write(ch);
    em_last_recv = 0;
  }

  if (em_last_recv > LOOP_DELAY_MS) {
    digitalToggleFast(13);
    // output new packet
    setPortTX(); // put port in TX mode
    SERIALM.printf("Loop: %d\n", loop_count++);
    setPortRX();  // put back into RX mode.
    em_last_recv = 0;
  }
}

And I confirmed by Logic Analyzer that the Baud rate is more or less correct.
screenshot.jpg

Not much else I can do at this point.
 
@Paul and all,

As Half duplex keeps coming up in different topics, I thought about migrating the half duplex support I did in another Pull request that had additional stuff, much of which was already merged in...
So I have created a new branch: https://github.com/KurtE/cores/tree/serial_half_duplex

Which I think I migrated the half duplex support in from that other PR, which I will close out.

The idea is to add a new format to the format defines for SerialX.begin

Something like: Serial1.begin(1000000, SERIAL_HALF_DUPLEX);
And it takes care of changing the the right register to allow half duplex, plus sets up that when you do a TX it switches to TX mode and when the ISR comes in saying we have no more to transmit it switches back to RX mode.

For T3.x it is pretty clean as I do the setup in begin and sets up the same variables used by transmitterEnable to set or clear the GPIO pin, but in this case the register and mask are setup for the bitband address of the correct bit of the register that controls the direction... So no changes to the TX or ISR needed.

For T4.x not as easy, as we don't have bitband. Instead the transmitterEnable code uses the two different registers that Set or Clear bits in the Port register associated with the pin. Unfortunately we don't have set and clear on the Uarts CTRL register so have to set or clear that bit manually...

Note: without these types of changes, what I have done in the past in some libraries like my Dynamixel Library (bioloidSerial) is to something like:
Code:
#if defined(KINETISK) || defined (KINETISL)
static KINETISK_UART_t *s_pkuart = nullptr;
void dxlInit(long baud, HardwareSerial* pserial, int direction_pin, int tx_pin, int rx_pin ) {
    // Need to enable the PU resistor on the TX pin
    s_paxStream = pserial;
    s_direction_pin = direction_pin;    // save away.
    if (pserial == &Serial1) s_pkuart = &KINETISK_UART0;
    else if (pserial == &Serial2) s_pkuart = &KINETISK_UART1;
    else if (pserial == &Serial3) s_pkuart = &KINETISK_UART2;
#if defined(__MK64FX512__) || defined(__MK66FX1M0__) 
    else if (pserial == &Serial4) s_pkuart = &KINETISK_UART3;
    else if (pserial == &Serial5) s_pkuart = &KINETISK_UART4;
#endif
...
Which again can be made to work. But it also has an unintended consequence. That is the current versions of Teensyduino are developed, such that if you don't use a SerialX object, then that object is not create, nor any of the other data structures associated with it, like RX and TX Buffers.

But this code references all of them, even though only one of them will actually be used... But all are now included in the Sketch...

The #else for the T4.xs is similar but different actual registers and structure...

For the fun of it, I started playing with a test sketch to see if I can avoid bringing in all of the Serial objects, starting off with T4.x... All of the data needed is actually stored to do this is contained within the SerialX object... And it is more complex than T3.x as we may need to also setup the Hardware Input Select register, depending on the pin...

I have done a similar hack before for SPI, but that was easier as it is a base class currently with no Vtable so I know the first entry is the pointer to the LPSPI object and the next is a pointer to our Hardware Structure.
But Serial is a sub-class of Stream which is a sub-class of Print... So playing around to figure out where it is... Have a sketch that properly can now print out some of the information...

Code:
extern void printSerData(HardwareSerial *pserial);

uint8_t index_imxrt_lpuart = -1;
void setup() {
  while (!Serial) ;
  Serial.begin(115200);
  Serial1.begin(115200);
  delay(50);
  uint32_t *pserial = (uint32_t*)&Serial1;
  Serial.printf("PS:%x PU:%x PH:xx\n", &Serial1,
                (uint32_t*)&IMXRT_LPUART6/*, &UART6_Hardware*/);
  for (uint8_t i = 0; i < 10; i++) {
    Serial.printf("%d:%x\n", i, pserial[i]);
    if ((pserial[i] >= (uint32_t)&IMXRT_LPUART1) && (pserial[i] <= (uint32_t)&IMXRT_LPUART8)) {
      index_imxrt_lpuart = i;
      Serial.println("    imxrt_lpuart index");
    }
  }
  printSerData(& Serial1);
  printSerData(& Serial2);
  printSerData(& Serial3);
  printSerData(& Serial4);
  printSerData(& Serial8);

}

void printSerData(HardwareSerial *pserial) {
  pserial->begin(115200); // make sure we have access.
  uint32_t *p32 = (uint32_t *)pserial;
  IMXRT_LPUART_t *port = (IMXRT_LPUART_t*)p32[index_imxrt_lpuart];
  HardwareSerial::hardware_t * phard = (HardwareSerial::hardware_t *)p32[index_imxrt_lpuart + 1];
  Serial.println("---------------------------------------------------------------");
  Serial.printf("%x %x %x\n", (uint32_t*)pserial, (uint32_t)port, (uint32_t)phard);
  Serial.flush();
  Serial.printf("    B:%x ST:%x CT: %x\n", port->BAUD, port->STAT, port->CTRL);
  Serial.flush( );
  Serial.printf("    I:%d P:%d %x %x\n", phard->serial_index, phard->tx_pins[0].pin,
               (uint32_t)phard->tx_pins[0].select_input_register, phard->tx_pins[0].select_val);
  Serial.flush();

}

void loop() {
}

Output from sketch:
Code:
PS:200007fc PU:40198000 PH:xx
0:20000124
1:0
2:3e8
3:0
4:40198000
    imxrt_lpuart index
5:20000140
6:0
7:200017b8
8:200017f8
9:0
---------------------------------------------------------------
200007fc 40198000 20000140
    B:19000008 ST:c00000 CT: 3c0000
    I:0 P:1 401f8554 0
---------------------------------------------------------------
200008c0 40190000 2000085c
    B:19000008 ST:800000 CT: 3c0000
    I:1 P:8 401f8544 2
---------------------------------------------------------------
20000920 40188000 20000980
    B:19000008 ST:800000 CT: 3c0000
    I:2 P:14 401f8530 1
---------------------------------------------------------------
20000a48 4018c000 200009e4
    B:19000008 ST:800000 CT: 3c0000
    I:3 P:17 401f853c 0
---------------------------------------------------------------
20000b0c 40194000 20000aa8
    B:19000008 ST:c00000 CT: 3c0000
    I:7 P:35 401f854c 1

One might call this a little bit of a hack ;) :0

As for T3.x/LC - not so simple as our Serial objects are not controlled by structures like this. They all have their own code with hard coded registers... So not sure how I would hack this one and remove the look at the Serial pointer and have my own table to drive it....

But now to testing some of the above new branch, and then maybe do a PR...
 
@KurtE
I've tested your above half duplex sketch on a Teensy 4.1 and the loopback between TX3 and TX4 seems to work well. I'd like to use this code to control a Dynamixel AX12 but I'm not sure how best to do the level shifting (3.3V to 5v and back). I'm cautious about driving a 5V TTL with a non 5V tolerant 3.3V UART. Do you have any recommendations on the simplest hardware and circuit to achieve this? I've got a 2 channel Sparkfun level shifter but not sure if I can configure it for half duplex operation.
Also do you think the new format eg Serial1.begin(1000000, SERIAL_HALF_DUPLEX); will be available soon?
 
as for the new HALF_DUPLEX, hopefully Paul will pull it in for the next beta...

Best way to setup to do half duplex to control AX12 servos. I have been doing it several different ways... Which way is best? Not sure.

Example one of my later boards I used a: TC7WT141FU chip

screenshot.jpg

I also played around with a larger chip:
screenshot3.jpg

Also have done with a couple of transistors which is closer to the Robotis schematic, like:
screenshot2.jpg

But for quick and dirty, you can also just try simple plug in... Like: https://www.sparkfun.com/products/12009
Simply connect the low level side to teensy TX pin and the High level to go out to DXLs...
 
@KurtE Update on this. I got the AX12 half duplex working on a Teensy 4.1 using the Bioloid library but found that it only works on Serials 2-7. For Serial1 I never receive a response from the AX12 commands. Not sure why. It's not a problem as I only need 1 x half duplex port but I thought I'd let you know in case there is an issue specific to the 4.1
 
@Al3xand3r05
Hi. Any update on the crossfire crsf communication? I'm doing research for a robot project, and was hoping if I could use the TBS Tango 2 radio with telemetry..
 
@Al3xand3r05 || @all

Any updates on this topic?
I'm interested in interpreting CRSF with a Teensy. One could also look up in the Betaflight, ExpressLRS or other projects source code:

https://github.com/betaflight/betaflight/blob/master/src/main/rx/crsf.c
https://github.com/cleanflight/cleanflight/blob/master/src/main/rx/crsf.c
https://github.com/dncoder/crsf-link-tester


[...]

CRSF (Crossfire)
CRSF is developed by TBS for their Crossfire RC system. It’s similar to SBUS or other digital RX to FC protocols.

The main advantages include fast update rate and two-way communication capabilities, allowing features such as hassle free Telemetry to be injected into the radio link with no additional UART port required.
[...]

It can be used in halfduplex one wire and fullduplex two wire mode: [...]The UART runs at 416666baud 8N1 at 3.0 to 3.3V level.[...]

Thanks
 
I can just imagine this can be brought over to One wire as I have been studying the code. why not take advantage of the buffers when you can and DMA for the over flow.
 
Back
Top