Dual channel El Cheapo pulse generator

From Electriki
Jump to navigationJump to search

So, what is this all about?

Fig.1: schematic. An ATtiny2313 receives a constant bitstream from a host-PC, and outputs the next bit/sample on each of 2 output-channels at fixed intervals. Note: this schematics is actually WRONG (wrong lines for statusLED/A/B outputs); if I ever happen to put source online, there will be a mismatch.
Fig.2: beer. Drink to forget? Perhaps drink to forget the fact we didn't have a scope then yet, so for tuning timing-stuff, trip to work was required.
Fig.3: old and new board. New one (left) still lacked clamp-diodes and series-resistors at time of pic-taking, mainly because of CBA.

For some thing at work it was useful/necessary to generate a not-too-tast, not-too-perfect yet arbitrary long and complex 2-channel digital signal. The thing described here is not that interesting, but can be built in a jiffy (2.5 hours by yours truly, starting from scratch :-) and is really cheap.

The device itself acts as a dumb interpreter/timer of an incoming headerless bitstream (2 bits per sample, ad inifinitum), and outputs samples on each of 2 output-channels (A and B) at fixed intervals.


Apart from a fairly uninteresting LM317 regulator and an equally boring Maxim MAX232 level-converter, there is...


Workhorse (although it's burning cycles most of the time) is the faithful ATtiny2313 (although the schematic in fig.1 shows an AT90s2313 there). Input-bitstream comes from a RS232 connection from a host-PC, although a 'self-test' jumper can be closed to have it feed a nice bitstream to itself - mainly useful to test output-levels and -timing. Samples are output as 0V (low) or 5V (high) on 2 pins 'A' and 'B'.


This is the 3rd or even 4th unit I had the honour of soldering together; the other ones blew up, quite literally. Looking at fig.1, a diode after the 24VDC-terminal can be seen, as well as 2 clamp-diodes for each channel. In addition, a 4k7 resistor is placed after the clamp-diodes. This prevents damage in case of short-circuit or connecting an output to the gnd- or 24V-rail.


This is the less-boring part, perhaps. Software consists of a real-time part (inside the MCU) and a piece of software on PC that basically feeds the device with a bitstream. The latter can be your average TeraTerm.EXE, or it can be a custom interpreter/generator - I chose the latter.


The bitstream from the PC arrives in an 10-bit frame (startbit + 8N1) @ 115k2. Only 8 useful bits are present in each frame. Since a sample-(pair) consists of 2 bits (1 for each channel, to be output simultaneously), this gives us a sample-rate of ( 115200 / 10 ) * 4 ) ~ 46 kHz = freq_sample. This is not just a Good Idea - it's a fact of life, and follows directly from sample-size (2 bits), frame-layout (10 bits) and serial bit-time ( 1 / 115200 ). We can only output samples at freq_sample Hz using this simple 'bitstream'-method, period.

Therefore, 1 sample-period = ( 1 / freq_sample ) ~ 21.7 usec. Bytes will be arriving (assuming there is no delay between serial frames, i.o.w. each stop-bit is immediately followed by the start-bit for the next frame) at ( (1 / 115200 ) * 10 ) ~ 86.8 usec = ( 1 / freq_frame ). This makes sense, since freq_sample = ( 4 * freq_frame ), since there are 4 samples in a frame. Lovely.

The software will basically do the following:

wait for a byte, then, 4 times in a row, snoop and output the next 2 sample-bits from the byte at freq_sample. After that, the cycle repeats when a new byte comes in.

To do this, 2 interrupts -'Rx-complete' ('RXC') and a timer - are used. The timer interrupt is normally disabled, but when enabled, is executed when a user-writable, but automatically incrementing timer-register tmr_count overflows/wraps.

Pseudo-code for the RXC-handler:

  // Receive next 4 samples, and prepare to output them
  sample_byte = receive_from_serial();
  num_remaining_sample = 4;
  tmr_count = T_first;

  // This is the only location where timer-interrupt is enabled

