C/C++ coding tutorial - bitty xmas bauble

micro:bit code

As with all our apps you can simply download and install a hex file which we've prepared for you or you can code it yourself, following the Bitty Software coding tutorial below. Naturally, we recommend you code it yourself!


before you start

To program your micro:bit using C/C++ you need the Yotta tools and the micro:bit 'DAL' source code on a PC or Mac. If you haven't yet set this environment up, read the information on our tools page and make sure you can build and install the Hello World sample code onto your micro:bit before going any further. If you've accomplished this then you have a working C/C++ environment for micro:bit development and can move on.

You should also install the bitty xmas bauble application on your phone so you can use it for testing whilst you develop the micro:bit code. Check the bitty xmas bauble page for details of where to find the app for your type of phone or tablet.


About PWM

The micro:bit code for Bitty Xmas Bauble uses Pulse Width Modulation (PWM) to create audio beeps at a given frequency and duration from a speaker connected to a micro:bit pin. PWM is a technique for making a digital signal behave like an analog signal, by switching it on and off rapidly. There are lots of web sites which explain PWM in more detail. Try this one, for example.


Audio Connections

For your micro:bit to be able to make sound, you'll need to connect something to pin 0. A piezo buzzer is probably the simplest option but you could just as well connect a set of headphones.


step 1 - set up initial source code files

Download our project starter source code file to your microbit-samples\source folder. Delete any ".cpp" files you already had there. Rename the file to Bauble.cpp so it has a name that reflects what it does.

During the course of the tutorial, you'll create a micro:bit hex file which does not require Bluetooth pairing. At the end of the tutorial, when everything is working, we'll do one final build that does need your phone or tablet to pair with your micro:bit so that nobody else can connect to your micro:bit and stop you from controlling your Xmas bauble!

Download this config.json file and save it in your microbit-samples folder. Rename it to config.json.

config.json includes various build properties that affects certain aspects of your micro:bit hex file once you have created it:

The property "open" which has a value of "1" means that your micro:bit will not need to be paired with your phone to be usable. This makes life easier during development but before you finish your project, remember to review this and if you decide you do want security then change the property to "0" and build again. You'll now need to pair your micro:bit with your phone.

Open Bauble.cpp in a text editor and let's get to work.

step 2 - variables and data definitions

