Floating Point Representation

It’s exam season again which means my build time is a bit limited. I thought I’d take the opportunity while I’m studying to write some more content content for the basics area of this site. To start out I thought I’d build on my post regarding Binary Representation. With an understanding of binary we can begin to explore one of the more confusing data types, Floating point. I know this topic is straying a little bit from audio synthesis but if you plan on working with microprocessors it’s a good thing to understand.

Binary Fractional Representation

The first piece of this puzzle is understanding how fractional numbers are represented in binary. That is to say non-integer numbers. You may remember that in binary each bits value can be determined by raising 2 to the power of that bit. So the first bit is 2^0 = 1, the second is 2^1 = 2 and so on. For numbers on the other side of the decimal point we continue the same pattern but move into negative exponents. The table below should offer some clarity.

This means if we wanted to express a decimal number like 10.625. We could do so in binary by writing 1010.101.

Conversions of Fractional Numbers

You may remember that we can convert an integer number in decimal to binary by repeatedly halving it and noting the remainders. There is a similar algorithm for converting fractional numbers. This time however we repeatedly double the number and note whether the result is greater than one. Taking the example 10.625 from earlier. For the whole number part (10).

Reading from the bottom up, this gives us the binary representation of the whole number portion (1010). Next for the fractional portion (0.625) we can repeatedly double.

This time we read from the top down to get the fractional part in binary .101. Putting it together we get the answer from above 1010.101.

Scientific Notation in Base 2

If you’ve ever had to do math involving very large or very small numbers you’ve probably encountered scientific notation. It’s a fairly straight forward concept. If you have a number with a bunch of zeros like 30,000,000. You know that dividing by 10 would remove one of the zeroes. If you divided by 10 7 times you would be left with just 3. That means 3 * 10 * 10 * 10 * 10 * 10 * 10 * 10 would be equivalent to 30,000,000. For brevity we just write 3 * 10^7.

This process also works for zeros on the other side of the decimal point. We simply use a negative exponent in these cases. for instance 0.000015 can be represented as 1.5 * 10^-5. The key here is that we multiply or divide by ten in a base ten system. So guess what we use in base 2.

You may not have encountered scientific notation in binary but it works in much the same way. You can slide the decimal point back and forth by multiplying or dividing by 2. for instance our example from earlier (1010.101) could be represented by writing 1.010101 * 2^3. This is at the core of floating point representation.

Floating Point

So that was a lot of preamble… but what actually is floating point. At a high level floating point is a way to represent fractional numbers in a digital way. Through some clever design floating point allows us to represent both incredibly large and incredibly small numbers with the same basic architecture.

A floating point number is made up of three parts. The sign, the exponent and the mantissa. The first, the sign bit, is the easiest to understand. If the sign bit is a 1, the number is negative, if it’s a 0, the number is positive.

The Mantissa

The mantissa holds the actual digits of the number. If we go back to our example of 10.625 (1010.101). We would slide the decimal to the first digit giving us 1.010101. Take note of how many decimal places we move the decimal point, we’ll need that later. Also notice that regardless of the number we do this with we will always have a 1 on the left of the decimal point. Since we can always assume this one will be there we can actually leave it out of our final representation. This leaves us with 010101 which is what you would find in the mantissa segment of a floating point number.

The Exponent

The final piece is the exponent. This represents how many digits the decimal point must be moved from the actual number to reach the mantissa. In our example we’ve moved the decimal point 3 places to the left, but there’s a little more to the story. In order for the exponent to represent both positive and negative numbers a bias is added. The bias is typically half the range of numbers that can be represented in the exponent. That means if we have 8-bits set aside for the exponent (0-255) a bias of 127 would be used. That way even if we have a negative exponent it can still be represented using an unsigned number. Adding 127 to our exponent (3) we end up with a value of 130. We can represent this in binary with 1000 0010.

Putting It Together

Now that we’ve determined the values for our sign bit, exponent and mantissa we can put them together into a floating point number. We can do so in one of two ways. Floating point numbers are divided into either singles or doubles which refers to the level of precision each has available. A single is made of 32 bits (4-bytes) while a double takes 64 bits (8-bytes). I will go over each of them with our current example.

Single Precision

In single precision we use 32 bits. Of these 32-bits, 1 is used for the sign, 8 for the exponent and 23 for the mantissa. Using the values we determined previously that means the single point representation of 10.625 would be:

S | Exponent | Mantissa

0 | 1000 0010 | 0101 0100 0000 0000 0000 000

