Arduino Frequentie generator en pulsbreedte

Op 7 december 2019 21:58:36 schreef deKees:
Vorig jaar heb ik iets soortgelijks gebouwd om spoelen te testen.
Was een AtTiny24 om frequentie te genereren, en 2 rotary encoders om in te stellen. Een voor frequentie, de ander voor DutyCycle pulsbreedte.

Geen display of zo, omdat ik toch altijd de skoop bij de hand heb om de spoelen te meten.

[bijlage]

Dat wordt wel een flink stuk code.
Werkt op een Uno en op een Nano.
2 rotary encoders op PORTD (Arduino : D2, D3, D4 en D5 D6 D7).
Frequentie komt uit PB2 (D10)

Instellen gaat soort van logarithmisch om een groot bereik te krijgen.
En dan kun je puls breedte switchen tussen grof/fijn met de rotary button.

Dit kun je in een .ino bestand in de arduino Ide kopieren en dan naar een Uno/Nano uploaden als het goed is.

Edit 13:07
- Aangepast om frequentie en pulsbreedte uit te rekenen.

code:



// ================================================================================================

/*
 * File         : RotaryEncoder.cpp
 * Created      : August 3, 2016
 * By           : CJ van der Hoeven
 *
 * Purpose      : Implementation of an interface to a rotary encoder
 *
 * Featuring    : Rotary encoder with push-button
 *                Detection of rotation and speed
 *                Detection of button clicks
 *                Supports multiple rotary encoders in a single system.
 */

#include <math.h>
#include <avr/interrupt.h>

static const uint16_t Delay_1 =   300;   // Duration for 'slow' rotations.
static const uint16_t Delay_2 =   200;   // Duration for 'fast' rotations

static const uint16_t Delay_3 =  5000;  // Minimum duration of 'Long press".
static const uint16_t Delay_4 = 10000;  // Max value of duration counter.

// We use a class definition so we can declare multiple rotary encoders.
// - Use one object per encoder.
class RotaryEncoder
{
public:
   RotaryEncoder()
   {  m_Armed     = false;
      m_PinStatus = 0x00;
      m_Debounce  = 100;
   }

   // Return true if the rotary button was pressed.
   bool Button();      // Short Press ( < 500 mS)
   bool LongButton();  // Long Press  ( > 500 mS)

   // Return NrClick rotation since last check.
   // Positive / Negative depending on direction.
   int16_t GetRotation();
   bool    GetRotation(int16_t &Delta);

   // Update 'Value' using rotary movement, but stay within range Min-Max
   // - No display updates. Just the value is updated.
   // - Return true when value was changed.
   bool UpdateValue (uint8_t  &Value, uint8_t  MinValue, uint8_t  MaxValue);
   bool UpdateInt   (uint16_t &Value, uint16_t MinValue, uint16_t MaxValue);
   bool UpdateDouble(double &Value, double MinValue, double MaxValue, double Delta);

   // Clear the status of the rotary encoder. Discard Buttons and movement.
   void Clear();

   // Count the rotation clicks.
   // - Positive for ClockWise rotation,
   // - Negative for CounterClockWise
   int16_t           m_Rotation;     //< Number of rotation clicks

   // Register current pin status from timer interrupt.
   // Bits are active low.
   // -  A = 0b00000001
   // -  B = 0b00000010
   // -  S = 0b00000100
   void Update(uint8_t PinStatus);

   // m_PinStatus retains the status of the pins for the rotary encoder
   // Are used to detect which pins have changed
   // - Can be used by application
   // -  A = 0b00000001
   // -  B = 0b00000010
   // -  S = 0b00000100
   uint8_t           m_PinStatus;

private:
   uint8_t           m_Armed;      //< State for Rotation
   uint16_t          m_Timer;      //< Timer for Rotation speed

   int8_t            m_Button;     //< Indicates that button was pressed. Code depends on Duration.
   int8_t            m_Debounce;   //< Debounce counter.
   uint16_t          m_Duration;   //< Count duration of button press.
};


// Handle Rotary status.
// - Pin status in lower 3 bits : SAB, Active Low.
// - Please note : Hardware is connected as SBA, but actual encoder produces SAB
//   - Perhaps these chinese encoders have pins A and B swapped!
void RotaryEncoder::Update(uint8_t PinStatus)
{  // Rotary movement:
   // - ClockWise    :  11 - 01 - 00 - 10 - 11 (with actual chinese encoders).
   // - CounterClock :  11 - 10 - 00 - 01 - 11 (with actual chinese encoders)
   uint8_t Rotary = PinStatus & 0b00000011;

   // Check Rotary AB
   if ((m_Armed == 0) && (Rotary == 0b0000000))
   {  // Both pins Low (contacts closed) indicates middle-of-click.
      m_Armed = 1;
   }

   if (m_Armed && (Rotary == 0b00000011))
   {  // Check previous value to determine direction.
      // - Both pins high indicates idle position.

      // Derive increment from duration.
      // - Faster movements have more effect (bigger steps).
      uint16_t Increment = 1;
      if(m_Timer > Delay_1)       // Slower than 10 clicks per second ?
      {  Increment = 1;           //
      }
      else if(m_Timer > Delay_2)  // Slower than 20 clicks per second ?
      {  Increment = 5;           // Big steps for fast movements
      }
      else
      {  Increment = 10;          // SuperBig steps for ultrafast movements
      }

      if((m_PinStatus & 0b00000011) == 0b00000010)
      {  // Moving forwards
         m_Rotation += Increment;
      }
      if((m_PinStatus & 0b00000011) == 0b00000001)
      {  // Moving backwards
         m_Rotation -= Increment;
      }
      m_Armed  = false;
      m_Timer  = 0;
   }
   m_PinStatus = Rotary; // Register new Pin Status

   // Also Check button
   // -- Button is bouncing quit a bit
   // -- Debouncing required.
   if(PinStatus & 0b00000100)  //< Check Switch
   {  // Pin High : Button switch open
      // Button-release stable: Allow new button-press.
      if(m_Debounce < 0)       // Counter started at -100
      {  m_Debounce += 1;      // Count interrupts with switch open
      }
      if(m_Debounce == 0)      // Counter started at -100
      {  if(m_Duration < Delay_3) // Less than 500 ms :
         {  m_Button = 1;      // Short Press
         }
         m_Debounce = 100;     // Count interrupts with switch open.
      }
      if(m_Debounce > 0)
      {  m_Debounce = 100;     // Restart counter when Switch is open.
      }
   }
   else
   {  // Pin Low : Button switch closed
      // Button active (=Low)
      if(m_Debounce > 0)       // Countdown started at 100
      {  m_Debounce -= 1;
      }
      if(m_Debounce == 0)      // Register Click after 100 interrupts (50 ms)
      {  m_Debounce = -100;
         m_Duration =  0;      // Start new Duration counter.
      }
      if(m_Debounce < 0)       // Keep it low as long as switch remains closed.
      {  m_Debounce = -100;

         if(m_Duration == Delay_3)// Notify user when the button is down long enough
         {  m_Button = 2;         // Long Press.
         }

         if(m_Duration < Delay_4) // Up to a Max.
         {  m_Duration += 1;      // Count duration of button Down
         }
      }
   }

   // Increment timer counter at every interrupt.
   // - So we get time between clicks (5000 increments per second)
   if (m_Timer < 1000)
   {  m_Timer += 1;
   }
}


// Interface functions to check if a button is pressed.
// Return true if the rotary button was pressed
bool RotaryEncoder::Button()
{  if(m_Button == 1)
   {  // Clear button, so it is used only once.
      // Please note: The surrounding if() is required here because clearing too often may
      //   clear the Button before it is used.
      //   I.E. when ISR writes a new button between ButtonRead and ButtonClear.
      m_Button = 0;
      return true;
   }
   else
   {  return false;
   }
}

// Interface functions to check if a button is pressed.
// Return true if the rotary button was pressed
bool RotaryEncoder::LongButton()
{  if(m_Button == 2)
   {  // Clear button, so it is used only once.
      // Please note: The surrounding if() is required here because clearing too often may
      //   clear the Button before it is used.
      //   I.E. when ISR writes a new button between ButtonRead and ButtonClear.
      m_Button = 0;
      return true;
   }
   else
   {  return false;
   }
}

// User function to check if the rotary is turned.
// Return signed Count
//    < 0   : Nr clicks, turned backwards
//    = 0   : No movement
//    > 0   : Nr clicks, turned forwards
int16_t RotaryEncoder::GetRotation()
{  if(m_Rotation)
   {  // Yes, Action:
      int16_t Rotation = m_Rotation; //< Register current value
      m_Rotation = 0;                //< Clear
      return Rotation;               //< Return current value to application
   }
   else
   {  return 0;  //< No movement
   }
}

bool RotaryEncoder::GetRotation(int16_t &Delta)
{  Delta = GetRotation();
   return (Delta != 0);
}

// Clear the status of the rotary encoder. Discard Buttons and movement.
void RotaryEncoder::Clear()
{  m_Rotation = 0;
   m_Button   = 0;
}

// ================================================================================================

