Microsecond counter with PWM

I’m trying to develop a microsecond counter. It would be a part of the driver of a temperature sensor, whose data transmission pattern requires monitoring time intervals on the microsecond order of magnitude, like for example the DHT11 humidity and temperature Sensor. And there is a chance to develop this delay timer with the Pulse Width Modulation (PWM) interrupt system, as recommended in the FE310-G002 Manual.

The basic idea is to count the interrupts associated to the pwmcmp0 register of a 1 MHz frequency PWM. And for debugging purposes there are three led that blink accordingly to the required frequencies.

The variable counter holds the count of the interrupts served. Everything goes fine with low frequencies. With 1 Hz is easy to count the blinks of the leds and confirm that the value of counter gets 60 in one minute, as expected.

But when trying with 1 MHz everything goes wrong. The system destabilizes and crashes frequently, so much that it requires a reboot. Moreover, it doesn’t count well. The counter is expected to go up by one million every second. However, it does not reach more than 100,000 per second.

Why does it work with low frequencies and not with high ones? I suspect that JTAG Clocking may have something to do with it, or that there is an interrupt priority problem. In any case, I would appreciate any kind of information to be able to continue.

Below is a summarized and commented version of the code used.

Thank you!

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <math.h>

typedef volatile uint32_t addr_t;

#define MAX_INTERRUPTS 16
// Arrays of pointers to void functions.
void (*interrupt_handler [MAX_INTERRUPTS]) ();
void (*exception_handler [MAX_INTERRUPTS]) ();

// -------- Pulse Width Modulator (PWM) -----------------------
#define FREQ_BASE   16  // MHz
#define PIN_PWM     11  // PWM dedicated pin
// For the time being, we will only work with instance number 2.
#define PWM_BASE_ADDR2 0x10035000U
struct PWM_ADDRS...
struct PWM_ADDRS *PWM02 = (struct PWM_ADDRS*) PWM_BASE_ADDR2;

// -------- General Purpose Input/Output Controller (GPIO) ---
#define GPIO_BASE_ADDR 0x10012000U
struct GPIO_ADDRS...
struct GPIO_ADDRS *GPIO = (struct GPIO_ADDRS*) GPIO_BASE_ADDR;

// -------- Platform-Level Interrupt Controller (PLIC) --------
#define PLIC_ENABLE_REG2        0x0C002004U
#define PLIC_PRIORITY_REG_48    0x0C0000C0U // PWM2 Interrupt source 48 -> 16
#define PLIC_THRESHOLD_REG      0x0C200000U
#define PLIC_CLAIM_COMP_REG     0x0C200004U
#define PLIC_PENDING_REG2       0x0C001004

// -------- Core-Local Interruptor (CLINT) --------------------
#define MTIMECMP    (uint64_t *) 0x02004000UL
#define MTIME       (uint64_t *) 0x200bff8UL
#define TIMER_FREQ  32768   // 2^15 Hz
#define BLINK_FREQ  4

// Macros for reading and writing the control and status registers (CSRs)
#define read_csr(reg) ({ unsigned long __tmp; asm volatile ("csrr %0, " #reg : "=r"(__tmp)); __tmp; })
#define write_csr(reg, val) ({ asm volatile ("csrw " #reg ", %0" :: "rK"(val)); })

volatile uint64_t counter;

int main(void)
{
    // General setup. Three LEDs have been configured to display the frequencies.
    // Pin 5 for CLINT timer interrupts. Pin 11 is dedicated to the PWM output. 
    // Pin 10 will display each time the PLIC handler is called.
    // "counter" is the variable in which a counter that tracks the PWM frequency
    // is expected to be stored.
    pin_setup (5, 'O');             // Configured as Ouput pins.
    pin_setup (10, 'O');
    counter = 0;
    reset_timer (TIMER_FREQ / (2 * BLINK_FREQ));

    interrupt_handler [7] = timer_handler;
    interrupt_handler [11] = PLIC_handler;
    
    // Registration of the trap handler in mtvec.
    write_csr(mtvec, ((unsigned long) handle_trap) & ~(0b11));

    // Enabling of the interrupts
    write_csr(mstatus, read_csr(mstatus) | (1 << 3));
    write_csr(mie, read_csr(mie) | (1 << 7));
    write_csr(mie, read_csr(mie) | (1 << 11));


    // PLIC setup.
    // 1.- Enabling in this case PWM2 Interrupt source 48, Interrupt ID 16
    // whitch corresponds to pwmcmp0 of instance number 2 of PWM.
    uint32_t *PLIC_I_EN = (uint32_t *) PLIC_ENABLE_REG2;
    int val_en = (*PLIC_I_EN >> 16) & 0x1;
    if (!val_en) *PLIC_I_EN |= (1 << 16);

    // 2.- Priority setup of th interrupt source 48.
    uint32_t *PLIC_PRTY = (uint32_t *) PLIC_PRIORITY_REG_48;
    *PLIC_PRTY &= 0x0;
    *PLIC_PRTY |= 0x1;

    // 3.- Threshold setup of the HART.
    uint32_t *PLIC_THR = (uint32_t*) PLIC_THRESHOLD_REG;
    *PLIC_THR &= 0x0;


    // PWM setup. In these conditions pin 10 blinks once per second (1 Hz), 
    // and pin 11 blinks once every 2 seconds (0.5 Hz).  
    pwmInit ();
    pwm (1, 0.5);
    
    printf ("counter %lld\n", counter);

    while(1) {};

    return 0;
}

