Simulate Arduino-like Interrupts in C++11
Asked Answered
A

1

7

I am working on a crude Arduino simulator. It's main function should be to test simple code consisting of control structures, loops, switches and subroutines.

My main idea is to simply provide the functions of the Arduino library myself, for example functions like digitalWrite() or digitalRead(), which would read and send the pin states from and to an external application (like a virtual breadboard).

The following diagram shows my current concept. The simulator is basically a thread which executes the setup() function once and then starts to execute the loop() function until stopped. It can be stopped or paused from the control (main) thread.

enter image description here

The implemention of the setup() and loop() function, as well as some variables, are provided by the user and cannot be modified or accessed.

So far, so good. Now I want so simulate interrupts. While the simulator thread is executing the loop() function the external application triggers an interrupt. This should result in the execution of the interrupt handler isr(), which is also provided by the user and cannot be changed.

I had two different approaches to this problem:

  1. Suspend the simulator thread, execute the interrupt handler in a different thread and resume the simulator thread.
  2. Use a signal handler instead, send a signal to the process when an interrupt occurs.

Both approaches have their own problems. With the first one, I need to synchronize state somehow, and it seems more like a horrible hack. For the second option, as far as I know, I can't specify which thread will execute the signal handler.

If possible, the solution should be platform independent. However, the solution absolutely needs to compile and run under Windows (MinGW or even Cygwin).

Arborization answered 16/8, 2020 at 21:38 Comment(9)
For option 2, you also need to remember signal handlers are forbidden from using almost everything. Even a simple printf in a signal handler is undefined behavior.Mawkish
I think I would put the isr on its own thread that waits for a condition variable. When the isr is triggered, release the condition variable and let the isr thread execute once then go back to the wait state. I'm not sure I see a good reason to halt the simulator thread unless something about the arduino architecture makes that a more accurate simulation.Benignity
@Benignity In my scenario loop() and isr() would share some variables, meaning that the interrupt routine would modify a variable used in the main function. I'm not sure if that would cause some problems. I'm pretty sure you mean std::condition_variable, right? I will give it a try, thanks!Arborization
How does the arduino handle the shared data between the isr and the main execution? If it automatically deals with the shared data, I would feel differently about halting the simulator thread. (Which is what I meant about if there is something about the arduino that makes halting a more accurate simulation.) If not, you should model the shared data handling in a way that is like the arduino, because your simulator would help determine if you are doing the shared data without stomping on data, etc.Benignity
And yes, std::condition_variable is what I meant. It is a perfect tool for blocking and releasing a thread that is waiting on something.Benignity
@Benignity There is no concurrent access on shared variables, because when the interrupt handler is being executed, the main program is halted. Usually the volatile keyword is used to indicate that a variable may be changed from outside the main program. Because of this I think halting the simulator thread is more accurate.Arborization
Why not use the control thread for servicing the interrupt? Besides calling the isr function, there is usually more work to do. Example: The interrupt is caused by an UART where data is ready to be read. The interface to the external app should have something like UART1IncomingData(array with bytes) which in turn would set some interrupt status flags, some UART registers and then call the interrupt handler if interrupts are enabled. The control thread should have some command queue it reads. (marshalling)Merchantable
https://mcmap.net/q/785298/-correct-way-to-pause-amp-resume-an-std-thread/5092123Cranwell
cplusplus.com/reference/thread/this_thread/yieldCranwell
J
0

IMO all the interrupts might be considered as threads (or rudimentary threads) with a very low latency, so the idle thread is the main and the int-threads can preempts the main thread but not be preempted.

So basically, the purpose is to execute all the threads until a condition in one of the int-threads is met, when that happens, block all the threads (kind of critical region, i.e. mutex or condition variable) until the int-thread finishes its job. After that, execute all the threads again (the less time in the interrupt the better):

void interrupt1_thd(void) {
    // try_lock the mutex
    // check the condition of this interrupt
    // if true, do the job
    // release the mutex if locked

]

With low-end uCs looks simple ( i.e. ATmega328P), there are not nested interrupts nor priorities. With more expensive uCs (let´s say an ATSAMD51 Cortex M4), things are much more complicated. Now the threads have to be able to trigger, block all the other lower or equal priority threads and be able to be blocked by higher priority threads. Threads based on priorities is not a big deal (pthread_setschedparam or SetThreadPriority), but nested mutexs in threads without deadlocks is not trivial, so here condition_variable makes more sense due to the capabilities of notifying:

Event:                      Int1   Int3   Int2  Int3         Int3
Main           : -----------                              ---              --------
Task1 (mid)    :             -------------            ----
Task2 (high)   :                          ----------
Task3 (low)    :                                             --------------
Take mutexLow  :            ---------- by 1 ---------        ---- by 3 ----
Take mutexMid  :            ---------- by 1 ---------
Take mutexHigh :                          -- by 2 --

If mutexHigh is taken (by a high prio task), Task1 and Task3 waits until Task2 notifies to all the threads.

If mutexLow is taken (by a low prio task as Task3 is), Task3 performs its job (which includes checking higher notifications), whilst Task1 and Task2 keep checking their conditions.

I would avoid sharing resources between different levels of priorities in order to not add more sync mechanisms.


All of this depends on the Arduino you want to simulate and the level of simulation you want to achieve, not sure if you want to go deeper and include interrupts queue to minimise the latency, context switching,...

Jeth answered 3/9, 2020 at 16:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.