Difference between revisions of "826 counters"

From Sensoray Technical Wiki
Jump to: navigation, search
(Output pulse every N encoder pulses)
(Using interrupts with encoders: +section: Interrupts on IX edges)
Line 428: Line 428:
 
   default:
 
   default:
 
     printf("Error detected: errcode=%d", errcode);
 
     printf("Error detected: errcode=%d", errcode);
 +
}
 +
 +
====Interrupts on IX edges====
 +
 +
Sometimes it's useful to be able to automatically call a function once per encoder revolution. This is easily done in cases where the encoder sends an Index signal to the counter's IX input. The next example shows how to generate interrupts on every IX rising edge.
 +
 +
// Interrupt upon rising edges on counter's Index input.
 +
 +
uint bd = 0;                    // board number
 +
uint ch = 0;                    // counter channel number
 +
uint counts, tstamp, reason;    // snapshot info
 +
 +
S826_CounterModeWrite(bd, ch, S826_CM_K_QUADX4);                            // Configure counter
 +
S826_CounterSnapshotConfigWrite(bd, ch, S826_SS_ALL_IXRISE, S826_BITWRITE); // Enable snapshots upon index rising edges
 +
S826_CounterStateWrite(bd, ch, 1);                                          // Start tracking encoder
 +
// Repeat forever:
 +
while (S826_CounterSnapshotRead(bd, ch, &counts, &tstamp, &reason, S826_WAIT_INFINITE) == S826_ERR_OK) {  // Wait for next Index pulse
 +
  ProcessIndexPulse();  // TODO: HANDLE INTERRUPT
 
  }
 
  }
  

Revision as of 16:13, 2 December 2021

This page contains application notes and other information about Model 826 counters, which can be used as incremental encoder interfaces, counters, timers, and in a wide variety of other applications.

For other 826-related topics, please go to the top-level 826 page.

Contents

Connector pinouts

Reading counts

There are two ways to read the counts from a counter. The simplest way is to directly read the counter:

uint counts;
S826_CounterRead(0, 0, &counts);          // Fetch the current counts from counter 0.
printf("Counts = %d\n", counts);          // Display the counts.

Often it's necessary to know exactly when the counts were sampled. To do this under software control, you must either read the counter at an exact moment in time or record the exact time when the counter is read. Fortunately there's a much easier and more accurate way: trigger a snapshot and then, at your leisure, read the simultaneously captured counts and timestamp — accurate to within 1 µs:

uint c, t;      // simultaneously-sampled counts and timestamp

S826_CounterSnapshot(0, 0);                        // Trigger a snapshot on counter 0.
S826_CounterSnapshotRead(0, 0, &c, &t, NULL, 0);   // Fetch the snapshot.
printf("Counts = %d at timestamp = %d\n", c, t);   // Display counts and timestamp.

The first method bypasses the snapshot FIFO and thus allows you to immediately read the current counts at any time. However, when using the second method, note that the FIFO may not be empty when you trigger the snapshot. In such cases, you must read all previous snapshots from the FIFO before you can read the one you just triggered. If you're not sure the FIFO was empty and you don't care about previous snapshots, you can use the following code to discard all previous snapshots and receive the one you just triggered:

uint c, t;      // simultaneously-sampled counts and timestamp

S826_CounterSnapshot(0, 0);                                              // Trigger snapshot.
while (S826_CounterSnapshotRead(0, 0, &c, &t, NULL, 0) == S826_ERR_OK);  // Fetch the newest snapshot.
printf("Counts = %d at timestamp = %d\n", c, t);                         // Display counts and timestamp.

Jamming counts into a counter

To programmatically load a particular counts value into a counter, write the value to the counter's preload0 register and invoke a soft counter preload:

// Jam a value into a counter.
void JamCounts(uint board, uint ctr, uint value)
{
  S826_CounterPreloadWrite(board, ctr, 0, value);   // Write value to preload0 register.
  S826_CounterPreload(board, ctr, 1, 0);            // Copy preload0 to counter.
}

JamCounts(0, 3, 1234);  // Example: Jam 1234 into counter3 of board0.

Event counter

A common counter application is counting external events, where each event is indicated by a pulse. To implement this, connect the TTL/CMOS pulse signal to the counter's ClkA+ input. The following code shows how to configure a counter to tally incoming pulses.

// Configure board0, counter0 as a pulse counter and start it running.
S826_CounterModeWrite(0, 0,            // Configure counter:
  S826_CM_K_ARISE);                    //   clock = ClkA rising edge
  S826_CM_PX_START);                   //   preload when counter enabled.
S826_CounterPreloadWrite(0, 0, 0, 0);  // Zero counts upon preload.
S826_CounterStateWrite(0, 0, 1);       // Start the pulse counter running.

While the pulse counter is running, you can read the event count as explained in Reading counts.

How to use interrupts

I see that counters have an "Interrupt System (IRQ)" signal; how do I use this?

An interrupt request (IRQ) is issued when a counter event (e.g., Compare register match) is detected and information about the event (a "snapshot") is stored in the counter's FIFO buffer. Counter IRQs are transparently managed by the S826_CounterSnapshotRead function; this API function enables/disables interrupts and handles IRQs as required so that developers need not be concerned with the complexities of interrupt programming.

To wait for the next IRQ, simply call S826_CounterSnapshotRead. The function will return when an event snapshot is available, and will block and allow other threads to run while the event FIFO is empty. See the following sections for examples of how this works.

Interrupt performance

A number of factors affect how quickly S826_CounterSnapshotRead returns following an IRQ. Real-time operating systems typically provide short, predictable delays, whereas other operating systems cannot guarantee performance. In the latter case (e.g., Linux, Windows), you may be able to speed up responsiveness by increasing the priority of the calling thread.

Canceling an interrupt wait

Sometimes it's necessary to force S826_CounterSnapshotRead to unblock and return before an IRQ occurs. The calling thread can do this itself by specifying a time limit for the block. But what if a different thread needs to force the unblock? For example, suppose threadA is blocked by S826_CounterSnapshotRead (because it's waiting for an IRQ) and threadB must force threadA to unblock. There are several ways for threadB to accomplish this:

  • Call S826_CounterWaitCancel. This will cause S826_CounterSnapshotRead to immediately return S826_ERR_CANCELLED to threadA.
  • Call S826_CounterSnapshot. Note that this doesn't actually cancel the block; instead it triggers a snapshot and generates an IRQ. This will cause S826_CounterSnapshotRead to immediately return S826_ERR_OK to threadA, with trigger flag S826_SSRMASK_SOFT set to indicate the snapshot was triggered by software.
  • Call S826_SystemClose. This will cause threadA (and all other threads blocked by the API) to immediately become unblocked; all blocking functions will return S826_ERR_SYSCLOSED.

Time delay

A counter can be used to delay software execution for a specific amount of time. To implement this, preload the counter with the desired delay time and configure it to capture a snapshot upon counting down to zero. For example, the following function creates a delay timer with 1 µs resolution (because it uses the 1 MHz internal clock; for 20 ns resolution, use the 50 MHz internal clock):

// Create a delay timer and start it running.
int CreateDelay(uint board, uint ctr, uint delay)  // delay specified in microseconds
{
  S826_CounterModeWrite(board, ctr,                // Configure counter mode:
    S826_CM_K_1MHZ |                               //   clock source = 1 MHz internal
    S826_CM_UD_REVERSE |                           //   count down
    S826_CM_PX_START |                             //   preload delay counts upon counter enabled
    S826_CM_TD_ZERO);                              //   stop counting when counts reach 0
  S826_CounterPreloadWrite(board, ctr, 0, delay);  // Delay time in microseconds.
  S826_CounterSnapshotConfigWrite(board, ctr,      // Configure snapshot triggering:
      S826_SSRMASK_ZERO |                          //  capture snapshot when counts reach 0
      (S826_SSRMASK_ZERO << 16),                   //  disable snapshots when counts reach 0
      S826_BITWRITE);
  return S826_CounterStateWrite(board, ctr, 1);    // Start the delay timer running.
}

After starting the delay timer, call S826_CounterSnapshotRead to wait for the time interval to elapse:

int WaitForDelay(uint board, uint ctr)
{
  return S826_CounterSnapshotRead(board, ctr,   // Block while waiting for timer:
    NULL, NULL, NULL,                           //  ignore snapshot counts, timestamp and reason
    S826_WAIT_INFINITE);                        //  never timeout
}

For example, the following code will block the calling thread for 0.5 seconds while other threads are allowed to run, using counter0 on board number 0:

CreateDelay(0, 0, 500000);           // Create 0.5 second delay timer and start it running.
WaitForDelay(0, 0);                  // Wait for delay time to elapse.
printf("Delay time has elapsed!");

If desired, the calling thread can do some work while the delay timer is running:

CreateDelay(0, 0, 500000);           // Create 0.5 second delay timer and start it running.
DoSomeWork();                        // Do some work while the timer runs.
WaitForDelay(0, 0);                  // Wait for remaining delay time (if it hasn't already elapsed).
printf("Delay time has elapsed!");

In the above example, WaitForDelay may or may not block depending on how long it takes for DoSomeWork to execute. If DoSomeWork takes more than 0.5 s to execute, WaitForDelay will return immediately without blocking.

The next example performs work while the delay timer is running, but it doesn't block at all. Instead, it simply polls the timer.

CreateDelay(0, 0, 500000);           // Create 0.5 second delay timer and start it running.
while (S826_CounterSnapshotRead(0, 0, NULL, NULL, NULL, 0) == S826_ERR_NOTREADY) {
  DoSomeWork();                        // Do some work while the timer runs.
}
printf("Delay time has elapsed!");

Periodic timer

A counter can be used as a periodic timer, which is useful for periodically executing software or triggering ADC conversions. To implement this, configure the counter so that it repeatedly counts down to zero and then preloads (see examples below). The preload value and clock frequency determine the period. The following functions create a timer having 1 µs resolution because they use the 1 MHz internal clock; for higher resolution (20 ns), use the 50 MHz internal clock.

Hardware timer

The following function will cause a counter to periodically pulse its ExtOut output. The pulses can be internally routed to the ADC and used to trigger conversion bursts as shown in this example. If desired, ExtOut may be routed to a DIO for external use, but note that the output pulses are always 20 ns wide regardless of the input clock frequency (for longer pulses, use a PWM generator).