Notice I have added zeros to the tail end of the mantissa to fill the available bits. This won’t affect the number. It’s also pretty clear looking at this example that even with the lower precision of a single we can represent an incredible spectrum of numbers. We can use exponents from 127 to -127. Just to give an idea of the scale there that would approximately translate to a number with 38 zeros before or after the decimal point in base 10.

Double Precision

In case single doesn’t provide the range or precision necessary another larger option is available. With double precision we use 64 bits to represent a number. This is divided into, 1 sign bit, 11 bits for the exponent and 52 bits for the mantissa.

One important note is that since we now have more than 8 bits for the exponent we need to adjust our bias. 11 bits provides a range from 0-2047. Half of that range gives us a bias of 1023. This makes the exponent for our example 1026 (1023+3). In binary 1026 is 1000 0000 010. By putting this together with the other results calculated earlier we get a double precision floating point representation as follows:

S | Exponent | Mantissa

0 | 1000 0000 010 | 0101 0100 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

With 11 bits available for the exponent the available number range has grown exponentially (pun intended). We can now use exponents anywhere from 1023 to -1023. These numbers actually go beyond what my poor Casio calculator is capable of outputting. In base 10 they equate to approximately 307 zeroes on either side of the decimal place!

In Closing

Floating point representation can seem really intimidating at first. That being said with some practice they start to make sense and you’ll begin to develop an intuition while working with them. As you go further working with micro-controllers you will start encountering situations where you have to send and receive data, read or write from registers or make bare-metal conversions between data-types. In any of these situations a strong grasp on floating point (and other common data types) will serve you well.

Arduino R2R DAC: Frequency

Modified R2R DAC With Added Analog Input

As promised I’m back with another update for this project. Today I’ve been playing around with my Arduino trying to get frequency control working. It took a bit of experimentation due to my use of timer interrupts but I got there. Additionally I’ve made some slight modifications to the other code to clean things up a bit.

The Big Idea

I played around with a few different approaches to adjust the frequency. After some exploration I settled on modifying the overflow counter (OCR1A) value. If you remember from my article on Timer Interrupts this is the value which the timer counts up to before outputting an interrupt. By manipulating this value using a potentiometer we can adjust the frequency in real time.

Some Math

To get started we need to figure out what values we’ll need to set the OCR1A register to. My goal is to get a frequency range of approximately 50Hz – 1kHz. Luckily the calculation to find the OCR value given a frequency is fairly straight forward.

By inputting the known values for clock frequency (16MHz on Arduino), prescaler (8) and N (50) I can solve for my upper and lower frequency bounds.

From here I know that the minimum OCR value I want to use is going to be 40. Additionally, the range of values between the lowest and highest value on the potentiometer should be 760. To set the lower limit we can add 40 to whatever our reading is on the potentiometer. For the upper limit we have to do a little more fancy footwork. Since the analog inputs on the Arduino output a value between 0 and 1023, I need to normalize this value. To achieve this I multiply the reading by 760.0 and then divide by 1023. Note the .0 is important as it forces the division to be handled as a float (maintains decimal points). Once the calculation is complete I can type cast it back to an int to be used by the program.

The final implementation of this algorithm is as follows. Note FREQ is the name I defined for the analog input at the start of my code.

ARR_check = int(((analogRead(FREQ)*760.0)/1023)+40);

Dealing With Ripple

You may notice in the code above I do not write the value directly to the OCR. I ran into problems as the oscillations in the circuit caused a fair amount of noise on my ground and voltage lines. This issue was compounded when connected to a speaker. This caused the analogRead value to bounce around and the OCR to be constantly updating with different values. The constant updates broke the flow of my waveform and left the output frequency bouncing around even when the pot wasn’t being touched.

By adding a set of decoupling capacitors I was able to limit this noise but not completely remove it. I used a 0.1uF capacitor and a 100uF capacitor between the ground and power rails.

My final solution in the program itself was to set the value of the analog read (after normalizing it to the desired range) to a variable (ARR_check). I then took the absolute value of this check minus the current value of the OCR. With this I could set the OCR to update only if the difference reached a certain margin. In my experimentation I found 25 was a large enough margin to stabilize things. You can see the resulting main loop below.

void loop(){
  ARR_check = int(((analogRead(FREQ)*760.0)/1023)+40);
  if (abs(ARR_check - OCR1A) > 25){
    OCR1A = ARR_check;  
    }
  
  delay(50);
  }

