SAMD51 ARM Serial1 Read in an ISR - Better Approaches?

Functions that deal with the reception and transmission of characters modify the flags in the INTENSET register: when the reception buffer is full the ISR disables the RXC interrupt (it’s no use servicing the interrupt when there’s no room in the buffer), while USART_receive() re-enables it, since it makes room for other data to be stored into the buffer. If the transmission buffer is empty the ISR disables the DRE interrupt (we don’t service the interrupt if there’s no data to be sent) and USART_send() re-enables it, because now the buffer is no longer empty. Now our application is free to read and write onto the serial whenever it needs to, and is free to process other instructions in the meantime (for example, to blink a LED):

int main(void)
{
  clock_init();
  port_init();
  FIFO_init();
  USART_init();
  NVIC_init();

  for (int i = 4 ; i > 0 ; i--)
    USART_writeln("all work and no play makes jack a dull boy");   // write to tx buffer and return (don't wait for the USART to be ready)
	
  char buffer[100]; 

  while (1)
  {
    blink();                             // do something else
    USART_read(buffer);                  // read from rx buffer when needed (without waiting for data) TODO: implement timeout
    USART_write("received: ");
    USART_writeln(buffer);
  }
}
int main(void)
{
	clock_init();  // initialize system and peripheral clocks
	port_init();   // configure serial GPIO pins
	USART_init();  // configure and enable USART

	USART_write_wait("hello from SAMD21!\r\n");

	char buf[100];  // 100 characters buffer (don't exceed)

	while (1)
	{
		USART_read_wait(buf);
		USART_write_wait("received: ");
		USART_writeln_wait(buf);
	}
}

USART_write_wait() writes a null-terminated character string on the serial while USART_read_wait() waits for a character string terminated by the ‘\n’ line feed character and stores it in a local buffer. USART_writeln_wait() appends a line teminator character to the string. All these functions use USART_send_wait() and USART_receive_wait() to send and receive single characters:

It's worth pointing out again the second post I made here since the above is talking about the SAMD21 and not the more advanced SAMD51 but the general use should be similar.

The SERCOM peripheral on the SAMD51 and SAMD21 generates a number of interrupts, such as DRE (DataRegister Empty), TXC (Transmit Complete) and RXC (Receive Complete), to name just three of the most commonly used.
The SAMD21's SERCOMx module has only a single Interrupt Service Routine (ISR) called SERCOMx_Handler(), therefore within the ISR it's necessary to test each of the interrupt flags to find which interrupt has occurred when the handler function is called.

The SAMD51's SERCOMx module on the other hand has separate ISR's for each interrupt, with the ISR number relating to the position of the interrupt in the Interrupt Flag (INTFLAG) register, for example SERCOM2_0_Handler() is called for DRE, SERCOM2_1_Handler() is called for TXC and SERCOM2_2_Handler() for RXC, etc.... Having separate ISRs for each interrupt flag removes the need test which interrupt has occurred within the handler function, thereby providing the SAMD51 with a small speed optimization over the SAMD21.

Also, in the SAMD21 all of the bits in INTFLAG are ORed together so that only one is connected to the NVIC. In the SAMD51 they are split over four lines.

Not entirely sure how that means the above code examples would need to be slightly modified to work correctly with the SAMD51 product line yet though it is probably related to the void SERCOM5_Handler(void) but much of the core functionality should already be there to handle this.

What I don't get is why Adafruit and ARM don't seem to offer example code to easily use RS485 over any of their SAMD51 chips. In fact, Adafruit makes it such that you literally cannot easily compile the code without modifying the actual variant.cpp file manually and each release or update means you have to do so again because they do not weakly define it. I have no idea why that's the case and it is helpful that they "define" the TX/RX of Serial1 by default since that's literally what the pins are typically used for (although you can change that around once you consult an elaborate third party chart of what actual pads go with what pins and what is actually available on given pins).

Uart Serial1( &sercom0, PIN_SERIAL1_RX, PIN_SERIAL1_TX, PAD_SERIAL1_RX, PAD_SERIAL1_TX ) ;

