Skip to content

Christmas Tree - CircuitPython version

The completed code for this project: code.py.

This is a modification of the MakeCode Maker Christmas Tree instructions but using CircuitPython rather than MakeCode Maker. The primary difference is that this version of the materials uses two additional pins to allow for control of the white star LED and a buzzer.

For this project I also used the more powerful Pimoroni Tiny 2350 microcontroller rather than the Tiny 2040 microcontroller used with the MakeCode Maker Christmas Tree, simply because I had lots of them readily available. Either microcontroller will work equally well and the instructions are identical; just make sure you install the correct variant of CircuitPython for the microcontroller you use.

It is recommended to use either Thonny or the online Viper IDE to do this project.

Christmas Tree

Project overview

The layout of the LEDs on the Christmas Tree is as follows:

Layer Colour, Pin Colour, Pin Colour, Pin Colour, Pin
Buzzer Black, A3
Star White, A2/GP28
Top row Red, GP2
Second row Green, GP1 Blue, GP4
Third row Yellow, A1/GP27 Red, GP3 Green, GP6
Bottom row Blue, GP0 Yellow, A0/GP26 Red, GP5 Green, GP7

Step 1 - Prepare the microcontroller with CircuitPython

This project is built using CircuitPython and the cptkip library. This project has been built using version 0.1.0 of cptkip.

The directory CircuitPython in the cptkip project contains the binaries for CircuitPython for the microcontrollers which have been tested as compatible with cptkip. Use the appropriate binary to install CircuitPython on your microcontroller.

Bundled with the CircuitPython binaries is a lib directory which contains the dependencies of the cptkip library. Copy the contents of that lib directory into the lib directory of the microcontroller.

Copy the cptkip directory from cptkip to the root directory of the microcontroller.

Copy the config.py file to the root directory of the microcontroller. This config.py file has been setup for a Pimoroni Tiny 2040 or Tiny 2350 microcontroller based on the wiring diagram for this project. If you have a different microcontroller or are using a different wiring arrangement, you will need to adjust the contents of the config.py file to match.

Copy the examples directory from cptkip to the root directory of the microcontroller.

Use Thonny or the online Viper IDE to test the examples. If the microcontroller has been setup correctly and the settings in config.py) are correct, the examples should run without error. Good examples to test are:

  • examples/b_config_load_values.py
  • examples/f_device_button.py
  • examples/f_device_buzzer.py
  • examples/f_device_led_blink.py
  • examples/f_device_play_melody.py

The cptkip library provides a range of classes that simplifies creating visual effects and interactions with LEDs, buzzers and buttons; allowing the user to concentrate of creating nice visual effects instead. This project will introduce a number of these effects, starting with blinking an LED. The LED that we will blink is the red LED in the top row which is connected to the board.GP2 pin.

Modify the code in the code.py file so it reads as follows and then execute it. The red LED should blink.

import board
from adafruit_led_animation.animation.blink import Blink

import cptkip.device.led as device
import cptkip.pin.pwm_pin as pin
import cptkip.task.basic_runner_async as runner
import cptkip.task.periodic_task_async as periodic_task

BRIGHTNESS = (128, 128, 128)

pinGP2 = pin.PwmPin(board.GP2)
ledGP2 = device.Led(pinGP2)
animationGP2 = Blink(ledGP2, speed=1/2, color=BRIGHTNESS)

# Step 3 Flicker code goes here.

# Step 4 Pulse code goes here.

# Step 5 Sparkle code goes here.

async def animate() -> None:
    animationGP2.animate()

animate_task = periodic_task.create(animate)

# Step 6 Chase code goes here.

# Step 7 Melody code goes here.

# Step 8 Button code goes here.

runner.run([animate_task])

How does it work?

In this code, we are using cptkip library to treat the GP2 pin as an LED. This is done in two steps:

  • Treating the pin as a PwmPin pin (Pulse Width Modulation or PWM). This allows the underlying library code to adjust the brightness.
  • Wrapping the pin in an Led class that allows us to turn the LED on and off.

The Led class is designed to allow the LED to be used with CircuitPython animations. These are mostly exposed through adafruit_led_animation and in this case the Blink animation is used. Animations generally have a speed parameter. In the case of Blink this parameter determines how quickly the animation will blink. A speed of 1/2 means the LED will blink on an off every half-second.