There are lots of variables and data definitions needed in this application. Rather than type them in in a laborious (and boring!) manner, we'll start by copying and pasting almost everything we're going to need in one go. The purpose of each variable will become apparent as we complete the code. Copy and pasts the following definitions to the top of your source code, underneath the #include statement:

    MicroBit uBit;
    MicroBitImage pattern(5,5);
    
    // State report is used to tell the client the current light and audio selections
    const int STATE_REPORT = 9010;
    // State control allows the client to ask what the current light and audio selections are so it can get its UI in sync with the micro:bit state
    const int STATE_QUERY = 9009;
    // Behaviour events control the display and audio behaviour of the microbit and have a value whose first octet is a light sequence selector and second octet is an audio sequence selector
    const int BEHAVIOUR_CONTROL = 9007;
    const int MAX_LIGHT_EVENT=6;
    const int MAX_AUDIO_EVENT=3;
    int selected_light_seq=0;
    int selected_audio_seq=0;
    int playing_audio_seq=0;
    int display_on=0;
    int audio_on=0;
    int num_rnd_pixels = 4;
    
    const int MERRY_XMAS           = 1;
    const int FLASH                = 2;
    const int RANDOM_PIXELS_FLASH  = 3;
    const int RIPPLE               = 4;
    const int SPIRAL               = 5;
    const int CHECKER              = 6;
    const int STOP                 = 254;
    
    const int QUERY_STATE          = 1;
    
    // The period of the PWM determines the tone that will be played; the duty cycle changes the volume.
    const int JINGLE_BELLS         = 1;
    const int SILENT_NIGHT         = 2;
    const int DING_DONG            = 3;
    
    int bluetooth_connected = 0;
    
    int ripples[4][25] = {
        {0,0,0,0,0,  0,0,0,0,0,  0,0,0,0,0,  0,0,0,0,0,  0,0,0,0,0}, 
        {0,0,0,0,0,  0,0,0,0,0,  0,0,1,0,0,  0,0,0,0,0,  0,0,0,0,0}, 
        {0,0,0,0,0,  0,1,1,1,0,  0,1,0,1,0,  0,1,1,1,0,  0,0,0,0,0},
        {1,1,1,1,1,  1,0,0,0,1,  1,0,0,0,1,  1,0,0,0,1,  1,1,1,1,1} 
    };
    
    int spiral[25] = {
        12, 13, 8, 7, 6,   11, 16, 17, 18, 19,   14, 9, 4, 3, 2,   1, 0, 5, 10, 15,   20, 21, 22, 23, 24
    };
    
    int sleep_time=100;
    int flash_state=1;
    int inx=0;
    int delta = 1;
    int pixel_value=255;
    
    // BPM tempo
    int tempo =  120;
    
    // frequencies
    // unused
    //     C = 262, CSharp = 277, D = 294, Eb = 311, E = 330, F = 349, FSharp = 370, G = 392, GSharp = 415, A = 440, Bb = 466, B = 494, 
    //    C3 = 131, CSharp3 = 139, D3 = 147, Eb3 = 156, E3 = 165, F3 = 175, FSharp3 = 185, G3 = 196, GSharp3 = 208, A3 = 220, Bb3 = 233, B3 = 247,
    
    enum Note {
        C4 = 262, CSharp4 = 277, D4 = 294, Eb4 = 311, E4 = 330, F4 = 349, FSharp4 = 370, G4 = 392, GSharp4 = 415, A4 = 440, Bb4 = 466, B4 = 494,
        C5 = 523, CSharp5 = 555, D5 = 587, Eb5 = 622, E5 = 659, F5 = 698, FSharp5 = 740, G5 = 784, GSharp5 = 831, A5 = 880, Bb5 = 932, B5 = 989,
    };
    
    // rest
    #define RE        0
    // demisemiquaver (1/32)
    //#define N64       1
    // demisemiquaver (1/32)
    #define N32       2
    // demisemiquaver - dotted
    #define N32D      3
    // semiquaver (1/16)
    #define N16       4
    // semiquaver - dotted
    #define N16D      6
    // quaver     (1/8 )
    #define N08       8
    // quaver - dotted
    #define N08D     12
    // crotchet   (1/4)
    #define N04      16
    // crotchet - dotted
    #define N04D     24
    // minim      (1/2)
    #define N02      32
    // minim - dotted
    #define N02D     48
    // semibreve  (1/1)
    #define N01      64
    // semibreve  (1/1)
    #define N01D     96
    // end of tune
    #define TUNE_END -1
    
    int N64_duration_ms;
    
    const int jingle_bells[][2] = {
        {E5,N16}, {RE,N16}, {E5,N16}, {RE,N16}, {E5,N16}, {RE,N16}, {RE,N16}, {RE,N16},  
        {E5,N16}, {RE,N16}, {E5,N16}, {RE,N16}, {E5,N16}, {RE,N16}, {RE,N16}, {RE,N16},
        {E5,N16}, {RE,N16}, {G5,N16}, {RE,N16}, {C5,N08}, {RE,N16}, {D5,N32}, {RE,N32}, {E5,N08}, {RE,N16}, {RE,N16}, {RE,N16}, {RE,N16}, {RE,N16}, {RE,N16},
        {F5,N16}, {RE,N16}, {F5,N16}, {RE,N16}, {F5,N16}, {RE,N16}, {F5,N32}, {RE,N32}, {F5,N32}, {RE,N32}, {F5,N16}, {RE,N16}, 
        {E5,N16}, {RE,N16}, {E5,N16}, {RE,N16}, {E5,N32}, {RE,N32}, {E5,N32}, {RE,N32}, {E5,N32}, {RE,N32}, {RE,N16}, 
        {D5,N16}, {RE,N16}, {D5,N16}, {RE,N16}, {E5,N16}, {RE,N16}, {D5,N16}, {RE,N16}, {RE,N16}, {RE,N16}, {G5,N16}, {RE,N04}, 
        {TUNE_END, 00} 
    };
    
    const int silent_night[][2] = {
        {G4,N04D},{A4,N08}, {G4,N04},   
        {E4,N02D},   
        {G4,N04D},{A4,N08}, {G4,N04},   
        {E4,N02D}, 
        {D5,N02}, {D5,N04},
        {B4,N02D},
        {C5,N02}, {C5,N04},{G4,N02D},
        {A4,N02}, {A4,N04},
        {C5,N04D},{B4,N08},{A4,N04},
        {G4,N04D},{A4,N08},{G4,N04},{E4,N02D},
        {A4,N02}, {A4,N04},
        {C5,N04D},{B4,N08},{A4,N04},
        {G4,N04D},{A4,N08},{G4,N04},
        {E4,N02D},
        {D5,N02}, {D5,N04},
        {F5,N04D},{D5,N08},{B4,N04},
        {C5,N02D},{E5,N02D},
        {C5,N04}, {G4,N04},{E4,N04},
        {G4,N04D},{F4,N08},{D4,N04},
        {C4,N02D},{C4,N02D},
        {TUNE_END, 00} 
    };
    
    const int ding_dong[][2] = {
        {F4,N04}, {F4,N04},{G4,N08},{F4,N08},{E4,N08},{D4,N08},{C4,N02D},{C4,N04},
        {D4,N04}, {F4,N04},{F4,N04},{E4,N04},{F4,N02},{F4,N02},
        {F4,N04},{F4,N04},{G4,N08},{F4,N08},{E4,N08},{D4,N08},{C4,N02D},{C4,N04},
        {D4,N04}, {F4,N04},{F4,N04},{E4,N04},{F4,N02},{F4,N02},
    
        {C5,N04D},{Bb4,N08},{A4,N08},{Bb4,N08},{C5,N08},{A4,N08},
        {Bb4,N04D},{A4,N08},{G4,N08},{A4,N08},{Bb4,N08},{G4,N08},
        {A4,N04D},{G4,N08},{F4,N08},{G4,N08},{A4,N08},{F4,N08},
        {G4,N04D},{F4,N08},{E4,N08},{F4,N08},{G4,N08},{E4,N08},
        {F4,N04D},{E4,N08},{D4,N08},{E4,N08},{F4,N08},{D4,N08},
    
        {E4,N04D},{D4,N08},{C4,N04},{C4,N04},
        {D4,N04}, {F4,N04},{F4,N04},{E4,N04},
        {F4,N02},{F4,N04},{RE,N04},
        {TUNE_END, 00} 
    };