and so on is there too but in order to do much work at all with SERCOM0 or serial here, you have to basically also edit the variant.cpp file as well.

/*void SERCOM0_2_Handler()
{
 Serial1.IrqHandler();
}*

I did come across this official Arduino RS485 code as well, which at least provides some of this functionality to SAMD21 though I am not sure how in depth the provided software is. It does not seem to utilize interrupts or anything particularly fancy or lower level which is sort of what needs to be happening here for this to work.

Enables sending and receiving data using the RS485 standard with RS485 shields, like the MKR 485 Shield.

https://www.arduino.cc/en/Reference/ArduinoRS485

Anyway, going to dig more into this and see if I cannot better understand how Serial commands can be handled using existing lower level routines such that the Arduino is always listening and passing along instructions sent from the Serial Monitor to downstream devices. Any ideas on if there is example code for this or other related Serial use from ARM by chance? There might be some in their main software package or start.atmel.com? The Grand Central is using the SAMD51P20A chipset with the TQFP128 form factor.

All I initially see there is a driver example.

/**
 * Example of using USART_0 to write "Hello World" using the IO abstraction.
 */
void USART_0_example(void)
{
	struct io_descriptor *io;
	usart_sync_get_io_descriptor(&USART_0, &io);
	usart_sync_enable(&USART_0);

	io_write(io, (uint8_t *)"Hello World!", 12);
}

Asynchronous UART / USART Serial Communications on Atmel SAM D21 Xplained Pro - Tutorial
Is also really helpful with regards to setting up UART / USART Serial on a SAM D21.

Here are pad layouts for the SAMD51 Grand Central as well.

S0 P0 and S0 P1 here for Sercom and notably PB24 and PB25 if you are trying to use the start.atmel.com layout which doesn't seem to automatically assign those.

Here are pad layouts for the SAMD21. It's worth noting that the pads are distinct compared to the SAMD51 and I am pretty sure that also explains why the above code is using SERCOM5. It uses PA10 and PA11 and S0 P2 and S0 P3 instead as well.

Here are application notes for, again, the SAMD21 and not the SAMD51 but it seems helpful, plus there are some examples as well.

AT03256: SAM D/R/L/C Serial USART (SERCOMUSART) Driver

This driver for Atmel® | SMART ARM®-based microcontrollers provides an interface for the configuration and management of the SERCOM module inits USART mode to transfer or receive USART data frames. The following driver API modes are covered by this manual:

• Polled APIs
• Callback APIs

The following peripheral is used by this module:
• SERCOM (Serial Communication Interface)

The following devices can use this module:
• Atmel | SMART SAM D20/D21

To summarize what I am looking for is basically a way for SAMD51 chipsets to be able to constantly listen in for TX serial / UART commands sent over to it by a user using the Arduino serial monitor (that will be sent through Serial1 then to RS485 to the receiving slave units) no matter where it is in the void loop() state and once a /n is finally received in the serial monitor, blast that entire command of perhaps a few dozen bytes or so downstream over Serial1 pins 0 and 1 to the RS485 shield then out to slave units. When it hears something back from the responding slave unit that the message was sent to, proceed to send that RX back to the serial monitor so you can see what it sent back.

The rest of the code should just run normally and loop awfully rapidly. This is really just basically an alternate method for the user to send serial commands out such that things are not waiting on the main script to finish up anything it might be doing or waiting on and therefore not be able to send commands out over serial at that time.

Hopefully that makes sense and helps to illustrate why the prior examples, while very helpful and very good practices did not seem to be terribly helpful for this specific use of serial on this particular ARM hardware although they are generally very good practices and very useful guides to follow.

The part for me that is the oddest here is that I cannot imagine that I am the first person in the 2 or so years that this chipset has been readily available over retail who is trying to use serial / UART in this way. Has nobody else come up with an explanation or code that handles this as an intact module or single driver? I have spent several days now looking around and have not come up with anything but if anyone knows of something, please let me know!

Your lucadavidian links are giving my virus scanner a migraine…

You’ve written a lot of stuff here and I wont bother to read it in detail but what I gather – and correct me if I’m wrong – is that you are giving a treatise on the basics of serial interrupts but aren’t maybe realizing that this functionality is already in the libraries. From the HardwareSerial.cpp library, for example:

