PWM + Encoder = Please Help :-)

Post here first, or if you can't find a relevant section!
GVisser
Posts: 19
Joined: Thu Jun 11, 2020 5:17 pm
Answers: 1
Location: Hatfield, UK

PWM + Encoder = Please Help :-)

Post by GVisser »

Hi there,

I am brand new to this world so am struggling with the basics :oops:

I am trying to set up a PWM output from my STM32F401CC Black Pill which will have 2 encoders to vary the frequency and the duty cycle.

I have the PWM output working and I have 1 encoder working but cannot figure out how to set up an interrupt on the PWM code so that it will notice when I turn the encoder?

Any pointers would be appreciated! :D

EDIT: Ok it seems that I completely misunderstood interrupts, so the new question would be how to adjust the frequency and duty cycle of the HarwareTimer from within the loop? For that matterm I would also need to be able to enable and disable (pause/resume) the timer... :?:

Code: Select all

#include <LiquidCrystal.h> // include the LCD library
 
const int rs = PB_10, en = PB_2, d4 = PB_1, d5 = PB_0, d6 = PA_7, d7 = PA_6; //STM32 Pins to which LCD is connected
LiquidCrystal lcd(rs, en, d4, d5, d6, d7); //Initialize the LCD

#define PWMOUT PA0 // Main Output (Tim2Ch1)
#define STM32DUINO_CORE
#define ENC_CLK PB8
#define ENC_DATA PB9
#define LEDPIN PC13

volatile uint32_t encoderCount;

void encoder1_read(void)
{
  volatile static uint8_t ABs = 0;
  ABs = (ABs << 2) & 0x0f; //left 2 bits now contain the previous AB key read-out;
  ABs |= (digitalRead(ENC_CLK) << 1) | digitalRead(ENC_DATA);
  switch (ABs)
  {
    case 0x0d:
      encoderCount++;
      break;
    case 0x0e:
      encoderCount--;
      break;
  }
}

void led_blink()
{
  static uint8_t led_state = 0;
  led_state = 1 - led_state;
  digitalWrite(LEDPIN, led_state);
}
 
void setup() {
  // LCD setup
  lcd.begin(16, 2);//Defining 16*2 LCD
  lcd.setCursor(0, 0); //LCD Row 0 and Column 0
  lcd.print("Freaky ISSTC"); //Print this Line
  lcd.setCursor(0, 1); //LCD Row 0 and Column 1
  lcd.print("Interrupter V1.0"); //Print this Line
 
  delay(4000); //wait for four secounds
  lcd.clear(); //Clear the screen

  // Encoder setup
  Serial.begin(9600);
  pinMode(ENC_CLK, INPUT);
  pinMode(ENC_DATA, INPUT);
  pinMode(LEDPIN, OUTPUT);
  encoderCount = 10;

  // PWM output setup
  pinMode(PWMOUT, OUTPUT);
  // Automatically retrieve TIM instance and channel associated to pin
  // This is used to be compatible with all STM32 series automatically.
  TIM_TypeDef *Instance = (TIM_TypeDef *)pinmap_peripheral(digitalPinToPinName(PWMOUT), PinMap_PWM);
  uint32_t channel = STM_PIN_CHANNEL(pinmap_function(digitalPinToPinName(PWMOUT), PinMap_PWM));

  // Instantiate HardwareTimer object. Thanks to 'new' instantiation, HardwareTimer is not destructed when setup() function is finished.
  HardwareTimer *MyTim = new HardwareTimer(Instance);

  // Configure and start PWM
  MyTim->attachInterrupt(encoder1_read);
  MyTim->setPWM(channel, PWMOUT, encoderCount, 10); // encodercount Hertz, 10% dutycycle
  MyTim->resume();

}
 
void loop() {
  static uint32_t count;
  static uint32_t prevCount;
  count = encoderCount;
  if (count != prevCount)
  {
    prevCount = count;
    led_blink();
  }
  
lcd.print("BPS: ");
lcd.print(encoderCount); // display the beats per second
lcd.setCursor(0,1); // move cursor to next line
lcd.print("PW: ");
//lcd.print(PW); // display the pulse width
delay(200);
lcd.clear();

}

