Blog as you go: sigma delta DAC

I have some piezo speakers from another project. That one used a bi-level amp to drive them. I figured it would  be fun to try a tri-level drive, using an H bridge allows you to have +, – and 0V across the device. And for fun, why not make it a direct sigma delta encoder?

It’s going to run on a microcontroller (an arduino). It’ll need very precise timings, so I’ll not be using the arduino environment.

Here’s a first pass in C++ for linux:


#include <iostream>
#include <cmath>
#include <utility>

using namespace std;

float signal(int t)
{
	//Roughly 0-1
	return (1 + sin(t/20. - 1))/2.1;
}

float quantize(float f, int levels)
{
	return min(levels-1.f, max(0.f, floor(f*levels)))/(levels-1);
}

int main()
{

	float integral = 0;

	for(int i=0; i < 200; i++)
	{
		float output = quantize(integral, 3);
		float difference = signal(i) - output;
		integral += difference;

		cout << signal(i) << " " << output << endl;
	}

}

And this produces output like this (mildly prettified):
graph.png

Which looks about right. The code will pretty much stink on an arduino since it’s all floating point. It’s easy to convert to integer code though:

#include <iostream>
#include <cmath>
#include <utility>

using namespace std;

uint16_t signal(int t)
{
	//Roughly 0-1
	return 65535 * (1 + sin(t/20. - 1))/2.1;
}

uint16_t quantize(int32_t i)
{
	if(i < 21845)
		return 0;
	else if (i < 43690)
		return 32768;
	else
		return 65535;
}

int main()
{

	int32_t integral = 0;

	for(int i=0; i < 200; i++)
	{
		uint16_t output = quantize(integral);
		float difference = signal(i) - output;
		integral += difference;

		cout << signal(i) << " " << integral << " " << output << endl;
	}

}

I’ve used a uint16_t for the value, which will effectively represent both positive and negative levels with 32768 being the zero point. Note that the error integral must be both signed and wider since the errors can grow beyond the signal range:

graph.png

Now to port to the arduino. So, I’ll get my Makefile from here.

I’m going to pick pins 8 and 9 on the UNO, that is PB0,1 on the chip for my outputs. and here’s how you get 3 way opposed outputs as +/-/0. To demo, I connected a pair of LEDs in parallel but facing the other way:


#include <avr/io.h>
#include <util/delay.h>
int main()
{
	int i=0;

    while(1)
    {
		if(i==0)
		{
			DDRB = 3;
			PORTB = 1;
		}
		else if(i==1)
		{

			DDRB = 3;
			PORTB = 2;
		}
		else
		{
			DDRB=0;
			PORTB = 0;
		}

		i++;
		if(i > 2)
			i=0;

        _delay_ms(200);
    }
}

So I started the port and BOOM! 😦

It stopped working. The reason was simple: the simple makefile takes one object file and converts it to HEX. Since I’m using sin(), we actually need to link the .o and .a into a .elf file, then convert THAT to HEX. The snippet is:

%.hex: %.elf
avr-objcopy -j .text -j .data -O ihex $< $@

prog.elf: delta_sigma.o
avr-gcc $(FLAGS) -o prog.elf delta_sigma.o -lm

Obvious, really, in hindsight…

So, OK, now to convert the modulator code to the arduino. Lots of things went wrong. But first, here’s the code:

#include <math.h>
#include <stdint.h>
#include <avr/io.h>

uint16_t signal(int32_t t)
{
	float u = t / 1024.f;

	//Roughly 0-1
	return 65535 * (1 + sin(2*3.151592f*u))/2.1;

}

uint16_t quantize(int32_t i)
{
	if(i < 21845)
		return 0;
	else if (i < 43690)
		return 32768;
	else
		return 65535;
}

int main()
{

	int32_t integral = 0;

	DDRB|=32;

	for(uint32_t i=0; ; i++)
	{
		uint16_t output = quantize(integral);
		int32_t difference = (int32_t)signal(i) - output;
		integral += difference;

		if(output == 0)
		{
			DDRB=255;
			PORTB=1; //Output 1 0
		}
		else if(output == 65535)
		{
			DDRB =255;
			PORTB=2; //Output 0 1
		}
		else
		{
			DDRB=255;
			PORTB=0; //Output 0 0
		}
	}
}

What didn’t go wrong? Nothing! I wasn’t nearly careful enough with my ints (only 16 bits on AVR), ints of specific width, overflow and that sort of thing. Also, initially, I decided to output a 0 level by tri-stating the two outputs, so they both float to the middleish. Turns out that didn’t work well since they float extremely slowly (not surprising really!). Forcing them both down to 0 worked much better.

After all that, I then connected a simple RC filter across it so you an see the results:

That’s actually a pretty nice sine wave there! It ought to be: there’s really not much room for nonlinearity and other distortions to creep in. I’ve zoomed in a few levels so you can see how it looks in detail.

