Timers are one of the common hardware features in all microcontrollers. Timers and timer interrupts are of great use in microcontroller applications. Timer interrupts are often used when precise timing is required without a fraction of error. All MicroPython ports have one or more timers. Some of these timers can be reserved for specific functions, such as networking or Wi-Fi, while the remaining timers can be used in the user's application. This article will explore timers and implementing timer interrupts in MicroPython. Next, we will look at the timers available on the ESP8266 and ESP32. Finally, we will use the concepts mentioned above to design an ESP8266 ticker.
MicroPython time-related functions
MicroPython provides a timing module for time delays, intervals, and date and time maintenance. This module is a reimplementation of a CPython module of the same name. MicroPython's time-related functions support built-in ports, which use the epoch of 2000-01-01 00:00:00 UTC instead of the POSIX systems epoch of 1970-01-01 00:00:00 UTC. It is important to note that setting and maintaining the calendar time depends on the operating system or RTOS installed on the supported port like on Raspberry Pico. An integrated RTC can manage this in case of microcontroller ports. MicroPython time-related functions consult the OS/RTOS or RTC to maintain or set date and time. Setting the time can be done manually via a network protocol or battery backup.
The time module provides the following functions related to time, date and delay.
time.sleep(seconds) : This blocking method provides a delay in seconds. Some ports allow you to specify the delay time as a floating point number. Once the method is called, the controller stops executing the user program for the defined number of seconds.
tempo.sleep_ms(ms) : This is another blocking method that provides a delay in milliseconds. Compared with time.sleep , this method is more accurate. A positive number or zero can be passed as an argument to this method. If used for a timer interrupt, the delay may extend until execution of the interrupt service routine is complete. If 0 is passed as an argument, the delay is equal to the time spent executing the interrupt service routine.
time.sleep_us(nodes) : This is yet another blocking method that provides a delay in microseconds. A positive number or zero can be passed as an argument. If used for a timer interrupt, the delay may extend until execution of the interrupt service routine is complete. If 0 is passed as an argument, the delay is equal to the time spent executing the interrupt service routine.
time.ticks_ms : This method returns the number of milliseconds passed in an arbitrary reference. It is somewhat similar to the Arduino millis function. Although the millis function in Arduino returns a time interval in milliseconds from the initialization of the Arduino board, time.ticks_ms takes an arbitrary time point for reference. The envelope value used as the maximum range is always in the power of 2, so it remains the same throughout the MicroPython implementation, regardless of port. It is called TICKS_PERIOD, which is equal to one more than TICKS_MAX. Therefore, a call to this function always returns a non-negative value ranging from 0 to TICKS_MAX inclusive, which is indicative of the milliseconds in the mentioned range passed at an arbitrary point in time. It is important to note that unlike Arduino, standard mathematical operators or relational operators, directly or as arguments to ticks_diff or ticks_add methods on the values returned by this method or marking as per the methods mentioned above are not allowed. Such operations may lead to a syntax error (depending on the IDE) or erroneous results.
time.ticks_us : This method is similar to time.ticks_ms except that it returns the elapsed time in microseconds from an arbitrary point in time.
time.ticks_cpu : This method is similar to time.ticks_ms and time.ticks_us except that it returns the past CPU clock cycles from an arbitrary point in time. It provides the highest possible resolution. Instead of clock cycles, another higher resolution unit provided by an integrated timer may be returned on some ports. It should be noted that this method is not available for all MicroPython ports. Therefore, port-specific documentation should be checked before using this method in a MicroPython script.
time.ticks_add(ticks, delta) : This method calculates deadlines for microcontroller events and tasks. Depending on the time, it returns a deadline in milliseconds, microseconds or CPU cycle . ticks_ms , time.ticks_us or time.ticks_cpu is used as ticks parameter respectively. The delta argument can be an integer or numeric argument. It is equal to the number of ticks, where ticks can be milliseconds, microseconds or CPU cycles defined as deadline. For example, the following statement returns a deadline of 200 milliseconds.
deadline = ticks_add(time.ticks_ms, 200)
The following statement returns a deadline of 100 microseconds.
deadline = ticks_add(time.ticks_us, 100)
The following statement returns a deadline of 20 CPU cycles.
deadline = ticks_add(time.ticks_cpu, 20)
time.ticks_diff(ticks1, ticks2) : This method returns the difference between two ticks. The tick difference can be in milliseconds, microseconds or CPU cycles depending on the value returned by time.ticks_ms , time.ticks_us or time.ticks_cpu functions are used as ticks respectively. The returned value is ticks1-ticks2, which can range from -TICKS_PERIOD/2 to TICKS_PERIOD/2 – 1. Time.ticks_diff is useful for looking up a timeout, scheduling a built-in task, or calculating a deadline.
time.hour : This method returns the number of seconds since the epoch as long as the RTC of a given port is set and maintained. The epoch supports embedding and the time returned is the number of seconds since 2000-01-01 at 00:00:00 UTC. The epoch can be a port-specific reference, such as time since boot or reboot on some ports.
hour.time_ns : This method returns the number of microseconds since the epoch. It is very useful for determining absolute time. The value returned by this method is an integer.
time.mktime : This method returns a number of seconds passed between the epoch (i.e. 2000-01-01 00:00:00 UTC) and a local time passed as an argument. The method considers a complete tuple of 8 as local time, where the tuple is in the following format – (year, month, day, hour, minute, second, day of the week, day of the year) where the values of the tuple must be in the following interval.
Year | year in DC |
month | 1~12 |
today | 1~31 |
hour | 0~23 |
minute | 0~59 |
second | 0~59 |
day of the week | 0~6 for Mon~Sun |
day of the year | 1~366 |
time.gmtime((secs)) : This method returns date and time in UTC from the seconds specified as argument. The time returned is a tuple in the format (year, month, day, hour, minute, second, day of the week, day of the year).
time.localtime((secs)) : This method returns date and time in local time as seconds are specified as an argument. The time returned is a tuple in the format (year, month, day, hour, minute, second, day of the week, day of the year). Local time can be set according to OS/RTOS or RTC.
Using ticking functions
Marking functions are useful when precise timing is required in a MicroPython user program. Timing functions can calculate the time spent performing a built-in task, set a deadline for a built-in task, set timeouts, and schedule built-in tasks.
Below is a valid example of calculating the time spent executing a part of the MicroPython script.
import time
start = time.ticks_us
…#MicroPython Statements for Timing Test
print(time.ticks_diff(time.ticks_us, start))
The following is a valid example of finding TICKS_MAX for a given port.
print(ticks_add(0, -1))
The following is a valid example of setting a deadline for a built-in task.
deadline = ticks_add(time.ticks_ms, 200)
while ticks_diff(deadline, time.ticks_ms ) > 0:
do_a_little_of_something
The following is a valid example of searching for a timed event.
start = time.ticks_us
while pin.value == 0:
if time.ticks_diff(time.ticks_us , start) > 500:
raise TimeoutError
Following is a valid example of scheduling built-in tasks using ticking functions.
now = time.ticks_ms
scheduled_time = task.scheduled_time
if ticks_diff(scheduled_time, now) == 0:
print(“Time to execute the task!”)
task.run
Problem with time-related functions
The ticking functions are quite accurate. The marking functions are useful for calculating time intervals, setting timeouts for events, and even scheduling tasks. Although non-blocking, these functions are not often used to provide delays or schedule tasks. The main reason behind this is the dependency of the timing module on OS/RTOS or RTC. Secondly, these methods can be interrupted by other higher priority microcontroller events.
On the other hand, the time module's delay-related functions, such as time.sleep , time.sleep_ms , and time.sleep_us , have two issues worth noting. First, these methods are blocking in nature and stop the script when called. Secondly, these methods do not provide an accurate delay. For example, a delay of a few seconds in the time.sleep method may give an error of a few milliseconds. These errors can be as high as 1 or 2 percent.
In these situations, timers arrive at the resort. Timers have higher priority interrupts that often cannot be overridden except with a reset. Its interrupts use underlying hardware i.e. the timer registers which leave no room for any error. To set timeouts or schedule time-critical built-in tasks, timer interrupts are the best options. At the same time, ticking functions can be used to set deadlines or calculate the time spent executing critical parts of the MicroPython script.
What is a timer?
Every microcontroller has some built-in hardware features. The timer/counter is one of the important integrated peripherals almost present in all microcontrollers. A timer/counter is used to measure time events or operate as a counter. A timer is linked to the microcontroller system clock, which allows you to track time with high precision and accuracy. There can be multiple timers in a microcontroller. Each timer is set, tracked and controlled by a set of internal registers.
What is a timer interrupt?
One of the important functions of timers is to time events. This is done with the help of timer interrupts. An event is nothing more than the execution of a specific block of code on a microcontroller. This code block is included within Interrupt Service Routine (ISR) Function . An ISR is executed when an interrupt is generated.
Normally, the microcontroller executes instructions in a sequential manner. When an interrupt is generated, the microcontroller skips the current code execution and executes the ISR first. Once the ISR completes, it resumes normal code execution.
Timer interrupts are triggered when the timer reaches a set count. A register updates the timer count, often called a timer register. There is another register where the user defines the reference count. This is often called a compare and match record. There may be one or more registers associated with configuring timer settings. There is a register that maintains values of several interrupt masks. Whenever a timer interrupt is triggered, its corresponding mask bit is toggled in the interrupt mask register. By tracking the interrupt mask bit, a timer interrupt is detected. This can provide a delay, set a timeout, or schedule tasks in an interrupt service routine.
MicroPython timer class
MicroPython provides a timer class to handle timers and timer interrupts of supported ports. The timer class is part of the machine module. It is imported into a MicroPython script using the following statement.
Timer machine import
If the port is WiPy, the following instruction must be used.
from importing the TimerWiPy machine
It is important to note that if errors are generated in the execution of an interrupt service routine, MicroPython does not produce an error report unless a special buffer is created for this. Therefore, a buffer must be created for debugging when timer interrupts or other interrupts are used in a MicroPython script. The buffer can be created using the following instructions.
import micropython
micropython.alloc_emergency_exception_buf(100)
It's important to note that the buffer only stores the most recent exception stack trace here. If a second exception is thrown while the heap is blocked, the stack trace of the second exception overwrites the original trace.
After importing the timer class, a timer object must be created. This is done by calling the constructor method. The constructor method has the following prototype.
class machine.Timer(id, /,…)
The constructor method takes the timer id as an argument. It can be a positive number 0, 1, 2, etc., for a hardware timer or -1 for a virtual timer, as long as the port supports it. The other configuration parameters can also be defined when calling the constructor method. Otherwise, the timer object can be set explicitly using the timer.init method. The timer.init method has the following prototype.
Timer.init(*, mode=Timer.PERIODIC, period=- 1, callback=None)
The mode can be set to timer.ONE_SHOT or Timer.PERIODIC. If set to timer.ONE_SHOT, the timer will only run once until the specified period in milliseconds has passed. If set to timer.PERIODIC, the timer runs periodically at an interval in milliseconds passed as the period argument. The period argument is the period of time in milliseconds used as a timeout to execute the callback function once or periodically, depending on the mode set. Call back is a call executed at the end of the time period. The interrupt service routine is called to perform the desired built-in tasks by raising the timer interrupt. The callable can be a function or even a method belonging to a class object.
The timer class allows you to stop the timer and disable the timer peripheral. This is done by calling the timer.deinit method. A call to this method immediately stops the timer if it is running, de-initializes the timer object, and disables the timer peripheral. It has the following prototype.
Timer.deinit
If a deactivated timer is to be reactivated, a timer object with the same ID must be created again in the user program.
MicroPython interrupt handlers
The callback function specified as an argument when initializing or creating a timer object is the interrupt service routine that runs when the timer interrupt is triggered. Interestingly, MicroPython does not expose register-level programming for timers. Instead, it uses timer interrupts to provide a timeout in milliseconds. The methods are available through the machine. Timers are typically applicable to all supported ports.
The callback function or interrupt service routine for timer objects requires certain programming precautions to avoid runtime failures. We have already discussed one of these precautions: defining a buffer object to store the last exception error. Let's discuss some more recommendations for writing interrupt handlers in MicroPython.
The body of an interrupt service routine should be as short and direct as possible. Interrupt service routines are intended to perform time-critical actions. These should not be misused for regular scheduling of built-in tasks. If any built-in task scheduling is required in a MicroPython script, it must be done using micropython.schedule . There should not be any type of memory allocation within an interrupt service routine. Avoid floating point values by inserting them into dictionaries or adding items to lists. However, global variables can be updated in interrupt service routines.
Most microcontroller platforms do not allow interrupt service routines to return values. However, MicroPython allows you to interrupt service routines and return one or more values. The ISR can return a single value by updating a global variable. If multiple values need to be returned, a pre-allocated byte array must be updated by the routine. A pre-allocated array object must be updated if the routine returns multiple integer values. However, this sharing of variables and objects between the ISR and the main loop can cause a race condition, where both the main program loop and the ISR race change the variable value. Therefore, updating the value of these variables in the main loop of the program requires extra care. Before updating the shared variable/byte array/array in the main loop of the program, interrupts must be disabled by calling the command pyb.disable_irq method. After updating the shared variable/byte array/array in the main program, interrupts can be re-enabled by calling the pyb.enable_irq method.
Timers in ESP8266
In ESP8266, there are two timers – timer0 and timer1. Timer0 is used for network functions. Timer1 is available for use on the ESP8266, but MicroPython provides access to the ESP8266 timers. Instead, ESP8266 provides an API for a virtual timer. This RTOS-based virtual timer has an id of -1. Following is a valid example of timer interrupt in ESP8266.
Timer machine import
tim = Timer(-1)
tim.init(period=500, mode=Timer.ONE_SHOT, callback=lambda t:print(1))
tim.init(period=200, mode=Timer.PERIODIC, callback=lambda t:print(2))
Timers in ESP32
ESP32 has four hardware timers with id 0 to 3. All timers are available to the user. Following is a valid example of timer interrupts in ESP32.
Timer machine import
tim0 = Timer(0)
tim0.init(period=2000, mode=Timer.ONE_SHOT, callback=lambda t:print(0))
tim1 = Timer(1)
tim1.init(period=1000, mode=Timer.PERIODIC, callback=lambda t:print(1))
Using ESP8266 timer for LED ticker
Let's now use MicroPython timers to toggle the state of an LED.
Required components
- ESP8266/ESP32 x1
- 5mm LED x1
- 330Ω resistance x1
- Test board
- Connecting wires/jumper wires
Circuit Connections
Connect the LED anode with GPIO14 of ESP8266 or ESP32. Connect a 330Ω resistor to the cathode of the LED and ground the other end of the resistor.
MicroPython Script
How it works
The LED is connected to the GPIO14 of the ESP8266 in such a way that it glows when the board supplies current to it, while it turns off when there is a low signal from the pin. The MicroPython script implements a timer interrupt every 1 second, where the LED state is toggled and the number of blinks is updated in the console.
Conclusion
From the above discussion, we can conclude that MicroPython ticking functions are useful in calculating timeouts and setting deadlines for executing parts of the script. Timer interrupts are useful when time-critical built-in tasks must be precisely scheduled within the script, regardless of whether they are required to run once or periodically. Timer interrupts are much more efficient at producing exact delays and intervals.