Porting Teensy 3 library to Teensy 4

danieltsz

Well-known member
Hi, I'm trying to port this library SDI-12 for Teensy 3 to Teensy 4.1.

My problem is almost all of the components are from Kinetis.h of Teensy 3, while Teensy 4 is using imxrt.h. One component of the library from kinetis.h is IRQ_UART0_STATUS to IRQ_UART3_STATUS, then looking back to imxrt.h, there is no IRQ for uart status. Can you guide me on how to migrate the IRQ_Number_t of Kinetis.h to Imxrt.h?
 
Looks like the owner of that library @duff has not logged in here for over two and a half years now, and similarly very little on github during that timeframe as well. It looks like that library is specifically setup for T3.1 and T3.2. although I would think works on T3.5 and 3.6 as well.

It looks like it is bypassing most of the Serial code and is hard coded to certain pins and the like, as you can see in their initialization:

With the T4.x code base, all of the Uarts are handled by one class (HardwareSerial.cpp), and the constructors are given some pointers to some Hardware specific details about each uart. For example Serial1 object is defined in HardwareSerial1.cpp
C++:
const HardwareSerial::hardware_t UART6_Hardware = {
    0, IRQ_LPUART6, &IRQHandler_Serial1,
    &serialEvent1,
    CCM_CCGR3, CCM_CCGR3_LPUART6(CCM_CCGR_ON),
    #if defined(ARDUINO_TEENSY41)
    {{0,2, &IOMUXC_LPUART6_RX_SELECT_INPUT, 1}, {52, 2, &IOMUXC_LPUART6_RX_SELECT_INPUT, 0}},
    {{1,2, &IOMUXC_LPUART6_TX_SELECT_INPUT, 1}, {53, 2, &IOMUXC_LPUART6_TX_SELECT_INPUT, 0}},
    #else
    {{0,2, &IOMUXC_LPUART6_RX_SELECT_INPUT, 1}, {0xff, 0xff, nullptr, 0}},
    {{1,2, &IOMUXC_LPUART6_TX_SELECT_INPUT, 1}, {0xff, 0xff, nullptr, 0}},
    #endif
    0xff, // No CTS pin
    0, // No CTS
    IRQ_PRIORITY, 38, 24, // IRQ, rts_low_watermark, rts_high_watermark
    XBARA1_OUT_LPUART6_TRG_INPUT    // XBar Tigger
};
HardwareSerial Serial1(IMXRT_LPUART6_ADDRESS, &UART6_Hardware, tx_buffer1, SERIAL1_TX_BUFFER_SIZE,
    rx_buffer1,  SERIAL1_RX_BUFFER_SIZE);

From this you can see that it uses LPUART6.
The IRQ is: IRQ_LPUART6
The register you need to use to enable access to this device: CCM_CCGR3 with the value CCM_CCGR3_LPUART6(CCM_CCGR_ON)

If you are using the default pins (0, 1), you need to setup pin 1 into mode 2.
And since it is also used for input, there is another register: IOMUXC_LPUART6_TX_SELECT_INPUT which must be set to 1

Our Serial objects code has support for half duplex. So look through the code for where half_duplex_mode_ is used.
For example:
Code:
    if (half_duplex_mode_) ctrl |= (LPUART_CTRL_LOOPS | LPUART_CTRL_RSRC);

    // write out computed CTRL
    port->CTRL = ctrl;
and:

Code:
if(half_duplex_mode_) {       
        __disable_irq();
        port->CTRL |= LPUART_CTRL_TXDIR;
        __enable_irq();
        //digitalWriteFast(2, HIGH);
    }

As for process the status in ISR, I have not looked over his code enough to know what he needed to change:
But our code is in the method:
Code:
void HardwareSerial::IRQHandler()
{
    //digitalWrite(4, HIGH);
    IMXRT_LPUART_t *port = (IMXRT_LPUART_t *)port_addr;
...

For Fast GPIO to PIN1,
Code:
SET        = CORE_PIN1_PORTSET;
CLEAR      = CORE_PIN1_PORTCLEAR;
#STATUS_REG = &UART0_S1;
BITMASK    = CORE_PIN1_BITMASK;

Hope that helps. Good luck.
 
Thank you very much for the help sir @KurtE , I will try to add check which library is better for Teensy 4.1, this library which uses softwareserial , or sir @duff's library which uses Hardware serial. This is the last library that hinders me from porting all of my programs from Arduino to Teensy. Hope I can tweak any of this library
 
Colin's library is the better hardware-based approach, but more difficult to port to new hardware.

The software bit bashing library looks pretty simple.
 
hi sir @PaulStoffregen sorry for the other thread. That thread is for the same application but a different library. Either way, can I ask how to use the timer control register of Teensy 4.1?

Code:
#if defined(__AVR_ATmega168__) || defined(__AVR_ATmega328P__) || \
  defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) ||  \
  defined(__AVR_ATmega644P__) || defined(__AVR_ATmega644__) ||   \
  defined(__AVR_ATmega1284P__) || defined(__AVR_ATmega1284__)

/**
 * @brief The value of timer control register 2A prior to being set for SDI-12.
 */