Note that this is not an ideal situation. The use of this buffer means I will not have a smooth range of frequencies as I turn the potentiometer. The output is more like a step tone generator with the value of the margin defining the step size. One interesting experiment might be to add a second potentiometer to adjust the margin value which would produce something similar to an Atari Punk Console. Only this APC would be capable of outputting sine and triangle waves. I’ll be looking into better ways to isolate this circuit and minimize the effect of this noise in future posts. For now though this solution still gets the variance I was after.

Some Optimization

One of my experiments while I was trying to get the frequency adjustment working involved increasing the size of the tables and adjusting how quickly I iterated through them. Attempting to do this however I ran into some problems with the Arduino Uno’s memory constraints. This led me to make some changes to the code to optimize the memory use.

uint8_t wave_table[N];

The first thing I did was do away with the 4 separate table arrays and replace them with a single array. I additionally changed the type of this array from int (32-bits) to uint8_t (8-bits). Since the DAC’s values range from 0-15 the uint8 is plenty to handle them. In fact even if I move up to 8-bits the uint8 will still be sufficient.

void get_sine_table(){
  for (int i = 0; i < N; i++){
    wave_table[i] = int(7.5+7.5*sin(i*(2*3.14)/N));
  }
}

void get_rsaw_table(){
  for (int i = 0; i < N; i++){
    wave_table[i] = int(i*(15.0/N));
  }
}

void get_lsaw_table(){
  for (int i = 0; i < N; i++){
    wave_table[i] = int(15 - i*(15.0/N));
  }
}

void get_triangle_table(){
  for (int i = 0; i < N; i++){
    if (i<(N/2)){
      wave_table[i] = int(i*(15.0/(N/2)));
      }
    else{
      wave_table[i] = int(15 - (i-N/2)*(15.0/(N/2)));
      }
  }
}

Next I modified the get table functions to write directly to this single wave table rather than their respective tables.

void switch_pin_ISR(){
  curr_table = (curr_table + 1)%4;
    switch(curr_table){
    case 0:
      get_sine_table();
      break;
    
    case 1:
      get_rsaw_table();
      break;
    
    case 2:
      get_lsaw_table();
      break;

    case 3:
      get_triangle_table();
      break;
    }
  }

I then removed the calls to each get table function from set-up. To replace them I reated a switch statement in the button interrupt. This switch causes the wave_table array to be overwritten with a new table each time the button is pressed. The curr_table variable still tracks which table is currently selected and governs which get table function is called after each press.

ISR(TIMER1_COMPA_vect){
  v_level = wave_table[table_index];
  PORTB = v_level;         
  table_index = (table_index+1)%N;
  
}

Finally I removed the switch statement in the timer interrupt and set v_level to be updated directly from the wave_table array.

That wraps it up for today. I’m going to be doing some work to stabilize the circuit and hopefully generate a cleaner frequency sweep. As always I’ll provide any updates as I find them.

#define PIN_8 8
#define PIN_9 9
#define PIN_10 10
#define PIN_11 11

#define FREQ A0
#define SWITCH_PIN 2         //Push button to switch waveform

#define N 50            //Samples per cycle

volatile int switch_state = HIGH;

int table_index = 0;
uint8_t v_level;

uint16_t ARR_check = 35;

//Lookup tables for waveforms
uint8_t wave_table[N];

uint8_t curr_table = 1;

void timer1_init();

//Function definitions to generate wave tables
void get_sine_table();
void get_rsaw_table();
void get_lsaw_table();
void get_triangle_table();

void setup() {
  DDRB = B1111;      //Set Pins 8-11 as Inputs
  
  timer1_init();    //Initiate timer

  //initiate button interrupt to change waveform
  pinMode(SWITCH_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(SWITCH_PIN), switch_pin_ISR, FALLING);

  //Initialize wave tables
  get_sine_table();
}

void loop(){
  ARR_check = int(((analogRead(FREQ)*760.0)/1023)+40);
  if (abs(ARR_check - OCR1A) > 25){
    OCR1A = ARR_check;  
    }
  
  delay(50);
  }

void timer1_init(){
  noInterrupts();        
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;

  OCR1A = 40;               //Compare Value
  TCCR1B |= (1 << WGM12);    // CTC mode
  TCCR1B |= (1<<CS11);       // 8 prescaler 
  TIMSK1 |= (1 << OCIE1A);   // enable timer compare interrupt

  interrupts();     
}

ISR(TIMER1_COMPA_vect){
  v_level = wave_table[table_index];
  PORTB = v_level;         
  table_index = (table_index+1)%N;
  
}

void switch_pin_ISR(){
  curr_table = (curr_table + 1)%4;
    switch(curr_table){
    case 0:
      get_sine_table();
      break;
    
    case 1:
      get_rsaw_table();
      break;
    
    case 2:
      get_lsaw_table();
      break;

    case 3:
      get_triangle_table();
      break;
    }
  }

