Oscilloscope trigger problem

Go To Last Post
15 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I have been trying to build a simple low frequency usb oscilloscope using only ATtiny45 (please don't laugh). Originally inspired by this project:

http://yveslebrac.blogspot.com/

I decided I would try to get a little more out of it by using only one channel at 8 bit resolution and implementing a custom class usb interface for better though-put.
The results achieved so far have been relatively pleasing, being able to monitor a 10khz sine wave at almost the same resolution I get from a soundcard based scope which far exceeds my initial expectations.

The problem I have is finding a reliable trigger source. My idea was to use TIMER0 set for an external clock then trigger the A/D conversion phase on a compare-match interrupt. Theoretically the internal edge detector should be accurate enough for my needs, however this doesn't seem to work very well in practice. Here's the code, note that I am using V-USB to interface with the host:

/* main.c */

#include 
#include 
#include 
#include      /* for _delay_ms() */
#include   /* for sei() */
#include    /* required by usbdrv.h */

#include "usbdrv.h"         /* for V-USB */
#include "oddebug.h" 

#define GET_ADDRESS_VALUE          0
#define SET_ADDRESS_VALUE          1
#define ADC_BUFFER_READ            2
#define ADC_BUFFER_SIZE            160

volatile uchar buffer[ADC_BUFFER_SIZE];
volatile uchar buffer_index = 0;
volatile uchar tx_complete = 0;

/* ------------------------------------------------------------------------- */

static void timerInit(void)
{
	/* Timer 0 Init */
	TCCR0A = 0x02;
	TCCR0B = 0x07;
	OCR0A = 0x10;
	/* Timer 1 Init */
	TCCR1 = 0x88;
	OCR1A = 0x10;
	OCR1C = 0x10;
	TIFR |= (1 << OCF1A);
	/* Timer Interrupts */
	TIMSK |= (1 << OCIE1A) | (1 << OCIE0A);
}

static void adcInit(void)
{
	ADCSRA = 0x85;                 // 1000 0101
	ADMUX  = 0x23;                 // 0010 0011
}

/* ------------------------------------------------------------------------- */

ISR(ADC_vect)
{
	cli();
	if(buffer_index < ADC_BUFFER_SIZE)
		buffer[buffer_index++] = ADCH;

	sei();
}

ISR(TIMER0_COMPA_vect)
{
	cli();
	TCNT1 = 0;
	TIMSK = (1 << OCIE1A);
	ADCSRA |= (1 << ADIE);
	sei();
}

ISR(TIMER1_COMPA_vect)
{
	ADCSRA |= (1 << ADSC);
}

/* ------------------------------------------------------------------------- */
 
uchar usbFunctionRead(uchar *data, uchar len)
{
	uchar i;
	if(len > buffer_index)
		len = buffer_index;

	for(i = 0; ibRequest)
	{
	case ADC_BUFFER_READ:
		if(buffer_index != ADC_BUFFER_SIZE)
			return 0;

		TIMSK &= ~(1 << OCIE1A);
		ADCSRA &= ~(1 << ADIE);
		return USB_NO_MSG;

	case GET_ADDRESS_VALUE:
		usbMsgPtr = (uchar *)((unsigned int)rq->wIndex.word);
		return 1;

	case SET_ADDRESS_VALUE:
		*(uchar *)((unsigned int)rq->wIndex.word) = rq->wValue.bytes[0];
		return 1;
	}
	return 0;
}

/* ------------------------------------------------------------------------- */

int main(void)
{
	wdt_enable(WDTO_1S);
	odDebugInit();
	usbInit();
	usbDeviceDisconnect();
	uchar i = 0; 
	while(--i)
	{
		wdt_reset();
		_delay_ms(1);
	}
	usbDeviceConnect();
	adcInit();
	timerInit();
	sei();
	for(;;)
	{
		wdt_reset();
		usbPoll();
		if(tx_complete == 1)
		{
			cli();
			if(TCCR0B == 0)
			{
				TCNT1 = 0;
				ADCSRA |= (1 << ADIE);
				TIMSK |= (1 << OCIE1A);
			}
			else {
				TCNT0 = 0;
				TIMSK |= (1 << OCIE0A);
			}
			tx_complete = 0;
			sei();
		}
	}
	return 0;
}

The strange thing is, if I increase OCR0A to something like 64 or more the trigger begins to hold but at the expense of the resulting waveform becoming distorted. This I suspect is because it takes so long the initiate the conversions the capture is being interrupted by a subsequent usb poll from the host. I have proved this to some extent by slowing the host-side polling. The thing I find really peculiar is that although the waveform improves with slower polling, OCR0A requires an even higher value to reliably hold the trigger point and so the distortions re-appear.

I was wondering if anyone could offer some explanation and/or possible solution.
I realise that I could perform the triggering in software, host-side. I have experimented with this and it works quite well, however this is wasteful of my already RAM-limited buffer. Another reason I wanted a hardware trigger was to facilitate 'down-sampling' but that's my next challenge assuming I can get this to work.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Do you mean triggering to restart the waveform capture to get a stable display like the trigger controls on a real scope or a source to trigger conversions?

In the latter case, I assume you want to set the sampling frequency (or time per division), you can indeed use a compare match, see section on auto trigger sources. I would set the timer in CTC mode, it will clear the counter on compare match and trigger the ADC without any software intervention. You won't need your ISR. Just vary the compare value and prescaler to change the timebase.

And by the way, it's not necessary to have cli() and sei() at the beginning of each ISR. Interrupts are disabled anyway ;) Also, there is also no need to reenable interrupts as this is done when the ISR has completed.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Quote:
Do you mean triggering to restart the waveform capture to get a stable display like the trigger controls on a real scope
Yes, that's exactly what I am trying to do.
Quote:
or a source to trigger conversions?

Actually I find the free running ADC mode better results at the highest frequencies. However you may notice TIMER1 is used in a similar way to which you describe for lower frequencies.

I should perhaps point out that I am able to set register values host-side via these lines in usbFunctionSetup. This enables my to switch time-bases etc.

	case GET_ADDRESS_VALUE:
		usbMsgPtr = (uchar *)((unsigned int)rq->wIndex.word);
		return 1;

	case SET_ADDRESS_VALUE:
		*(uchar *)((unsigned int)rq->wIndex.word) = rq->wValue.bytes[0];
		return 1;

Thanks for your quick reply.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

In that way your device is very hackable :)