void HardwareSerial::_tx_udr_empty_irq(void)
{
  // If interrupts are enabled, there must be more data in the output
  // buffer. Send the next byte
  unsigned char c = _tx_buffer[_tx_buffer_tail];
  _tx_buffer_tail = (_tx_buffer_tail + 1) % SERIAL_TX_BUFFER_SIZE;

  *_udr = c;

  // clear the TXC bit -- "can be cleared by writing a one to its bit
  // location". This makes sure flush() won't return until the bytes
  // actually got written. Other r/w bits are preserved, and zeroes
  // written to the rest.

#ifdef MPCM0
  *_ucsra = ((*_ucsra) & ((1 << U2X0) | (1 << MPCM0))) | (1 << TXC0);
#else
  *_ucsra = ((*_ucsra) & ((1 << U2X0) | (1 << TXC0)));
#endif

  if (_tx_buffer_head == _tx_buffer_tail) {
    // Buffer empty, so disable interrupts
    cbi(*_ucsrb, UDRIE0);
  }
}

The low-level interrupt already gather and transmit data to and from buffers. If you use “Serial.write(…”) to send a byte then, if the TX buffer is empty, it will be sent right away. If there’s data already in the buffer it will be queued and sent as part of the TX interrupt logic.

There is an RX and a TX buffer (~64 bytes each if memory serves, at least in an AVR) the allow the ISRs to buffer data ready for your app to come get them. When you do a Serial.available() call you’re really checking if the head and tail pointers to the RX buffer show no unread data (head == tail) or data available (head != tail.)

It seems like you want to go in and add all your serial message processing to the ISRs. This would be a bad idea.

Leave the serial ISRs alone; they’re stable, efficient and small. Read the provided buffers frequently and act on them in a timely fashion by writing the rest of your code so that the processor isn’t blocked or otherwise occupied for long periods of time doing other stuff.

The RX and a TX buffer on the SAMD51 isn’t 64 or even 128 bytes though. It’s 1 byte if I am understanding correctly. It just handles things markedly differently than AVR does.

It seems like you are looking at an AVR HardwareSerial.cpp file though?
https://github.com/arduino/ArduinoCore-avr/blob/master/cores/arduino/HardwareSerial.cpp

Here is the relevant part of the HardwareSerial.h file for the SAMD line here. There isn’t even a HardwareSerial.cpp file.

https://github.com/adafruit/ArduinoCore-samd/blob/master/cores/arduino/HardwareSerial.h
Which was forked from
https://github.com/arduino/ArduinoCore-samd/blob/master/cores/arduino/HardwareSerial.h

class HardwareSerial : public Stream
{
  public:
    virtual void begin(unsigned long) {}
    virtual void begin(unsigned long, uint16_t) {}
    virtual void end() {}
    virtual int available(void) = 0;
    virtual int peek(void) = 0;
    virtual int read(void) = 0;
    virtual void flush(void) = 0;
    virtual size_t write(uint8_t) = 0;
    using Print::write; // pull in write(str) and write(buf, size) from Print
    virtual operator bool() = 0;
};

extern void serialEventRun(void) __attribute__((weak));

I am unclear if the functionality I am looking for is actually in the SAMD51 libraries. Adafruit doesn’t seem to offer what seems exactly like a robust library for how to use this chipset or if they do, they sure don’t exactly document it well.

There is a RingBuffer.h and SERCOM.cpp / SERCOM.h file now though.
https://github.com/adafruit/ArduinoCore-samd/tree/master/cores/arduino

https://github.com/adafruit/ArduinoCore-samd/blob/master/cores/arduino/SERCOM.cpp in particular looks promising at least to better understand what is there now.

