PWM Sequence from HAL to NRFX

Hello

I am trying to change the code from the HAL code up to NRFX in order to make it event-based instead of using infinite while loops. 

I am using 52840 on a custom PCA with goal in mind to create a half-duplex-half-UART communication.

Working with HAL drivers, the code works as intended, but it is not optimized and is very power hungry for our needs. 

The HAL Setup and sending of the message (Byte by byte, each byte 10 bits at a time (Start - 8xData - Stop)): 

TxSetup()
{
    NRF_P0->DIRSET = ((1UL) << PIN_NEG); // PIN_NEG as output
    NRF_P0->OUTCLR = ((1UL) << PIN_NEG); // initial LOW on PIN_NEG

    NRF_P0->DIRSET = ((1UL) << PIN_POS); // PIN_POS as output
    NRF_P0->OUTSET = ((1UL) << PIN_POS); // initial HIGH on PIN_POS

    CounterTopRegister = F_CLK / F_PWM; // F_CLK = 16000000
    cyclesPerBit = F_PWM / LL_BAUDRATE; // F_PWM = 32400 ; LL_BAUDRATE = 1200
    P_PWM_HIGH = CounterTopRegister;
    P_PWM_LOW = (int)(0.5 * CounterTopRegister);
    N_PWM_HIGH = P_PWM_HIGH + 0x8000;
    N_PWM_LOW = P_PWM_LOW + 0x8000;

    NRF_PWM2->PSEL.OUT[0] = (PIN_POS << PWM_PSEL_OUT_PIN_Pos) | (PIN_POS << PWM_PSEL_OUT_PORT_Pos) | (PWM_PSEL_OUT_CONNECT_Connected << PWM_PSEL_OUT_CONNECT_Pos);
    NRF_PWM2->PSEL.OUT[1] = (PIN_NEG << PWM_PSEL_OUT_PIN_Pos) | (PIN_NEG << PWM_PSEL_OUT_PORT_Pos) | (PWM_PSEL_OUT_CONNECT_Connected << PWM_PSEL_OUT_CONNECT_Pos);
    NRF_PWM2->MODE = (PWM_MODE_UPDOWN_Up << PWM_MODE_UPDOWN_Pos);
    NRF_PWM2->PRESCALER = (PWM_PRESCALER_PRESCALER_DIV_1 << PWM_PRESCALER_PRESCALER_Pos);
    NRF_PWM2->COUNTERTOP = (CounterTopRegister << PWM_COUNTERTOP_COUNTERTOP_Pos);
    NRF_PWM2->LOOP = (0 << PWM_LOOP_CNT_Pos);
    NRF_PWM2->DECODER = (PWM_DECODER_LOAD_Individual << PWM_DECODER_LOAD_Pos) | (PWM_DECODER_MODE_RefreshCount << PWM_DECODER_MODE_Pos);
    NRF_PWM2->SEQ[0].PTR = ((uint32_t)(initialSequence) << PWM_SEQ_PTR_PTR_Pos);

    NRF_PWM2->SEQ[0].CNT = 44;
    NRF_PWM2->SEQ[0].REFRESH = cyclesPerBit - 1;
    NRF_PWM2->SEQ[0].ENDDELAY = 0;
}

void TxWrite(uint8_t txVal)
{
    int i;
    // Generate the Startbit (LOW)
    initialSequence[0] = P_PWM_LOW;
    initialSequence[1] = N_PWM_LOW;
    initialSequence[2] = 0;
    initialSequence[3] = 0;
    // Generate the 8 databits
    for (i = 1; i < 9; i++)
    {
        if (txVal & 1)
        {
            // Bit is HIGH
            initialSequence[4 * i + 0] = P_PWM_HIGH;
            initialSequence[4 * i + 1] = N_PWM_HIGH;
        }
        else
        {
            // Bit is LOW
            initialSequence[4 * i + 0] = P_PWM_LOW;
            initialSequence[4 * i + 1] = N_PWM_LOW;
        }
        initialSequence[4 * i + 2] = 0;
        initialSequence[4 * i + 3] = 0;
        txVal = txVal >> 1;
    }

    // Generate the Stopbit (HIGH)
    initialSequence[36] = P_PWM_HIGH;
    initialSequence[37] = N_PWM_HIGH;
    initialSequence[38] = 0;
    initialSequence[39] = 0;
    initialSequence[40] = P_PWM_HIGH;
    initialSequence[41] = N_PWM_HIGH;
    initialSequence[42] = 0;
    initialSequence[43] = 0;

    NRF_PWM2->ENABLE = (PWM_ENABLE_ENABLE_Enabled << PWM_ENABLE_ENABLE_Pos);

    NRF_PWM2->TASKS_SEQSTART[0] = 1;
}