/*
 * Main.cpp
 *
 * Created    : 2018-21-10
 * Author     : Kees
 * Purpose    : Firmware for Inductor tester.
 *              - Using rotary encoders:
 *                MyRotary1 : Set Pwm frequency.
 *                MyRotary2 : Set Pwm PulseWidth.
 *
 *              - AtTiny24
 *
 * Schematics : Projects\Kicad\InductorTester
 */

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <avr/sleep.h>


// ================================================================================================

// Frequency to be decided.
// -- But we need high speed to create short pulses.
// -- Lets start with 1 MHz.
#define F_CPU 1000000

// ================================================================================================
// Hardware definition (Pin mapping).

// ==================================================================
// PWM Output pin
// -- PB2, D10 : (PWM through OC1B), Active Low (using TC426 Mosfet driver).
// -- Variable Frequency and Duty cycle to control led power.
static const uint8_t PIN_PWM_OUT = 0b00000100;  // PB2

// ================================================================================================

struct TIMER_DSCR
{
   uint16_t    m_Started;

   void Start()
   {  m_Started = millis();
   }

   // Check if 'Duration'
   bool Sync(uint16_t Duration)
   {  if( (((uint16_t)millis()) - m_Started) >= Duration )
      {  m_Started += Duration;
         return true;
      }
      else
      {  return false;
      }
   }
};

TIMER_DSCR Timer;

// ================================================================================================

// Instantiate a couple Rotary encoder handlers
RotaryEncoder MyRotary1;  // To set Pwm Frequency
RotaryEncoder MyRotary2;  // To set Pwm PulseWidth

// We have a single interrupt handler for our 2 rotary encoders.
// - To be called from a timer interrupt at regular intervals.
//    - Should be fast enough to detect all rotary pulses.
// - This handler will check the encoder's I/O pins and derive rotation movements.
//
// This is not included in the rotary library function because:
// - Here we can define as many encoders as needed in an application..
// - And we provide actual link between processor pins and encoder switch.
void RotaryInterruptHandler()
{
   // Setup I/O port for Rotary encoder SW2 SW1
   DDRD   &=  ~0b11111100;  // Set Inputs      SW2:SBA SW1:SBA
   PORTD  |=   0b11111100;  // Enable Pullups

   MCUCR  &=  ~(1 << PUD);  // Enable all pull-up functions.

   // First read rotary encoder.
   uint8_t PinStatus = PIND & 0b11111100;  // PD.7 .. PD.2

   // SW1 :
   MyRotary1.Update(PinStatus >> 2);      // Rotary1 uses PD.4 = S, PD.3 = B, PD.2 = A

   // SW2 :
   MyRotary2.Update(PinStatus >> 5);      // Rotary2 uses PD.7 = S, PD.6 = B, PD.5 = A
}


// ================================================================================================
// We use Timer0 as time base for frequency measurement.
// -- Should generate interrupts at every 100 uSec
//
// And we use a 16-bit timer 1 for the Pwm Engine.
// -- So we get a 16-bit frequency range.
//
// - Clock frequency = 1 MHz (Prescaler = 8)
// - Use OCR1A to set Frequency, Clear on Compare Match
//   -- WGM = 010 : CTC, Fast PWM
// - Use OCR1B to generate PWM wave form
//   COM0B = 11 : Set at Compare Match, Clear at BOTTOM.
//                -- This mode allows Zero pulse output, I.E. Mosfet off.
//                -- Active Pulse width : OCR1A - OCR1B

// Lowest possible frequency = 20 Hz, 50 ms
static const uint16_t MaxCount = 50000;

void StartPwmEngine()
{  // ====================================================================
   // Set TIMER1 to generate PWM signal.
   TCCR1A = (0b11 <<  WGM10)    // CTC (on OCR1A), Fast PWM.
          + (0b11 << COM1B0)    // TC427 (Active High) : PWM on OC1B (PB2) Set on Compare Match, Clear at Bottom.
//          + (0b10 << COM1B0)    // TC426 (Active Low)  : PWM on OC1B (PB2) Clear on Compare Match, Set at Bottom.
          + (0b00 << COM1A0);   // Disable PWM on OC1A

   TCCR1B = (0b010 <<  CS10)    // Clock divide by 8 --> 2 MHz
          + (0b11  << WGM12)    // CTC (on OCR1A), Fast PWM.
          + (0b0   << ICNC1)    // Not used
          + (0b0   << ICES1);   // Not used

   TCCR1C = (0b0   << FOC1B)
          + (0b0   << FOC1A);

   // We use a variable PWM frequency, as set by OCR1A
   // And a Pulse duration set by OCR1B
   OCR1A  = MaxCount;          // Set Pwm Frequency.
   OCR1B  = MaxCount;          // Set Pwm Pulse width off initially.

   TIMSK1 = (0 << TOIE1)       // Disable all interrupts on Timer 1
          + (0 << OCIE1A)
          + (0 << OCIE1B)
          + (0 << ICIE1);

   DDRB |= PIN_PWM_OUT;        // Enable output for PWM OC1B.
}

#define NR_ENTRIES(a) (sizeof(a)/sizeof(a[0]))

void SetPwmPower(uint16_t PwmPeriod, uint16_t PulseWidth)
{  // Prevent extended pulse output when changing frequency.
   // - Better to miss a pulse than to generate long pulse.
   if(PwmPeriod < OCR1A)
   {  // Setting Shorter period : Update frequency first.
      // - Update frequency first
      OCR1A = PwmPeriod;
      OCR1B = PwmPeriod - PulseWidth;
   }
   else
   {  // Setting longer period time:
      // - Update pulseWidth first
      OCR1B = PwmPeriod - PulseWidth;
      OCR1A = PwmPeriod;
   }
   DDRB |= PIN_PWM_OUT;        // Enable output for PWM OC1B.
}

// ================================================================================================

struct FREQUENCY_DSCR
{  uint16_t  m_Period;   // MicroSeconds between pulses, 0 .. 65535
};

// Higher Index gives higher frequency
static const FREQUENCY_DSCR FrequencyTable[] PROGMEM =
{  // Pwm Frequency
   {  ( F_CPU /    20 ) },  // 20 Hz (50000 uSec)
   {  ( F_CPU /    30 ) },
   {  ( F_CPU /    50 ) },
   {  ( F_CPU /   100 ) },
   {  ( F_CPU /   200 ) },
   {  ( F_CPU /   300 ) },
   {  ( F_CPU /   500 ) },
   {  ( F_CPU /  1000 ) },  //< Default
   {  ( F_CPU /  2000 ) },
   {  ( F_CPU /  3000 ) },
   {  ( F_CPU /  5000 ) },
   {  ( F_CPU / 10000 ) },
   {  ( F_CPU / 20000 ) },
   {  ( F_CPU / 30000 ) },
   {  ( F_CPU / 50000 ) },  // 50 kHz (20 uSec)
};


struct PWM_DSCR
{  uint16_t  m_PulseWidth;  // Actual Pulse width from table

   uint16_t  m_StepSize;    // Step-Size for fine-tuning
                            // - Typically 1% of m_PulseWidth (if possible)
};

// Higher Index gives larger pulses
static const PWM_DSCR PulseWidthTable[] PROGMEM =
{  // in uSecs (counter clocks)
   {     0,   1 },
   {     1,   1 },
   {     2,   1 },
   {     3,   1 },
   {     5,   1 },
   {    10,   1 },
   {    20,   1 },
   {    30,   1 },
   {    50,   1 },
   {   100,   1 },
   {   200,   2 },
   {   300,   3 },
   {   500,   5 },
   {  1000,  10 },
   {  2000,  20 },
   {  3000,  30 },
   {  5000,  50 },
   { 10000, 100 },
   { 20000, 200 },
   { 30000, 300 },
   { 50000, 500 },
};


// =================================================================================================================

void setup() 
{
   // put your setup code here, to run once:
   Serial.begin(115200);

   // Set Pin Direction register
   DDRB |= PIN_PWM_OUT;      // Enable output for PWM OC1B.
   StartPwmEngine();
   Timer.Start();
}