void get_sine_table(){
  for (int i = 0; i < N; i++){
    wave_table[i] = int(7.5+7.5*sin(i*(2*3.14)/N));
  }
}

void get_rsaw_table(){
  for (int i = 0; i < N; i++){
    wave_table[i] = int(i*(15.0/N));
  }
}

void get_lsaw_table(){
  for (int i = 0; i < N; i++){
    wave_table[i] = int(15 - i*(15.0/N));
  }
}

void get_triangle_table(){
  for (int i = 0; i < N; i++){
    if (i<(N/2)){
      wave_table[i] = int(i*(15.0/(N/2)));
      }
    else{
      wave_table[i] = int(15 - (i-N/2)*(15.0/(N/2)));
      }
  }
}

Arduino R2R DAC: Waveforms

Modified R2R DAC Schematic with Push Button Control

I’ve spent my last few posts tuning my R2R DAC and cleaning up the code. With the changes I’ve made I’ve got things running pretty smoothly. Now I’m ready to start building up the functionality of this circuit to see what I can get it to do.

All The Waveforms

First up I’d like to set up some waveform tables so that I can start switching between states as necessary. In order to do this I will have to set up a button interrupt to select waves, create the state tables and develop logic to select between them. This process will mirror fairly closely what was done in my PWM based oscillator so it may seem somewhat familiar. There are some important differences however due to the limited resolution of the DAC and the use of timer interrupts. I’ll get into these differences in more detail later in this post.

Declarations

#define SWITCH_PIN 2         //Push button to switch waveform
#define N 50                 //Samples per cycle

volatile int switch_state = HIGH;   

int table_index = 0;
int curr_table = 1;
int v_level;

//Lookup tables for waveforms
int sine_table[N];
int rsaw_table[N];
int lsaw_table[N];
int triangle_table[N];

//Function definitions to generate wave tables
void get_sine_table();
void get_rsaw_table();
void get_lsaw_table();
void get_triangle_table();

Lots going on there! Lets go through it step by step.

Definitions:

First in the definitions I define my switch pin and table size (N). These definitions are not strictly necessary but make the code a lot easier to read and allow quick modifications when necessary.

Switch State:

Next up we’ve got the switch_state variable. This is part of the implementation of the push button. Note the keyword volatile which lets the Arduino know this variable can change at any time (even without the program changing it). This will allow the variable to pick up changes to the push button during runtime.

Tracking Variables:

Next up we’ve got some tracking variables. Since I’m using timer interrupts rather than a loop with delays I need to track where in the wave I am. That is the function of table_index. Each time I write a voltage to the DAC table_index will be incremented to move to the next value in the table. Curr_table is similar except that it will track which table is being used currently. Finally v_level is used as a place holder which will receive values from the tables to be written to the PORTB register.

Tables:

Last but certainly not least we initialize the tables and functions to generate them. Note each table is of size N. This means if I change my N value the table size will automatically be updated.

Setup


void setup() {
  DDRB = B1111;      //Set Pins 8-11 as Inputs
  
  timer1_init();    //Initiate timer

  //initiate button interrupt to change waveform
  pinMode(SWITCH_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(SWITCH_PIN), switch_pin_ISR, FALLING);

  //Initialize wave tables
  get_sine_table();
  get_rsaw_table();
  get_lsaw_table();
  get_triangle_table();
}

The setup function has a couple new additions since my last post. First I needed to setup and attach the button interrupt which will let me change waves. This is done by first setting the pinMode of the previously defined SWITCH_PIN. Once set up we attach an interrupt to this pin using the attachInterrupt() Arduino function. If you remember from my PWM build this function takes three arguments. First we call digitalPinToInterrupt(SWITCH_PIN) to show what pin is being attached. Then we give the name of the function to be called and finally that the interrupt should trigger on the falling edge of a button press.

Additionally I’ll be calling my four get table functions (which I’ll be setting up soon). These functions populate the tables with values and get them ready to be indexed as the oscillator oscillates.

Switch Interrupt and Tables

void switch_pin_ISR(){
  curr_table = (curr_table + 1)%4;
  }

void get_sine_table(){
  for (int i = 0; i < N; i++){
    sine_table[i] = int(7.5+7.5*sin(i*(2*3.14)/N));
  }
}

void get_rsaw_table(){
  for (int i = 0; i < N; i++){
    rsaw_table[i] = int(i*(15.0/N));
  }
}

