Dual channel El Cheapo pulse generator

From Electriki
Revision as of 22:10, 30 December 2008 by Michai (talk | contribs)
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.
Fig.2: 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.

Hardware

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

Workhorse

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'.

Safety-stuff

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.

Not exactly a safety-thingie but useful nevertheless: In case of framing error (i.e. serial connector making bad contact, host unplugged, etc), the device halts, and blinks an error-LED. This is a matter of taste, I guess, but ok.

Software

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.

MCU-software

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

RXC_ISR()
{
  sample_byte = receive_from_serial();
  num_remaining_sample = 4;
  tmr = T_first;
  enable_timer_interrupt();
}

Pseudo-code for the timer-handler:

TMR_ISR()
{
  bit_for_chan_A = sample_byte & 1;
  bit_for_chan_B = !!( sample_byte & 1 );
  output_dual_channel_sample( bit_for_chan_A, bit_for_chan_B );
  if ( --num_remaining_sample ) {
    tmr = T_next_sample;
  }
  else {
    disable_timer_interrupt();
  }
}

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)

= 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 Cconsise 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
enddef



def BlinkOnce

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

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

enddef



def Blink5Times
  rep 5
    do BlinkOnce
  endrep
enddef

def OutputSingleGrayCycle

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

  hi 0
  emit

  hi 1
  emit

  lo 0
  emit

  lo 1
  emit

enddef



def OutputAllGrayCycles
  rep 1000
    do OutputSingleGrayCycle
  endrep
enddef

def main

  do Blink5Times

  do OutputAllGrayCycles

  rep
    do BlinkOnce
  endrep

  # (never reaches here)

enddef

So... what does it look like?

Well, see fig.2 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 ;-)

Drawbacks

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