void loop() 
{
   if(Timer.Sync(1))
   {  RotaryInterruptHandler();
   }

   // Set initial values at Start-up
   static uint8_t FrequencyIndex  = 7;       // 1000 Hz
   static uint8_t PulseWidthIndex = 0;       // No pulse at startup.

   // Units of 1 uSec.
   // - Range 0 .. 50000 (Stay within 16 bits value).
   static uint16_t PwmPeriod     = pgm_read_word(&FrequencyTable[FrequencyIndex].m_Period);
   static uint16_t PwmPulseWidth = 0;        // Duration of the Pwm Pulse.

   static bool SetFinePulseWidth  = false;

   int16_t Delta; // To see direction and magnitude to Rotary encoder

   // ==========================================================================================
   // Update PWM frequency
   if(MyRotary1.GetRotation(Delta))
   {
      if(Delta < 0)
      {  if(FrequencyIndex > 0)
         {  FrequencyIndex -= 1;
         }
      }

      if(Delta > 0)
      {  if( (FrequencyIndex + 1) < NR_ENTRIES(FrequencyTable))
         {  FrequencyIndex += 1;
         }
      }

      // Update PWM timer for new frequency
      PwmPeriod = pgm_read_word(&FrequencyTable[FrequencyIndex].m_Period);
      SetPwmPower(PwmPeriod, PwmPulseWidth);

      float Frequency = 2000000 / PwmPeriod;
      Serial.print("f = "); Serial.println(Frequency);
   }

   // ==========================================================================================
   // Update PWM Pulse Width
   if(MyRotary2.Button())
   {  // Rotary-Switch toggles between Fine and course
      SetFinePulseWidth = (SetFinePulseWidth ? false : true);

      if(SetFinePulseWidth)
      {  Serial.println("w = fine");
      }
      else   
      {  Serial.println("w = coarse");
      }
   }

   if(MyRotary2.GetRotation(Delta))
   {  // Update Pwm PulseWidth

      if(SetFinePulseWidth)
      {
         // Get StepSize from Table. Use as Fine-tuning.
         uint16_t StepSize = pgm_read_word(&PulseWidthTable[PulseWidthIndex].m_StepSize);

         if(Delta < 0)  // Turning CounterClock-wise
         {  // Down to Pulse-width = 0
            if(PwmPulseWidth >= StepSize)
            {  PwmPulseWidth -= StepSize;
            }
            else
            {  PwmPulseWidth = 0;
            }
         }

         if(Delta > 0)  // Turning Clock-wise
         {  // Up to 100 % pulse-width
            if( (PwmPulseWidth + StepSize) <= PwmPeriod)
            {  PwmPulseWidth += StepSize;
            }
            else
            {  PwmPulseWidth  = PwmPeriod;
            }
         }
      }
      else
      {  // Coarse mode. Set Index and get new value from table
         if(Delta < 0)
         {  if(PulseWidthIndex > 0)
            {  PulseWidthIndex -= 1;
            }
         }

         if(Delta > 0)
         {  if( (PulseWidthIndex + 1) < NR_ENTRIES(PulseWidthTable))
            {  PulseWidthIndex += 1;
            }
         }
         PwmPulseWidth = pgm_read_word(&PulseWidthTable[PulseWidthIndex].m_PulseWidth);
      }

      // Update PWM timer for new frequency
      SetPwmPower(PwmPeriod, PwmPulseWidth);

      float PulseWidth = 0.5 * PwmPulseWidth;
      Serial.print("pw = "); Serial.println(PulseWidth);
   }
}


Net nog een kleine wijziging doorgevoerd want het signaal kwam niet naar buiten.

Nu instelbaar tussen 40 Hz en 50 kHz.
Met puls breedte in stappen van 0.5 uS.

blackdog

Golden Member

Hi deKees,

Natuurlijk is mijn dank weer GROOT voor je bijdrage. _/-\o_

Ik heb wel even "snel" naar je code gekeken en zal het later vandaag in een Arduino proppen.
Moet eerst echter nog een klus voor een klant afmaken en die computers afleveren, jaja geen zondags rust...

In je laatste opmerking heb je het over 40Hz tot 50KHz, maar de lijst in de code geeft 20Hz aan.
Kan ik die code uitbreiden naar zeg 1Hz en b.v. voor mijn toepassing de hogere frequenties weglaten of er bij zetten?

Mijn bedoeleing is een Nokia LCD schermpje te gaan gebruiken i.v.m. het lage verbruik.
Welke variabel moet ik dan gebruiken om de frequentie en puls breedte op het scherm weer te geven.

Is dat voor Frequentie dit: FrequencyIndex
En voor pulsbreedte dit: PulseWidthIndex

Groet,
Bram

You have your way. I have my way. As for the right way, the correct way, and the only way, it does not exist.

De code was oorspronkelijk gemaakt voor een Tiny24 op een RC oscillator van 1 MHz. De timer1 in de nano loopt 2 keer zo hard (16 MHz xtal, prescaler = 8), dus dat geeft dubbele frequenties.

Bereik uitbreiden is niet zo heel gemakkelijk want dan ga je over de grenzen van de 16-bit. Dus dan moet je de prescaler gaan omschakelen.

Met een grotere prescaler kan de frequentie wel omlaag, maar de mogelijkheden zijn beperkt en dan wordt ook de minimum pulsbreedte groter.

En de frequentie is niet direct beschikbaar in een variabele. Wel de periode duur. Dus dan kun je de frequentie uitrekenen met

code:


   float Frequency = 2000000 / PwmPeriod;

Ik zal het er wel bij zetten in de code.

Die beide index variabelen zijn niet direct bruikbaar, want dat is alleen een index in de tabellen.

Let er wel op dat de loop() op hoge snelheid zijn rondjes moet blijven draaien. Want hier is er geen interrupt gebruikt of zo. De rotary encoders worden gewoon in de loop gepolled. Dus alleen het display updaten als er echt iets verandert.

blackdog

Golden Member

Hi deKees,

Kan je dit voor mij uitleggen:

2 rotary encoders op PORTD (Arduino : D2, D3, D4 en D5 D6 D7)

Bij mijn encoders zijn dit nu de kleuren en functies.
Schakelaar: Grijs + Lila

Encoder: Common = Blauw
Encoder: A-contact = Geel
Encoder: B-contact = Groen

Zijn de schakelaar en de A+B contacten gerefereerd aan de voedings "0"?

http://www.bramcam.nl/Diversen/deKees-Pulsgenerator-Testomgeving-01.png

Groet,
Bram

You have your way. I have my way. As for the right way, the correct way, and the only way, it does not exist.

Arduino pin nummers D2 D3 D4 komen overeen met AtMega328 Pin nummers PORTD.2, PORTD.3 en PORTD.4. Die zijn voor de eerste rotary A, B, en S.
Moeten schakelen naar massa.

Arduino pin nrs D5 D6 D7 zijn voor de andere rotary (A, B en S).
Maar misschien (waarschijnlijk) moet je A en B verwisselen.

Ik heb het hier werken op een UNO, maar zou op een Nano evengoed moeten werken.

Succes.

blackdog

Golden Member

Hi deKees,

Er is geen Pull-Up spanning aanwezig voor de encoders en schakelaars.
Staat dit intern niet aan, of moet ik zelf Pull-Up weerstanden toevoegen?

De standaard frequentie komt trouwnens wel op de uitgang.

Groet,
Bram

You have your way. I have my way. As for the right way, the correct way, and the only way, it does not exist.

Interne pullups zouden aktief moeten zijn.

code:


   // Setup I/O port for Rotary encoder SW2 SW1
   DDRD   &=  ~0b11111100;  // Set Inputs      SW2:SBA SW1:SBA
   PORTD  |=   0b11111100;  // Enable Pullups

Al kan het nooit kwaad om extern pullups toe te voegen natuurlijk.

Als het goed is staat de pulsbreedte op nul na inschakelen. Dus dan is er geen signaal totdat je aan de rotaries gaat draaien. Dus ik snap niet goed wat je bedoelt met standaard frequentie.

De Arduino monitor werkt nu als display over de USB-Serial port, 115200 baud.

blackdog

Golden Member

Hi deKees,

Er gaat iets mis met de conters, ze tellen maar een kant op, je kan dus niet de andere kant op...
http://www.bramcam.nl/Diversen/deKees-Pulsgenerator-Up-Down-Counter-Error-01.png

Tot dat ik dacht... veel van die aansluitplaatjes van de bekende groene rotary encoders kunnen natuurlijk ook niet overeen komen met de groene die ik heb... BINGO!
Groen op mijn eerdere plaatje is dus de common en dus niet Blauw.
Nu tellen beide counter mooi zoals de bedoeling is, een enkele keer heb ik wat dender en er staan al 10nF condensatoren over de encoders.
Misschien deze nog een stapje groter maken.

De drukknop van de frequentie regelaar heeft geen functie, klopt dat?

Groet,
Bram

You have your way. I have my way. As for the right way, the correct way, and the only way, it does not exist.

Ja, klopt. Voor mijn spoelentester had ik dat niet nodig. Daar ging het vooral om pulsjes.

blackdog

Golden Member

Hi deKees,

Ik heb een vraag over het eerste stukje code, dat is de code waarbij je direct de frequentie en de duty cycle kan ingeven.
Deze ga ik als eerste toepassen in een testkastje die ene dummy load kan aansturen en een kortsluittester.

Voor de weergave van de instelling van de frequentie en de duty cycle wil ik een Nokia display gebruiken en bijna alle drivers hiervoor gebruiken Pin-10.
Kan jouw software ook een andere pin aansturen voor de uitgang, kan dit worden omgezet naar b.v. pin-13?

Groet,
Bram

You have your way. I have my way. As for the right way, the correct way, and the only way, it does not exist.

Nee helaas. Op dat gebied zijn deze Atmels wel wat beperkt. Er zit maar een enkele 16-bit timer in, met 2 compare units. Daarbij is CompareA de enige die de timer kan resetten, en die heb je nodig om de frequentie in te stellen. Dan blijft alleen CompareB over om het signaal te genereren, en die is hard gekoppeld aan OC1B (PB2).

Je ziet wel steeds meer processors waarbij je zelf een pin kunt koppelen, maar bij deze gaat dat niet.