The animations are originally designed to work with NeoPixels which can change colour. All animations will have a colour parameter and this is used to control the colour and adjust the colour of the relevant strand of NeoPixels. NeoPixels have their colour specified as a (Red, Green, Blue) triplet. Because LEDs have a fixed colour, the color parameter instead represents the brightness of the LED. Each of the three values in the triplet can have a value from 0 (off) to 255 (maximum). In this case, we are using 128 for each 3 values and this translates to the LED brightness of approximately half.

For the animations to work, the animate() method needs to be called regularly (many times per second). This can easily be done in a loop when there only a small number of LEDs to animate. This gets more complicated however when your program has lots of devices, buzzers and buttons all working at the same time. The cptkip library provides a simple way to run multiple tasks at the same time for us using asynchronous (async) functions. In this example we create an animate_task that will call our animate() function repeatedly. Our animate() function will then call the animate() method on the Blink animation, making it blink.

Modify the Blink animation to make the red LED blink at different speeds and observe what happens. Good values to try are:

  • 1/16
  • 1/8
  • 2

Experiment: Make the LED brighter

Adjust the BRIGHTNESS variable and observe what happens. Good examples to try are:

  • (2, 2, 2)
  • (64, 64, 64)
  • (255, 255, 255)

Step 3 - Make an LED flicker

In this step we are going to make the green LED on the left of the second row flicker. This LED is connected to the microcontroller pin board.GP1. The animation that we will be using is one provided by the cptkip library and called Flicker. The code follows a similar pattern to that used to make the red LED blink.

Add the following code to your program in the location indicated by the comment.

from cptkip.animation.flicker import Flicker

pinGP1 = pin.PwmPin(board.GP1)
ledGP1 = device.Led(pinGP1)
animationGP1 = Flicker(ledGP1, speed=1/20, color=BRIGHTNESS)

To keep the code simpler, rather than create an additional task to animate the flicker, we will modify the existing animate() function to also animate the flickering green LED. Change your existing animate() function to call animationGP1.animate(). It should look like the code below.

async def animate() -> None:
    animationGP2.animate()
    animationGP1.animate()

Experiment: Change the flicker speed

Modify the Flicker animation to make the green LED flicker at different speeds and observe what happens. Good values to try are:

  • 1
  • 1/2
  • 1/10
  • 1/50
  • 1/100

Extension: Make another LED flicker

Using what you have learnt about Flicker, modify your code to make the blue LED on the second row flicker. The blue LED is on pin board.GP4.

Hint 1: animationGP3 = Flicker(ledGP4, speed=1 / 40, color=BRIGHTNESS)

Hint 2: Remember to add the call to animationGP4.animate() in the animate() function.

There are an additional two parameters that can be specified to Flicker. Each takes a value of between 0 and 255. The total of the two values should not exceed 255. These two parameters are base (default value 150) and flame (default value 105). The flicker effect is created by adding a randomly generated number between 0 and flame to base. Experiment with different values for these and observe what happens. Good examples to try are:

  • 0 and 255
  • 50 and 205
  • 100 and 155
  • 150 and 105
  • 200 and 55
  • 250 and 5

If you are stuck and don't know how to do this, scroll to the end of the page for the solution.

Step 4 - Make an LED pulse

In this step we are going to make the yellow LED on the left of the third row pulse. This LED is connected to the microcontroller pin board.GP27. The animation that we will be using is one provided by the Adafruit animation library for CircuitPython and is called Pulse. The code follows a similar pattern to that used to make the red LED blink and green LED flicker.

Add the following code to your program in the location indicated by the comment.

from adafruit_led_animation.animation.pulse import Pulse

pinGP27 = pin.PwmPin(board.GP27)
ledGP27 = device.Led(pinGP27)
animationGP27 = Pulse(ledGP27, speed=1/100, period=2, color=BRIGHTNESS)

As before, we are going to keep things simple and modify your existing animate() function to call animationGP27.animate(). Your animate() function should look like the code below.

async def animate() -> None:
    animationGP2.animate()
    animationGP1.animate()
    animationGP4.animate()
    animationGP27.animate()

Experiment: Change the pulse speed

Modify the Pulse animation to make the yellow LED pulse at different speeds and observe what happens. Good values to try are:

  • 1/4
  • 1/10
  • 1/20
  • 2

Experiment: Change the pulse duration

Modify the Pulse animation to change the length of time (the period parameter) the yellow LED takes to pulse and observe what happens. Good values to try are:

  • 1/4
  • 1/2
  • 1
  • 5

Extension: Make the red and green LEDs pulse

Use the knowledge that you have gained from this step to make the other two LEDs in the third row pulse. The red LED is on pin board.GP3 and the green pin is on board.GP6.

If you are stuck and don't know how to do this, scroll to the end of the page for the solution.

