|
/*EXPERIMENTAL Savitzky-Golay version (12thJuly09) of the previous 14 June 09 version
with 0.999 multiplier in the monowheel. This works pretty well and seems much more responsive. Needs some outdoor testing when it stops raining. In previous version of the code I used an averaging filter: I sampled the gyro and accel inputs 20 times in quick succession and then took the mean of each. This value was then fed into the PID algorithm with each loop of the program.
The aim was to remove random noise, particularly in the accelerometer signal. However, I have recently seen that there is a filter called the Savitzky-Golay filter that will take several sample points and weight each one according to an algorithm. This removes noise but preserves more of the underlying information compared to a moving average method.
Another useful feature is that my program now loops much faster at 156 times per second.
I have used this SG filter on the noisy accelerometer information only.
The gyro is sampled now 7 times in quick succession with each loop of the program, the mean taken and this mean is fed into the PID. I cannot use a rolling mean or the SG variation for the gyro as we need the PID to respond instantly to gyro changes.
For more information on this filter:
The TOBB (Totally overengineered balancing bot) robot pages where I first saw it used: Click Here
Description of how it works (from a stocks and shares perspective!): Click Here
Really good explanation which also gives you a neat algorithm for doing the calculation: Click Here
Another really good article on the subject with graphs that explain what happens: Click Here
Working features:
#include <avr/io.h>
#define CLOCK_SPEED 16000000
#define OCR1_MAX 1023 typedef unsigned char u8; void set_motor_idle(void); void InitPorts(void); float level=0;
float Throttle_pedal; float aa;
float accelraw;
float x_acc; float accsum; float x_accdeg; float gyrosum; float gangleratedeg; float gangleraterads; float ti = 2.2; float overallgain; float gaincontrol; float batteryvolts = 24; float gyroangledt;
float angle; float anglerads; float balance_torque; float softstart; float cur_speed; float cycle_time = 0.0064; float Balance_point; float a0, a1, a2, a3, a4, a5, a6;//Savitzky-Golay variables for accelerometer
int i; int j; int tipstart; void InitPorts(void)
{ PORTC=0x00; //Port C pullups set to low (no output voltage) to begin with
DDRC=0xFF; //Port C pins all set as output via the port C direction register //PORTC |= (1<<PC1); //make C1 +ve so disables OSMC during startup DDRA=0x00; //all port A pins set as input PORTA=0x00; //Port A input pullups set to low pullups DDRD=0xFF; //Configure all port D pins as output as prerequisite for OCR1A (PinD5) and OCR1B (Pin D4) working properly PORTB=0x00; //Port B pullups set to low (no output voltage) to begin with DDRB=0xFF; //All port B pins set to output } /*
IO: I am using ATMega32 16MHz with external crystal clock. New planned pin arrangement to OSMC motor controller
PC4 Onboard LED PD5/OC1A ALI -> OSMC pin 6 PD4/OC1B BLI -> OSMC pin 8 PC1 Disable -> OSMC pin 4
PC2 BHI -> OSMC pin 7 PC3 AHI -> OSMC pin 5 PA6/ADC6 Vbatt/10 -> OSMC pin 3
PA1/ADC1 pitch rate gyro PA0/ADC0 accelerometer */ void adc_init(void) {
/* turn off analogue comparator as we don't use it */ ACSR = (1 << ACD); /* select PA0 */
ADMUX = 0; ADMUX |=(1<<REFS0); //This tells it to use VCC (approx 5V) as the reference voltage NOT the default which is the internal 2.5V reference /* Set ADC prescaler to 128, enable ADC, and start conversion */
ADCSRA = 0 | (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0) | (1<<ADEN) //enable ADC | (1<<ADSC); //start first conversion /* wait until bogus first conversion finished */
while (ADCSRA & (1 << ADSC)) { } } uint16_t adc_read(uint8_t channel) { /* select channel */
ADMUX = channel; ADMUX |=(1<<REFS0); //here it is again /* start conversion */
ADCSRA |= (1 << ADSC); /* wait until conversion finished */
while (ADCSRA & (1 << ADSC)) {
} /* return the result */
return ADCW; } /* 156 cycles per sec, 6.4ms per cycle MEASURED ON OSCILLOSCOPE*/ /* read all the ADC inputs and do some conversion */
void sample_inputs(void) { uint16_t adc0, adc1, adc2, adc3, adc4, adc5; gyrosum=0; adc0 = adc_read(0); /* accelerometer pin PA0 */
accelraw = (float) adc0; for (j=0; j<7; j++) { adc1 = adc_read(1); //gyro pin PA1 gyrosum = (float) gyrosum + adc1; //using a mean of 7 samples per loop for the gyro so it gets a complete update with each loop of the program } adc2 = adc_read(2); /* grey wire overallgain (via cutout switch) position PA2*/ adc3 = adc_read(3); /* Position lever pulled back position PA3*/ adc4 = adc_read(4); /* Throttle_pedal position PA4*/ adc5 = adc_read(5); /* Position lever pushed forwards position PA5*/ //adc6 = adc_read(6); /* Vbatt input from OSMC (not used at present) position PA6*/ //Sav Golay filter for accel only a0 = a1; a1 = a2; a2 = a3; a3 = a4; a4 = a5; a5 = a6; a6 = (float) accelraw; accsum = (float) ((-2*a0) + (3*a1) + (6*a2) + (7*a3) + (6*a4) + (3*a5) + (-2*a6))/21; //Sav Golay calculation
gaincontrol = (float) gaincontrol*0.9 + 0.1*adc2/341; //smooths any voltage spikes and gives range 0-3 Throttle_pedal=(float) Throttle_pedal*0.9 + 0.1*adc4/341; //smooths any voltage spikes and gives range 0-3 //Cuts the motor if the dead mans button is let go //(gaincontrol variable also wired in through this button to adc2 if (adc2<100) { Throttle_pedal=0.001; gaincontrol=0.001; } overallgain = gaincontrol*softstart; //what to do if lever pulled back or pushed forwards or not doing anything:
Balance_point = 514; if (adc3>100) Balance_point=534; if (adc5>100) Balance_point=494; PORTB |= (1<<PB2);//Port B2 turned on/off once per loop so I can measure loop time with an oscilloscope /*ACCELEROMETER signal processing*/ /*Subtract offsets*/ x_acc=(float) accsum - Balance_point; //accsum is SG value for accelerometer, not a true "sum" so no need to divide by 7 if (x_acc<-250) x_acc=-250; //cap accel values to a range of -250 to +250 (80 degree tilt each way) if (x_acc>250) x_acc=250; /* Accelerometer angle change is about 3.45 units per degree tilt in range 0-30 degrees(sin theta)
Convert tilt to degrees of tilt from accelerometer sensor. Sin angle roughly = angle for small angles so no need to do trigonometry. x_acc below is now in DEGREES*/ x_accdeg= (float) x_acc/-3.45; //The minus sign corrects for a back to front accelerometer mounting! /*GYRO signal processing*/
/*Subtract offsets: Sensor reading is 0-1024 so "balance point" i.e. my required zero point will be that reading minus 512*/ /*Gyro angle change of 20mV per deg per sec from datasheet gives change of 4.096 units (on the scale of 0 - 1023) per degree per sec angle change This limits the rate of change of gyro angle to just less than the maximum rate it is actually capable of measuring (100deg/sec). Note all these fractions are rounded up to an integer later just before it is sent to the PWM generator which in turn is connected to the motor controller*/ gangleratedeg=(float)((gyrosum/7) - 508)/4.096; //gyrosum is a sum of a group of 7 samples so divide by 7 for gyro value
if (gangleratedeg < -92) gangleratedeg=-92;
if (gangleratedeg >92) gangleratedeg=92 /*I turn port B2 on and off once per main program cycle so I can attach an oscilloscope to it and work out the program cycle time
aa=0.005; /*new angle in DEGREES is old angle plus change in angle from gyro since last cycle with little bit of new accel reading factored in*/
angle = (float)((1-aa) * (angle+gyroangledt)) + (aa * x_accdeg); //the main angle calculating function*/ //Convert angle from degrees to radians
anglerads=(float)angle*0.017453; balance_torque=(float)(4.5*anglerads) + (0.5*gangleraterads);
cur_speed = (float)(cur_speed + (Throttle_pedal * balance_torque * cycle_time)) * 0.999; /*The level value is from -1 to +1 and represents the duty cycle to be sent to the motor. Converting to radians helps us stay within these limits
level = (balance_torque + cur_speed) * overallgain;
}
/* Configure timer and set up the output pins OC1A(Pin PD5 on my micro) and OC1B(Pin PD4 on my micro) as phase-correct PWM channels
Note: Some strongly feel that locked-antiphase is the way to go as get regenerative braking and good control around mid-balance point Downside is that you can get a lot more noise and voltage spikes in system but these can be smoothed out with filters Others are far more expert on this than I am so need to look into this for yourself but this is my understanding. My aim is to start with phase-correct as I just about understand it and others have used it OK, then develop from there*/ void timer_init() { TCCR0 = 0 |
(1<<WGM11) | (1<<WGM10); //OCR1_Max is 1023 so these are set like this
int16_t leveli = (int16_t)(level*1023); //NOTE here we take the floating point value we have ended up with for "level", we multiply it by 1023 and then make it into an integer before feeding the value into the PWM generator as "leveli" if (leveli<-1020) leveli=-1020;//double-checks we are within sensible PWM limits as do not want to suddenly be thrown off the board
if (level<-0.7 || level>0.7) { softstart = (float) softstart+0.001;
adc_init(); timer_init();
while (tipstart<1){ // you need this to allow the SG filter to wind up to the proper stable value when you first turn machine on, before looking at the value of accsum (below).
for (i=0; i<20; i++) { sample_inputs(); }
if (accsum<504 || accsum>524) { angle=0;
set_motor(); }
} |