As you can see, amongst the data definitions are a number of arrays containing data which defines xmas tunes like ding_dong and visual patterns like ripples.


step 3 - build and test

If you haven't already done so, find and launch the "Run Yotta" shortcut that should have been created for you when you installed Yotta. This will result in a window you can type commands into appearing. Use 'cd' to navigate to your microbit-samples folder.

Your micro:bit does not process C/C++ code directly. We have to translate it into a binary format which the micro:bit understands using a process called "building". Before we go any further, let's build and install your application now to make sure everything is working as expected. Follow the steps for building and installing your code and then return here and continue.

Tip: It's a good idea to progress by making small changes, building and testing at each step rather than typing huge amounts of code and then trying to build and test all in one go. It's much easier to solve problems when we're only making small changes each time.


step 4 - initialisation

There are various things we need to do when our code first executes. Add the following function outside of your main function

    void calculateNoteDuration() {
        N64_duration_ms = ( 60.0 / tempo / 16.0 * 1000.0);    
    }    

and then update the main function so that it looks like this:

    int main()
    {
        uBit.init();
        uBit.seedRandom();
        calculateNoteDuration();
        uBit.messageBus.listen(MICROBIT_ID_BLE, MICROBIT_BLE_EVT_CONNECTED, onConnected);
        uBit.messageBus.listen(MICROBIT_ID_BLE, MICROBIT_BLE_EVT_DISCONNECTED, onDisconnected);
        uBit.messageBus.listen(MICROBIT_ID_BUTTON_A, MICROBIT_BUTTON_EVT_CLICK, onButton);
        uBit.messageBus.listen(MICROBIT_ID_BUTTON_B, MICROBIT_BUTTON_EVT_CLICK, onButton);
        uBit.messageBus.listen(BEHAVIOUR_CONTROL, 0, onControllerEvent); 
        uBit.messageBus.listen(STATE_QUERY, 0, onControllerEvent); 
    
        selected_light_seq=0;
        selected_audio_seq=0;
        display_on=0;
        audio_on=0;
    
        // If main exits, there may still be other fibers running or registered event handlers etc.
        // Simply release this fiber, which will mean we enter the scheduler. Worse case, we then
        // sit in the idle task forever, in a power efficient sleep.
        release_fiber();
    }

