PWM base clock frequency periodically changing

I have an Arduino Nano 33 BLE using the nRF52840, and I'm trying to use PWM to output audio.  As a test I have the PWM configured to output a 440 Hz sine wave using no prescaler (so 16 MHz clock) and a CLOCKTOP of 400 resulting in a PWM frequency of 40kHz for audio.  I also have two LEDs running on the same PWM peripheral at 1Hz and 2Hz.  The issue I'm having is that for about 4 seconds everything is (mostly) fine and I get a 440 Hz tone out of the speaker, then suddenly the frequency drops to 436 Hz (15 cents flat of A4) then after about 30 seconds it changes to 470 Hz (15 cents sharp of A#4) then it will drop to 403 Hz then jump to 537 Hz where it will stay (until it stops completely because of floating point precision error in my code).

these frequency changes are always to the same frequency and always at the same time.  The two LEDs can also be visually seen to be blinking at different rates as the audio frequency changes.


I'm also having a secondary issue of the tone having an added buzzing sound to it that shouldn't be there, I have not looked further into this yet

float cos_table[256];

constexpr float _2_PI = 2.0f * 3.14159265358979f;

static uint32_t out_pins[4] = {43, 6, 13, NRF_PWM_PIN_NOT_CONNECTED};
static uint16_t pwm_duties[4*256];

float t = 0;
float dt = 1.0f / 40000.0f;

float cos_(float x) {
  x = x - (int)x;
  x *= 256.0f;
  int x_ = (int)x;
  return cos_table[x_];
}

void get_data() {
  Serial.println("Data Get Begin");
  for (int i = 0; i < 256; i++) {
    float value1 = (cos_(t * 440.0f) * 0.2f + 0.5f);
    float value2 = (cos_(t * 1.0f) * 0.5f + 0.5f);
    float value3 = (cos_(t * 2.0f) * 0.5f + 0.5f);
    t += dt;
    pwm_duties[i * 4 + 0] = (uint16_t) (value1 * 400.0f);
    pwm_duties[i * 4 + 1] = (uint16_t) (value2 * 400.0f);
    pwm_duties[i * 4 + 2] = (uint16_t) (value3 * 400.0f);
  }
  Serial.println("Data Get End");
}

void setup() {

  for (int i = 0; i < 256; i++) {
    cos_table[i] = cos(i / 256.0f * _2_PI);
  }
  memset(pwm_duties, 0, sizeof(pwm_duties));
  
}

void loop() {
  
  nrf_pwm_pins_set(NRF_PWM0, out_pins);
  nrf_pwm_enable(NRF_PWM0);
  nrf_pwm_configure(NRF_PWM0, NRF_PWM_CLK_16MHz, NRF_PWM_MODE_UP, 400);
  nrf_pwm_loop_set(NRF_PWM0, 1);
  nrf_pwm_decoder_set(NRF_PWM0, NRF_PWM_LOAD_INDIVIDUAL, NRF_PWM_STEP_AUTO);
  nrf_pwm_seq_ptr_set(NRF_PWM0, 0, pwm_duties);
  nrf_pwm_seq_cnt_set(NRF_PWM0, 0, ((sizeof(pwm_duties) / sizeof(uint16_t)) << PWM_SEQ_CNT_CNT_Pos));
  nrf_pwm_seq_refresh_set(NRF_PWM0, 0, 0);
  nrf_pwm_seq_end_delay_set(NRF_PWM0, 0, 0);
  nrf_pwm_seq_ptr_set(NRF_PWM0, 1, pwm_duties);
  nrf_pwm_seq_cnt_set(NRF_PWM0, 1, ((sizeof(pwm_duties) / sizeof(uint16_t)) << PWM_SEQ_CNT_CNT_Pos));
  nrf_pwm_seq_refresh_set(NRF_PWM0, 1, 0);
  nrf_pwm_seq_end_delay_set(NRF_PWM0, 1, 0);
  nrf_pwm_shorts_enable(NRF_PWM0, NRF_PWM_SHORT_LOOPSDONE_SEQSTART0_MASK);
  nrf_pwm_task_trigger(NRF_PWM0, NRF_PWM_TASK_SEQSTART0);

  while(1) {
    if (nrf_pwm_event_check(NRF_PWM0, NRF_PWM_EVENT_SEQEND0)) {
      nrf_pwm_event_clear(NRF_PWM0, NRF_PWM_EVENT_SEQEND0);
      get_data();
    }
    if (nrf_pwm_event_check(NRF_PWM0, NRF_PWM_EVENT_SEQEND1)) {
      nrf_pwm_event_clear(NRF_PWM0, NRF_PWM_EVENT_SEQEND1);
      get_data();
    }
  }
}

Parents
  • For the buzzing, I set the sin wave frequency to 400Hz to get exactly 100 PWM cycles per sin wave cycle and I set the buffer to 25 duties to get exactly 4 buffer refreshes per sine wave cycle.  The result is what is in the attached image, you can very clearly see a voltage jump every time the PWM loop restarts. 
    (edit: I also was only using sequence 1 and not sequence 0 in this test)


    Oscilloscope picture showing a voltage jump every buffer refresh

  • it seems my code can't swap the buffer fast enough and the last two values of the previous frame get reused before the buffer is swapped.  Unfortunately I can't seem to get PWM interrupts to work in order to swap the buffer fast enough.

    void pwm_handler(nrfx_pwm_evt_type_t event_type) {
      Serial.println(event_type);
      switch (event_type) {
        case NRFX_PWM_EVT_END_SEQ0:
        case NRFX_PWM_EVT_END_SEQ1:
          get_data();
          break;
      }
    }
    
    ...
    
      nrfx_err_t err = nrfx_pwm_init(&p_instance, &p_config, pwm_handler);
      Serial.println(err);
      nrfx_pwm_simple_playback(&p_instance, &p_sequence, 1, NRFX_PWM_FLAG_LOOP | NRFX_PWM_FLAG_SIGNAL_END_SEQ0 | NRFX_PWM_FLAG_SIGNAL_END_SEQ1);


    `pwm_handler` is never called

  • turns out it probably was being called, but having Serial inside the IRQ freezes the system so I thought it wasn't being called, so IRQ is working now.  I was also able to get rid of the buzzing caused by the buffer not being refreshed fast enough, but I needed to use a tripple buffer and I don't know why (double buffer got rid of every other jump)

    void swap_buffer() {
      nrf_pwm_seq_ptr_set(NRF_PWM0, 1, pwm_duties[buffer_id]);
      buffer_id = (buffer_id + 1) % NUM_BUFFERS;
      buffer_flag = true;
    }
    
    void pwm_irq_handler(void) {
      nrf_pwm_event_clear(NRF_PWM0, NRF_PWM_EVENT_SEQEND1);
      swap_buffer();
      while(NRF_PWM0->EVENTS_SEQEND[1]);
    }


    here is my code for the tripple buffering, NUM_BUFFERS needs to be set to 3 to remove the buzzing.  (the data is filled into the buffer outside of the IRQ)

    though I just realized that this is still basically double buffering because the 3rd buffer is just sitting idle with old data, so now I'm even more confused about why 3 buffers work and 2 don't.

Reply
  • turns out it probably was being called, but having Serial inside the IRQ freezes the system so I thought it wasn't being called, so IRQ is working now.  I was also able to get rid of the buzzing caused by the buffer not being refreshed fast enough, but I needed to use a tripple buffer and I don't know why (double buffer got rid of every other jump)

    void swap_buffer() {
      nrf_pwm_seq_ptr_set(NRF_PWM0, 1, pwm_duties[buffer_id]);
      buffer_id = (buffer_id + 1) % NUM_BUFFERS;
      buffer_flag = true;
    }
    
    void pwm_irq_handler(void) {
      nrf_pwm_event_clear(NRF_PWM0, NRF_PWM_EVENT_SEQEND1);
      swap_buffer();
      while(NRF_PWM0->EVENTS_SEQEND[1]);
    }


    here is my code for the tripple buffering, NUM_BUFFERS needs to be set to 3 to remove the buzzing.  (the data is filled into the buffer outside of the IRQ)

    though I just realized that this is still basically double buffering because the 3rd buffer is just sitting idle with old data, so now I'm even more confused about why 3 buffers work and 2 don't.

Children
Related