Both sides previous revisionPrevious revisionNext revision | Previous revision |
en:iot-open:introductiontoembeddedprogramming2:cppfundamentals:programmingpatterns [2023/11/17 14:36] – pczekalski | en:iot-open:introductiontoembeddedprogramming2:cppfundamentals:programmingpatterns [2024/05/27 10:53] (current) – [Finite State Machine] ktokarz |
---|
====== Programming patterns ====== | ====== Programming patterns ====== |
| {{:en:iot-open:czapka_b.png?50| General audience classification icon }}{{:en:iot-open:czapka_m.png?50| General audience classification icon }}{{:en:iot-open:czapka_e.png?50| General audience classification icon }}\\ |
This chapter presents some programming templates and fragments of the code that are common in embedded systems. Some patterns, such as non-blocking algorithms, do not use ''delay(x)'' to hold program execution but use a timer-based approach instead. It has also been discussed in other chapters, such as in the context of timers | This chapter presents some programming templates and fragments of the code that are common in embedded systems. Some patterns, such as non-blocking algorithms, do not use ''delay(x)'' to hold program execution but use a timer-based approach instead. It has also been discussed in other chapters, such as in the context of timers |
[[en:iot-open:introductiontoembeddedprogramming2:cppfundamentals:timing|]] or interrupts [[en:iot-open:introductiontoembeddedprogramming2:cppfundamentals:interrupts|]]. | [[en:iot-open:introductiontoembeddedprogramming2:cppfundamentals:timing|]] or interrupts [[en:iot-open:introductiontoembeddedprogramming2:cppfundamentals:interrupts|]]. |
| |
===== Tracing vs Debugging - Serial Ports ===== | ==== Tracing vs Debugging - Serial Ports ==== |
Almost any MCU has a hardware debugging capability. This complex technique requires an external debugger using an interface such as JTAG. Setting up hardware and software for simple projects may not be worth a penny; thus, the most frequent case is tracing over debugging. Tracing uses a technique where the Developer explicitly sends some data to the external device (usually a terminal, over a serial port, and eventually a display) that visualises it. The Developer then knows the variables' values and how the algorithm runs. The use of the serial port is common because this is the one that is most frequently used for programming. Thus, it can be used in reverse for tracing. For this reason, Arduino Framework implements a singleton object ''Serial'' present in every code. It is implemented by each Arduino Framework vendor at the level of the general library with Arduino Framework.\\ | Almost any MCU has a hardware debugging capability. This complex technique requires an external debugger using an interface such as JTAG. Setting up hardware and software for simple projects may not be worth a penny; thus, the most frequent case is tracing over debugging. Tracing uses a technique where the Developer explicitly sends some data to the external device (usually a terminal, over a serial port, and eventually a display) that visualises it. The Developer then knows the variables' values and how the algorithm runs. The use of the serial port is common because this is the one that is most frequently used for programming. Thus, it can be used in reverse for tracing. For this reason, Arduino Framework implements a singleton object ''Serial'' present in every code. It is implemented by each Arduino Framework vendor at the level of the general library with Arduino Framework.\\ |
Note to use a ''Serial'', it is obligatory to initialise it using the ''Serial.begin(x)'' method, providing the correct bps, where ''x'' is a transmission speed (rate) that suits the rate configured in the terminal. The most common rates are 9600 (default) and 115200, but other options are possible. On the terminal side, configuration is usually done in the menu or a configuration file, such as in the case of the ''platformio.ini'' file. Calling ''Serial.begin(x)'' is usually done as one of the first actions implemented in the ''Setup()'' function of the application code: | Note to use a ''Serial'', it is obligatory to initialise it using the ''Serial.begin(x)'' method, providing the correct bps, where ''x'' is a transmission speed (rate) that suits the rate configured in the terminal. The most common rates are 9600 (default) and 115200, but other options are possible. On the terminal side, configuration is usually done in the menu or a configuration file, such as in the case of the ''platformio.ini'' file. Calling ''Serial.begin(x)'' is usually done as one of the first actions implemented in the ''Setup()'' function of the application code: |
* ''Serial.println(x)'' prints as above but adds the end of line/newline character by the end of the transmission. Note that the Linux style is used in Arduino, so only ASCII 13 character is sent. | * ''Serial.println(x)'' prints as above but adds the end of line/newline character by the end of the transmission. Note that the Linux style is used in Arduino, so only ASCII 13 character is sent. |
| |
===== Interfacing with the Device - Serial Port ===== | ==== Interfacing with the Device - Serial Port ==== |
The serial port and a class ''Serial'' handling the communication are bi-directional. It means one can send a message from the MCU to the terminal and the opposite. This can be used as a simple user interface. All configuration above steps to ensure seamless cooperation of the MCU serial interface and terminal (application) are also in charge here. As data is streamed byte by byte, it is usually necessary to buffer it. Technically, the serial port notifies the MCU every time a character comes to the serial port using the interrupts. Luckily, part of the job is done by the ''Serial'' class: all characters are buffered in an internal buffer, and one can check their availability using ''Serial.available()''. This function returns the number of bytes received so far from the external device (here, e.g. a terminal) connected to the corresponding serial port. | The serial port and a class ''Serial'' handling the communication are bi-directional. It means one can send a message from the MCU to the terminal and the opposite. This can be used as a simple user interface. All configuration above steps to ensure seamless cooperation of the MCU serial interface and terminal (application) are also in charge here. As data is streamed byte by byte, it is usually necessary to buffer it. Technically, the serial port notifies the MCU every time a character comes to the serial port using the interrupts. Luckily, part of the job is done by the ''Serial'' class: all characters are buffered in an internal buffer, and one can check their availability using ''Serial.available()''. This function returns the number of bytes received so far from the external device (here, e.g. a terminal) connected to the corresponding serial port. |
<note important>Many MCUs provide hardware and software serial ports and allow multiple ports to be used. However, one serial port is usually considered the main one and is used for programming (flashing) the MCU. It is also common that other ports are implemented as software ones, so they put extra load on the MCU's processor and resources such as RAM, timers and interrupt system.</note> | <note important>Many MCUs provide hardware and software serial ports and allow multiple ports to be used. However, one serial port is usually considered the main one and is used for programming (flashing) the MCU. It is also common that other ports are implemented as software ones, so they put extra load on the MCU's processor and resources such as RAM, timers and interrupt system.</note> |
Data in the serial port are sent as bytes; thus, it is up to the developer to handle the correct data conversion. Reading a single byte of the data using ''Serial.read()'' gets another character from the FIFO queue behind the serial port software buffer. As most communication is done textual way, the ''Serial'' class has support to ease the reading of the strings: ''Serial.readString()'', but use involves some extra logic such as the function may timeout. Also, it may contain the END-OF-LINE / NEXT-LINE characters that should be trimmed before usage ((https://www.arduino.cc/reference/en/language/functions/communication/serial/readstring/)). | Data in the serial port are sent as bytes; thus, it is up to the developer to handle the correct data conversion. Reading a single byte of the data using ''Serial.read()'' gets another character from the FIFO queue behind the serial port software buffer. As most communication is done textual way, the ''Serial'' class has support to ease the reading of the strings: ''Serial.readString()'', but use involves some extra logic such as the function may timeout. Also, it may contain the END-OF-LINE / NEXT-LINE characters that should be trimmed before usage ((https://www.arduino.cc/reference/en/language/functions/communication/serial/readstring/)). |
| |
===== Hardware buttons ===== | ==== Hardware buttons ==== |
Hardware buttons tend to vibrate when switching. This physical effect causes bouncing of the state forth and back, generating, in fact, many pulses instead of a single edge during switching. Getting rid of this is called debouncing. In most cases, switches (buttons) short to 0 (GND) and use pull-up resistors, as in the figure {{ref>pullupsample}}. | Hardware buttons tend to vibrate when switching. This physical effect causes bouncing of the state forth and back, generating, in fact, many pulses instead of a single edge during switching. Getting rid of this is called debouncing. In most cases, switches (buttons) short to 0 (GND) and use pull-up resistors, as in the figure {{ref>pullupsample}}. |
<figure pullupsample> | <figure pullupsample> |
A flow between the states can be then described in the following diagram (figure {{ref>statemachine}}). | A flow between the states can be then described in the following diagram (figure {{ref>statemachine}}). |
<figure statemachine> | <figure statemachine> |
{{ :en:iot-open:introductiontoembeddedprogramming2:cppfundamentals:debouncing_state_diagram.drawio.png?400 | State machine and transitions for button handling with software debouncing}} | {{ :en:iot-open:introductiontoembeddedprogramming2:cppfundamentals:debouncing_state_diagram.drawio.png?350 | State machine and transitions for button handling with software debouncing}} |
<caption>State machine and transitions for button handling with software debouncing</caption> | <caption>State machine and transitions for button handling with software debouncing</caption> |
</figure> | </figure> |
unsigned long tDebounceTime; | unsigned long tDebounceTime; |
unsigned long DTmr; | unsigned long DTmr; |
void(*ButtonPressed)(void); //On button pressed callback | void(*ButtonPressed)(void); //On button pressed callback |
void(*ButtonReleased)(void); //On button relased callback | void(*ButtonReleased)(void); //On button released callback |
void btReleasedAction() { //Action to be done when current state is RELEASED | void btReleasedAction() { //Action to be done |
| //when current state is RELEASED |
if(digitalRead(ButtonPin)==LOW) { | if(digitalRead(ButtonPin)==LOW) { |
buttonState = DEBOUNCING; | buttonState = DEBOUNCING; |
} | } |
} | } |
void btDebouncingAction() { //Action to be done when current state is DEBOUNCING | void btDebouncingAction() { //Action to be done |
| //when current state is DEBOUNCING |
if(millis()-DTmr > tDebounceTime) | if(millis()-DTmr > tDebounceTime) |
if(digitalRead(ButtonPin)==LOW) { | if(digitalRead(ButtonPin)==LOW) { |
buttonState=RELEASED; | buttonState=RELEASED; |
} | } |
void btPressedAction() { //Action to be done when current state is PRESSED | void btPressedAction() { //Action to be done |
| //when current state is PRESSED |
if(digitalRead(ButtonPin)==HIGH) { | if(digitalRead(ButtonPin)==HIGH) { |
buttonState=RELEASED; | buttonState=RELEASED; |
} | } |
public: | public: |
PullUpButtonHandler(uint8_t pButtonPin, unsigned long pDebounceTime) { //Constructor | PullUpButtonHandler(uint8_t pButtonPin, unsigned long pDebounceTime) { |
| //Constructor |
ButtonPin = pButtonPin; | ButtonPin = pButtonPin; |
tDebounceTime = pDebounceTime; | tDebounceTime = pDebounceTime; |
} | } |
void fRegisterBtPressCalback(void (*Callback)()) { //Function registering a On PRESSED callback | void fRegisterBtPressCalback(void (*Callback)()) { |
| //Function registering |
| //a button PRESSED callback |
ButtonPressed = Callback; | ButtonPressed = Callback; |
} | } |
void fRegisterBtReleaseCalback(void (*Callback)()) { //Function registering a On RELEASED callback | void fRegisterBtReleaseCalback(void (*Callback)()) { |
| //Function registering |
| //a button RELEASED callback |
ButtonReleased = Callback; | ButtonReleased = Callback; |
} | } |
void fButtonAction() //Main, non blocking loop. Handles state machine logic | void fButtonAction() //Main, non blocking loop. |
{ //along with private functions above | //Handles state machine logic |
| { //along with private functions above |
switch(buttonState) { | switch(buttonState) { |
case RELEASED: btReleasedAction(); | case RELEASED: btReleasedAction(); |
} | } |
</code> | </code> |
<note important>The ''PullUpButtonHandler'' is instantiated with a 200ms deboucing time. That defines a minimum press time to let the machine recognise the button press correctly. That time is quite long for most applications and use cases and can be easily shortened.</note> | <note important>The ''PullUpButtonHandler'' is instantiated with a 200ms deboucing time. That defines a minimum press time to let the machine recognise the button press correctly. That time is quite long for most applications and can be easily shortened.</note> |
The great feature of this FSM is that it can be easily extended with new functions, such as detecting the double click or long button press. | The great feature of this FSM is that it can be easily extended with new functions, such as detecting the double click or long button press. |
| |