Step 5 - Make the star sparkle

Whilst there is a Sparkle animation available in CircuitPython, it can't be used with an Led instance because Sparkle requires at least two elements to function and Led only supports a single LED. We can however do a good approximation of a sparkle effect by turning the LED on and off quickly. We can do this without the need of PWM and can therefore simply create an OutputPin which can be turned on and off by changing it's value property. The star is on pin board.GP28.

Add the following code to your program in the location indicated by the comment.

import cptkip.pin.output_pin as outputpin
ledGP28 = outputpin.OutputPin(board.GP28)

As before, we are going to keep things simple and modify your existing animate() function to switch the LED on and off quickly. Your animate() function should look like the code below.

async def animate() -> None:
    animationGP2.animate()
    animationGP1.animate()
    animationGP4.animate()
    animationGP27.animate()
    animationGP3.animate()
    animationGP6.animate()
    ledGP28.value = not ledGP28.value

Step 6 - Make the bottom row chase

Using the same technique for controlling an LED using OutputPin, this step will animate the bottom row of four LEDs into a chase pattern. The LEDs are connected to the following pins: Blue: board.GP0, Yellow: board.GP26, Red: board.GP5 and Green: board.GP7. To make the chase effect work, we will be tracking which LED is currently lit and then moving it to the right, one LED at a time. To make the chase effect work the way we want, we don't want to execute this logic as fast as possible as we have done with the other animate methods we've used. We want the logic to execute at a pre-determined rate to give us the speed of chase we desire. To achieve this we will create a new task with the logic and get the cptkip library to execute it at our desired rate.

Add the following code to your program in the location indicated by the comment.

ledGP0 = outputpin.OutputPin(board.GP0)
ledGP26 = outputpin.OutputPin(board.GP26)
ledGP5 = outputpin.OutputPin(board.GP5)
ledGP7 = outputpin.OutputPin(board.GP7)

led = 0

async def chase() -> None:
    global led
    ledGP0.off()
    ledGP26.off()
    ledGP5.off()
    ledGP7.off()
    if led == 0:
        ledGP0.on()
        led = 1
    elif led == 1:
        ledGP26.on()
        led = 2
    elif led == 2:
        ledGP5.on()
        led = 3
    else:
        ledGP7.on()
        led = 0

chase_task = periodic_task.create(chase, frequency=3)

If you run your program now, the LEDs wont chase. The reason for this is that even though we have created the task, we have not told the cptkip runner about it. To do so, modify the runner statement at the end of your program to also execute the chase_task. It should now look like the code below.

runner.run([animate_task, chase_task])

Extension: Change the speed of the chase

The speed of the chase effect is controlled by the frequency (rate) at which the logic in the chase function is called. This frequency is controlled by the value of the frequency parameter specified when creating the chase_task task. Experiment with different values for these and observe what happens. Good examples to try are:

  • 1/2
  • 1
  • 16
  • 64

Experiment: Reverse the chase

Currently, the direction of the chase is from left to right. Can you make it chase from right to left instead?

Experiment: Bounce the chase

Currently, the chase pattern goes from one side to the other and then goes back to the start to chase again. Can you change the code so it instead reverses direction? That is, the chase goes from left to right, then when it reaches the rightmost LED it reverses direction and goes from right to left.

Step 7 - Playing sounds with the buzzer

What better way to jazz up your Christmas Tree than to have it play music! Luckily, the cptkip library makes this really easy by using the Melody class to generate tones which we can then play through a buzzer.

So how are melodies specified? A song is list of notes that specify both the note and duration. Each note can optionally also specify the Octave. If no octave is specified, then the octave will be the last octave set by a previous note or 4 if it is the first note in the melody. The duration is the number of beats the note should last for. An example of a simple C scale is:

scale = [
    "C4:1", "D:1", "E:1", "F:1", "G:1", "A:1", "B:1", "C5:1",
    "B4:1", "A:1", "G:1", "F:1", "E:1", "D:1", "C:1"]

Sharps and Flats can be specified using #, S, F or B respectively. For example:

song = ["C#3:4", "FS7:2", "Eb3:1", "AF1:1"]

As with the previous step, we will create a new task to play the Melody. Add the following code to your program in the location indicated by the comment.

import cptkip.pin.buzzer_pin as buzzerpin
import cptkip.device.melody as melody

buzzer = buzzerpin.BuzzerPin(board.GP29)
buzzer.volume = 0.1

