measure pulses with Ctimer registers

Hi, so I have been working on a project where I really need a way to measure external pulses without using the pulseIn() Funktion from arduino. I searched in the Datasheet of the Apollo 3(I’m using a Redboard Artemis) and found some hints that it is possible, but somehow i can’t really understand how I need to set it up.

Here is the picture: https://ibb.co/vjTV5RM

Thanks in advance

I fully agree with this request. I am still using Teensy because of their polished FreqMeasureMulti library which gives easy access to input capture pulse time buffering. Please do NOT tell me to use a general ISR and use micros(). I need the STIMER input capture registers to give me a continuous pulse time buffer as poorly and confusingly explained in Section 14 of the 909 page PDF for the Apollo3.

Can an expert from SparkFun please elucidate proper usage of these registers to time pulses?

If I can get the equivalent of FreqMeasureMulti working on the Artemis with the Apollo’s 4 capture registers, I can foresee throwing away my Teensy and switching to SparkFun Apollo.

Thank you!

Hello senaex and PaulM007,

Artemis is an open source hardware platform that we are developing with lots of input from our community. As such, it’s constantly changing and improving. I have reached out to our internal team members about your specific question. They advised me that your specific features would be better served using the AmbiqSuite SDK: https://github.com/sparkfun/AmbiqSuiteSDK. Using the SDK you can use the methods there, or just directly configure registers themselves to achieve advanced functionality.

Can an expert from SparkFun please elucidate proper usage of these registers to time pulses?

Unfortunately, with the nature of this open source product we are constantly improving and making tweaks. We don't have a full educational guide for the entire module. However, our resources get you started and the software tools help you expand into new parts of the module outside of the starting guides. Feel free to share your experience for others as well.

TS-Brandon:
Hello senaex and PaulM007,

Artemis is an open source hardware platform that we are developing with lots of input from our community. As such, it’s constantly changing and improving. I have reached out to our internal team members about your specific question. They advised me that your specific features would be better served using the AmbiqSuite SDK: https://github.com/sparkfun/AmbiqSuiteSDK. Using the SDK you can use the methods there, or just directly configure registers themselves to achieve advanced functionality.

Can an expert from SparkFun please elucidate proper usage of these registers to time pulses?

Unfortunately, with the nature of this open source product we are constantly improving and making tweaks. We don't have a full educational guide for the entire module. However, our resources get you started and the software tools help you expand into new parts of the module outside of the starting guides. Feel free to share your experience for others as well.

Hey Brandon, 6 months later I am wondering if by chance there has been any example/library/demonstration of using the Apollo 3/Artemis chip to do input capture timer buffered pulse timestampping/frequency measurement? Using a regular ISR and calling micros() will not cut it. I need to log all pulse timings to SD Card, and doing other work like this will hang and corrupt regular digital ISR timings. I need input capture like Teensy’s FreqMeasureMulti library. It would be awesome if I could have the small form factor and low-power of the Artemis combined with this mission critical ability of the Teensy.

Unfortunately I can’t find any examples demonstrating this, I don’t believe we’ve written any.

I have a working example. I use the capture mechanism to determine the actual frequency of the 32KHz XTAL so that my system can continuously calculate the proper XTAL cal factor setting. Give me a bit of time to come up with a more stand-alone example for you.

What are your capture timing requirements? If you clock STIMER with HFRC, you can get sub-microsecond resolution, but the accuracy is limited to the basic 2% accuracy of the HFRC. If you clock STIMER with XTAL, then your tick resolution is about 30.5 microseconds, but with crystal accuracy of maybe 50 parts per million.

Here is a working example. This is designed for a stand-alone build. It could be incorporated into Arduino except that Arduino already defines its own capture ISR which will collide with the one defined in this example. There is probably a way around that, but I don’t feel like digging through the Arduino code to see what it wants me to do.