static uint8_t preSDI12_TCCR2A;
/**
 * @brief The value of timer control register 2B prior to being set for SDI-12.
 */
static uint8_t preSDI12_TCCR2B;

#if F_CPU == 16000000L

void SDI12Timer::configSDI12TimerPrescale(void) {
  preSDI12_TCCR2A = TCCR2A;
  preSDI12_TCCR2B = TCCR2B;
  TCCR2A = 0x00;  // TCCR2A = 0x00 = "normal" operation - Normal port operation, OC2A &
                  // OC2B disconnected
  TCCR2B = 0x07;  // TCCR2B = 0x07 = 0b00000111 - Clock Select bits 22, 21, & 20 on -
                  // prescaler set to CK/1024
}
void SDI12Timer::resetSDI12TimerPrescale(void) {
  TCCR2A = preSDI12_TCCR2A;
  TCCR2B = preSDI12_TCCR2B;
}

This line of code uses the TCR of atmega, I'm trying to implement this using Teensy 4.1.
I did try to add this lines but i'm not sure if this is the correct registers and values for the prescaler
Code:
#elif defined(ARDUINO_TEENSY41)

/**
 * @brief The value of timer control register 0~3 prior to being set for SDI-12.
 */
static uint32_t preSDI12_TCCR0;
static uint32_t preSDI12_TCCR1;
static uint32_t preSDI12_TCCR2;
static uint32_t preSDI12_TCCR3;

#if F_CPU == 150000000L   //150Mhz

void SDI12Timer::configSDI12TimerPrescale(void) {
    preSDI12_TCCR0 = TCCR0;  //not sure about this timer control register
    preSDI12_TCCR1 = TCCR1;  //not sure about this timer control register
    preSDI12_TCCR2 = TCCR2;  //not sure about this timer control register
    preSDI12_TCCR3 = TCCR3;  //not sure about this timer control register

    TCCR0 = 0x00;  // TCCR0 = 0x00 = "normal" operation - Normal port operation, OC2A & OC2B disconnected
                   // This line is used for atmega

    TCCR1 = 0x07;  // TCCR1 = 0x07 = 0b00000111 - Clock Select bits 22, 21, & 20 on - prescaler set to CK/1024
                   // This line is used for atmega

    TCCR2 = 0x00;  // TCCR2 = 0x00 = "normal" operation - Normal port operation, OC2A & OC2B disconnected
                   // This line is used for atmega

    TCCR3 = 0x00;  // TCCR3 = 0x07 = 0b00000111 - Clock Select bits 22, 21, & 20 on - prescaler set to CK/1024
                   // This line is used for atmega
}
void SDI12Timer::resetSDI12TimerPrescale(void) {
    TCCR0 = preSDI12_TCCR0;
    TCCR1 = preSDI12_TCCR1;
    TCCR2 = preSDI12_TCCR2;
    TCCR3 = preSDI12_TCCR3;
}
 
Using AVR timer names like TCCR0 won't work.

Here's an attempt to port that library using the ARM cycle counter. Rather than try to implement the fudge factor, I just replaced the bitTimes() function with an accurate version using floating point, since we have a FPU.

I ran this here on Teensy 4.1 with the "d_simple_logger" example. I don't have any sensors, but this is the waveform I see on pin 7. Those narrow pulses look like almost 1ms, so probably pretty close to 1200 baud.

Before trying to contribute this back to the original library, I'm really depending on you to share feedback whether it really works?

1700573430778.png
 
Remember Teensy 4.1 is not 5 volt tolerant. If the sensor sends 5 volts, you must use some sort of level shift to prevent it from damaging your Teensy 4.1.
 
Good day sir @PaulStoffregen , I already tried the library but first, I tried to test TXB0108 if it will work with 5v to 3v3 so I tried to test it first with Arduino Mega UART

1. Connected Arduino Mega2560 Serial3 TX RX to TXB0108
2. Connected TXB0108 to Serial1 of Teensy 4.1

for Arduino Mega, this is my program
Code:
String test;
void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  Serial3.begin(9600);
  Serial3.setTimeout(100);
}

void loop() {
  // put your main code here, to run repeatedly:
  if (Serial3.available() > 0) {
    while (Serial3.available()) {
      test = Serial3.readString();
    }
    Serial.println(test);
  }
  test = "";
}

And for Teensy 4.1
Code:
void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  Serial1.begin(9600);
}

void loop() {
  // put your main code here, to run repeatedly:
  Serial1.print("TEENSY4.1 TO ARDUINO MEGA 2560");
  delay(1000);
}

And this is the result:
Teensy.PNG


TXB0108 works so now I tried to connect my sensor first with Arduino Mega to test the output. Here is the output with Arduino Mega:
SDI.PNG


It works so now I tried to connect my sensor with Teensy 4.1 to test the output. Here is the output with Teensy 4.1:
TeensySDI.PNG


No data output. What do I need to check to help in testing the library sir @PaulStoffregen? Or do I need to replace TXB0108 with different Level Shifter?

Thank you in advance.
 