// Configure a counter as a 20 ns pulse generator and start it running.
int CreateHwTimer(uint board, uint chan, uint period) // period in microseconds
{
  S826_CounterModeWrite(board, chan,                // Configure counter mode:
    S826_CM_K_1MHZ |                                //   clock source = 1 MHz internal
    S826_CM_PX_START | S826_CM_PX_ZERO |            //   preload @start and counts==0
    S826_CM_UD_REVERSE |                            //   count down
    S826_CM_OM_NOTZERO);                            //   ExtOut = (counts!=0)
  S826_CounterPreloadWrite(board, chan, 0, period); // Set period in microseconds.
  return S826_CounterStateWrite(board, chan, 1);    // Start the timer running.
}

Software timer

This function configures a counter to capture snapshots at regular intervals:

// Configure a counter as a periodic software timer and start it running.
int CreateTimer(uint board, uint chan, uint period)   // period in microseconds
{
  S826_CounterModeWrite(board, chan,                // Configure counter mode:
    S826_CM_K_1MHZ |                                //   clock source = 1 MHz internal
    S826_CM_PX_START | S826_CM_PX_ZERO |            //   preload @start and counts==0
    S826_CM_UD_REVERSE);                            //   count down
  S826_CounterPreloadWrite(board, chan, 0, period); // Set period in microseconds.
  S826_CounterSnapshotConfigWrite(board, chan,      // Generate IRQ when counts==0.
      S826_SSRMASK_ZERO, S826_BITWRITE);
  return S826_CounterStateWrite(board, chan, 1);    // Start the timer running.
}

While the timer is running, a thread can call the following function to wait for the next snapshot. Other threads are allowed to run while S826_CounterSnapshotRead() blocks the calling thread, resulting in efficient, event-driven operation.

// Wait for the next timer period. Returns 0=ok, else=826 error code.
int WaitForTimer(uint board, uint chan)
{
  return S826_CounterSnapshotRead(board, chan,      // Block until next period.
      NULL, NULL, NULL,  // ignore snapshot values
      S826_WAIT_INFINITE);
}

Call a function periodically

How can I use a counter to periodically call a function?

First, create a timer as shown above. For example, the following code creates a 10 millisecond timer using counter0:

CreateTimer(0, 0, 10000);   // Create 10 ms (10000 us) periodic timer and start it running.

Now run the following loop, which will execute PeriodicFunction at 100 Hz. Note that other threads can do useful work when PeriodicFunction is not executing.

// Repeat while no errors detected:
while (!WaitForTimer(0, 0)) {   // Block until next timer period.
  PeriodicFunction();           // Execute the periodic function.
}

Alternatively, if you need to perform other processing while waiting for the next period and cannot use another thread to do so, you can poll the timer by calling S826_CounterSnapshotRead with a zero timeout:

while (1) { // Repeat forever:
  uint errcode = S826_CounterSnapshotRead( // Poll to see if period has elapsed (don't block).
      0, 0, NULL, NULL, NULL, 0);
  if (errcode == S826_ERR_OK)              // If it's a new period
    PeriodicFunction();                    //  execute the periodic function.
  else if (errcode == S826_ERR_NOTREADY)   // Else if not yet a new period
    DoSomeOtherStuff();                    //  do other processing.
  else                                     // Else wait was cancelled or error detected, so
    break;                                 //  exit the polling loop.
}

Frequency counter

The frequency of an external digital signal can be precisely measured using just one counter channel. To do this, connect the external signal to the ClkA+ input and route the internal tick generator to the Index input. The selected tick signal is used as a counting gate and it also determines the sampling rate and measurement resolution. The sampling rate is equal to the tick frequency, whereas resolution is inversely proportional to the tick frequency.

The following function shows how to configure a counter for frequency measurement.

// Configure a frequency counter and start it running.
int CreateFrequencyCounter(uint board, uint chan, uint ticksel)
{
  if ((ticksel < S826_CM_XS_R1HZ) || (ticksel > S826_CM_XS_1MHZ))
    return S826_ERR_S826_ERR_VALUE;               // Abort if invalid tick selector.
  S826_CounterModeWrite(board, chan,              // Configure counter:
    S826_CM_K_ARISE |                             //   clock = ClkA (external digital signal)
    ticksel |                                     //   route tick generator to Index input
    S826_CM_PX_IXRISE);                           //   preload upon tick rising edge
  S826_CounterPreloadWrite(board, chan, 0, 0);    // Reset counts upon tick rising edge.
  S826_CounterSnapshotConfigWrite(board, chan,    // Acquire counts upon tick rising edge.
      S826_SSRMASK_IXRISE, S826_BITWRITE);
  return S826_CounterStateWrite(board, chan, 1);  // Start the frequency counter running.
}

While the frequency counter is running, a thread can call the following function to wait for the next sample. Other threads are allowed to run while it waits for the next sample to arrive.

// Receive the next frequency sample.
int ReadFrequency(uint board, uint chan, uint *sample)
{
  return S826_CounterSnapshotRead(board, chan,      // Block until next sample
      sample, NULL, NULL,                           //   and then receive accumulated counts.
      S826_WAIT_INFINITE);
}

For example, the following code will measure frequency 10 times per second using counter0 on board 0. Note that the first sample is discarded as it may not be valid.

// Measure frequency 10 times per second.
uint counts;
CreateFrequencyCounter(0, 0, S826_CM_XS_10HZ);  // Create frequency counter and start it running.
ReadFrequency(0, 0, NULL);                      // Discard first sample.
while (!ReadFrequency(0, 0, &counts)) {         // Wait for next sample; if no errors then
  printf("Frequency = %d Hz\n", counts * 10);   //   display measured frequency.
}

Routing a counter output to DIO pins

The following function will route a counter's ExtOut signal to a DIO pin, which is useful when a physical output is needed (e.g., PWMs, one-shots).

int RouteCounterOutput(uint board, uint ctr, uint dio)
{
  uint data[2];      // dio routing mask
  if ((dio >= S826_NUM_DIO) || (ctr >= S826_NUM_COUNT))
    return S826_ERR_VALUE;  // bad channel number
  if ((dio & 7) != ctr)
    return S826_ERR_VALUE;  // counter output can't be routed to dio

  // Route counter output to DIO pin:
  S826_SafeWrenWrite(board, S826_SAFEN_SWE);        // Enable writes to DIO signal router.
  S826_DioOutputSourceRead(board, data);            // Route counter output to DIO
  data[dio > 23] |= (1 << (dio % 24));              //   without altering other routes.
  S826_DioOutputSourceWrite(board, data);
  return S826_SafeWrenWrite(board, S826_SAFEN_SWD); // Disable writes to DIO signal router.
}

Each DIO is associated with a specific counter and can only be routed to that counter's ExtOut (see S826_DioOutputSourceWrite documentation for details). For example, counter3 can be routed to dio 3, 11, 19, 27, 35 or 43, or to any combination of these. This example shows how to route counter3's ExtOut signal to both dio3 and dio11:

RouteCounterOutput(0, 3, 3);   // On board number 0, route counter3's ExtOut signal to dio3
RouteCounterOutput(0, 3, 11);  //   and also to dio11.

RouteCounterOutput is not thread-safe because it uses an unprotected read-modify-write (RMW) sequence to access and configure the DIO signal router. If multiple threads will be calling this function then a mutex should be used to ensure thread safety.

One-shot

