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):

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:

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.