#ifdef __cplusplus
  extern "C" {
#endif
// C++ compilations must declare the ISR as a "C" routine or else its name will get mangled
// and the linker will not use this routine to replace the default ISR
void am_stimer_isr();

#ifdef __cplusplus
  }
#endif

volatile uint32_t capturedCount;

#define ISR_SIGNAL_PAD  48

void am_stimer_isr()
{
  // Drive a GPIO so that a scope or logic analyzer can see when the interrupt actually runs
  am_hal_gpio_output_set(ISR_SIGNAL_PAD);
  
  // 'true' indicates that the returned status should only reflect those interrupts that are both enabled and asserted.
  // 'false' would indicated that the status will reflect all asserted interrupts, even those that are not enabled.
  uint32_t requestStatus = am_hal_stimer_int_status_get(true);
  
  // Clear all enabled/asserted interrupts
  am_hal_stimer_int_clear(requestStatus);
  
  // All we care about is the capture A/0 interrupt:
  if (requestStatus & AM_HAL_STIMER_INT_CAPTUREA) {
    // Read the captured count
    uint32_t count = CTIMER->SCAPT0;
    
    // A real program would do something with the count like stick it in an RTOS queue or a 
    // circular buffer where the foreground task would be waiting for it to arrive.
    // For this test program, we simply store the captured value in a global
    // and set a special variable to indicate that the global has new data:
    capturedCount = count;
  }
  
  am_hal_gpio_output_clear(ISR_SIGNAL_PAD);
}

// We will be capturing the STIMER when a rising transition occurs on this pad.
// For the purposes of this test program, the pad will be driven by our own software
// instead of some external device.  To simplify the test setup, we simultaneously use 
// the same pad as an output to create the capture request, and as an input that
// drives the capture interrupt request.  Using the same pin means no external 
// wiring required so this test will work for any Artemis board as long as this pad
// is not connected to any hardware that would be harmed by us driving it.
#define CAPTURE_PAD 7

void captureExample()
{
  // Set up the ISR_SIGNAL_PAD as an output.  We will be driving ISR_SIGNAL_PAD high during the interval
  // while the ISR is running so we can see the time relationship between the edge of the capture request
  // and the actual capture event on a scope.
  am_hal_gpio_pinconfig(ISR_SIGNAL_PAD, g_AM_HAL_GPIO_OUTPUT_4);
  
  // Configure our CAPTURE_PAD as an output that is also readable.  We start by configuring it as an output.
  // A more typical application would probably just configure the CAPTURE_PAD as an input.
  am_hal_gpio_pinconfig(CAPTURE_PAD, g_AM_HAL_GPIO_OUTPUT_4);
  
  // Jigger the GPIO configuration we just set up so that we can also read the output pad state
  GPIO->PADKEY = GPIO_PADKEY_PADKEY_Key;
  GPIO->PADREGB_b.PAD7INPEN  = GPIO_PADREGB_PAD7INPEN_EN;
  GPIO->PADKEY = 0;
    
  // Make sure our capture request output starts off at '0'
  am_hal_gpio_output_clear(CAPTURE_PAD);
  
  // Pick a clock source for the STIMER.  There are lots of choices.
  // We will use a really slow clock source just to make the timing relationship between the capture request
  // and the capture event extremely obvious.
  am_hal_stimer_config(AM_HAL_STIMER_LFRC_1KHZ /*AM_HAL_STIMER_XTAL_32KHZ*/ /*AM_HAL_STIMER_XTAL_1KHZ*/);
  
  // Optional: Clear & restart the STIMER
  am_hal_stimer_counter_clear();
  
  // Enable capture 0/A to watch the CAPTURE_PAD input to capture rising edges (if false) or falling edges (if true).
  am_hal_stimer_capture_start(0, CAPTURE_PAD, false);     // capture on rising edges
  
  // Flush any pre-existing capture interrupt, then enable CAPTURE A/0 events
  am_hal_stimer_int_clear(AM_HAL_STIMER_INT_CAPTUREA);
  am_hal_stimer_int_enable(AM_HAL_STIMER_INT_CAPTUREA);
  //NVIC_SetPriority(STIMER_IRQn, 0);
  NVIC_EnableIRQ(STIMER_IRQn);
  
  while (1) {
    // Generate a capture request by driving the CAPTURE_PAD GPIO to '1', then back to '0' again
    am_hal_gpio_output_set(CAPTURE_PAD);
    am_hal_flash_delay(FLASH_CYCLES_US(10));
    am_hal_gpio_output_clear(CAPTURE_PAD);
    
    // The capture request gets latched immediately by the silicon, but it is not actually recognized
    // and acted on by the STIMER until the next STIMER clock tick.
    // In the worst case, this can take as much as 1 clock period of the STIMER clock!
        
    // We are not allowed to request another capture until at least 1 clock period of the STIMER has expired
    // We will wait an extra-long time here before we generate another capture request:
    am_hal_flash_delay(FLASH_CYCLES_US(10000));
  }
}

