Jump to content

Uno32 + Motor Board + PID DC Motor Fun


VaguelyAmused

Recommended Posts

All,

 

First post, so may I say hi and introduce myself :) I've long been into electronics and good old DIY dabbling, took a course in "Applied Computing and Electronics" at Bournemouth Uni in the UK which was a bit of everything from VHDL, circuit layout/design and 8051 programming to some C++, Java and C#. Since then I've gone from creative fun programmer to boring paper pusher manager in the hunt for something to label a career.

 

Along came the little Uno32 and the Motor Shield for some of that home hobby messing about I've so missed - boy is it good to be doing it again! :-D

 

So some more background - many years ago my dad and I effectively retrofitted a highly accurate drive system to a telescope mount for astro-photography, this had a DC servo motor drive unit at the heart and back then (12 years now) that little magic box cost well into the triple figures. Before the days of my training, the insides of that box were simply black magic. Fast forward to now and just for fun I wanted to see what a sub £50 bit of kit, a la the Uno32 and some rusty knowledge could do to imitate that once mysterious and extortionately priced system.

 

Well, its not exactly stellar, but I'm quite chuffed with what I've done so far. Lots of googling, lots of libraries downloaded and fiddled with and some slightly oddball approaches to the motor control and I have something that, despite the rambling, may be of interest to others.

 

Basic Description:

 

Uses the ChipKit Uno32, the Motor Shield, a 9V 3A supply for the shield and a Maxon 12V 3.8:1 gearmotor with 512CPT encoder (£25 off eBay). Just the one motor so far, though 2 should be simply a few extra lines in the sketch. 

 

Libraries that have been used include the SoftPWMServo, modified to run at 30KHz (20KHz still made some audible wine) and accept a range of 0-65535 (finer control, in theory). Checked the output on a scope at work and it looks pretty darn accurate for a software 30KHz PWM - I'm impressed :-)

 

Not clear if I am allowed to post links here, but a google search will take you straight to the library.

 

Another library was the Arduino PID Library, modified to match changes to SoftPWMServo accepting a range from 0-65535.

 

A bit of code was stolen from one of the MPIDE examples to assist with serial data parsing so a few commands can be sent over the serial port to set either position or velocity. That came from the BasicSerialParser example in Communications. 

 

Motor Control:

 

Control is somewhat odd, my own thinking but I completely expect by no means original! I drive the H Bridge completely backwards. By this I mean I keep the enable pin high, and send the PWM through the direction pin. This is probably considered completely mental, but it has a few interesting benefits (IMHO).

 

Before PID, I was seeing how slow the motor would turn before stalling using just open loop PWM. This is of interest (to me) because having a large dynamic range is useful for telescopes - sloooow tracking but speedy slewing to an object. Driving the H Bridge in this alternate way definitely allowed me to drive it slower by about 3 fold compared to driving the H Bridge normally. Similarly, holding torque is improved since in 0 position the motor is not actually off, but oscillating constantly. The motor does obviously get hot though since it is constantly being driven at max duty cycle just in different directions all the time! No magic smoke released, so it seems the H Bridge copes with it OK - I'll let you know if it changes!

 

Having said all that, the motor wasn't exactly great on torque (its only 22mm as it is and under driven at 9V for a 12V motor).

 

The "steady state" PWM value for the motor is 32767 (effectively 50/50 duty cycle in forward/reverse resulting in 0 angular velocity), 0 sets max reverse speed and 65535 sets max forward speed. 

 

Now with PID thrown in the mix that forward/reverse control of the oddball H Bridge driving makes things really easy. Oh, PID is just brilliant by the way. Hopefully the sketch attached shows how simple it is to throw a PID algorithm in there, and it ends up allowing for some really cool control. Position (not worked out how to allow for going negative yet) is between 0 and _alot_.

 

It is just a case of setting a "setpoint" of encoder pulses as the desired position, throwing in the current encoder pulses when you sample and letting it the PID do its thing. Its crazy, holding torque at any position is stupid - the thing feels alive and fights with you. The more you try and turn it, the more you are deviating it from the setpoint and so the more the PID injects as an offset to fight with you, really is quite cool.

 

I've seen loads on how to do velocity online and people seemed to find it difficult, so I've probably done this completely wrong as all I do is keep count of the last encoder pulse sample value and sample time and then when you next sample you can work out how many pulses you are doing in a specified time frame. You use this and the desired value for the PID and it then ensures the speed is kept.

 

