Mirror with some correction

Dependencies:   mbed FastIO FastPWM USBDevice

Embed: (wiki syntax)

« Back to documentation index

Show/hide line numbers NewPwm.h Source File

NewPwm.h

00001 // New PWM 
00002 //
00003 // This is a replacement for the mbed PwmOut class.  It's both stripped
00004 // down and beefed up.  It's stripped down to just the functionality we 
00005 // need in the Pinscape code, and to a purely KL25Z implementation, which
00006 // allows for a smaller memory footprint per instance.  It's beefed up to
00007 // correct a number of problems in the mbed implementation.  
00008 //
00009 // Note that this class isn't quite API-compatible with the mbed version.
00010 // We make the channel/TPM unit structure explicit, and we put the period()
00011 // method (to change the PWM cycle time) on the unit object rather than the
00012 // channel.  We do this to emphasize in the API that the period is a property
00013 // of the unit (which contains multiple channels) rather than the channel.
00014 // The mbed library is misleading when it pretends that the period is a
00015 // property of the channel, since this confusingly suggests that a channel's
00016 // period can be set independently.  It can't; the period can only be set for
00017 // the whole group of channels controlled by a unit.
00018 //
00019 // Improvements over the mbed version:
00020 //
00021 // 1. We provide an alternative, non-glitching version of write().  The mbed
00022 // version of write(), and our default version with the same name, causes a 
00023 // glitch on every write by resetting the TPM counter, which cuts the cycle
00024 // short and causes a momentary drop in brightness (from the short cycle) 
00025 // that's visible if an LED is connected.  This is particularly noticeable 
00026 // when doing a series of rapid writes, such as when fading a light on or off.
00027 //
00028 // We offer a version of write() that doesn't reset the counter, avoiding the 
00029 // glitch.  This version skips the counter reset that the default version does.
00030 //
00031 // But this must be used with caution, because there's a whole separate
00032 // problem if you don't reset the counter, which is why the mbed library
00033 // does this by default.  The KL25Z hardware only allows the value register
00034 // to be written once per PWM cycle; if it's written more than once, the
00035 // second and subsequent writes are simply ignored, so those updates will
00036 // be forever lost.  The counter reset, in addition to casuing the glitch,
00037 // resets the cycle and thus avoids the one-write-per-cycle limitation.
00038 // Callers using the non-glitchy version must take care to time writes so
00039 // that there's only one per PWM period.  Or, alternatively, they can just
00040 // be sure to repeat updates periodically to ensure that the last update is
00041 // eventually applied.
00042 //
00043 // 2. We optimize the TPM clock pre-scaler to maximize the precision of the
00044 // output period, to get as close as possible to the requested period.  The
00045 // base mbed code uses a fixed pre-scaler setting with a fixed 750kHz update
00046 // frequency, which means the period can be set in 1.333us increments.  The
00047 // hardware is capable of increments as small as .02us.  The tradeoff is that
00048 // the higher precision modes with smaller increments only allow for limited
00049 // total period lengths, since the cycle counter is 16 bits: the maximum
00050 // period at a given clock increment is 65535 times the increment.  So the
00051 // mbed default of 1.333us increments allows for periods of up to 87ms with
00052 // 1.333us precision, whereas the maximum precision of .02us increments only
00053 // allows for a maximum period of 1.36ms.
00054 //
00055 // To deal with this tradeoff, we choose the scaling factor each time the
00056 // period is changed, using the highest precision (smallest time increment,
00057 // or lowest pre-scaling clock divider) available for the requested period.
00058 // 
00059 // Similar variable pre-scaling functionality is available with the FastPWM
00060 // class.
00061 //
00062 // 3. We properly handle the shared clock in the TPM units.  The mbed library
00063 // doesn't, nor does FastPWM.
00064 //
00065 // The period/frequency of a PWM channel on the KL25Z is a function of the
00066 // TPM unit containing the channel, NOT of the channel itself.  A channel's
00067 // frequency CANNOT be set independently; it can only set for the entire 
00068 // group of channels controlled through the same TPM unit as the target
00069 // channel.
00070 //
00071 // The mbed library and FastPWM library pretend that the period can be set
00072 // per channel.  This is misleading and bug-prone, since an application that
00073 // takes the API at its word and sets a channel's frequency on the fly won't
00074 // necessarily realize that it just changed the frequency for all of the other
00075 // channels on the same TPM.  What's more, the change in TPM period will
00076 // effectively change the duty cycle for all channels attached to the PWM,
00077 // since it'll update the counter modulus, so all channels on the same TPM
00078 // have to have their duty cycles reset after any frequency change.
00079 //
00080 // This implementation changes the API design to better reflect reality.  We
00081 // expose a separate object representing the TPM unit for a channel, and we
00082 // put the period update function on the TPM unit object rather than on the
00083 // channel.  We also automatically update the duty cycle variable for all
00084 // channels on a TPM when updating the frequency, to maintain the original
00085 // duty cycle (or as close as possible, after rounding error).
00086 //
00087 // Applications that need to control the duty cycle on more than one channel
00088 // must take care to ensure that the separately controlled channels are on 
00089 // separate TPM units.  The KL25Z offers three physical TPM units, so there
00090 // can be up to three independently controlled periods.  The KL25Z has 10
00091 // channels in total (6 on unit 0, 2 on unit 1, 2 on unit 2), so the remaining
00092 // 7 channels have to share their periods with their TPM unit-mates.
00093 //
00094 
00095 
00096 #ifndef _NEWPWMOUT_H_
00097 #define _NEWPWMOUT_H_
00098 
00099 #include <mbed.h>
00100 #include <pinmap.h>
00101 #include <PeripheralPins.h>
00102 #include <clk_freqs.h>
00103 
00104 // TPM Unit.  This corresponds to one TPM unit in the hardware.  Each
00105 // unit controls 6 channels; a channel corresponds to one output pin.
00106 // A unit contains the clock input, pre-scaler, counter, and counter 
00107 // modulus; these are shared among all 6 channels in the unit, and
00108 // together determine the cycle time (period) of all channels in the
00109 // unit.  The period of a single channel can't be set independently;
00110 // a channel takes its period from its unit.
00111 //
00112 // Since the KL25Z hardware has a fixed set of 3 TPM units, we have
00113 // a fixed array of 3 of these objects.
00114 //
00115 class NewPwmUnit
00116 {
00117 public:
00118     NewPwmUnit()
00119     {
00120         // figure our unit number from the singleton array position
00121         int tpm_n = this - unit;
00122         
00123         // start with all channels disabled
00124         activeChannels = 0;
00125         
00126         // get our TPM unit hardware register base
00127         tpm = (TPM_Type *)(TPM0_BASE + 0x1000*tpm_n);
00128         
00129         // Determine which clock input we're using.  Save the clock
00130         // frequency for later use when setting the PWM period, and 
00131         // set up the SIM control register for the appropriate clock
00132         // input.  This setting is global, so we really only need to
00133         // do it once for all three units, but it'll be the same every
00134         // time so it won't hurt (except for a little redundancy) to
00135         // do it again on each unit constructor.
00136         if (mcgpllfll_frequency()) {
00137             SIM->SOPT2 |= SIM_SOPT2_TPMSRC(1); // Clock source: MCGFLLCLK or MCGPLLCLK
00138             sysClock = mcgpllfll_frequency();
00139         } else {
00140             SIM->SOPT2 |= SIM_SOPT2_TPMSRC(2); // Clock source: ExtOsc
00141             sysClock = extosc_frequency();
00142         }
00143     }
00144     
00145     // Default PWM period, in seconds
00146     static float defaultPeriod;
00147     
00148     // enable a channel
00149     void enableChannel(int ch)
00150     {
00151         // if this is the first channel we're enabling, enable the
00152         // unit clock gate
00153         if (activeChannels == 0)
00154         {
00155             // enable the clock gate on the TPM unit
00156             int tpm_n = this - unit;
00157             SIM->SCGC6 |= 1 << (SIM_SCGC6_TPM0_SHIFT + tpm_n);
00158             
00159             // set the default PWM frequency (period)
00160             period(defaultPeriod);
00161         }
00162         
00163         // add the channel bit to our collection
00164         activeChannels |= (1 << ch);
00165     }
00166     
00167     // Set the period for the unit.  This updates all channels associated
00168     // with the unit so that their duty cycle is scaled properly to the
00169     // period counter.
00170     void period(float seconds)
00171     {        
00172         // First check to see if we actually need to change anything.  If
00173         // the requested period already matches the current period, there's
00174         // nothing to do.  This will avoid unnecessarily resetting any
00175         // running cycles, which could cause visible flicker.
00176         uint32_t freq = sysClock >> (tpm->SC & TPM_SC_PS_MASK);
00177         uint32_t oldMod = tpm->MOD;
00178         uint32_t newMod = uint32_t(seconds*freq) - 1;
00179         if (newMod == oldMod && (tpm->SC & TPM_SC_CMOD_MASK) == TPM_SC_CMOD(1))
00180             return;
00181     
00182         // Figure the minimum pre-scaler needed to allow this period.  The
00183         // unit counter is 16 bits, so the maximum cycle length is 65535
00184         // ticks.  One tick is the system clock tick time multiplied by
00185         // the pre-scaler.  The scaler comes in powers of two from 1 to 128.
00186         
00187         // start at scaler=0 -> divide by 1
00188         int ps = 0;
00189         freq = sysClock;
00190         
00191         // at this rate, the maximum period is 65535 ticks of the system clock
00192         float pmax = 65535.0f/sysClock;
00193         
00194         // Now figure how much we have to divide the system clock: each
00195         // scaler step divides by another factor of 2, which doubles the
00196         // maximum period.  Keep going while the maximum period is below
00197         // the desired period, but stop if we reach the maximum per-scale
00198         // value of divide-by-128.
00199         while (ps < 7 && pmax < seconds)
00200         {
00201             ++ps;
00202             pmax *= 2.0f;
00203             freq /= 2;
00204         }
00205 
00206         // Before writing the prescaler bits, we have to disable the
00207         // clock (CMOD) bits in the status & control register.  These
00208         // bits might take a while to update, so spin until they clear.
00209         while ((tpm->SC & 0x1F) != 0)
00210             tpm->SC &= ~0x1F;
00211 
00212         // Reset the CnV (trigger value) for all active channels to
00213         // maintain each channel's current duty cycle.
00214         for (int i = 0 ; i < 6 ; ++i)
00215         {
00216             // if this channel is active, reset it
00217             if ((activeChannels & (1 << i)) != 0)
00218             {
00219                 // figure the old duty cycle, based on the current
00220                 // channel value and the old modulus
00221                 uint32_t oldCnV = tpm->CONTROLS[i].CnV;
00222                 float dc = float(oldCnV)/float(oldMod + 1);
00223                 if (dc > 1.0f) dc = 1.0f;
00224                 
00225                 // figure the new value that maintains the same duty
00226                 // cycle with the new modulus
00227                 uint32_t newCnV = uint32_t(dc*(newMod + 1));
00228                 
00229                 // if it changed, write the new value
00230                 if (newCnV != oldCnV)
00231                     tpm->CONTROLS[i].CnV = newCnV;
00232             }
00233         }
00234 
00235         // reset the unit counter register
00236         tpm->CNT = 0;
00237         
00238         // set the new clock period
00239         tpm->MOD = newMod = uint32_t(seconds*freq) - 1;
00240         
00241         // set the new pre-scaler bits and set clock mode 01 (enabled, 
00242         // increments on every LPTPM clock)
00243         tpm->SC = TPM_SC_CMOD(1) | TPM_SC_PS(ps);
00244     }
00245     
00246     // wait for the end of the current cycle
00247     inline void waitEndCycle()
00248     {
00249         // clear the overflow flag (note the usual KL25Z convention for 
00250         // hardware status registers like this: writing '1' clears the bit)
00251         tpm->SC |= TPM_SC_TOF_MASK;
00252         
00253         // The flag will be set at the next overflow
00254         while (!(tpm->SC & TPM_SC_TOF_MASK)) ;
00255     }
00256     
00257     // hardware register base
00258     TPM_Type *tpm;
00259     
00260     // Channels that are active in this unit, as a bit mask:
00261     // 1<<n is our channel n.
00262     uint8_t activeChannels;
00263     
00264     // fixed array of unit singletons
00265     static NewPwmUnit unit[3];
00266     
00267     // system clock frequency
00268     static uint32_t sysClock;
00269 };
00270 
00271 
00272 class NewPwmOut
00273 {
00274 public:
00275     // Set up the output pin.
00276     //
00277     // 'invertedCycle' means that the output is OFF during the first phase 
00278     // of each PWM period (the part between the start of the period and the
00279     // duty cycle percentage) and ON during the second phase.  This makes
00280     // the duty cycle setting in the write() calls the OFF duty cycle.  For
00281     // example, with an inverted cycle, write(.1) means that the output will
00282     // be OFF 10% of the time and ON 90% of the time.  This is primarily
00283     // for complex timing situations where the caller has to be able to
00284     // coordinate the alignment of up/down transitions on the output; in
00285     // particular, it allows the caller to use the waitEndCycle() to sync
00286     // with the falling edge on the output.
00287     NewPwmOut(PinName pin, bool invertedCycle = false)
00288     {
00289         // determine the TPM unit number and channel
00290         PWMName pwm = (PWMName)pinmap_peripheral(pin, PinMap_PWM);
00291         MBED_ASSERT(pwm != (PWMName)NC);
00292         unsigned int port = (unsigned int)pin >> PORT_SHIFT;
00293         
00294         // decode the port ID into the TPM unit and channel number
00295         tpm_n = (pwm >> TPM_SHIFT);
00296         ch_n  = (pwm & 0xFF);
00297         
00298         // enable the clock gate on the port (PTx)
00299         SIM->SCGC5 |= 1 << (SIM_SCGC5_PORTA_SHIFT + port);
00300         
00301         // enable the channel on the TPM unit
00302         NewPwmUnit::unit[tpm_n].enableChannel(ch_n);
00303         
00304         // Figure the ELSB:ELSA mode according to whether we want the normal
00305         // "high-true" cycle (high after reset, low after match) or the
00306         // inverted "low-true" cycle (low after reset, high after match)        
00307         uint32_t els_bits = invertedCycle ? TPM_CnSC_ELSA_MASK : TPM_CnSC_ELSB_MASK;
00308 
00309         // set the channel control register:
00310         //   CHIE                = 0    = interrupts disabled
00311         //   MSB:MBA:ELSB:ELSA   = 10cc = edge-aligned PWM (cc = 10 high-true, 01 low-true = inverted cycle)
00312         //   DMA                 = 0    = DMA off
00313         TPM_Type *tpm = getUnit()->tpm;
00314         tpm->CONTROLS[ch_n].CnSC = (TPM_CnSC_MSB_MASK | els_bits);
00315                 
00316         // wire the pinout
00317         pinmap_pinout(pin, PinMap_PWM);
00318     }
00319     
00320     float read()
00321     {
00322         TPM_Type *tpm = getUnit()->tpm;
00323         float v = float(tpm->CONTROLS[ch_n].CnV)/float(tpm->MOD + 1);
00324         return v > 1.0f ? 1.0f : v;
00325     }
00326     
00327     void write(float val)
00328     {
00329         // do the glitch-free write
00330         glitchFreeWrite(val);
00331         
00332         // Reset the counter.  This is a workaround for a hardware problem
00333         // on the KL25Z, namely that the CnV register can only be written
00334         // once per PWM cycle.  Any subsequent attempt to write it in the
00335         // same cycle will be lost.  Resetting the counter forces the end
00336         // of the cycle and makes the register writable again.  This isn't
00337         // an ideal workaround because it causes visible brightness glitching
00338         // if the caller writes new values repeatedly, such as when fading
00339         // lights in or out.
00340         TPM_Type *tpm = getUnit()->tpm;
00341         tpm->CNT = 0;    
00342     }
00343 
00344     // Write a new value without forcing the current PWM cycle to end.
00345     // This results in glitch-free writing during fades or other series
00346     // of rapid writes, BUT with the giant caveat that the caller MUST NOT
00347     // write another value before the current PWM cycle ends.  Doing so
00348     // will cause the later write to be lost.  Callers using this must 
00349     // take care, using mechanisms of their own, to limit writes to once
00350     // per PWM cycle.
00351     void glitchFreeWrite(float val)
00352     {
00353         // limit to 0..1 range
00354         val = (val < 0.0f ? 0.0f : val > 1.0f ? 1.0f : val);
00355         
00356         // Write the duty cycle register.  The argument value is a duty
00357         // cycle on a normalized 0..1 scale; for the hardware, we need to
00358         // renormalize to the 0..MOD scale, where MOD is the cycle length 
00359         // in clock counts.  
00360         TPM_Type *tpm = getUnit()->tpm;
00361         tpm->CONTROLS[ch_n].CnV = (uint32_t)((float)(tpm->MOD + 1) * val);
00362     }
00363     
00364     // Wait for the end of a cycle
00365     void waitEndCycle() { getUnit()->waitEndCycle(); }
00366     
00367     // Get my TPM unit object.  This can be used to change the period.
00368     // 
00369     // (Note that it's intentional that we make you ask for the unit to
00370     // modify the period.  It might seem attractive to provide a convenience
00371     // method here that sets the period in the unit on the caller's behalf,
00372     // but we omit that *on purpose*, to make it explicit to people calling
00373     // this code that the period is an attribute of the unit, not of the
00374     // channel, and to make it self-documenting in all calling code that 
00375     // this is the case.  The original mbed interface makes it look like the
00376     // period is abstractly an attribute of the channel, allowing a naive 
00377     // developer to believe that a channel's period can be changed in
00378     // isolation.  In fact, changing a channel's period in the mbed API
00379     // has global side effects, in that it also changes the period for
00380     // all other channels on the same unit.  Global side effects like that
00381     // violate the principle of encapsulation.  This reflects a defect in the
00382     // mbed API's design, not its implementation, in that the hardware forces
00383     // this implementation.  For this reason, we deliberately require callers 
00384     // to spell out in the code that they're operating on the unit when 
00385     // changing attributes that belong in fact to the unit.)
00386     inline NewPwmUnit *getUnit() { return &NewPwmUnit::unit[tpm_n]; }
00387     
00388     // Get my TPM unit number and channel number
00389     inline int getUnitNum() const { return tpm_n; }
00390     inline int getChannelNum() const { return ch_n; }
00391     
00392 protected:
00393     // TPM unit number and channel number
00394     uint8_t tpm_n;
00395     uint8_t ch_n;
00396 };
00397 
00398 #endif