Solution Architecture Advice - RS485 device polling

I am building a product which reads from and sends commands to Control Unit board via RS485 bus.
The Control Unit boards each control up to 16 actuators/sensors and can be daisy chained. I envisage maximum of 3 Control Boards on the RS485 bus.

Due to the way the Control Unit board controller works, simple reading of the status of actuators/sensors can take 400mSec. I am trying to be as responsive as possible to a change in sensor value. Currently I check the sensors values every second from the application loop(). However, this is causing problems with other things such as recognising button presses (read from a GPIO expander) with only 1 CU board.

I am wondering what the best solution is. I have already spun out control of addressable LED strip to a co-MCU controlled via I2C. I did this because controlling the LEDs to flash resulted in very uneven timing. One option might be to do the same for the control/reading status of the CU boards. I not so keen on this having just gone through design on the board for this. Another might be to use a specific thread for the RS485 comms and decouple the application thread. Has anyone got any advice about how best to setup the thread or examples I could study of solutions doing the same?
I already have one separate thread running the PublishQueueAsyncRK library and not a lot of spare RAM.

@armor, what speed are you communicating on the RS485 bus? Are you using RS485 because of distance to the CU board? What protocol are using for communicating to the CU boards? Are these CU boards commercial products (if so, which ones)?

More importantly, what response time are you looking for if 400ms is too slow?

Communication is with a commercial product that uses RS485 - that is a given. Also a given is the communication message structure which is proprietary and clunky.
The time which is actually 500mS (400 was a typo) is because to get the status from the CU board you need to send it a request and then wait for it to respond and read the response. The comms process is 2-3mS. I have tried reducing the delay but then it doesn’t respond/comms become unreliable.
A loop cadence of 100/s is as low as I want to go - so 6mS for reading the status of this peripheral. I guess that this means not waiting in the application loop() for the response. What I don’t know yet is whether when I have 2 or 3 CU boards, I can send the requests to all together, wait without blocking for 500mS and read the responses back together. Another model could be sequentially for each CU board requesting, non blocking wait, and reading - this is where having the process running continuously on a separate thread came from.

How big is the message? Can it fit in the serial buffer? If so, then you could send the originating message, set RS485 to RX mode, then go do something else while the Boron or Electron catch the return message in their RX buffer, coming back to it once the message is complete.

Not big at all - the command is 5 bytes and the return 9 bytes including a checksum.

What would be your route to do this keep check Serial1.available() OR serial1Event()?
Because it is necessary to send other commands to this CU board this would need to be blocked whilst it was waiting to hear back? Do you have any code examples you could point me towards?

serialEvent seems attractive to use except that it doesn’t appear to add to just calling available()?

The serialEvent functions are called in between calls to the application loop() . This means that if loop() runs for a long time due to delay() calls or other blocking calls the serial buffer might become full between subsequent calls to serialEvent and serial characters might be lost. Avoid long delay() calls in your application if using serialEvent .

Since serialEvent functions are an extension of the application loop, it is ok to call any functions that you would also call from loop() . Because of this, there is little advantage to using serial events over just reading serial from loop().

Serial events won’t gain you much of anything. You will likely need to structure the code either as multiple state machines running out of loop, or using threads. Both will work with USART serial.

Rick thanks for chipping in on this topic.

I am using your PublishQueueAsync library so have lost a chunk of RAM to the stack for the thread it uses - I think that is out of consideration because I can’t spare another 6KB. Incidently, is there a way to reduce the stack claimed by a thread from 6KB?

I already heavily use the FSM model so I am comfortable with building something that uses that concept.

When you create a worker thread, the default OS_THREAD_STACK_SIZE_DEFAULT is a 3K stack. Only the main loop is 6K. But you can set it to whatever you want:

This is the prototype for Thread:

Thread(const char* name, os_thread_fn_t function, void* function_param=NULL,
            os_thread_prio_t priority=OS_THREAD_PRIORITY_DEFAULT, size_t stack_size=OS_THREAD_STACK_SIZE_DEFAULT)

Software timers use a 1024 byte stack. You can make it even smaller, though you may run into issues if you make it significantly smaller, depending on your code and what system functions you call.

Thanks all for ideas and contributions - I am closing as solved. Need to go and do a bit of testing.

I do think with Serial1.available(). I check the rx buffer every 1000ms and then process complete packets when I have the right amount of bytes in the buffer.

This code is running in a multiple state machine paradigm.