If there is a single signal to monitor, the implementation of the loop is simple. But if you have several different things to monitor and control, and would like to keep your code extensible, you need a good pattern for the main loop of your application.
Game developer face the same problem.Actually game developers face a similar situation. Probably not complex 3D games, where many tasks running in separate threads, but small simple 2D games. They are usually build around a simple main loop. This loop works basically in two parts: A. Display the current scene, B. react to controls, collisions, etc. and calculate the next scene.
For the game, a key factor is the time. You try to reach a high “frame rate”, therefore the code in your loop should run enough fast to allow 60 frames per second. Usually the loop is synchronised with the display, to make animations look smooth. But, you never know if your code get interrupted by other high priority processes (e.g. on a phone), so you have to write your code flexible. If for once you do not reach the 60 frames per second, the animation should nevertheless process in the same speed, just not same smooth as possible.
Introducing the Loop Time
To reach this goal, each loop has an absolute time, which is used for all calculations in this loop. For the Arduino, we can use the same principle. Depending on your requirements for precision, you can use a time in microseconds, or in milliseconds. In my case, the time in milliseconds is precise enough.
The loop in my case is starting always with the time measurement.
void loop() { const uint32_t currentTime = millis(); // The logic. }
The Logic
Each component in the system usually has two important methods. The first is a setup
or initialize
method, which has to be called in the setup()
method, and a loop(const uint32_t currentTime)
method, which has to be called in the loop()
.
The loop
in this project looks like this:
void loop() { const unsigned long currentTime = millis(); ledController.loop(currentTime); if (logicState != ErrorState) { motionSensor.loop(currentTime); if (logicState == AlarmState) { if (!audioPlayer.play("voice.snd")) { return; } logicState = IdleState; } } }
You can see, there is a LED controller which just handles the “display” of the multicolour LED. This controller can let this LED display different colours, and let the LED blink in different patterns and intervals. Everything is done in the loop
method of this controller.
There is also a motion sensor component, which handles the state of the motion sensor. This motion sensor component uses a callback method, which is always called when the state of the sensor changes. This works similar to an interrupt, but is more like the traditional event handling from user interface applications.
The callback method is registered in the setup()
method like this:
void setup() { // Set the initial state. logicState = WaitForSensor; // setup the LED controller ledController.setup(); // setup the motion sensor. motionSensor.setup(); motionSensor.setCallback(&onMotion);
And the callback itself looks like this:
void onMotion(const unsigned long currentTime, MotionSensor::Status status) { if (status == MotionSensor::WaitStablilize) { ledController.setState(LEDController::Orange, LEDController::BlinkSlow); } else if (status == MotionSensor::Idle) { ledController.setState(LEDController::Green, LEDController::FlashVerySlow); logicState = IdleState; // Ready to observe. } else if (status == MotionSensor::Alarm) { ledController.setState(LEDController::Red, LEDController::On); logicState = AlarmState; // Activate the alarm and play a sound. } }
Internally the components are using two classes Timer
and TimeDelta
to measure the time and internally working with callbacks and events. I will explain this two classes on the next pages.
Conclusion
The shown loop pattern is very flexible and extensible. You can add any number of components in the loop, which each can work independent from each other. You can implement complex logic, using callbacks or just by providing other methods on the components where one can read or change the status.
One of the biggest benefit is its simplicity. There are no interrupts, everything runs at a defined place, so you can always access all variables without any precautions. You even know the exact order of the calls, and know from which loop()
call the callbacks are started.
The only disadvantage is the controller is always busy. You can for example put the controller into a low energy state, by disabling many features and let it been wake up from a signal. If energy preservation is important for you, this is not the right pattern for you.
Continue with: Time Events