I would keep the ADC running at all times, and when the trigger condition occurs reset the index into the buffer and only then start writing into the buffer. When the buffer is full, set a flag. This flag is only reset when the PC has read out the buffer. This behaviour would like the 'normal' mode found on 'real' scopes. If you want to implement 'auto' mode, simulate a trigger event when a certain time has elapsed since the last trigger.

The code needed for this trigger system might limit the max. sampling rate though.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Thanks, I'll give that a go. Sorry I just read the last bit of your first post again.

Quote:
And by the way, it's not necessary to have cli() and sei() at the beginning of each ISR. Interrupts are disabled anyway Wink Also, there is also no need to reenable interrupts as this is done when the ISR has completed.
I didn't realise that. I guess that means I can't disable the interrupt from within the ISR. Don't think that's the problem here but I will re-work the code to avoid this.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

There is no point in disabling interrupts whilst being in an ISR as they have been already disabled by the AVR the moment it detected the trigger of interrupt.

But it won't hurt, it just wastes a cycle and a program word ;)

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

jayjay1974 wrote:
I would keep the ADC running at all times, and when the trigger condition occurs reset the index into the buffer and only then start writing into the buffer.
I would continuously write to the buffer (ring buffer). When the trigger triggers I would continue writing for some (configurable) number of samples, than stop to send the result.

If you chose that number smaller than the buffer size, you retain some samples from before the trigger, so you can look at some part of the signal before the trigger happened, which is often interesting.

