Wishful Coding

Didn't you ever wish your
computer understood you?

TIS-AVR

Last week I played some TIS-100, an assembly puzzle game where you are left to debug a fantasy computer with only the manual to guide you.

The TIS-100 is composed of many small nodes running a very limited assembly language with only one addressable register. Every node can however read and write data to its 4 neighbors.

After finishing the game, I remembered this talk by Chuck Moore on programming a grid of tiny Forth computers, not entirely unlike TIS-100, except more practical.

What if you could make an actual TIS-100? Not just an emulator, but an actual thing that sits on your desk. I set out to try.

I went to the electronics store and asked for a hand full of Attiny chips, which cost about one Euro each. They gave me some Attiny25, with 2Kb of flash, 128 bytes of ram, and 6 output pins.

I figured that with 4 neighbors and a clock line, I’d have exactly enough pins. But I’d have to do bi-directional communication over a single wire. So I decided on a pull-up design.

For now I’ll use the Raspberry Pi for input and output of the grid, but there is no reason this task can’t be delegated to another AVR chip and some buttons and 7-segment displays.

After a day of hacking, I can program 2 Attinies to double a number and pass it on. To run the more advanced puzzles, I’ll need a better way to upload code and more Attinies. I have yet to implement the stack memory node as well.

This is the code the two node currently run. (hardcoded)

Command program[15] = {
  {MOVr, UP, ACC},
  {ADDr, ACC, 0},
  {MOVr, ACC, DOWN},
  {JMP, 0, 0}
};

When you send a number, this is what happens.

pi@raspberrypi ~ $ sudo python tis-console.py 
Number: 9
done sending
Result:  36

The source code is on Github

Hybrid Headphone Valve Amplifier

In university we currently learn about MOSFETs and BJTs, but valves(vacuum tubes) are completely absent from the material. Of course valves are essentially obsolete, and I’ve seen before that they try to avoid letting students work with hundreds of volts, but valves interest me.

I mean, how could you not be interested in glowing tubes with vacuum. The other part is that some audiophiles claim they produce superior sound. But sometimes audiophiles are full of bullshit, so I’d better verify all those claims for myself.

The plan

I might eventually actually use hundreds of volts on my valves, but for now the plan is to operate them at a low voltage and use modern transistors to create an output buffer.

I draw inspiration from this 12v headphone amp and this document about triodes at low voltages.

The point is that you can operate valves at low voltages, but they just provide very low current. So you need a separate output stage to drive anything.

The referenced designs use an A-class source-follower for the output. Some claim that even though they have terrible efficiency, can have less distortion than AB-class output stages. This will be tested of course.

Equations please

For MOSFETs and BJTs I learned how to use mathematical models to design amplifier stages. For valves, I could not find any such models. I asked the professor, and he referred me to the MOSFET equations in ohmic mode:

\[I_A= K \left( (V_{GK}-V_{th})V_{AK}-\frac{V_{AK}^2}{2} \right)\]

But with K and \(V_{th}\) not known, this is not very useful. I asked around a bit, and it seems that people just take the graphs from the datasheet and use them for biasing. But as said in the linked PDF, voltages in the datasheet range from 50 to 200 volt usually, so we’ll have to make our own graphs.

To do so, I used the NI MyDAQ arbitrary waveform generator, oscilloscope, and an XY-plot to plot the relation between grid voltage and anode current at several anode voltages.

12AU7 anode current against grid voltage

In this graph I plotted the anode current for anode voltages from 1v to 10v and grid voltages from -1v to 1v. Current never exceeds 2.2mA for 10v.

But valves are always biased with negative voltages, even though a positive voltage clearly gives more current. To see why, we need to look at grid current.

12AU7 grid current against grid voltage

As can be seen, for positive grid voltages, the control grid starts to leak current to the cathode, which is undesirable. But as described in the linked PDF, this current itself can be used to bias the valve.

Class war