Last edited:
Update, I tried to measure Pin 12 (SDI-12 pin connected to Arduino Mega 2560) using a voltmeter, it measures 1v during idle, and when I hit enter, it registers 2v. Based on SDI12 standard it should be 0v and 5v for mark and space but either way, I tried to directly connect my sensor to Pin 1 of Teensy 4.1 (I edited the data pin to pin 7) and uploaded the program but still no response from the sensor.

To check if I fried my Pin 1 (Serial1 TX), I re-uploaded my program to test the connection with Teensy 4.1 and SDI-12, and it still works :)
 
Or you could try to discover what is wrong or missing. Maybe add Serial.print() within the receive code and compare results between Teensy and Mega?

But I can not do this for you. I don't have the real hardware. I've done everything I can. You need to take it from here.
 
Hmmm, I have been thinking about trying to port this library over to the T4 for a project on monitoring the soil moisture using the a couple of decagon SDI12 soil moisture, EC and temp senors I have. Maybe now is good time to try to do this but I have been out of the programming game for a couple of years now. I'll see what I can do but it's going to take me a bit to get up to speed on all the new Teensy Hardware.
 
I just got my Teensy's ordered! Was looking at the ref manual and see that at least some of the lp-uarts have the "single-wire mode" that we need for SDI12 communications, so that is a good start. I haven't really dug into the T4's serial code in the core yet but it looks promising we can get a fully hardware solution to this.
 
Using AVR timer names like TCCR0 won't work.

Here's an attempt to port that library using the ARM cycle counter. Rather than try to implement the fudge factor, I just replaced the bitTimes() function with an accurate version using floating point, since we have a FPU.

I ran this here on Teensy 4.1 with the "d_simple_logger" example. I don't have any sensors, but this is the waveform I see on pin 7. Those narrow pulses look like almost 1ms, so probably pretty close to 1200 baud.

Before trying to contribute this back to the original library, I'm really depending on you to share feedback whether it really works?

View attachment 32429
I found an issue with inverted serial and half duplex mode where the current serial setup configures the pin for pull-up which causes an issue when you change the TX pin to RX by TXDIR bit in LPUART ctrl register. Using the sketch below and looking at the trace picture below you can see that when Serial1 finishes transmitting and switches the pin direction the state is held HIGH when it should be LOW after transmission and during idle time.

For this test Serial1 TX connects to Serial2 RX where Serial1 is configured as half duplex RX_TX inverted and Serial2 is just inverted for testing sake.

Also you will see the recieved serial output is not right. In the trace you can clearly see that at least on my scope's setup to decode serial that it interprets the pull-up on Serial2's RX pin as a NULL character and serial monitor confirms this when it prints to the console.
To fix this I added some logic to the serial's setup code to configure the pad's IOMUXC_PAD_PUS as a pulldown instead when using inverted serial.

The last picture below shows the correct output when the pad is configured as a pulldown.

C++:
#define SERIAL_7E1_RXINV_TXINV_HALF_DUPLEX (SERIAL_7E1_RXINV_TXINV | SERIAL_HALF_DUPLEX)
#define SERIAL_8N1_RXINV_TXINV_HALF_DUPLEX (SERIAL_8N1_RXINV_TXINV | SERIAL_HALF_DUPLEX)

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  // Serial1.begin(1200, SERIAL_8N1_RXINV_TXINV_HALF_DUPLEX);
  // Serial2.begin(1200, SERIAL_8N1_RXINV_TXINV);
  Serial1.begin(1200, SERIAL_7E1_RXINV_TXINV_HALF_DUPLEX);
  Serial2.begin(1200, SERIAL_7E1_RXINV_TXINV);
  while (!Serial);
  delay(10);
  Serial.println("Hardware Serial TXINV_RXINV Test...");
}

void loop() {
  if (Serial.available() > 0) {
    digitalWrite(LED_BUILTIN, HIGH);
    char read_c = Serial.read();
    Serial1.print(read_c);
  }
  if (Serial2.available() > 0) {
    char read_c = Serial2.read();
    read_c &= ~0x80; // comment out if using 8N1
    Serial.printf("HEX: %02x | ASC: %c\n", read_c, read_c);
    digitalWrite(LED_BUILTIN, LOW);
  }
}
NewFile2.png

NewFile1.png
 
Hi all, another update, things moving slowly but progress has been made. I have the break-marking sequence mostly working and the receiver code able to get the data back from the "virtual" sensor which is a Teensy taking the place of a SDI12 sensor. The break-marking sequence is done in hardware but there are still a few hiccups that can happen that need to be rectified. As far as the API I'm not sure if I will use the existing SDI12 library for Teensy 3's but I haven't really thought about that yet. The code currently just used the existing Hardware Serial code but I'm coming to realization that I will have to modify it quite a bit so I can make most of the SDI12 spec done in hardware. My goal is to make a library where it is non-blocking and robust.
 
So I just want let you know I haven't abandoned this project it's that I found out two weeks after getting my new Teensy's that I am moving again and have all my equipment in storage now. I hope this is the last time I have to move but I just don't have a lot of time or equipment to work on this now.
 
Back
Top