/* =========================
 * ===== Sercom UART
 * =========================
*/
void SERCOM::initUART(SercomUartMode mode, SercomUartSampleRate sampleRate, uint32_t baudrate)
{
  initClockNVIC();
  resetUART();

  //Setting the CTRLA register
  sercom->USART.CTRLA.reg = SERCOM_USART_CTRLA_MODE(mode) |
                            SERCOM_USART_CTRLA_SAMPR(sampleRate);

  //Setting the Interrupt register
  sercom->USART.INTENSET.reg = SERCOM_USART_INTENSET_RXC |  //Received complete
                               SERCOM_USART_INTENSET_ERROR; //All others errors

  if ( mode == UART_INT_CLOCK )
  {
    uint16_t sampleRateValue;

    if (sampleRate == SAMPLE_RATE_x16) {
      sampleRateValue = 16;
    } else {
      sampleRateValue = 8;
    }

    // Asynchronous fractional mode (Table 24-2 in datasheet)
    //   BAUD = fref / (sampleRateValue * fbaud)
    // (multiply by 8, to calculate fractional piece)
#if defined(__SAMD51__)
    uint32_t baudTimes8 = (SERCOM_FREQ_REF * 8) / (sampleRateValue * baudrate);
#else
    uint32_t baudTimes8 = (SystemCoreClock * 8) / (sampleRateValue * baudrate);
#endif

    sercom->USART.BAUD.FRAC.FP   = (baudTimes8 % 8);
    sercom->USART.BAUD.FRAC.BAUD = (baudTimes8 / 8);
  }
}
void SERCOM::initFrame(SercomUartCharSize charSize, SercomDataOrder dataOrder, SercomParityMode parityMode, SercomNumberStopBit nbStopBits)
{
  //Setting the CTRLA register
  sercom->USART.CTRLA.reg |=
    SERCOM_USART_CTRLA_FORM((parityMode == SERCOM_NO_PARITY ? 0 : 1) ) |
    dataOrder << SERCOM_USART_CTRLA_DORD_Pos;

  //Setting the CTRLB register
  sercom->USART.CTRLB.reg |= SERCOM_USART_CTRLB_CHSIZE(charSize) |
    nbStopBits << SERCOM_USART_CTRLB_SBMODE_Pos |
    (parityMode == SERCOM_NO_PARITY ? 0 : parityMode) <<
      SERCOM_USART_CTRLB_PMODE_Pos; //If no parity use default value
}

void SERCOM::initPads(SercomUartTXPad txPad, SercomRXPad rxPad)
{
  //Setting the CTRLA register
  sercom->USART.CTRLA.reg |= SERCOM_USART_CTRLA_TXPO(txPad) |
                             SERCOM_USART_CTRLA_RXPO(rxPad);

  // Enable Transceiver and Receiver
  sercom->USART.CTRLB.reg |= SERCOM_USART_CTRLB_TXEN | SERCOM_USART_CTRLB_RXEN ;
}

void SERCOM::resetUART()
{
  // Start the Software Reset
  sercom->USART.CTRLA.bit.SWRST = 1 ;

  while ( sercom->USART.CTRLA.bit.SWRST || sercom->USART.SYNCBUSY.bit.SWRST )
  {
    // Wait for both bits Software Reset from CTRLA and SYNCBUSY coming back to 0
  }
}

void SERCOM::enableUART()
{
  //Setting  the enable bit to 1
  sercom->USART.CTRLA.bit.ENABLE = 0x1u;

  //Wait for then enable bit from SYNCBUSY is equal to 0;
  while(sercom->USART.SYNCBUSY.bit.ENABLE);
}

void SERCOM::flushUART()
{
  // Skip checking transmission completion if data register is empty
  if(isDataRegisterEmptyUART())
    return;

  // Wait for transmission to complete
  while(!sercom->USART.INTFLAG.bit.TXC);
}

void SERCOM::clearStatusUART()
{
  //Reset (with 0) the STATUS register
  sercom->USART.STATUS.reg = SERCOM_USART_STATUS_RESETVALUE;
}

bool SERCOM::availableDataUART()
{
  //RXC : Receive Complete
  return sercom->USART.INTFLAG.bit.RXC;
}

bool SERCOM::isUARTError()
{
  return sercom->USART.INTFLAG.bit.ERROR;
}

void SERCOM::acknowledgeUARTError()
{
  sercom->USART.INTFLAG.bit.ERROR = 1;
}

