Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
en:iot-open:practical:hardware:sut:esp32:adv1_1 [2024/03/25 17:39] – [Steps] pczekalskien:iot-open:practical:hardware:sut:esp32:adv1_1 [2024/03/26 08:46] (current) – [Steps] pczekalski
Line 1: Line 1:
 ====== ADV1: Using timers to execute code asynchronously ======  ====== ADV1: Using timers to execute code asynchronously ====== 
-It is advised to use timers that periodically execute a function to handle repeating tasks. Hardware timers work parallel to the CPU and consume few CPU resources. ESP32-S3 has 4 hardware timers, but each timer can execute multiple handlers. +It is advised to use timers that periodically execute a function to handle repeating tasks. Hardware timers work parallel to the CPU and consume few CPU resources. ESP32-S3 has 4 hardware timers, but each timer can execute multiple handlers. You can think about these handlers as they are interrupted handling functions, but instead of externally triggered interrupts, those are initiated internally by the hardware timer.\\ 
-The idea of using the timer is to encapsulate a piece of compact code that can be run virtually asynchronously and executed is a precisely-defined time manner. In this scenario, we use a timer to update the LCD screen periodically. We choose a dummy example where Timer 1 is used to increment a value of the ''byte'' type, and Timer 2 reads this value and writes it on the LCD. Naturally, a collision may occur whenever two processes are to access a single memory block (variable). It is critical when both processes (tasks, asynchronous functions) are writing to it. However, in our example, the handler executed by Timer 1 only writes to the variable, while Timer 2 only reads from it. In this scenario, there is no need to use mutexes (semaphores). A scenario with detailed description of when to use mutexes is beyond the scope of this example.+The idea of using the timer is to encapsulate a piece of compact code that can be run virtually asynchronously and executed is a precisely-defined time manner. In this scenario, we use a timer to update the LCD screen periodically. We choose a dummy example where Timer 1 is used to increment a value of the ''byte'' type, and Timer 2 reads this value and writes it on the LCD. Naturally, a collision may occur whenever two processes are to access a single memory block (variable). It is critical when both processes (tasks, asynchronous functions) are writing to it. However, in our example, the handler executed by Timer 1 only writes to the variable, while Timer 2 only reads from it. In this scenario, there is no need to use mutexes (semaphores). A scenario with detailed description of when to use mutexes is beyond the scope of this example.
  
  
Line 21: Line 21:
  
 ==== Task to be implemented ==== ==== Task to be implemented ====
-Present on the LCD current value of the ''byte'' variable; update the LCD every 3 seconds using one timer. Use another timer to increase this variable every second.+Present on the LCD current value of the ''byte'' variable; update the LCD every 3 seconds using timer. Use another timer to increase this variable every second.
  
 ==== Start ==== ==== Start ====
Line 29: Line 29:
 We used to separate tasks, but for this case, complete code is provided in chunks, including LCD handling. It presents relations on where particular parts of the code should be located when using timers and how timing relates between components.  We used to separate tasks, but for this case, complete code is provided in chunks, including LCD handling. It presents relations on where particular parts of the code should be located when using timers and how timing relates between components. 
  
-<note tip>It is important e.g. when LCD had to be set up and configured before asynchronous handlers execute writing to it, so set up order is meaningful!</note>+<note tip>It is importante.g. when LCD had to be set up and configured before asynchronous handlers execute writing to it, so set up order is meaningful!</note>
 === Step 1 === === Step 1 ===
 Include libraries: Include libraries:
Line 37: Line 37:
 </code> </code>
  
 +=== Step 2 ===
 +Define LCD configuration pins (see details and explanation in the scenario: [[en:iot-open:practical:hardware:sut:esp32:emb5_1|]]):
 +<code c>
 #define LCD_RS 2 #define LCD_RS 2
 #define LCD_ENABLE 1 #define LCD_ENABLE 1
Line 43: Line 46:
 #define LCD_D6 41 #define LCD_D6 41
 #define LCD_D7 42 #define LCD_D7 42
 +</code>
  
 +=== Step 3 ===
 +Declare variables and classes (timers):
 +<code c>
 volatile byte i = 0; volatile byte i = 0;
 hw_timer_t *Timer1 = NULL; hw_timer_t *Timer1 = NULL;
 hw_timer_t *Timer2 = NULL; hw_timer_t *Timer2 = NULL;
 +static Adafruit_LiquidCrystal lcd(LCD_RS, LCD_ENABLE, LCD_D4, LCD_D5, LCD_D6, LCD_D7);
 +</code>
 +Above, we declare two separate timers: ''Timer1'' and ''Timer2'' as pointers. They are initialised later in the setup code.
 +
 +=== Step 4 ===
 +Declare constants that define how frequently timers will be calling handlers:
 +<code c>
 const int baseFrequency = 80; //MHz const int baseFrequency = 80; //MHz
 const int interval1 = 1000000;  // Interval in microseconds = 1s const int interval1 = 1000000;  // Interval in microseconds = 1s
 const int interval2 = 3000000;  // Interval in microseconds = 3s const int interval2 = 3000000;  // Interval in microseconds = 3s
 +</code>
 +The base frequency for the timers in ESP32 is 80 MHz. Each timer counts in microseconds, so we define 2 intervals: 1s (1000000us) for the counter increase and 3s (3000000us) for the LCD update routine.
  