#ifdef STM32DUINO_CORE
void HAL_SYSTICK_Callback()
{
  encoder1_read();
}
#endif
by GVisser » Mon Jun 15, 2020 9:00 am
Morning all :-)

@fredbox @ag123 @ABOSTM THANK YOU all for your help :D

The code is now running as I had originally intended and is included below in case it helps someone else in the future.

Note: I did have to move the 'setPWM' out of the main loop as it was 'resetting' the output on every tick even if there was no change, and thus messing up the output. Only executing this IF there was movement in the encoder solved the problem.

As my plan, for the later version, was to have 5 encoders (all on seperate timers), I am rather going to simplify my life (and probably the code overhead) by moving to linear potentiometers.

Code: Select all

#include <LiquidCrystal.h> // include the LCD library
 
const int rs = PB_10, en = PB_2, d4 = PB_1, d5 = PB_0, d6 = PA_7, d7 = PA_6; //STM32 Pins to which LCD is connected
LiquidCrystal lcd(rs, en, d4, d5, d6, d7); //Initialize the LCD

#define STM32DUINO_CORE
#define PWMPIN PA0 // Main Output
#define RUN_FREQ_ENC_CLK PB8 //Encoder1
#define RUN_FREQ_ENC_DATA PB9
#define RUN_PW_SW PA5
#define RUN_PW_ENC_CLK PB4 //Encoder2
#define RUN_PW_ENC_DATA PB5
#define RUN_PW_SW PA6

#define LEDPIN PC13

volatile uint32_t RUN_FREQ = 100;
volatile uint32_t RUN_PW = 10;
// volatile uint32_t BURST_FREQ = 10;
// volatile uint32_t BURST_PW = 50;
// volatile uint32_t ONESHOT_PW = 50;

// Automatically retrieve TIM instance and channel associated to pin
// This is used to be compatible with all STM32 series automatically.
TIM_TypeDef *Instance = (TIM_TypeDef *)pinmap_peripheral(digitalPinToPinName(PWMPIN), PinMap_PWM);
uint32_t channel = STM_PIN_CHANNEL(pinmap_function(digitalPinToPinName(PWMPIN), PinMap_PWM));

// Instantiate HardwareTimer object. Thanks to 'new' instantiation, HardwareTimer is not destructed when setup() function is finished.
HardwareTimer *MyTim = new HardwareTimer(Instance);


void run_freq_enc_read(void)
{
  volatile static uint8_t F_ABs = 0;
  F_ABs = (F_ABs << 2) & 0x0f; //left 2 bits now contain the previous AB key read-out;
  F_ABs |= (digitalRead(RUN_FREQ_ENC_CLK) << 1) | digitalRead(RUN_FREQ_ENC_DATA);
  switch (F_ABs)
  {
    case 0x0d:
      RUN_FREQ++;
      break;
    case 0x0e:
      RUN_FREQ--;
      break;
  }
}

void run_pw_enc_read(void)
{
  volatile static uint8_t P_ABs = 0;
  P_ABs = (P_ABs << 2) & 0x0f; //left 2 bits now contain the previous AB key read-out;
  P_ABs |= (digitalRead(RUN_PW_ENC_CLK) << 1) | digitalRead(RUN_PW_ENC_DATA);
  switch (P_ABs)
  {
    case 0x0d:
      RUN_PW++;
      break;
    case 0x0e:
      RUN_PW--;
      break;
  }
}

void led_blink()
{
  static uint8_t led_state = 0;
  led_state = 1 - led_state;
  digitalWrite(LEDPIN, led_state);
}
 
void setup() {
  // LCD setup
  lcd.begin(16, 2);//Defining 16*2 LCD
  lcd.setCursor(0, 0); //LCD Row 0 and Column 0
  lcd.print("Freaky ISSTC"); //Print this Line
  lcd.setCursor(0, 1); //LCD Row 0 and Column 1
  lcd.print("Interrupter V1.0"); //Print this Line
 
  delay(4000); //wait for four secounds
  lcd.clear(); //Clear the screen

  // Encoder setup
  Serial.begin(9600);
  pinMode(RUN_FREQ_ENC_CLK, INPUT);
  pinMode(RUN_FREQ_ENC_DATA, INPUT);
  pinMode(RUN_PW_ENC_CLK, INPUT);
  pinMode(RUN_PW_ENC_DATA, INPUT);
  pinMode(LEDPIN, OUTPUT);

  // Configure and start PWM
  MyTim->setPWM(channel, PWMPIN, RUN_FREQ, RUN_PW); // RUN_FREQ Hertz, RUN_PW dutycycle
}
 
