Michael J. McCaffrey

Tagline and stuff.

Digital Metronome

Using an MSP430 with Energia


 As one of my first projects in my exploration of microcontrollers, I designed a digital metronome with an LCD. At the time, I was unfamiliar with the use of interrupts, timers, and other peripherals, so I opted to code this project using Energia, an Arduino-like IDE for Texas Instruments' MSP430 line. Because of its simplicity, this metronome could be a good project for those just getting into the world of microcontrollers.


Overview

 A metronome is a device which keeps a consistent tempo reference for musicians. Mechanical metronomes use a pendulum-like motion to produce a periodic clicking sound that musicians can keep time with. Modern electronic metronomes have many features, and provide a combination of audio and visual feedback. This project aimed to incorporate many features of digital metronomes with relatively simple methods. Also, the hardware designed for this project will be outlined including a prototyping method using perf-board and copper foil tape.


Software: Initialization

#include <LiquidCrystal.h>

#define TAP 10
#define ROT_A 8
#define ROT_B 9
#define START_STOP 6
#define LED 11
#define ROT_BTN 7

#define RUN 0
#define TEN 1
#define ONE 2
#define NUM 3
#define DEN 4

int tempo;
int ptempo;
int tap_index = 0;
long tref1 = 0;
long tref2 = 0;
long tavg = 0;
long tperiod;
long period;
long ON_time;
long OFF_time;
int pmeter = 4;
int meter = 4;
int beat = 1;
long ref;
int dispO;
int dispT;
int dispH;
int disp;
int pmic = 0;
long btn_ref = 0;
long ptn_ref = 0;
long rdb = 0;
int denom = 4;
int pdenom = 4;
unsigned long rotdb = 0;
unsigned long tapdb = 0;
unsigned long ssdb = 0;

char MODE = RUN;

bool METRONOME_STATE = false;
bool SSS;
bool pclickON;
bool clickON;
bool PSSS = true;
bool METER_BTN_STATE;
bool PMETER_BTN_STATE = true;
bool ROT_B_STATE;
bool PROT_B_STATE = true;
bool ROT_A_STATE;
bool PROT_A_STATE = true;
bool ROT_BTN_STATE;
bool PROT_BTN_STATE = true;
bool PTAP_STATE = true;
bool TAP_STATE;
bool COARSE_FINE = false;

LiquidCrystal lcd(12,13,14,15,18,19);

byte eighth[8] = {
  0b00100,
  0b00110,
  0b00101,
  0b00101,
  0b00100,
  0b11100,
  0b11100,
  0b00000,
};

  We begin by including the LiquidCrystal library which will help us integrate the HD44780 or similar LCD driver into our project. Then, we define names for the pins that we will use to interface with the controller, and some state names that will describe modes of operation. We create a LiquidCrystal variable and apply the pin assignment as decided in the lcd() arguments (lcd(RS, E, D4, D5, D6, D7)). After creating the variables we will need, we are ready for the setup() function.


Setup

  Energia and Arduino have two main functions, one of which is setup(), a function that runs only once at the beginning of the program. First, the LCD is configured as a 16 column, 2 row display with the arguments in lcd.begin(). Then a custom character is created using the array of bytes created among the other variables. The inputs and outputs are configured, the LED pin is written low, the tempo is set to 120 beats-per-minute, and finally, the LCD is updated for the first time.

void setup()
{
  // put your setup code here, to run once:
  lcd.begin(16,2);
  lcd.createChar(1,eighth);
  pinMode(LED, OUTPUT);

  pinMode(ROT_BTN,INPUT_PULLUP);
  pinMode(ROT_B,INPUT_PULLUP);
  pinMode(ROT_A,INPUT_PULLUP);
  pinMode(START_STOP,INPUT);
  pinMode(TAP,INPUT_PULLUP);
  digitalWrite(LED,LOW);

  tempo = ptempo = 120;
  lcddisp();
}

User Control

/*
  control() detects and handles all button presses and knob adjustments. 
 */