void TxWaitDone()
{

    while (NRF_PWM2->EVENTS_SEQEND[0] == 0)
        ;
    NRF_PWM2->EVENTS_SEQEND[0] = 0;
}

void TxStop()
{

    NRF_PWM2->TASKS_STOP = 1;

    while (NRF_PWM2->EVENTS_STOPPED == 0)
        ;
    NRF_PWM2->EVENTS_STOPPED = 0;

    NRF_PWM2->ENABLE = (PWM_ENABLE_ENABLE_Disabled << PWM_ENABLE_ENABLE_Pos);
}


WriteMessage(uint8_t *msg, uint8_t len)
{
    nrfx_lpcomp_disable();
    NRFX_IRQ_DISABLE(SAADC_IRQn);
    for (int i = 0; i < len; i++)
    {
        TxWrite(msg[i]);
        TxWaitDone();
        TxStop();
    }
    lpcomp_event_count = 0;
    is_carrier_found = 0;
    NRFX_IRQ_ENABLE(COMP_LPCOMP_IRQn);
    nrfx_lpcomp_enable();
    return 0;
}

The issue with this approach, again, is the while loops that, among other issues, can sometimes get stuck and hang the rest of the process. 

The actual issue rewriting this with NRFX drivers is the fact that I cannot, for the life of me, figure out how to write out my "initialSequence." I can get the PWM to toggle on or off, but not actually make it do that in sequence as I need it to. I tried following a few examples I found on Devzone, but nothing concrete enough to help me with my issue. 