bool SERCOM::isBufferOverflowErrorUART()
{
  //BUFOVF : Buffer Overflow
  return sercom->USART.STATUS.bit.BUFOVF;
}

bool SERCOM::isFrameErrorUART()
{
  //FERR : Frame Error
  return sercom->USART.STATUS.bit.FERR;
}

void SERCOM::clearFrameErrorUART()
{
  // clear FERR bit writing 1 status bit
  sercom->USART.STATUS.bit.FERR = 1;
}

bool SERCOM::isParityErrorUART()
{
  //PERR : Parity Error
  return sercom->USART.STATUS.bit.PERR;
}

bool SERCOM::isDataRegisterEmptyUART()
{
  //DRE : Data Register Empty
  return sercom->USART.INTFLAG.bit.DRE;
}

uint8_t SERCOM::readDataUART()
{
  return sercom->USART.DATA.bit.DATA;
}

int SERCOM::writeDataUART(uint8_t data)
{
  // Wait for data register to be empty
  while(!isDataRegisterEmptyUART());

  //Put data into DATA register
  sercom->USART.DATA.reg = (uint16_t)data;
  return 1;
}

void SERCOM::enableDataRegisterEmptyInterruptUART()
{
  sercom->USART.INTENSET.reg = SERCOM_USART_INTENSET_DRE;
}

void SERCOM::disableDataRegisterEmptyInterruptUART()
{
  sercom->USART.INTENCLR.reg = SERCOM_USART_INTENCLR_DRE;
}

It seems like you want to go in and add all your serial message processing to the ISRs. This would be a bad idea.

I completely agree. But what I am looking to figure out how to do is have it send and receive Serial1 commands basically at any point in time when typed into the Serial Monitor and pass them along to downstream RS485 devices, getting the response back and displaying that in the Serial Monitor. Even if in a delay or in a loop or otherwise not paying attention to Serial1. That’s really all I am trying to accomplish here specifically. I get that this is doable, probably in several different ways and it looks like with a number of built in tools that are already there. I just have yet to come across a good guide or good method of how to do that exactly is all.

One would think and hope that using Serial.write("…") to send a byte if the TX buffer is empty, it will be sent right away would work but it does not seem to do so if the main loop is in a while or delay or anything else, even in very simple testing. Maybe it has to do with how information is treated differently over the Serial Monitor? That is what I am trying to figure out because the goal is really just to pass the serial commands sent out such that they are sent out through RS485 as soon as they are entered into the Serial Monitor by the user.

I'm pretty sure the SAMD core does use a ring buffer (see RingBuffer.h) for UART comms. It's normal for the processor to have a single TX and RX register for each UART. It's up to the ISR to handle the RX/TX FIFO/ring buffers.

Can you define "as soon as"? What's the baud rate from the monitor? What's an acceptable latency from the time a complete message is received from the console to it going out as an RS485 message?

Remember you've got a 120MHz core there with a clock period of 8.3nS. What is the processor doing elsewhere that would take it so long to get back to pull received characters out of the buffer?

I believe you are correct. It does seem to use a ring buffer. Looking at 115,200 baud here with the serial monitor. The fastest that a user is going to be sending commands is a few dozen bytes every second or two. Not fast at all.

As soon as meaning within half a second or so of pressing the enter button for it to send the command over to the downstream device over RS485. An eternity in terms of the SAMD51. It does NOT need to be within a few nanoseconds but it has to actually send it over, not wait until the unit has someone press a start button for example or simply never send it at all because it times out or otherwise isn't sent at all.

The issue isn't that the unit is so busy. The issue is that it needs to actually send the serial commands out over Serial Monitor (or technically Serial1, pins 0 and 1 here out to RS485) once the user presses enter and then have it send back what it hears back as an interrupt also to the Serial Monitor then move back to doing other things. That's really all I am trying to do here.

arm_isr_serial:
I believe you are correct. It does seem to use a ring buffer. Looking at 115,200 baud here with the serial monitor. The fastest that a user is going to be sending commands is a few dozen bytes every second or two. Not fast at all.

