From 2cd0ddc62b6c7f7d6d15e5e8a37335c1537ad25a Mon Sep 17 00:00:00 2001 From: Greg Matthew Crossley Date: Mon, 17 Nov 2025 13:13:51 -0500 Subject: [PATCH] Expand PWM support from 3 to 5 simultaneous channels This commit expands the PWM driver to support 5 simultaneous PWM channels (up from 3), utilizing both TIMER0 and TIMER1 hardware peripherals. Changes: - Increased max_pwm_channels from 3 to 5 in pwm.h - Implemented 2-timer allocation strategy in pwm.cpp: * Channels 0-2 use TIMER0 (CC0, CC1, CC2) * Channels 3-4 use TIMER1 (CC0, CC1) - Added helper methods get_timer_for_channel() and get_cc_channel_for_pwm_channel() - All channels share the same frequency (GSDK constraint) with independent duty cycles - Updated test_sketch.ino to test all 5 PWM channels - Added pwm_multi_channel example demonstrating 5-channel usage Hardware tested on Arduino Nano Matter (EFR32xG24) with oscilloscope verification of 4 channels (the limit of my oscilloscope) operating simultaneously with different duty cycles. All 28 automated build tests pass. --- cores/gecko/pwm.cpp | 36 ++++++-- cores/gecko/pwm.h | 20 ++++- .../pwm_multi_channel/pwm_multi_channel.ino | 89 +++++++++++++++++++ test/build/test_build.py | 1 + test/build/test_sketch/test_sketch.ino | 6 +- 5 files changed, 142 insertions(+), 10 deletions(-) create mode 100644 libraries/SiliconLabs/examples/pwm_multi_channel/pwm_multi_channel.ino diff --git a/cores/gecko/pwm.cpp b/cores/gecko/pwm.cpp index 8a163a36..0b4150a4 100644 --- a/cores/gecko/pwm.cpp +++ b/cores/gecko/pwm.cpp @@ -41,13 +41,13 @@ PwmClass::PwmClass() : .polarity = PWM_ACTIVE_HIGH, }; - for (auto& pwm_pin : pwm_pins) { - pwm_pin.pin = PIN_NAME_MAX; - pwm_pin.inst.timer = TIMER0; - pwm_pin.inst.channel = 0; - pwm_pin.inst.port = gpioPortA; - pwm_pin.inst.pin = 0; - pwm_pin.inst.location = 0; + for (uint8_t i = 0; i < max_pwm_channels; i++) { + pwm_pins[i].pin = PIN_NAME_MAX; + pwm_pins[i].inst.timer = get_timer_for_channel(i); + pwm_pins[i].inst.channel = get_cc_channel_for_pwm_channel(i); + pwm_pins[i].inst.port = gpioPortA; + pwm_pins[i].inst.pin = 0; + pwm_pins[i].inst.location = 0; } this->pwm_mutex = xSemaphoreCreateMutexStatic(&this->pwm_mutex_buf); @@ -71,7 +71,7 @@ bool PwmClass::init(PinName pin, int frequency) this->pwm_pins[pwm_channel_idx].duty_cycle_percent = 101; this->pwm_pins[pwm_channel_idx].inst.port = getSilabsPortFromArduinoPin(pin); this->pwm_pins[pwm_channel_idx].inst.pin = getSilabsPinFromArduinoPin(pin); - this->pwm_pins[pwm_channel_idx].inst.channel = pwm_channel_idx; + this->pwm_pins[pwm_channel_idx].inst.channel = get_cc_channel_for_pwm_channel(pwm_channel_idx); GPIO_PinModeSet(this->pwm_pins[pwm_channel_idx].inst.port, this->pwm_pins[pwm_channel_idx].inst.pin, gpioModePushPull, 0); pwm_config.frequency = frequency; @@ -240,4 +240,24 @@ void PwmClass::deinit_all_pwm_channels() } } +TIMER_TypeDef* PwmClass::get_timer_for_channel(uint8_t channel_idx) +{ + // Channels 0-2 use TIMER0, channels 3-4 use TIMER1 + if (channel_idx <= 2) { + return TIMER0; + } else { + return TIMER1; + } +} + +uint8_t PwmClass::get_cc_channel_for_pwm_channel(uint8_t channel_idx) +{ + // Map PWM channel to timer's CC channel + if (channel_idx <= 2) { + return channel_idx; // 0->CC0, 1->CC1, 2->CC2 + } else { + return channel_idx - 3; // 3->CC0, 4->CC1 + } +} + arduino::PwmClass PWM; diff --git a/cores/gecko/pwm.h b/cores/gecko/pwm.h index 92969dfd..ba4ac87d 100644 --- a/cores/gecko/pwm.h +++ b/cores/gecko/pwm.h @@ -124,7 +124,7 @@ class PwmClass { SemaphoreHandle_t pwm_mutex; StaticSemaphore_t pwm_mutex_buf; - static const uint8_t max_pwm_channels = 3u; + static const uint8_t max_pwm_channels = 5u; static const uint32_t pwm_stabilization_time_ms = 2u; uint32_t duty_cycle_set_time; @@ -165,6 +165,24 @@ class PwmClass { *****************************************************************************/ uint8_t get_num_of_pwm_channels_in_use(); + /**************************************************************************//** + * Returns the timer peripheral for the given PWM channel index + * + * @param[in] channel_idx PWM channel index (0-4) + * + * @return Pointer to timer peripheral (TIMER0 or TIMER1) + *****************************************************************************/ + TIMER_TypeDef* get_timer_for_channel(uint8_t channel_idx); + + /**************************************************************************//** + * Returns the compare/capture channel number for the given PWM channel index + * + * @param[in] channel_idx PWM channel index (0-4) + * + * @return CC channel number (0, 1, or 2) + *****************************************************************************/ + uint8_t get_cc_channel_for_pwm_channel(uint8_t channel_idx); + /**************************************************************************//** * Deinitializes all active PWM channels *****************************************************************************/ diff --git a/libraries/SiliconLabs/examples/pwm_multi_channel/pwm_multi_channel.ino b/libraries/SiliconLabs/examples/pwm_multi_channel/pwm_multi_channel.ino new file mode 100644 index 00000000..158084ca --- /dev/null +++ b/libraries/SiliconLabs/examples/pwm_multi_channel/pwm_multi_channel.ino @@ -0,0 +1,89 @@ +/* + Multi-Channel PWM + + This sketch demonstrates the use of up to 5 simultaneous PWM channels on Silicon Labs boards. + Each channel can have an independent duty cycle (0-100%), all running at the same frequency (1kHz default). + + The sketch outputs PWM signals with different duty cycles on pins D3-D7: + - D3: 20% duty cycle + - D4: 40% duty cycle + - D5: 60% duty cycle + - D6: 80% duty cycle + - D7: 100% duty cycle (always HIGH) + + You can connect LEDs (with appropriate current-limiting resistors) to these pins to see + different brightness levels, or measure the PWM signals with an oscilloscope. + + Hardware Implementation: + - Channels 0-2 use TIMER0 (CC0, CC1, CC2) + - Channels 3-4 use TIMER1 (CC0, CC1) + - All channels share the same frequency (GSDK constraint) + - Each channel has independent duty cycle control + + Compatible with all Silicon Labs Arduino boards. + + Author: Greg Matthew Crossley (github.com/gregmatthewcrossley) + */ + +// Define PWM output pins +#define PWM_PIN_1 D3 +#define PWM_PIN_2 D4 +#define PWM_PIN_3 D5 +#define PWM_PIN_4 D6 +#define PWM_PIN_5 D7 + +// Define duty cycles (0-255, where 255 = 100%) +#define DUTY_20_PERCENT 51 // 20% of 255 +#define DUTY_40_PERCENT 102 // 40% of 255 +#define DUTY_60_PERCENT 153 // 60% of 255 +#define DUTY_80_PERCENT 204 // 80% of 255 +#define DUTY_100_PERCENT 255 // 100% of 255 + +void setup() +{ + Serial.begin(115200); + delay(2000); // Wait for Serial connection + + Serial.println("Multi-Channel PWM Demo"); + Serial.println(); + Serial.println("This demo shows 5 simultaneous PWM channels"); + Serial.println("with different duty cycles on pins D3-D7."); + Serial.println(); + + // Configure pins as outputs + pinMode(PWM_PIN_1, OUTPUT); + pinMode(PWM_PIN_2, OUTPUT); + pinMode(PWM_PIN_3, OUTPUT); + pinMode(PWM_PIN_4, OUTPUT); + pinMode(PWM_PIN_5, OUTPUT); + + // Start PWM on all 5 channels with different duty cycles + Serial.println("Starting PWM channels..."); + + analogWrite(PWM_PIN_1, DUTY_20_PERCENT); + Serial.println(" D3: 20% duty cycle"); + + analogWrite(PWM_PIN_2, DUTY_40_PERCENT); + Serial.println(" D4: 40% duty cycle"); + + analogWrite(PWM_PIN_3, DUTY_60_PERCENT); + Serial.println(" D5: 60% duty cycle"); + + analogWrite(PWM_PIN_4, DUTY_80_PERCENT); + Serial.println(" D6: 80% duty cycle"); + + analogWrite(PWM_PIN_5, DUTY_100_PERCENT); + Serial.println(" D7: 100% duty cycle (always HIGH)"); + + Serial.println(); + Serial.println("All PWM channels active at 1kHz frequency!"); + Serial.println("Connect LEDs or an oscilloscope to see the outputs."); +} + +void loop() +{ + // PWM continues running automatically + // You can add code here to dynamically change duty cycles if desired + + delay(1000); +} diff --git a/test/build/test_build.py b/test/build/test_build.py index 54063285..66b9cd3d 100644 --- a/test/build/test_build.py +++ b/test/build/test_build.py @@ -163,6 +163,7 @@ "../../libraries/SiliconLabs/examples/ble_xg27_devkit_sensors/ble_xg27_devkit_sensors.ino": xg27devkit_ble_silabs, "../../libraries/SiliconLabs/examples/dac_sawtooth/dac_sawtooth.ino": boards_with_dac, "../../libraries/SiliconLabs/examples/hwinfo/hwinfo.ino": all_variants, + "../../libraries/SiliconLabs/examples/pwm_multi_channel/pwm_multi_channel.ino": all_variants, "../../libraries/SiliconLabs/examples/xg27devkit_sensors/xg27devkit_sensors.ino": xg27devkit_ble_silabs, "../../libraries/SiliconLabs/examples/thingplusmatter_debug_unix/thingplusmatter_debug_unix.ino": all_ble_silabs, "../../libraries/SiliconLabs/examples/thingplusmatter_debug_win/thingplusmatter_debug_win.ino": all_ble_silabs, diff --git a/test/build/test_sketch/test_sketch.ino b/test/build/test_sketch/test_sketch.ino index ff6509e2..f3e35019 100644 --- a/test/build/test_sketch/test_sketch.ino +++ b/test/build/test_sketch/test_sketch.ino @@ -62,7 +62,11 @@ void setup() val = analogRead(PA1); Serial.println(val, OCT); - analogWrite(PA0, 128); + analogWrite(PA0, 51); + analogWrite(PA1, 102); + analogWrite(PA2, 153); + analogWrite(PA3, 204); + analogWrite(PA4, 255); tone(PA0, 440, 0); noTone(PA0);