Mirror with some correction
Dependencies: mbed FastIO FastPWM USBDevice
NewPwm/NewPwm.h
- Committer:
- arnoz
- Date:
- 2021-10-01
- Revision:
- 116:7a67265d7c19
- Parent:
- 109:310ac82cbbee
File content as of revision 116:7a67265d7c19:
// New PWM // // This is a replacement for the mbed PwmOut class. It's both stripped // down and beefed up. It's stripped down to just the functionality we // need in the Pinscape code, and to a purely KL25Z implementation, which // allows for a smaller memory footprint per instance. It's beefed up to // correct a number of problems in the mbed implementation. // // Note that this class isn't quite API-compatible with the mbed version. // We make the channel/TPM unit structure explicit, and we put the period() // method (to change the PWM cycle time) on the unit object rather than the // channel. We do this to emphasize in the API that the period is a property // of the unit (which contains multiple channels) rather than the channel. // The mbed library is misleading when it pretends that the period is a // property of the channel, since this confusingly suggests that a channel's // period can be set independently. It can't; the period can only be set for // the whole group of channels controlled by a unit. // // Improvements over the mbed version: // // 1. We provide an alternative, non-glitching version of write(). The mbed // version of write(), and our default version with the same name, causes a // glitch on every write by resetting the TPM counter, which cuts the cycle // short and causes a momentary drop in brightness (from the short cycle) // that's visible if an LED is connected. This is particularly noticeable // when doing a series of rapid writes, such as when fading a light on or off. // // We offer a version of write() that doesn't reset the counter, avoiding the // glitch. This version skips the counter reset that the default version does. // // But this must be used with caution, because there's a whole separate // problem if you don't reset the counter, which is why the mbed library // does this by default. The KL25Z hardware only allows the value register // to be written once per PWM cycle; if it's written more than once, the // second and subsequent writes are simply ignored, so those updates will // be forever lost. The counter reset, in addition to casuing the glitch, // resets the cycle and thus avoids the one-write-per-cycle limitation. // Callers using the non-glitchy version must take care to time writes so // that there's only one per PWM period. Or, alternatively, they can just // be sure to repeat updates periodically to ensure that the last update is // eventually applied. // // 2. We optimize the TPM clock pre-scaler to maximize the precision of the // output period, to get as close as possible to the requested period. The // base mbed code uses a fixed pre-scaler setting with a fixed 750kHz update // frequency, which means the period can be set in 1.333us increments. The // hardware is capable of increments as small as .02us. The tradeoff is that // the higher precision modes with smaller increments only allow for limited // total period lengths, since the cycle counter is 16 bits: the maximum // period at a given clock increment is 65535 times the increment. So the // mbed default of 1.333us increments allows for periods of up to 87ms with // 1.333us precision, whereas the maximum precision of .02us increments only // allows for a maximum period of 1.36ms. // // To deal with this tradeoff, we choose the scaling factor each time the // period is changed, using the highest precision (smallest time increment, // or lowest pre-scaling clock divider) available for the requested period. // // Similar variable pre-scaling functionality is available with the FastPWM // class. // // 3. We properly handle the shared clock in the TPM units. The mbed library // doesn't, nor does FastPWM. // // The period/frequency of a PWM channel on the KL25Z is a function of the // TPM unit containing the channel, NOT of the channel itself. A channel's // frequency CANNOT be set independently; it can only set for the entire // group of channels controlled through the same TPM unit as the target // channel. // // The mbed library and FastPWM library pretend that the period can be set // per channel. This is misleading and bug-prone, since an application that // takes the API at its word and sets a channel's frequency on the fly won't // necessarily realize that it just changed the frequency for all of the other // channels on the same TPM. What's more, the change in TPM period will // effectively change the duty cycle for all channels attached to the PWM, // since it'll update the counter modulus, so all channels on the same TPM // have to have their duty cycles reset after any frequency change. // // This implementation changes the API design to better reflect reality. We // expose a separate object representing the TPM unit for a channel, and we // put the period update function on the TPM unit object rather than on the // channel. We also automatically update the duty cycle variable for all // channels on a TPM when updating the frequency, to maintain the original // duty cycle (or as close as possible, after rounding error). // // Applications that need to control the duty cycle on more than one channel // must take care to ensure that the separately controlled channels are on // separate TPM units. The KL25Z offers three physical TPM units, so there // can be up to three independently controlled periods. The KL25Z has 10 // channels in total (6 on unit 0, 2 on unit 1, 2 on unit 2), so the remaining // 7 channels have to share their periods with their TPM unit-mates. // #ifndef _NEWPWMOUT_H_ #define _NEWPWMOUT_H_ #include <mbed.h> #include <pinmap.h> #include <PeripheralPins.h> #include <clk_freqs.h> // TPM Unit. This corresponds to one TPM unit in the hardware. Each // unit controls 6 channels; a channel corresponds to one output pin. // A unit contains the clock input, pre-scaler, counter, and counter // modulus; these are shared among all 6 channels in the unit, and // together determine the cycle time (period) of all channels in the // unit. The period of a single channel can't be set independently; // a channel takes its period from its unit. // // Since the KL25Z hardware has a fixed set of 3 TPM units, we have // a fixed array of 3 of these objects. // class NewPwmUnit { public: NewPwmUnit() { // figure our unit number from the singleton array position int tpm_n = this - unit; // start with all channels disabled activeChannels = 0; // get our TPM unit hardware register base tpm = (TPM_Type *)(TPM0_BASE + 0x1000*tpm_n); // Determine which clock input we're using. Save the clock // frequency for later use when setting the PWM period, and // set up the SIM control register for the appropriate clock // input. This setting is global, so we really only need to // do it once for all three units, but it'll be the same every // time so it won't hurt (except for a little redundancy) to // do it again on each unit constructor. if (mcgpllfll_frequency()) { SIM->SOPT2 |= SIM_SOPT2_TPMSRC(1); // Clock source: MCGFLLCLK or MCGPLLCLK sysClock = mcgpllfll_frequency(); } else { SIM->SOPT2 |= SIM_SOPT2_TPMSRC(2); // Clock source: ExtOsc sysClock = extosc_frequency(); } } // Default PWM period, in seconds static float defaultPeriod; // enable a channel void enableChannel(int ch) { // if this is the first channel we're enabling, enable the // unit clock gate if (activeChannels == 0) { // enable the clock gate on the TPM unit int tpm_n = this - unit; SIM->SCGC6 |= 1 << (SIM_SCGC6_TPM0_SHIFT + tpm_n); // set the default PWM frequency (period) period(defaultPeriod); } // add the channel bit to our collection activeChannels |= (1 << ch); } // Set the period for the unit. This updates all channels associated // with the unit so that their duty cycle is scaled properly to the // period counter. void period(float seconds) { // First check to see if we actually need to change anything. If // the requested period already matches the current period, there's // nothing to do. This will avoid unnecessarily resetting any // running cycles, which could cause visible flicker. uint32_t freq = sysClock >> (tpm->SC & TPM_SC_PS_MASK); uint32_t oldMod = tpm->MOD; uint32_t newMod = uint32_t(seconds*freq) - 1; if (newMod == oldMod && (tpm->SC & TPM_SC_CMOD_MASK) == TPM_SC_CMOD(1)) return; // Figure the minimum pre-scaler needed to allow this period. The // unit counter is 16 bits, so the maximum cycle length is 65535 // ticks. One tick is the system clock tick time multiplied by // the pre-scaler. The scaler comes in powers of two from 1 to 128. // start at scaler=0 -> divide by 1 int ps = 0; freq = sysClock; // at this rate, the maximum period is 65535 ticks of the system clock float pmax = 65535.0f/sysClock; // Now figure how much we have to divide the system clock: each // scaler step divides by another factor of 2, which doubles the // maximum period. Keep going while the maximum period is below // the desired period, but stop if we reach the maximum per-scale // value of divide-by-128. while (ps < 7 && pmax < seconds) { ++ps; pmax *= 2.0f; freq /= 2; } // Before writing the prescaler bits, we have to disable the // clock (CMOD) bits in the status & control register. These // bits might take a while to update, so spin until they clear. while ((tpm->SC & 0x1F) != 0) tpm->SC &= ~0x1F; // Reset the CnV (trigger value) for all active channels to // maintain each channel's current duty cycle. for (int i = 0 ; i < 6 ; ++i) { // if this channel is active, reset it if ((activeChannels & (1 << i)) != 0) { // figure the old duty cycle, based on the current // channel value and the old modulus uint32_t oldCnV = tpm->CONTROLS[i].CnV; float dc = float(oldCnV)/float(oldMod + 1); if (dc > 1.0f) dc = 1.0f; // figure the new value that maintains the same duty // cycle with the new modulus uint32_t newCnV = uint32_t(dc*(newMod + 1)); // if it changed, write the new value if (newCnV != oldCnV) tpm->CONTROLS[i].CnV = newCnV; } } // reset the unit counter register tpm->CNT = 0; // set the new clock period tpm->MOD = newMod = uint32_t(seconds*freq) - 1; // set the new pre-scaler bits and set clock mode 01 (enabled, // increments on every LPTPM clock) tpm->SC = TPM_SC_CMOD(1) | TPM_SC_PS(ps); } // wait for the end of the current cycle inline void waitEndCycle() { // clear the overflow flag (note the usual KL25Z convention for // hardware status registers like this: writing '1' clears the bit) tpm->SC |= TPM_SC_TOF_MASK; // The flag will be set at the next overflow while (!(tpm->SC & TPM_SC_TOF_MASK)) ; } // hardware register base TPM_Type *tpm; // Channels that are active in this unit, as a bit mask: // 1<<n is our channel n. uint8_t activeChannels; // fixed array of unit singletons static NewPwmUnit unit[3]; // system clock frequency static uint32_t sysClock; }; class NewPwmOut { public: // Set up the output pin. // // 'invertedCycle' means that the output is OFF during the first phase // of each PWM period (the part between the start of the period and the // duty cycle percentage) and ON during the second phase. This makes // the duty cycle setting in the write() calls the OFF duty cycle. For // example, with an inverted cycle, write(.1) means that the output will // be OFF 10% of the time and ON 90% of the time. This is primarily // for complex timing situations where the caller has to be able to // coordinate the alignment of up/down transitions on the output; in // particular, it allows the caller to use the waitEndCycle() to sync // with the falling edge on the output. NewPwmOut(PinName pin, bool invertedCycle = false) { // determine the TPM unit number and channel PWMName pwm = (PWMName)pinmap_peripheral(pin, PinMap_PWM); MBED_ASSERT(pwm != (PWMName)NC); unsigned int port = (unsigned int)pin >> PORT_SHIFT; // decode the port ID into the TPM unit and channel number tpm_n = (pwm >> TPM_SHIFT); ch_n = (pwm & 0xFF); // enable the clock gate on the port (PTx) SIM->SCGC5 |= 1 << (SIM_SCGC5_PORTA_SHIFT + port); // enable the channel on the TPM unit NewPwmUnit::unit[tpm_n].enableChannel(ch_n); // Figure the ELSB:ELSA mode according to whether we want the normal // "high-true" cycle (high after reset, low after match) or the // inverted "low-true" cycle (low after reset, high after match) uint32_t els_bits = invertedCycle ? TPM_CnSC_ELSA_MASK : TPM_CnSC_ELSB_MASK; // set the channel control register: // CHIE = 0 = interrupts disabled // MSB:MBA:ELSB:ELSA = 10cc = edge-aligned PWM (cc = 10 high-true, 01 low-true = inverted cycle) // DMA = 0 = DMA off TPM_Type *tpm = getUnit()->tpm; tpm->CONTROLS[ch_n].CnSC = (TPM_CnSC_MSB_MASK | els_bits); // wire the pinout pinmap_pinout(pin, PinMap_PWM); } float read() { TPM_Type *tpm = getUnit()->tpm; float v = float(tpm->CONTROLS[ch_n].CnV)/float(tpm->MOD + 1); return v > 1.0f ? 1.0f : v; } void write(float val) { // do the glitch-free write glitchFreeWrite(val); // Reset the counter. This is a workaround for a hardware problem // on the KL25Z, namely that the CnV register can only be written // once per PWM cycle. Any subsequent attempt to write it in the // same cycle will be lost. Resetting the counter forces the end // of the cycle and makes the register writable again. This isn't // an ideal workaround because it causes visible brightness glitching // if the caller writes new values repeatedly, such as when fading // lights in or out. TPM_Type *tpm = getUnit()->tpm; tpm->CNT = 0; } // Write a new value without forcing the current PWM cycle to end. // This results in glitch-free writing during fades or other series // of rapid writes, BUT with the giant caveat that the caller MUST NOT // write another value before the current PWM cycle ends. Doing so // will cause the later write to be lost. Callers using this must // take care, using mechanisms of their own, to limit writes to once // per PWM cycle. void glitchFreeWrite(float val) { // limit to 0..1 range val = (val < 0.0f ? 0.0f : val > 1.0f ? 1.0f : val); // Write the duty cycle register. The argument value is a duty // cycle on a normalized 0..1 scale; for the hardware, we need to // renormalize to the 0..MOD scale, where MOD is the cycle length // in clock counts. TPM_Type *tpm = getUnit()->tpm; tpm->CONTROLS[ch_n].CnV = (uint32_t)((float)(tpm->MOD + 1) * val); } // Wait for the end of a cycle void waitEndCycle() { getUnit()->waitEndCycle(); } // Get my TPM unit object. This can be used to change the period. // // (Note that it's intentional that we make you ask for the unit to // modify the period. It might seem attractive to provide a convenience // method here that sets the period in the unit on the caller's behalf, // but we omit that *on purpose*, to make it explicit to people calling // this code that the period is an attribute of the unit, not of the // channel, and to make it self-documenting in all calling code that // this is the case. The original mbed interface makes it look like the // period is abstractly an attribute of the channel, allowing a naive // developer to believe that a channel's period can be changed in // isolation. In fact, changing a channel's period in the mbed API // has global side effects, in that it also changes the period for // all other channels on the same unit. Global side effects like that // violate the principle of encapsulation. This reflects a defect in the // mbed API's design, not its implementation, in that the hardware forces // this implementation. For this reason, we deliberately require callers // to spell out in the code that they're operating on the unit when // changing attributes that belong in fact to the unit.) inline NewPwmUnit *getUnit() { return &NewPwmUnit::unit[tpm_n]; } // Get my TPM unit number and channel number inline int getUnitNum() const { return tpm_n; } inline int getChannelNum() const { return ch_n; } protected: // TPM unit number and channel number uint8_t tpm_n; uint8_t ch_n; }; #endif