It is however really really slow. I’m using full floating point, and a transcendental operation every iteration of the sigma delta encoder. That is really slowing down the cycle time since the AVR isn’t very fast. That accidentally solves the other problem which I’ve made no attempt to make sure every path takes the same number of cycles. But that sin() is dominating so heavily that it doesn’t matter.

And that’s it for part 1: a working sigma delta encoder. For part 2, I’ll make it fast enough to generate audio tones which aren’t simply the sigma-delta encoder transitions (I hope).

Oh also here’s tehe obligatory github link.

 

Advertisements

Arduino without the arduino environment (a minute guide)

The only way to learn a new programming language is by writing programs in it. The first program to write is the same for all languages:

Print the words
hello, world

This is the big hurdle; to leap over it you have to be able to create the program text somewhere, compile it successfully, load it, run it, and find out where the output went. With these mechanical details mastered, everything else is comparatively easy.

–Kernighan & Ritchie, 1978

This quote is annoyingly true in the world of microcontrollers. “Hello, world” is considered to be flashing an LED, and it’s more a case of learning the microcontroller rather than a language, though there’s often not that much different, due to the idiosyncrasies of microcontrollers.

This time, I’m diving in to Atmel, via Arduino. Today I came to the conclusion that I needed some reasonably precise timing on some I/O lines. So, I went off to Maplin to see what they had and perhaps not very surprisingly they had a selection of Arduinos (they no longer stock PICKIT in store), so I picked one up. The device consists of an Atmega328p MCU, another Atmega acting as a USB-serial device, a USB port, some bootloader software and some headers including in circuit programming ones.

Essentially the Arduino is everything you need to do to get started with microcontrollers (except the USB cable), a standard breakout layout and an easy to use programming environment. The Arduino environment is not suitable for me today. This is not a criticism, but the trade of simplicity for flexibility means it deals with all the interrupts and timers and good control over them is precisely what I need.

Without the nice environment, the main problem is figuring out the correct magic incantations to go from some C code to a blinking LED. It of course also took me far longer than it ought to have done to figure out, which is why I’m writing about it here.

Here’s the C++ program (test.cc) which I want to run. Note that avr-gcc comes with some nice utilities like time delays, and the Arduino UNO (and most other arduinos) have an LED wired up to what is labelled as pin 13 on the board which corresponds to B5 for the UNO.

#include <avr/io.h>
#include <util/delay.h>
int main()
{
    // DDRB  Data Direction Register for port B
    // 1 corresponds to output. 0 for input.
    // _BV is Bit Value, i.e. BV(x) is 1<<x
    DDRB |= _BV(DDB5); 

    while(1) 
    {
	PORTB^=_BV(PB5);
        _delay_ms(100);
    }
}

And here are the magic incantations in the form of a Makefile:

#This is the form of names cross compilers usually have
CXX=avr-g++
CC=avr-gcc
LD=avr-g++

#atmega328p is used on the Arduino Uno
#It needs to be specified in several places so it's in the 
#Makefile, not the source code.
MCU=atmega328p

#serial port for programmer
#Find this by typing "dmesg" after plugging in the cable
PORT=/dev/ttyACM0

#Specify the CPU frequency in Hz
#Required for the delay() function in the C source.
F_CPU=16000000UL

#Specify optimizations:t's more common to optimize for size
#not speed (-Os) due to limited flash. We need to tell the compiler
#what the MCU is (of course), and F_CPU needs to be #defined before
#the util header is included.
FLAGS=-Os -mmcu=$(MCU) -DF_CPU=$(F_CPU)

#Set these flags so they're picked up by the implicit C and C++ 
#compiler rules.
CXXFLAGS=$(FLAGS)
CFLAGS=$(FLAGS)


OBJECTS=test.o

#Bog standard linker line
test:$(OBJECTS)
	$(LD) -o $@ $<


# Programmers expect HEX files not ELF files.
# Copy the text and data sections to make a hex file.
# .text is the machine code. .data is all the static data
# I don't believe we need anything else
%.hex: %
	avr-objcopy -j .text -j .data -O ihex $< $@

clean:
	rm -f *.o *.hex test

# "make upload" target to program the MCU
# -p specify MCU
# -c progrmmer type.
# -e erase
# -U actions which are flash memory write test.hex
# -P perial port
upload:test.hex
	avrdude -p $(MCU) -c arduino -e -U flash:w:test.hex  -P $(PORT)

And that’s it. It was as simple as I expected/hoped. Other than a bit of Make guff and some verbosification to make it a bit more managable and obvious, there’s 4 lines of active code, one of which is a completely standard linker instruction. How many LOC per day would that make it? 🙂

I’d like to thank Micah Carrick and Sudar Muthu for their guides/tools which is where I got most of the information from in the end.