void PLIC_handler (void)
{
    // Dummy handler function that simply toggles the LED in pin 10 whenever 
    // the PLIC interrupt is served, and also increments the counter.
    int pin_val = (GPIO->output_val >> 10) & 1;
    if (pin_val) gpio_clear_set (10,0);
    else         gpio_clear_set (10,1);
    counter++;
}

void timer_handler (void)
{
    // Dummy handler function that simply toggles LED in pin 5 whenever the   
    // CLINT interrupt is served.
    int pin_val = (GPIO->output_val >> 5) & 1;
    if (pin_val) gpio_clear_set (5,0);
    else         gpio_clear_set (5,1);
    reset_timer (TIMER_FREQ / (2 * BLINK_FREQ));
}

void handle_trap (void) 
{
    uint32_t mcause_value = read_csr (mcause);
    uint32_t mcause_type = (mcause_value >> 31) & 1;
    uint32_t mcause_code = mcause_value & 0x3FF;
    uint32_t *plic_id = (uint32_t *) PLIC_CLAIM_COMP_REG;

    if (mcause_type == 1)
        switch (mcause_code)
        {
        case 7:
            interrupt_handler [7] ();
            break;
        case 11:
            if (*plic_id == 48)
            {
                interrupt_handler [11] ();
                *plic_id = 48;
            }
            else *plic_id = *plic_id;
            break;
        }
        
    else
        exception_handler [mcause_code] ();
}

Very interesting @daniel_g !

At faster speeds, you’re probably running into latency concerns. There’s two things you could try: Speed up the hfclk system clock (using the PLL, perhaps), or streamline and reduce the size of handle_trap and the other interrupt functions.

The downside of a higher system clock rate is higher power consumption.

The downside of a leaner handler is less generic adaptability and possibly coding in assembly. The latter isn’t too bad, with careful thought. Try to avoid as many of the if()'s and branches as you can.

Maybe this MTVEC Demonstration might give you some ideas.

Good luck, your almost there!

First of all, thank you very much @pds for your hints and ideas which have helped me unblock the situation. I have focused on solving possible latency problems taking into account these aspects:

  • Simplification of the interrupt handler. I have worked with a super simplified version that only works when it detects the PLIC interrupt case and specifically for source 48. With this solution there is some improvement, although it does not seem to be the main cause of the problem.

  • Clock frequency decrease. I have used the HFROSC source, since it seems easier to configure. It improves crashing issues during the debugging session.

  • Activation of PWM interrupts only when they are going to be necessary, and keep them deactivated at any other time. This has been a great improvement to avoid crashes.

In this repository I have posted the complete code, in a single file to facilitate its interpretation and portability.

With all these improvements, it was possible to generate a counter with an approximate frequency of 510 kHz, which would allow a precision of 2 microseconds, in principle sufficient for the temperature sensor data communication system.

It is quite hard to measure the real frequency reached. The system that I have used is based on measuring, with the mtime register, the time it takes to complete a certain count. It is important that the count lasts long enough, because the contador variable measures microseconds, but mtime runs in a millisecond range. For a statistical calculation I have repeatedly run this code:

*PLIC_I_EN |= (1 << 16);
*MTIME = 0;
do{} while (counter < 1000);
*PLIC_I_EN &= ~(1 << 16);
volatile float duration = (float) *MTIME / TIMER_FREQ;
volatile float freq = counter / duration;
  • PLIC interrupts are activated.
  • The stopwatch is reset.
  • The PWM-based counter is started. In this case, a long-term count is made (counter < 1000) to estimate an average value of the frequency.
  • Interrupts are turned off.
  • The observed frequency is calculated. As I mentioned before, around 510kHz, but with quite a bit of variability.

However, to understand how it behaves in the microsecond range, the counter loop should be around 10 to 20 (19 us – 39 us). But to know what’s really going on, you’d need an oscilloscope to display the behaviour of pin 10, I guess… If anyone dares to measure it I would appreciate it! :slight_smile:

I don’t know, but maybe this way of using such high PWM frequencies is not the right option, or I’m not managing it correctly.