/* ChibiOS - Copyright (C) 2006..2018 Giovanni Di Sirio Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ /* This driver is based on the work done by Matteo Serva available at http://github.com/matteoserva/ChibiOS-AVR */ /** * @file TIMv1/hal_pwm_lld.c * @brief AVR/MEGA PWM subsystem low level driver source. * * @addtogroup PWM * @{ */ #include "hal.h" #if HAL_USE_PWM || defined(__DOXYGEN__) /*==========================================================================*/ /* Driver local definitions. */ /*==========================================================================*/ #define TIM1_CS_OFFSET CS10 #define TIM1_WGM_OFFSET1 WGM10 #define TIM1_WGM_OFFSET2 WGM12 typedef struct { volatile uint8_t *tccra; volatile uint8_t *tccrb; volatile uint8_t *ocrah; volatile uint8_t *ocral; volatile uint8_t *ocrbh; volatile uint8_t *ocrbl; volatile uint8_t *ocrch; volatile uint8_t *ocrcl; volatile uint8_t *tifr; volatile uint8_t *timsk; volatile uint16_t *icr; } timer_registers_t; static timer_registers_t regs_table[]= { #if AVR_PWM_USE_TIM1 || defined(__DOXYGEN__) #if defined(OCR1C) {&TCCR1A, &TCCR1B, &OCR1AH, &OCR1AL, &OCR1BH, &OCR1BL, &OCR1CH, &OCR1CL, &TIFR1, &TIMSK1, &ICR1}, #else {&TCCR1A, &TCCR1B, &OCR1AH, &OCR1AL, &OCR1BH, &OCR1BL, NULL, NULL, &TIFR1, &TIMSK1, &ICR1}, #endif #endif #if AVR_PWM_USE_TIM2 || defined(__DOXYGEN__) {&TCCR2A, &TCCR2B, &OCR2A, &OCR2A, &OCR2B, &OCR2B, NULL, NULL, &TIFR2, &TIMSK2, NULL}, #endif #if AVR_PWM_USE_TIM3 || defined(__DOXYGEN__) {&TCCR3A, &TCCR3B, &OCR3AH, &OCR3AL, &OCR3BH, &OCR3BL, &OCR3CH, &OCR3CL, &TIFR3, &TIMSK3, &ICR3}, #endif #if AVR_PWM_USE_TIM4 || defined(__DOXYGEN__) {&TCCR4A, &TCCR4B, &OCR4AH, &OCR4AL, &OCR4CH, &OCR4CL, &OCR4CH, &OCR4CL, &TIFR4, &TIMSK4, &ICR4}, #endif #if AVR_PWM_USE_TIM5 || defined(__DOXYGEN__) {&TCCR5A, &TCCR5B, &OCR5AH, &OCR5AL, &OCR5BH, &OCR5BL, &OCR5CH, &OCR5CL, &TIFR5, &TIMSK5, &ICR5}, #endif }; /*==========================================================================*/ /* Driver exported variables. */ /*==========================================================================*/ /** @brief PWM driver identifiers.*/ #if AVR_PWM_USE_TIM1 || defined(__DOXYGEN__) PWMDriver PWMD1; #endif #if AVR_PWM_USE_TIM2 || defined(__DOXYGEN__) PWMDriver PWMD2; #endif #if AVR_PWM_USE_TIM3 || defined(__DOXYGEN__) PWMDriver PWMD3; #endif #if AVR_PWM_USE_TIM4 || defined(__DOXYGEN__) PWMDriver PWMD4; #endif #if AVR_PWM_USE_TIM5 || defined(__DOXYGEN__) PWMDriver PWMD5; #endif /*==========================================================================*/ /* Driver local variables. */ /*==========================================================================*/ /*==========================================================================*/ /* Driver local functions. */ /*==========================================================================*/ static void config_channel(volatile uint8_t *tccra, uint8_t com1, uint8_t com0, pwmmode_t mode) { *tccra &= ~((1 << com1) | (1 << com0)); if (mode == PWM_OUTPUT_ACTIVE_HIGH) *tccra |= ((1 << com1) | (0 << com0)); /* Non inverting mode. */ else if (mode == PWM_OUTPUT_ACTIVE_LOW) *tccra |= (1 << com1) | (1 << com0); /* Inverting mode. */ } static uint8_t timer_index(PWMDriver *pwmp) { uint8_t index = 0; #if AVR_PWM_USE_TIM1 || defined(__DOXYGEN__) if (pwmp == &PWMD1) return index; else index++; #endif #if AVR_PWM_USE_TIM2 || defined(__DOXYGEN__) if (pwmp == &PWMD2) return index; else index++; #endif #if AVR_PWM_USE_TIM3 || defined(__DOXYGEN__) if (pwmp == &PWMD3) return index; else index++; #endif #if AVR_PWM_USE_TIM4 || defined(__DOXYGEN__) if (pwmp == &PWMD4) return index; else index++; #endif #if AVR_PWM_USE_TIM5 || defined(__DOXYGEN__) if (pwmp == &PWMD5) return index; else index++; #endif /* This is an error! */ return index; } /*==========================================================================*/ /* Driver interrupt handlers. */ /*==========================================================================*/ /* * Interrupt for compare1&2 and clock overflow. pwmd1 & pwmd2. */ #if AVR_PWM_USE_TIM1 || defined(__DOXYGEN__) OSAL_IRQ_HANDLER(TIMER1_OVF_vect) { OSAL_IRQ_PROLOGUE(); PWMD1.config->callback(&PWMD1); OSAL_IRQ_EPILOGUE(); } OSAL_IRQ_HANDLER(TIMER1_COMPA_vect) { OSAL_IRQ_PROLOGUE(); PWMD1.config->channels[0].callback(&PWMD1); OSAL_IRQ_EPILOGUE(); } OSAL_IRQ_HANDLER(TIMER1_COMPB_vect) { OSAL_IRQ_PROLOGUE(); PWMD1.config->channels[1].callback(&PWMD1); OSAL_IRQ_EPILOGUE(); } #if PWM_CHANNELS > 2 OSAL_IRQ_HANDLER(TIMER1_COMPC_vect) { OSAL_IRQ_PROLOGUE(); PWMD1.config->channels[2].callback(&PWMD1); OSAL_IRQ_EPILOGUE(); } #endif #endif #if AVR_PWM_USE_TIM2 || defined(__DOXYGEN__) OSAL_IRQ_HANDLER(TIMER2_OVF_vect) { OSAL_IRQ_PROLOGUE(); PWMD2.config->callback(&PWMD2); OSAL_IRQ_EPILOGUE(); } OSAL_IRQ_HANDLER(TIMER2_COMPA_vect) { OSAL_IRQ_PROLOGUE(); PWMD2.config->channels[0].callback(&PWMD2); OSAL_IRQ_EPILOGUE(); } OSAL_IRQ_HANDLER(TIMER2_COMPB_vect) { OSAL_IRQ_PROLOGUE(); PWMD2.config->channels[1].callback(&PWMD2); OSAL_IRQ_EPILOGUE(); } #endif #if AVR_PWM_USE_TIM3 || defined(__DOXYGEN__) OSAL_IRQ_HANDLER(TIMER3_OVF_vect) { OSAL_IRQ_PROLOGUE(); PWMD3.config->callback(&PWMD3); OSAL_IRQ_EPILOGUE(); } OSAL_IRQ_HANDLER(TIMER3_COMPA_vect) { OSAL_IRQ_PROLOGUE(); PWMD3.config->channels[0].callback(&PWMD3); OSAL_IRQ_EPILOGUE(); } OSAL_IRQ_HANDLER(TIMER3_COMPB_vect) { OSAL_IRQ_PROLOGUE(); PWMD3.config->channels[1].callback(&PWMD3); OSAL_IRQ_EPILOGUE(); } OSAL_IRQ_HANDLER(TIMER3_COMPC_vect) { OSAL_IRQ_PROLOGUE(); PWMD3.config->channels[2].callback(&PWMD3); OSAL_IRQ_EPILOGUE(); } #endif #if AVR_PWM_USE_TIM4 || defined(__DOXYGEN__) OSAL_IRQ_HANDLER(TIMER4_OVF_vect) { OSAL_IRQ_PROLOGUE(); PWMD4.config->callback(&PWMD4); OSAL_IRQ_EPILOGUE(); } OSAL_IRQ_HANDLER(TIMER4_COMPA_vect) { OSAL_IRQ_PROLOGUE(); PWMD4.config->channels[0].callback(&PWMD4); OSAL_IRQ_EPILOGUE(); } OSAL_IRQ_HANDLER(TIMER4_COMPB_vect) { OSAL_IRQ_PROLOGUE(); PWMD4.config->channels[1].callback(&PWMD4); OSAL_IRQ_EPILOGUE(); } OSAL_IRQ_HANDLER(TIMER4_COMPC_vect) { OSAL_IRQ_PROLOGUE(); PWMD4.config->channels[2].callback(&PWMD4); OSAL_IRQ_EPILOGUE(); } #endif #if AVR_PWM_USE_TIM5 || defined(__DOXYGEN__) OSAL_IRQ_HANDLER(TIMER5_OVF_vect) { OSAL_IRQ_PROLOGUE(); PWMD5.config->callback(&PWMD5); OSAL_IRQ_EPILOGUE(); } OSAL_IRQ_HANDLER(TIMER5_COMPA_vect) { OSAL_IRQ_PROLOGUE(); PWMD5.config->channels[0].callback(&PWMD5); OSAL_IRQ_EPILOGUE(); } OSAL_IRQ_HANDLER(TIMER5_COMPB_vect) { OSAL_IRQ_PROLOGUE(); PWMD5.config->channels[1].callback(&PWMD5); OSAL_IRQ_EPILOGUE(); } OSAL_IRQ_HANDLER(TIMER5_COMPC_vect) { OSAL_IRQ_PROLOGUE(); PWMD5.config->channels[2].callback(&PWMD5); OSAL_IRQ_EPILOGUE(); } #endif /*==========================================================================*/ /* Driver exported functions. */ /*==========================================================================*/ /** * @brief Low level PWM driver initialization. * * @notapi */ void pwm_lld_init(void) { #if AVR_PWM_USE_TIM1 || defined(__DOXYGEN__) pwmObjectInit(&PWMD1); PWMD1.channels = PWM_CHANNELS; #endif #if AVR_PWM_USE_TIM2 || defined(__DOXYGEN__) pwmObjectInit(&PWMD2); PWMD2.channels = PWM_CHANNELS; #endif #if AVR_PWM_USE_TIM3 || defined(__DOXYGEN__) pwmObjectInit(&PWMD3); PWMD3.channels = PWM_CHANNELS; #endif #if AVR_PWM_USE_TIM4 || defined(__DOXYGEN__) pwmObjectInit(&PWMD4); PWMD4.channels = PWM_CHANNELS; #endif #if AVR_PWM_USE_TIM5 || defined(__DOXYGEN__) pwmObjectInit(&PWMD5); PWMD5.channels = PWM_CHANNELS; #endif } /** * @brief Configures and activates the PWM peripheral. * @note We do not use the period value in Timer2 in order to * be able to use both PWM channels. * * @param[in] pwmp pointer to the @p PWMDriver object * * @notapi */ void pwm_lld_start(PWMDriver *pwmp) { if (pwmp->state == PWM_STOP) { uint8_t cs_value, wgm_value; #if AVR_PWM_USE_TIM2 || defined(__DOXYGEN__) if (pwmp == &PWMD2) { /* For now only fast pwm is supported. */ wgm_value = 0x3; cs_value = 1; /* Period is fixed for timer2. */ PWMD2.period = 0xFF; /* A prescaler value can only be a suitable power of 2 (1, 8, 32, 64, 128 256 or 1024), so we choose the one that makes F_CPU divided by it equal to the given frequency (fallback value is 1, to keep compatibility with old code. */ const uint8_t log_ratio_timer2[] = {0, 3, 5, 6, 7, 8, 10}; uint8_t n; for (n=0; nconfig->frequency == (F_CPU >> log_ratio_timer2[n])) { cs_value = n + 1; break; } } TCCR2A = (wgm_value & 0x3) << TIM1_WGM_OFFSET1; TCCR2B = ((cs_value << TIM1_CS_OFFSET) | ((wgm_value >> 2) << TIM1_WGM_OFFSET2)); if (pwmp->config->callback != NULL) TIMSK2 |= (1 << TOIE2); return; } #endif /* For now only fast pwm is supported. */ wgm_value = 0xE; cs_value = 0x5; /* A prescaler value can only be a suitable power of 2 (1, 8, 64, 256 or 1024), so we choose the one that makes F_CPU divided by it equal to the given frequency (fallback value is 1024, to keep compatibility with old code. */ const uint8_t log_ratio_timer1[] = {0, 3, 6, 8, 10}; uint8_t n; for (n=0; nconfig->frequency == (F_CPU >> log_ratio_timer1[n])) { cs_value = n + 1; break; } } uint8_t i = timer_index(pwmp); *regs_table[i].icr = pwmp->period; *regs_table[i].tccra = (wgm_value & 0x3) << TIM1_WGM_OFFSET1; *regs_table[i].tccrb = ((cs_value << TIM1_CS_OFFSET) | ((wgm_value >> 2) << TIM1_WGM_OFFSET2)); if (pwmp->config->callback != NULL) *regs_table[i].timsk = (1 << TOIE1); } } /** * @brief Deactivates the PWM peripheral. * * @param[in] pwmp pointer to the @p PWMDriver object * * @notapi */ void pwm_lld_stop(PWMDriver *pwmp) { #if AVR_PWM_USE_TIM2 || defined(__DOXYGEN__) if (pwmp == &PWMD2) { TCCR2A = 0; TCCR2B = 0; TIMSK2 = 0; return; } #endif uint8_t i = timer_index(pwmp); *regs_table[i].tccra = 0; *regs_table[i].tccrb = 0; *regs_table[i].timsk = 0; } /** * @brief Changes the period the PWM peripheral. * @details This function changes the period of a PWM unit that has already * been activated using @p pwmStart(). * @pre The PWM unit must have been activated using @p pwmStart(). * @post The PWM unit period is changed to the new value. * @note The function has effect at the next cycle start. * @note If a period is specified that is shorter than the pulse width * programmed in one of the channels then the behavior is not * guaranteed. * * @param[in] pwmp pointer to a @p PWMDriver object * @param[in] period new cycle time in ticks * * @notapi */ void pwm_lld_change_period(PWMDriver *pwmp, pwmcnt_t period) { #if AVR_PWM_USE_TIM2 || defined(__DOXYGEN__) /* Can't change period in timer2. */ if (pwmp == &PWMD2) { PWMD2.period = 0xFF; return; } #endif uint8_t i = timer_index(pwmp); *regs_table[i].icr = period; } /** * @brief Enables a PWM channel. * @pre The PWM unit must have been activated using @p pwmStart(). * @post The channel is active using the specified configuration. * @note Depending on the hardware implementation this function has * effect starting on the next cycle (recommended implementation) * or immediately (fallback implementation). * * @param[in] pwmp pointer to a @p PWMDriver object * @param[in] channel PWM channel identifier (0...PWM_CHANNELS-1) * @param[in] width PWM pulse width as clock pulses number * * @notapi */ void pwm_lld_enable_channel(PWMDriver *pwmp, pwmchannel_t channel, pwmcnt_t width) { uint16_t val = width; #if AVR_PWM_USE_TIM2 || defined(__DOXYGEN__) if (pwmp == &PWMD2) { config_channel(&TCCR2A, 7 - 2*channel, 6 - 2*channel, pwmp->config->channels[channel].mode); /* Timer 2 is 8 bit. */ if (val > 0xFF) val = 0xFF; switch (channel) { case 0: OCR2A = val; break; case 1: OCR2B = val; break; } TIFR2 = 1 << (OCF2A + channel); if (pwmp->config->channels[channel].callback) TIMSK2 |= (1 << (OCIE2A + channel)); return; } #endif uint8_t i = timer_index(pwmp); config_channel(regs_table[i].tccra, 7 - 2*channel, 6 - 2*channel, pwmp->config->channels[channel].mode); volatile uint8_t *ocrh, *ocrl; switch (channel) { case 1: ocrh = regs_table[i].ocrbh; ocrl = regs_table[i].ocrbl; break; case 2: ocrh = regs_table[i].ocrch; ocrl = regs_table[i].ocrcl; break; default: ocrh = regs_table[i].ocrah; ocrl = regs_table[i].ocral; } *ocrh = val >> 8; *ocrl = val & 0xFF; *regs_table[i].tifr = (1 << (channel + 1)); if (pwmp->config->channels[channel].callback != NULL) *regs_table[i].timsk |= (1 << (channel + 1)); } /** * @brief Disables a PWM channel. * @pre The PWM unit must have been activated using @p pwmStart(). * @post The channel is disabled and its output line returned to the * idle state. * @note Depending on the hardware implementation this function has * effect starting on the next cycle (recommended implementation) * or immediately (fallback implementation). * * @param[in] pwmp pointer to a @p PWMDriver object * @param[in] channel PWM channel identifier (0...PWM_CHANNELS-1) * * @notapi */ void pwm_lld_disable_channel(PWMDriver *pwmp, pwmchannel_t channel) { uint8_t i = timer_index(pwmp); config_channel(regs_table[i].tccra, 7 - 2*channel, 6 - 2*channel, PWM_OUTPUT_DISABLED); *regs_table[i].timsk &= ~(1 << (channel + 1)); } /** * @brief Enables the periodic activation edge notification. * @pre The PWM unit must have been activated using @p pwmStart(). * @note If the notification is already enabled then the call has no effect. * * @param[in] pwmp pointer to a @p PWMDriver object * * @notapi */ void pwm_lld_enable_periodic_notification(PWMDriver *pwmp) { uint8_t i = timer_index(pwmp); *regs_table[i].timsk |= (1 << TOIE1); } /** * @brief Disables the periodic activation edge notification. * @pre The PWM unit must have been activated using @p pwmStart(). * @note If the notification is already disabled then the call has no effect. * * @param[in] pwmp pointer to a @p PWMDriver object * * @notapi */ void pwm_lld_disable_periodic_notification(PWMDriver *pwmp) { uint8_t i = timer_index(pwmp); *regs_table[i].timsk &= ~(1 << TOIE1); } /** * @brief Enables a channel de-activation edge notification. * @pre The PWM unit must have been activated using @p pwmStart(). * @pre The channel must have been activated using @p pwmEnableChannel(). * @note If the notification is already enabled then the call has no effect. * * @param[in] pwmp pointer to a @p PWMDriver object * @param[in] channel PWM channel identifier (0...channels-1) * * @notapi */ void pwm_lld_enable_channel_notification(PWMDriver *pwmp, pwmchannel_t channel) { uint8_t i = timer_index(pwmp); *regs_table[i].timsk |= (1 << (channel + 1)); } /** * @brief Disables a channel de-activation edge notification. * @pre The PWM unit must have been activated using @p pwmStart(). * @pre The channel must have been activated using @p pwmEnableChannel(). * @note If the notification is already disabled then the call has no effect. * * @param[in] pwmp pointer to a @p PWMDriver object * @param[in] channel PWM channel identifier (0...channels-1) * * @notapi */ void pwm_lld_disable_channel_notification(PWMDriver *pwmp, pwmchannel_t channel) { uint8_t i = timer_index(pwmp); *regs_table[i].timsk &= ~(1 << (channel + 1)); } #endif /* HAL_USE_PWM */ /** @} */