step 5 - event handlers

Our initialisation code sets up a number of event handlers. Let's add the required functions but for now, leave them empty. Add the following code outside of your main function:

    void onControllerEvent(MicroBitEvent e)
    {
    }    

    void onButton(MicroBitEvent e)
    {
    }

    void onConnected(MicroBitEvent)
    {
    }
    
    void onDisconnected(MicroBitEvent)
    {
    }

Run yt build to check your code compiles OK.


step 6 - Bluetooth connection handlers

Complete the onConnected and onDisconnected functions which are called whenever a Bluetooth connection to the micro:bit is established or lost:

    void onConnected(MicroBitEvent)
    {
        printf("onConnected\n");
        uBit.display.stopAnimation();
        uBit.display.print("C");
        bluetooth_connected = 1;
    }
    
    void onDisconnected(MicroBitEvent)
    {
        printf("onDisconnected\n");
        bluetooth_connected = 0;
    }

Most importantly, these functions set a flag called bluetooth_connected which is used to track whether or not there's a connection from another device like a smartphone. Connecting will also stop any animation which is already running on the micro:bit LED display. We haven't written the stopAnimation() function yes, of course.


step 7 - functions for playing tones

Add the following functions, which provide the ability to play individual tones and whole tunes, directed by the data arrays we created earlier. Make sure the calculateNoteDuration function is above these new functions otherwise you'll get compilation errors.

    void playTone(int frequency, int duration_ms) {
        if (frequency <= 0) {
            uBit.io.pin[0].setAnalogValue(0);
        } else {
            uBit.io.pin[0].setAnalogValue(512);
            uBit.io.pin[0].setAnalogPeriodUs(1000000/frequency);
        }
        if (duration_ms > 0) {
            fiber_sleep(duration_ms);
            uBit.io.pin[0].setAnalogValue(0);
            wait_ms(5);
        }
    }
    
    void rest(int duration_ms) {
        playTone(0, duration_ms);
    }
    
    void audioOff() {
        uBit.io.pin[0].setAnalogValue(0);
    }
    
    void playTune(const int tune_data[][2]) {
        printf("play_tune\n");
        calculateNoteDuration();
        int i=0;
        while (tune_data[i][0] != TUNE_END) {
            if (selected_audio_seq != playing_audio_seq) {
                return;
            } 
            int duration = N64_duration_ms * tune_data[i][1];
            if (tune_data[i][0] != RE) {
                playTone(tune_data[i][0] , duration);
            } else {
                rest(duration);
            }
            i++;
            if (selected_audio_seq != playing_audio_seq) {
                return;
            } 
        }
    }

Now would be a good time to run 'yt build' to make sure your code builds OK.


step 8 - functions for LED animations

Add the following functions, which implement the various LED patterns.


    void lightMerryXmas() {
        uBit.display.scroll("Merry Xmas");
    }
    
    void lightFlash() {
        int i;
        for (i=0;i<25;i++) {
            pattern.setPixelValue(i % 5,floor(i / 5), flash_state * 255);
        }
        uBit.display.image.paste(pattern);
        flash_state = !flash_state;
    }
    
    void lightRandomPixelsFlash() {
        if (flash_state == 1) {
            pattern.clear();
            int i=0;
            for (i=0;i<num_rnd_pixels;i++) {
                uint8_t pixel = uBit.random(25);
                pattern.setPixelValue(pixel % 5,floor(pixel / 5), 255);
            }
            flash_state = 0;
        } else {
            pattern.clear();
            flash_state = 1;
        }
        uBit.display.image.paste(pattern);
    }
    
    void lightRipple() {
        int i=0;
        for (i=0;i<25;i++) {
            pattern.setPixelValue(i % 5,floor(i / 5), ripples[inx][i]);
        }
        uBit.display.image.paste(pattern);
        inx = inx + delta;
        if (inx == 3 || inx == 0) {
            delta = delta * -1;
        }
    }
    
    void lightSpiral() {
        int x = spiral[inx] % 5;
        int y = floor(spiral[inx] / 5);
        pattern.setPixelValue(x,y, pixel_value);
        uBit.display.image.paste(pattern);
        inx = inx + delta;
        if (inx == 25 || inx == -1) {
            delta = delta * -1;
            inx = inx + delta;
            if (pixel_value == 255) {
                pixel_value = 0;
            } else {
                pixel_value = 255;
            }
        }
    }
    
    void lightAlternatingCheckerPattern() {
    // every other pixel on to begin with. Then toggle state of each pixel on | off periodically. 
        int i;
        for (i=0;i<25;i++) {
            int x = spiral[i] % 5;
            int y = floor(spiral[i] / 5);
            pattern.setPixelValue(x,y, flash_state * 255);
            flash_state = !flash_state;
        }
        uBit.display.image.paste(pattern);
    }
    
    void stopLight() {
        if (display_on == 1) {
            uBit.display.stopAnimation();
        }
    }
            

