Mirror with some correction
Dependencies: mbed FastIO FastPWM USBDevice
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
Generated on Thu Jul 14 2022 12:20:36 by 1.7.2