diff --git a/Makefile b/Makefile index 664a0811f..bdfc4f11b 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,7 @@ per/spi \ per/spiMultislave \ per/tim \ per/uart \ +per/pwm \ ui/UI \ ui/AbstractMenu \ ui/FullScreenItemMenu \ diff --git a/examples/PWM_Output/CMakeLists.txt b/examples/PWM_Output/CMakeLists.txt new file mode 100644 index 000000000..cb1de4f7c --- /dev/null +++ b/examples/PWM_Output/CMakeLists.txt @@ -0,0 +1,3 @@ +set(FIRMWARE_NAME PWM_Output) +set(FIRMWARE_SOURCES PWM_Output.cpp) +include(DaisyProject) diff --git a/examples/PWM_Output/Makefile b/examples/PWM_Output/Makefile new file mode 100644 index 000000000..33100f1c7 --- /dev/null +++ b/examples/PWM_Output/Makefile @@ -0,0 +1,12 @@ +# Project Name +TARGET = PWM_Output + +# Sources +CPP_SOURCES = PWM_Output.cpp + +# Library Locations +LIBDAISY_DIR = ../.. + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/examples/PWM_Output/PWM_Output.cpp b/examples/PWM_Output/PWM_Output.cpp new file mode 100644 index 000000000..9e152e2db --- /dev/null +++ b/examples/PWM_Output/PWM_Output.cpp @@ -0,0 +1,86 @@ +// Hardware PWM demo +// +// Demonstrates how to use a PWMHandle to output hardware-generated PWM. +// No special setup is required, as the built-in LED on the Seed is used for display. +// +#include "daisy_seed.h" +#include + +using namespace daisy; +using namespace daisy::seed; + +DaisySeed hw; + +const float TWO_PI = 6.2831853072f; + +int main(void) +{ + // Initialize the Daisy Seed hardware + hw.Init(); + hw.StartLog(false); + + // First, we'll create a PWMHandle that enables PWM mode on a given timer. Each handle provides up to 4 output channels. + PWMHandle pwm_tim3; + + { + // Configure the PWM peripheral + PWMHandle::Config pwm_config; + // We'll use TIM3 for PWM output. + pwm_config.periph = PWMHandle::Config::Peripheral::TIM_3; + // TIM3 is a 16-bit timer, so the max period is 0xffff. We'll use 0xff to give a higher-frequency PWM signal at the expense of lower precision. + pwm_config.period = 0xff; + + // Initialize + if(pwm_tim3.Init(pwm_config) != PWMHandle::Result::OK) + { + hw.PrintLine("Could not initialize PWM handle"); + } + + // You can also create the config inline when initializing: + // pwm_tim3.Init({PWMHandle::Config::Peripheral::TIM_3}); + } + + { + // Next, configure an individual channel. We'll use Channel 2, which can connect to the Seed's internal LED + PWMHandle::Channel::Config channel_config; + // Each timer and channel supports a different set of pins. If no pin is selected, the default for that channel will be selected. + channel_config.pin = {PORTC, 7}; + // Polarity can be changed for individual channels + channel_config.polarity = PWMHandle::Channel::Config::Polarity::HIGH; + + // Initialize + if(pwm_tim3.Channel2().Init(channel_config) != PWMHandle::Result::OK) + { + hw.PrintLine("Could not initialize PWM channel"); + } + + // Like before, you can also create the config inline, or leave it blank to use defaults. + // pwm_tim3.Channel2().Init(); + + // Each PWMHandle supports up to 4 channels at once. + } + + // Instead of calling pwm_tim3.Channel2() every time, you can also store a reference. Note that this reference is valid even if taken before initialization, + // but it must be initialized after the PWMHandle and before calling Set(). + auto& led_pwm = pwm_tim3.Channel2(); + + float phase = 0.0f; + while(1) + { + // Generate a 1 Hz pulse + float brightness = std::cos(TWO_PI * phase) * 0.5f + 0.5f; + + // Set the PWM channel duty cycle. We also apply a cubic gamma correction factor here to linearize the LED's brightness + // When calling this method with a float, the value is normalized to [0, period] + led_pwm.Set(brightness * brightness * brightness); + + // You can also call SetRaw to directly set the duty cycle. + // led_pwm.SetRaw(0x7f); + + phase += 0.01f; + if(phase > 1.0f) + phase -= 1.0f; + + hw.DelayMs(10); + } +} diff --git a/src/daisy.h b/src/daisy.h index 98af3c581..0d07e8163 100644 --- a/src/daisy.h +++ b/src/daisy.h @@ -10,6 +10,7 @@ #include "per/dac.h" #include "per/gpio.h" #include "per/tim.h" +#include "per/pwm.h" #include "dev/leddriver.h" #include "dev/mpr121.h" #include "dev/sdram.h" diff --git a/src/per/pwm.cpp b/src/per/pwm.cpp new file mode 100644 index 000000000..a0a87df30 --- /dev/null +++ b/src/per/pwm.cpp @@ -0,0 +1,324 @@ +#include "pwm.h" +#include "stm32h7xx_hal.h" +#include "util/hal_map.h" + +namespace daisy +{ +class PWMHandle::Impl +{ + public: + PWMHandle::Result Init(const Config &config); + PWMHandle::Result DeInit(); + void SetPrescaler(uint32_t prescaler); + void SetPeriod(uint32_t period); + + Config config_; + TIM_HandleTypeDef tim_hal_handle_{0}; +}; + +#define NUM_TIMERS 3 + +// Static storage for implementations +static PWMHandle::Impl pwm_handles[NUM_TIMERS]; + +// --------------------------------------------- + +Pin GetDefaultPin(PWMHandle::Config::Peripheral timer, uint32_t channel) +{ + if(timer == PWMHandle::Config::Peripheral::TIM_3) + { + switch(channel) + { + case TIM_CHANNEL_1: return {PORTA, 6}; + case TIM_CHANNEL_2: return {PORTC, 7}; + case TIM_CHANNEL_3: return {PORTC, 8}; + case TIM_CHANNEL_4: return {PORTB, 1}; + default: break; + } + } + + if(timer == PWMHandle::Config::Peripheral::TIM_4) + { + switch(channel) + { + case TIM_CHANNEL_1: return {PORTB, 6}; + case TIM_CHANNEL_2: return {PORTB, 7}; + case TIM_CHANNEL_3: return {PORTB, 8}; + case TIM_CHANNEL_4: return {PORTB, 9}; + default: break; + } + } + + if(timer == PWMHandle::Config::Peripheral::TIM_5) + { + switch(channel) + { + case TIM_CHANNEL_1: return {PORTA, 0}; + case TIM_CHANNEL_2: return {PORTA, 1}; + case TIM_CHANNEL_3: return {PORTA, 2}; + case TIM_CHANNEL_4: return {PORTA, 3}; + default: break; + } + } + + return Pin(); +} + +PWMHandle::Result +PWMHandle::Channel::Init(const PWMHandle::Channel::Config &config) +{ + if(owner_.pimpl_ == nullptr) + return PWMHandle::Result::ERR; + + config_ = config; + handle_ = &(owner_.pimpl_->tim_hal_handle_); + + // float multiplier + scale_ = static_cast(owner_.pimpl_->config_.period); + + // Configure channel + TIM_OC_InitTypeDef oc_config = {0}; + oc_config.OCMode = TIM_OCMODE_PWM1; + oc_config.Pulse = 0; + oc_config.OCPolarity + = (config_.polarity == PWMHandle::Channel::Config::Polarity::LOW) + ? TIM_OCPOLARITY_LOW + : TIM_OCPOLARITY_HIGH; + oc_config.OCFastMode = TIM_OCFAST_DISABLE; + if(HAL_TIM_PWM_ConfigChannel(handle_, &oc_config, channel_) != HAL_OK) + { + return PWMHandle::Result::ERR; + } + + // Configure pin + if(!config_.pin.IsValid()) + { + config_.pin = GetDefaultPin(owner_.pimpl_->config_.periph, channel_); + } + if(!config_.pin.IsValid()) + { + return PWMHandle::Result::ERR; + } + GPIO_InitTypeDef GPIO_InitStruct = {0}; + GPIO_TypeDef * GPIO_Port = GetHALPort(config_.pin); + + GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; + GPIO_InitStruct.Pull = GPIO_NOPULL; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; + + switch(owner_.pimpl_->config_.periph) + { + case PWMHandle::Config::Peripheral::TIM_3: + GPIO_InitStruct.Alternate = GPIO_AF2_TIM3; + break; + case PWMHandle::Config::Peripheral::TIM_4: + GPIO_InitStruct.Alternate = GPIO_AF2_TIM4; + break; + case PWMHandle::Config::Peripheral::TIM_5: + GPIO_InitStruct.Alternate = GPIO_AF2_TIM5; + break; + } + + GPIO_InitStruct.Pin = GetHALPin(config_.pin); + + switch(config_.pin.port) + { + case PORTA: __HAL_RCC_GPIOA_CLK_ENABLE(); break; + case PORTB: __HAL_RCC_GPIOB_CLK_ENABLE(); break; + case PORTC: __HAL_RCC_GPIOC_CLK_ENABLE(); break; + case PORTD: __HAL_RCC_GPIOD_CLK_ENABLE(); break; + case PORTE: __HAL_RCC_GPIOE_CLK_ENABLE(); break; + case PORTF: __HAL_RCC_GPIOF_CLK_ENABLE(); break; + case PORTG: __HAL_RCC_GPIOG_CLK_ENABLE(); break; + case PORTH: __HAL_RCC_GPIOH_CLK_ENABLE(); break; + case PORTI: __HAL_RCC_GPIOI_CLK_ENABLE(); break; + case PORTJ: __HAL_RCC_GPIOJ_CLK_ENABLE(); break; + case PORTK: __HAL_RCC_GPIOK_CLK_ENABLE(); break; + default: break; + } + + HAL_GPIO_Init(GPIO_Port, &GPIO_InitStruct); + + // Start PWM on channel + if(HAL_TIM_PWM_Start(handle_, channel_) != HAL_OK) + { + return PWMHandle::Result::ERR; + } + + return PWMHandle::Result::OK; +} + +PWMHandle::Result PWMHandle::Channel::Init() +{ + return Init({}); +} + +PWMHandle::Result PWMHandle::Channel::DeInit() +{ + if(handle_ == nullptr) + return PWMHandle::Result::OK; + + auto *handle = &(owner_.pimpl_->tim_hal_handle_); + if(HAL_TIM_PWM_Stop(handle, channel_) != HAL_OK) + { + return PWMHandle::Result::ERR; + } + + handle_ = nullptr; + + return PWMHandle::Result::OK; +} + + +// ------------------------------------------------------------------------- + +PWMHandle::Result PWMHandle::Impl::Init(const PWMHandle::Config &config) +{ + config_ = config; + + // TIM3 and TIM4 are 16-bit; TIM5 is 32-bit + switch(config_.periph) + { + case PWMHandle::Config::Peripheral::TIM_3: + case PWMHandle::Config::Peripheral::TIM_4: + config_.period = (uint16_t)config_.period; + break; + default: break; + } + + // Build TIM handle + constexpr TIM_TypeDef *pwm_handles[NUM_TIMERS] = {TIM3, TIM4, TIM5}; + tim_hal_handle_.Instance = pwm_handles[static_cast(config.periph)]; + tim_hal_handle_.Init.CounterMode = TIM_COUNTERMODE_UP; + tim_hal_handle_.Init.Prescaler = config_.prescaler; + tim_hal_handle_.Init.Period = config_.period; + tim_hal_handle_.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; + tim_hal_handle_.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; + + // Enable timer clock + switch(config_.periph) + { + case PWMHandle::Config::Peripheral::TIM_3: + __HAL_RCC_TIM3_CLK_ENABLE(); + break; + + case PWMHandle::Config::Peripheral::TIM_4: + __HAL_RCC_TIM4_CLK_ENABLE(); + break; + + case PWMHandle::Config::Peripheral::TIM_5: + __HAL_RCC_TIM5_CLK_ENABLE(); + break; + } + + // Init timer + if(HAL_TIM_Base_Init(&tim_hal_handle_) != HAL_OK) + { + return PWMHandle::Result::ERR; + } + + // Set clock source to internal + TIM_ClockConfigTypeDef sClockSourceConfig = {0}; + sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; + if(HAL_TIM_ConfigClockSource(&tim_hal_handle_, &sClockSourceConfig) + != HAL_OK) + { + return PWMHandle::Result::ERR; + } + + // Init PWM + if(HAL_TIM_PWM_Init(&tim_hal_handle_) != HAL_OK) + { + return PWMHandle::Result::ERR; + } + + // Set synchronization config + TIM_MasterConfigTypeDef sMasterConfig = {0}; + sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; + sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; + if(HAL_TIMEx_MasterConfigSynchronization(&tim_hal_handle_, &sMasterConfig) + != HAL_OK) + { + return PWMHandle::Result::ERR; + } + + return PWMHandle::Result::OK; +} + +PWMHandle::Result PWMHandle::Impl::DeInit() +{ + if(HAL_TIM_PWM_DeInit(&tim_hal_handle_) != HAL_OK) + { + return PWMHandle::Result::ERR; + } + + if(HAL_TIM_Base_DeInit(&tim_hal_handle_) != HAL_OK) + { + return PWMHandle::Result::ERR; + } + + return PWMHandle::Result::OK; +} + +void PWMHandle::Impl::SetPrescaler(uint32_t prescaler) +{ + config_.prescaler = prescaler; + __HAL_TIM_SET_PRESCALER(&tim_hal_handle_, prescaler); +} + +void PWMHandle::Impl::SetPeriod(uint32_t period) +{ + config_.period = period; + __HAL_TIM_SET_AUTORELOAD(&tim_hal_handle_, period); +} + +// --------------------------------------------------------------------- + +PWMHandle::PWMHandle() +: pimpl_(nullptr), + ch1_(this, TIM_CHANNEL_1), + ch2_(this, TIM_CHANNEL_2), + ch3_(this, TIM_CHANNEL_3), + ch4_(this, TIM_CHANNEL_4) +{ +} + +PWMHandle::Result PWMHandle::Init(const PWMHandle::Config &config) +{ + const int pwm_idx = static_cast(config.periph); + if(pwm_idx >= NUM_TIMERS) + return PWMHandle::Result::ERR; + + pimpl_ = &pwm_handles[pwm_idx]; + return pimpl_->Init(config); +} + +PWMHandle::Result PWMHandle::DeInit() +{ + int result = 0; + + result |= (int)ch1_.DeInit(); + result |= (int)ch2_.DeInit(); + result |= (int)ch3_.DeInit(); + result |= (int)ch4_.DeInit(); + result |= (int)pimpl_->DeInit(); + + return static_cast(result); +} + +const PWMHandle::Config &PWMHandle::GetConfig() const +{ + return pimpl_->config_; +} + +void PWMHandle::SetPrescaler(uint32_t prescaler) +{ + pimpl_->SetPrescaler(prescaler); +} + +void PWMHandle::SetPeriod(uint32_t period) +{ + pimpl_->SetPeriod(period); +} + +} // namespace daisy \ No newline at end of file diff --git a/src/per/pwm.h b/src/per/pwm.h new file mode 100644 index 000000000..b68461b48 --- /dev/null +++ b/src/per/pwm.h @@ -0,0 +1,228 @@ +#pragma once +#ifndef DSY_PWM_H +#define DSY_PWM_H + +#include "daisy_core.h" +#include "stm32h7xx_hal.h" +#include + +namespace daisy +{ +/** @brief Hardware PWM using the timer peripheral. + * + * * Supports the following TIM peripherals: + * - TIM3, TIM4, TIM5 + * + * A single TIM peripheral can be used to control up to four PWM output + * channels, which share the same resolution and frequency but have independent + * duty cycles. + * + * Note that PWM interferes with the use of a TimerHandle on the same timer. + * + * Some channels have the option to choose which output pin they connect to. + * Only one pin can be connected to a given channel. Use the following table to + * determine which pin corresponds to each timer and channel. + * + * TIM3: + * - Channel 1: PA6 or PB4 (Daisy Seed -- D19, D9; default PA6/D19) + * - Channel 2: PA7, PB5, or PC7 (D18, D10, internal LED; default PC7/LED) + * - Channel 3: PC8 (D4) + * - Channel 4: PB1 or PC9 (D17, D3; default PB1/D17) + * + * TIM4: + * - Channel 1: PB6 (D13) + * - Channel 2: PB7 (D14) + * - Channel 3: PB8 (D11) + * - Channel 4: PB9 (D12) + * + * TIM5: + * - Channel 1: PA0 (D25) + * - Channel 2: PA1 (D24) + * - Channel 3: PA2 (D28) + * - Channel 4: PA3 (D16) + * + * Future work: + * - Support other timers, including HRTIM, TIM1, TIM8 + * - DMA + */ +class PWMHandle +{ + public: + class Impl; + + /** @brief Configuration struct for the timer peripheral + * @note These settings are used during initialization + * and changing them afterwards may not have the desired effect. + */ + struct Config + { + /** @brief Hardware Timer to use for PWM. */ + enum class Peripheral + { + TIM_3 = 0, /**< 16-bit counter (max period 0xffff) */ + TIM_4 = 1, /**< 16-bit counter (max period 0xffff) */ + TIM_5 = 2 /**< 32-bit counter (max period 0xffffffff) */ + }; + Peripheral periph; /**< Hardware Peripheral */ + + /** @brief Prescaler that divides the PWM timer frequency. The final + * frequency will be sysclk / (2 * (period + 1) * (prescaler + 1)). + */ + uint32_t prescaler; + + /** @brief period in ticks at TIM frequency before the counter resets. + * Affects both the frequency and resolution of PWM. + * @note TIM3 and TIM4 are both 16-bit timers so the max period is 0xffff. + * TIM5 is a 32-bit timer so the max period is 0xffffffff (about 20 seconds + * per reset). + */ + uint32_t period; + + Config() : periph(Peripheral::TIM_3), prescaler(0), period(0xffff) {} + Config(Peripheral periph_, + uint32_t prescaler_ = 0, + uint32_t period_ = 0xffff) + : periph(periph_), prescaler(prescaler_), period(period_) + { + } + }; + + /** @brief Return values for PWM functions. */ + enum class Result + { + OK = 0, + ERR = 1, + }; + + class Channel + { + public: + /** @brief Configuration struct for an individual channel + * @note These settings are used during initialization + * and changing them afterwards may not have the desired effect. + */ + struct Config + { + /** @brief Pin to use for this channel. Ensure that this is the proper pin + * for the timer and channel. Use PORTX (default) to select the channel's + * default pin. + */ + Pin pin; + + /** @brief Output polarity */ + enum class Polarity + { + HIGH = 0, /**< Output is high when channel is active */ + LOW /**< Output is low when channel is active */ + }; + Polarity polarity; + + Config() : pin(), polarity(Polarity::HIGH) {} + Config(Pin pin_, Polarity polarity_ = Polarity::HIGH) + : pin(pin_), polarity(polarity_) + { + } + }; + + /** @brief Private constructor for channel. Do not use. */ + Channel(PWMHandle *owner, uint32_t channel) + : owner_(*owner), channel_(channel), scale_(65535.0f), handle_(nullptr) + { + } + + /** @brief Returns a const reference to the Config struct */ + inline const Config &GetConfig() const { return config_; } + + /** @brief Initialize the channel. Must be called manually, after + * PWMHandle::Init */ + PWMHandle::Result Init(const Channel::Config &config); + + /** @brief Initialize the channel using all defaults. Must be called + * manually, after PWMHandle::Init */ + PWMHandle::Result Init(); + + /** @brief Deinitialize the channel. Called automatically by + * PWMHandle::DeInit */ + PWMHandle::Result DeInit(); + + /** @brief Set the duty cycle for the PWM channel. + * \param raw Must be less than or equal to the timer's period + */ + inline void SetRaw(uint32_t raw) + { + __HAL_TIM_SET_COMPARE(handle_, channel_, raw); + } + + /** @brief Set the duty cycle for the PWM channel. Automatically + * normalized to the timer's period. + * \param val Relative value, [0.0, 1.0] + * \note May experience rounding errors when period is > 2^24; use SetRaw. + */ + inline void Set(float val) + { + if(val < 0.0f) + val = 0.0f; + if(val > 1.0f) + val = 1.0f; + SetRaw(static_cast(val * scale_)); + } + + private: + PWMHandle & owner_; + const uint32_t channel_; + Channel::Config config_; + float scale_; + TIM_HandleTypeDef *handle_; + }; + + PWMHandle(); + + PWMHandle(const PWMHandle &other) = default; + PWMHandle &operator=(const PWMHandle &other) = default; + ~PWMHandle() {} + + /** @brief Initialize the PWM peripheral according to the config */ + Result Init(const Config &config); + + /** @brief Deinitialize the peripheral */ + Result DeInit(); + + /** @brief Returns a const reference to the Config struct */ + const Config &GetConfig() const; + + /** @brief Get a reference to CH1 of this peripheral. Must be initialized + * before use. */ + inline Channel &Channel1() { return ch1_; } + + /** @brief Get a reference to CH2 of this peripheral. Must be initialized + * before use. */ + inline Channel &Channel2() { return ch2_; } + + /** @brief Get a reference to CH3 of this peripheral. Must be initialized + * before use. */ + inline Channel &Channel3() { return ch3_; } + + /** @brief Get a reference to CH4 of this peripheral. Must be initialized + * before use. */ + inline Channel &Channel4() { return ch4_; }; + + /** @brief Set the prescaler */ + void SetPrescaler(uint32_t prescaler); + + /** @brief Set the period */ + void SetPeriod(uint32_t period); + + private: + Impl *pimpl_; + + // NOTE: These are stored here, not in the Impl class, so that channel + // references are valid even if taken before a call to PWMHandle::Init + Channel ch1_; + Channel ch2_; + Channel ch3_; + Channel ch4_; +}; + +} // namespace daisy + +#endif