If it was not clear reading through the code, the capture mechanism was not exactly intuitive to me. The main issue is that when a signal generates a capture event request, an oscilloscope shows that actual capture will not occur until the next tick of the STIMER clock. If you are using a slow 1 KHz clock, that could be over 1 millisecond later. Since the STIMER clock is not synchronized to the HFRC processor clock, the precise time between any given request and the capture will be essentially random, but contained within 1 STIMER clock period. See the attached photo for what I am trying to say.

In the photo, the top trace shows the signal that is requesting the capture. The bottom trace is set up to show when the ISR runs. The bottom trace is in a ‘persist’ mode where the traces persist on the scope display over time. Since the STIMER clock is not synchronized with the processor clock, the ISR will occur at an almost random time afterwards. The persistence display allows us capture thousands of events where each narrow pulse remains on the display as a dim event. Over time, the scope display builds up a display of all of the capture ISR events that it ever saw. From that, you can determine the minimum time between requesting the capture and the ISR servicing the event (about 500 uSec) and the maximum time (about 1500 uSec). This is with a slow 1 KHz STIMER clock, specifically chosen to make this lag effect between the request and the ISR obvious. If STIMER was running from a crystal at 32 KHz, you would still see a block of possible ISR times, but the block would be much closer to the request, and would be much narrower in width.