Pseudo-code for the timer-handler:

  // Snoop the next 2 bits off the input
  bit_for_chan_A = sample_byte & 1;
  bit_for_chan_B = !!( sample_byte & 2 );
  sample_byte >>= 2;
  output_dual_channel_sample( bit_for_chan_A, bit_for_chan_B );
  // Either prepare for 1 more sample, or stop output (if we aready output 4 samples)
  if ( --num_remaining_sample ) {
    tmr_count = T_next_sample;
  else {
    // This is the only location where timer-interrupt is disabled

An that's basically all. Meanwhile, the main()-routine is keeping itself busy by stuffing 0x78-bytes our of its serial port (which give a fairly interesting gray'ish pattern when viewed on a scope). When aforementioned jumper is set to test-mode, this pattern will be output on channels A and B.

PC-software (interpreter/generator)

On the PC-side, I chose to implement a simple interpreter for a simple macro-oriented ad-hoc pattern definition language, with as only method of output to 'emit' the 'current sample-value' to channels A and B simultaneously. A 'program' basically manipulates this 'current sample value', waits, and emits it. There are no conditionals (they make little sense anyway, since there is no input, and there exist no variables outside the 'current sample-value' register). Interpreting a constant program always results in the same output being generated. This is Good.

Pattern-definition language grammar

Fuzzy BNF-grammar of the 'language' is as follows:

Program       ::= Macro*
Macro         ::= 'def' NL Statement* 'enddef' NL
Statement     ::= Hi | Lo | Emit | MacroInstance | Repetition
Hi            ::= 'hi' <pin> NL
Lo            ::= 'lo' <pin> NL
Emit          ::= 'emit' [<count>] NL
MacroInstance ::= 'do' <macro_name> NL
Repetition    ::= 'rep' [<count>] NL Statement* 'endrep' NL
NL            ::= '\n'

Actually I CBA to think of a name for the 'language'; Pattern Definition Format sounds nice, but somehow I think I saw that before already, hmm... Perhaps Pattern Notation Grammar is something? Or what about Consise Pattern Presentation? Hmmm, no, no, no. Oh well. :-)

An example pattern-definition

Behold, a simple program to do the following:

  • blink LED on channel A 5 times @ 1Hz,
  • output 1000 cycles of 'Ab', 'AB', 'aB', 'ab' (uppercase = channel-output high; lowercase = channel-output low),
  • blink the LED forever @ 1Hz.

Although this is a trivial example, it demonstrates the use of repetition, comments and (nested) macros. And yes, I happen to like whitespace. :-)

# Simple program to blink led, output pattern, and blink LED forever

def DelayHalfSec
  # Emit last sample for 23k periods @ 46kHz ~ 0.5 s
  emit 23000

def BlinkOnce

  # Turn LED at channel A on, and keep it on for a while
  hi 0
  do DelayHalfSec

  # Turn it off, and keep it off for a while
  lo 0
  do DelayHalfSec


def Blink5Times
  rep 5
    do BlinkOnce

def OutputSingleGrayCycle

  # (surrounding code makes sure that on entry, both channels are LOW)

  hi 0

  hi 1

  lo 0

  lo 1


def OutputAllGrayCycles
  rep 1000
    do OutputSingleGrayCycle

def main

  do Blink5Times

  do OutputAllGrayCycles

    do BlinkOnce

  # (never reaches here)


So... what does it look like?

Well, see fig.3 for an idea. 24VDC, gnd and both channels A and B can be connected using screw-terminals. Serial DE-9 is glued onto the board using superglue. The underside (not shown here) is just solder and copper; best kept on a non-conducting desk ;-)

And children, ...


..., by whatever means necessary. See fig.2 for how much fun it is to change 1 delay-parameter, then going to work (where they do have a scope), seeing which one works (by swapping labeled MCU's in and out of the socket), if it works, remember which one worked, go home, adjust code, redo from start. </rant!> :-) But ok, got a scope now, finally.


2 things that are sub-optimal perhaps are...

  • samples are output at discrete intervals (46 kHz); i.e. it's impossible to make a clean 40 kHz pattern
  • there should not be gaps in between serial frames. A good old 16550A with a 4k Tx-buffer on a non-ancient PC should have no problems with this; however, I've seen USB-to-serial converters screw this up on 2 occasions. Depending on the application this might or might not be a problem.

Have fun -- Michai