Run 'yt build'


step 9 - Add the audioLoop function

Add this function. It will run continuously as long as the audio_on variable has a value of 1 and play the selected tune.

    void audioLoop() {
        printf("audioLoop\n\n");
        while (audio_on == 1) {
           switch (selected_audio_seq) {
             case STOP:
               printf("audioLoop.STOP\n");
               audio_on = 0;
               break;
             case JINGLE_BELLS:
               printf("audioLoop.JINGLE_BELLS\n");
               audio_on = 1;
               tempo = 120;
               playing_audio_seq = JINGLE_BELLS;
               playTune(jingle_bells);
               break;
             case SILENT_NIGHT:
               printf("audioLoop.SILENT_NIGHT\n");
               audio_on = 1;
               tempo = 120;
               playing_audio_seq = SILENT_NIGHT;
               playTune(silent_night);
               break;
             case DING_DONG:
               printf("audioLoop.DING_DONG\n");
               audio_on = 1;
               tempo = 160;
               playing_audio_seq = DING_DONG;
               playTune(ding_dong);
               break;
           }
        }
        printf("audioLoop exiting\n");
    }    

Run 'yt build'


step 10 - Add the animationLoop function

Add the following function. As long as the display_on variable has a value of 1, it will loop and execture the selected LED animation on the micro:bit display:

    void animationLoop() {
        // printf("animationLoop %d\n\n",selected_light_seq);
        // exit if no animation requested to save power
        while(display_on == 1) {
           printf("animationLoop continuing %d\n\n",selected_light_seq);
           switch (selected_light_seq) {
             case STOP:
               stopLight();
               display_on = 0;
               break;
            case MERRY_XMAS:
               display_on = 1;
               lightMerryXmas();
               break;
            case FLASH:
               display_on = 1;
               lightFlash();
               break;
            case RANDOM_PIXELS_FLASH:
               display_on = 1;
               lightRandomPixelsFlash();
               break;
            case RIPPLE:
               display_on = 1;
               lightRipple();
               break;
            case SPIRAL:
               display_on = 1;
               lightSpiral();
               break;
            case CHECKER:
               display_on = 1;
               lightAlternatingCheckerPattern();
               break;
           }
           uBit.sleep(sleep_time); 
        }
        printf("animationLoop exiting\n");
    }

Run 'yt build'


step 11 - complete the onControllerEvent function

