For those of you who might have not been in attendance at the first Maker's Faire in Vienna, we are bring the workshop to you. When working with limited resources, the concept of running an operating system is so far from reality that it is never considered. Since we work at the bare metal level, our programs run in a super loop that allows for only one task to be performed. With the MikroE Flip n click, the MCU runs at a mild 80 MHz, has 512 KB of flash and 100 KB of SRAM. Seems a shame to waste all the juicy space on LED blinking. To do more you need an operating system or some other type of application that can manage the MCU time, memory, and processing. Or.... we can run Python.
For the workshops we use a new product produced by Zerynth in Italy called Zerynth Studio. 100% supported by Flip n click and MikroE clickboards.
Threads
Since we have only 1 processor we can in reality only do one thing at a time. But in perception we can do many things at a time if we employ the use of threading. Several techniques are used in threading, the simplest form is round robin scheduling. This is the process of dividing up the time of the MCU processing into as many "slices" as the application has threads. If you have 2 things you want to do, in round robin you will divide the total available processing time into equal times of 50% / 50%. One process would run half the time, it processor would then switch to the other task for the remainder of the time.
We can then increase the intuitive nature of the thread by giving it a level of priority. This means that the tasks are no longer equal in their time sharing but a task with a higher priority could be ran before a lower one if the situation arose where they needed to run at the same time. It makes more sense in code, so let's do that.
Multi-Threaded Hello World - Lab 1
# MakerFaireVienna Hands on workshop import streams # Create a serial console streams.serial() sleep(1000) print("^^^^^^MakerFaireVienna^^^^^^") def system_setup(): pinMode(LED1, OUTPUT) pinMode(LED2, OUTPUT) pinMode(LED3, OUTPUT) pinMode(LED4, OUTPUT) # LAB 1 - Flashing LEDs def flash_led(led, delay): while True: pinToggle(led) sleep(delay) # Setup the system system_setup() # create the various threads using the same # function but passing different parameters thread(flash_led, LED1, 500) thread(flash_led, LED2, 1100) thread(flash_led, LED3, 2000) thread(flash_led, LED4, 100)
Some of those functions should be very familiar to you if you've ever used Arduino. (pinMode perhaps).
- We setup all 4 LEDs on the back of the Flip n click to be OUTPUTs.
- Create a function that is going to accept a LED to toggle every DELAY period. This function can be made to run in separate tasks, each unique from one another.
- Create 4 threads that will be independent applications and provide some arguments to pass to it.
This syntax for threads are thread(target=None, *args, prio=PRIO_NORMAL, size=512) which means we can create a task with just providing a function and everything else is optional.
Verify and Upload to the Flip n click and you should have 4 independent flashing LEDs. While this can be done in a super loop, what is really crucial to recognize is that these 4 LEDs are 4 seperate applications from one another and are sharing the MCU. But wait... there's more
Polling and Interrupts - Lab 2
Interrupts are those things that say to the MCU.... STOP what you are doing, I have something important to say. Interrupts cease the flow of normal code execution to execute a small program. Interrupts can be a rising edge, falling edge or both. In Python we register an interrupt with an easy onPinFall(pin, func, *args ) or onPinRise(pin, func, *args) call. We add a TouchKey click board to give an easy 4 capacitive buttons to our project. We are going to use the terminal to output a message and register an interrupt that when the A button is pressed on the board, we get a response.
# MakerFaireVienna Hands on workshop import streams # Create a serial console streams.serial() sleep(1000) print("^^^^^^MakerFaireVienna^^^^^^") def system_setup(): pinMode(LED1, OUTPUT) pinMode(LED2, OUTPUT) pinMode(LED3, OUTPUT) pinMode(LED4, OUTPUT) pinMode(A1, INPUT) onPinFall(A1, btn_press_b) # LAB 1 - Flashing LEDs def flash_led(led, delay): while True: pinToggle(led) sleep(delay) # LAB 2 - Polling and Interrupts def btn_press_b(): print("Button B Pressed") # Setup the system system_setup() # create the various threads using the same # function but passing different parameters thread(flash_led, LED1, 500) thread(flash_led, LED2, 1100) thread(flash_led, LED3, 2000) thread(flash_led, LED4, 100)
Now we have 4 separate applications running with the occasional interruption by a button press. All working independently. Ever wonder how we get the pin numbers? Press the icon by the i information at the top of the toolbar.
Kind of looks like MikroBUS sockets with pin numbers.
How about pin polling? What is polling you ask? Are you happy, Are you happy, Are you happy, Are you happy, Are you happy, Are you happy, Are you happy, Are you happy, ...
I just polled you. The same thing happens with our MCU, we can ask as fast as the CPU will go if a pin is low or high. Unfortunately, this is a very taxing loop and leaves no time for the MCU to process anything else but to wait for a pin to respond with a answer we are looking for. The good news is that since we have threads, we can put a polling question in it's own application separate from the others. Hopefully that thought sunk in and the light came on. Let's see that in action. Also, let's change the priority of our LED blinking to be a lower priority since that isn't the most important thing anymore and use Button A.
# MakerFaireVienna Hands on workshop import streams # Create a serial console streams.serial() sleep(1000) print("^^^^^^MakerFaireVienna^^^^^^") def system_setup(): global display pinMode(LED1, OUTPUT) pinMode(LED2, OUTPUT) pinMode(LED3, OUTPUT) pinMode(LED4, OUTPUT) pinMode(D24, INPUT) pinMode(A1, INPUT) onPinFall(A1, btn_press_b) # LAB 1 - Flashing LEDs def flash_led(led, delay): while True: pinToggle(led) sleep(delay) # LAB 2 - Polling and Interrupts def btn_press_a(): while True: sleep(100) if digitalRead(D24) == 1: print("Button A Pressed") def btn_press_b(): print("Button B Pressed") # Setup the system system_setup() # create the various threads using the same # function but passing different parameters thread(flash_led, LED1, 500, prio=PRIO_LOW) thread(flash_led, LED2, 1100, prio=PRIO_LOW) thread(flash_led, LED3, 2000, prio=PRIO_LOW) thread(flash_led, LED4, 100, prio=PRIO_LOW) thread(btn_press_a)
At present, we now have 5 tasks, all separate from each other, running happily on a single MCU. One button interrupts the flow and prints, but the other has the same behavior but the other is in a completely new thread running alongside the others and constantly polling the button. You want more?
*NOTE: the command sleep() is an important concept. You don't want to poll the button 20 Million times a second. Humans aren't that fast. So let's give the MCU a break by letting it sleep every once in awhile.
I2C Communications - Lab 3
/p>
We have 5 threads running at the same time, but you want more. How about I2C communications at the same time? This time let's at a Temp n Hum click to our project and read the current temperature and humidity. For this we are using the Temp&Hum click placed in MikroBUS slot C.
*NOTE: The libraries used in the following example will be published shortly under the mikroe community in Zerynth.
# MakerFaireVienna Hands on workshop import streams from community.mikroe.temp_n_hum import temp_n_hum as TH # Create a serial console streams.serial() sleep(1000) print("^^^^^^MakerFaireVienna^^^^^^") # Temp and humidity sensor temp_hum = TH.TempHumClick(I2C1) def system_setup(): pinMode(LED1, OUTPUT) pinMode(LED2, OUTPUT) pinMode(LED3, OUTPUT) pinMode(LED4, OUTPUT) pinMode(D24, INPUT) pinMode(A1, INPUT) onPinFall(A1, btn_press_b) # LAB 1 - Flashing LEDs def flash_led(led, delay): while True: pinToggle(led) sleep(delay) # LAB 2 - Polling and Interrupts def btn_press_a(): while True: sleep(100) if digitalRead(D24) == 1: print("Button A Pressed") def btn_press_b(): print("Button B Pressed") # LAB 3 - I2C Sensors def print_temp_humidity(): while True: tmp, hum = temp_hum.get_temp_humidity() print("Temp is:", tmp, "Humidity is:", hum) sleep(2000) # Setup the system system_setup() # create the various threads using the same # function but passing different parameters thread(flash_led, LED1, 500, prio=PRIO_LOW) thread(flash_led, LED2, 1100, prio=PRIO_LOW) thread(flash_led, LED3, 2000, prio=PRIO_LOW) thread(flash_led, LED4, 100, prio=PRIO_LOW) thread(print_temp_humidity, prio=PRIO_HIGH) thread(btn_press_a)
Result:
We have lowered the priority of the flashing leds to level LOW and added a new HIGH priority task, reading our temperature and humidity. Those values read from the sensor is displayed and printed every 2 seconds. Even though we have 6 separate application threads running you say.... give me more. Ok.
SPI Display - Lab4
We are really going to be turning up the heat on our little MCU by asking to to constantly update a 8x8 LED display while doing it's other work at the same time. The code is modified to speed up the display when a button is pressed and slowed down when the alternate button is pressed. It will also be constantly be updating the LED intensity level on the display while refreshing. Here we go:
# MakerFaireVienna Hands on workshop import streams import community.mikroe.temp_n_hum import temp_n_hum as TH import community.mikroe.eight_leds import eight_display as Display # Create a serial console streams.serial() sleep(1000) print("^^^^^^MakerFaireVienna^^^^^^") # Temp and humidity sensor temp_hum = TH.TempHumClick(I2C1) display = Display.LedDisplay(D17) display_speed = 100 def system_setup(): pinMode(LED1, OUTPUT) pinMode(LED2, OUTPUT) pinMode(LED3, OUTPUT) pinMode(LED4, OUTPUT) pinMode(D24, INPUT) pinMode(A1, INPUT) onPinFall(A1, btn_press_b) display.shutdown(0,False) # LAB 1 - Flashing LEDs def flash_led(led, delay): while True: pinToggle(led) sleep(delay) # LAB 2 - Polling and Interrupts def btn_press_a(): global display_speed while True: sleep(100) if digitalRead(D24) == 1: print("Button A Pressed") display_speed -= 100 if display_speed < 100: display_speed = 10 def btn_press_b(): global display_speed print("Button B Pressed") display_speed += 100 # LAB 3 - I2C Sensors def print_temp_humidity(): while True: tmp, hum = temp_hum.get_temp_humidity() print("Temp is:", tmp, "Humidity is:", hum) sleep(2000) # LAB 4 - Adding SPI Display def led_display(): intensity = 10 while True: for row in range(8): for col in range(8): display.set_led( 0, col, row, 1 ) sleep(display_speed) display.clear_display(0) display.set_intensity( 0, intensity ) intensity += 1 if intensity > 16: intensity = 0 # Setup the system system_setup() # create the various threads using the same # function but passing different parameters thread(flash_led, LED1, 500, prio=PRIO_LOW) thread(flash_led, LED2, 1100, prio=PRIO_LOW) thread(flash_led, LED3, 2000, prio=PRIO_LOW) thread(flash_led, LED4, 100, prio=PRIO_LOW) thread(print_temp_humidity, prio=PRIO_HIGH) thread(btn_press_a) thread(led_display)
Result:
We now have 4 threads running our LEDs, 1 thread waiting for button presses, 1 thread printing the temp and humidity, and finally 1 thread animated display. All running at the same time and sharing the resource of the MCU. 7 application threads running at the same time with the MCU showing no signs of slowdown....... impressed? If not, go ahead and try this on bare metal and let me know how it went.
When you press the buttons what is the result of the display speed?
If you would like a PDF of the lab, Lab Workbook.
References
Maker Faire Vienna www.mikroe.com 2016
Multithreading www.wikipedia.org 2016
Round-robin scheduling www.wikipedia.org 2016
Priority Scheduling www.techopedia.com 2016