As soon as meaning within half a second or so of pressing the enter button for it to send the command over to the downstream device over RS485. An eternity in terms of the SAMD51. It does NOT need to be within a few nanoseconds but it has to actually send it over, not wait until the unit has someone press a start button for example or simply never send it at all because it times out or otherwise isn't sent at all.

The issue isn't that the unit is so busy. The issue is that it needs to actually send the serial commands out over Serial Monitor (or technically Serial1, pins 0 and 1 here out to RS485) once the user presses enter and then have it send back what it hears back as an interrupt also to the Serial Monitor then move back to doing other things. That's really all I am trying to do here.

All very doable. Would be willing to offer help if you would post your code. Otherwise, thoughts and prayers.

Happy to post code. We sort of have a workaround, albeit a very, very crude one. It feels like it is about the worst way that you could solve this issue. It polls every 1 ms. It has to have you use MyDelay instead of Delay. It makes you put something to listen for it into every single while or if loop or anything else that pauses in any way.

It also technically sort of works.

What I would like to figure out though is what is the actual correct way of doing this? Clearly there are built in interrupts specifically designed only to operate for a few micro seconds while serial data is actually in the buffer and able to be sent.

#define CPU_HZ 48000000
int pinValue = 9;
int val1 = 1;

void MyDelay(int ms) {
   for(int i=0;i<ms;i++){
      if (Serial.available())
      {
        Serial.read();
      }
      delay(1);
   }
}

void setup(){

Serial.begin(115200);
 while (!Serial)
    delay(100);  // wait for USB Serial to enumerate

pinMode(pinValue, INPUT_PULLUP);
}