void loop() {
  static uint32_t freq_count;
  static uint32_t freq_prevCount;
  freq_count = RUN_FREQ;
  if (freq_count != freq_prevCount)
  {
    freq_prevCount = freq_count;
    led_blink();
    MyTim->setPWM(channel, PWMPIN, RUN_FREQ, RUN_PW); // RUN_FREQ Hertz, RUN_PW dutycycle
  }
  static uint32_t pw_count;
  static uint32_t pw_prevCount;
  pw_count = RUN_PW;
  if (pw_count != pw_prevCount)
  {
    pw_prevCount = pw_count;
    led_blink();
    MyTim->setPWM(channel, PWMPIN, RUN_FREQ, RUN_PW); // RUN_FREQ Hertz, RUN_PW dutycycle
  }
  
lcd.print("BPS: ");
lcd.print(RUN_FREQ); // display the beats per second
lcd.setCursor(0,1); // move cursor to next line
lcd.print("PW: ");
lcd.print(RUN_PW); // display the pulse width
delay(200);
lcd.clear();

}

#ifdef STM32DUINO_CORE
void HAL_SYSTICK_Callback()
{
  run_freq_enc_read();
  run_pw_enc_read();
}
#endif
pic_7_1.png
Go to full post
ABOSTM
Posts: 60
Joined: Wed Jan 08, 2020 8:40 am
Answers: 7

Re: PWM + Encoder = Please Help :-)

Post by ABOSTM »

Hi @GVisser,
Unfortunately, HardwareTimer library doesn't support Encoder mode,
but hardware support it: A timer can be used with 2 input signal from encoder, and timer counter will count according to encoder rotation (backward / forward).
So one solution would be to use STM32 cube HAL API to benefit from Hardware Encoder mode.
Note: STM32 cube HAL API can be used with an arduino sketch. But its usage is not as simple as Arduino API.

The other solution is to handle encoder signal by software like you did in your sketch.
And so what you need is not an interrupt on PWM (output) but rather an interrupt on input signal. It means an interrupt on digital input GPIO.
In that case you can use one of the following API:
void attachInterrupt(uint32_t pin, callback_function_t callback, uint32_t mode);
or
void attachInterrupt(uint32_t pin, void (*callback)(void), uint32_t mode);
ABOSTM
Posts: 60
Joined: Wed Jan 08, 2020 8:40 am
Answers: 7

Re: PWM + Encoder = Please Help :-)

Post by ABOSTM »

All API for HardwareTimer are detailed here: https://github.com/stm32duino/wiki/wiki ... ibrary#API

You will found API to change period (Overflow) pulse duartion or duty cycle (CaptureCompare). No need to pause/resume timer for that (even if aPI exist if needed)
You can also found some hardwareTimer examples.
GVisser
Posts: 19
Joined: Thu Jun 11, 2020 5:17 pm
Answers: 1
Location: Hatfield, UK

Re: PWM + Encoder = Please Help :-)

Post by GVisser »

Hi @ABOSTM ,

Many thanks for your response!

Unfortunately, the only part that I understood was that the HarwareTimer library does not support encoders :D The rest went completely over my head, so it seems I may be out of my depth :oops: (I have no idea what the STM32 cube HAL API is)

All I wanted to do was start a PWM signal running at a variable freq and pulse width on the push of a button, have the ability to vary the parameters using encoders (or pots) and stop it when the button is pressed again.

For this I suppose a soft PWM analogwrite would be my easiest option?

OR are you saying that I can still use hardware timers, just not the library?
ABOSTM
Posts: 60
Joined: Wed Jan 08, 2020 8:40 am
Answers: 7

Re: PWM + Encoder = Please Help :-)

Post by ABOSTM »

STM32 Cube HAL is the StMicroelectronics official drivers.

In you jroject you really need to distinguish 2 things:
* PWM outuput. In your sketch it is done thanks to HardwareTimer library. This is very good.
My proposal, in order to simply you things, is to use also use following function to update the frequency/duty cycle in the loop.

