Pelsea

This is an Arduino program that provides high speed pulse width modulation suitable (with appropriate electronic drivers) to control LEDS, lamps, DC motors and DC locos. So far I have tested it with LEDs and a small DC motor. I invite those so inclined to try it and point out the bugs and suggest improvements. (There's more than one way to code a cat.)

The hardware design is loosely based on a description of LDBennett's project at  https://forum.mrhmag.com/post/pulse-width-modulation-results-good-and-bad-12193495. I've prototyped this on an UNO, some other boards may require different pin assignments. There are three control buttons and two potentiometers connected as follows:

  • pin 2  start up --   Normally Open button to ground
  • pin 3 coast to stop  -- Normally Open button to ground
  • pin 4 emergency stop -- Normally Open button to ground
  • pin A0 momentum or fade time -- Potentiometer between +5 and ground
  • pin A1 speed or brightness -- Potentiometer between +5 and ground
  • pin 9 is the PWM output -- test with LED and 330 ohm resistor to ground
  • pin 13 will go high when unit is up to speed -- led is on the board

I'll put the complete code in the next post, then post discussions of individual sections.

pqe

Reply 0
Pelsea

Here's the code

I think this is right, but the friendly editor keeps trying to make a mess of it.

/********
Throttle 1.1  P Q Elsea May 20, 2013
An Arduino program to control the pulse width at pin 9 with adjustable fade times.
This can be used, with suitable driver electronics, to power leds, lamps or motors
The output frequency is 31250 to avoid throttle noise
there are two control inputs
A0 sets fade time
A1 sets power on duty cycle
there are three contol pins - pull low to activate
pin 2 starts a fade up to target duty cycle
pin 3 starts a fade down to 0
pin 4 is an emergency stop
outputs
pin 9 is pulse width modulated, active high
pin 13 shows running status- active high to work with built-in LED
*/
const int upPin = 2;     // the number of the faster pin
const int downPin = 3;     // the number of the slower pin
const int stopPin = 4;     // the number of the emergency stop pin
const int statusPin = 13;  // will light when unit reaches full speed
const int momentumPin = A0;  // Analog input pin that the potentiometer is attached to
const int speedPin = A1;  // Analog input pin that the potentiometer is attached to
const int outPin = 9;     // Analog output pin that the LED is attached to
const int STOPNOW = 0;    // poor man's enum
const int RUNNING = 1;    // these modes control operation via runMode
const int SPEEDUP = 2;    //
const int SLOWDOWN = 3;   //

int runMode = 0;          // buttons set this to one of the above
unsigned int rate = 0;    // contains output duty * 32 for counting
unsigned int target = 0;  //contains target output duty * 32 for comparison
int buttonState = 0;      //read buttons into here
unsigned int momentum = 64;  // value read from momentum pot
unsigned int stepValue = 1;  // value added to rate for ramp
int outValue = 0;         // value output to the PWM (analog out)


void setup() {
TCCR1B = TCCR1B & 0b11111000

pinMode(stopPin, INPUT);
digitalWrite(stopPin,HIGH); // turn on pullup
pinMode(upPin, INPUT);
digitalWrite(upPin,HIGH); // turn on pullup
pinMode(downPin, INPUT);
digitalWrite(downPin,HIGH); // turn on pullup
pinMode(statusPin, OUTPUT);
digitalWrite(statusPin,LOW); // turn off running light
}

void loop() {
  //check the buttons
  buttonState = digitalRead(stopPin);  // emergency stop!
  if(buttonState == LOW){  //read buttons when low makes then easy to wire
    rate = 0;
    analogWrite(outPin, 0); 
    runMode = STOPNOW;
  }
    buttonState = digitalRead(upPin);  // speed up to target
  if(buttonState == LOW){
     runMode = SPEEDUP;
  }
    buttonState = digitalRead(downPin);  // slow to stop
  if(buttonState == LOW){
    runMode = SLOWDOWN;
  }
  //acquire target speed and momentum from pots
  target = analogRead(speedPin)< < 3;       // 32 loops per step at max momentum
  momentum = analogRead(momentumPin);
  stepValue = map(momentum,0,1023,128,1);  // adjust map to change momentum range

  switch(runMode){
    case SLOWDOWN:
if(rate> stepValue) {
   rate -= stepValue; 
}else{
   rate = 0;
   runMode = STOPNOW;}  // edit 5-20   caught a bug
break;
case SPEEDUP :
if(rate < target) {
   rate += stepValue; 
  }else{
   rate = target;
   runMode = RUNNING;
   }
break;
case RUNNING:  // follow change in speed knob
   if(rate> target) rate -= stepValue;
   else if (rate < target) rate += stepValue;
break;
  }

if(runMode == RUNNING) // turn on a light when speed matches target.
digitalWrite(statusPin, HIGH);
else
digitalWrite(statusPin, LOW);
  outValue = rate 5;  // right shift is 2 more than left shift on target
  // make load compensations here
  analogWrite(outPin, outValue);          
//2mS is the minimum loop time if you want to do analog reads
//loop time is delay mS + 250 uS
  delay(2);                    
}

Reply 0
Pelsea

Discussion: the Globals

// Global variables are used throughout the program

const int upPin = 2;     // the number of the faster pin
const int downPin = 3;     // the number of the slower pin
const int stopPin = 4;     // the number of the emergency stop pin
const int statusPin = 13;  // will light when unit reaches full speed
const int momentumPin = A0;  // Analog input pin that the potentiometer is attached to
const int speedPin = A1;  // Analog input pin that the potentiometer is attached to
const int outPin = 9;     // Analog output pin that the LED is attached to
const int STOPNOW = 0;    // poor man's enum
const int RUNNING = 1;    // these modes control operation via runMode
const int SPEEDUP = 2;    //
const int SLOWDOWN = 3;   //

int runMode = 0;          // buttons set this to one of the above
unsigned int rate = 0;    // contains output duty * 32 for coounting
unsigned int target = 0;  //contains target output duty * 32 for comparison
int buttonState = 0;      //read buttons into here
unsigned int momentum = 64;  // value read from momentum pot
unsigned int  stepValue = 1;  // value to add or subtract from rate
int outValue = 0;         // value output to the PWM (analog out)

Discussion: There's not much to say here that's not in the comments-- I believe in descriptive variable names rather than terse ones. Big CAP constants help me keep the modes straight when things get dense.

pqe

Reply 0
Pelsea

Discussion: the Setup()

void setup() {
TCCR1B = TCCR1B & 0b11111000

pinMode(stopPin, INPUT);
digitalWrite(stopPin,HIGH); // turn on pullup
pinMode(upPin, INPUT);
digitalWrite(upPin,HIGH); // turn on pullup
pinMode(downPin, INPUT);
digitalWrite(downPin,HIGH); // turn on pullup
pinMode(statusPin, OUTPUT);
digitalWrite(statusPin,LOW); // turn off running light
}

The enigmatic TCCR1B line sets the Atmega prescaler to run the PWM mechanism at its highest rate. This is a hardware subsystem in the Atmega chip that runs independently of the CPU. It's not affected by delays or wait states in the main program. Changing the timing of some of the PWM pins affects delay() and millis() functions, but this one doesn't seem to have any side effects.

The pins on the Atmega are multi function and need to be told to be input or output. You write HIGH to an input to set a pullup resistor that will make the pin read HIGH if nothing is connected. I like to use my inputs in active low mode (ground to make something happen) so I can connect several buttons in parallel.

pqe

Reply 0
Pelsea

Discussion: read Buttons

void loop() {
  //check the buttons
  buttonState = digitalRead(stopPin);  // emergency stop!
  if(buttonState == LOW){  //read buttons when low makes then easy to wire
    rate = 0;
    analogWrite(outPin, 0); 
    runMode = STOPNOW;
  }
    buttonState = digitalRead(upPin);  // speed up to target
  if(buttonState == LOW){
     runMode = SPEEDUP;
  }
    buttonState = digitalRead(downPin);  // slow to stop
  if(buttonState == LOW){
    runMode = SLOWDOWN;
  } //(loop continues)

The loop() function is called repeatedly as long as the Arduino is running. Each time around, we need to look for new input and do something about it. All the buttons actually do is set the current run mode. The four modes:

  • STOPNOW  is the emergency stop state. This clause shuts the output off immediately and leaves it off until a speed up command comes in.
  • SPEEDUP starts when this button is pressed and continues until the speed reaches the target.
  • SLOWDOWN starts when this button is pressed and continues unitl the speed reaches zero.

The run mode affects what happens later in the loop.

pqe

Reply 0
Pelsea

Discussion: speed and momentum

 //acquire target speed and momentum from pots
  target = analogRead(speedPin)< < 3;       // 32 loops per step at max momentum
  momentum = analogRead(momentumPin);
  stepValue = map(momentum,0,1023,128,1);  // adjust map arguments to change momentum range

The heart of this program consists of the rate and target variables. I set a target speed, and gradually change the rate until it matches the target. I slow this process down by using a big number for the target- the analog read gives me a 10 bit number (up to 1023) the output requires an 8 bit number (up to 255). Usually we get from analog in to analog out (that's what Arduino calls PWM) by dividing by 4, which can also be done by a right shift of the bits by 2 (   2). Those extra bits might be thought of as a fractional part of the eventual output. The slowest we can change the rate value is one bit each time around the loop. A loop takes 2 milliseconds, so counting to 10 bits maximum will give a 2 second fade or start up time. Here I increase the target to 13 bits by a left shift operation that gives me a maximum fade time around 16 seconds. You can play with this shift (up to < < 6 for 2 hours) to get various fade times.

To speed up the transition, I add a bigger value than 1 on each loop. That is what stepValue is for. One each loop I will add stepValue to rate. Momentum comes in as a value from 0 to 1023, and gets remapped into stepValue to fit from 128 to 1. Adding 128 on each loop will cut the transit time to 120 ms. You can adjust the mapping to provide a range of momentum suitable to your need. Note that large momentum implies a low stepValue (edited 5-20 -13 to clarify operation.)

pqe

Reply 0
Pelsea

Discussion: doing the math

 

  switch(runMode){
    case SLOWDOWN:
 if(rate> stepValue) {
   rate -= stepValue;  
 }else{
   rate = 0;
   runMode = STOPNOW;}   // edit 5-20-13, fixed bug and changed variable name
 break;
 case SPEEDUP :
 if(rate < target) {
   rate += stepValue;  
  }else{
   rate = target;
   runMode = RUNNING;
   }
 break;
 case RUNNING:  // follow change in speed knob
   if(rate> target) rate -= stepValue;
   else if (rate < target) rate += stepValue;
 break;
  }
 
This is the part of the code that does the work. The case statement chooses from 3 options according to runMode.
  • In SLOWDOWN mode, subtract stepValue from rate until rate is less than one step from 0. Then the rate gets set to 0 and runMode changed to STOPNOW.  We can't afford to go below zero because rate is an unsigned int, and -1 looks like a very big number to it. If we tried to write a negative here the output would just cycle from bright to off repeatedly (ask me how I know this.)
  • In SPEEDUP mode add stepValue to rate until rate is greater than the target, then set it at the target and change the mode to RUNNING.
  • In RUNNING mode, compare the rate to the target and add or subtract stepValue as needed. This way if someone turns the speed knob, the output will gradually follow it.

This kind of mechanism is a bit complex, but has the advantage that computing goes on while the output is changing, so there is quick response to button pushes and knob turns.

pqe

Reply 0
Pelsea

Discussion: output

Finally we get to output something.

 

 if(runMode == RUNNING) // turn on a light when speed matches target.
 digitalWrite(statusPin, HIGH);
 else
 digitalWrite(statusPin, LOW);
  outValue = rate 5;  // right shift is 2 more than left shift on target
  // make load compensations here
  analogWrite(outPin, outValue);           
  delay(2);                     
}// end of program
 
There are only two outputs so far, but there are plenty of pins so things like direction control can be easily added. Even adding more channels of control is possible. I started turning the pin 13 light on  as a diagnostic tool (you didn't think this worked out of the box did you?) but left it on since it might signal to another system what is going on.
 
Since the rate is calculated on 13 bits, I right shift to get the 8 needed for output. The speed change is linear, which may not be appropriate for locos. You can compensate for mechanical behavior between the shift operation and writing the value out. 0 should remain 0, but you may want  to remap step 1 to a higher output value so the loco starts right away. I'll discuss that in a later  post after I do some experimenting.
 
Considering the speed with which I wrote this and the 90 minutes of rigorous testing, (not to mention the current hour) I am sure there are some whoppers in here. Please run it through the wringer and add your comments.
pqe
Reply 0
Logger01

Code Testing

I do not have a spare Arduinos at the moment, but I should be able to free one up in a few weeks to test the code. After initial testing I will probably restructure the Globals to work with an Arduino Motor Shield and / or an Adafruit Motor/Stepper/Servo Shield. The Arduino Shield will drive two DC motors with BEMF support and an Adafruit Shield will drive four DC or two Stepper and / or two Servo motors. Neither board is very expensive, and both have support libraries.

Ken K

gSkidder.GIF 

Reply 0
GUATAPARO

There should be a basic arduino application for model railroade

Great article. There should be a basic arduino application for model railroader every month. Could you include a link with some pictures....... Thanks!
Reply 0
Pelsea

Thanks

Thanks Gutaparo, there's nothing to see yet, but I hope to produce a video once I attach this to something more interesting than a light.

Note that I have slightly edited the code. If there is enough interest, I'll post more applications (suggestions welcome) and set up a place to download source code.

pqe

Reply 0
mayhaw9999

Arduino Interest

Thanks for posting the Arduino program and discussion.  I'm definitely interested in learning more.  I've just started with microprocessors and have made a couple of small projects for my house and orchard  using the Atmega 328P chip on a minimalist board programmed with the Arduino.  Since my programming skills are in the kindergarten stage, any programs that will do things that I'm likely to use are welcome!

David Ulmer

Reply 0
SP-Oregon-Branch

late to the party...but can we have an encore please?

When I got the brilliant idea to control my simple, point-to-point streetcar/trolley layout with an Arduino and L298N motor driver I had a sneaking suspicion that I was reinventing the wheel. I got a jumpstart with this helpful Russian version but I think the author is eventually going to commercialize his idea and then I will be stuck paying up or going a different route.

But then enter  LDBennett's concept project and then this Arduino source code from Pelsea and I'm ecstatic with optism! As soon as I'm done here I'm going to load up this code on my Arduino and start testing and report back.

In the mean time, I want to second David's motion and say YES! PLEASE SHARE MORE OF THIS!!! :-D

Reply 0
BruceNscale

Maximum Speed Pot

Hi Pelsea,

Thanks for sharing all the Arduino and circuitry with other modelers.

I added a "max speed" pot to my throttle to prevent newbies(grandchildren?) from high speed chases around the layout. No matter how many times the "faster" button is pressed, speed is limited to the setting of the max speed pot. 

ignature.jpg 

Happy Modeling, Bruce

Reply 0
Reply