The encoder counter is using an interrupt timer and set within the sketch. Pretty straight forward logic but I think it might be slightly floored, or I am missing counts or something as I can't quite yet get the thing to 0 at the same point repeatably. At fully chat its doing about 170KHz encoder pulses though so not bad going. The default PID values I'm using (20,400,0.2) are interesting but work well. The velocity goes down to about 20RPM smoothly, 10RPM it does but not happily - 4000RPM is the max it will do at 9V and it appears the encoder counting is keeping up (I had a poor(er) implementation before and when it started missing counts behavior went haywire). Pretty good dynamic range I think. Dialing in a position is great - snaps to it with no hesitation although need to work out why the lack of really precise repeatability. 

 

Next things to do are to try and work out how to do a position+velocity move which is proving a head scratcher. Then I think its time to tidy it up and put it into something a bit more manageable and start thinking about wrapping it with some stuff to make it useful for actual telescope mount control.

 

Anyway, I must say apologies for such a rambling first post. It kind of befits my enthusiasm for this little thing, so nice to be back in the thick of a bit of hardware with wires hanging out of it and led's flashing away and the like.

 

If anyone is genuinely interested in progress with this let me know and I'll keep this updated. 

 

PS: The sketch probably won't build! It requires the libraries mentioned above, but I did modify them a bit so some of the variables will have type changes. I could package the whole lot up as a zip but I honestly don't know where I'd stand with GPL licenses and the like so any problems shout and I can explain what I did.

 

Hopefully there are some useful scraps in there for others to use if nothing else :-)

 

... UPDATE: I am not permitted to upload pde files, which is fine (not grumbling - just noticed the error when about to hit submit). I'll still post this so if anyone is interested for the sketch or the encoder counting code or whatnot let me know I guess I am allowed to post code snippets of my own in posts?

 

Thanks all!

Chris

 

Link to comment
Share on other sites

If you want to share your code I would suggest github. You could copy and paste it as a code block, but I assume it's a little long for that. Can I move this topic into the project vault? The technical forum is intended for support related questions. Sounds like a cool project!

Link to comment
Share on other sites

I'm curious about the PID control. I haven't gotten to take a look at it, but from what I remember of implementing PID in college, I think you might be able to take a positive output from PID and then subtracting/shifting down to the scale you need...but again, it's been a bit since I learned it.

Link to comment
Share on other sites

JColvin, thanks for the tip. I've cracked the negative encoder position problem - there wasn't much too it in the end other than my own poor knowledge, it seems the PID algorithm is happy enough being fed negative setpoints, and since I have the funny way of driving the H-Bridge, the < midway part of the scale drives the motor in reverse so negative position achieved :)

 

RPM haven't a clue on so I will consider your tip! Though I've become somewhat distracted by a new technique after playing (which I'm sure isn't new at all, just I'm new stumbling on it). The motor has tremendous holding torque, but still not great reaction times to friction etc when in velocity mode especially at the slower speeds. Thus, I've been playing with just driving the motor in a sort of step/direction fashion. 

 

As a test I have a small loop, motor is first positioned at 0, PID setpoint at 0 and then I simply loop round incrementing the setpoint by 1 each time and feeding in the current encoder position which makes the PID ensure the motor stays set to new position. This kind of turns the DC motor into a 2048 steps per rev stepper (with my 512CPT encoder anyway) with seemingly loads of torque, and a top end of 4000rpm and a bottom end down to about 6RPM still with loads of torque :-D

 

That is about as low as it will go with current PID values where each step request results in actually one single step rather than a stall/jump/stall/jump sort of motion (we're talking 5-6 encoder pulse jumps but still, precision and all). The motor does get really warm though, the sort of warm that makes me think its not going to last all too long!

 

Fun fun fun

Link to comment
Share on other sites

I don't know how you're working your encoder in software to get feedback, but if you are taking an average over a set amount of time, it makes sense that a slower speeds you'll receive less frequent inputs resulting in a slower respond to friction and other terrain changes. PID control also tends to have a tradeoff between how quickly it responds to any oscillations that it might have; there is a sweet spot, but it'll be based on whatever you prefer. 

 

Yeah, motors don't usually try to run at full power the whole time, much like car motors, so I wouldn't count on it lasting its full lifetime either.  Oh well.

 

This definitely sounds like a fun project though!

Link to comment
Share on other sites

Effectively yes, I sort of normalise the tics like this (PS probably been a bit greedy on brackets):

 

  else if(motorDriveMethod == VELOCITY)
  {
    //If motor is being driven at a velocity, then to be "user friendly" convert into RPM
    calculatedMotorRPM = (((currentEncoderCount - lastEncoderCount)/(currentReadTime - lastReadTime))*6000000) / 2048;
    //Keep a track of the last encoder count, and when that count was taken. Then you can  use 
    //the current encoder count and time to work out how many counts occur. This then becomes 
    //the input (current RPM) to the PID that it attempts to match with its setpoint (desired RPM). 
    Input = smoothRPM(calculatedMotorRPM, 0.2, Input);
  }
 
It is odd though, when in "position" mode, the slightest deviation and the PID sorts it out and that is only a deviation of a few counts whereas RPM can go from 300 to nothing (I grab the shaft) and the PID is very slow to react (> 2 seconds). I need to dig into the library for a better understanding of what PID is actually doing I think!
 
Worked out the problem with repeatability too: noise :( Causing an undershoot in position as the noise makes it think it has more pulses than it really does. I now double sample the pulses and will only adjust the pulse counter if I see the same state for two samples in a row and position repeatability is now spot on, and its smoothed the lower RPM side of things to be useable < 1RPM but it really limits the max RPM. There might be a hardware solution with some pulldowns and capacitors which would be better.
 
It certainly is fun, at least until the motor burns out :-D
Link to comment
Share on other sites

Small update:

 

Think I've resolved the problem with the encoder pulses. Unfortunately, in a way it is resolved by driving the encoder from the 5V rail rather than 3.3V as output from the encoder headers. No big deal just makes a solution a little less neat - given the vast majority of encoders run 5V+, having a jumper that allowed you to route VIN to the encoder +ve would have made things more flexible but no great dramas.

 

Below is the current setup, all pretty simple:

 

post-517-0-83707700-1424802446_thumb.jpg

 

It was getting quite tedious using a serial terminal to drive the thing so below is a screen shot of a small C# app I wrote to make life a bit easier. Also added a nifty auto scaling graph plotting the encoder pulses which when moving really small distances (thus scale is very "zoomed in") you can really see the effect of the PID tuning values (in this case, showing some over aggressive overshoot). Pretty nifty I thought :-)

 

post-517-0-56819000-1424802439_thumb.png

 

That's it for now, I'm still juts having fun playing and exploring so just test apps and whatnot for now, soon it may get a bit more serious and become something useful!

Link to comment
Share on other sites

  • 2 weeks later...

So this is due a bit of an update since things have taken a slightly different turn as the project advances.

 

Although it does work really well, it was proving really difficult to use the SoftPWMServo library in the end as more "features" were added and needing to only control two motors I decided to use two of the timers and the PWM provided by the PIC. I also ditched the odd ball way of driving the H-Bridge and am now using the standard way of doing things (PWM to enable pin, Direction pin normal). The motor shield has the two motors hooked up to OC1 and OC2 anyway so a little bit of searching resulted in the following code (heavily chopped, but the important bits there):

 

******************************************
  #include <plib.h>
 
  void setup()
  {
  //Configure the pins as outputs
  pinMode(3, OUTPUT);        
  pinMode(4, OUTPUT); 
  pinMode(5, OUTPUT);        
  pinMode(34, OUTPUT); 
  
  //Configure the PWM for Motor 1 using TIMER2 and OC1
  T2CONCLR = T2_ON;                // Turn the timer off
  T2CON = T2_PS_1_1;                // Set prescaler to 1:1 (80Mhz)
  TMR2 = 0;                                   // Clear the counter
  PR2 = 4096;                                // Set the period (12-Bit, Roughly 19.5KHz with the 1:1 prescaler)
  T2CONSET = T2_ON;                // Turn the timer on
  OC1R = 0;                                   // Load initial value into duty cycle register
  OC1RS = 0;                                 // When a period finishes the contents of OC1RS is loaded to OC1R
  OC1CON = OC_TIMER2_SRC | OC_PWM_FAULT_PIN_DISABLE;    // Set Timer2 as source | enable pwm mode without fault protection
  OC1CONSET = OC_ON;            // Set everything going
 
  //Configure the PWM for Motor 2 using TIMER3 and OC2
  T3CONCLR = T3_ON;               // Turn the timer off
  T3CON = T3_PS_1_1;               // Set prescaler to 1:1 (80Mhz)
  TMR3 = 0;                                  // Clear the counter
  PR3 = 4096;                               // Set the period (12-Bit, Roughly 19.5KHz with the 1:1 prescaler)
  T3CONSET = T2_ON;                // Turn the timer on
  OC2R = 0;                                  // Load initial value into duty cycle register
  OC2RS = 0;                                // When a period finishes the contents of OC2RS is loaded to OC2R
  OC2CON = OC_TIMER3_SRC | OC_PWM_FAULT_PIN_DISABLE; // Set Timer3 as source | enable pwm mode without fault protection
  OC2CONSET = OC_ON;           // Set everything going
  }

 

********************************************

 

Also make sure jumpers JP1 and JP2 separate out the two motors (ie motor 2 is driven by pins 5 and 34) and the above should give you at least the standard PWM control for two DC motors using the motor shield. 

 

In loop() you can play about with incrementing OC1RS (to adjust motor 1 speed) or OC2RS (to adjust motor 2 speed). Playing with these values is (apparently) better than OC1R and OC2R as you then "work with" the way the timers work - pre-loading a value to be picked up on next timer timeout rather than shoving the new value in its face and yelling "take that".

 

Understanding the timers did help clear up some misconceptions I had. You cannot run a 16-Bit PWM at 40KHz, there is not enough resolution at 80Mhz to be able to do this.

 

Say for example, we have 80,000,000 processor tics in which to fit our PWM. If we wanta 16- Bit PWM  resolution then we need 65535 processor tics. So 80,000,00065535 = 1220 (ish). So at 16-Bit resolution and a processor running at 80Mhz we can only get 1.22Khz PWM frequency. Hence some compromise is needed, we need to accept less resolution so that we need less processor tics for that resolution allowing the PWM to run faster.

 

The above sketch code puts the PWM resolution at 12-Bit. This is the maximum resolution you can really achieve when using the 1:1 prescaler and keep the PWM frequency inaudible through the motor (around 19.5KHz) - some may be able to hear this, but I'm deaf enough for it not to bother me.

 

For the PID I now set limits of -4096 to +4096. When I see a negative value, I invert it so that OCXRS is always fed a positive value and also change the direction pin state to reverse the motor. I have a feeling I am pushing my luck and will soon run out of resource, but now I've found them, I am using timers all over the place! Timer2 and Timer3 provide PWM for the motors. I have an encoder counter ISR and a PID compute ISR attached to the core timer interrupt and am using Timer4 and Timer5 to precisely "step" the motors. Loop() just handles serial stuff really. I am probably maxed out now, but it is proving to have made quite a difference to the way it is all behaving and feels like it is working a little bit better all round.

 

I will put a proper sketch together for people which might be useful, the motorshield is pretty fun to play with and now using timers most of the code is within a sketch which should make for a reasonable example to get going with however the above allows someone to just have a bit of a mess around for little effort :)

 

Cheers

Link to comment
Share on other sites

Whilst I think of it, another useful snippet perhaps below. I found digitalWrite really slow to change the motor direction pin state when "reacting" to negative values returned from PID which made the response to input (my grubby fingers twisting at the motor shaft) sluggish.

 

Thus the below was done based on similar techniques in the SoftPWMServo library in the way it performs fast set/clear's of pins:

 

******************************************************

volatile uint32_t  *SetMotor1Direction;  // Pointer to port write for motor direction
uint32_t Direction1PinPort;                   // Port number for motor direction pin
uint32_t Direction1PinBit;                     // Bit of port that the motor direction pin is
 

void setup()

{

Direction1PinPort = digitalPinToPort(4);
Direction1PinBit = digitalPinToBitMask(4);  
SetMotor1Direction = portOutputRegister(Direction1PinPort);
}
 
//Output1 is the PID output (error correction). Method would be called
//from inside loop() or similar 
void moveMotor1(double Output1)
{
  if(Output1 < 0)
  {
    Output1 *= -1;
 
    //What we are doing here is doing a bitwise AND on the port our direction
    //pin is on with the bitwise NOT bitmask of the pin. This clears the pins state
    //example bitmask is 00001000, when we bitwise NOT it we get 11110111
    //when we then bitwise AND this with the current state of the port, we clear 
    //the direction pin state
 
    *SetMotor1Direction &= ~Direction1PinBit;
  }
  else
  {
    //this is simply a bitwise OR of the bitmask for the direction pin with the
    //current state of the port to set the pin
 
    *SetMotor1Direction |= Direction1PinBit;   
  }
 
  OC1RS = Output1;
}
 
*************************************************************
 
Apologies if the comments are over the top. I like to be a bit descriptive for my own sake when I come back to it and wonder what I was doing. It may be some of the above code can be optimised and tidied up, not sure if the pointer bit is needed so take it with a grain of salt. It is however snappy quick and works well.
 
Cheers
Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...