A counter can be used to implement a "one-shot" that outputs a pulse when triggered. The trigger can be an external signal (applied to the counter's IX input) or another counter's ExtOut signal. The one-shot can also be triggered by software by invoking a counter preload. The output pulse appears on the counter's ExtOut signal, which can be routed to a DIO pin and/or another counter's Index input, if desired. The following function will configure a counter as a one-shot, with pulse width (ontime) specified in microseconds.

// Configure a counter as as a one-shot -----------
// trig specifies the trigger source: 0 = external (IX input); 2-7 = counter[trig-2] ExtOut.
int CreateOneShot(uint board, uint ctr, uint trig, uint ontime)
{
  S826_CounterModeWrite(board, ctr,                // Configure counter:
    S826_CM_K_1MHZ |                               //   clock = internal 1 MHz
    S826_CM_UD_REVERSE |                           //   count down
    trig |                                         //   route trig to Index input
    S826_CM_PX_IXRISE |                            //   preload @ trig rising edge
    S826_CM_TE_PRELOAD |                           //   enable upon preload
    S826_CM_TD_ZERO |                              //   disable when counts reach 0
    S826_CM_OM_NOTZERO);                           //   ExtOut = (counts!=0)
  S826_CounterPreloadWrite(board, ctr, 0, ontime); // Pulse width in microseconds.
  return S826_CounterStateWrite(board, ctr, 1);    // Enable the 1-shot.
}

The above function can be used to implement a retriggerable or non-retriggerable one-shot. These examples show how to create a 700 µs externally-triggered one shot using counter0, with output on dio0:

// Implement a retriggerable one-shot.
RouteCounterOutput(0, 0, 0); // Route one-shot (counter0) output to dio0.
CreateOneShot(0, 0, 0, 700); // Create 700 us retriggerable one-shot.
// Implement a non-retriggerable one-shot.
RouteCounterOutput(0, 0, 0); // Route one-shot (counter0) output to dio0.
CreateOneShot(0, 0,          // Create 700 us non-retriggerable one-shot.
  S826_CM_NR_NORETRIG,
  700);

Period measurement

A counter can be used to measure the period of a digital signal applied to its IX+ input. It does this by counting internal clocks during one complete cycle of the input signal. At the end of each cycle, the accumulated counts are captured in a snapshot and the counter is zeroed to start the next measurement. Since a new measurement begins at the end of each input cycle, the sampling rate will equal the signal's input frequency.

The signal's period is indicated by the snapshot counts, so a higher clock frequency will result in a greater number of counts and higher resolution. In the example functions below, set hires=0 to select the 1 MHz internal clock (provides 1 µs resolution for periods up to approximately 71.6 minutes) or set hires=1 to select the 50 MHz internal clock (provides 20 ns resolution for periods up to approximately 1.43 minutes).

// Create a period counter and start it running.
int CreatePeriodCounter(uint board, uint ctr, int hires)
{
  S826_CounterModeWrite(board, ctr,             // Configure the period counter:
    (hires ?                                    //   select internal clock based on resolution:
      S826_CM_K_50MHZ :                         //     for high res (20 ns) use 50 MHz
      S826_CM_K_1MHZ) |                         //     for low res (1 us) use 1 MHz
    S826_CM_UD_NORMAL |                         //   count up
    S826_CM_XS_EXTNORMAL |                      //   connect index input to IX (external signal)
    S826_CM_PX_IXRISE);                         //   preload upon signal rising edge

  S826_CounterPreloadWrite(board, ctr, 0, 0);   // Preload 0 at start of each cycle.
  S826_CounterSnapshotConfigWrite(board, ctr,   // Snapshot upon signal rising edge.
    S826_SSRMASK_IXRISE, S826_BITWRITE);
  return S826_CounterStateWrite(board, ctr, 1); // Enable period measurement.
}
// Read measured period in seconds.
int ReadPeriod(uint board, uint ctr, double *period, int hires)
{
  uint counts;
  int errcode = S826_CounterSnapshotRead(board, ctr,  // Block until next sample available.
     &counts, NULL, NULL, S826_WAIT_INFINITE);
  *period = counts * (hires ? 2e-8 : 1e-6);
  return errcode;
}
// Example: Measure period of external digital signal.
double period;     // period in seconds
int hires = TRUE;
CreatePeriodCounter(0, 0, hires);     // Create high-resolution (20 ns) period counter.
ReadPeriod(0, 0, &period, hires);     // Discard the first sample (may not be a complete cycle).
ReadPeriod(0, 0, &period, hires);
printf("Signal period (in seconds) = %f", period);

Incremental encoders

An incremental encoder

Every counter channel provides comprehensive support for incremental encoders, including:

  • Differential line receivers
  • Noise filters
  • High-speed (50 MS/s) quadrature clock decoder
  • 32-bit position counter
  • 32-bit timestamps with 1 µs resolution
  • Dual compare registers
  • Dual preload registers
  • Flexible event triggering system
  • 16-deep event FIFO
  • 5 V encoder power

Programming fundamentals

Which API functions should I use for incremental encoders?

The flexible counter architecture allows for many options, but basic operation works as follows:

  • First configure and enable the counter channel:
S826_CounterModeWrite(0, 0, S826_CM_K_QUADX4);  // Configure counter0 as incremental encoder interface.
S826_CounterStateWrite(0, 0, 1);                // Start tracking encoder position.
  • You can read the current encoder position on demand, either by reading the counts directly or by invoking and reading a snapshot, as explained in Reading counts.
  • Two snapshots allow you to precisely measure speed -- see Measuring speed for details.
  • The encoder counts can be changed by software (see Jamming counts into a counter). This is typically done when the encoder is stopped at a known position, but not while the encoder is moving as doing so could disrupt position tracking.

Using interrupts with encoders

The following example uses hardware interrupts to block the calling thread until the encoder counts reach a particular value. Other threads are allowed to run while the calling thread blocks. A snapshot is captured when the target count is reached. The snapshot generates an interrupt request, which in turn causes S826_CounterSnapshotRead() to return. The example ignores the snapshot counts (which will always equal the target value as explained here), the timestamp, and the reason code (which will always indicate a Match0 event).

// Wait for counts to reach 5000. Allow other threads to run while waiting.

S826_CounterCompareWrite(0, 0, 0,         // Set Compare0 register to target value:
    5000);                                //  5000 counts (for this example)
S826_CounterSnapshotConfigWrite(0, 0,     // Enable snapshot:
    S826_SSRMASK_MATCH0,                  //  upon counts==Compare0
    S826_BITWRITE);                       //  disable all other snapshot triggers
S826_CounterSnapshotRead(0, 0,            // Wait for counter to reach target counts:
    NULL, NULL, NULL,                     //  ignore snapshot counts, timestamp and reason
    S826_WAIT_INFINITE);                  //  disable function timeout
printf("Counter reached target counts");

The next example waits for counts to reach an upper or lower threshold (whichever occurs first). Furthermore, it will only wait for a limited amount of time. To set this up, the threshold values are programmed into Compare registers and snapshots are enabled for matches to both Compare registers. To set a limit on how long to wait, a maximum wait time is specified by tmax when calling S826_CounterSnapshotRead. Since a snapshot can now be triggered by multiple events, it's necessary to know which event triggered a snapshot in order to decide how to handle it; this is indicated by the snapshot's reason flags.

// Wait for counts to reach 3000 or 4000, or for 10 seconds to elapse, whichever comes first.

uint counts;      // encoder counts when the snapshot was captured
uint tstamp;      // time the snapshot was captured
uint reason;      // event(s) that caused the snapshot
uint errcode;     // API error code

S826_CounterCompareWrite(0, 0, 0, 3000);  // Set Compare0 register to low limit (3000 counts)
S826_CounterCompareWrite(0, 0, 1, 4000);  // Set Compare1 register to high limit (4000 counts)
S826_CounterSnapshotConfigWrite(0, 0,     // Enable snapshot:
    S826_SSRMASK_MATCH0                   //  upon counts==3000
    | S826_SSRMASK_MATCH1,                //    or counts==4000
    S826_BITWRITE);                       //  disable all other snapshot triggers
errcode = S826_CounterSnapshotRead(0, 0,  // Wait for snapshot:
    &counts, &tstamp, &reason,            //  receive the snapshot info here
    10000000);                            //  timeout if wait exceeds 10 seconds (10,000,000 us)

switch (errcode) {                        // Decode and handle the snapshot:
  case S826_ERR_NOTREADY:                 //  snapshot not available (wait timed out), so
    S826_CounterRead(0, 0, &counts);      //   read counts manually
    printf("Timeout -- counts didn't hit limits within 10 seconds; current counts = %d", counts);
    break;
  case S826_ERR_OK:
    if (reason & S826_SSRMASK_MATCH0)
      printf("Counter reached 3000 counts at timestamp %d", tstamp);
    if (reason & S826_SSRMASK_MATCH1)
      printf("Counter reached 4000 counts at timestamp %d", tstamp);
    break;
  default:
    printf("Error detected: errcode=%d", errcode);
}

Interrupts on IX edges

Sometimes it's useful to be able to automatically call a function once per encoder revolution. This is easily done in cases where the encoder sends an Index signal to the counter's IX input. The next example shows how to generate interrupts on every IX rising edge.

// Interrupt upon rising edges on counter's Index input.

uint bd = 0;                    // board number
uint ch = 0;                    // counter channel number
uint counts, tstamp, reason;    // snapshot info

S826_CounterModeWrite(bd, ch, S826_CM_K_QUADX4);                            // Configure counter
S826_CounterSnapshotConfigWrite(bd, ch, S826_SS_ALL_IXRISE, S826_BITWRITE); // Enable snapshots upon index rising edges
S826_CounterStateWrite(bd, ch, 1);                                          // Start tracking encoder
// Repeat forever:
while (S826_CounterSnapshotRead(bd, ch, &counts, &tstamp, &reason, S826_WAIT_INFINITE) == S826_ERR_OK) {  // Wait for next Index pulse
  ProcessIndexPulse();  // TODO: HANDLE INTERRUPT
}

Measuring speed

Speed measurement depends on the ability to sample displacement (position) at known times. Counter snapshots are ideally suited for this because they include simultaneously-sampled displacement and time information (counts and timestamp, respectively). Two snapshots allow you to precisely measure the average speed over a time interval. The interval is determined by the snapshot times: it begins when the first snapshot is captured and ends when the second snapshot is captured.

Measuring speed on demand

Here's a simple way to precisely measure speed over a 100 ms (approximate) interval:

uint c0, t0;      // first snapshot (simultaneously sampled counts and timestamp)
uint c1, t1;      // second snapshot
uint t;           // elapsed time between snapshots (in microseconds)

S826_CounterSnapshot(0, 0);  // Trigger first snapshot.
Sleep(100);                  // Wait for end of measurement interval (not recommended for production code).
S826_CounterSnapshot(0, 0);  // Trigger second snapshot.

S826_CounterSnapshotRead(0, 0, &c0, &t0, NULL, 0);    // Read first snapshot.
S826_CounterSnapshotRead(0, 0, &c1, &t1, NULL, 0);    // Read second snapshot.

t = t1 - t0;                                          // Compute measurement interval.
printf("Measurement period = %d µs\n", t);
printf("Average speed = %d counts per second\n", (1000000 * (c1 - c0)) / t);

Periodic speed measurement

To set this up, configure the counter to periodically capture snapshots. Each time a new snapshot arrives, the new and previous snapshots are used to precisely compute speed. An easy way to do this is to select the internal tick generator as the IX signal source, and configure the counter to capture snapshots on IX rising edges. The following example produces a speed measurement exactly ten times per second. Note: you can change the sampling period as needed (e.g., to change measurement resolution).

// Measure and display speed 10 times per second ---------------

#define DISP_PER_CNT 0.005  // Displacement per encoder count (e.g., 0.005 meters)

uint tstamp[2];       // Previous and current timestamps.
uint counts[2];       // Previous and current encoder counts.
double cnts_per_us;   // Encoder counts per microsecond.
double speed;         // Speed in user units (e.g., meters/second).

// Configure counter0 as encoder interface; automatically snapshot every 0.1 seconds.
S826_CounterModeWrite(0, 0,  // Configure counter mode:
  S826_CM_K_QUADX4 |         //   clock source = external quadrature x4
  S826_CM_XS_10HZ);          //   route internal 10 Hz clock to IX
S826_CounterSnapshotConfigWrite(0, 0, 0x10, 0); // Enable snapshots upon IX^.
S826_CounterStateWrite(0, 0, 1);                // Start tracking encoder position.

// Wait for first snapshot (captures starting displacement).
S826_CounterSnapshotRead(0, 0, counts, tstamp, NULL, S826_WAIT_INFINITE);

while (1) {  // Repeat every 0.1 seconds:
  // Wait for next snapshot, then compute and display speed.
  S826_CounterSnapshotRead(0, 0, &counts[1], &tstamp[1], NULL, S826_WAIT_INFINITE);
  cnts_per_us = (double)(counts[1] - counts[0]) / (tstamp[1] - tstamp[0]);
  speed = DISP_PER_CNT * 1000000 * cnts_per_us;  // convert to meters/second
  printf("%f\n", speed);
  counts[0] = counts[1];   // Prepare for next snapshot.
  tstamp[0] = tstamp[1];
}

If IX is already being used, create a thread that executes approximately 10 times per second. Each time the thread runs, call S826_CounterSnapshot to trigger a snapshot. In this case the sample rate will not be exactly 10 Hz, but speed will still be measured precisely because timestamps are included in the snapshots.

uint c0, t0;      // previous snapshot
uint c1, t1;      // current snapshot

S826_CounterSnapshot(0, 0);    // Trigger and read the initial "previous" snapshot.
S826_CounterSnapshotRead(0, 0, &c0, &t0, NULL, 0);

while (1) {  // Repeat every 0.1 seconds (approximately):
  Sleep(100);                  // Wait for next sample time.
  S826_CounterSnapshot(0, 0);  // Trigger and read the "current" snapshot.
  S826_CounterSnapshotRead(0, 0, &c1, &t1, NULL, 0);
  printf("Speed = %d counts per second\n", (1000000 * (c1 - c0)) / (t1 - t0));
  c0 = c1;                     // Prepare for next snapshot.
  t0 = t1;
}

Precise homing

To achieve high-accuracy homing, it's necessary to register the encoder counts with minimal delay when a pulse is issued by the home position sensor. This becomes increasingly important as speed increases and is critical in high-performance systems that seek home at high speeds. To facilitate this, the counters feature the ability to automatically register the home position with a maximum delay of 20 ns.

The following example shows how to precisely register the home position at any traversal speed. It does this by configuring the counter to snapshot the counts upon rising edge of the Home signal. The home sensor is connected to the counter's IX+ terminal, which is internally routed to the counter's index input. The counter is "armed" to enable sampling upon Home's first rising edge. When the edge is detected, a snapshot is captured and the counter is automatically "disarmed" so that subsequent edges won't trigger snapshots.

First, configure the counter to snapshot the counts upon Home rising edge. Note that the snapshot doesn't affect counting -- the counter will continue to track position without interruption:

S826_CounterSnapshotConfigWrite(0, 0,  // Set up snapshot triggering for counter0 (on board0):
  S826_SS_FIRST_IXRISE,                //   arm for snapshot upon first IX rising edge (auto-disarm upon snapshot)
  S826_BITSET);                        //   leave other triggering rules intact

Now seek the home position. The program can either block or poll while waiting for the Home signal. When blocking is used, other threads are allowed to run while the blocked thread waits for the signal:

uint offset;
S826_CounterSnapshotRead(0, 0, &offset, NULL, NULL, S826_INFINITE_WAIT);  // Block while waiting for Home signal.
printf("Exact counts at home position = %d.\n", offset);

Alternatively, the program can poll while waiting for the Home signal:

uint offset;
while (S826_CounterSnapshotRead(0, 0, &offset, NULL, NULL, 0) == S826_ERR_NOTREADY) { // While home signal not yet detected ...
  // do some other things
}
printf("Exact counts at home position = %d.\n", offset);

Resetting the counter

If desired, the captured homing counts value can be immediately used to correct all subsequent position samples. Note that this does not reset the hardware counts; it performs a software correction by subtracting the homing offset counts:

uint curposn;
S826_CounterRead(0, 0, &curposn);    // Get current position.
uint position = curposn - offset;    // Compute corrected position.

The counts value can be used this way even if the encoder is still moving. However, it may be necessary to defer such usage to avoid motion control disruption, as explained below.

Avoiding PID loop disruption

It might seem like it would be ideal to be able to simply zero the counts when the Home signal is detected, but it's often impractical to do so because the sudden counts change could disrupt PID loop control. To solve this problem, use the above code (or similar) to snapshot the counts and notify the application when Home is detected. Upon receiving the notification, the application will terminate the Home seek operation and turn off the motor. When the encoder stops, turn off the loop controller and "retroactively" zero the counts by taking into account the the captured counts and the distance the encoder has moved since Home was detected.

This could be done as shown above (by correcting every reading), but since the encoder has stopped, it's now safe to read-modify-write the counts, thus making further software corrections unnecessary:

uint curposn;
S826_CounterRead(0, 0, &curposn);       // Sample current position.
void JamCounts(0, 0, curposn - offset); // Correct current position and jam it into counter.

From now on, the application can read the corrected counts directly from the counter.

Output pulse at specific position

A counter can output a pulse on a DIO when the counts reach a particular value. The pulse duration will be 20 ns regardless of how long the counts remain at the target value. Consequently, an external pull-up must be added to the DIO to speed up its rising edge (see Using external pull-up resistors for details) and thereby ensure proper pulse formation.

The following code will generate a 20 ns output pulse when the counts reach 5000, using counter0 and dio0 on board number 0. A snapshot is captured when the pulse is issued; this is required to produce the pulse but the snapshot data is not used in this example.

// Output a 20 nanosecond pulse when counts reach 5000 ----------

// Configure counter0 as encoder interface; output 20ns pulse when counts reach 5000.
S826_CounterModeWrite(0, 0,                  // Configure counter mode:
  S826_CM_K_QUADX4 |                         //   clock = external quadrature x4
  S826_CM_OM_MATCH);                         //   pulse ExtOut upon snapshot.
S826_CounterCompareWrite(0, 0, 0, 5000);     // Load target counts into compare0 reg.
S826_CounterSnapshotConfigWrite(0, 0, 1, 2); // Snapshot when counts reach target.
RouteCounterOutput(0, 0, 0);                 // Route counter0 output to dio0.
S826_CounterStateWrite(0, 0, 1);             // Start tracking encoder position.

Programmable pulse width

If desired, a second counter can be used to stretch the pulse to any desired width. To do this, configure the second counter as a one-shot (using the encoder counter's ExtOut as a preload trigger) and program its preload counts to the desired pulse width. In this case the one-shot's (vs. encoder counter's) ExtOut must be routed to the DIO. Note that if rise time is critical, an external pull-up may still be required.

The following code uses a second counter to stretch the pulse width: it will output a 500 µs pulse on dio0 when 5000 counts is reached.

// Output a 500 microsecond pulse when counts reach 5000 ----------

// Configure counter0 as one-shot, used to stretch output pulses from counter1.
CreateOneShot(0, 0, 3, 500);                 // Create 500 us 1-shot, trig=ExtOut1.
RouteCounterOutput(0, 0, 0);                 // Route one-shot output to dio0.

// Configure counter1 as encoder interface; output 20ns pulse upon compare0 match.
S826_CounterModeWrite(0, 1,                  // Configure counter mode:
  S826_CM_K_QUADX4 |                         //   clock = external quadrature x4
  S826_CM_OM_MATCH);                         //   pulse ExtOut upon snapshot.
S826_CounterCompareWrite(0, 0, 0, 5000);     // Load target counts into compare0 reg.
S826_CounterSnapshotConfigWrite(0, 0, 1, 2); // Snapshot when counts reach target.
S826_CounterStateWrite(0, 1, 1);             // Start tracking encoder position.

Output pulse every N encoder pulses

The following code will generate an output pulse every 20K encoder counts, using counter0 and dio0 on board number 0, while tracking encoder position. A snapshot is captured each time an output pulse is issued, which is used to set up the next target counts.

Note that the pulse width will always be 20 ns regardless of how long the counts remain at the target value. Consequently, an external pull-up must be added to the DIO to speed up its rising edge as explained in Using external pull-up resistors. If a longer pulse is needed, a second counter can be used to stretch counter0's output pulse as shown in Programmable pulse width.

If it isn't necessary to track encoder position then it's recommended to use the simpler method shown in Without tracking.

// Output a 20 ns pulse every 20000 encoder counts while tracking position ----------

#define PULSE_INTERVAL 20000     // output a DIO pulse every 20,000 encoder counts

uint targ = PULSE_INTERVAL;      // Generate pulse when encoder counts reach this.

// Configure counter0 as encoder interface; output 20ns pulse upon compare0 match.
S826_CounterModeWrite(0, 0,                  // Configure counter mode:
  S826_CM_K_QUADX4 |                         //   clock = external quadrature x4
  S826_CM_OM_MATCH);                         //   pulse ExtOut when counts reach target
S826_CounterSnapshotConfigWrite(0, 0, 1, 2); // Enable snapshots upon counts match.
RouteCounterOutput(0, 0, 0);                 // Route counter0 output to dio0.
S826_CounterStateWrite(0, 0, 1);             // Start tracking encoder position.

do {  // Repeat forever ...
  S826_CounterCompareWrite(0, 0, 0, targ);   // Program target counts and wait for match.
  int errcode = S826_CounterSnapshotRead(0, 0, NULL, NULL, NULL, S826_WAIT_INFINITE);
  targ += PULSE_INTERVAL;                    // Bump target counts.

  // TODO: PERFORM ANY OTHER REQUIRED PROCESSING

} while (errcode == S826_ERR_OK);

Programmable pulse width

The following code builds on the previous example by using a second counter to stretch the pulse width: it will output a 500 µs pulse on dio0 every 20K encoder counts.

// Output a 500 microsecond pulse every 20000 encoder counts while tracking position ----------

#define PULSE_INTERVAL 20000        // output a DIO pulse every 20,000 encoder counts

uint targ = PULSE_INTERVAL;         // generate pulse when encoder counts reach this

// Configure counter0 as one-shot, used to stretch output pulses from counter1.
CreateOneShot(0, 0, 3, 500);                 // Create 500 us 1-shot, trig=ExtOut1.
RouteCounterOutput(0, 0, 0);                 // Route one-shot output to dio0.

// Configure counter1 as encoder interface; output 20ns pulse upon compare0 match.
S826_CounterModeWrite(0, 1,                  // Configure counter mode:
  S826_CM_K_QUADX4 |                         //   clock = external quadrature x4
  S826_CM_OM_MATCH);                         //   pulse ExtOut when counts reach target
S826_CounterSnapshotConfigWrite(0, 1, 1, 2); // Enable snapshots upon compare0 match.
S826_CounterStateWrite(0, 1, 1);             // Start tracking encoder position.

do {  // Repeat forever ...
  S826_CounterCompareWrite(0, 1, 0, targ);   // Program target counts and wait for match.
  int errcode = S826_CounterSnapshotRead(0, 1, NULL, NULL, NULL, S826_WAIT_INFINITE);
  targ += PULSE_INTERVAL;                    // Bump target counts.

  // TODO: PERFORM ANY OTHER REQUIRED PROCESSING

} while (errcode == S826_ERR_OK);

Without tracking

If position tracking is not required then there's no need to reload the Compare0 register for each output pulse (as in the above examples). Instead, configure the counter to automatically preload zero counts upon Compare0 match. Counter snapshots must be enabled to generate an output pulse upon Compare0 match, but in this example the snapshots are not used (the snapshot FIFO is allowed to fill and harmlessly overflow). If a wider output pulse is needed, use a one-shot to stretch the 20 ns output pulse, as shown in the previous above.

// Output a 20 ns pulse every 20000 encoder counts (without position tracking) ----------

// Configure counter0 as encoder interface; output 20ns pulse upon compare0 match.
S826_CounterModeWrite(0, 0,                  // Configure counter mode:
  S826_CM_K_QUADX4 |                         //   clock = external quadrature x4
  S826_CM_OM_MATCH |                         //   pulse ExtOut when counts==compare0
  S826_CM_PX_MATCH0);                        //   reset counts to 0 when counts==compare0
S826_CounterCompareWrite(0, 1, 0, 20000);    // Set compare0 value to 20000.
S826_CounterPreloadWrite(board, 0, 0, 0);    // Set preload0 value to zero.
S826_CounterSnapshotConfigWrite(0, 0, 1, 2); // Enable snapshots upon counts match.
RouteCounterOutput(0, 0, 0);                 // Route ExtOut to dio0.
S826_CounterStateWrite(0, 0, 1);             // Start the counter running.

Here's a trick you can use to generate wide output pulses with a single counter: have the counter count down to zero and then preload. Both preload registers are used, alternately, with ExtOut active when Preload1 is selected (similar to a PWM generator). Snapshots are no longer needed to generate output pulses because ExtOut is controlled by the preload selector. In the following example a pulse will be output every 20000 encoder counts and the pulse will be active for 100 encoder counts. Note that the encoder direction must not change.

// Output a pulse every 20000 encoder counts with pulse width = 100 counts ----------

#define PULSE_INTERVAL 20000                 // output a DIO pulse every 20,000 encoder counts
#define PULSE_WIDTH    100                   // pulse width = 100 counts

#define PL0 (PULSE_INTERVAL - PULSE_WIDTH - 1)   // Preload0 counts = counts prior to pulse
#define PL1 (PULSE_WIDTH - 1)                    // Preload1 counts = counts during pulse

// Configure counter0 as encoder interface; output 20ns pulse upon compare0 match.
S826_CounterModeWrite(0, 0,                  // Configure counter mode:
  S826_CM_K_QUADX4 |                         //   clock = external quadrature x4
  S826_CM_UD_REVERSE |                       //   count down
  S826_CM_BP_BOTH |                          //   alternate between preload0 and preload1
  S826_CM_OM_PRELOAD |                       //   assert ExtOut when preload0 is selected
  S826_CM_PX_ZERO);                          //   preload when counts reach 0
S826_CounterPreloadWrite(board, 0, 0, PL0);  // Set preload0 value to pulse activation counts.
S826_CounterPreloadWrite(board, 0, 1, PL1);  // Set preload1 value to pulse width.
RouteCounterOutput(0, 0, 0);                 // Route ExtOut to dio0.
S826_CounterStateWrite(0, 0, 1);             // Start the counter running.

Encoder FAULT output

My encoder has a FAULT output. Can the 826 monitor the FAULT signal and notify the processor when it changes state?

Some incremental encoders output a FAULT (or FLT) signal to indicate problems such as damaged bearings, code disc defects/contamination, or malfunctioning emitters/detectors. The FLT signal is typically conveyed over an RS-422 differential pair, which is compatible with the 826's counter inputs. This hardware compatibility and the counter's flexible architecture allow it to monitor the FLT signal and automatically notify the processor when FLT changes state. Note that the general technique discussed here can be used to monitor the state of any RS-422 signal.

To implement this, connect FLT to the counter's IX input. If FLT is active-low, swap the differential pair between line driver and receiver as shown below (don't swap if active-high) so that a disconnected or unpowered encoder will be reported as a fault.

Using an 826 counter channel to monitor an incremental encoder's active-low FAULT output

The following code initializes the counter, starts it running, and then monitors the FLT signal state. It should be run in a dedicated thread because it blocks while waiting for FLT to go active. The counter is configured to count up at 1 MHz. A preload is triggered whenever IX is high, which holds the counts at 0, and the first Compare0 match triggers a snapshot; these allow the FLT state to be determined when the counter is enabled. Snapshots are also triggered upon IX rising and falling edges; these are used to notify the processor that FLT has changed state.

int FLT_state = -1;    // Public FLT state indicator: -1=unknown, 0=normal, 1=fault

// FLT monitor thread: maintains FLT_state and notifies other threads when FLT_state changes.
int EncoderFaultMonitor(uint board, uint chan)
{
  uint errcode;
  S826_CounterModeWrite(board, chan,                    // Configure counter0 mode:
    S826_CM_K_1MHZ |                                    //   count up at 1 MHz
    S826_CM_PX_IXHIGH);                                 //   preload while IX is high
  S826_CounterSnapshotConfigWrite(board, chan,          // Capture snapshot upon:
    S826_SSRMASK_MATCH0 | (S826_SSRMASK_MATCH0 << 16) | //   first compare0 match
    S826_SSRMASK_IXRISE |                               //   all IX rising edges
    S826_SSRMASK_IXFALL,                                //   all IX falling edges
    S826_BITWRITE);
  S826_CounterPreloadWrite(board, chan, 0, 0);          // Set preload0 to 0.
  S826_CounterCompareWrite(board, chan, 0, 1);          // Set compare0 to 1.
  S826_CounterStateWrite(board, chan, 1);               // Enable counter.
  errcode = ReadFLT(board, chan, 1);                    // Determine FLT initial state.
  while (errcode == S826_ERR_OK);                       // Monitor FLT state transitions.
    errcode = ReadFLT(board, chan, S826_WAIT_INFINITE);
  return errcode;
}

The following function is called by the above code to determine the state of FLT. Note that you must insert code that notifies other threads (e.g., via semaphore) when FLT_state has changed.

// Wait up to tmax for a FLT state change, then copy the current state to FLT_state.
static int ReadFLT(uint board, uint chan, uint tmax)
{
  uint reason;
  int errcode = S826_CounterSnapshotRead(board, chan, NULL, NULL, &reason, tmax);
  if (errcode == S826_ERR_NOTREADY) {     // If no snapshots captured within finite tmax then
    errcode = S826_ERR_OK;                //   cancel error;
    FLT_state = 1;                        //   counts holding at 0 because FLT asserted.
  } else if (errcode != S826_ERR_OK)      // Else if error detected then
    FLT_state = -1;                       //   abort and declare FLT state unknown.
  else                                    // Else
    FLT_state = ((reason & S826_SSRMASK_IXRISE) != 0);  //   FLT state determined by snapshot trig.
    
  // TODO: NOTIFY OTHER THREADS THAT FLT STATE CHANGED
    
  return errcode;
}

Interfacing touch-trigger probes

A touch-trigger probe

Touch-trigger probes (such as those made by Renishaw, Tormach, Marposs, etc.) are commonly used in CNC machine tools and coordinate measuring machines (CMMs). In a typical application, three or more incremental encoders monitor the position of a probe relative to a workpiece as the probe or workpiece is moved. The probe will output a digital signal when its stylus contacts the workpiece. When this happens, the stylus location must be determined by sampling all of the encoder counters.

At lower speeds and in cases where moderate accuracy is acceptable, a probe-triggered interrupt service routine (ISR) can be used to sequentially sample the counters. This is feasible because ISR-related measurement errors (due to trigger latency and sampling skew) are negligible in such cases. However, these errors can be significant when high accuracy and throughput is required. Consequently, in high-performance machines a hardware mechanism is needed that will simultaneously sample all counters with very low trigger latency.

High-performance measurement

Model 826 directly supports high-performance probe measurement by providing the ability to simultaneously sample up to six encoder counters with sub-microsecond trigger latency. In the following example, the probe's trigger signal is connected to dio17 (an arbitrary choice), and consecutive counters (starting with counter0) are assigned to the encoders of a 3-axis probe. The counters' high-speed FIFOs will simultaneously capture all encoder positions within 20 ns of receiving a probe trigger.

First we will define a data structure and function to receive coordinates. The function returns the sampled encoder counts along with a code (trig) that indicates what caused the counts to be sampled (trig may be set to NULL if the software doesn't need to know what triggered the sample).

typedef struct POINT {  // Probe coordinates (extensible to higher dimensions)
  uint x;
  uint y;
  uint z;
} POINT;

int InputTouch(POINT *p, uint *trig)
{
  // Wait for a snapshot, then get X coordinate and snapshot's trigger source.
  int errcode = S826_CounterSnapshotRead(0, 0, &p->x, NULL, trig, S826_WAIT_INFINITE);

  // Get Y and Z coordinates; no need to wait because they've already been triggered.
  if (errcode == S826_ERR_OK) {
    errcode = S826_CounterSnapshotRead(0, 1, &p->y, NULL, NULL, 0);
    if (errcode == S826_ERR_OK)
      errcode = S826_CounterSnapshotRead(0, 2, &p->z, NULL, NULL, 0);
  }
  return errcode;
}

This function will set up the probe interface and start it running:

CreateProbeInterface(uint trig, uint dio)
{
  for (int i = 0; i < 3; i++) {  // For each axis, assign a counter and configure it:
    S826_CounterExtInRoutingWrite(0, i, dio);       // Route dio to counter's ExtIn input.
    S826_CounterSnapshotConfigWrite(0, i, trig, 0); // Capture counts upon these events.
    S826_CounterModeWrite(0, i, S826_CM_K_QUADX4);  // Counter clock = external quadrature x4.
    S826_CounterStateWrite(0, i, 1);                // Start tracking encoder position.
  }
}

Now put it all together. This code will instantiate the probe interface and then loop while it processes touch events:

// Implement a high-performance touch probe interface --------

#define PROBE_DIO   17      // The probe signal is connected to this DIO (0-47)
#define PROBE_EDGE  0x20    // Trigger on this probe edge (0x20=falling, 0x40=rising)

POINT point;  // touch coordinates

CreateProbeInterface(PROBE_EDGE, PROBE_DIO);    // Config probe interface and start it running.
                                                // Process triggers from the probe:
while (InputTouch(&point, NULL) == S826_ERR_OK) //   Wait for next touch and get coordinates.
  ProcessTouchPoint(&point);                    //   TODO: PROCESS TOUCH (e.g., add to point list).
Halting the probe interface

This expanded version of the previous example provides an easy way to halt the probe interface:

// High-performance touch probe interface with Halt function --------

POINT point;  // touch coordinates
uint  trig;   // trigger flags

CreateProbeInterface(PROBE_EDGE, PROBE_DIO);       // Config probe interface and start it running.
                                                   // Process triggers from the probe:
while (InputTouch(&point, &trig) == S826_ERR_OK) { //   Wait for next touch and get coordinates.
  if (trig & S826_SSRMASK_SOFT)                    //   If snapshot triggered by software then
    break;                                         //     halt probe interface.
  ProcessTouchPoint(&point);                       //   TODO: PROCESS TOUCH (e.g., add to point list).
}

To halt the probe interface, simply trigger a soft snapshot on counter0:

S826_CounterSnapshot(0, 0);

Handling false triggers

Touch-trigger probes commonly detect false triggers due to vibration, contact bounce, or both. Most probes internally debounce detected triggers to reduce the frequency of false output triggers. This debouncing increases the trigger latency, which in turn causes measurement error because the probe is moving while the trigger is being delayed. To mitigate this, systems will typically maintain constant probe speed while seeking a touch and then, when the debounced trigger has been received and all encoders have been sampled, estimate the actual touch coordinates via extrapolation (using the probe calibration).

Unfortunately, even internally debounced probes will sometimes output false triggers. This problem is especially prevalent in high-vibration CNC machines, and in both CNC and CMM systems when low-latency probes (ones with very fast or no internal debounce) break contact with a workpiece.

An obvious solution is to suppress false triggers by externally debouncing the probe. Since the probe signal enters the 826 board via a DIO, an easy way to do this is to use the DIO's built-in debounce filter. However, external debouncing will introduce additional trigger latency, which defeats the purpose of high-speed probes and further reduces the accuracy of lower-performance probes. Furthermore, probe speed cannot be allowed to change until debouncing completes — an inconvenient constraint that becomes a performance bottleneck when high throughput is required. What is needed is a way to record the probe position at the leading edge of a touch so that later, when the touch has been validated, its precise position will already be known.

Model 826 offers an elegant solution to this problem which produces exact touch coordinates while at the same time rejecting false triggers. Also, since coordinates are captured at the leading edge of a touch, it's no longer necessary to maintain constant probe speed during touch acquisition. This method capitalizes on counter features used in the previous example (simultaneously sampling multiple counters; storing a sequence of samples from each counter in a dedicated high-speed FIFO) and the counters' ability to accept simultaneous sampling triggers from multiple sources. In the following example, counter5 is used as a retriggerable one-shot to debounce the probe signal's leading edge. The probe signal must be connected to dio17 as in the previous example, and also to counter5's IX+ input for the debounce function.

// High-accuracy touch probe interface with false trigger rejection ---------------

#define PROBE_DIO   17      // The probe signal is connected to this DIO (0-47)
#define BOUNCE_TIME 100     // Suppress triggers shorter than this (microseconds)
#define DB_TIMER    5       // Counter used as debounce timer.

POINT point;        // Exact probe coordinates at probe trigger rising edge.
int touching = 0;   // Not currently touching.
uint trig;          // Event type that triggered a counter snapshot.
int i;
int errcode;

CreateOneShot(0, DB_TIMER,               // Create retriggerable one-shot:
  S826_CM_XS_EXTNORMAL,                  //   trigger source = IX input (probe signal).
  BOUNCE_TIME);                          //   output pulse width = debounce interval.

for (i = 0; i < 3; i++) {  // Create encoder interfaces: counter0/1/2 = X/Y/Z axis -----
  S826_CounterExtInRoutingWrite(0, i,    // Connect counter's ExtIn input
    PROBE_DIO);                          //   to probe signal.
  S826_CounterModeWrite(0, i,            // Configure counter:
    S826_CM_K_QUADX4 |                   //   clock = external quadrature x4.
    (DB_TIMER + 2));                     //   connect Index input to one-shot ExtOut.
  S826_CounterSnapshotConfigWrite(0, i,  // Capture snapshot to FIFO when:
    S826_SSRMASK_EXTRISE |               //   probe rising edge
    S826_SSRMASK_EXTFALL |               //   probe falling edge
    S826_SSRMASK_IXFALL,                 //   one-shot trailing edge
    0);
  S826_CounterStateWrite(0, i, 1);       // Start tracking the encoder.
}

while (1)   // Repeat forever ...
{
  // Wait for next probe snapshot, get its coordinates and trigger source.
  POINT p;
  errcode = InputTouch(&p, NULL, &trig);
  if (errcode != S826_ERR_OK)   // Abort if problem reading counters.
    break;

  if (reason & S826_SSRMASK_EXTFALL) { // If probe trailing edge,
    touching = 0;   // discard coordinates and start next touch acquisition
  }
  else if (reason & S826_SSRMASK_IXFALL) { // If one-shot trailing edge,
    if (touching) { // discard coordinates and, if currently touching,
      // We have a new, valid touch point, so we will now pass the point to the
      // application so it can be processed as needed (e.g., add to point list).
      ProcessTouchPoint(&point);  // TODO: PROCESS THE NEW POINT
      touching = 0; // start next touch acquisition
    }
  }
  else if (reason & S826_SSRMASK_IXRISE) {  // If probe leading edge,
    point = p;     // record the coordinates
    touching = 1;  // touch acquisition is in progress
  }
  else
    break;  // unexpected snapshot trigger (e.g., fifo overflowed)
}

Panel encoders

Surface-mounted panel encoder with integrated pushbutton
Panel encoders are commonly used as manually operated controls for audio volume, power supply output voltage, and other analog phenomena. Some (like the SMT device shown here) have a built-in pushbutton that can be monitored with an 826 DIO, either by polling the DIO or by taking advantage of the DIO's interrupt-driven edge detection system.

Encoder-controlled voltage

The following example shows how to use a panel encoder to control an analog voltage by polling the encoder counts. The encoder is connected to counter0 (via its clkA/clkB inputs) and the output voltage is generated by dac0. The encoder counts are sampled (and the dac is updated accordingly) at 10 Hz.

// Use a panel encoder to control a DC voltage -----------------

#define MIN_COUNTS  0          // Minimum dac value, corresponds to 0V.
#define MAX_COUNTS  65535      // Maximum dac value, corresponds to +10V.

uint counts;
uint prev = ~0;

// Configure DAC and encoder interface.
S826_DacRangeWrite(0, 0, 1, 0);        // Set dac output range to 0 to +10 V.
S826_CounterModeWrite(0, 0,            // Configure counter:
  S826_CM_K_QUADX4 |                   //   Clock = external quadrature x4.
  S826_CM_XS_10HZ);                    //   Use 10 Hz tick generator as sampling trigger.
S826_CounterSnapshotConfigWrite(0, 0,  // Capture counter snapshot upon
     S826_SSRMASK_IXRISE, 0);          //   rising edge of sampling trigger.
S826_CounterStateWrite(0, 0, 1);       // Start tracking the encoder position.

// Repeat forever: wait for next sample and then process it.
while (S826_CounterSnapshotRead(0, 0, &counts, NULL, NULL, INFINITE_WAIT) == S826_ERR_OK) {
  if (counts != prev) {                // If counts changed since previous sample ...
    if ((int)counts < MIN_COUNTS) {      // Limit counts to legal dac values.
      counts = MIN_COUNTS;
      JamCounts(0, 0, MIN_COUNTS);
    }
    else if (counts > MAX_COUNTS) {
      counts = MAX_COUNTS;
      JamCounts(0, 0, MAX_COUNTS);
    }
    S826_DacDataWrite(0, 0, counts, 0);  // Program the dac output voltage.
    prev = counts;
  }
}

The next example shows how to do the same thing in an event-driven (vs. polled) way. Connect the encoder A signal to clkA as before and also to dio0. Similarly, connect B to both clkB and dio1. Configure the board to detect all edges on dio0 and dio1 so that, whenever the encoder moves, an interrupt will be generated that triggers a DAC update.

#define MIN_COUNTS  0          // Minimum dac value, corresponds to 0V.
#define MAX_COUNTS  65535      // Maximum dac value, corresponds to +10V.

uint edges[] = {3, 0};
S826_DioCapEnablesWrite(0, edges, edges, S826_BITSET);  // Detect all dio0 & dio1 edges.
S826_DacRangeWrite(0, 0, 1, 0);                         // Set dac output range to 0 to +10 V.
S826_CounterModeWrite(0, 0, S826_CM_K_QUADX4);          // Config counter0 for quadrature decode.
S826_CounterStateWrite(0, 0, 1);                        // Start tracking encoder position.

while (TRUE) { // Loop:
  uint counts;
  uint chlist[] = {3, 0};
  S826_DioCapRead(0, chlist, 0, INFINITE_WAIT);   // Wait for any dio0/dio1 edge.
  S826_CounterRead(0, 0, &counts);                // Read counts.
  if ((int)counts < MIN_COUNTS) {                 // Limit counts to legal dac values.
    counts = MIN_COUNTS;
    JamCounts(0, 0, counts);
  }
  else if (counts > MAX_COUNTS) {
    counts = MAX_COUNTS;
    JamCounts(0, 0, counts);
  }
  S826_DacDataWrite(0, 0, counts, 0);             // Program the dac output voltage.
}

Quadrature errors

In the event of a noise spike, does the 826 perform any error checking or would this noise just show up as an extra count or 2?

The 826 encoder interfaces will properly handle any noise that doesn't exceed the line receiver electrical ratings. The counts will not change at all if the noise spike is very short (<20ns) and occurs between sampling clocks. Otherwise, if the noise affects only the A or B state then the counts will briefly change by 1 (increment or decrement) and then return to the correct value. If the noise affects both A and B then a quadrature error will be detected and reported (see 826 technical manual section 7.1.2 for details); in this case the counts cannot be relied on to be accurate.

Unexpected snapshots

Why do I occasionally get two snapshots (upon counts match) from my incremental encoder when only one is expected?

Assuming your encoder has not changed direction, the unexpected snapshots are probably caused by noise or slow edges on the encoder clock signals. This is possible even when the snapshot timestamps are identical, because encoder clocks are sampled every 20 nanoseconds whereas timestamp counts are incremented only once per microsecond.

If this is what is happening, unexpected snapshots can be prevented by calling S826_CounterFilterWrite() to establish a clock filter. A small filter value is usually sufficient -- just enough to clean up clock edges, but not so long that valid encoder counts will be missed. For example, this will set the clock filter to 100 ns (index input will not be filtered):

#define FILT_NS    100                // Filter time in ns -- change as desired to multiple of 20.
#define FILT_RES   20                 // Filter resolution in nanoseconds.
#define FILT_CLK   (1 << 30)          // Bit flag to enable clock filtering.
S826_CounterFilterWrite(0, 0,         // Activate clock filter on counter 0.
    FILT_CLK + FILT_NS / FILT_RES);

Another way to handle this is to configure the Match snapshot trigger to become automatically disabled when it fires. Note that if you use this method, you will need to re-enable the Match trigger to capture snapshots of subsequent matches.

S826_CounterSnapshotConfigWrite(0, 5, // Configure snapshots on counter 5:
   S826_SSRMASK_MATCH0                //  enable snapshot upon Match0 (counts==Compare0 register)
   | (S826_SSRMASK_MATCH0 << 16),     //  disable subsequent Match0 snapshots upon Match0 
   S826_BITWRITE);                    //  disable all other snapshot triggers

Missed index pulses

Why do I occasionally miss index pulses?

Pulses on the IX input will be properly detected if they conform to the input signal voltage and timing specifications. The minimum pulse width is 20 ns by default, but will be longer if the counter's programmable noise filter is activated.

To resolve this problem, check the following:

  • View the IX input signals with an oscilloscope. Verify that the pulse duration is greater than 20 ns, and that the voltage transitions between valid logic levels.
  • Make sure the IX noise filter is disabled. When the IX filter is enabled, the filter will reject index pulses that are too short. The IX noise filter is disabled by default at power-up. If previously enabled by software, the noise filter can be disabled by calling S826_CounterFilterWrite().

Encoder maximum frequency

As explained in clock frequency limits, the ClkA Nyquist frequency is 12.5 MHz in quadrature modes and 25 MHz in single-phase modes. Consequently, if you expect ClkA to exceed 12.5 MHz during a particular motion segment then you should use a single-phase clock mode across that entire segment. This is recommended because even though the clock mode can be changed "on the fly" (by calling S826_CounterModeWrite), you will likely accumulate errors if you switch modes while the encoder is rotating.

Encoder wiring

I want to connect two TTL/CMOS incremental encoders to counter channels 0 and 1 — what's the correct way to do this?

Connect the encoders as shown below. If the encoder has a "Z" (index) output, connect Z to the counter's +IX input. Encoders may be connected to counter channels 2-5 in a similar fashion.

Wiring diagram showing how to connect two TTL/CMOS-compatible single-ended incremental encoders to the 826 board using counter channels 0 and 1

Timing

Clock frequency limits

Quadrature clocks

In all quadrature clock modes (x4, x2, and x1) the ClkA and ClkB inputs are simultaneously sampled at 50 Ms/s (every 20 ns). In any two consecutive samples, ClkA may change, or ClkB may change, or neither, but not both. Consequently, the Nyquist frequency of ClkA (or ClkB) is 12.5 MHz (four times the sampling period). This corresponds to a maximum count rate of 50 Mcounts/s, but in practice some margin should always be allowed for differential noise and quadrature clock phase/duty cycle errors.

If both ClkA and ClkB change in consecutive samples then it must be assumed that counts have been missed. This event is critically important to the application software because the current counts can no longer be relied on to accurately indicate the encoder position. To account for this, the counter will immediately notify the application software by capturing a snapshot with the S826_SSRMASK_QUADERR bit asserted in the reason flags. The software should check every snapshot for this notification — regardless of the expected reason for the snapshot — in a fashion similar to this:

// In quadrature clock modes, always check for quadrature clock errors -------

uint counts, tstamp, reason;
int errcode = S826_CounterSnapshotRead(board, ctr,  // Wait for next snapshot.
   &counts, &tstamp, &reason, S826_WAIT_INFINITE);
if (reason & S826_SSRMASK_QUADERR)                  // Check for quadrature clock error.
   printf("Quadrature clock error!");
else
   // ... process other reason bits normally

Single-phase clocks

In single-phase (non-quadrature) modes, ClkB is ignored and the clock decoder samples ClkA at 50 Ms/s (every 20 ns). In any two consecutive samples, ClkA can change at most one time. Since ClkA must be sampled at least every half-cycle, the Nyquist frequency of ClkA is 25 MHz. Note that the counter has no way of knowing if ClkA exceeds 25 MHz, so it cannot notify the application if this occurs.

Input filters

The ClkA, ClkB and Index inputs are acquired using an internal 50 MHz sampling clock (SysClk) with 3 ns minimum setup time. Consequently, an edge on any of these signals will will be detected 0.3 to 20.3 ns after it occurs. In the below timing diagram, the input edge is detected at SysClk 2 because it occurs after SysClk 1 and ≥ 0.3 ns before SysClk 2.

Each input has a digital filter with programmable time F. A filter's input signal will appear on its output when the input has remained stable for time F. The filter delays the detected edge by F+1 SysClk periods. This diagram show filter timing in the general case:

External clock delay due to clock sampler and noise filter

The filter delay can be minimized by setting F=0, which results in the following timing:

External clock delay due to clock sampler and noise filter

Counter functions

The filtered ClkA/B signals are decoded and used to control the counter. As shown in the following diagram, counts will change one SysClk period after a filter output change and then, one SysClk later, a snapshot is captured to the event FIFO and ExtOut goes active for one SysClk period. If ExtOut is routed to a DIO, it will be delayed one additional SysClk period by the DIO output sampler on its way to the DIO connector pin.

Counter timing diagram

Summary of counter timing parameters:

Timing Parameter Minimum Maximum
ClkA/ClkB setup time 3 ns
ClkA/ClkB hold time 0 ns
ClkA/ClkB to counts change 40 ns 60 ns
ClkA/ClkB to counts change 40 ns 60 ns
ClkA/ClkB to snapshot 60 ns 80 ns
ClkA/ClkB to ExtOut (internal) 60 ns 80 ns
ClkA/ClkB to ExtOut (DIO pin) 80 ns 100 ns

Using external pull-up resistors

My PWM stops working when it outputs high frequencies. Why does this happen and how can I prevent it?

The PWM signal (from the counter's ExtOut) is output by a DIO channel. When the DIO driver transitions to the off state, its 10 KΩ source resistance (combined with circuit capacitance) stretches the DIO rise time and thereby delays its transition to logic '1' (see DIO rising edge in the above timing diagram). As the PWM off time decreases, the rise time (which is constant) becomes a higher percentage of the off time. When the off time is too short (i.e., off time < rise time), the PWM output will seem to "stop" because there is not enough time for the signal to reach logic '1'.

This situation can arise when the PWM is operating at high frequencies or generating short positive pulses, and can be avoided by speeding up the DIO rise time.

PWM operation

How to configure a counter for PWM operation

The following functions configure a counter to operate as a PWM generator and start it running. The PWM period is defined by ontime and offtime, which are specified in microseconds. The PWM output appears on the counter's ExtOut signal, which can be routed to a DIO pin if desired.

int SetPWM(uint board, uint ctr, uint ontime, uint offtime)
{
  S826_CounterPreloadWrite(board, ctr, 0, ontime);   // On time in us.
  S826_CounterPreloadWrite(board, ctr, 1, offtime);  // Off time in us.
}

int CreatePWM(uint board, uint ctr, uint ontime, uint offtime)
{
  S826_CounterModeWrite(board, ctr,        // Configure counter for PWM:
    S826_CM_K_1MHZ |                       //   clock = internal 1 MHz
    S826_CM_UD_REVERSE |                   //   count down
    S826_CM_PX_START | S826_CM_PX_ZERO |   //   preload @startup and counts==0
    S826_CM_BP_BOTH |                      //   use both preloads (toggle)
    S826_CM_OM_PRELOAD);                   //   assert ExtOut during preload0 interval
  SetPWM(board, ctr, ontime, offtime);     // Program initial on/off times.
}

int StartPWM(uint board, uint ctr)
{
  return S826_CounterStateWrite(board, ctr, 1);      // Start the PWM generator.
}
// Example: Configure counter0 and dio0 as a PWM generator; ontime = 900, offtime = 500 microseconds.
CreatePWM(0, 0, 0, 900, 500);    // Configure counter0 as PWM.
RouteCounterOutput(0, 0, 0);     // Route counter0 output to dio0.
StartPWM(0, 0);                  // Start the PWM running.

Fail-safe PWM generator

I'm using a counter-based PWM to control a motor. Is there a way to automatically shut off the motor if my program crashes?

Yes, you can use the watchdog timer and fail-safe controller to force the PWM output to a constant state. To do this, configure the watchdog to activate safemode when it times out, as shown in this simplified block diagram:

Fail-safe PWM generator

Before enabling the PWM generator or watchdog, program the desired PWM failsafe level into the DIO channel's SafeData register (see Programming safemode states); this specifies the signal that will be sent to your motor controller when your program crashes (which will shut off the motor). Note that the DIO output is active-low. The SafeEnable register is set to '1' by default, thus enabling fail-safe operation on the DIO channel. Next, program the watchdog interval and start the watchdog running. Finally, start the PWM running.

After enabling the watchdog, your program must periodically kick it to prevent it from timing out, by calling S826_WatchdogKick(). When your program is running normally, the PWM signal will appear on the DIO pin. If your program crashes (or fails to kick the watchdog in a timely manner), the watchdog will time-out and activate the fail-safe controller. This will switch the DIO pin to the level specified by the SafeData register, which in turn will halt the motor.

#define CRASH_DET_SECONDS 0.5  // Halt motor if program fails to kick watchdog within this time.

uint dio_mask[2]= {1, 0};                     // dio bitmask for PWM output (dio0)
uint wdtime[5] = {                            // watchdog timing:
  (uint)(50000000 * (CRASH_DET_SECONDS)),     //   timer0
  1, 1, 0, 0}; // watchdog interval           //   others

// Create a fail-safe PWM generator using counter0 and dio0.
CreatePWM(0, 0, 900, 500);                    // Config counter0 as PWM w/initial 0.9/0.5 ms on/off times.
RouteCounterOutput(0, 0, 0);                  // Route counter0 output to DIO 0.
S826_SafeWrenWrite(0, S826_SAFEN_SWE);        // Enable writes to watchdog and SafeData.
S826_DioSafeWrite(0, dio_mask, S826_BITCLR);  // Force PWM output to '0' (+5V) when program crashes.
S826_WatchdogConfigWrite(0, 0x10, wdtime);    // Set wdog interval; trig safemode upon timeout.

// Start the PWM generator running.
S826_WatchdogEnableWrite(0, 1);               // Start watchdog. PROGRAM MUST KICK IT FROM NOW ON!
S826_SafeWrenWrite(0, S826_SAFEN_SWD);        // Prevent errant writes to watchdog and SafeData.
StartPWM(0, 0);                               // Start the PWM generator.

Phase-locked PWM outputs

Some applications require multiple, phase-locked PWM outputs. Although the 826's counters do not directly implement phase locking, it is possible to simulate phase-locked PWM outputs by using counters configured as hardware-triggered one-shots. This technique can be used in a variety of ways.

Example: quadrature generator

A quadrature clock generator can be implemented using three counters and two DIOs as shown in the example below. Except for DIO load connections, no external wiring is required (counter and DIO interconnects are established by the board's programmable signal router).

Quadrature generator

Counter0 is a PWM running at 50% duty cycle, which directly generates the quadrature "A" output clock on dio0. Counter2 is used to hold off the "B" generator after "A" rises, to create the 90° phase angle between "A" and "B" clocks. Counter1 is a one-shot that generates the "B" clock on dio1; it does this by issuing a pulse when triggered by Counter2.

This quadrature generator has 1 µs resolution because the counters are using the board's internal 1 MHz clock. If higher resolution is needed, the functions can be modified to use the internal 50 MHz clock.

// Generate A/B quadrature clocks ---------

#define PERIOD 600          // Output period in microseconds.
#define HP ((PERIOD) / 2)   // Half-period.
#define QP ((PERIOD) / 4)   // Quarter-period.

CreatePWM(0, 0, HP, HP);      // Generate "A" clock with counter0.
CreateOneShot(0, 2, 2, QP);   // Generate phase shift with counter2.
CreateOneShot(0, 1, 4, HP);   // Generate "B" clock with counter1.
RouteCounterOutput(0, 0, 0);  // Route "A" clock (ExtOut0) to dio0.
RouteCounterOutput(0, 1, 1);  // Route "B" clock (ExtOut1) to dio1.
StartPWM(0, 0);               // Start the quadrature generator.

Example: 3-phase controller

Here's a way to implement a 3-phase PWM generator using five counters and three DIOs:

3-phase PWM generator

High resolution pulse-burst generator

Many applications (e.g., radar, laser) require a fixed number of precisely-timed pulses. This example shows how to generate a burst of pulses with selectable resolution of 20 ns or 1 µs. It uses two counters (counter0 and counter1) to generate the pulse burst and one DIO (dio0) to output the pulses. The DIO must be externally connected to counter1's clkA+ input to allow it to control the pulse count. A snapshot is used to signal burst completion, which allows other threads to run while the burst is in progress. A pulse burst can be triggered by software (by calling OutputBurst) or by an external signal applied to counter1's IX+ input (optional).

// Create a pulse burst generator.
//  res - timing resolution: set to 1 (microsecond) or 20 (nanoseconds).
void CreateBurstGenerator(uint board, int res)
{
  S826_CounterModeWrite(board, 0,           // Config counter0 as PWM generator:
    ((res == 1) ?                           //   select clock based on resolution:
      S826_CM_K_1MHZ :                      //     1 us -> internal 1 MHz
      S826_CM_K_50MHZ) |                    //     20 ns -> internal 50 MHz
    S826_CM_UD_REVERSE |                    //   count down
    S826_CM_BP_BOTH |                       //   use both preloads (toggle)
    S826_CM_XS_EXTOUT(1) |                  //   connect index input to ExtOut1 (pulse gate)
    S826_CM_PX_IXRISE | S826_CM_PX_ZERO |   //   preload @ index^ and counts=0
    S826_CM_TE_IXRISE |                     //   enable counting upon index^
    S826_CM_TD_IXFALL |                     //   disable counting upon index falling edge
    S826_CM_OM_PRELOAD);                    //   assert ExtOut during preload0 interval

  S826_CounterModeWrite(board, 1,           // Config counter1 as pulse counter (PWM enable):
    S826_CM_K_ARISE |                       //   clock = external clkA (wire from ExtOut0)
    S826_CM_UD_REVERSE |                    //   count down
    S826_CM_TE_PRELOAD |                    //   enable counting upon preload
    S826_CM_PX_IXRISE |                     //   preload @ index^ (allows hardware triggering)
    S826_CM_TD_ZERO |                       //   disable counting upon counts=0
    S826_CM_OM_NOTZERO);                    //   assert ExtOut while counts!=0

  S826_CounterSnapshotConfigWrite(board, 1, // Assert IRQ when a burst has completed.
    S826_SSRMASK_ZERO,
    S826_BITWRITE);

  RouteCounterOutput(board, 0, 0);               // Route PWM output to dio0.
  S826_CounterStateWrite(board, 0, 1);            // Enable PWM generator.
  S826_CounterStateWrite(board, 1, 1);            // Enable the pulse counter.
}

// Specify attributes of next pulse burst.
//  npulses - number of pulses in burst.
//  period  - pulse period in res units (e.g., when res=20, period=5 -> 5*20ns = 100ns).
//  width   - pulse width in res units (e.g, when res=1, width=4 -> 4*1us = 4us).
void ConfigBurst(uint board, int npulses, uint period, uint width)
{
  S826_CounterPreloadWrite(board, 0, 0, width);   // Set pulse width (in res units).
  S826_CounterPreloadWrite(board, 0, 1,           // Set pulse period (in res units).
    period - width);
  S826_CounterPreloadWrite(board, 0, 0, npulses); // Set pulse count.
  S826_CounterStateWrite(board, 0, 1);            // Enable PWM generator.
  S826_CounterStateWrite(board, 1, 1);            // Enable the pulse counter.
}

void OutputBurst(uint board)
{
  S826_CounterPreload(board, 1, 1, 0);     // Trigger a pulse burst.
  S826_CounterSnapshotRead(board, 1, NULL, // Block until burst completes.
    NULL, NULL, S826_WAIT_INFINITE);
}
// Example usage:
CreatePulseGenerator(0, 20); // Create a pulse-burst generator w/20 ns resolution.
ConfigBurst(0, 99, 25, 3);   // Burst attributes: 99 pulses @ 2.0 MHz, pulse width = 60 ns.
OutputBurst(0);              // Output the burst.

Stepper motor control

Input section of a basic stepper motor driver

A stepper motor driver has two digital inputs: DIR specifies the rotation direction, and STEP causes the motor to rotate by a fixed-angle "step" each time it receives a pulse. To move the motor through a desired angle, it is necessary to assert or negate DIR and then issue a burst of pulses to the STEP input. The total angular displacement is determined by the number of pulses issued on STEP, whereas motor speed is determined by the pulse rate.

Model 826 does not include dedicated stepper motor controllers in its repertoire of I/O interfaces, but the resources it does provide can be used to control stepper motors in various ways, as shown in the following sections.

Via pulse burst

The following example shows one way to generate DIR and STEP signals for a stepper motor. It uses two counters (counter0 and counter1) to generate a burst of pulses, and two DIOs to convey DIR and STEP to the controller. A counter snapshot is used to signal burst completion, which allows other threads to do productive work while the motor is moving.

#define PULSE_WIDTH  5    // STEP pulse width in microseconds

// Create stepper motor driver interface. STEP appears on dio0, DIR on dio1.
void CreateStepperInterface(uint board)
{
  CreateBurstGenerator(board, 1);  // Create burst generator w/1 us resolution.
}

// Move the stepper motor.
void StepperMotorMove(uint board, uint dir, uint period, int npulses)
{
  uint diomask[] = {2, 0};  // bitmask for dio1 (DIR)
  S826_DioOutputWrite(board, diomask,      // Output DIR signal to motor driver.
    dir ? S826_BITSET : S826_BITCLR);
  ConfigBurst(board, npulses, period, PULSE_WIDTH);
  OutputBurst(board);
}

// Example usage:
CreateStepperInterface(0);           // Create motor driver interface.
StepperMotorMove(0, 1, 1000, 100);   // Move motor cw 100 steps at 1 KHz (1000 us/step).
StepperMotorMove(0, 0, 800, 50);     // Move motor ccw 50 steps at 1.25 KHz (800 us/step).

Serial data capture

A counter can be used to capture serial data by leveraging its snapshot FIFO. When the counter has been appropriately configured and enabled, the computer allows the channel hardware to acquire snapshots while it asynchronously reads and processes snapshots from the FIFO.

Serial data capture (asynchronous)

A timestamp is included in every snapshot, which is especially useful for capturing data from asynchronous sources and irregularly-timed sources such as bar code wands. The basic idea is to apply the serial data signal to the counter's Index or ExtIn input and have the counter automatically capture snapshots at signal edges.

In each snapshot, only the timestamp and reason flags are of interest (counts are ignored). The reason flag indicates whether a snapshot was triggered by rising or falling edge and the timestamp indicates the time when the edge occurred. For example, in the serial data waveform shown below, the first rising edge (A) caused a snapshot to be captured when the timestamp generator value was 100, and the reason code indicates the snapshot was triggered by a rising edge.

Serial data capture

Any two consecutive snapshots represent a matched pair of rising/falling or falling/rising edges, corresponding to an interval during which the serial data value was '1' or '0', respectively. For example, in the waveform shown above, snapshots A and B bracket a '1' interval. It is possible to determine the binary value of the serial data from the first reason code, and the duration of the data value from the difference between the timestamps.

To see how this works, consider the above serial data waveform. The counter automatically captures a snapshot for each of the edge events A, B, C and D. When the computer considers snapshots A and B, it determines that the serial data was '1' during interval A-B because A was triggered by a serial data rising edge. Furthermore, it knows that the serial data held at '1' for 300 µs (the difference between the A and B timestamps). Similary, it can determine that interval B-C was a logic '0' lasting 200 µs, and C-D was a 400 µs logic '1'.

For maximum efficiency, consider using a dedicated thread to read the FIFO. This eliminates polling and makes the application event-driven because the thread can wait in S826_CounterSnapshotRead() for the next snapshot without wasting CPU time. Also, this decouples the timing of serial data acquisition from other tasks, thereby greatly simplifying overall software development and maintenance. If fast processing of the serial data is required, raise the thread priority to an appropriately high level.

For some example code, see 826 example: async capture.

Serial data capture (synchronous)

To capture clocked serial data (e.g., SPI, I2C), apply the clock signal to the counter's ExtIn or Index input and configure the counter to capture snapshots at either the rising or falling edge of the clock signal (whichever edge coincides with stable data). Apply the data signal to the counter's ClkA input and set the counter's clock mode K=6 (external quadrature clock, x2 multiplier); this will cause the counts to change (it will alternate between two values) upon each data edge.

A serial data bit can be obtained directly from the counts lsb (least significant bit) of each snapshot. Timestamps may be used to identify data packet boundaries, or ignored if frame markers are encoded in the data.

An open-source demo of this technique is available at synchronous serial receiver.

Mode register decoder utility

Is there an easy way to convert a mode register value to a human readable description of counter settings?

Yes: download Sensoray's counter mode decoder utility program (Windows compatible). Run the program and enter the mode register value to see an English language description of the counter mode settings.

Snapshot counts upon match

In case it's not obvious: When a snapshot is caused by counts equal to a compare register, the snapshot counts will always equal the compare register value. Similarly, when a snapshot is caused by counts reaching zero, the snapshot counts will always be zero.

Personal tools
Namespaces

Variants
Actions
Toolbox