The main takeaways regarding the Capture process:

  • - don’t expect the capture event to occur when the request occurs, but rather, when the STIMER gets around to it
  • - there is an upper limit on how often you can generate a capture operation: if the system takes 1 STIMER clock to respond, you obviously can't possibly capture faster than the STIMER clock rate. In truth, it appears that the max capture rate is probably 1/2 the STIMER clock rate. More info on that topic is here: [https://forum.sparkfun.com/viewtopic.ph ... 74#p223574](https://forum.sparkfun.com/viewtopic.php?p=223574#p223574)
  • - If you are trying for super accuracy with your capturing, beware that the STIMER has an undocumented bug which can make this a profoundly difficult proposition. More info here: [https://forum.sparkfun.com/viewtopic.ph ... 33#p223733](https://forum.sparkfun.com/viewtopic.php?p=223733#p223733)
  • Here is a scope shot of the uncertainty between a capture request and an ISR service event with the timer running at 32 KHz. As expected, the ISR still falls into a basic uncertainty of one STIMER clock, but the 32 KHz clock is much faster than the 1 KHz clock in the original example so the uncertainty interval is a lot shorter.

    robin_hodgson:
    Here is a working example. This is designed for a stand-alone build. It could be incorporated into Arduino except that Arduino already defines its own capture ISR which will collide with the one defined in this example. There is probably a way around that, but I don’t feel like digging through the Arduino code to see what it wants me to do.

    #ifdef __cplusplus
    

    extern “C” {
    #endif
    // C++ compilations must declare the ISR as a “C” routine or else its name will get mangled
    // and the linker will not use this routine to replace the default ISR
    void am_stimer_isr();

    #ifdef __cplusplus
    }
    #endif

    volatile uint32_t capturedCount;

    #define ISR_SIGNAL_PAD 48

    void am_stimer_isr()
    {
    // Drive a GPIO so that a scope or logic analyzer can see when the interrupt actually runs
    am_hal_gpio_output_set(ISR_SIGNAL_PAD);

    // ‘true’ indicates that the returned status should only reflect those interrupts that are both enabled and asserted.
    // ‘false’ would indicated that the status will reflect all asserted interrupts, even those that are not enabled.
    uint32_t requestStatus = am_hal_stimer_int_status_get(true);

    // Clear all enabled/asserted interrupts
    am_hal_stimer_int_clear(requestStatus);

    // All we care about is the capture A/0 interrupt:
    if (requestStatus & AM_HAL_STIMER_INT_CAPTUREA) {
    // Read the captured count
    uint32_t count = CTIMER->SCAPT0;

    // A real program would do something with the count like stick it in an RTOS queue or a 
    // circular buffer where the foreground task would be waiting for it to arrive.
    // For this test program, we simply store the captured value in a global
    // and set a special variable to indicate that the global has new data:
    capturedCount = count;
    

    }

    am_hal_gpio_output_clear(ISR_SIGNAL_PAD);
    }

    // We will be capturing the STIMER when a rising transition occurs on this pad.
    // For the purposes of this test program, the pad will be driven by our own software
    // instead of some external device. To simplify the test setup, we simultaneously use
    // the same pad as an output to create the capture request, and as an input that
    // drives the capture interrupt request. Using the same pin means no external
    // wiring required so this test will work for any Artemis board as long as this pad
    // is not connected to any hardware that would be harmed by us driving it.
    #define CAPTURE_PAD 7

    void captureExample()
    {
    // Set up the ISR_SIGNAL_PAD as an output. We will be driving ISR_SIGNAL_PAD high during the interval
    // while the ISR is running so we can see the time relationship between the edge of the capture request
    // and the actual capture event on a scope.
    am_hal_gpio_pinconfig(ISR_SIGNAL_PAD, g_AM_HAL_GPIO_OUTPUT_4);

    // Configure our CAPTURE_PAD as an output that is also readable. We start by configuring it as an output.
    // A more typical application would probably just configure the CAPTURE_PAD as an input.
    am_hal_gpio_pinconfig(CAPTURE_PAD, g_AM_HAL_GPIO_OUTPUT_4);

    // Jigger the GPIO configuration we just set up so that we can also read the output pad state
    GPIO->PADKEY = GPIO_PADKEY_PADKEY_Key;
    GPIO->PADREGB_b.PAD7INPEN = GPIO_PADREGB_PAD7INPEN_EN;
    GPIO->PADKEY = 0;

    // Make sure our capture request output starts off at ‘0’
    am_hal_gpio_output_clear(CAPTURE_PAD);

    // Pick a clock source for the STIMER. There are lots of choices.
    // We will use a really slow clock source just to make the timing relationship between the capture request
    // and the capture event extremely obvious.
    am_hal_stimer_config(AM_HAL_STIMER_LFRC_1KHZ /AM_HAL_STIMER_XTAL_32KHZ/ /AM_HAL_STIMER_XTAL_1KHZ/);

    // Optional: Clear & restart the STIMER
    am_hal_stimer_counter_clear();

    // Enable capture 0/A to watch the CAPTURE_PAD input to capture rising edges (if false) or falling edges (if true).
    am_hal_stimer_capture_start(0, CAPTURE_PAD, false); // capture on rising edges

    // Flush any pre-existing capture interrupt, then enable CAPTURE A/0 events
    am_hal_stimer_int_clear(AM_HAL_STIMER_INT_CAPTUREA);
    am_hal_stimer_int_enable(AM_HAL_STIMER_INT_CAPTUREA);
    //NVIC_SetPriority(STIMER_IRQn, 0);
    NVIC_EnableIRQ(STIMER_IRQn);

    while (1) {
    // Generate a capture request by driving the CAPTURE_PAD GPIO to ‘1’, then back to ‘0’ again
    am_hal_gpio_output_set(CAPTURE_PAD);
    am_hal_flash_delay(FLASH_CYCLES_US(10));
    am_hal_gpio_output_clear(CAPTURE_PAD);

    // The capture request gets latched immediately by the silicon, but it is not actually recognized
    // and acted on by the STIMER until the next STIMER clock tick.
    // In the worst case, this can take as much as 1 clock period of the STIMER clock!
        
    // We are not allowed to request another capture until at least 1 clock period of the STIMER has expired
    // We will wait an extra-long time here before we generate another capture request:
    am_hal_flash_delay(FLASH_CYCLES_US(10000));
    

    }
    }

    
    
    
    If it was not clear reading through the code, the capture mechanism was not exactly intuitive to me. The main issue is that when a signal generates a capture event request, an oscilloscope shows that actual capture will not occur until the <B>**next tick**</B> of the STIMER clock. If you are using a slow 1 KHz clock, that could be over 1 millisecond later. Since the STIMER clock is not synchronized to the HFRC processor clock, the precise time between any given request and the capture will be essentially random, but contained within 1 STIMER clock period. See the attached photo for what I am trying to say. 
    
    
    
    In the photo, the top trace shows the signal that is requesting the capture. The bottom trace is set up to show when the ISR runs. The bottom trace is in a 'persist' mode where the traces persist on the scope display over time. Since the STIMER clock is not synchronized with the processor clock, the ISR will occur at an almost random time afterwards. The persistence display allows us capture thousands of events where each narrow pulse remains on the display as a dim event. Over time, the scope display builds up a display of all of the capture ISR events that it ever saw. From that, you can determine the minimum time between requesting the capture and the ISR servicing the event (about 500 uSec) and the maximum time (about 1500 uSec). This is with a slow 1 KHz STIMER clock, specifically chosen to make this lag effect between the request and the ISR obvious. If STIMER was running from a crystal at 32 KHz, you would still see a block of possible ISR times, but the block would be much closer to the request, and would be much narrower in width. 
    
    
    
    The main takeaways regarding the Capture process:
    <LIST><LI>- don't expect the capture event to occur when the request occurs, but rather, when the STIMER gets around to it</LI>
    <LI>- there is an upper limit on how often you can generate a capture operation: if the system takes 1 STIMER clock to respond, you obviously can't possibly capture faster than the STIMER clock rate. In truth, it appears that the max capture rate is probably 1/2 the STIMER clock rate. More info on that topic is here: [viewtopic.php?p=223574#p223574](https://forum.sparkfun.com/viewtopic.php?p=223574#p223574)</LI>
    <LI>- If you are trying for super accuracy with your capturing, beware that the STIMER has an undocumented bug which can make this a profoundly difficult proposition. More info here: [viewtopic.php?p=223733#p223733](https://forum.sparkfun.com/viewtopic.php?p=223733#p223733)</LI></LIST>
    
    Thank you so much Robin for sharing your expertise on this matter. I regret that I took so long to check back here and appeared unappreciative of your contribution. It is a shame a functionality so valuable is so difficult to configure. I am convinced though you have given me an excellent starting point. When I use naive digital ISRs to record pulse times and then save those buffered pulse timings to SD card, I always get corrupted pulse timings during when the SD card writing is happening (having tried this on Apollo but in general). Whereas using Teensy's FreqMeasure library that does real-deal input capture buffered, the results are separately and pristinely buffered by the hardware timestamppings themselves and not affected by other blocking operations. I'm hoping I can adapt the code you've provided above to do just that on the Artemis Apollo3.