I read a lot about the advantages and disadvantages of A-class, B-class and AB-class amplifiers, but most of it is not very scientific.

One thing is for sure, A-class uses a lot more power and is at best 25% efficient. When the output is at rest, a constant current is just dissipated as heat. When the output is low, that is the amount of current that can be drawn. But when the output is high, it needs to supply double that current, of which half is just burned and the other half flows into the output.

A more interesting and vague topic is distortion. It is claimed that distortion in A-class amps is mainly even harmonics and B-class distortion is mainly odd harmonics. Why this is the case and why the one would be superior is not frequently explained.

Why even harmonics are more pleasing is understandable. First, it is important to understand that harmonics drop off in amplitude very quickly, so you mostly hear the first few. If you look at the first harmonics, you see that the even ones are octaves, while the odd ones are fifths and thirds.

Octaves are just the same note, but twice as high. It seems reasonable to assume this sounds more pure and full than odd harmonics, which are other notes altogether.

But why, how, and by how much?

Equations please

The voltage-current relationship of a BJT is

\[I_D=C_0\left(e^{\frac{q}{Kt}V_{be}}-1\right)\]

If you squint a bit, you could say that the output current is an e-power of the input voltage. Taking a cosine for the input, we can formulate equations for A-class and B-class amps.

\[\begin{aligned} f_a(x)&=e^{cos(x)}\\ f_b(x)&=\frac{1}{2}(e^{cos(x)}-e^{-cos(x)}) \end{aligned}\]

To take a look at the harmonics we can calculate the Fourier series for these functions. Except the indefinite integral of this equation does not exist. So instead I kindly asked my CPU to perform the following integral numerically for me. (the sine coefficients are trivially 0 since \(f(x)=f(-x)\))

\[a_n=\frac{1}{\pi}\int^{\pi}_{-\pi} f(x)cos(nx) dx\]
Harmonics 1 2 3 4 5 6
A-class 1.1303 0.2714 0.0443 0.0054 0.0005 0
B-class 1.1303 0 0.0443 0 0.0005 0

From this you can clearly see that odd B-class harmonics are identical to the A-class harmonics. The interesting part is that the even harmonics are gone. They simply cancel out.

Of coures, you can build either class with very low distortion, but for the same biasing/distortion level, B-class will have less overall distortion, but concentrated entirely in odd harmonics. Meanwhile, A-class has a more balanced harmonic distribution, which might sound more pleasing if the overall distortion is low enough.

Initial design

Initially, I’m mostly going to use the design of the linked headphone amp and iterate based on that. There is one major change though.

While transistors operated in the saturated region don’t depend on drain/collector voltage, valve anode current depends linearly on anode voltage.

As described, the linked headphone amplifier uses a variable resistor that is tuned such that at rest the output voltage sits at the center of the available range, meaning there is only 6v across the valve instead of 12v.

Instead I used a current mirror so that there is a constant 11.4v across the valve. The current is then mirrored to the other BJT, where it drops over the resistor connected to the buffer to create the output signal.

Normal cathode biasing is used for now, but grid-leak biasing will also be tested later.

Initial amp design

AB-class design

The gain stage for this design is unchanged, but an AB-class output stage is added.

