Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

analogWrite() problems with PWM #705

Open
tedtoal opened this issue Jul 22, 2023 · 0 comments
Open

analogWrite() problems with PWM #705

tedtoal opened this issue Jul 22, 2023 · 0 comments

Comments

@tedtoal
Copy link

tedtoal commented Jul 22, 2023

analogWrite() has some problems with regard to using a timer for pulse width modulation:

  1. It uses a fixed value of 0xFFFF for the waveform period with a TCC timer. The user might very well want to change the period on the fly.

  2. On the first call to analogWrite() for a given timer, the timer is initialized and the CC register is written, but not the CCB register. On subsequent calls the timer is not initialized and the CCB register is written. Initialization should be separate from setting/changing the on-time and period, and the user should be able to re-initialize a timer whenever he wants.

  3. A minor issue is that the variable name 'value' is sort of misleading when it comes to PWM, as both the on-time and period are "values" that determine the PWM waveform. I suggest renaming 'value' to 'on_time' in the new functions I suggest below (which are PWM-specific).

  4. Some TCC timers are 16-bits and some are 24-bits and it varies depending on SAMD flavor. The user should have access to the size programmatically so he can make decisions regarding setting of resolution. Furthermore, the PWM period must never be larger than the largest number available with the user-set PWM resolution (2^resolution-1). Also if that resolution is larger than the number of TCC bits, the period and on-time should be mapped from the user resolution to TCC timer resolution, and that is the ONLY time it should be mapped. (Unlike with ADC, where a DIFFERENCE between the resolutions implies mapping; with PWM, the resolution may be set to the largest period used and the user is probably not concerned that a potentially LARGER resolution could be used with that TCC).

I suggest these fixes:

  1. Move this code section outside of analogWrite() to the file level so it can be used by additional functions below:
// Define a table of clock control IDs for initializing GCLK.
static const uint16_t GCLK_CLKCTRL_IDs[] = {
  GCLK_CLKCTRL_ID(GCM_TCC0_TCC1), // TCC0
  GCLK_CLKCTRL_ID(GCM_TCC0_TCC1), // TCC1
  GCLK_CLKCTRL_ID(GCM_TCC2_TC3),  // TCC2
  GCLK_CLKCTRL_ID(GCM_TCC2_TC3),  // TC3
  GCLK_CLKCTRL_ID(GCM_TC4_TC5),   // TC4
  GCLK_CLKCTRL_ID(GCM_TC4_TC5),   // TC5
  GCLK_CLKCTRL_ID(GCM_TC6_TC7),   // TC6
  GCLK_CLKCTRL_ID(GCM_TC6_TC7),   // TC7
};
  1. Add function analogGetResolution_TCC_SAMD_TT() for getting the TCC resolution (bits):