Waar heb je PIN 10 nu voor nodig?

Ik wil wel een voorzetje maken om de beide stukken te combineren, dan krijg je de gewenste frequentie bereik gekoppeld aan de 2 rotaries.

De rotaries kunnen wel zonder probleem naar andere pinnen.

blackdog

Golden Member

Hi deKees,

Dit is een stukje uit de LCD driver, misschien kan ik de DC functie naar pin-13 zetten.
Maar zijn de andere pinnummers nog wel beschikbaar van die poort?

// It is assumed that the LCD module is connected to
// the following pins using a levelshifter to get the
// correct voltage to the module.
// SCK - Pin 8
// MOSI - Pin 9
// DC - Pin 10
// RST - Pin 11
// CS - Pin 12

Groet,
Bram

You have your way. I have my way. As for the right way, the correct way, and the only way, it does not exist.

Zo te zien worden die pinnen softwarematig aangestuurd. Dus dan zou het geen probleem moeten zijn om ze te verplaatsen.

En ja, de andere pinnen van PORTB zijn nog vrij.

Je kunt zo even proberen om het lcd aan te sturen op
// SCK - Pin 8
// MOSI - Pin 9
--
// DC - Pin 13
// RST - Pin 11
// CS - Pin 12

blackdog

Golden Member

Hi deKees,

Ga ik morgen of woensdag even proberen.
Net wat testjes gedaan met een Arduino Pro mini, zelfde processor maar wat lager verbruik.
Dit is een 8MHZ 3.3V model.

Ik zal later in dit topic een blokschema laten zien hoe ik je software toepas.
Ik wil een stuk of 6 drukkopjes gebruiken die de frequentie en de duty cycle bepalen voor twee functies van dit apparaatje.

Het display laat de uitgangs frequentie en de duty cycle zien, of de signalen enable zijn en misschien de batterij spanning.

Groet,
Bram

You have your way. I have my way. As for the right way, the correct way, and the only way, it does not exist.

Dat is het leuke van die controllertjes. Dan kun je het precies zo maken als je wilt.

Ik ben gisteravond ook nog wat aan het spelen geweest. Voortbordurend op de 2 rotaries.

Ik heb nu:
-- Frequentie bereik 1 Hz tot 100 kHz.
-- Logaritmische schaal, 35 kliks per decade.
-- DutyCycle 0 tot 100 %
-- Fijnregeling op frequentie en op duty-cycle.
-- Grotere sprongen bij snel draaien.
Was wel even een uitdaging. Leuk.

Hierbij de code:

code:



// ================================================================================================

/*
 * File         : RotaryEncoder.cpp
 * Created      : August 3, 2016
 * By           : CJ van der Hoeven
 *
 * Purpose      : Implementation of an interface to a rotary encoder
 *
 * Featuring    : Rotary encoder with push-button
 *                Detection of rotation and speed
 *                Detection of button clicks
 *                Supports multiple rotary encoders in a single system.
 */

#include <math.h>
#include <avr/interrupt.h>

static const uint16_t Delay_1 =   60;   // Duration for 'slow' rotations.
static const uint16_t Delay_2 =   40;   // Duration for 'fast' rotations

static const uint16_t Delay_3 =  1000;  // Minimum duration of 'Long press".
static const uint16_t Delay_4 = 10000;  // Max value of duration counter.

// We use a class definition so we can declare multiple rotary encoders.
// - Use one object per encoder.
class RotaryEncoder
{
public:
   RotaryEncoder()
   {  m_Armed     = false;
      m_PinStatus = 0x00;
      m_Debounce  = 100;
   }

   // Return true if the rotary button was pressed.
   bool Button();      // Short Press ( < 500 mS)
   bool LongButton();  // Long Press  ( > 500 mS)

   // Return NrClick rotation since last check.
   // Positive / Negative depending on direction.
   int16_t GetRotation();
   bool    GetRotation(int16_t &Delta);

   // Update 'Value' using rotary movement, but stay within range Min-Max
   // - No display updates. Just the value is updated.
   // - Return true when value was changed.
   bool UpdateValue (uint8_t  &Value, uint8_t  MinValue, uint8_t  MaxValue);
   bool UpdateInt   (uint16_t &Value, uint16_t MinValue, uint16_t MaxValue);
   bool UpdateDouble(double &Value, double MinValue, double MaxValue, double Delta);

   // Clear the status of the rotary encoder. Discard Buttons and movement.
   void Clear();

   // Count the rotation clicks.
   // - Positive for ClockWise rotation,
   // - Negative for CounterClockWise
   int16_t           m_Rotation;     //< Number of rotation clicks

   // Register current pin status from timer interrupt.
   // Bits are active low.
   // -  A = 0b00000001
   // -  B = 0b00000010
   // -  S = 0b00000100
   void Update(uint8_t PinStatus);

   // m_PinStatus retains the status of the pins for the rotary encoder
   // Are used to detect which pins have changed
   // - Can be used by application
   // -  A = 0b00000001
   // -  B = 0b00000010
   // -  S = 0b00000100
   uint8_t           m_PinStatus;

private:
   uint8_t           m_Armed;      //< State for Rotation
   uint16_t          m_Timer;      //< Timer for Rotation speed

   int8_t            m_Button;     //< Indicates that button was pressed. Code depends on Duration.
   int8_t            m_Debounce;   //< Debounce counter.
   uint16_t          m_Duration;   //< Count duration of button press.
};


// Handle Rotary status.
// - Pin status in lower 3 bits : SAB, Active Low.
// - Please note : Hardware is connected as SBA, but actual encoder produces SAB
//   - Perhaps these chinese encoders have pins A and B swapped!
void RotaryEncoder::Update(uint8_t PinStatus)
{  // Rotary movement:
   // - ClockWise    :  11 - 01 - 00 - 10 - 11 (with actual chinese encoders).
   // - CounterClock :  11 - 10 - 00 - 01 - 11 (with actual chinese encoders)
   uint8_t Rotary = PinStatus & 0b00000011;

   // Check Rotary AB
   if ((m_Armed == 0) && (Rotary == 0b0000000))
   {  // Both pins Low (contacts closed) indicates middle-of-click.
      m_Armed = 1;
   }

   if (m_Armed && (Rotary == 0b00000011))
   {  // Check previous value to determine direction.
      // - Both pins high indicates idle position.

      // Derive increment from duration.
      // - Faster movements have more effect (bigger steps).
      uint16_t Increment = 1;
      if(m_Timer > Delay_1)       // Slower than 10 clicks per second ?
      {  Increment = 1;           //
      }
      else if(m_Timer > Delay_2)  // Slower than 20 clicks per second ?
      {  Increment = 5;           // Big steps for fast movements
      }
      else
      {  Increment = 10;          // SuperBig steps for ultrafast movements
      }

      if((m_PinStatus & 0b00000011) == 0b00000010)
      {  // Moving forwards
         m_Rotation += Increment;
      }
      if((m_PinStatus & 0b00000011) == 0b00000001)
      {  // Moving backwards
         m_Rotation -= Increment;
      }
      m_Armed  = false;
      m_Timer  = 0;
   }
   m_PinStatus = Rotary; // Register new Pin Status

   // Also Check button
   // -- Button is bouncing quit a bit
   // -- Debouncing required.
   if(PinStatus & 0b00000100)  //< Check Switch
   {  // Pin High : Button switch open
      // Button-release stable: Allow new button-press.
      if(m_Debounce < 0)       // Counter started at -100
      {  m_Debounce += 1;      // Count interrupts with switch open
      }
      if(m_Debounce == 0)      // Counter started at -100
      {  if(m_Duration < Delay_3) // Less than 500 ms :
         {  m_Button = 1;      // Short Press
         }
         m_Debounce = 100;     // Count interrupts with switch open.
      }
      if(m_Debounce > 0)
      {  m_Debounce = 100;     // Restart counter when Switch is open.
      }
   }
   else
   {  // Pin Low : Button switch closed
      // Button active (=Low)
      if(m_Debounce > 0)       // Countdown started at 100
      {  m_Debounce -= 1;
      }
      if(m_Debounce == 0)      // Register Click after 100 interrupts (50 ms)
      {  m_Debounce = -100;
         m_Duration =  0;      // Start new Duration counter.
      }
      if(m_Debounce < 0)       // Keep it low as long as switch remains closed.
      {  m_Debounce = -100;

         if(m_Duration == Delay_3)// Notify user when the button is down long enough
         {  m_Button = 2;         // Long Press.
         }

         if(m_Duration < Delay_4) // Up to a Max.
         {  m_Duration += 1;      // Count duration of button Down
         }
      }
   }

   // Increment timer counter at every interrupt.
   // - So we get time between clicks (5000 increments per second)
   if (m_Timer < 1000)
   {  m_Timer += 1;
   }
}


// Interface functions to check if a button is pressed.
// Return true if the rotary button was pressed
bool RotaryEncoder::Button()
{  if(m_Button == 1)
   {  // Clear button, so it is used only once.
      // Please note: The surrounding if() is required here because clearing too often may
      //   clear the Button before it is used.
      //   I.E. when ISR writes a new button between ButtonRead and ButtonClear.
      m_Button = 0;
      return true;
   }
   else
   {  return false;
   }
}

// Interface functions to check if a button is pressed.
// Return true if the rotary button was pressed
bool RotaryEncoder::LongButton()
{  if(m_Button == 2)
   {  // Clear button, so it is used only once.
      // Please note: The surrounding if() is required here because clearing too often may
      //   clear the Button before it is used.
      //   I.E. when ISR writes a new button between ButtonRead and ButtonClear.
      m_Button = 0;
      return true;
   }
   else
   {  return false;
   }
}

// User function to check if the rotary is turned.
// Return signed Count
//    < 0   : Nr clicks, turned backwards
//    = 0   : No movement
//    > 0   : Nr clicks, turned forwards
int16_t RotaryEncoder::GetRotation()
{  if(m_Rotation)
   {  // Yes, Action:
      int16_t Rotation = m_Rotation; //< Register current value
      m_Rotation = 0;                //< Clear
      return Rotation;               //< Return current value to application
   }
   else
   {  return 0;  //< No movement
   }
}

bool RotaryEncoder::GetRotation(int16_t &Delta)
{  Delta = GetRotation();
   return (Delta != 0);
}

// Clear the status of the rotary encoder. Discard Buttons and movement.
void RotaryEncoder::Clear()
{  m_Rotation = 0;
   m_Button   = 0;
}

// ================================================================================================

/*
 * Main.cpp
 *
 * Created    : 2018-21-10
 * Author     : Kees
 * Purpose    : Firmware for Inductor tester.
 *              - Using rotary encoders:
 *                MyRotary1 : Set Pwm frequency.
 *                MyRotary2 : Set Pwm PulseWidth.
 *
 *              - AtTiny24
 *
 * Schematics : Projects\Kicad\InductorTester
 */

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <avr/sleep.h>

// ==================================================================
// PWM Output pin
// -- PB2, D10 : (PWM through OC1B), Active Low (using TC426 Mosfet driver).
// -- Variable Frequency and Duty cycle to control led power.
static const uint8_t PIN_PWM_OUT = 0b00000100;  // PB2

// ================================================================================================

struct TIMER_DSCR
{
   uint16_t    m_Started;

   void Start()
   {  m_Started = millis();
   }

   // Check if 'Duration'
   bool Sync(uint16_t Duration)
   {  if( (((uint16_t)millis()) - m_Started) >= Duration )
      {  m_Started += Duration;
         return true;
      }
      else
      {  return false;
      }
   }
};

TIMER_DSCR Timer;

// ================================================================================================

// Instantiate a couple Rotary encoder handlers
RotaryEncoder MyRotary1;  // To set Pwm Frequency
RotaryEncoder MyRotary2;  // To set Pwm PulseWidth

// We have a single interrupt handler for our 2 rotary encoders.
// - To be called from a timer interrupt at regular intervals.
//    - Should be fast enough to detect all rotary pulses.
// - This handler will check the encoder's I/O pins and derive rotation movements.
//
// This is not included in the rotary library function because:
// - Here we can define as many encoders as needed in an application..
// - And we provide actual link between processor pins and encoder switch.
void RotaryInterruptHandler()
{
   // Setup I/O port for Rotary encoder SW2 SW1
   DDRD   &=  ~0b11111100;  // Set Inputs      SW2:SBA SW1:SBA
   PORTD  |=   0b11111100;  // Enable Pullups

   MCUCR  &=  ~(1 << PUD);  // Enable all pull-up functions.

   // First read rotary encoder.
   uint8_t PinStatus = PIND & 0b11111100;  // PD.7 .. PD.2

   // SW1 :
   MyRotary1.Update(PinStatus >> 2);      // Rotary1 uses PD.4 = S, PD.3 = B, PD.2 = A

   // SW2 :
   MyRotary2.Update(PinStatus >> 5);      // Rotary2 uses PD.7 = S, PD.6 = B, PD.5 = A
}

// ================================================================================================
// We use a 16-bit timer 1 for the Pwm Engine.
// -- So we get a 16-bit frequency range.
//
// - Use OCR1A to set Frequency, Clear on Compare Match
//   -- WGM = 0100 : CTC, Fast PWM
// - Use OCR1B to generate PWM wave form
//   COM0B = 11 : Set at Compare Match, Clear at BOTTOM.
//                -- This mode allows Zero pulse output, I.E. Mosfet off.
//                -- Active Pulse width : OCR1A - OCR1B
void SetFrequency(float Frequency, float DutyCycle)
{
   uint32_t Count = 16000000ul / Frequency;
   uint16_t Prescaler;

   // Timer 1 has 16 bits so we need a count below 65536
   if( Count < 65536ul)
   {  // Ok, Prescaler can be 1
      Prescaler = ( 0b001 << CS10);
   }
   else if (Count < (8 * 65536ul))
   {  // We need a prescaler of 8
      Count /= 8;
      Prescaler = ( 0b010 << CS10);
   }
   else if (Count < (64 * 65536ul))
   {  // We need a prescaler of 64
      Count /= 64;
      Prescaler = ( 0b011 << CS10);
   }
   else
   {  // We need a prescaler of 256
      Count /= 256;
      Prescaler = ( 0b100 << CS10);
   }

   uint16_t Width = (Count * DutyCycle) / 100;

   TCCR1A = ( 0b00 << COM1A0 )  // Compare A is not used
          + ( 0b11 << COM1B0 )  // Set at compare match, Clear at Bottom
          + ( 0b11 << WGM10  ); // Clear timer on Compare A match

   TCCR1B = ( 0b0  << ICNC1  )
          + ( 0b0  << ICES1  )
          + ( 0b11 << WGM12  )
          + ( Prescaler      ); 

   TIMSK1 = (0 << TOIE1)       // Disable all interrupts on Timer 1
          + (0 << OCIE1A)
          + (0 << OCIE1B)
          + (0 << ICIE1);
          
   if(Count < OCR1A)
   {  // Setting Shorter period : Update frequency first.
      // - Update frequency first
      OCR1A = Count;
      OCR1B = Count - Width;
   }
   else
   {  // Setting longer period time:
      // - Update pulseWidth first
      OCR1B = Count - Width;
      OCR1A = Count;
   }

   DDRB |= PIN_PWM_OUT; // Enable driver for OC1B (PB2)

   Serial.print("f = "); 
   if(Frequency < 100.)
   {  Serial.print(Frequency, 3); Serial.print(" Hz, ");
   }  
   else if(Frequency < 1000.)
   {  Serial.print(Frequency, 2); Serial.print(" Hz, ");
   }  
   else if(Frequency < 10000.)
   {  Serial.print(Frequency, 1); Serial.print(" Hz, ");
   }  
   else
   {  Serial.print(Frequency / 1000, 3); Serial.print(" kHz, ");
   }  
   Serial.print(DutyCycle, 1); Serial.print(" %");
   Serial.println();
}

// =================================================================================================================

// To convert a linear scale to a logarithmic scale.
// To get 35 clicks per decade :
//  - 1 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0 2.2 2.4 2.6 2.8 3.0 3.2 3.4 3.6 3.8 4.0 4.2 4.4 4.6 4.8 5.0 5.5 6.0 6.5 7.0 7.5 8.0 8.5 9.0 9.5 10.0
// In other words:
//  - 0.1 from 1.0 to  2.0
//  - 0.2 from 2.0 to  5.0
//  - 0.5 from 5.0 to 10.0  


struct LookupTable
{  float Value;
   float Delta;
};

static LookupTable MyTable[]=
{  {      1,    0.1 },
   {      2,    0.2 },
   {      5,    0.5 },
   {     10,    1.  },
   {     20,    2.  },
   {     50,    5.  },
   {    100,   10.  },
   {    200,   20.  },
   {    500,   50.  },
   {   1000,  100.  },
   {   2000,  200.  },
   {   5000,  500.  },
   {  10000, 1000.  },
   {  20000, 2000.  },
   {  50000, 5000.  },
   { 500000, 5000.  },
};

float DoLogScale(unsigned int n, float &Delta)
{  
   int   Index    = 0;
   float NewValue = MyTable[Index].Value;

   Delta = MyTable[Index].Delta;
   Index += 1;
   float Limit = MyTable[Index].Value;

   while(n > 0)
   {  if( (NewValue +  0.05) > Limit)
      {  Delta = MyTable[Index].Delta;
         Index += 1; 
         Limit = MyTable[Index].Value;
      }

      NewValue += Delta;
      n -= 1;
   }
   return NewValue;
}

float Update(float Value, int Direction, float Delta, float MinValue, float MaxValue)
{  
   while(Direction < 0)  // Turning CounterClock-wise
   {  if(Value > (MinValue + Delta))
      {  Value -= Delta;
      }
      else
      {  Value = MinValue;
      }
      Direction += 1;
   }
   
   while(Direction > 0)  // Turning Clock-wise
   {  if( (Value + Delta) < MaxValue)
      {  Value += Delta;
      }
      else
      {  Value  = MaxValue;
      }
      Direction -= 1;
   }
   return Value;
}

// =================================================================================================================

void setup() 
{  Serial.begin(115200);
   Timer.Start();
}