jingle_bells = [
    "E4:2", "E:2", "E:4", "E:2", "E:2", "E:4",
    "E:2", "G:2", "C:2", "D:2", "E:8",
    "F:2", "F:2", "F:2", "F:2", "F:2", "E:2", "E:2", "E:1", "E:1",
    "E:2", "D:2", "D:2", "E:2", "D:4", "G:2", "R:2",
    "E:2", "E:2", "E:4", "E:2", "E:2", "E:4",
    "E:2", "G:2", "C:2", "D:2", "E:8",
    "F:2", "F:2", "F:2", "F:2", "F:2", "E:2", "E:2", "E:1", "E:1",
    "G:2", "G:2", "F:2", "D:2", "C:8",
    "R:8"]

song = melody.Melody(buzzer, melody.decode_melody(jingle_bells), 480)

async def play_music() -> None:
    song.update()

music_task = periodic_task.create(play_music)

As in the previous step modify the runner statement at the end of your program to also execute the music_task. It should now look like the code below.

runner.run([animate_task, chase_task, music_task])

Experiment: Adjust the music volume

The volume of the music is controlled by the volume specified for the buzzer. The value is currently specified as 0.1. The value can be anything from 0 (off) to 1 (maximum). Experiment with different values for these and observe what happens. Good examples to try are:

  • 0
  • 0.5
  • 1.0

Experiment: Adjust the music tempo

The temp (speed) the music is played is controlled by the tempo specified when creating the Melody. The value is current specified as 480. Experiment with different values and observe what happens. Good examples to try are:

  • 80
  • 240
  • 1480

Experiment: Change the song

There is no reason to stick with Jingle Bells. Experiment with changing the song. Either create your own or use one of the following pre-arranged songs.

Song: Rudolph the red nosed reindeer

rudolph = [
    "G4:1", "A:2", "G:1", "E:2", "C5:2", "A4:2", "G:6",
    "G:1", "A:1", "G:1", "A:1", "G:2", "C5:2", "B4:8",
    "F:1", "G:2", "F:1", "D:2", "B:2", "A:2", "G:6",
    "G:1", "A:1", "G:1", "A:1", "G:2", "A:2", "E:8",
    "G:1", "A:2", "G:1", "E:2", "C5:2", "A4:2", "G:6",
    "G:1", "A:1", "G:1", "A:1", "G:2", "C5:2", "B4:8",

    "F:1", "G:2", "F:1", "D:2", "B:2", "A:2", "G:6",
    "G:1", "A:1", "G:1", "A:1", "G:2", "D5:2", "C:8",
    "A4:2", "A:2", "C5:2", "A4:2", "G:2", "E:2", "G:4",
    "F:2", "A:2", "G:2", "F:2", "E:8",
    "D:2", "E:2", "G:2", "A:2", "B:2", "B:2", "B:4",
    "D5:2", "C:2", "B4:2", "A:2", "G:2", "F:2", "D:4",

    "G:1", "A:2", "G:1", "E:2", "C5:2", "A4:2", "G:6",
    "G:1", "A:1", "G:1", "A:1", "G:2", "C5:2", "B4:8",
    "F:1", "G:2", "F:1", "D:2", "B:2", "A:2", "G:6",
    "G:1", "A:1", "G:1", "A:1", "G:2", "D5:2", "C:8",
    "R:8"]

Song: When Santa got stuck up the chimney

santa = [
    "G4:1",
    "C:1", "C:1", "D:1", "E:1", "E:1", "F:1", "G:3", "C5:3",
    "A4:2", "G:1", "F:2", "A:1", "G:3", "R:2", "G:1",

    "C5:2", "C:1", "B4:2", "B:1", "A:1", "A:1", "A:1", "G:2", "G:1",
    "E:2", "E:1", "D:2", "C:1", "G:3", "R:2", "G:1",

    "C5:2", "C:1", "B4:2", "B:1", "A:1", "A:1", "A:1", "G:2", "G:1",
    "E:2", "E:1", "G:1", "F:1", "E:1", "D:3", "R:2", "G:1",

    "C:1", "C:1", "D:1", "E:1", "E:1", "F:1", "G:3", "C5:2", "A4:1",
    "G:2", "C5:1", "B4:2", "D5:1", "C:7",

    "R:8"]    

Song: We wish you a Merry Christmas