-static Adafruit_LiquidCrystal lcd(LCD_RS, LCD_ENABLE, LCD_D4, LCD_D5, LCD_D6, LCD_D7); +=== Step 5 === 
- +Declare and implement functions that are timer handles: timer calls the bound function every execution period: 
-void IRAM_ATTR onTimer1()+<code c> 
 +void IRAM_ATTR onTimer1() //handler for Timer1
 { {
     i++;     i++;
 } }
  
-void IRAM_ATTR onTimer2()+void IRAM_ATTR onTimer2() //handler for Timer2
 { {
   lcd.clear();   lcd.clear();
Line 64: Line 81:
   lcd.print(i);   lcd.print(i);
 } }
 +</code>
 +
 +<note warning>In no case should you use ''float'' variables when using asynchronous calls such as timer routines (handlers) and ISR (interrupt functions)! This is because of the limitation of the ESP32 where type ''float'' is hardware implemented in FPU: FPU cannot share its resources among cores, and timer routines may switch from one core to another during consecutive executions, so in that case, your application can hang and throw an exception. An exception won't be seen on the screen as you're remote, so you may notice only the laboratory node becoming unresponsive. This kind of error is usually tough to trace. </note>
 +<note tip>If you do need to use floating point calculations in ISR or timer functions, you can use ''double'' as this type is not hardware accelerated in ESP32s and is software implemented. Note, however, that there is a significant performance drop between ''float'' (faster) and ''double'' (slower) calculations.</note>
 +
 +<note important>Timer handlers are marked with ''IRAM_ATTR'' to be kept in RAM rather than flash. It is because of the performance. Interrupt and Timer functions should be kept as simple as possible. A watchdog exception can be thrown if more complex tasks are to be done and handler execution takes too long, particularly when the previous execution is not finished before the next one starts. This sort of problem may require fine-tuning the timer frequency and eventually changing the algorithm to set up only a flag in the handler that is later detected and handled in the ''loop()'' function.</note>
 +=== Step 6 ===
 +Configure timers, start LCD and enable timers. Note the correct order: LCD have to be ready when the timer calls ''LCD.write(...);'':
 +<code c>
 void setup(){ void setup(){
   //Timer1 config   //Timer1 config
Line 69: Line 95:
   timerAttachInterrupt(Timer1, &onTimer1, true);   timerAttachInterrupt(Timer1, &onTimer1, true);
   timerAlarmWrite(Timer1, interval1, true);   timerAlarmWrite(Timer1, interval1, true);
-  timerAlarmEnable(Timer1); 
      
   //Timer2 config   //Timer2 config
Line 75: Line 100:
   timerAttachInterrupt(Timer2, &onTimer2, true);   timerAttachInterrupt(Timer2, &onTimer2, true);
   timerAlarmWrite(Timer2, interval2, true);   timerAlarmWrite(Timer2, interval2, true);
-  timerAlarmEnable(Timer2); 
      
   //start LCD   //start LCD
   lcd.begin(16,2);   lcd.begin(16,2);
   lcd.clear();   lcd.clear();
 +  
   //start both timers   //start both timers
   timerAlarmEnable(Timer1);   timerAlarmEnable(Timer1);
   timerAlarmEnable(Timer2);   timerAlarmEnable(Timer2);
 } }
 +</code>
 +In the code above, ''Timer1 = timerBegin(0, baseFrequency, true);'' creates a new timer bound to the hardware timer 0 (first, timers are zero-indexed). The last parameter causes the timer to count up (false counts down) internally, as every timer has its counter (here 64-bit).\\
 +Following, ''timerAttachInterrupt(Timer1, &onTimer1, true);'' attaches function ''onTimer1'' (by its address, so we use ''&'' operator) to the ''Timer1'' to be executed periodically.\\
 +Then we define how frequently the execution of the function above will occur: ''timerAlarmWrite(Timer1, interval1, true);''. The last parameter causes the timer to reload after each execution automatically. Timers can also trigger an action only once (you need to set up the last parameter to ''false''). In our example, we wish continuous work, so we use ''true''.\\
 +Note that at this moment, timers are not executing the handlers yet; the last step is required: ''timerAlarmEnable(Timer1);''. This step is separated from the other configuration steps because the LCD has to be initialised before timers can use the LCD.
  
 +
 +=== Step 7 ===
 +This way, a main loop is empty: everything runs asynchronously, thanks to the timers. As suggested above, when timer handlers require long or unpredictable execution time (e.g. external communication, waiting for the reply), handlers should set a flag that is read in the main loop to execute the appropriate task and then clear the flag.
 +<code c>
 void loop() void loop()
 { {
  
 } }
- +</code>
-=== Step 2 === +
- +
-... +
- +
-=== Step n === +
-//Describe activities done in Step n.// +
 ==== Result validation ==== ==== Result validation ====
 On the LCD screen, you should see values starting from number 3, then 6, 9, 12 and so on (=3 every 3 seconds). Note, as ''byte'' has a capacity of 256 (0...255), the sequence changes in the following increments once it overflows. On the LCD screen, you should see values starting from number 3, then 6, 9, 12 and so on (=3 every 3 seconds). Note, as ''byte'' has a capacity of 256 (0...255), the sequence changes in the following increments once it overflows.
en/iot-open/practical/hardware/sut/esp32/adv1_1.1711388344.txt.gz · Last modified: 2024/03/25 17:39 by pczekalski
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