/**************************************************************************/
/*!
  @brief    Return the resolution of a TCC.
  @param    pin         Arduino pin number assigned to TCC, an index into table
                        g_APinDescription[] in SAMD core file variant.cpp.
  @returns  16 or 24 if TCC is a 16-bit or 24-bit timer, 0 if 'pin' is non-PWM
            or is a TC-type timer.
  @note     This resets the TCC timer, it must be reinitialized to use.
  @note     A portion of TCC timer initialization is done here, which is relied
            upon by analogStartPWM_TCC_SAMD_TT().
*/
/**************************************************************************/
int analogGetResolution_TCC_SAMD_TT(pin_size_t pin) {
  PinDescription pinDesc = g_APinDescription[pin];

  // Return 0 if non-PWM pin specified.
  uint32_t attr = pinDesc.ulPinAttribute;
  if ((attr & PIN_ATTR_PWM) != PIN_ATTR_PWM)
    return(0);

  // Return 0 if TC-type timer rather than TCC timer.
  uint32_t tcNum = GetTCNumber(pinDesc.ulPWMChannel);
  if (tcNum >= TCC_INST_NUM)
    return(0);

  uint8_t tcChannel = GetTCChannelNumber(pinDesc.ulPWMChannel);

  // Configure the pin.
  if (attr & PIN_ATTR_TIMER) {
    #if !(ARDUINO_SAMD_VARIANT_COMPLIANCE >= 10603)
    // Compatibility for cores based on SAMD core <=1.6.2
    if (pinDesc.ulPinType == PIO_TIMER_ALT) {
      pinPeripheral(pin, PIO_TIMER_ALT);
    } else
    #endif
    {
      pinPeripheral(pin, PIO_TIMER);
    }
    // We suppose that attr has PIN_ATTR_TIMER_ALT bit set...
    pinPeripheral(pin, PIO_TIMER_ALT);

  GCLK->CLKCTRL.reg = (uint16_t) (GCLK_CLKCTRL_CLKEN | 
   GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_IDs[tcNum]);
  while (GCLK->STATUS.bit.SYNCBUSY == 1);

  // Get the TCC pointer.
  Tcc* TCCx = (Tcc*) GetTC(pinDesc.ulPWMChannel);

  // Disable TCC.
  TCCx->CTRLA.bit.ENABLE = 0;
  syncTCC(TCCx);

  // Set TCC as normal PWM
  TCCx->WAVE.reg |= TCC_WAVE_WAVEGEN_NPWM;
  syncTCC(TCCx);

  // Test if TCC is 24-bit. If not, it is 16-bit.
  TCCx->PER.reg = 0xFFFFFFUL;
  if (TCCx->PER.reg == 0xFFFFFFUL)
    return(24);
  return(16);
}
  1. Add function analogStartPWM_TCC() for starting the PWM:
/**************************************************************************/
/*!
  @brief    Initialize the PWM and set its initial period and duty cycle.
  @param    pin         Arduino pin number to write, an index into table
                        g_APinDescription[] in SAMD core file variant.cpp.
  @returns  TCC timer resolution in bits, 16 or 24 if TCC is a 16-bit or 24-bit
            timer, or 0 if 'pin' is non-PWM or is a TC-type timer.
  @note     This supports PWM only on TCC timers.
  @note     Single-slope (NPWM) mode is used.
  @note     Dithering is not enabled.
  @note     The generic clock used depends on the TCC which depends on the pin,
            see g_APinDescription[]. It will be either GCM_TCC0_TCC1 or
            GCM_TCC2_TC3 for the SAMD21G.
  @note     This initializes the pulse on-time to 0 and pulse width to an
            arbitrary setting, so the PWM output remains off until you call
            analogSetPWM_TCC_SAMD_TT() to set a pulse on-time and period.
*/
/**************************************************************************/
int analogStartPWM_TCC_SAMD_TT(pin_size_t pin)
{
  int res = analogGetResolution_TCC_SAMD_TT(pin);
  if (res == 0)
    return(0);
  PinDescription pinDesc = g_APinDescription[pin];
  uint8_t tcChannel = GetTCChannelNumber(pinDesc.ulPWMChannel);
  Tcc* TCCx = (Tcc*) GetTC(pinDesc.ulPWMChannel);

  // Set the initial CC and CCB register values to 0.
  TCCx->CC[tcChannel].reg = 0;
  syncTCC(TCCx);
  TCCx->CCB[tcChannel].reg = 0;
  syncTCC(TCCx);
  // Set both PER and PERB to an arbitrary non-zero value. Since the minimum TCC
  // size is 16 bits, 0xFFFF will work fine.
  TCCx->PER.reg = 0xFFFF;
  syncTCC(TCCx);
  TCCx->PERB.reg = 0xFFFF;
  syncTCC(TCCx);
  // Enable TCCx
  TCCx->CTRLA.bit.ENABLE = 1;
  syncTCC(TCCx);
  return(res);
}
  1. Add function analogSetPWM_TCC() for setting/changing the PWM on-time and period values:
/**************************************************************************/
/*!
  @brief    Update the PWM on-time and period. It must already be initialized.
  @param    pin         Arduino pin number to write, an index into table
                        g_APinDescription[] in SAMD core file variant.cpp.
  @param    res         The TCC timer resolution in bits, as returned by
                        analogStartPWM_TCC_SAMD_TT().
  @param    on_time     The pulse on-time value, 0 <= on_time <= period. This is
                        mapped linearly FROM the resolution requested by calling
                        one of the functions analogWriteResolution_PWM_SAMD_TT()
                        or analogWriteResolution_SAMD_TT() TO the ACTUAL
                        resolution of the TCC timer implied by the 'pin'
                        argument ONLY IF THAT ACTUAL RESOLUTION IS LOWER THAN
                        THE RESOLUTION THAT WAS REQUESTED. For example, if
                        requested resolution is 16 bits and the TCC is 24 bits,
                        no mapping is done, but if the requested resolution is
                        24 bits and the TCC is 16 bits, mapping is done.
  @param    period      The initial PWM period, 1 <= period <= 2^resolution-1 of
                        resolution set with analogWriteResolution_PWM_SAMD_TT().
                        This is also mapped linearly in the same way as'on_time.
                            100% * (on_time/period).
  @returns  true if successful, false if 'pin' is non-PWM or is a TC-type timer
            or on_time > period or period > 2^res-1.
*/
/**************************************************************************/
bool analogSetPWM_TCC_SAMD_TT(pin_size_t pin, int res, uint32_t on_time,
    uint32_t period)
{
  if (res == 0 || on_time > period || period > ((1UL << res)-1))
    return(false);
  PinDescription pinDesc = g_APinDescription[pin];
  uint8_t tcChannel = GetTCChannelNumber(pinDesc.ulPWMChannel);

  // Get the TCC pointer.
  Tcc* TCCx = (Tcc*) GetTC(pinDesc.ulPWMChannel);

  // Update the TCC.
  TCCx->CTRLBSET.bit.LUPD = 1;
  syncTCC(TCCx);
  if (res < _writeResolution_PWM)
    on_time = mapResolution(on_time, _writeResolution_PWM, res);
  TCCx->CCB[tcChannel].reg = on_time;
  syncTCC(TCCx);
  if (res < _writeResolution_PWM)
    period = mapResolution(period, _writeResolution_PWM, res);
  TCCx->PERB.reg = period;
  syncTCC(TCCx);
  TCCx->CTRLBCLR.bit.LUPD = 1;
  syncTCC(TCCx);
  return(true);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant