Software PWM Generierung mit 1-20Hz und variablen Tastverhältnnis

Hallo,

ich versuche gerade ein Software PWM Generator mit variabler Frequenz (1-20Hz) und variablem Tastverhältnis zu programmieren.

Da die gewünschten Frequenzen sehr niedrig sind, wollte ich das ganze einfach als Software PWM aufbauen und einen Digital Pin ein und wieder ausschalten.
Dazu verwende ich micros(), doch irgendwie bekomme ich kein sauberes Ausgangssignal hin.

Zur Veränderung der Grundfrequenz und des Tastverhältnisses verwende ich einen Drehgeber, was soweit auch zu funktionieren scheint, weil die Sollwerte jeweils korrekt in die entsprechenden Variablen abgelegt werden.

Hier mal der entsprechende Code Auszug der SoftPWM Funktion. Die SoftPWM Funktion rufe ich innerhalb einer switch case Struktur innerhalb loop() auf.

Verwendete Variablen:

// Variables for Solenoid Control
int SolenoidFrequency = 2;
int MinFrequency =1;
int MaxFrequency =20;
int SolenoidDutyCycle = 50;
int MinDutyCycle =0;
int MaxDutyCycle =100;
int OldSolenoidFrequency=0;
int OldSolenoidDutyCycle =0;

unsigned long ActualMicros=0;       // actual time in µs
unsigned long PeriodTime =500000;   // Time of one DC in µs ( 1000000/SolenoidFrequency)
unsigned long OnTime =0;            // On time of PWM DC in µs calculated with selected frequency and selected DC
unsigned long OffTime =0;           // Off time of PWM DC in µs calculated with selected frequency and selected DC
unsigned long LastDCUpdate=0;       // save the time when last DC was started

bool PWMPinState=LOW;
bool ChangeDC=true;

SoftPWM Funktion:

void SoftPWM(){
// Calculated Total DC Time, on and off Time with selected DC and Frequency
if (SolenoidFrequency != OldSolenoidFrequency) {
   PeriodTime=1000000/SolenoidFrequency;
   OldSolenoidFrequency = SolenoidFrequency;
}
//PeriodTime=1000000/SolenoidFrequency;    // calculate TotalDCTime in µS

if (SolenoidDutyCycle != OldSolenoidDutyCycle) {
   OnTime=PeriodTime*SolenoidDutyCycle/100; //calculate on time of DC in µS
   OldSolenoidDutyCycle = SolenoidDutyCycle;
   OffTime=PeriodTime-OnTime;
   DEBUG_PRINT("OnTime soll ");
   DEBUG_PRINTLN(OnTime);
}

//ActualMicros=get_T2_micros(); // used when Timer2 library is used
ActualMicros=micros();

if (ActualMicros - LastDCUpdate >=  PeriodTime && PWMPinState==LOW){
  //Its time set output high
  DEBUG_PRINT("Periodtime soll ");
  DEBUG_PRINTLN(PeriodTime);
  DEBUG_PRINT("Periodtime ist ");
  DEBUG_PRINTLN(ActualMicros -LastDCUpdate);
  PWMPinState = HIGH;
  digitalWrite(solenoidPwmPin, PWMPinState );
  LastDCUpdate=ActualMicros;  
}
if (ActualMicros - LastDCUpdate >= OnTime && PWMPinState ==HIGH) {
    // its time to switch off again 
    PWMPinState = LOW;
    digitalWrite(solenoidPwmPin, PWMPinState );
    DEBUG_PRINT("OnTime ist : ");
    DEBUG_PRINTLN(ActualMicros - LastDCUpdate);
}
}

Auszug aus der case Struktur

case S_OpenLoopControl:
/* Case Description:
 * One can control open loop the solenoid voltage
 * By dafault one can manipulate the Duty Cycle by turning the knob
 * To change the Frequency doubleclick the knob and the change the frequency
 * to change again the Duty Cycle doubleklick again the knob
 */
lcd.setCursor(0,0);
lcd.print("Openloop Control");
lcd.setCursor(0,1);
lcd.print("Frequenz in Hz : ");
lcd.print(SolenoidFrequency);
lcd.setCursor(0,2);
lcd.print("Dutycycle in % : ");
lcd.print(SolenoidDutyCycle);
SoftPWM();
break;

Mich wundert die Abweichung in der Frequenz. Hier ein Auszug des Debug Prints:

"
OnTime soll 260000
OnTime ist : 263584
Periodtime soll 500000
Periodtime ist 525976
"

Zudem wird eine Veränderung des Tastverhältnisses (einstellbar zw. 0-100%) nicht immer übernommen.

Über Tipps freue ich mich sehr!
Gruß Alex

Ich verstehe das Problem so, dass selbst bei nur 2 Hz (500000µs) heftige Abweichungen zu sehen sind?

Da du nicht alles zeigst, schreib doch einfach einen Testsketch der nur das enthält, was du auch zeigst. :wink:
Vielleicht kann man da sehen, wo die 3.5 bzw. 26 ms verplempert werden?
Oder das Problem taucht dann gar nicht so heftig auf?

Ausgaben auf den seriellen Monitor und LCD brauchen Zeit. Die solltest Du nur jede halbe Sekunde machen. Schneller kannst Du sowieso nicht lesen.

Hallo,

habe dein Rat befolgt und nun scheint das Timing sehr gut zu passen. Problematisch war, dass ich bei jedem Durchlauf von loop das LCD Display neu beschrieben habe, was wohl zu lange dauert.

Allerdings glaube ich, dass mein Programm wohl generell einige Schwächen hat.
Daher hier mal der gesamte Code. Dachte gestern nur, dass das verwirrt.

  1. Teil des Codes
/* References:
 *  1. Rotary Encoder
 *  http://github.com/aleh/ec11
 *  Copyright (C) 2016, Aleh Dzenisiuk.
 *  
 *  2. MultliClick
 * http://forum.arduino.cc/index.php?topic=14479.0
 * 
 * 3. Timer2  
 * By Gabriel Staples
 *  http://electricrcaircraftguy.blogspot.com/
 * 7 Dec. 2013
 */
#include <EC11.h>
#include <Wire.h> //Wire.h Bibliothek einbinden
#include <LiquidCrystal_I2C.h> //LiquidCrystal_I2C.h Bibliothek einbinden

LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE); //

EC11 encoder;

// PIN Defintions

const int encoderPinA = 2;    // Pin for  Encoder A (Interrupt)
const int encoderPinB = 3;    // Pin for  Encoder B (Interrupt)
const int buttonPin = 4;      // Pin for  Encoder integrated Pushbutton
// Display connected to A4(SDA) and A5 (SCL)
const int solenoidPwmPin = 13; // Output for lowside driver
const int hallsensorPin = 6;   // Input Pin for hall sensor measurement
const int ResistorMeasurePin= A7; // Input for resistance measurement

// Constants for Displaycontrol
const int MaxCharacters= 20;
const int MaxLines = 4;
const int RefreshRateLCD = 2000; // update time of display in ms

// Definition of different Test Modes as states

const int S_StartScreen       = 0;
const int S_BasisTest         = 1;
const int S_Drehzahlmessung   = 2;
const int S_OpenLoopControl   = 3;
const int S_CloseLoopControl  = 4;
const int S_DauerhubTest      = 5;
const int S_Modusauswahl      = 6;

// Variables for resistance Measurement
float Vref= 5;  // Refernece Voltage
int R1= 1998.0; // Known Resistor in voltage divider
long MeasuredValue;
float VoltageR2; //Spannung über dem zu messenden Widerstand
float R2;
float CoilResistance;
const int UpdateRateCoilResistance=1000; //update interval in ms for coil resistance measurement
unsigned long LastCoilUpdate=0;

// Variables for Hall Sensor data aquisition
int HallPulseCounter = 0; // variable to save measured Hall Pulses in standard test (0...6 Pulses)
bool ActualHallPulse = HIGH;
bool LastHallPulse = HIGH;
int HallCounts= 0;
unsigned long PulseWidth = 0;
unsigned long RotationalSpeed= 0;
const int PulsesPerRev= 6;

// Variables for Solenoid Control
int SolenoidFrequency = 2;
int MinFrequency =1;
int MaxFrequency =20;
int SolenoidDutyCycle = 50;
int MinDutyCycle =0;
int MaxDutyCycle =100;
int OldSolenoidFrequency=0;
int OldSolenoidDutyCycle =0;

unsigned long ActualMicros=0;       // actual time in µs
unsigned long PeriodTime =500000;   // Time of one DC in µs ( 1000000/SolenoidFrequency)
unsigned long OnTime =0;            // On time of PWM DC in µs calculated with selected frequency and selected DC
unsigned long OffTime =0;           // Off time of PWM DC in µs calculated with selected frequency and selected DC
unsigned long LastDCUpdate=0;       // save the time when last DC was started

bool PWMPinState=LOW;
bool ChangeDC=true;

// Variables for Display control
int CursorLine = 0;
int DisplayFirstLine = 0;
char* TestModes[] = {"Basis Test","Drehzahlmessung","OpenLoop Control","CloseLoop Control","Dauerhub Test"};
int DisplayedMode =0;
int MenueItems;
int ActualCharacter= 0;
int ActualLine= 0;
int CursorPosition = 0;

void pinDidChange() {
  encoder.checkPins(digitalRead(encoderPinA), digitalRead(encoderPinB));
}
void prepare() {
  attachInterrupt(0, pinDidChange, CHANGE);
  attachInterrupt(1, pinDidChange, CHANGE);
}

// definition for debug console
#define DEBUG //"Schalter" zum aktivieren

#ifdef DEBUG
#define DEBUG_PRINT(x) Serial.print(x)
#define DEBUG_PRINTLN(x) Serial.println(x)
#else
#define DEBUG_PRINT(x)
#define DEBUG_PRINTLN(x)
#endif

void setup() {
  
  Serial.begin(9600);
  Serial.println("Test Box V1");

  // We can use internal pull-up with the encoder pins, assuming pin C is simply grounded.
  pinMode(encoderPinA, INPUT_PULLUP);
  pinMode(encoderPinB, INPUT_PULLUP);
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(hallsensorPin, INPUT);
  digitalWrite(buttonPin, HIGH );
  pinMode(solenoidPwmPin, OUTPUT);
  prepare();

  lcd.begin(MaxCharacters,MaxLines); //Display  wird gestartet und die Größe des Displays eingestellt
  lcd.backlight(); //Beleuchtung Display 1 einschalten
  MenueItems = sizeof(TestModes)/sizeof(TestModes[0]);
//  setup_T2();
}

static int  state =  S_StartScreen;

void loop() {
  // Check for Encoder rotation
  EC11Event e;
  if (encoder.read(&e)) {

    // OK, got an event waiting to be handled.
    
    if (e.type == EC11Event::StepCW) {
      TurnedCW();
      DEBUG_PRINTLN("Encoder turned clockwise");
    } 
  
    if (e.type == EC11Event::StepCCW) {
      TurnedCCW();
      DEBUG_PRINTLN("Encoder turned counter clockwise");
  }
  }
  // Check for Encoder push button action
   int b = checkButton();
   if (b == 1) clickEvent();
   if (b == 2) doubleClickEvent();
   if (b == 3) holdEvent();
   if (b == 4) longHoldEvent();

   // Main programme with different test modes
   switch (state)
   {
    
case S_StartScreen:
  lcd.setCursor(0,0);
  lcd.print(" Test Box");
  lcd.setCursor(0,1);
  lcd.print("zum Test starten");
  lcd.setCursor(0,2);
  lcd.print("Knopf kurz druecken");
  lcd.setCursor(0,3);
  lcd.print("Warte auf Start");
break;

case S_BasisTest:
// Measure Resistence of Coil
if (millis() - LastCoilUpdate >= UpdateRateCoilResistance) {
CoilResistance=MeasureResistence();
lcd.clear(); 
lcd.setCursor(0,0);
lcd.print("Basis Test");
lcd.setCursor(0,1);
lcd.print("Widerstand: ");
lcd.print(CoilResistance,1);
LastCoilUpdate=millis();
//DEBUG_PRINTLN("Resistance Measured");
//DEBUG_PRINTLN(LastCoilUpdate);
}
HallCounts =CountHallPulses();
lcd.setCursor(0,2);
lcd.print("HallPulse: ");
lcd.print(HallCounts);

break;

case S_Drehzahlmessung:
/* Case Description:
 *  Measure the rotational speed with the built in Hallsensor and display
 *  the value on the LCD
 */
PulseWidth=pulseIn(hallsensorPin,HIGH);
RotationalSpeed=PulseWidth*PulsesPerRev*60/1000/1000;
lcd.setCursor(0,0);
lcd.print("Drehzahlmessung");
lcd.setCursor(0,1);
lcd.print("RPM: ");
lcd.print(RotationalSpeed);
DEBUG_PRINTLN(PulseWidth);
break;

case S_OpenLoopControl:
/* Case Description:
 * One can control open loop the solenoid voltage
 * By dafault one can manipulate the Duty Cycle by turning the knob
 * To change the Frequency doubleclick the knob and the change the frequency
 * to change again the Duty Cycle doubleklick again the knob
 */

SoftPWM();
break;

case S_CloseLoopControl:
/* Case Description:
 * not defined yet
 */
break;

case S_DauerhubTest:
/* Case Description
 *  not definded yed
 *  
 */
break;

case S_Modusauswahl:
/* Case Description
 *  switch between the different cases
 *  turn rotary encoder cw or ccw to change case
 *  use a short click to select the case
 *  
 */
break;
   }
}
  1. Teil
void TurnedCW() {
  // use the knob in S_Modusauswahl
  if (state==S_Modusauswahl){
    
   if (DisplayedMode < MenueItems-1) {
    DisplayedMode++;
  }
  else{
    DisplayedMode=0;
      }
  print_MenuAuswahl();
                            }
  // use the knob in S_OpenLoopControl
  if (state==S_OpenLoopControl){
  if (ChangeDC==true){
     if (SolenoidDutyCycle<MaxDutyCycle){
       SolenoidDutyCycle++;
     }else{
     SolenoidDutyCycle=MinDutyCycle;
     lcd.clear();
     }
  }else{
   if (SolenoidFrequency<MaxFrequency){
       SolenoidFrequency++;
     }else{
     SolenoidFrequency=MinFrequency;
     lcd.clear();
     } 
  }
}
  }
  
void TurnedCCW(){
  // use the knob in S_Modusauswahl
  if (state==S_Modusauswahl){
   if (DisplayedMode >0) {
    DisplayedMode--;
   }
  else{
    DisplayedMode=MenueItems-1;
    }
    print_MenuAuswahl();
  }
  // use the knob in S_Modusauswahl
  if (state==S_OpenLoopControl){
  if (ChangeDC==true){
     if (SolenoidDutyCycle>MinDutyCycle){
       SolenoidDutyCycle--;
     }else{
     SolenoidDutyCycle=MaxDutyCycle;
     }
   }else{
    if (SolenoidFrequency>MinFrequency){
        SolenoidFrequency--;
        }else{
        SolenoidFrequency=MaxFrequency;
        } 
  }
}
  }
  
void clickEvent() {
DEBUG_PRINT("Click Event");
  if (state == S_StartScreen){
    state=S_BasisTest;
    lcd.clear(); 
  }
  if (state == S_Modusauswahl){
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print(TestModes[DisplayedMode]);
    lcd.setCursor(0,1);
    lcd.print("ausgewaehlt");
    delay(1000);
    lcd.clear();
    state=DisplayedMode+1;
    DEBUG_PRINT(DisplayedMode);

  }
  
}

void doubleClickEvent() {
DEBUG_PRINT("Double Click Event");
  // use the Double click Event in S_Modusauswahl
  if (state==S_OpenLoopControl){
    if(ChangeDC==true){
      ChangeDC=false;
    }else{
      ChangeDC=true;
    }
  }
}
void holdEvent() {
  DEBUG_PRINT(" Hold Event");
  state=S_Modusauswahl;
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("Bitte Modus waehlen");
  lcd.setCursor(0,1);
  lcd.print(TestModes[DisplayedMode]);
}

void longHoldEvent() {
   DEBUG_PRINT("Long Hold Event");
     
}

float  MeasureResistence(){
  MeasuredValue=0;
  for(int i=0;i<100;i++){
    MeasuredValue+=analogRead(ResistorMeasurePin);
  }
  MeasuredValue=trunc(MeasuredValue/100);
  //Spannung berechnen
  VoltageR2=(Vref/1023.0)*MeasuredValue;
  //Berechnung: (R2 = R1 * (U2/U1))
  R2=R1*(VoltageR2/(Vref-VoltageR2));
  return R2;
}

int CountHallPulses(){
  ActualHallPulse=digitalRead(hallsensorPin);
  if (ActualHallPulse!=LastHallPulse && ActualHallPulse==LOW){
    if (HallPulseCounter<PulsesPerRev){
      HallPulseCounter++;
    }
    else{
    HallPulseCounter=0;
    }
    LastHallPulse=ActualHallPulse;
    }
    else{
    LastHallPulse=ActualHallPulse;
    }   
  return HallPulseCounter;  
    }
void print_MenuAuswahl() {
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Bitte Modus waehlen");
lcd.setCursor(0,1);
lcd.print(TestModes[DisplayedMode]); 

}
void SoftPWM(){

Wie ihr seht, versuche ich verschiedene Testfälle mit einer switch case Struktur zu realisieren. Je nach ausgewähltem Case bekommt der Rotary Encoder unterschiedliche Funktionen zu gewiesen.

  • Ist dies so ein gängiges Vorgehen oder wie wird sowas normalerweise gelöst?
  • Sollte ich, um Timing Probleme zu verhindern, dass LCD Display in einem fest eingestellten Intervall updaten?