void control(){

  // poll rotary encoder
  ROT_B_STATE = digitalRead(ROT_B);
  ROT_A_STATE = digitalRead(ROT_A);

  if (ROT_A_STATE != PROT_A_STATE && ROT_A_STATE == false && millis() > rdb){
    rdb = millis();
    if (ROT_B_STATE)
      switch(MODE){
      case TEN: 
        {
          COARSE_FINE = 0; 
          tu(); 
          break;
        }
      case ONE: 
        {
          COARSE_FINE = 1; 
          tu(); 
          break;
        }
      case NUM: 
        {
          pmeter = (pmeter < 12) ? (pmeter+1):pmeter; 
          break;
        }
      case DEN: 
        {
          du(); 
          break;
        }
      }
    else
      switch(MODE){
      case TEN: 
        {
          COARSE_FINE = 0; 
          td(); 
          break;
        }
      case ONE: 
        {
          COARSE_FINE = 1; 
          td(); 
          break;
        }
      case NUM: 
        {
          pmeter = (pmeter-1) ? (pmeter-1):pmeter; 
          break;
        }
      case DEN: 
        {
          dd(); 
          break;
        }
      }
    lcddisp();
  }
  PROT_A_STATE = ROT_A_STATE;

  // poll Mode button
  ROT_BTN_STATE = digitalRead(ROT_BTN);
  if (ROT_BTN_STATE != PROT_BTN_STATE && ROT_BTN_STATE == false && millis() - 100 > rotdb){
    rotbtn();
    rotdb = millis();
    lcddisp();
  }
  PROT_BTN_STATE = ROT_BTN_STATE;

  // reset tap counter after 1.9 seconds without tap
  if (millis() - tref2 > 1900)
    tap_index = 0;

  // poll Tap button
  TAP_STATE = digitalRead(TAP);
  if (TAP_STATE != PTAP_STATE && TAP_STATE == false && millis() - 100 > tapdb){
    ref = millis();
    beat = 1;
    click();
    METRONOME_STATE = false;
    tap_index++;
    tapdb = millis();
    tap();
  }
  PTAP_STATE = TAP_STATE;

  // poll Start/Stop button
  SSS = digitalRead(START_STOP); 
  if (SSS != PSSS && SSS == false && millis() - 100 > ssdb){  
    if (!METRONOME_STATE){ 
      tempo = ptempo;
      denom = pdenom;
      meter = pmeter;
      ssdb = millis();
      lcddisp(); 
    }
    PSSS = SSS; 
    METRONOME_STATE = !METRONOME_STATE;
    ref = millis();
    beat = 1;
  }
}

  The control system is entirely based on polling inputs and taking action if any of them are changed. Each input has a state value and a previous state value, both initialized to 1 or HIGH. An input is read, and its state is recorded into the current state variable. If the current state is LOW and does not match the previous state, we know that a negative edge has occured on the input. We take the proper action according to the input that changed and update the previous state variable. With this technique we can ensure that only actions are only taken for button state changes, so that a button being held down will not repeat any actions. You can read more about the use and importance of rising and falling edges on the web.

 The rotary encoder will increase or decrease a variable depending on the current state that the metronome is in. TEN and ONE allow the tempo to be changed with either coarse or fine precision. NUM makes adjustments to the meter of the metronome, or how many clicks are in each measure. The first click of a measure has a higher pitched tone than the rest of the measure. Finally, DEN changes the note length of each click. Each click can represent a whole, half, quarter, eighth, or sixteenth note, as well as eighth note and quarter note triplets (1/6th and 1/12th). The rotary encoder doubles as a button which cycles the state of the metronome through TEN, ONE, NUM, DEN, and RUN, which disables any changes from being made.

The Tap button provides a "tap tempo" feature. This allows the user to change the metronome's tempo by pressing the tap button repeatedly at the desired rate. After two or more presses, the average time between presses is updated and used to determine the period between clicks. After 1.9 seconds with no tap button presses, the tap counter resets and any new taps will start a new tempo average. The start/stop button starts and stops the metronome, updates any outstanding changes to the tempo, meter, or denomination, amd updates the LCD.


Timing and Feedback

  When the metronome is started, the current time in milliseconds is saved in a variable ref. The period between beats is some number in milliseconds that is multiplied by 4 and divided by the chosen beat denomination. This is the mechanism that allows for whole notes, triplets, etc. to be clicked on instead of only quarter notes. For example, if the chosen denomination is eighth-note triplets (1/12), the period between beats is multiplied by 4/12 or 1/3. This means there will be three clicks per quarter-note instead of one, which is the length of an eight note triplet. This feature can be very helpful to musicians looking for a reference other than the usual quarter notes.

/*
  Whenever the current time in milliseconds since the last start signal matches the tempo's 
 beat period, timer() calls the click function. timer() also keeps 
 track of the beat count, which resets to 1 if the beat count is not less
 than the current meter.
 */