The HAL Implementation repeats every 100 milliseconds, so the NRFX one should be able to match that. If anyone has an idea of how to write this using the NRFX drivers, or to point me in the direction of a set-up example, I'd be very grateful. 
To add to this: The idea is for this code to run along with a Zephyr based system that runs the rest of the functionality of the board (Thus why we're trying to make it event based as opposed to current implementation).

Thank you in advanced for response. If you need any more information, please ask. 

EDIT: Added the missing code that was calling the TX sequence in order. (Write, Wait, Stop)

  • I should add the current (latest) attempt at implementing, so maybe that could be corrected in the right way:

    nrfx_pwm_t pwm = NRFX_PWM_INSTANCE(2);
    static nrf_pwm_values_individual_t pwm_values[2];
    nrf_pwm_sequence_t pwm_sequence;
    
    void pwm_tx_init()
    {
        nrfx_pwm_config_t pwm_config;
    
        pwm_config.output_pins[0] = PIN_POS;
        pwm_config.output_pins[1] = PIN_NEG;
        pwm_config.output_pins[2] = NRFX_PWM_PIN_NOT_USED;
        pwm_config.output_pins[3] = NRFX_PWM_PIN_NOT_USED;
    
        pwm_config.base_clock = NRF_PWM_CLK_16MHz;
        pwm_config.count_mode = NRF_PWM_MODE_UP;
        pwm_config.top_value = (F_CLK / F_PWM);
        pwm_config.load_mode = NRF_PWM_LOAD_WAVE_FORM;
        pwm_config.step_mode = NRF_PWM_STEP_AUTO;
        pwm_config.skip_gpio_cfg = false;
        pwm_config.skip_psel_cfg = false;
        pwm_config.irq_priority = 5;
    
        nrfx_pwm_init(&pwm, &pwm_config, pwm_transmit_evt_handler, 0);
        IRQ_DIRECT_CONNECT(COMP_LPCOMP_IRQn, 5, nrfx_pwm_2_irq_handler, 0);
    }
    
    
    void pwm_tx_transmit(uint8_t *msg)
    {
        set_sequence(0x80);
        nrf_pwm_values_t sequence_values;
        sequence_values.p_raw = initialSequence;
        nrf_pwm_sequence_t sequence1;
        sequence1.repeats = (F_PWM / LL_BAUDRATE) - 1;
        sequence1.length = 44;
        sequence1.end_delay = 0;
        sequence1.values = sequence_values;
       
    }
    void make_sequence(uint16_t duty_p_p, uint16_t duty_p_n, uint16_t duty_n_p, uint16_t duty_n_n)
    {
    
        pwm_sequence.values.p_individual = pwm_values;
        pwm_sequence.length = NRF_PWM_VALUES_LENGTH(pwm_values);
        uint32_t repeats = (F_PWM / LL_BAUDRATE) - 1;
        pwm_sequence.repeats = repeats;
        pwm_sequence.end_delay = 0;
    
        pwm_values[0].channel_0 = duty_p_p;
        pwm_values[0].channel_1 = duty_p_n;
    
        pwm_values[1].channel_0 = duty_n_p;
        pwm_values[1].channel_1 = duty_n_n;
        nrfx_pwm_simple_playback(&pwm, &pwm_sequence, 1, NRFX_PWM_FLAG_STOP);
    }

    This is just a few itterations after, at this point I'm unsure if it has any relation to my initial sequence anyway. 
    The "initialSequence" array is filled in the same way as in the HAL implementation.

  •    I feel I should add the way the signal looks as well with HAL(first picture) and   NRFX (Second picture)

  • Assuming you can't just use the nRF52840 UART, typically using the PWM would involve constructing the complete message sequence in a table then just transmit the table as a single operation. Here is a pedantic example <STX><ETX>, using waveform mode although since all the countertop values are the same wavefom mode is not strictly necessary but allows controlling the hardware direction with the 3rd output pin (DE /RE)). Grouped Mode could be used to save space, but Waveform is the example I show here. I personally would encode all possible 256 bytes in 256 separate tables in Flash and simply copy the character sequences to RAM to construct a message, then send the entire message in one PWM burst, more efficient long-term in terms of power consumption. Note for greatest accuracy (unless you switch to Manchester encoding, easily done) the HFCLK has to stay active for the duration of transmission. With Manchester encoding that is not required, leading to power savings. Manchester_code

    #define LL_BAUDRATE      1200
    #define F_PWM           32400
    #define F_CLK        16000000
    #define COUNTER_TOP  ((F_CLK+(F_PWM/2)) / F_PWM) // F_CLK = 16000000
    #define BIT_LOW            0              // Normal encoding
    #define BIT_HIGH   (COUNTER_TOP)          // Normal encoding
    //#define BIT_LOW    ((2*COUNTER_TOP)/3)  // Manchester encoding 2/3 bit
    //#define BIT_HIGH   (COUNTER_TOP/3)      // Manchester encoding 1/3 bit
    #define STX_BYTE 0x02 // Example start character: STX (^B)
    #define ETX_BYTE 0x03 // Example start character: ETX (^C)
    
    // Example STX byte in little-endian format
    #define STX_BIT_0 (((STX_BYTE>>0) & 0x01) ? BIT_HIGH : BIT_LOW)
    #define STX_BIT_1 (((STX_BYTE>>1) & 0x01) ? BIT_HIGH : BIT_LOW)
    #define STX_BIT_2 (((STX_BYTE>>2) & 0x01) ? BIT_HIGH : BIT_LOW)
    #define STX_BIT_3 (((STX_BYTE>>3) & 0x01) ? BIT_HIGH : BIT_LOW)
    #define STX_BIT_4 (((STX_BYTE>>4) & 0x01) ? BIT_HIGH : BIT_LOW)
    #define STX_BIT_5 (((STX_BYTE>>5) & 0x01) ? BIT_HIGH : BIT_LOW)
    #define STX_BIT_6 (((STX_BYTE>>6) & 0x01) ? BIT_HIGH : BIT_LOW)
    #define STX_BIT_7 (((STX_BYTE>>7) & 0x01) ? BIT_HIGH : BIT_LOW)
    // Example ETX byte in little-endian format
    #define ETX_BIT_0 (((ETX_BYTE>>0) & 0x01) ? BIT_HIGH : BIT_LOW)
    #define ETX_BIT_1 (((ETX_BYTE>>1) & 0x01) ? BIT_HIGH : BIT_LOW)
    #define ETX_BIT_2 (((ETX_BYTE>>2) & 0x01) ? BIT_HIGH : BIT_LOW)
    #define ETX_BIT_3 (((ETX_BYTE>>3) & 0x01) ? BIT_HIGH : BIT_LOW)
    #define ETX_BIT_4 (((ETX_BYTE>>4) & 0x01) ? BIT_HIGH : BIT_LOW)
    #define ETX_BIT_5 (((ETX_BYTE>>5) & 0x01) ? BIT_HIGH : BIT_LOW)
    #define ETX_BIT_6 (((ETX_BYTE>>6) & 0x01) ? BIT_HIGH : BIT_LOW)
    #define ETX_BIT_7 (((ETX_BYTE>>7) & 0x01) ? BIT_HIGH : BIT_LOW)
    
    nrf_pwm_values_wave_form_t halfDuplexUartMsg[] = {
      //   Index   Normal pin          Inverted     (DE/RE)  Top Value
      //   =====   =================== ==========   =======  =========
    //{ /* -:-  */ 0x8000|(BIT_HIGH),  (BIT_HIGH),  BIT_HIGH, COUNTER_TOP }, // RS485 start-of-packet pre-drive (optional, 1st byte only)
      { /* 0:0  */ 0x8000|(BIT_LOW),   (BIT_LOW),   BIT_HIGH, COUNTER_TOP }, // Start bit
      { /* 0:1  */ 0x8000|(STX_BIT_0), (STX_BIT_0), BIT_HIGH, COUNTER_TOP },
      { /* 0:2  */ 0x8000|(STX_BIT_1), (STX_BIT_1), BIT_HIGH, COUNTER_TOP },
      { /* 0:3  */ 0x8000|(STX_BIT_2), (STX_BIT_2), BIT_HIGH, COUNTER_TOP },
      { /* 0:4  */ 0x8000|(STX_BIT_3), (STX_BIT_3), BIT_HIGH, COUNTER_TOP },
      { /* 0:5  */ 0x8000|(STX_BIT_4), (STX_BIT_4), BIT_HIGH, COUNTER_TOP },
      { /* 0:6  */ 0x8000|(STX_BIT_5), (STX_BIT_5), BIT_HIGH, COUNTER_TOP },
      { /* 0:7  */ 0x8000|(STX_BIT_6), (STX_BIT_6), BIT_HIGH, COUNTER_TOP },
      { /* 0:8  */ 0x8000|(STX_BIT_7), (STX_BIT_7), BIT_HIGH, COUNTER_TOP },
      { /* 0:9  */ 0x8000|(BIT_HIGH),  (BIT_HIGH),  BIT_HIGH, COUNTER_TOP }, // Stop bit 1
    
      { /* 1:0  */ 0x8000|(BIT_LOW),   (BIT_LOW),   BIT_HIGH, COUNTER_TOP }, // Start bit
      { /* 1:1  */ 0x8000|(ETX_BIT_0), (ETX_BIT_0), BIT_HIGH, COUNTER_TOP },
      { /* 1:2  */ 0x8000|(ETX_BIT_1), (ETX_BIT_1), BIT_HIGH, COUNTER_TOP },
      { /* 1:3  */ 0x8000|(ETX_BIT_2), (ETX_BIT_2), BIT_HIGH, COUNTER_TOP },
      { /* 1:4  */ 0x8000|(ETX_BIT_3), (ETX_BIT_3), BIT_HIGH, COUNTER_TOP },
      { /* 1:5  */ 0x8000|(ETX_BIT_4), (ETX_BIT_4), BIT_HIGH, COUNTER_TOP },
      { /* 1:6  */ 0x8000|(ETX_BIT_5), (ETX_BIT_5), BIT_HIGH, COUNTER_TOP },
      { /* 1:7  */ 0x8000|(ETX_BIT_6), (ETX_BIT_6), BIT_HIGH, COUNTER_TOP },
      { /* 1:8  */ 0x8000|(ETX_BIT_7), (ETX_BIT_7), BIT_HIGH, COUNTER_TOP },
      { /* 1:9  */ 0x8000|(BIT_HIGH),  (BIT_HIGH),  BIT_HIGH, COUNTER_TOP }, // Stop bit 1
    
      { /* 1:10 */ 0x8000|(BIT_HIGH),  (BIT_HIGH),  BIT_LOW,  COUNTER_TOP }, // Stop bit 2 (optional, drive RS483 DE/RE lo here)
    };
    #define NUM_PWM_ITERATIONS  ( sizeof(halfDuplexUartMsg)/sizeof(halfDuplexUartMsg[0].channel_0) )
    #define NUM_PWM_TABLE_LINES ( sizeof(halfDuplexUartMsg)/sizeof(halfDuplexUartMsg[0]) )
    STATIC_ASSERT(sizeof(nrf_pwm_values_wave_form_t) == sizeof(halfDuplexUartMsg[0]), "halfDuplexUartMsg line size");
    
    //  NUM_PWM_ITERATIONS WaveLength (.CNT) is 15-bit Amount of values (duty cycles) in this sequence

    If this is an RS485 application such as Modbus or similar it is helpful to embed the RS485 control pin in the table as above then timing of enable/disable of the driver is deterministic.

    Edit: Adding the Grouped Mode table:

    // Example using grouped, notes uses pin channels 0 and 2
    nrf_pwm_values_grouped_t halfDuplexUartMsg[] = {
      //   Index   Normal pin 0        Inverted pin 2   
      //   =====   =================== ============== 
      { /* 0:0  */ 0x8000|(BIT_LOW),   (BIT_LOW),  }, // Start bit
      { /* 0:1  */ 0x8000|(STX_BIT_0), (STX_BIT_0) },
      { /* 0:2  */ 0x8000|(STX_BIT_1), (STX_BIT_1) },
      { /* 0:3  */ 0x8000|(STX_BIT_2), (STX_BIT_2) },
      { /* 0:4  */ 0x8000|(STX_BIT_3), (STX_BIT_3) },
      { /* 0:5  */ 0x8000|(STX_BIT_4), (STX_BIT_4) },
      { /* 0:6  */ 0x8000|(STX_BIT_5), (STX_BIT_5) },
      { /* 0:7  */ 0x8000|(STX_BIT_6), (STX_BIT_6) },
      { /* 0:8  */ 0x8000|(STX_BIT_7), (STX_BIT_7) },
      { /* 0:9  */ 0x8000|(BIT_HIGH),  (BIT_HIGH), }, // Stop bit 1
    
      { /* 1:0  */ 0x8000|(BIT_LOW),   (BIT_LOW),  }, // Start bit
      { /* 1:1  */ 0x8000|(ETX_BIT_0), (ETX_BIT_0) },
      { /* 1:2  */ 0x8000|(ETX_BIT_1), (ETX_BIT_1) },
      { /* 1:3  */ 0x8000|(ETX_BIT_2), (ETX_BIT_2) },
      { /* 1:4  */ 0x8000|(ETX_BIT_3), (ETX_BIT_3) },
      { /* 1:5  */ 0x8000|(ETX_BIT_4), (ETX_BIT_4) },
      { /* 1:6  */ 0x8000|(ETX_BIT_5), (ETX_BIT_5) },
      { /* 1:7  */ 0x8000|(ETX_BIT_6), (ETX_BIT_6) },
      { /* 1:8  */ 0x8000|(ETX_BIT_7), (ETX_BIT_7) },
      { /* 1:9  */ 0x8000|(BIT_HIGH),  (BIT_HIGH), }, // Stop bit 1
    };
    #define NUM_PWM_ITERATIONS  ( sizeof(halfDuplexUartMsg)/sizeof(halfDuplexUartMsg[0].channel_0) )
    #define NUM_PWM_TABLE_LINES ( sizeof(halfDuplexUartMsg)/sizeof(halfDuplexUartMsg[0]) )
    STATIC_ASSERT(sizeof(nrf_pwm_values_grouped_t) == sizeof(halfDuplexUartMsg[0]), "halfDuplexUart line size");
    

  • Hi, thank you for the response!
    Unfortunaetly, your assumption is correct. I cannot just use the UART. 
    You may notice that in my code the table is being populated in a loop, because it allows for a more dynamic way of constructing a message. (Or it did in this case). I am unsure it's the best way of doing it, but it's what worked at the time.

    There is an extra limitation to the system, and that's the receiving end, which has FW that cannot be changed at this time, so Manchester Encoding is also not an option. The same limitation prevents burst sending.

    I appreciate the way you've constructed the message, but it still doesn't answer the question of: How do I actually send it, so that it mimics the behavior of the HAL version? With the current way my PWM instance is set up, even with your message, I still get the same gitter-y output. 

    If you have any comments on what is wrong with the current set up of the PWM sequence, or the PWM config, that'd be great! 
    Thus far, I've tried both the wave form and the groupped values. Each mode provides the same output signal, and sadly it's not the correct one. 

  • I can't get this reply to line up, but anyway I see your issue. The PWM continues to play the last (maybe only)  cycle indefinitely until you issue a STOP TASK and wait for the STOPPED Event. This can be automated by setting SHORTS:

       NRF_PWM_Type * const pNRF_PWM = NRF_PWM2;
       // PWM_SHORTS_SEQEND0_STOP_Msk         Shortcut between SEQEND[0] event and STOP task.
       // PWM_SHORTS_SEQEND1_STOP_Msk         Shortcut between SEQEND[1] event and STOP task.
       // PWM_SHORTS_LOOPSDONE_SEQSTART0_Msk  Shortcut between LOOPSDONE event and SEQSTART[0] task.
       // PWM_SHORTS_LOOPSDONE_SEQSTART1_Msk  Shortcut between LOOPSDONE event and SEQSTART[1] task.
       // PWM_SHORTS_LOOPSDONE_STOP_Msk        Shortcut between LOOPSDONE event and STOP task.
       pNRF_PWM->SHORTS = NRF_PWM_SHORT_LOOPSDONE_STOP_MASK;

    "After the last value in the sequence has been loaded and started executing, a SEQEND[n] event is generated. The PWM generation will then continue with the last loaded value."

Related