If you chose that number larger than the buffer size, you have effectively a delayed trigger, which is also often interesting.

So by just preloading / fiddling with one number you get two features almost for free.

Stealing Proteus doesn't make you an engineer.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Quote:
I would keep the ADC running at all times, and when the trigger condition occurs reset the index into the buffer and only then start writing into the buffer.
Not using TIMER0 to enable the ADC interrupt but instead set a flag has improved matters immensely!
It's not perfect as for the highest sample rates I must disable ADC interrupts for the usb driver to operate correctly.
Quote:
I would continuously write to the buffer (ring buffer). When the trigger triggers I would continue writing for some (configurable) number of samples, than stop to send the result.
This sounds like an interesting idea. I originally had a circular/ring buffer implemented but I discarded it when I realised I was never going to achieve isochronos transfer.
I will try re-implementing it and see if it makes any difference.

Although the truth is I have probably reached the limits of what is possible with an eight bit eight legged uC I am still open to ideas for improvement.

TY All

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I would suggest that your external "Trigger" signal be connected to an Input Capture pin of the AVR, causing it to snapshot the current state of a counter/timer at (close to) the instant of trigger. All the while, the ADC's been running in continuous conversion mode, producing a sample of your input signal every
65uS at the maximum 200KHz clocking rate. As others have suggested, you've been assiduously keeping these samples in a ring buffer. When input-capture interrupt goes off, you consult the value frozen by the input capture register to discover the timing, relative to your sampling process, of when the trigger occurred, down to a resolution of 1/8 uS. That will let you fit the most recent batch of samples into the right "bins" of a larger record, so that with enough memory, you can achieve an "equivalent time" sampling rate equal to your AVR's clock frequency, if the sample/hold feature of the ADC is good enough (obviously, this process assumes a repetitive signal, as it builds up the final record over many trigger events.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Levenkay, I love this idea because that is what I meant in my initial post about downsampling, although 'equivalent time' sampling is the correct term I have since learned. I have re-read your post a dozen or more times today but I still can't quite get it to gel. I understand it all pretty much up to this bit.

Quote:
That will let you fit the most recent batch of samples into the right "bins" of a larger record

Also, I assume for this to work the timer would need to be running at the same speed as the ADC clock? or is this not necessary?

There are other issues to consider in this case, I can't find any reference to input-capture on the tiny45 maybe I missed it but even then the RAM is at its limits for an adequate sample buffer.
The 'equivalent time magic' I was hoping to perform host-side.

Thank you for your input.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

The T45 does not have ICP because it lacks a 16 bit timer. You better select another AVR with more memory too if you need it.

Basically this ET magic is based on very accurately shifting the sampling interval relative to the trigger point in very small time steps.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Let's say you wanted to build up a record of a portion of a repetitive
signal from three milliseconds before some triggering event, to two
milliseconds after the event, for five milliseconds total duration.

An AVR clocked by the traditional 8MHz crystal will digitize one 10-bit
sample every 65us, at the maximum recommended 200KHz A/D clock. Over
that 5ms window, the AVR would take 5000/65, or about 77 samples. Out
of a love of nice round numbers, let's set up a 128-sample ring buffer.
Run the A/D in continuous-conversion mode, and set up an interrupt service
routine for each conversion. The job of that routine would be roughly:

volatile int triggerPhase;

   
ISR(TIMER1_CAPT_vect)
{
    static bool triggerAccepted;

    record[ringpt] = ADCW;
    ringpt = (ringpt + 1 ) & 0x7f;

    // Do we need to ignore a trigger event because we haven't
    // acquired enough "before the trigger" information?
    //
    if (0 < --preTriggerSamples)
    {
        TIFR = (1 << ICF1);  // Discard trigger
        triggerAccepted = false;
    }
    else
    {
        // Capture the first acceptable trigger, and record
        // how long ago it happened
        //
        if (!triggerAccepted && (TIFR & (1 << ICF1) )
        {
            triggerPhase = TCNT1 - ICR1
            triggerAccepted = true;
        }


        // picture of what happens after the trigger?
        //
        if (triggerAccepted && (0 <= --postTriggerSamples) )
        {
            // Stop A/D converter; disable further interrupts
            //
            ADCSRA &= ~( (1 << ADEN) | (1 << ADIE) );
        }
    }
}

At some previous time, you decided to initiate an acquisition cycle by
initializing the pre- and post-trigger sample counters, and enabling
and starting the A/D converter. Then, you sat back and waited for it
to turn itself off. When it does, you know that the last N samples
(the sum of the counters) represent a contiguous record of the input
signal, sampled every 65uS.

Now, suppose the ultimate "picture" of this five millisecond interval
will be a record of a sequence of samples at a much finer resolution
than one every 65us. Without fancier hardware, let's shoot for samples
taken at the AVR's 8MHz clock rate. So the 5ms picture we're trying to
paint will have 40 thousand samples in it, whereas the ADC conversion-
complete interrupt will only supply about 80. However, it's very unlikely
that the signal we're trying to reconstruct is precisely synchonized with
the AVR's 8MHz clock. So, from one trigger to the next, we get a batch
of 80 consecutive samples, spaced 520 of our ultimate 8MHz samples apart,
at a hopefully random time within one 520-sample-long conversion cycle.
If we're patient, and acquire few thousand or so of these batches of 80
samples, we'll find a batch that includes a sample that lands right on
our trigger, and a batch that includes a sample taken one AVR clock
(1/8 us) after the trigger, and a batch that includes a sample taken two
AVR clocks after the trigger, and so on... Enough to fill in all 520
of the sub-intervals our clocking can establish between the ADC conversions.

The code fragment above does have a problem, though, in that it assumes
that the time between when the ADC conversion completed and the time at
which the trigger occurred can be computed by simply subtracting the
input-capture time from the current state of the running timer. That's
close to being correct, but it will unfortunately include the AVR's
interrupt-response latency, which is not always constant. (especially
if your application ever disables interrupts! :) ). I haven't the time
to work out an exact solution, but I'm pretty sure it would yield to
treatment.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

thanks for taking the time to explain in so much detail, the concept is much clearer to me now. However, as jayjay points out I'm probably gonna need a bigger AVR to do this in firmware.
I think if I was going to invest much more time/money on this project I would consider using FPGA and build myself a really decent PC Scope.

As it goes though I am pretty happy with what I've got now, following jayjay's advice to keep the ADC running at all times, my original trigger idea is working suprisingly well.
I managed to streamline the code enough to allow for this even during usb transfer. The only small problem I have left is knowing for sure when this transfer has completed. I've posted this question on the obdev forums but am still awaiting a satisfactory solution.
If anyone here has any experience of V-USB and knows the answer I would love to here from you.

Thank you all once again for your help, this has been very educational.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Quote:

I think if I was going to invest much more time/money on this project I would consider using FPGA and build myself a really decent PC Scope.

What kind of speeds are you aiming for?

Consider that with an Xmega you can get two channels of 12-bit ADC at 1Msps, and for most (nearly all?) 'scope work 8 bits would be fine. DMA into twin buffers gives (comparatively) lots of time to sample. Double-buffers that should be able to hold a whole "screenful" of samples. Various triggering options.

An ATXMEGA128A1 is about $8 in qty. 1. That's only about $2 more than a Mega32 without the features above.

The input ranging and signal conditioning and protection needs to be done in any case, so that is a wash.

Add an inexpensive graphics LCD display and Bob's your uncle.

You can put lipstick on a pig, but it is still a pig.

I've never met a pig I didn't like, as long as you have some salt and pepper.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Sounds interesting, I will keep that one in mind. I have been sticking with smaller DIL devices up to now for easy soldering.

The reason I mention FPGA is because I have been considering buying one of these for some time:

http://www.fpga4fun.com/Hands-on_Flashy.html

Perhaps there are similar devel boards available with the Xmega ready soldered on?