Code: Select all

MyTim->setPWM(.......)
* Input signal from encoder:
Let's forget about hardware encoder mode (a bit complex).
Your sketch propose to handle it by software with digitalRead(). Fine.

Now there is the point about interrupt:
you can call function

Code: Select all

 attachInterrupt()
on CLK pin or on each encoder pins (it depends on your encoder, but reading the code it looks like the function should be called for each pin), in setup. The parameter 'callback' should be your function

Code: Select all

HAL_SYSTICK_Callback()
Thus each time pin state is changed, the callback( and the encoder1_read() ) function will be called.
GVisser
Posts: 19
Joined: Thu Jun 11, 2020 5:17 pm
Answers: 1
Location: Hatfield, UK

Re: PWM + Encoder = Please Help :-)

Post by GVisser »

ABOSTM wrote: Fri Jun 12, 2020 1:42 pm STM32 Cube HAL is the StMicroelectronics official drivers.
ok thanks :-)
ABOSTM wrote: Fri Jun 12, 2020 1:42 pm In you jroject you really need to distinguish 2 things:
* PWM outuput. In your sketch it is done thanks to HardwareTimer library. This is very good.
My proposal, in order to simply you things, is to use also use following function to update the frequency/duty cycle in the loop.

Code: Select all

MyTim->setPWM(.......)
How do I do this? When I put it in the loop I get compile errors as MyTim and channel are not in this scope ... Do I need to move the HarwareTimer out of setup()?
ABOSTM wrote: Fri Jun 12, 2020 1:42 pm * Input signal from encoder:
Let's forget about hardware encoder mode (a bit complex).
Your sketch propose to handle it by software with digitalRead(). Fine.

Now there is the point about interrupt:
you can call function

Code: Select all

 attachInterrupt()
on CLK pin or on each encoder pins (it depends on your encoder, but reading the code it looks like the function should be called for each pin), in setup. The parameter 'callback' should be your function

Code: Select all

HAL_SYSTICK_Callback()
Thus each time pin state is changed, the callback( and the encoder1_read() ) function will be called.
Where in my sketch do I add

Code: Select all

 attachInterrupt()
and

Code: Select all

HAL_SYSTICK_Callback()
?

Sorry ABOSTM, I really appreciate your assistance, am just very confused.
ag123
Posts: 1653
Joined: Thu Dec 19, 2019 5:30 am
Answers: 24

Re: PWM + Encoder = Please Help :-)

Post by ag123 »

i've not tried meddling with the h/w timer in the official core
the api is described in detail
https://github.com/stm32duino/wiki/wiki ... er-library
that won't give much clues but there are examples
https://github.com/stm32duino/wiki/wiki ... y#Examples
try those out and the 'hello world' of hardware timer is to use it to blink the led. apparently that's the 1st example.
try that out and the 1st step is to blink the led with the hardware timer.
and keep the reference manual handy
https://www.st.com/resource/en/referenc ... ronics.pdf
https://www.st.com/resource/en/referenc ... 096844.pdf
for the duty cycle and period if i'm lazy i'd play with

Code: Select all

void setPWM(uint32_t channel, PinName pin, uint32_t frequency, uint32_t dutycycle, callback_function_t PeriodCallback = nullptr, callback_function_t CompareCallback = nullptr); // Set all in one command freq in HZ, Duty in percentage. Including both interrup.
it doesn't look too ideal, but one can probably dig into the codes to see how things like prescalar factors, and the 'overflow' and 'capture compare' is set within the function with a specified frequency and duty cycle
i think it would be good if the core has a separate set period or set frequency function.
the frequency or period is some combination of prescalar and the 'overflow'.
pre-scalar does PCLK (pheriperial clock speed) / pre-scalar. then the 'overflow' normally counts the number of PCLK / prescalar coarse cycles.
duty cycle may have something to do with the capture compare registers, so setting capture compare varies the duty cycle.
this should be channel specific. i.e. pre scalar and overflow sets the common base frequency, capture compare sets the duty cycle - per channel