The onControllerEvent function responds to events received from the Bitty Xmas Bauble app over Bluetooth to select a tune and/or animation pattern. Complete it so that it looks like this:

    void onControllerEvent(MicroBitEvent e)
    {
        printf("onControllerEvent %d.%d\n",e.source,e.value);
    
        if ( e.source == STATE_QUERY) {
            // prime the Microbit Event characteristic with the current display/audio values so that they
            // are returned when the client issues a read against the characteristic
            // remember: current_selections is a 16 bit number... when transmitted over BLE it will be done so in Little Endian format
            // This is why it might look like the audio and light selection values are the wrong way around here. They're not :-)
            uint16_t current_selections = (selected_audio_seq << 8) + selected_light_seq;
            MicroBitEvent(STATE_REPORT, current_selections);
            return;
        }
    
        // 0 means no change being requested
        const uint8_t audio_sequence = (uint8_t)((e.value & 0xFF00) >> 8);
        const uint8_t light_sequence = (uint8_t)(e.value & 0x00FF);
        printf("onControllerEvent light=%d audio=%d\n",light_sequence,audio_sequence);
        printf("onControllerEvent selected_light=%d\n",selected_light_seq);
    
        if (light_sequence != 0 && light_sequence != selected_light_seq) {
            printf("light sequence selected\n");
            if (display_on == 0) { 
                display_on = 1;
                // we only create the animation loop if an animation has been requested (to save power)
                printf("starting animation loop\n");
                create_fiber(animationLoop);
            }
            sleep_time = 1000;
            if (light_sequence == RANDOM_PIXELS_FLASH) {
                sleep_time = 100;
            } else if (light_sequence == RIPPLE) {
                sleep_time = 400;
                delta = 1;
                inx = 0;
            } else if (light_sequence == SPIRAL) {
                sleep_time = 50;
                inx = 0;
                delta = 1;
                pixel_value=255;
            }
        } 
        uBit.display.stopAnimation();
        pattern.clear();
        if (audio_sequence > 0) {
            selected_audio_seq = audio_sequence;
            if (audio_on == 0) { 
                audio_on = 1;
                create_fiber(audioLoop);
            }
        }
        if (light_sequence != 0) {
            selected_light_seq = light_sequence;
        }
    }

Run 'yt build'


step 12 - complete the onButton function

Pressing either of the micro:bit buttons will cause animations and any playing tune to stop immediately. An essential function just in case someone is finding the incessant beeping a little annoying! :-)

Complete onButton so that it looks like this:

    void onButton(MicroBitEvent e)
    {
        // "emergency stop" - no need to dig out your phone and connect just to stop the music from annoying people!
        // Either  button will stop both sound and animation
        printf("onButton\n");
        if (e.source == MICROBIT_ID_BUTTON_A || e.source == MICROBIT_ID_BUTTON_B) {
            // stop current light and/or audio
            selected_light_seq = STOP;
            selected_audio_seq = STOP;
            uBit.display.stopAnimation();
            uint16_t stopped = (STOP << 8) + STOP;
            printf("raising state report event: %d\n",stopped);
            MicroBitEvent(STATE_REPORT, stopped);
            return;
        }
    }

Run 'yt build'

Install the resultant hex file and test with Bitty Xmas Bauble!

step 13 - make Bluetooth pairing required

For this application it makes sense to use Bluetooth pairing. If you pair your micro:bit with your phone then nobody else will be able to connect to your micro:bit.

Edit the config.json file in your microbit-samples folder and change it so that the 'open' property is set to 0 and the 'pairing_mode' property is set to 1, as shown here:

{
    "microbit-dal": {
        "bluetooth": {
            "enabled": 1,
            "pairing_mode": 1,
            "private_addressing": 0,
            "open": 0,
            "whitelist": 1,
            "advertising_timeout": 0,
            "tx_power": 6,
            "dfu_service": 0,
            "event_service": 1,
            "device_info_service": 1
        },
        "gatt_table_size": "0x300"
    }
}

Run 'yt clean' and then 'yt build' to rebuild your application in full and install the hex file on your micro:bit.

you've finished!

That's it! You've finished and are all set to have fun with the bitty xmas bauble application. You've also learned about Bluetooth connection trackingand how to activate the Bluetooth accelerometer service on your micro:bit. Congratulations!

if you get stuck

Check the steps above carefully and see if you can spot where things have gone wrong. If necessary, start again and proceed very slowly. building and testing at each step. Take a look at the solution and compare the code with yours.

If you did not install the correct config.json file in the microbit-samples directory or put it in the wrong place (e.g. in microbit-samples/source) then the app may not work because the pairing settings on the phone compared with the micro:bit may not match. Check the location and content of config.json. Try pairing your phone with your micro:bit. See our list of common issues for more information on this.

Make sure no other app on your phone is connected to your micro:bit. If you've been experimenting with other apps like nRF Connect, for example, this can easily happen. List the running apps on your phone and kill any that use Bluetooth, just to be sure. Kill and restart the bitty xmas bauble app too.