It turns out the input impedance of the output stage is \(1k//1k=500\Omega\), which created a nice current divider with the current mirror. So I had to add another follower stage as a buffer.

Initial amp design

Execution

I built the first design and after making sure it looks okay on the oscilloscope I connected it to an electric guitar and an old pair of headphones, and it worked!

Initially I used a cheap wall wart to power the amplifier, but this gave a lot of noise and hum. Now I switched to an old PC power supply, which is not perfect either, but at least the 50Hz hum is gone.

For the A-class circuit the THD is about 1% to 2% depending on god knows what.

Amp output

The AB-class circuit does not have the nice space-heater output stage, and performs slightly worse than the A-class circuit with a THD of around 2% to 3%.

Interestingly, the harmonics are about the same. Both have strong even harmonics and nearly no odd harmonics. Whether these harmonics are caused by the gain or the output stage remains to be seen. So much for theory I guess.

Amp output

To be continued…

Theremin

I made another silly thing. I came across a musical instrument called a Theremin. The real deal uses a really cool concept where your body and 2 antennas act as a capacitor in an LC circuit. By changing the distance between your body and the antennas, the capacitance changes, which changes the resonance frequency and modifies the pitch and amplitude of the output signal.

This thing is much more silly. It uses the IR sensor to measure the distance to your hand and generates a tone based on that. It works nothing like the real thing.

The sound quality of the EV3 is terrible, because it’s just a small speaker attached to a PWM port with a low-pass filter.

The IR sensor is slow, inaccurate and discrete. So you can do none of the slides and vibrato you can do with a Theremin. At first I tried to smooth the input to get a more natural sound, but that made it even slower and impossible to tune. So in the end I mapped the discrete input steps to discrete notes, so that it at least sounds in tune. You still can’t play anything on it though.

I wrote the code for this in C on ev3dev. I use ev3c to talk to the sensors and libasound to generate the sound. This took a while to get working.

#include "ev3c.h"
#include <stdio.h>
#include <math.h>
#include <stdint.h>
#include "alsa/asoundlib.h"

static char *device = "default";                        /* playback device */
snd_output_t *output = NULL;
unsigned char buffer[800];                          /* some random data */

int main(void)
{
  int err;
  unsigned int i;
  snd_pcm_t *handle;
  snd_pcm_sframes_t frames;
  if ((err = snd_pcm_open(&handle, device, SND_PCM_STREAM_PLAYBACK, 0)) < 0) {
          printf("Playback open error: %s\n", snd_strerror(err));
          exit(EXIT_FAILURE);
  }
  if ((err = snd_pcm_set_params(handle,
                                SND_PCM_FORMAT_U8,
                                SND_PCM_ACCESS_RW_INTERLEAVED,
                                1,
                                8000,
                                1,
                                500000)) < 0) {   /* 0.5sec */
          printf("Playback open error: %s\n", snd_strerror(err));
          exit(EXIT_FAILURE);
  }

  const double interval = pow(2, 1.0/12.0);
  const double R=8000; // sample rate (samples per second)
  double F=440; // frequency of middle-C (hertz)
  double Fp = F;
  double V=127; // a volume constant
  double t; // doudle counter, yeaaaa


  //Loading all sensors
  ev3_sensor_ptr sensors = ev3_load_sensors();
  ev3_sensor_ptr prox1 = sensors;
  ev3_mode_sensor(prox1,0);
  ev3_open_sensor(prox1);
  while(1)
  {
    ev3_update_sensor_val(prox1);
    Fp = F;
    F=220*pow(interval, prox1->val_data[0].s32/4);
    t*=Fp/F; // scale time with frequency change
    // this is to maintain a continuous sine

    fprintf(stderr, "%d, %f\n", prox1->val_data[0].s32, F);
    
    for ( i=0; i<800; i++ ) {
      t+=1;
      buffer[i] = (sin(t*2*M_PI*F/R)+1)*V;
    }

    //printf("%d\n", snd_pcm_avail(handle));
    frames = snd_pcm_writei(handle, buffer, sizeof(buffer));
    if (frames < 0)
            frames = snd_pcm_recover(handle, frames, 0);
    if (frames < 0) {
            printf("snd_pcm_writei failed: %s\n", snd_strerror(frames));
            break;
    }
    if (frames > 0 && frames < (long)sizeof(buffer))
            printf("Short write (expected %li, wrote %li)\n", (long)sizeof(buffer), frames);
  }
  //Let's delete the list in the very end. It will also close the
  //sensors
  ev3_delete_sensors(sensors);
  snd_pcm_close(handle);
  return 0;
}
Published on