Programming patterns

 General audience classification icon  General audience classification icon  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 Timing or interrupts Interrupts.

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.
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:

void setup(){
  delay(100);
  Serial.begin(115200);
  Serial.println();
  ...
}
A rule of thumb is that after programming and during a boot, every MCU drops some garbage to the serial buffer. That is visualised as several random characters in the terminal. To easily distinguish the tracing from the garbage, it is advised to put some delay(100) at the beginning of the code and drop one or two “new line” characters to scroll garbage up using dummy println() call (once or twice is usually enough).

The Serial object has several handy methods that can help represent various variable types in a textual form to be sent via a serial port to the terminal. The most common are:

  • Serial.print(x) where x is any simple type available in the Arduino Framework, such as integers and floats, but also visualises arrays of characters and String objects.
  • 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

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.

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.

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 [1].

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 1.

 Sample circuit of the switch with an external pull-up resistor connected to the GPIO2 of the MCU
Figure 1: Sample circuit of the switch with an external pull-up resistor connected to the GPIO2 of the MCU

The switch, when open, results in VCC through R1 driving the GPIO2 (referenced as HIGH), and when short, 0 is connected to it, so it becomes LOW:

  • button short → GPIO2=LOW,
  • button released → GPIO2=HIGH.

Some MCUs offer internal pull-ups and pull-downs, configurable from the software level. The transition state between HIGH and LOW causes bouncing.

A dummy debouncing mechanism only checks periodically for a press/release of the button. The common period for debouncing is between 50ms and 200ms. The code below shows an example that has been provided for presentation purposes. Yet, it is not flexible nor pragmatic due to the exhausting use of the loop() function and extensive use of delay(). An internal pull-up resistor is in use in this example:

#define BUTTON_GPIO 2
 
bool bButtonPressed=false;
 
void setup() {
  Serial.begin(9600);
  pinMode(BUTTON_GPIO, INPUT_PULLUP);
}
 
void loop() {
  if (digitalRead(BUTTON_GPIO)==LOW && !bButtonPressed)
  {
    Serial.println("Button pressed");
    delay(200);
    bButtonPressed=true;
  }
  if (bButtonPressed && digitalRead(BUTTON_GPIO)==HIGH)
  {
    Serial.println("Button released");
    bButtonPressed=false;
    delay(200);
  }
}

A more advanced technique for complex handling of the buttons is presented below in the context of the State Machines.

Finite State Machine

A Finite State Machine (FSM) idea represents states and flow conditions between the states that reflect how the software is built for the selected system or its component. An example of button handling using the FSM is present here. The FSM reflects the physical state of the device, sensor or system on the software level, becoming a digital twin of a real device.

For the simple case (without detecting double-click or long press), 3 different button states can be distinguished: released, debouncing and pushed. An enumerator is an excellent choice to model those states (it is easily expandable):

typedef enum {
  RELEASED = 0,
  DEBOUNCING,
  PRESSED
} tButtonState;

A flow between the states can be then described in the following diagram (figure 2).

 State machine and transitions for button handling with software debouncing
Figure 2: State machine and transitions for button handling with software debouncing
  • In the RELEASED state, there is waiting until the button is pressed (LOW, for the pull-up model). The time is noted when it occurs, and the state changes to the DEBOUNCING.
  • In the DEBOUNCING state, if debouncing time passes and the button is still pressed (LOW), the machine changes its state to PRESSED. If the button in DEBOUNCING becomes released (HIGH), then the machine returns to the state RELEASED.
  • In the PRESSED state, it transits to the RELEASED whenever the button goes HIGH.

The state machine is implemented as a simple class and has 2 additional fields that store handlers for functions that are called when the state machine enters PRESSED or RELEASED. Those functions are called callbacks. There are 2 public functions for callback registration as callback handlers class members are private. fButtonAction() is intended to be called in a loop() as many times as possible to “catch” all pushes of the button:

class PullUpButtonHandler{
  private:
    tButtonState buttonState=RELEASED;
    uint8_t ButtonPin;
    unsigned long tDebounceTime;
    unsigned long DTmr;
    void(*ButtonPressed)(void);    //On button pressed callback
    void(*ButtonReleased)(void);   //On button released callback
    void btReleasedAction() {      //Action to be done 
                                   //when current state is RELEASED
      if(digitalRead(ButtonPin)==LOW) {
        buttonState = DEBOUNCING;
        DTmr = millis();
      }
    }
    void btDebouncingAction() {    //Action to be done 
                                   //when current state is DEBOUNCING
      if(millis()-DTmr > tDebounceTime)
        if(digitalRead(ButtonPin)==LOW) {
          buttonState = PRESSED;
          if(ButtonPressed!=NULL) ButtonPressed();
        }
        else
          buttonState=RELEASED;
    }
    void btPressedAction() {       //Action to be done 
                                   //when current state is PRESSED
      if(digitalRead(ButtonPin)==HIGH) {
        buttonState=RELEASED;
        if(ButtonReleased!=NULL) ButtonReleased();
      }
    }
  public:
    PullUpButtonHandler(uint8_t pButtonPin, unsigned long pDebounceTime) {  
                                   //Constructor
      ButtonPin = pButtonPin;
      tDebounceTime = pDebounceTime;
    }
    void fRegisterBtPressCalback(void (*Callback)()) {    
                                   //Function registering 
                                   //a button PRESSED callback
      ButtonPressed = Callback;
    }
    void fRegisterBtReleaseCalback(void (*Callback)()) {  
                                   //Function registering 
                                   //a button RELEASED callback
      ButtonReleased = Callback;
    }
    void fButtonAction()           //Main, non blocking loop. 
                                   //Handles state machine logic 
    {                              //along with private functions above
      switch(buttonState) {
        case RELEASED: btReleasedAction();
          break;
        case DEBOUNCING: btDebouncingAction();
          break;
        case PRESSED: btPressedAction();
          break;
        default:
          break;
      }
    }
};

Sample use looks as follows:

#define BUTTON_GPIO 2
 
PullUpButtonHandler bh = PullUpButtonHandler(BUTTON_GPIO, 200);
void onButtonPressed() {
  Serial.println("Button pressed");
}
void onButtonReleased() {
  Serial.println("Released");
}
void setup() {
  Serial.begin(9600);
  pinMode(BUTTON_GPIO, INPUT_PULLUP);
  bh.fRegisterBtPressCalback(onButtonPressed);
  bh.fRegisterBtReleaseCalback(onButtonReleased);
}
 
void loop() {
  bh.fButtonAction();
}
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.

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.

en/iot-open/introductiontoembeddedprogramming2/cppfundamentals/programmingpatterns.txt · Last modified: 2024/05/27 10:53 by ktokarz
CC Attribution-Share Alike 4.0 International
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0