⬆️ ⬇️

ICA (state machine) for teapots on the example of the class "button" in the arduino

Why is all this necessary?



When a teapot, having rested on the need to move away from a simple sequence of actions, asks a question like “how to do this?” In Habré, with a probability of 70%, “google finite automata” and 30% “use finite state machine” depending on the employer's country professional The next question is "how?" sent to google. There is such a teapot that he just finished blinking with an LED and wiped sweat from his forehead, that he taught German at school and worked all his life as a bulldozer in this Google and sees there Wikipedia articles about finite automata with formulas and in which only prepositions are clear.



Since I am also a teapot, but before the bulldozer I worked as a programmer 30 years ago, stepping on a lot of rakes as I mastered programming microcontrollers, I decided to write this article in simple language for beginners.



So, the task is, for example, to teach an Arduin to understand pressing a button like click, pressing and holding, that is, a short button into a button, long and very long. Any beginner can do this in the loop () function and be proud of it, but what if there are several buttons? And if at the same time it is necessary not to interfere with the execution of another code? I wrote a library SmartButton for Arduino, then not to return to the topic of buttons. Here I will write how it works.



What prevents us from living and how to deal with it?



The delay () function prevents us from living, I overcame it with the help of the ICA and wrote my own class SmartDelay . I have already made a new class (SmartDelayMs) generated from SmartDelay, everything is the same there, but in milliseconds. In this article, I do not use this library, just bragging.



In order not to bore the reader again with a story about how to use the delay () function badly and how to live without it, I recommend reading my old article Replacing delay () for non-blocking delays in the Arduino IDE first . I will repeat the main points here a little more here as I write the code.



Some theory



One way or another, but you have objects . You can call them buttons, displays, LED strips, robots and a water level meter in the tank. If your code is executed not in one thread sequentially, but for some events, you keep the state of the objects. You can call it whatever you like, but that's a fact. For a button, there are, for example, "pressed" and "released" states. For a robot, for example: standing, going straight, turning. The number of these states of course. Well, in our case, yes. Add states "click", "click" and "hold". Already five states that we need to distinguish. Moreover, we are interested in only the last three. These five states live button. In the terrible external world, events occur that do not depend on the button in general: poking a finger at it, releasing it, holding it for a different time, and so on. Let's call them an event .



So, we have an object "button" which has a finite number of states and events affect it. This is the state machine ( SC ). In CA theory, the list of possible events is called a dictionary, and events are words. I wildly apologize, I passed this 30 years ago and do not remember the terminology very well. You don't need terminology, do you?



In this article we will write a wonderful code that will translate our spacecraft from state to state depending on the events. It is the actual machine of finite automata ( ICA ).



It is not necessary to push all your device into one huge spacecraft, it can be divided into separate objects, each of which is serviced by its own ICA and is even located in a separate code file. You can put many of your objects on GitHub in the form of ready-made arduin libraries and use them later without delving into their implementation.



If your code does not require portability and reuse, you can sculpt everything into one large file, which is what beginners do.



Practice



The basis for the design of the ICA is the table.





For a button, it will look like this:



State \ EventPressedReleasedPressed 20ms250ms pressedPressed 1sPressed 3s
Not pressed
Clicked
Pressed
Retained
Held too long


Why so hard? It is necessary to describe all possible states and take into account all possible events.



Developments:





You can set your time as you like. In order not to change the numbers in different places, it is better to immediately replace them with letters :)



#define SmartButton_debounce 20 #define SmartButton_hold 250 #define SmartButton_long 1000 #define SmartButton_idle 3000 


States:





So, we translate Russian into C ++



 //     byte btPin; //  enum event {Press, Release, WaitDebounce, WaitHold, WaitLongHold, WaitIdle}; //  enum state {Idle, PreClick, Click, Hold, LongHold, ForcedIdle}; //   enum state btState = Idle; //     unsigned long pressTimeStamp; 


I’ll draw your attention to the fact that both events and states are not specified as an integer variable of the type byte or int, but as enum. Such a record is not so loved by professionals as it does not save bits so expensive in memory microcontrollers, but, on the other hand, it is very visual. When using enum, we do not care how much each code is encoded, we use words, not numbers.



How to decipher enum?

enum Name {Constant, Constant, Constant};

So we create the data type enum Name, which has a set of fixed values ​​from those in curly braces. Actually, of course, this is a numeric integer type, but the value of the constants is numbered automatically.

You can also specify the values ​​manually:

enum Name {ConstantA = 0, ConstantB = 25};



To declare a variable of this type, you can write:

enum Name Variable;



In the example with the button:



  • enum event myEvent;
  • enum state currentState;


Let's fill the table already. The icon -> I denote the transition to a new state.



State \ EventPressedReleasedPressed 20ms250ms pressedPressed 1sPressed 3s
Not pressed-> Bounce-> Not Pressed
Bounce-> Not Pressed-> Clicked
Clicked-> Not Pressed-> Pressed
Pressed-> Not Pressed-> Retained
Retained-> Not Pressed-> Too long withheld
Held too long-> Not Pressed


To implement the table, we will make the function void doEvent (enum event e). This function receives the event and performs the action as described in the table.



Where do events come from?