as for rotary encoder i'd guess you're referring to rotary encoders
https://playground.arduino.cc/Main/RotaryEncoders/
i'd think it is possible to work that by tracking the state transitions, but these days there are libraries for it it seemed.
fredbox
Posts: 125
Joined: Thu Dec 19, 2019 3:05 am
Answers: 2

Re: PWM + Encoder = Please Help :-)

Post by fredbox »

For rotary encoder see this thread: viewtopic.php?f=7&t=365
Edit - looks like you already found that one. You don't need to attach the timer interrupt to encoder1_read().
Delete this line:

Code: Select all

  MyTim->attachInterrupt(encoder1_read);
Systick polls the encoder at 1 millisecond intervals.
Does the example from the other thread work on your encoder?
GVisser
Posts: 19
Joined: Thu Jun 11, 2020 5:17 pm
Answers: 1
Location: Hatfield, UK

Re: PWM + Encoder = Please Help :-)

Post by GVisser »

Hi @ag123 ,

Thank you for your reply and yes I will look further into the setPWM function.

As for the rest, the PWM is working properly and the reading of the decoder is working properly, the issue is that I cannot find a way to alter the PWM signal with values from the main loop. Once setup is done, the PWM signal runs regardless and I am way too new to programming to know how to fix it :-)

My aim is to be able to starts and stop the PWM output from within the main loop and, use the encoders (or potentiometers) to set and vary the PWM frequency and duty cycle (or preferably, the actual +pulse time). 8-)

ag123 wrote: Fri Jun 12, 2020 4:28 pm i've not tried meddling with the h/w timer in the official core
the api is described in detail
https://github.com/stm32duino/wiki/wiki ... er-library
that won't give much clues but there are examples
https://github.com/stm32duino/wiki/wiki ... y#Examples
try those out and the 'hello world' of hardware timer is to use it to blink the led. apparently that's the 1st example.
try that out and the 1st step is to blink the led with the hardware timer.
and keep the reference manual handy
https://www.st.com/resource/en/referenc ... ronics.pdf
https://www.st.com/resource/en/referenc ... 096844.pdf
for the duty cycle and period if i'm lazy i'd play with

Code: Select all

void setPWM(uint32_t channel, PinName pin, uint32_t frequency, uint32_t dutycycle, callback_function_t PeriodCallback = nullptr, callback_function_t CompareCallback = nullptr); // Set all in one command freq in HZ, Duty in percentage. Including both interrup.
it doesn't look too ideal, but one can probably dig into the codes to see how things like prescalar factors, and the 'overflow' and 'capture compare' is set within the function with a specified frequency and duty cycle
i think it would be good if the core has a separate set period or set frequency function.
the frequency or period is some combination of prescalar and the 'overflow'.
pre-scalar does PCLK (pheriperial clock speed) / pre-scalar. then the 'overflow' normally counts the number of PCLK / prescalar coarse cycles.
duty cycle may have something to do with the capture compare registers, so setting capture compare varies the duty cycle.
this should be channel specific. i.e. pre scalar and overflow sets the common base frequency, capture compare sets the duty cycle - per channel

as for rotary encoder i'd guess you're referring to rotary encoders
https://playground.arduino.cc/Main/RotaryEncoders/
i'd think it is possible to work that by tracking the state transitions, but these days there are libraries for it it seemed.
Last edited by GVisser on Sat Jun 13, 2020 5:33 pm, edited 1 time in total.
GVisser
Posts: 19
Joined: Thu Jun 11, 2020 5:17 pm
Answers: 1
Location: Hatfield, UK

Re: PWM + Encoder = Please Help :-)

Post by GVisser »

Hi @fredbox ,

Thanks for your reply. Yes, the example and most of my code runs perfectly, I just cannot figure out how to get a value in the main loop to change the settings of the running PWM output!

As for the interrupt, I added that in before I knew anything about interrupts, so yes, I have already removed it :-)
fredbox wrote: Fri Jun 12, 2020 5:16 pm For rotary encoder see this thread: viewtopic.php?f=7&t=365
Edit - looks like you already found that one. You don't need to attach the timer interrupt to encoder1_read().
Delete this line:

Code: Select all

  MyTim->attachInterrupt(encoder1_read);
Systick polls the encoder at 1 millisecond intervals.
Does the example from the other thread work on your encoder?
Post Reply

Return to “General discussion”