void loop() 
{
   if(Timer.Sync(1))
   {  RotaryInterruptHandler();
   }

   static bool     SetFineFrequency  = false;

   static uint16_t FrequencyIndex    = 0;
   static float    FrequencyStepsize = 0.1;
   static int      FrequencyOffset   = 0;    // For Fine-Tuning
   static float    Frequency;                // Actual active frequency

   static float    DutyCycleDelta = 1.0;     // or 0.1 when FineTuning 
   static float    DutyCycle;                // Actual DutyCycle.
   
   // ==========================================================================================

   // Update frequency
   if(MyRotary1.Button())   // Rotary-Switch toggles between Fine and course
   {  SetFineFrequency = ! SetFineFrequency;
      Serial.println(SetFineFrequency ? "Freq fine" : "Freq coarse");

      FrequencyOffset = 0; // Clear Adjustmenst when switching mode.

      Frequency = DoLogScale(FrequencyIndex, FrequencyStepsize);
      SetFrequency(Frequency, DutyCycle);
   }

   int16_t Delta; 
   if(MyRotary1.GetRotation(Delta))  // Turn rotary-1 to set frequency
   {  
      if(SetFineFrequency)
      {  FrequencyOffset = Update(FrequencyOffset, Delta, 1, -1000, 1000);
      }
      else
      {  // Update frequencyIndex with rotary data
         FrequencyIndex = Update(FrequencyIndex, Delta, 1, 0, 175);
      }
   
      // Convert to Logarithmic scale 
      Frequency = DoLogScale(FrequencyIndex, FrequencyStepsize);
      Frequency += ( FrequencyOffset * (FrequencyStepsize / 100) );

      SetFrequency(Frequency, DutyCycle);
   }

   // ==========================================================================================
   // Rotary-2 to update DutyCycle
   if(MyRotary2.Button()) // Switch toggles between Fine/Coarse
   {  if(DutyCycleDelta < 0.5)
      {  DutyCycleDelta = 1.0;       //< Coarse
         Serial.println("D coarse");
      }
      else
      {  DutyCycleDelta = 0.1;       //< Fine
         Serial.println("D fine");
      }
   }

   if(MyRotary2.GetRotation(Delta))
   {  DutyCycle = Update(DutyCycle, Delta, DutyCycleDelta, 0.0 , 100.0);
      SetFrequency(Frequency, DutyCycle);
   }
}


blackdog

Golden Member

Hi deKees,

Mooi dat je het een uitdaging vond! ;)
En anderen er dan ook nog iets aan hebben, toppie!

Ik heb vanavond in de slow modus (gaar van het werken) een Nokia scherpje aan een Pro mini gehangen en in de LCD setup pin-10 nar pin-13 verplaatst en dat werkt.

Nu proberen de software te maken voor de druk knopjes en de opbouw van het display netjes doen.
Het schrijven naar het scherm lijkt de frequentie generatie niet te beinvloeden.
Ik test dit dus met je eerste stukje code waar je de frequentie en duty cycle ingeeft op eé'n regel.

Als ik morgen nog wat tijd heb, test ik je nieuwe code!
Dank er voor, het wordt gewardeerd!

Groet,
Bram

You have your way. I have my way. As for the right way, the correct way, and the only way, it does not exist.
blackdog

Golden Member

Hi,

Ik ben wat code aan het samenvoegen, dat is de code van deKees.

c code:

void setup(){
unsigned long Frequency;
unsigned int DutyCycle;


Frequency = 1000;
DutyCycle = 50;
}

void loop() {
 
   unsigned long Count = 8000000ul / Frequency;
   unsigned char Prescaler;

   // Timer 1 has 16 bits so we need a count below 65536
   if( Count < 65536ul)
   {  // Ok, Prescaler can be 1
      Prescaler = ( 0b001 << CS10);
   }
   else if (Count < (8 * 65536ul))
   {  // We need a prescaler of 8
      Count /= 8;
      Prescaler = ( 0b010 << CS10);
   }
   else if (Count < (64 * 65536ul))
   {  // We need a prescaler of 64
      Count /= 64;
      Prescaler = ( 0b011 << CS10);
   }
   else if (Count < (256 * 65536ul))
   {  // We need a prescaler of 256
      Count /= 256;
      Prescaler = ( 0b100 << CS10);
   }

   TCCR1A = ( 0b00 << COM1A0 )  // Compare A is not used
          + ( 0b11 << COM1B0 )  // Set at compare match, Clear at Bottom
          + ( 0b01 << WGM10  ); // Clear timer on Compare A match
   TCCR1B = ( 0b0  << ICNC1  )
          + ( 0b0  << ICES1  )
          + ( 0b10 << WGM12  )
          + ( Prescaler      ); 

   OCR1B = Count * DutyCycle / 100;
   OCR1A = Count;

   DDRB |= 0b00000100; // Enable driver for OC1B (PB2)
}

En dat dus samen gevoegt zover ik dat kan, met de onderstaande code.
Deze code gebruikt een analoge pin en daar komen via een weerstand deler acht schakelaars aan te hangen via zeven 1K weerstanden vanaf de 5V
en dan 1K8 naar massa, simpel en effectief, de waarden per schakelaar liggen netjes uit elkaar en dat levert verder geen problemen op.

c code:

/*
AnalogButton_Combos
Version 0.1
Created By: Michael Pilcher
February 24, 2010
*/

int j = 1; // integer used in scanning the array designating column number
//2-dimensional array for asigning the buttons and there high and low values
int Button[9][3] = {{1, 1000, 1023}, // button 1
                    {2, 900, 910}, // button 2
                    {3, 782, 792}, // button 3
                    {4, 665, 675}, // button 4
                    {5, 548, 558}, // button 5
                    {6, 433, 445}, // button 6
                    {7, 316, 328}, // button 7
                    {8, 200, 214}}; // button 8                   

int analogpin = 5; // analog pin to read the buttons
int label = 0;  // for reporting the button label
int counter = 0; // how many times we have seen new value
long time = 0;  // the last time the output pin was sampled
int debounce_count = 50; // number of millis/samples to consider before declaring a debounced input
int current_state = 0;  // the debounced input value
int ButtonVal;


void setup()
{
 Serial.begin(9600);
unsigned long Frequency;
unsigned int DutyCycle;

Frequency = 400;
DutyCycle = 10;

}

void loop()
{
  // If we have gone on to the next millisecond
 if (millis() != time)
 {
   // check analog pin for the button value and save it to ButtonVal
   ButtonVal = analogRead(analogpin);
   if(ButtonVal == current_state && counter >0)
   {
     counter--;
   }
   if(ButtonVal != current_state)
   {
     counter++;
   }
   // If ButtonVal has shown the same value for long enough let's switch it
   if (counter >= debounce_count)
   {
     counter = 0;
     current_state = ButtonVal;
     //Checks which button or button combo has been pressed
     if (ButtonVal > 0)
     {
       ButtonCheck();
     }
   }
   time = millis();
 }


void ButtonCheck()
{
 // loop for scanning the button array.
 for(int i = 0; i <= 21; i++)
 {
   // checks the ButtonVal against the high and low vales in the array
   if(ButtonVal >= Button[i][j] && ButtonVal <= Button[i][j+1])
   {
     // stores the button number to a variable
     label = Button[i][0];
     Action();      
   }
 }
}

void Action()
{
 if(label == 1)
 {
Frequency = 400;
DutyCycle = 10;
 }
 if(label == 2)
 {
Frequency = 400;
DutyCycle = 20;
 }
 if(label == 3)
 {
  Frequency = 1000;
  DutyCycle = 10;  
 }
 if(label == 4)
 {
  Frequency = 1000;
  DutyCycle = 20;  
 }
 if(label == 5)
 {
   Frequency = 1;
   DutyCycle = 1;  
 }
 if(label == 6)
 {
    Frequency = 1;
    DutyCycle = 5;  
 }
 if(label == 7)
 {
   Frequency = 5;
   DutyCycle = 1;  
 }
 if(label == 8)
 {
   Frequency = 10;
   DutyCycle = 1;  
 }    
}
// Frequentie generatie geschreven door deKees, Circuits Online, Nederland

   unsigned long Count = 8000000ul / Frequency;
   unsigned char Prescaler;

   // Timer 1 has 16 bits so we need a count below 65536
   if( Count < 65536ul)
   {  // Ok, Prescaler can be 1
      Prescaler = ( 0b001 << CS10);
   }
   else if (Count < (8 * 65536ul))
   {  // We need a prescaler of 8
      Count /= 8;
      Prescaler = ( 0b010 << CS10);
   }
   else if (Count < (64 * 65536ul))
   {  // We need a prescaler of 64
      Count /= 64;
      Prescaler = ( 0b011 << CS10);
   }
   else if (Count < (256 * 65536ul))
   {  // We need a prescaler of 256
      Count /= 256;
      Prescaler = ( 0b100 << CS10);
   }

   TCCR1A = ( 0b00 << COM1A0 )  // Compare A is not used
          + ( 0b11 << COM1B0 )  // Set at compare match, Clear at Bottom
          + ( 0b01 << WGM10  ); // Clear timer on Compare A match
   TCCR1B = ( 0b0  << ICNC1  )
          + ( 0b0  << ICES1  )
          + ( 0b10 << WGM12  )
          + ( Prescaler      ); 

   OCR1B = Count * DutyCycle / 100;
   OCR1A = Count;

   DDRB |= 0b00000100; // Enable driver for OC1B (PB2)
 
}