merry_christmas = [
    "D4:2",
    "G:2", "G:1", "A:1", "G:1", "F:1", "E:2", "C:2", "E:2",
    "A:2", "A:1", "B:1", "A:1", "G:1", "F:2", "D:2", "F:2",
    "B:2", "B:1", "C5:1", "B4:1", "A:1", "G:2", "E:2", "D:1", "D:1",
    "E:2", "A:2", "F:2", "G:4", "D:2",
    "G:2", "G:2", "G:2", "F:4", "F:2",
    "G:2", "F:2", "E:2", "D:4", "A:2",
    "B:2", "A:1", "A:1", "G:1", "G:1", "D5:2", "D4:2", "D:1", "D:1",
    "E:2", "A:2", "F:2", "G:4", "R:2",
    "R:4"]

Step 8 - Button presses

The Pimoroni Tiny microcontrollers have two buttons on them. One is called Reset and this reboots the microcontroller. The other button is called Boot and this can be used by your own CircuitPython code. In this step, we are going to add a click handler to the Boot button which will pause and resume the song.

Add the following code to your program in the location indicated by the comment.

def click_handler() -> None:
    if song.paused:
        song.resume()
    else:
        song.pause()

import cptkip.pin.input_pin as inputpin
from cptkip.device.button import Button

input_pin = inputpin.InputPin(board.BUTTON, False)
button = Button(input_pin, click=click_handler)

To make the button "work" and respond to button presses, we need to call the button.update() method from a task. As this button is to pause and resume the currently playing song, we will add the call to the existing play_music() function. Modify your play_music() function so that it matches that below.

async def play_music() -> None:
    song.update()
    button.update()

Extension: Cycling through multiple songs

The cptkip library provides a way to chain songs together in a sequence and it will play each of them in turn. Using the 4 songs from step 7, we can chain them together. There are several steps to achieve this.

Step 8a: Replace song with melody_sequence

Remove this code that creates the song.

song = melody.Melody(buzzer, melody.decode_melody(jingle_bells), 480)

Add in this code that creates the melody_sequence. This should be placed in the same place that song was removed from.

melody_sequence = melody.MelodySequence(
    melody.Melody(buzzer, melody.decode_melody(santa), 480),
    melody.Melody(buzzer, melody.decode_melody(rudolph), 480),
    melody.Melody(buzzer, melody.decode_melody(merry_christmas), 480),
    melody.Melody(buzzer, melody.decode_melody(jingle_bells), 480))

Step 8b: Replace song.update() with melody_sequence.update()

The play_music() function currently calls song.update() as below.

async def play_music() -> None:
    song.update()
    button.update()

Replace the call to song.update() with melody_sequence.update() as in the code below.

async def play_music() -> None:
    melody_sequence.update()
    button.update()

Step 8c: Replace click_handler()

The existing click_handler() interacts with song which has now been deleted. Replace the click_handler() code with a new click_handler() like the one below.

def click_handler() -> None:
    if melody_sequence.paused:
        melody_sequence.resume()
    else:
        melody_sequence.pause()

Now you program should cycle through the different songs.

Making the Christmas Trees

Making the Christmas Trees is very similar to the MakeCode Maker version of these materials except the White LED is not connected to 3V3 but A2/GP28 pin instead. There is also a small buzzer which is connected to A2/ GP29. The reason those pins are not used in the MakeCode Maker variant of the Christmas Tree is because they are not accessible from that framework. For the CircuitPython to work, you will need the following config.py as well as cptkip (this was tested with version 0.1.0).

This project will work equally well with either of the following microcontrollers:

An image of the finished circuit board prior to assembly.

controls

Solution: How to make the blue LED flicker

Here is the example code to help with the experiment in step 3.

pinGP4 = pin.PwmPin(board.GP4)
ledGP4 = device.Led(pinGP4)
animationGP4 = Flicker(ledGP4, speed=1 / 40, color=BRIGHTNESS, base=150, flame=105)

Remember to add the call to animationGP4.animate() in the animate() function.

async def animate() -> None:
    animationGP2.animate()
    animationGP1.animate()
    animationGP4.animate()

Solution: How to make the red and green LEDs pulse

Here is the example code to help with the experiment in step 4.

pinGP3 = pin.PwmPin(board.GP3)
ledGP3 = device.Led(pinGP3)
animationGP3 = Pulse(ledGP3, speed=1/100, period=1/4, color=BRIGHTNESS)

pinGP6 = pin.PwmPin(board.GP6)
ledGP6 = device.Led(pinGP6)
animationGP6 = Pulse(ledGP6, speed=1/100, period=5, color=BRIGHTNESS)

Remember to add the calls to animationGP3.animate() and animationGP6.animate()in the animate() function.

async def animate() -> None:
    animationGP2.animate()
    animationGP1.animate()
    animationGP4.animate()
    animationGP27.animate()
    animationGP3.animate()
    animationGP6.animate()