void loop(){ 

val1 = digitalRead(pinValue);

while (val1 == 1)
 {
  val1 = digitalRead(pinValue);
  delay(10);
  if(Serial.available()){
    Serial.read();
}
  // Do nothing
 }

MyDelay(500);

Any ideas or suggestions or things to consider? Surely there are ways to TX and RX serial commands like this (over Serial Monitor while connected through the PC), the moment they send commands in despite using any Arduino code, right?

Why use delay()? You’re wasting thousands or millions of CPU cycles by blocking the processor 1 or 10mS all the time.

Use millis timing. Consider the following:

In principle, it toggles an LED every 250mS while receiving any characters that pop in on the serial port virtually instantly. It doesn’t use delay to wait for LED toggles; instead it peeks in every loop for a few microseconds to see if 250mS has elapsed. If not, it returns to loop() and allows other things to happen, like checks of the serial port.

This compiles and runs on a Due (ATSAM3X8E) and should work on yours I think:

#define CPU_HZ          48000000

#define BUFF_SIZE       256         //serial RX buffer size

char
    rxBuffer[BUFF_SIZE];
    
const uint8_t pinValue = 9;
const uint8_t pinLED = LED_BUILTIN;

void setup()
{
    Serial.begin(115200);

    pinMode(pinValue, INPUT_PULLUP);
    pinMode( pinLED, OUTPUT );
}

void loop()
{
    Check250mSThing();
    CheckSerial();

}//loop

void Check250mSThing( void )
{
    static bool
        state = false;
    static uint32_t
        timeCheck;
    uint32_t
        timeNow = millis();

    if( (timeNow - timeCheck) < 250ul )
        return;

    timeCheck = timeNow;

    //do whatever task needs doing every 250mS
    //.
    //.
    //.   
    //for example, toggle an LED on and off 
    
    state ^= true;
    digitalWrite( pinLED, state );
    
}//CheckPin_SM

void CheckSerial( void )
{
    static uint16_t
        ptr = 0;

    //check every single pass of loop()
    if( Serial.available() > 0 )
    {
        //if any are waiting, grab them all
        while( Serial.available() > 0 )
        {
            //read the next available character
            char cRx = Serial.read();
            
            //if LF (EOM marker, for example)...
            if( cRx == '\n' )
            {                
                //...send buffer out as received
                Serial.write( rxBuffer, --ptr );
                //zero the pointer ready for the next RX message
                ptr = 0;
                
                //...
                
            }//if
            else
            {
                //receive as many bytes are in the RX buffer
                rxBuffer[ptr] = cRx;
                ptr++;
                if( ptr >= BUFF_SIZE - 1 )
                    ptr--;

            }//else

        }//while
        
    }//if
    
}//CheckSerial

Believe me, I don't want to use delay()! I agree that is the WRONG way of doing it. It technically works but it is a horrible way of doing things. Trying to figure out a better approach.

That sounds like a better way to go. I compiles. I will try it out and see, it does sound like a better approach (though again, probably anything is better than the current approach).

#define CPU_HZ          48000000

I assume that is for a 48 MHz unit? The Due is 84 MHz and the Grand Central is 120 MHz. So change it to

#define CPU_HZ          120000000

?

I changed it and it compiles (but it compiles either way).

arm_isr_serial:
Believe me, I don't want to use delay()! I agree that is the WRONG way of doing it. It technically works but it is a horrible way of doing things. Trying to figure out a better approach.

That sounds like a better way to go. I compiles. I will try it out and see, it does sound like a better approach (though again, probably anything is better than the current approach).

#define CPU_HZ          48000000

I assume that is for a 48 MHz unit? The Due is 84 MHz and the Grand Central is 120 MHz. So change it to

#define CPU_HZ          120000000

?

I changed it and it compiles (but it compiles either way).

Didn't actually notice it (it was in your original code.) I don't think it affects operation or actual clocking.

[/quote]

So to do this “correctly” seems to involve looking further into the USB side of things, since the serial monitor is through USB and isn’t traditional SERCOM.

\Arduino15\packages\adafruit\hardware\samd\1.6.4\libraries\USBHost\src

Things like Usb.cpp and so on.

We wound up basically coming up with a slightly less crude but still workable version that uses a partial derivation of some of Martin’s code. It is now using timers and interrupts and seems to work well enough. Now it does not require one to literally paste commands in to make it constantly check serial every 1 ms while you are in a while loop or otherwise “idle” from the user’s perspective. Even though it is now still checking every 1 ms :slight_smile:

It now allows one to send commands over serial monitor and have it send them downstream then hear back from the RS485 device that is replying, even if the main sketch is otherwise “paused”.

Timer4.cpp
#include "Timer4.h"


void (*func2)();
static inline void TC4_wait_for_sync() {
  while (TC4->COUNT16.SYNCBUSY.reg != 0) {}
}
void Timer4_::setPeriod_4(unsigned long period) {
  int prescaler;
  uint32_t TC_CTRLA_PRESCALER_DIVN;

  TC4->COUNT16.CTRLA.reg &= ~TC_CTRLA_ENABLE;
  TC4_wait_for_sync();
  TC4->COUNT16.CTRLA.reg &= ~TC_CTRLA_PRESCALER_DIV1024;
  TC4_wait_for_sync();
  TC4->COUNT16.CTRLA.reg &= ~TC_CTRLA_PRESCALER_DIV256;
  TC4_wait_for_sync();
  TC4->COUNT16.CTRLA.reg &= ~TC_CTRLA_PRESCALER_DIV64;
  TC4_wait_for_sync();
  TC4->COUNT16.CTRLA.reg &= ~TC_CTRLA_PRESCALER_DIV16;
  TC4_wait_for_sync();
  TC4->COUNT16.CTRLA.reg &= ~TC_CTRLA_PRESCALER_DIV4;
  TC4_wait_for_sync();
  TC4->COUNT16.CTRLA.reg &= ~TC_CTRLA_PRESCALER_DIV2;
  TC4_wait_for_sync();
  TC4->COUNT16.CTRLA.reg &= ~TC_CTRLA_PRESCALER_DIV1;
  TC4_wait_for_sync();

  if (period > 300000) {
    TC_CTRLA_PRESCALER_DIVN = TC_CTRLA_PRESCALER_DIV1024;
    prescaler = 1024;
  } else if (80000 < period && period <= 300000) {
    TC_CTRLA_PRESCALER_DIVN = TC_CTRLA_PRESCALER_DIV256;
    prescaler = 256;
  } else if (20000 < period && period <= 80000) {
    TC_CTRLA_PRESCALER_DIVN = TC_CTRLA_PRESCALER_DIV64;
    prescaler = 64;
  } else if (10000 < period && period <= 20000) {
    TC_CTRLA_PRESCALER_DIVN = TC_CTRLA_PRESCALER_DIV16;
    prescaler = 16;
  } else if (5000 < period && period <= 10000) {
    TC_CTRLA_PRESCALER_DIVN = TC_CTRLA_PRESCALER_DIV8;
    prescaler = 8;
  } else if (2500 < period && period <= 5000) {
    TC_CTRLA_PRESCALER_DIVN = TC_CTRLA_PRESCALER_DIV4;
    prescaler = 4;
  } else if (1000 < period && period <= 2500) {
    TC_CTRLA_PRESCALER_DIVN = TC_CTRLA_PRESCALER_DIV2;
    prescaler = 2;
  } else if (period <= 1000) {
    TC_CTRLA_PRESCALER_DIVN = TC_CTRLA_PRESCALER_DIV1;
    prescaler = 1;
  }
  TC4->COUNT16.CTRLA.reg |= TC_CTRLA_PRESCALER_DIVN;
  TC4_wait_for_sync();

  int compareValue = (int)(CPU_HZ / (prescaler/((float)period / 1000000))) - 1;

  // Make sure the count is in a proportional position to where it was
  // to prevent any jitter or disconnect when changing the compare value.
  TC4->COUNT16.COUNT.reg = map(TC4->COUNT16.COUNT.reg, 0,
                               TC4->COUNT16.CC[0].reg, 0, compareValue);
  TC4->COUNT16.CC[0].reg = compareValue;
  TC4_wait_for_sync();

  TC4->COUNT16.CTRLA.bit.ENABLE = 1;
  TC4_wait_for_sync();
 }

void Timer4_::startTimer_4(unsigned long period, void (*f)()) {
  // Enable the TC bus clock, use clock generator 0
  GCLK->PCHCTRL[TC4_GCLK_ID].reg = GCLK_PCHCTRL_GEN_GCLK1_Val |
                                   (1 << GCLK_PCHCTRL_CHEN_Pos);
  while (GCLK->SYNCBUSY.reg > 0);

  TC4->COUNT16.CTRLA.bit.ENABLE = 0;
  
  // Use match mode so that the timer counter resets when the count matches the
  // compare register
  TC4->COUNT16.WAVE.bit.WAVEGEN = TC_WAVE_WAVEGEN_MFRQ;
  TC4_wait_for_sync();
  
   // Enable the compare interrupt
  TC4->COUNT16.INTENSET.reg = 0;
  TC4->COUNT16.INTENSET.bit.MC0 = 1;

  // Enable IRQ
  NVIC_EnableIRQ(TC4_IRQn);

  func2 = f;
  setPeriod_4(period); 
}

 
 void TC4_Handler() {
  // If this interrupt is due to the compare register matching the timer count
  if (TC4->COUNT16.INTFLAG.bit.MC0 == 1) {
    TC4->COUNT16.INTFLAG.bit.MC0 = 1;
    (*func2)();
    
  }
}
Timer4.h
#include "Arduino.h"
#define CPU_HZ 120000000

class Timer4_{
  public:
    void setPeriod_4(unsigned long period);
    void startTimer_4(unsigned long period, void (*f)()); 
};

Posted this here in case it helps anyone else.

Put this above void setup()

#include "Timer4.h"

Put this in void setup()

Timer4_ T4;
T4.startTimer_4(1000 , handler);

With a samd51, if you don’t have the discipline to keep your loop non-blocking (I’m don’t mean to imply that this is always easy to do), you could run one of the RTOSes that are available. One task to read serial and write rs485 message, another task to run your loop() code.

Can you elaborate on that a bit more? That sounds interesting to explore further and at the very least know more about. First time I have heard of such a thing.

Seems to speak more about this. Interesting. Seems to respect real time requirements while still allowing for other tasks to run. Will have to look into it further. Was unaware that the SAMD51 supported it. Doesn't seem likely to be compatible with the Arduino IDE though?

I've never used it myself. I don't know how "transparent" it is to the normal Arduino library use.
Your description sounds like exactly the use case for an RTOS...