It is time to briefly leave our cozy button and plunge into the terrible external world of the loop () function.



 void loop() { //     unsigned long mls = millis(); //   //   if (digitalRead(btPin)) doEvent(Press) else doEvent(Release) //    if (mls - pressTimeStamp > SmartButton_debounce) doEvent(WaitDebounce); //    if (mls - pressTimeStamp > SmartButton_hold) doEvent(WaitHold); //    if (mls - pressTimeStamp > SmartButton_long) doEvent(WaitLongHold); //     if (mls - pressTimeStamp > SmartButton_idle) doEvent(WaitIdle); } 


So, having an event generator at hand, you can start implementing the ICA.



In fact, events can be generated not only by raising the signal level at the controller's foot and by time. Events can be transferred from one object to another. For example, you want to press one button automatically to "release" another. To do this, just call its doEvent () with the Release parameter. ICA is not only buttons. Exceeding the temperature threshold is an event for the ICA and causes the fan to turn on in the greenhouse, for example. The fan fault sensor changes the state of the greenhouse to "emergency" and so on.



Three approaches to the implementation of the table in the code



Plan A: if {} elsif {} else {}



This is the first thing that comes to mind beginner. In fact, this is a good option if there are very few cells in the table and actions are reduced only to a change of states.



 void doAction(enum event e) { if (e == Press && btState == Idle) { //       btState=PreClick; //      pressTimeStamp=millis(); //     } if (e == Release) { //   btState=Idle; //      } if (e == WaitDebounce && btState == PreClick) { //    btState=Click; // ,    } //          } 


Pros:





Minuses:





Plan-B: Table of function pointers



The other extreme is a table of transitions or functions. I wrote about this approach in the article Processing keystrokes for Arduino. Cross OOP and ICA. . In a nutshell, you create a separate function for each different table cell and make a table of pointers to them.



 //  ,   typedef void (*MKA)(enum event e); //   MKA action[6][6]={ {&toDebounce,$toIdle,NULL,NULL,NULL,NULL}, {NULL,&toIdle,&toClick,NULL,NULL,NULL}, {NULL,&toIdle,NULL,&toHold,NULL,NULL}, {NULL,&toIdle,NULL,NULL,&toLongHold,NULL}, {NULL,&toIdle,NULL,NULL,NULL,&toVeryLongHold}, {NULL,&toIdle,NULL,NULL,NULL,NULL} }; //  doEvent    void doEvent(enum event e) { if (action[btState][e] == NULL) return; (*(action[btState][e]))(e); } //     //   " " void toIdle(enum event e) { btState=Idle; } //     void toDebounce(enum event e) { btState=PreClick; pressTimeStamp=millis(); } //    


What are the weird words and badges used here?

typedef allows you to define your data type and is often convenient to use. For example, the enum event can be defined as:



 typedef enum event BottonEvent; 


In the program after that you can write:



 BottonEvent myEvent; 


I have defined a pointer to a function that takes a single argument of type enum event and returns nothing:



 typedef void (*MKA)(enum event e); 


So I made a new data type MKA.



The & icon is translated into Russian as an "address" or "pointer." Thus, MKA is not a function value, but a pointer to it.



 MKA action[6][6]; //   action    


I immediately filled it with pointers to functions that perform actions. If there is no action, I put the "pointer to nowhere" NULL.



The doEvent function checks the pointer from the table in the coordinates "current state" x "incident event" and if there is a pointer to the function for this, calls it with the "event" parameter.



  if (action[btState][e] == NULL) return; //    -  (*(action[btState][e]))(e); //       


For more information about pointers and address arithmetic in the C language, you can google it in books like "C language for teapots".



Pros:





Minuses:





The minus turned out to be so bold that I after writing the Processing of button presses for the Arduino. Cross OOP and ICA. And the very first application of the code in the case I saw it all nafig. In the end, I came to the golden mean "switch {switch {}}".



Plan-In: Golden mean or switch {switch {}}



The most common, medium variant is the use of nested switch statements. For each event as a case of the switch statement, you must write a switch with the current state. It turns out long, but more or less clear.



 void doEvent(enum event e) { switch (e) { case Press: switch (btState) { case Idle: btState=PreClick; pressTimeStamp=millis(); break; } break; case Release: btState=Idle; break; // ...   ... } } 


A smart compiler will still build a transition table, as described in Plan B above, but this table will be in flash, in code, and will not occupy the memory of variables so dear to us.



Pros:





Minuses:





In fact, you can use Plan-B and Plan-A, that is, switch by event, but if inside by state. For my taste, switch and for that and for that it is clearer and more convenient, if you need to change something later.



Where to insert my code, which depends on the button presses?



In the tablet, we did not take into account the presence of other ICAs focusing on our button event handler. In practice, it is required not only to rejoice at the fact that the button works, but also to perform other actions.



We have two important places to insert good code:



 case Release: switch(btState) { case Click: //       .    . break; 


  case WaitDebounce: switch (st) { case PreClick: btState=Click; //    .           "" . break; 


It is best not to spoil the beauty, write your own functions like offClick () and onClick (), in which these events will be processed, it is possible that they will transfer it to another ICA: D



I wrapped all the logic of the ICA button in the class C ++ . This is convenient, since it does not distract from writing the main code, all the buttons work independently, I don’t need to reinvent the heap variable names. Only this is a completely different story and I can write how to arrange your ICA into classes and make libraries for Arduino out of them. Write in the comments wishes.



What is the result?



As a result, in theory, after reading this article, there should be a desire to replace in your favorite arduin sketch your unreadable buggy shit code with a beautiful structured one and using ICA.



Please note that in the loop () function, which in our case only generates events, there is not a single delay. Thus, after the above code, you can write something else: generate events for other ICA, execute some other code that does not contain delay ();



I really hope that this article has helped you understand what ICA is and how to implement them in your sketch.



')

Source: https://habr.com/ru/post/345960/



All Articles