|
|
View previous topic :: View next topic |
Author |
Message |
totalnoob
Joined: 22 Oct 2006 Posts: 24
|
Problems with line following response time. |
Posted: Sun Nov 26, 2006 5:20 pm |
|
|
I'm building a line following robot using a Devantech MD23 i2c motor controller and I'm having a lot of trouble with the implementation of my code. I use external circuitry to find an error signal so I know the error signal is fine. Where I have trouble is updating the motor controller with the correct speed values after computing the neccessary turn from my error signal. It feels like the motor controller isn't keeping up, the robot only corrects itself once it's completely off the line in either direction so what I end up with is a robot that just oscillates around the line. I don't know if it is because i2c is too slow for what I'm trying to do or if my code is sloppy and takes too long to process. Any assistance would be great. Here's the main part of the code I'm using, please let me know if there is a better way to do what i'm trying to do because this really isn't working like I feel it should.
Code: |
#include <16F877A.h>
#device ADC=8
#include <string.h>
#include <LCD.h>
#include <Motor2.h>
#use delay(clock=20000000)
#fuses HS,WDT,BROWNOUT,PUT,NOLVP
//set up i2c peripheral and use hardware SSP
#use i2c(Master,sda=PIN_C4,scl=PIN_C3,RESTART_WDT,FORCE_HW)
#BYTE ADCON0 = 0x85
unsigned char pos_or_neg, Error, K, speed, speed_left, speed_right;
float Turn;
#int_AD
AD_isr()
{
Error = read_adc(); //Reads Error voltage
set_adc_channel(0); //Read ADC from AN0
ADCON0 |= 0x4; //Restart ADC
}
void find_error(void)
{
//If Error Signal is outside of threshold use bang-bang controller
if (Error > 60)
{
pos_or_neg = input(PIN_A1); //Check for positive or negative
if (pos_or_neg==TRUE)
{ //If positive Turn Right
Turn = 15;
speed_left = speed - Turn;
speed_right = speed;
}
else
{ //If negative Turn Left
Turn = 15;
speed_right = speed - Turn;
speed_left = speed;
}
}
//Else use proportional controller if inside threshold
else
{
pos_or_neg = input(PIN_A1); //Check for positive or negative
if (pos_or_neg==TRUE)
{
Turn = Error / K;
speed_left = speed - Turn;
speed_right = speed;
}
else
{
Turn = Error / K;
speed_right = speed - Turn;
speed_left = speed;
}
}
set_speed(speed_left,speed_right); //writes updated speed to motors
}
void main()
{
startup_LCD();
init_motors();
K = 15; //Proportional Gain K
speed = 255;
setup_adc_ports(RA0_ANALOG);
setup_adc(ADC_CLOCK_DIV_32);
setup_counters(RTCC_INTERNAL,WDT_2304MS);
enable_interrupts(INT_AD);
enable_interrupts(global);
set_adc_channel(0);
ADCON0 |= 0x4;
set_speed(speed,speed);
while(1)
{
find_error();
restart_wdt(); //use WDT in case i2c 'hangs up' due to faulty slave
}
}
|
|
|
|
jma_1
Joined: 08 Feb 2005 Posts: 147 Location: Wisconsin
|
|
Posted: Sun Nov 26, 2006 6:47 pm |
|
|
Greetings,
Your description is very 'bare bones'.
Questions:
treads / tracks for left and right?
input(PIN_AI) switch? window comparator?
what is the device producing the error voltage?
is the device producing the error 'centered' / calibrated?
Things to examine:
what is the resolution of the device producing the error voltage? (feedback potentiometer or voltage follower w/ offset voltage)
what is the timing of your while(1) loop? (1..15 mSec?) Is the timing consistant?
The structure of your application is interesting. It combines both elements of fuzzy logic and pid. It sounds like you are just having a tuning issue.
General suggestion:
Verify all the values your software is calculating and sending are correct.
Tuning suggestions:
Try to make the adjustments either more quickly, slowly, or decreasing/increasing the gains (and/or smooth out your transition when switching gains point). Also, the resolution of your system might be an issue. Without specifics, there is no way from your description to tell. Adjusting the rate of travel of opposite treads will produce large amounts of change. Have you tried merely holdling one wheel constant and adjusting the opposing wheel? This potentially would be less aggressive, but more stable. Try changing the error level at which you switch gains. Increase the resolution of system. Try to find where your correction first starts occuring and what physically happens (both to initially correct the error and the subsequent oscillation). Is there any dead band around your set point / target? Try putting a dead band around your target where you do not make any corrections. Place additional logic which detects if you are closing on your target -> potentially lessen correction or maintain the correction level.
Alternate approaches:
Change to a PID algorithm and change the A/D to be sampled at a fixed interval (polling w/ each main loop iteration). I'm not sure if the interrupt A/D will provide you the consistant timing which PID requires. Providing a means for a proportional amount of correction seems the right direction for your application. I would start with a P and/or PI controller (and finally adding the derivative term if need be). Perhaps adding fuzzy logic like rules to the PID would be appropriate (limiting integrator; allowing integrator to run only when the error is withing a certain band -> more resembling a 'P' control, but integrates error to zero). Listed below are a few links for PID tutorials.
http://www.barello.net/Papers/Motion_Control/index.htm
http://www.ctc-control.com/customer/elearning/servotut/pid.asp
http://www.tcnj.edu/~rgraham/PID-tuning.html
Cheers,
JMA |
|
|
jma_1
Joined: 08 Feb 2005 Posts: 147 Location: Wisconsin
|
|
Posted: Sun Nov 26, 2006 7:37 pm |
|
|
Greetings totalnoob,
I made a mistake in my earlier posting. I failed to notice you keep one tread / wheel constant and vary the other one.
Are your values correct for your speed correction factor? If there is large error, you adjust the speed from 255 to 240 while keeping the other speed at 255. If the error is <= 60 you vary the speed over 187 to 251 depending on the error and keep the other speed at 255. When your robot follower oscillates, is the error large or small (or both)? If the error is large, your corrections appear to be slower? Do you ever set the speeds the same? This might be a clue as to how to tune the system.
Can you increase the resolution of any of your control variables above 8 bits? What is the speed? (pwm, or ?). Any chance of 10 bit A/D? If you use the PID method which I suggested, try and pick your variable size (and possibly use bit shifts) to get the maximum resolution from your system.
What is the time lag between setting a pwm frequency and the motor reaching the desired speed (incorporating mass of the robot and the instantaneous speed)? Perhaps the lag in the motor is causing you to over-compensate in your control. If the motor lag is huge, increasing the time of your while loop (busy wait until enough time has passed; slow your control down) might help stabilize things. Can you modify the hardware to use a hall effect or variable reluctance pickup on a gear? With the pickup in place, you can physically measure the frequency of each tread/wheel.
General comment: consider using the Hungarian notation and possibly the 'static' identifier to make your code easier to read and follow
Cheers,
JMA |
|
|
totalnoob
Joined: 22 Oct 2006 Posts: 24
|
|
Posted: Sun Nov 26, 2006 8:14 pm |
|
|
jma_1,
Thanks for your reply. To answer a few of your questions:
Wheels are smooth rubber.
input(PIN_A1) is a logic input from an analog circuit that indicates whether the error signal is positive or negative i.e. off to the left or right side of the line.
Device producing the error voltage is an array of sensors put through an analog circuit to determine the difference between the right side and left side.
The error is calibrated to be 0 when the robot is centered on the line and then increases to 5V when it is off the line on either side.
About changing to 10 bit A/D I left it in 8 bit because I have to feed an 8 bit number to my motor controller through i2c to control my speed and turn. I don't send a PWM frequency I just specify an 8-bit value 0 (Full Reverse) 128 (Stop) 255 (Full Forward)
As far as speed goes 255 was the value I had just used but it wasn't the value I had good results at. I'm only able to run at around 170 which is about 56rpm (very slow). When it goes off the line and begins the bang-bang control action I slow one wheel down to around 36rpm to turn the robot.
The acceleration of the controller is set to it's highest so it will increase the speed from a full stop to full forward in about .65ms
I think the resolution on my error signal is fine, I'm able to read it in and display it on an LCD and I can watch it go from low to high and it accurately displays where the robot is located over the line. Where I see a problem is in the motor controllers response. It feels like the motor controller will only correct once the error has gotten to be a large value. I don't ever see any subtle or minor corrections. |
|
|
jma_1
Joined: 08 Feb 2005 Posts: 147 Location: Wisconsin
|
|
Posted: Sun Nov 26, 2006 8:51 pm |
|
|
Greetings,
When you say the error increases to 5V on either side of the line, do you mean makes a step change (0 to 5V) or linear voltage swing based on position? If the error is only digital, good luck. I'm assuming the error is a varying analog voltage (0 to 5V).
I would use RS232 or your LCD display to verify the error goes over the range you think it should and the direction input works correctly. I personally do not like using another person's hardware. It might not always be obvious if they incorporated any other functionality (hysteresis, etc).
As you point out, you don't think small corrections have any change. Verifying this is crucial. Capture the data your application is seeing over RS232 and verfiy the behavior (error, direction, left sp, right sp. Please be aware the printf statements will slow your while() loop iterations down. Specifically look at the speed and error. Do your corrections change the error? This should help you figure out how to correct this (if error increasing, speed correction applied and nothing -> change correction factor or when it as applied; if error increasing and correction applied with too large of change -> change correction factor or when it is applied).
It sounds like the sensor array you are using is an off the shelf product. Do they have sample applications? How does your processor fit with the sensor array? Verfiy the wiring and connections are correct.
I have a bad feeling the hardware might not be responding as you think it should. Also, try slowing your loop time down (hard delay or wait for a timer to expire). You might not be giving your correction enough time to affect the robot's position. Perhaps also slow the robot down. If you know the rpm use this in your measurement / correction.
Cheers,
JMA |
|
|
Ttelmah Guest
|
|
Posted: Mon Nov 27, 2006 4:19 am |
|
|
First comment. Get rid of your ADC code!...
This is probably the core of the problem.
Basically, the 'read_adc command, used without any variables, will trigger an ADC conversion, and wait for it to complete, then read it. So as it currently stands, your ADC code, triggers the ADC, waits for it to complete, reads it, then triggeres it again, and exits the routine. Now your ADC is set to use the master clock/32. The ADC conversion takes eleven ADC clocks, so 88 processor instructions after the ADC is triggered, it'll interrupt again. Now caling an interrupt, takes typically about 60 instruction times, to handle the call and return saving of the registers, so your processor, is going to be spending about 80% of it's time in this hanlder. This is probably slowing the total response time so far, that the code fails...
Use this instead:
Code: |
//In the main where your current interrupt enables sit
//setup timer2, to interrupt at perhaps 2000Hz.
setup_timer_2(T2_DIV_BY_16,78,2);
//set this once only in the main. Don't add extra code to the handler
set_adc_channel(0);
//ensure ADC has had time to settle
delay_us(10);
//start an ADC conversionj
read_adc(ADC_START_ONLY);
//don't enable interrupts till the conversion has completed
delay_us(50);
enable_interrupts(int_timer2);
enable_interrupts(global);
//don't enable ADC interrupts
//Then as the interrupt handler
#int_timer2
void tick_int(void) {
Error=read_adc(ADC_READ_ONLY);
read_adc(ADC_START_ONLY);
}
|
Now, this interrupts about 2000 times a second. On each interrupt, it reads the value (note the 'ADC_READ_ONLY' command to read_adc, which makes it just return the waiting value). Then it triggers a new conversion, and returns immediately. You can adjust how often this is called, by altering the constants in the timer_2 setup. 2000*/sec, should be a good starting point, and will only use about 3% of the processor time.
The setup, selects the channel once only, and starts a conversion, then waits for it to have cmpleted, before enabling the timer, so you don't ge a sprurious value on the first read.
Best Wishes |
|
|
Guest
|
|
Posted: Mon Nov 27, 2006 8:44 am |
|
|
As another comment, looking a bit further through the code, 'Turn', will not behave as you expect. You perform 'Error/K', using _integer_ 8bit arithmetic. To perform floating point arithmetic, one or both the numbers, needs to be converted to a float _before_ the sum is performed. This is done using a 'cast'. So:
Turn=Error/K;
uses integer arithmetic.
Turn=(float)Error/K;
uses float arithmetic.
However _beware_ of using fpp arithmetic in a servo algorithm. It is _slow_, especially division. If you look at the example servo control code on the MicroChip site, you will see that they always use 'scaled integer' arithmetic instead. If (for instance), you declare:
Code: |
union access {
int16 sixteen[2];
int32 thirtytwo;
} value;
|
You can put a value into the thirty two bit register 'value.thritytwo', and then perform the arithmetic with integer operations, and access the 'high' sixteen bit 'half' of this value with 'value.sixteen[1]'.
You have to chose the scale factor of your arithmetic, so the result will be 65536* the integer value you want, with this being much faster than using fp arithmetic.
Best Wishes |
|
|
totalnoob
Joined: 22 Oct 2006 Posts: 24
|
|
Posted: Mon Nov 27, 2006 3:50 pm |
|
|
Ttelmah,
I thank you for pointing out the time it takes to run through my adc code. I'm new to CCS so I'm really not familiar with how long all the processes take. I have a question about interrupts, you say that interrupts typically take about 60 instruction cycles to complete. Is there a way for me to do this without using interrupts. If i place my adc code inside my find_error() function would my adc value be resonable up to date. The only reason why I used an interrupt was because I felt I needed to so that the adc value I'm using for calculations is always the most current. Would something like this process quickly and allow me to update faster?
Code: |
void find_error(void)
{
Error = read_adc(READ_ONLY);
pos_or_neg = input(PIN_A1);
...If-Else Statements...
set_speed(speed_left,speed_right);
read_adc(START_ONLY);
}
|
Code: | void main()
{
startup_LCD();
init_motors();
K = 5;
speed = 192;
setup_adc_ports(RA0_ANALOG);
setup_adc(ADC_CLOCK_DIV_32);
setup_counters(RTCC_INTERNAL,WDT_2304MS);
read_adc(START_ONLY);
set_speed(speed,speed);
while(1)
{
find_error();
restart_wdt();
}
}
|
Thanks again for your earlier reply. |
|
|
Ttelmah Guest
|
|
Posted: Mon Nov 27, 2006 4:23 pm |
|
|
The problem with the version you post now, is that to use the separate 'start', and 'read' operations, you need to ensure there is enough code between the 'start', and the 'read', that the conversion will have completed. As posted, there is almost nothing between the start and the read (just the restart WDT, and the 'while' loop).
Consider instead:
Code: |
void find_error(void)
{
Error = read_adc(READ_ONLY);
read_adc(ADC_START_ONLY);
pos_or_neg = input(PIN_A1);
...If-Else Statements...
set_speed(speed_left,speed_right);
}
|
Now the time taken for all the 'if' statements, is between the start, and the read, and it should work OK (though really you should add up the instruction times to make sure). You will actuially always be reading the 'last' ADC value, but given the relatively short loop time, this is not a problem.
Best Wishes |
|
|
totalnoob
Joined: 22 Oct 2006 Posts: 24
|
|
Posted: Mon Nov 27, 2006 5:21 pm |
|
|
I tried what you suggested but I'm still seeing a lag. I don't know if it's because I'm not acting on the current ADC or If it's because the motor controller is not reacting fast enough. What I'm basically trying to do is determine whether my problem is code based i.e. code too slow or whether it's hardware based i.e. controller too slow.
How would I be able to find out how long the entire process is taking. Is there some kind of table that tells me how long each instruction takes. I want to get an idea of how many times a second I should be seeing changes in my output so I can verify if the motor controller is acting on time or not. |
|
|
SherpaDoug
Joined: 07 Sep 2003 Posts: 1640 Location: Cape Cod Mass USA
|
|
Posted: Mon Nov 27, 2006 7:12 pm |
|
|
The easiest way to tell how fast your control loop is would be to toggle an output pin every time through the loop. Then you can use a scope, frequency counter, or listen to ticking in some headphones to tell how fast it is going. _________________ The search for better is endless. Instead simply find very good and get the job done. |
|
|
Ttelmah Guest
|
|
Posted: Tue Nov 28, 2006 6:27 am |
|
|
How fast can the I2C on the MD23 be driven?.
Change the I2C command to reflect this (FAST=xxxxx), where 'xxxxx' is the required rate. The I2C commands themselves are going to represent a large slice of your update time. Try 400000 and see if this works.
You use a multiple 'adjustment' rate, but make no adjustment to this for what happened before. Might be worth including a 'D' term.
There is a _very_ significant problem, with the fact that your 'error' signal, is read seperately from the 'direction' signal. This is almost impossible to deal with. If (for instance), the unit is sitting significantly left of the line, and moving right, ending up just one 'error count' right of the line, when the direction signal is read, you can have the situation that the 'error' reflects if being a significant distance from the line, when it isn't.
I'd suggest changing the method you read the signals to:
Code: |
read_adc(ADC_START_ONLY);
//The read should take 17.6uSec total
delay_us(8);
pos_or_neg=input(PIN_A1);
//the read, will delay till the conversion is complete
Error=read_adc(ADC_READ_ONLY);
|
The delay of the ADC, is a tiny period, an really does not matter for the sort of update speeds that are needed. This way the 'pos_or_neg' signal, will reflect the value in the middle of the 'reading' time, rather than the beginning or end.
Now, the update time of your loop, is so fast compared to the likely update rate of the motor control device, that I'd be looking at taking a little 'longer' over the decision what to do. Consider something like:
Code: |
int1 OldSign;
if (OldSign==pos_or_neg) {
//Here we are the same side of the line as last time
Old_sign=pos_or_neg;
//Apply adjustment just like your existing code here
}
else {
//Here we have _crossed_ the line
OldSign=pos_or_neg;
//Bring the motors to equal speed here
}
|
You might also try adjusting the motor speed _less_. At present, the code probably loops a few dozen times, in the likely update 'time' of the PWM on the MD23 controller. As such, it therefore tends to overcorrect.
You should also modify the speed change code with something like:
Code: |
Turn = 15;
if (speed_left>15) speed_left = speed - Turn;
else speed_left=0;
|
This needs to be done wherever you change the speed. Otherwise there is the potential, that the motor could be nearly stopped (with a speed of say 10), and then on the next update, would go massively too fast (since with unsigned arithmetic, 10-15 = 251!....).
The same applies when incrementing the motor speed.
Best Wishes |
|
|
|
|
You cannot post new topics in this forum You cannot reply to topics in this forum You cannot edit your posts in this forum You cannot delete your posts in this forum You cannot vote in polls in this forum
|
Powered by phpBB © 2001, 2005 phpBB Group
|