Als ik dit dus aan elkaar knoopt krijg ik een aantal error bij het compilen en ik kan niet zien wat de fout is van deze comby code.
De button code alleen werkt zonder problemen net als de frequentie code van deKees.

Ik zal zondermeer ergens een of meerdere fouten maken, maar ik zie ze niet 8)7

Dit is de foutcode die ik te zien krijg:

c code:


C:\Users\master21\Documents\Arduino\Analog-Button-1e-Versie\Analog-Button-1e-Versie.ino: In function 'void loop()':

Analog-Button-1e-Versie:62:8: error: 'ButtonCheck' was not declared in this scope

        ButtonCheck();

        ^~~~~~~~~~~

C:\Users\master21\Documents\Arduino\Analog-Button-1e-Versie\Analog-Button-1e-Versie.ino:62:8: note: suggested alternative: 'ButtonVal'

        ButtonCheck();

        ^~~~~~~~~~~

        ButtonVal

Analog-Button-1e-Versie:70:1: error: a function-definition is not allowed here before '{' token

 {

 ^

Analog-Button-1e-Versie:85:1: error: a function-definition is not allowed here before '{' token

 {

 ^

Analog-Button-1e-Versie:129:38: error: 'Frequency' was not declared in this scope

    unsigned long Count = 8000000ul / Frequency;

                                      ^~~~~~~~~

Analog-Button-1e-Versie:161:20: error: 'DutyCycle' was not declared in this scope

    OCR1B = Count * DutyCycle / 100;

                    ^~~~~~~~~

exit status 1
'ButtonCheck' was not declared in this scope

Dus wie helpt om mij te verlossen van mijn blinde vlekken. :+

Dank en groet,
Bram

You have your way. I have my way. As for the right way, the correct way, and the only way, it does not exist.

Paar kleine dingetjes:

code:


void setup(){
unsigned long Frequency;
unsigned int DutyCycle;


Frequency = 1000;
DutyCycle = 50;
}

de beide variabelen Frequency en DutyCycle bestaan niet meer zodra setup is afgelopen. Dus die moet je buiten de setup funktie declareren.

code:


void ButtonCheck()
{
 // loop for scanning the button array.
 for(int i = 0; i <= 21; i++)
 {
   // checks the ButtonVal against the high and low vales in the array
   if(ButtonVal >= Button[i][j] && ButtonVal <= Button[i][j+1])

De button array heeft maar 9 items. Dus dan moet je i binnen de limieten 0 - 8 houden. Dit geeft alleen een warning, geen foutmelding.

Analog-Button-1e-Versie:62:8: error: 'ButtonCheck' was not declared in this scope

De C++ compiler klaagt hier dat je een funktie gebruikt voordat die is gedefinieerd. Normaal is dat geen probleem omdat de arduino ide dat voor je oplost. Maar dat lukt hier niet omdat er een paar haakjes verkeerd staan.
- Geen sluithaakje aan het eind van loop(), dus voor de funktie ButtonCheck()
- en de code voor het instellen van de timer staat niet binnen een funktie. Die zou binnen de Action() moeten staan.

Probeer zo maar eens

code:


/*
AnalogButton_Combos
Version 0.1
Created By: Michael Pilcher
February 24, 2010
*/

//2-dimensional array for asigning the buttons and there high and low values
int Button[9][3] = {{1, 1000, 1023}, // button 1
                    {2, 900, 910}, // button 2
                    {3, 782, 792}, // button 3
                    {4, 665, 675}, // button 4
                    {5, 548, 558}, // button 5
                    {6, 433, 445}, // button 6
                    {7, 316, 328}, // button 7
                    {8, 200, 214}}; // button 8                   

int analogpin = 5; // analog pin to read the buttons
int label = 0;  // for reporting the button label
int counter = 0; // how many times we have seen new value
unsigned long time = 0;  // the last time the output pin was sampled
int debounce_count = 50; // number of millis/samples to consider before declaring a debounced input
int current_state = 0;   // the debounced input value
int ButtonVal;

void ButtonCheck();
void Action();


unsigned long Frequency;
unsigned int DutyCycle;

void setup()
{
   Serial.begin(9600);

   Frequency = 400;
   DutyCycle = 10;
}

void loop()
{
   // If we have gone on to the next millisecond
   if (millis() != time)
   {
      // check analog pin for the button value and save it to ButtonVal
      ButtonVal = analogRead(analogpin);
      if(ButtonVal == current_state && counter >0)
      {
         counter--;
      }
      if(ButtonVal != current_state)
      {
        counter++;
      }
      // If ButtonVal has shown the same value for long enough let's switch it
      if (counter >= debounce_count)
      {
         counter = 0;
         current_state = ButtonVal;
         //Checks which button or button combo has been pressed
         if (ButtonVal > 0)
         { 
             ButtonCheck();
         }
      }
      time = millis();
   }
}


void ButtonCheck()
{
   // loop for scanning the button array.
   for(int i = 0; i <= 8; i++)
   {
      // checks the ButtonVal against the high and low values in the array
      if(ButtonVal >= Button[i][1] && ButtonVal <= Button[i][2])
      {
        // stores the button number to a variable
        label = Button[i][0];
        Action();      
      }
   }
}

void Action()
{
   if(label == 1)
   {
      Frequency = 400;
      DutyCycle = 10;
   }
   if(label == 2)
   {
      Frequency = 400;
      DutyCycle = 20;
   }
   if(label == 3)
   {
      Frequency = 1000;
      DutyCycle = 10;  
   }
   if(label == 4)
   {
      Frequency = 1000;
      DutyCycle = 20;  
   }
   if(label == 5)
   {
      Frequency = 1;
      DutyCycle = 1;  
   }
   if(label == 6)
   {
      Frequency = 1;
      DutyCycle = 5;  
   }
   if(label == 7)
   {
      Frequency = 5;
      DutyCycle = 1;  
   }
   if(label == 8)
   {
      Frequency = 10;
      DutyCycle = 1;  
   }    

   // Frequentie generatie geschreven door deKees, Circuits Online, Nederland

   unsigned long Count = 8000000ul / Frequency;
   unsigned char Prescaler;

   // Timer 1 has 16 bits so we need a count below 65536
   if( Count < 65536ul)
   {  // Ok, Prescaler can be 1
      Prescaler = ( 0b001 << CS10);
   }
   else if (Count < (8 * 65536ul))
   {  // We need a prescaler of 8
      Count /= 8;
      Prescaler = ( 0b010 << CS10);
   }
   else if (Count < (64 * 65536ul))
   {  // We need a prescaler of 64
      Count /= 64;
      Prescaler = ( 0b011 << CS10);
   }
   else if (Count < (256 * 65536ul))
   {  // We need a prescaler of 256
      Count /= 256;
      Prescaler = ( 0b100 << CS10);
   }

   TCCR1A = ( 0b00 << COM1A0 )  // Compare A is not used
          + ( 0b11 << COM1B0 )  // Set at compare match, Clear at Bottom
          + ( 0b01 << WGM10  ); // Clear timer on Compare A match
   TCCR1B = ( 0b0  << ICNC1  )
          + ( 0b0  << ICES1  )
          + ( 0b10 << WGM12  )
          + ( Prescaler      ); 

   OCR1B = Count * DutyCycle / 100;
   OCR1A = Count;

   DDRB |= 0b00000100; // Enable driver for OC1B (PB2)
}

blackdog

Golden Member

Hi deKees, :-)

Deze:

c code:

 // loop for scanning the button array.
 for(int i = 0; i <= 21; i++)

Had ik over het hoofd gezien tijdens het aanpassen van de orginele code van de drukknoppen, 21 is wat veel voor dit meetinstrumentje.

En wat betreft dit

c code:

Analog-Button-1e-Versie:62:8: error: 'ButtonCheck' was not declared in this scope

Ik kan het niet vinden waar ik wat vergeten ben ook niet met de code van jou naast de code van "mij"

Dat de variabele in de setup niet meer werken in de loop was ik gewoon vergeten. :-)

Nu draait het geheel weer even op een Nano en als er wat tijd over is vandaag hang ik de Nokia display driver in de code.
Als dat goed gaat dan pas ik het weer aan naar de Pro Mini.

Ik weet nog niet of ik meetinstrumentje een eigen netvoeding geef of op een batterij laat draaien.
Dat wordt ook bepaald door het stuurniveau dat ik nodig heb voor mijn dummy loads, en hoe complex het wordt.

deKees als ik een extra drukknop wil toevoegen die de uitgang van jou genrator stil maakt (enable-disable),
dat als de uitgang in de disable stand staat deze "0" is waar kan ik dit dan het beste doen in jouw code?

Dank vast en gegroet,
Bram

You have your way. I have my way. As for the right way, the correct way, and the only way, it does not exist.

Ik kan het niet vinden waar ik wat vergeten ben ook niet met de code van jou naast de code van "mij"

De indenting in je oorspronkelijke code is niet helemaal consequent doorgevoerd. Daardoor wordt het al snel veel moeilijker om te zien of de haakjes allemaal goed staan. Dat is altijd het eerste wat ik aanpas. Altijd 3 posities inspringen bij elk haakjes niveau.

code:


void loop()
{
  // If we have gone on to the next millisecond
 if (millis() != time)
 {
   // check analog pin for the button value and save it to ButtonVal
   ButtonVal = analogRead(analogpin);
   if(ButtonVal == current_state && counter >0)
   {
     counter--;
   }
   if(ButtonVal != current_state)
   {
     counter++;
   }
   // If ButtonVal has shown the same value for long enough let's switch it
   if (counter >= debounce_count)
   {
     counter = 0;
     current_state = ButtonVal;
     //Checks which button or button combo has been pressed
     if (ButtonVal > 0)
     {
       ButtonCheck();
     }
   }
   time = millis();
 }
   <<< ******* Hier is een sluithaakje te weinig *******

void ButtonCheck()
{
 // loop for scanning the button array.
 

code:


 if(label == 8)
 {
   Frequency = 10;
   DutyCycle = 1;  
 }    
}  <<< *************** Haakje te veel ****************
// Frequentie generatie geschreven door deKees, Circuits Online, Nederland

   unsigned long Count = 8000000ul / Frequency;
   unsigned char Prescaler;

   // Timer 1 has 16 bits so we need a count below 65536
   if( Count < 65536ul)
   {  // Ok, Prescaler can be 1
      Prescaler = ( 0b001 << CS10);
   }

Ik vind het wel een leuke methode om zoveel knopjes op een enkele pin in te lezen. Een ekstra knopje toevoegen kan nog wel met een extra 1K weerstand. Maar dan veranderen de getallen in de button tabel omdat alle spanningen dan opschuiven. Dus dan zou ik de compiler de nieuwe getallen laten uitrekenen.

Dat wordt dan zoiets:

code:


//2-dimensional array for assigning the buttons and their high and low values

#define RTOT   (1800 + (8 * 1000))
#define DELTA   5   
#define BUTTON_VOLTAGE(R) (((1024 * R) / RTOT) - DELTA),(((1024 * R) / RTOT) + DELTA)  

int Button[9][3] = {{1, BUTTON_VOLTAGE(9800)},  // button 1
                    {2, BUTTON_VOLTAGE(8800)},  // button 2
                    {3, BUTTON_VOLTAGE(7800)},  // button 3
                    {4, BUTTON_VOLTAGE(6800)},  // button 4
                    {5, BUTTON_VOLTAGE(5800)},  // button 5
                    {6, BUTTON_VOLTAGE(4800)},  // button 6
                    {7, BUTTON_VOLTAGE(3800)},  // button 7
                    {8, BUTTON_VOLTAGE(2800)},  // button 8
                    {9, BUTTON_VOLTAGE(1800)}}; // button 9                   

Dan kun je in de Action() een funktie toevoegen.

blackdog

Golden Member

Hi deKees,

Hier is zichtbaar hoe ik de schakelaars electrisch heb gekoppeld, links de 8 knops versie en rechts de 9 knops versie.
Met de 1K weerstanden is dat het maximaal aantal standen.
Natuurlijk weer ik dat er ook andere manieren mogelijk zijn zzoals het r-2r netwerkjes enz.
Maar het leuke van deze setup is dat ik hier nu ook een draai schakelaar kan gebruiken.

En ja met 470Ω weerstanden kom je aan het dubbele aantal standen, de marge wordt wel steeds kleiner en je weerstadns nauwkeurigheid gaat da nook meespelen net als de lineairietijd van de ADC.
Het mooi van deze setup en dan heb ik het over de software die de uitlezing doet, is dat de uitgang vast blijft en geen last van dender heeft.
Dit alles net als de code van deKees zonder gebruik te maken van library's.

De "5V" Out van de Arduino is natuurlijk geen 5V er zit altijd en diode tussen,
maar voor de dudielijkheid heb ik hier in het schema de gemeten waarden aangegeven

Bij de 8-schakelaar uitvoering staat links van de schakelaar de 10Bit waarde weergegeven, voor b.v. S5 is dat de waarde 553.
Voor de vergelijking heb ik ongeveer + en -5 bitjes aangehouden en ik denk dat voor 8 of 9 schakelaars + en - 10 bitjes wat meer zekerheid geeft.

De weerstadn van de schakelaar zelf is verder onbelangrijk, R10 geeft ongeveer 1% belasting in de ongunstigste stand.
De Ri van de hier getoonde spanningsdeler is de totale waarde van de weerstadns string gedeeld door vier.
Dit gaat ook hier op omdat de impedantie van de + aansluiting bovenaan zeer laag is t.o.v. de weerstands string, zeg maar een paar Ohm.
Dan krijgen we dus een maximale uitgangs impedantie van de sting op stand-5 van 7x 1K + 1K8 gedeeld door vier endat geeft dan een waarde van 2K2.

Ik weet dat de Arduino graag voor hoge frequenties een lage impedantie ziet aan zijn analoge ingang, de 1nF heeft een impedantie van ongeveer 160Ω bij 1MHz.
Volgens mijn metijgn voldoende laag voor mijn gbruikt, ik zoek bij deze toepassing niet naar optimaal lineair gedrag. :-)

http://www.bramcam.nl/Diversen/Arduino-Analog-Button-01.png

.
Dit is de testsetup met een Nano, een 1K8 weerstand en zeven stuks 1K naar de 5V uitgang van de Nano.
De LED zit via een weerstand op de uitgang zodat ik kan zien dat de frequentie en/of pulsbreedte veranderd.
http://www.bramcam.nl/Diversen/Arduino-Analog-Button-02.png

deKees
Twee dingen...
Als eerste het stukje code dat je laat zien om de toetsen uit te lezen, de "Delta" variabele heeft dit de zelfde functie als dit in de orginele code:

c code:

int Button[9][3] = {{1, 1000, 1023}, // button 1
                    {2, 900, 910}, // button 2
                    {3, 782, 792}, // button 3

Werkt dit twee kanten op net als in de orginele code?

Ik zal de code later even testen.

Verder zal ik beter letten op de haakjes in de code. :-)

Oja, de tweede vraag...
Hoe zet ik je frequentie generator uit en dat de pin-10 een "0" niveau heeft.

Kan het door dit stukje de 1 door een 0 te vervangen?

c code:

DDRB |= 0b00000100; // Enable driver for OC1B (PB2)

Groet,
Bram

You have your way. I have my way. As for the right way, the correct way, and the only way, it does not exist.

Ja, de delta werkt 2 kanten op.
De BUTTON_VOLTAGE macro levert 2 getallen op. Dat is de berekende middenwaarde, en dan min DELTA en Plus DELTA.

Ik was daarbij uitgegaan van 1K8 als onderste weerstand, en 8 keer 1K.
In jouw geval heb je de 1K afgetrokken van de onderste weerstand, Dan kunnen alle getallen in de tabel hetzelfde blijven, en toch een regel toevoegen. Dat kan ook door de waarde bij RTOT aan te passen naar (820 + (8 * 1000)). En eigenlijk moet je ook R19 (R9) nog meenemen in de berekening.

Overigens vind ik een DELTA van 5 wel erg klein. Dat geeft een marge van 0.5 % op de gemeten waarde dus dan heb je al goede weerstanden nodig. Dat kan best omhoog naar 50 volgens mij.

Even uitrekenen:
- Button 1 : 1019 - 1029 (1024 +/- 5)
- Button 2 : 914 - 924 ( 919 +/- 5)
enz

De output driver uitzetten gaat zo:

code:


   DDRB &= ~0b00000100; // Disable driver for OC1B (PB2)
of
   pinMode(10, INPUT);

Daarmee wordt PB2 een input en dus tri-state.

Maar waarschijnlijk wil je de driver aktief laag houden. Dan moet je de waveform generator uitschakelen (0b00 naar COM1B0):

code:


   TCCR1A = ( 0b00 << COM1A0 )  // Compare A is not used
          + ( 0b00 << COM1B0 )  // << Waveform generator uitschakelen
          + ( 0b01 << WGM10  ); // Clear timer on Compare A match

En de pin laag maken:

code:


   PORTB &= ~(0b00000100);
of
   digitalWrite(10,LOW);
blackdog

Golden Member

Hi deKees, :-)

Vraagje voor als ik iets wil uitbreiden met de analoge input schakelaars.
Niet direct voor dit meetinstrumentje maar ik zou voor iets anders b.v. drie groepjes van druk knopjes.

Zo de drie groepjes krijgen ieder een eigen analog ingang toebedeeld.
Kan ik b.v. alle variabelen die gebruikt worden voor de drukknopjes voorzien van _a, _b, _c, voor de drie groepjes schakelaars?
Kom ik dan niet in de problemen met time = millis(); voor die drie groepjes schakelaars?

Groet,
Bram

You have your way. I have my way. As for the right way, the correct way, and the only way, it does not exist.