void timer(){
  if ((millis()-(ref))%((period*4)/denom) == 0){
    click();
    if (beat < meter)
      beat++;
    else beat = 1;
  }
}

/*
  click() lights an LED when called by timer(). Instead of a single pulse,
 it turns on and off at an audible frequency with a 50% duty cycle. The 
 LED and a speaker or buzzer can be connected to the same pin, providing
 audio and visual feedback. 
 */
void click(){
  if (beat == 1)
    for(int i = 1; i<=31;i++){

      digitalWrite(LED,HIGH);
      delayMicroseconds(80);
      digitalWrite(LED,LOW);
      delayMicroseconds(80);
    }

  else for(int i = 1; i<=21;i++){
    digitalWrite(LED,HIGH);
    delayMicroseconds(130);
    digitalWrite(LED,LOW);
    delayMicroseconds(130);
  }
}

/*
  lcddisp() updates the LCD display. 
 */
void lcddisp(){
  lcd.setCursor(0,0);
  lcd.write(1);
  if (ptempo <99)
    lcd.print(0);
  lcd.print(ptempo);
  lcd.print(" BPM");
  lcd.setCursor(0,1);
  lcd.print(pmeter);
  lcd.print("/");
  lcd.print(pdenom);
  if (pdenom < 10) lcd.print(" ");
  if (pmeter < 10) lcd.print(" ");
  lcd.setCursor(13,0);
  switch (MODE){
  case RUN: 
    lcd.print("RUN"); 
    break;
  case TEN: 
    lcd.print("TEN"); 
    break;
  case ONE: 
    lcd.print("ONE"); 
    break;
  case NUM: 
    lcd.print("NUM"); 
    break;
  case DEN: 
    lcd.print("DEN"); 
    break;
  }
}

  Another handy feature which digital metronomes provide is an indication of the first beat of every measure with a higher pitched click than with other beats. The implementation in this project simply resets the beat counter on a start signal, increments it with each beat until the value of meter is reached, at which point it resets to 1. Every time a click is required, the click() function toggles the output pin at an audible frequency, and visual and audio feedback are produced.


Hardware

  The metronome is designed as a stand-alone unit which will not rely on a development board or computer to operate. Aside from the processor, it will require a rotary encoder with push button functionality, two momentary tactile switches, a dc barrel jack, a 3.3V voltage regulator, an LED, an HD44780 or similar LCD display, some resistors, capacitors, an NPN transistor, two potentiometers, and a speaker. We can have the output pin perform both the visual and audio feedback functions by connecting both the LED and the base of the transistor to it. Most of the current will flow through the LED with enough still to saturate the transistor in order to drive the speaker. A bypass capacitor protects the processor from high frequency changes in its power supply, and a bulk capacitor does the same for the entire device. All debouncing is done in software, so there is no hardware requirement for that, and the processor provides internal pull-up resistors. There is a potentiometer configured as a simple variable resistor which controls the volume of the click, and another that adjusts the contrast of the LCD. A full schematic, drawn in DipTrace, can be seen below.


Prototyping

  This project gave me the opportunity to try a prototyping technique I've been itching to test out. I purchased some copper foil adhesive tape for the MIDI LaserHarp project and had quite a lot left over. I thought that the tape could be really helpful with prototyping on perf-boards. With some careful planning, all of the connections can be routed with thin strips of this tape, turning a perf-board that's usually destined for a mess of wires into a clean makeshift PCB. I planned the part placement and traces using fritzing before using a rotary tool to carve out space for larger and oddly-shaped parts that need to go through the board.

 The parts were soldered to the board first, with close attention paid to stay true to the design planned in fritzing. Once the components were all in place, it was time to begin routing the foil tape. The particular tape that I used was wide enough to cut in quarters. This came in handy when traces were needed on adjacent pins and as a bonus really satisfied the cheapskate in me. Typically, I would run the foil so that it just butted up against the pin to be connected. A small bit of solder established a good connection and kept the foil in place pretty well. Also, I added a little bit of solder anywhere that I needed to join two pieces of foil. The tape is advertised as having conductive adhesive, but I wasn't getting a great connection by just laying one piece on top of the other. The final product, being my first attempt, was more than good enough to sell me on this technique. I'm excited to improve my board prototyping and I encourage other engineers and hobbyists to try it out. Thanks for reading!

Back to Top