void get_lsaw_table(){
  for (int i = 0; i < N; i++){
    lsaw_table[i] = int(15 - i*(15.0/N));
  }
}

void get_triangle_table(){
  for (int i = 0; i < N; i++){
    if (i<(N/2)){
      triangle_table[i] = int(i*(15.0/(N/2)));
      }
    else{
      triangle_table[i] = int(15 - (i-N/2)*(15.0/(N/2)));
      }
  }
}

There are five new user defined functions needed for this program. The switch_pin_ISR function is identical to the one I used in my PWM project. It increments the curr_table variable and uses the modulo opperator to ensure the value is always between 0 and 3.

The table definitions are slightly different. Where with my PWM project I had a range from 0-255 to define my voltage levels I now only have 0-15 (for the 16 states of the 4bit DAC). You may notice as well I’ve added some strange typecasting to these functions. The issue I ran into was that for any N greater than 15 the result of 15/N returned 0. To get around this I used 15.0/N which gave a float result. Once I had finished the calculations I type casted this float back to an int so I could write it to the PORT register.

Timer Interrupt

ISR(TIMER1_COMPA_vect){
  switch(curr_table){
    case 0: 
      v_level = sine_table[table_index];
      break;
    
    case 1:
      v_level = rsaw_table[table_index];
      break;
    
    case 2:
      v_level = lsaw_table[table_index];
      break;

    case 3:
      v_level = triangle_table[table_index];
      break;
    }
  PORTB = v_level;         
  table_index = (table_index + 1)%(N);
}

Finally I modified the timer interrupt function. By adding a switch statement I allow the v_level variable to be set based on the value of curr_table. Once this v_level is set I write it to PORTB to generate the output voltage. Last I increment the table_index variable to move to the next wave step. Again I use the modulo operator to reset table_index back to zero once it reaches N.

That about covers it for today, In my next post I’ll be modifying the code again to add the ability to adjust the frequency. As always I’ll finish by posting the full code. Till next time!

#define PIN_8 8
#define PIN_9 9
#define PIN_10 10
#define PIN_11 11

#define SWITCH_PIN 2         //Push button to switch waveform

#define N 50                 //Samples per cycle

volatile int switch_state = HIGH;

int table_index = 0;
int v_level;

//Lookup tables for waveforms
int sine_table[N];
int rsaw_table[N];
int lsaw_table[N];
int triangle_table[N];

int curr_table = 1;

void timer1_init();

//Function definitions to generate wave tables
void get_sine_table();
void get_rsaw_table();
void get_lsaw_table();
void get_triangle_table();


void setup() {
  DDRB = B1111;      //Set Pins 8-11 as Inputs
  
  timer1_init();    //Initiate timer

  //initiate button interrupt to change waveform
  pinMode(SWITCH_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(SWITCH_PIN), switch_pin_ISR, FALLING);

  //Initialize wave tables
  get_sine_table();
  get_rsaw_table();
  get_lsaw_table();
  get_triangle_table();
}

void loop(){
  }

void timer1_init(){
  noInterrupts();        
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1  = 0;

  OCR1A = 125;                //Compare Value
  TCCR1B |= (1 << WGM12);      // CTC mode
  TCCR1B |= (1<<CS11);    // 8 prescaler 
  TIMSK1 |= (1 << OCIE1A);  // enable timer compare interrupt

  interrupts();     
}

ISR(TIMER1_COMPA_vect){
  switch(curr_table){
    case 0:
      v_level = sine_table[table_index];
      break;
    
    case 1:
      v_level = rsaw_table[table_index];
      break;
    
    case 2:
      v_level = lsaw_table[table_index];
      break;

    case 3:
      v_level = triangle_table[table_index];
      break;
    }
  PORTB = v_level;         
  table_index = (table_index + 1)%(N);
}

void switch_pin_ISR(){
  curr_table = (curr_table + 1)%4;
  }

void get_sine_table(){
  for (int i = 0; i < N; i++){
    sine_table[i] = int(7.5+7.5*sin(i*(2*3.14)/N));
  }
}

void get_rsaw_table(){
  for (int i = 0; i < N; i++){
    rsaw_table[i] = int(i*(15.0/N));
  }
}

void get_lsaw_table(){
  for (int i = 0; i < N; i++){
    lsaw_table[i] = int(15 - i*(15.0/N));
  }
}

void get_triangle_table(){
  for (int i = 0; i < N; i++){
    if (i<(N/2)){
      triangle_table[i] = int(i*(15.0/(N/2)));
      }
    else{
      triangle_table[i] = int(15 - (i-N/2)*(15.0/(N/2)));
      }
  }
}