View unanswered posts | View active topics It is currently Wed Apr 25, 2018 4:37 am






Reply to topic  [ 80 posts ]  Go to page Previous  1, 2, 3, 4, 5, 6  Next
PID Control 
Author Message
Rookie

Joined: Sat Jan 31, 2015 1:14 am
Posts: 38
Post Re: PID Control
MHTS wrote:
Since you are playing with the compass on turning, the top set is not very relevant (yPidCtrl). Only the bottom set (turnPidCtrl) is interesting. The first order of business is to make sure the compass sensor actually works as expected. You may want to enter the following code to test the compass sensor. Then turn the robot around manually and check if the numbers actually follows the magnetic north closely (i.e. gives you 90 when pointing East, 180 when pointing South etc). Only then should you start debugging PID control with it. I don't quite understand your description about the behavior so you may want to describe it in more details. Since you set your target at CompassGetHeading(g_compass) + 90.0, if T is 90, it means your robot starts pointing at perfect North! I am a little suspicious of that. That's why I want you to test the compass sensor to see if it really gives you reasonable readings. For example, if the sensor doesn't really work and gives you zero all the time, that would explain why the robot is turning round and round non-stop because your E (error) is always 90.
Code:
#pragma config(Hubs,  S1, HTMotor,  HTMotor,  HTServo,  none)
#pragma config(Sensor, S2,     irSeeker,       sensorI2CCustom)
#pragma config(Sensor, S3,     htComp,         sensorI2CHiTechnicCompass)
#pragma config(Sensor, S4,     htEOPD,         sensorI2CCustom)
#pragma config(Motor,  mtr_S1_C1_1,     rightWheel,    tmotorTetrix, openLoop, encoder)
#pragma config(Motor,  mtr_S1_C1_2,     leftWheel,     tmotorTetrix, openLoop, reversed, encoder)
#pragma config(Motor,  mtr_S1_C2_1,     rightLifter,   tmotorTetrix, PIDControl, reversed)
#pragma config(Motor,  mtr_S1_C2_2,     leftLifter,    tmotorTetrix, PIDControl)
#pragma config(Servo,  srvo_S1_C3_1,    servo1,               tServoNone)
#pragma config(Servo,  srvo_S1_C3_2,    servo2,               tServoNone)
#pragma config(Servo,  srvo_S1_C3_3,    irServo,              tServoStandard)
#pragma config(Servo,  srvo_S1_C3_4,    goalHook,             tServoStandard)
#pragma config(Servo,  srvo_S1_C3_5,    leftDoor,             tServoStandard)
#pragma config(Servo,  srvo_S1_C3_6,    rightDoor,            tServoStandard)
//*!!Code automatically generated by 'ROBOTC' configuration wizard               !!*//

// include files
#include "ftclib/trcdefs.h"
#include "ftclib/dbgtrace.h"
#include "ftclib/compass.h"

COMPASS    g_compass;

task main()
{
   CompassInit(g_compass, htComp);
    while (true)
    {
        CompassTask(g_compass);
        nxtDisplayTextLine(3, "heading=%d", CompassGetHeading(g_compass));
        wait1Msec(100);
    }
}


Ok sorry for my lack of explanation I'll try to explain more later when I can get back to the robot, but last night I stayed at school until like 8pm trying to figure out the compass and eventually I decided to try calibrating using xanders calibration code, which returned with a port setup error? not sure what the problem was. After that I went to the TryMe Files on the NXT and ran the compass program and when i first turned it on it gave me a value of like 211 or something along those lines (I'll post exact numbers later) and the compass direction arrow thing was pointing NNW and as I turned it the number value went up but the Directional Arrow didn't change unless I exited the program and ran it again and it gave me an updated direction of close to W with the value at like 219 or 229...something along those lines. Strangely when I flipped it 180 it was giving the same value of 219 as it did in another direction as if it was confused or getting false north's.

In the compass program the target would go up each time I ran it...it started low at around 90 and every time I ran the program, the Target would go up increasingly, it eventually got up to a target of like 720 or something ridiculously high and wrong. The strange part was how it would be continuously turning in circles but the E I O numbers would jump almost as if it reset itself at around 320. Another strange thing was how the upper set was all zeroed out except for the I and the lower set of numbers, the T would be stuck at a target value (some number like 90 and eventually worked up to 700 each time I ran ran) throughout the time I ran the program, and I believe I would always be stuck 90 the entire program with E changing depending on where it was while turning and O was at 45 I believe because of KP. To me, it almost seemed as if the Top I and the Bottom I were switched or something somehow? Because the Top I was changing like the bottom one should. I tried adding in a line at the top of "task main()" but it didn't seem to help at all.

CompassCalibration (I had to put random numbers in the wheel diameter and distance between wheels in mm because the real values weren't turning the 360 properly)

Code:
#pragma config(Hubs,  S1, HTMotor,  HTMotor,  HTServo,  none)
#pragma config(Sensor, S1,     ,               sensorI2CMuxController)
#pragma config(Sensor, S2,     irSeeker,       sensorI2CCustom)
#pragma config(Sensor, S3,     htComp,         sensorI2CHiTechnicCompass)
#pragma config(Sensor, S4,     htEOPD,         sensorI2CCustom)
#pragma config(Motor,  mtr_S1_C1_1,     rightWheel,    tmotorTetrix, openLoop, encoder)
#pragma config(Motor,  mtr_S1_C1_2,     leftWheel,     tmotorTetrix, openLoop, reversed, encoder)
#pragma config(Motor,  mtr_S1_C2_1,     rightLifter,   tmotorTetrix, PIDControl, reversed)
#pragma config(Motor,  mtr_S1_C2_2,     leftLifter,    tmotorTetrix, PIDControl)
#pragma config(Servo,  srvo_S1_C3_1,    servo1,               tServoNone)
#pragma config(Servo,  srvo_S1_C3_2,    servo2,               tServoNone)
#pragma config(Servo,  srvo_S1_C3_3,    irServo,              tServoStandard)
#pragma config(Servo,  srvo_S1_C3_4,    goalHook,             tServoStandard)
#pragma config(Servo,  srvo_S1_C3_5,    leftDoor,             tServoStandard)
#pragma config(Servo,  srvo_S1_C3_6,    rightDoor,            tServoStandard)
//*!!Code automatically generated by 'ROBOTC' configuration wizard               !!*//
 
/*
 * The code here is made to work with the standard NXT Tribot.  You will need to edit
 * the WHEELDIST, WHEELSIZE and MOTORSPEED figures to make it work with your robot.
 *
 * Remeber that the robot shouldn't spin more than 360 degrees per 20 seconds.  Also
 * make sure it spins a bit more than 360, perhaps 1 and 1/4 or 1 and 1/2.
 */
 
#include "RobotCDrivers/drivers/hitechnic-compass.h"
 
// These measurements are in milimeters.
#define WHEELDIST 330  // distance between the wheels
#define WHEELSIZE 40   // diameter of the wheels
 
#define MOTORSPEED 35   // speed at which motors should turn
 
// Lets you know when 20 seconds is over, can help with setting up
// the initial timing and motor speed.
task timeMe() {
  wait1Msec(20000);
  PlaySound(soundBeepBeep);
  while(bSoundActive) EndTimeSlice();
}
 
// Pulse a big "*" at the bottom of the screen to show that it's
// doing something.
task showPulse() {
  while (true) {
        nxtDisplayCenteredBigTextLine(6, " ");
        wait1Msec(400);
        nxtDisplayCenteredBigTextLine(6, "*");
        wait1Msec(400);
  }
}
 
 
// Does some voodoo maths to calculate how many times the wheels should rotate
// to make the robot spin about 360 degrees.
int numRotations() {
  return ((WHEELDIST * 3142) / 1000) / ((WHEELSIZE * 3142) / 1000);
}
 
// Start the calibration and complain loudly if something goes wrong
void startCalibration() {
  if (!HTMCstartCal(htComp)) {
    eraseDisplay();
    displayTextLine(1, "ERROR: Couldn't");
    displayTextLine(2, "calibrate sensor.");
    displayTextLine(4, "Check connection");
    displayTextLine(5, "and try again.");
    PlaySound(soundException);
    while(bSoundActive) EndTimeSlice();
    wait1Msec(5000);
    StopAllTasks();
  }
}
 
// Stop the calibration and complain loudly if something goes wrong
void stopCalibration() {
  if (!HTMCstopCal(htComp)) {
    eraseDisplay();
    nxtDisplayTextLine(1, "ERROR: Calibration");
    nxtDisplayTextLine(2, "has failed.");
    nxtDisplayTextLine(4, "Check connection");
    nxtDisplayTextLine(5, "and try again.");
    PlaySound(soundException);
    while(bSoundActive) EndTimeSlice();
    wait1Msec(5000);
    StopAllTasks();
  } else {
    nxtDisplayTextLine(1, "SUCCESS: ");
    nxtDisplayTextLine(2, "Calibr. done.");
    PlaySound(soundUpwardTones);
    while(bSoundActive) EndTimeSlice();
    wait1Msec(5000);
  }
}
 
task main () {
  bFloatDuringInactiveMotorPWM = true;
  int numDegrees = 0;
  nxtDisplayCenteredTextLine(0, "HiTechnic");
  nxtDisplayCenteredBigTextLine(1, "Compass");
  nxtDisplayCenteredTextLine(3, "Test 2");
 
 
  nMotorEncoder[rightWheel] = 0;
  nMotorEncoder[leftWheel] = 0;
  // This will make the robot spin about 1.5 times, depends on many factors, YYMV, etc
  numDegrees = ((numRotations() * 3) / 2) * 450;
  StartTask(timeMe);
  startCalibration();
  nxtDisplayCenteredTextLine(5, "Calibrating...");
  StartTask(showPulse);
  motor[rightWheel] = MOTORSPEED;
  motor[leftWheel] = -MOTORSPEED;
 
  while(nMotorEncoder[rightWheel] < numDegrees) wait1Msec(5);
  motor[leftWheel] = 0;
  motor[rightWheel]= 0;
  stopCalibration();
  nxtDisplayCenteredTextLine(5, "Calibration done");
  wait1Msec(5000);
}
 
/*
 * $Id: hitechnic-compass-test2.c 133 2013-03-10 15:15:38Z xander $
 */


PID_Comp

Code:
#pragma config(Hubs,  S1, HTMotor,  HTMotor,  HTServo,  none)
#pragma config(Sensor, S1,     ,               sensorI2CMuxController)
#pragma config(Sensor, S2,     irSeeker,       sensorI2CCustom)
#pragma config(Sensor, S3,     htComp,         sensorI2CHiTechnicCompass)
#pragma config(Sensor, S4,     htEOPD,         sensorI2CCustom)
#pragma config(Motor,  mtr_S1_C1_1,     rightWheel,    tmotorTetrix, openLoop, encoder)
#pragma config(Motor,  mtr_S1_C1_2,     leftWheel,     tmotorTetrix, openLoop, reversed, encoder)
#pragma config(Motor,  mtr_S1_C2_1,     rightLifter,   tmotorTetrix, PIDControl, reversed)
#pragma config(Motor,  mtr_S1_C2_2,     leftLifter,    tmotorTetrix, PIDControl)
#pragma config(Servo,  srvo_S1_C3_1,    servo1,               tServoNone)
#pragma config(Servo,  srvo_S1_C3_2,    servo2,               tServoNone)
#pragma config(Servo,  srvo_S1_C3_3,    irServo,              tServoStandard)
#pragma config(Servo,  srvo_S1_C3_4,    goalHook,             tServoStandard)
#pragma config(Servo,  srvo_S1_C3_5,    leftDoor,             tServoStandard)
#pragma config(Servo,  srvo_S1_C3_6,    rightDoor,            tServoStandard)
//*!!Code automatically generated by 'ROBOTC' configuration wizard               !!*//
 
// include files
#include "ftclib/trcdefs.h"
#include "ftclib/dbgtrace.h"
#include "ftclib/sm.h"
#include "ftclib/drive.h"
#include "ftclib/pidctrl.h"
#include "ftclib/piddrive.h"
#include "ftclib/compass.h"
 
// define distance with encoders
#define ENC_KP              1.6 // change this to meet distance target
#define ENC_KI              0.0015
#define ENC_KD              0.5
#define ENC_TOLERANCE       1.0
#define ENC_SETTLING        200
 
// define turns with encoder
#define COMP_KP             0.5 // change this to meet turn target
#define COMP_KI             0.0
#define COMP_KD             0.0
#define COMP_TOLERANCE      1.0
#define COMP_SETTLING       200
 
#define CLICKS_PER_INCH     118.5 // change this to adjust actual distance
 
#define EVTTYPE_PIDDRIVE    (EVTTYPE_NONE + 1)
 
COMPASS         g_compass;
DRIVE       g_drive;        //create the drive object.
PIDCTRL     g_yPidCtrl;     //create the Y PID controller
PIDCTRL     g_turnPidCtrl;  //create the turn PID controller
PIDDRIVE    g_pidDrive;     //create the PID drive object
SM          g_autoSM;       //create the autonomous state machine object
 
// calculate all the math
float PIDCtrlGetInput(PIDCTRL &pidCtrl)
{
    float inputValue = 0.0;
 
    if (IsSameReference(pidCtrl, g_yPidCtrl))
    {
        inputValue = (nMotorEncoder[leftWheel] + nMotorEncoder[rightWheel])/2.0/CLICKS_PER_INCH;
    }
    else if (IsSameReference(pidCtrl, g_turnPidCtrl))
    {
        inputValue = CompassGetHeading(g_compass);
    }
    return inputValue;
}
 
task main()
{
    CompassReset(g_compass);
    CompassInit(g_compass, htComp);
    // Initialize the drive object.
    DriveInit(g_drive, leftWheel, rightWheel);
    // Initialize the yPidCtrl object.
    PIDCtrlInit(g_yPidCtrl,
                            ENC_KP, ENC_KI, ENC_KD,
                            ENC_TOLERANCE, ENC_SETTLING);
    // Initialize the turnPidCtrl object.
    PIDCtrlInit(g_turnPidCtrl,
                            COMP_KP, COMP_KI, COMP_KD,
                            COMP_TOLERANCE, COMP_SETTLING);
    // Initialize the pidDrive object.
    PIDDriveInit(g_pidDrive, g_drive, g_yPidCtrl, g_turnPidCtrl);
    // Initialize the autonomous state machine and start it.
    SMInit(g_autoSM);
    SMStart(g_autoSM);
 
    while (true)
    {
        // display debug info on nxt
        displayTextLine(0, "state=%d", SMGetState(g_autoSM));
        PIDCtrlDisplayInfo(1, g_yPidCtrl);
        PIDCtrlDisplayInfo(3, g_turnPidCtrl);
        CompassTask(g_compass);
        // Check if state machine is ready.
        if (SMIsReady(g_autoSM))
        {
            int currState = SMGetState(g_autoSM);
            switch (currState)
            {
            case SMSTATE_STARTED:
                // Drive forward for 24 inches.
                PIDDriveSetTarget(g_pidDrive,
                0.0,
                90.0,
                false,
                &g_autoSM,
                EVTTYPE_PIDDRIVE);
                // Tell state machine to wait until done.
                SMAddWaitEvent(g_autoSM, EVTTYPE_PIDDRIVE);
                // Tell state machine to go to next state when done.
                SMWaitEvents(g_autoSM, currState + 1);
                break;
 
            default:
                // We are done, stop the state machine.
                SMStop(g_autoSM);
                break;
            }
        }
        //
        // Execute all periodic tasks.
        //
        PIDDriveTask(g_pidDrive);
        DriveTask(g_drive);
        wait1Msec(20);
    }
}


Thu Feb 19, 2015 3:21 pm
Profile
Guru
User avatar

Joined: Sun Nov 15, 2009 5:46 am
Posts: 1523
Post Re: PID Control
BTB1337 wrote:
Ok sorry for my lack of explanation I'll try to explain more later when I can get back to the robot, but last night I stayed at school until like 8pm trying to figure out the compass and eventually I decided to try calibrating using xanders calibration code, which returned with a port setup error? not sure what the problem was. After that I went to the TryMe Files on the NXT and ran the compass program and when i first turned it on it gave me a value of like 211 or something along those lines (I'll post exact numbers later) and the compass direction arrow thing was pointing NNW and as I turned it the number value went up but the Directional Arrow didn't change unless I exited the program and ran it again and it gave me an updated direction of close to W with the value at like 219 or 229...something along those lines. Strangely when I flipped it 180 it was giving the same value of 219 as it did in another direction as if it was confused or getting false north's.

That's what I suspected. If the compass sensor does not give you reasonable numbers, don't even try to debug PID control with it. You have to get the compass sensor working before you attempt anything else. Now using Xander's test program is a good start but eventually after you prove the compass sensor is working properly with Xander's program, you would want to use the test program I gave you in my previous reply because that's calling our library to read the compass. This is to make sure our compass module is working or not.

BTB1337 wrote:
In the compass program the target would go up each time I ran it...it started low at around 90 and every time I ran the program, the Target would go up increasingly, it eventually got up to a target of like 720 or something ridiculously high and wrong. The strange part was how it would be continuously turning in circles but the E I O numbers would jump almost as if it reset itself at around 320. Another strange thing was how the upper set was all zeroed out except for the I and the lower set of numbers, the T would be stuck at a target value (some number like 90 and eventually worked up to 700 each time I ran ran) throughout the time I ran the program, and I believe I would always be stuck 90 the entire program with E changing depending on where it was while turning and O was at 45 I believe because of KP. To me, it almost seemed as if the Top I and the Bottom I were switched or something somehow? Because the Top I was changing like the bottom one should. I tried adding in a line at the top of "task main()" but it didn't seem to help at all.

CompassCalibration (I had to put random numbers in the wheel diameter and distance between wheels in mm because the real values weren't turning the 360 properly)

If the target kept going up each time you ran it, it means CompassGetHeading is getting an increasing number each time. That could be normal. Say if you start by placing your robot facing North, CompassGetHeading should give you a value of 0, then you set your target to +90. If the code were working, the robot would turn facing East. Then if you ran it again, your new target would be CompassGetHeading + 90 which would be 90+90=180. So if everything were working properly, this is a normal behavior. Remember that the compass is an absolute heading device and your code is trying to ask it to turn 90 degree from its current position. Regarding the reset of the E, I and O numbers, I think I can explain that. Unlike the gyro which is a continuous device, compass will not give you any reading past 359 in theory. Say you start pointing North, you get a value 0. As the robot turn clockwise, the number will increase until it is almost a complete revolution, say 350. As the robot turns more and crossing the North, the reading will reset back to 0 (North). The way you are using the compass to turn is to turn an absolute heading device into a relative heading device (i.e. + 90 to current heading). For example, let's say, the robot is pointing at NNW (315) and you want to turn clockwise 90 degree from current heading, then your target would be 315+90 = 405. So initially, the error (E) would be 405 - 315 = 90. As the robot turn to facing almost north say 359, your error would be 405 - 359 = 46. Once you reach north, all the sudden, the heading becomes 0 so now your error would be 405 - 0 = 405 and since the heading will never go beyond 359, the error will never be 0 and the robot will turn round and round non-stop. There are two ways to fix it. One is to use the compass as a absolute heading device as it is intended to be, meaning that you set your target as absolute heading instead of relative to your current heading. This is actually better than relative heading because you will eliminate cumulative error. Imagine if you are using relative heading, your robot either ran into something or hit by another robot and your heading got turned, the next time your code does "turn right 90 degree", it would end up pointing at 90 degree + whatever heading your robot was pointing at when the robot got hit. It will not be your intended heading. Whereas if you are taking advantage of an absolute pointing device, say your robot starts at pointing North and you try to turn the robot facing East, you would set your target to 90 (instead of currentHeading + 90), let's say before you start, another robot hits you and you are now pointing Northeast instead of North, since you are setting an absolute target to turn to East, the robot will turn only 45 degree (target - currHeading = East(90) - Northeast(45) = 45). So at the end you will be facing correctly to the East.
So let's use this year's game field as an example and assume the field is squarely aligned with the magnetic north (i.e. Your robot parked on the platform is facing Perfect North). You go down the ramp, grab the mid-height goal and determine that the parking zone is at approx. SSW (approx. 200), you either set your turn target to 200 so the robot will swing around to SSW or you turn your robot to NNE (approx. 20) and backup to the parking zone. Let's say your opponent was doing the center goal and ended up in the corridor you were travelling in to the parking zone but it did not completely block you, just clipped you at the corner of your robot. Your robot hit the opponent's robot at the corner and got turned slightly and was pointing to SW instead of SSW but since the target is set to SSW, the robot will correct itself. Now I am not saying that since you have the compass sensor, you can easily pull this off. This scenario requires very tricky programming but an absolute heading device makes it possible. In fact, even a relative heading device such as a gyro can do it if you understand the interaction. Our team did this with a gyro and it worked pretty well.
Now the second way is to make the compass device a continuous device (i.e. If it turns a full revolution, it will give you 360 and not reset back to 0). This is possible if you are using our compass module. If you look at the code, you will see it keeps track of a field called accRev (accumulated revolution). In other words, instead of calling CompassGetHeading, you should call CompassGetAccumHeading. This will give you a number that can pass 360 and beyond.
Regarding the top set of debug info, don't worry about it. The reason why "I" is changing is because I is the encoder reading. As the robot turns, the encoder reading will change but since your are doing turn only, PIDDrive ignores the yPidCtrl, so the top set of debug info is irrelevant in this scenario.


Thu Feb 19, 2015 5:34 pm
Profile
Rookie

Joined: Sat Jan 31, 2015 1:14 am
Posts: 38
Post Re: PID Control
MHTS wrote:
BTB1337 wrote:
Ok sorry for my lack of explanation I'll try to explain more later when I can get back to the robot, but last night I stayed at school until like 8pm trying to figure out the compass and eventually I decided to try calibrating using xanders calibration code, which returned with a port setup error? not sure what the problem was. After that I went to the TryMe Files on the NXT and ran the compass program and when i first turned it on it gave me a value of like 211 or something along those lines (I'll post exact numbers later) and the compass direction arrow thing was pointing NNW and as I turned it the number value went up but the Directional Arrow didn't change unless I exited the program and ran it again and it gave me an updated direction of close to W with the value at like 219 or 229...something along those lines. Strangely when I flipped it 180 it was giving the same value of 219 as it did in another direction as if it was confused or getting false north's.

That's what I suspected. If the compass sensor does not give you reasonable numbers, don't even try to debug PID control with it. You have to get the compass sensor working before you attempt anything else. Now using Xander's test program is a good start but eventually after you prove the compass sensor is working properly with Xander's program, you would want to use the test program I gave you in my previous reply because that's calling our library to read the compass. This is to make sure our compass module is working or not.

BTB1337 wrote:
In the compass program the target would go up each time I ran it...it started low at around 90 and every time I ran the program, the Target would go up increasingly, it eventually got up to a target of like 720 or something ridiculously high and wrong. The strange part was how it would be continuously turning in circles but the E I O numbers would jump almost as if it reset itself at around 320. Another strange thing was how the upper set was all zeroed out except for the I and the lower set of numbers, the T would be stuck at a target value (some number like 90 and eventually worked up to 700 each time I ran ran) throughout the time I ran the program, and I believe I would always be stuck 90 the entire program with E changing depending on where it was while turning and O was at 45 I believe because of KP. To me, it almost seemed as if the Top I and the Bottom I were switched or something somehow? Because the Top I was changing like the bottom one should. I tried adding in a line at the top of "task main()" but it didn't seem to help at all.

CompassCalibration (I had to put random numbers in the wheel diameter and distance between wheels in mm because the real values weren't turning the 360 properly)

If the target kept going up each time you ran it, it means CompassGetHeading is getting an increasing number each time. That could be normal. Say if you start by placing your robot facing North, CompassGetHeading should give you a value of 0, then you set your target to +90. If the code were working, the robot would turn facing East. Then if you ran it again, your new target would be CompassGetHeading + 90 which would be 90+90=180. So if everything were working properly, this is a normal behavior. Remember that the compass is an absolute heading device and your code is trying to ask it to turn 90 degree from its current position. Regarding the reset of the E, I and O numbers, I think I can explain that. Unlike the gyro which is a continuous device, compass will not give you any reading past 359 in theory. Say you start pointing North, you get a value 0. As the robot turn clockwise, the number will increase until it is almost a complete revolution, say 350. As the robot turns more and crossing the North, the reading will reset back to 0 (North). The way you are using the compass to turn is to turn an absolute heading device into a relative heading device (i.e. + 90 to current heading). For example, let's say, the robot is pointing at NNW (315) and you want to turn clockwise 90 degree from current heading, then your target would be 315+90 = 405. So initially, the error (E) would be 405 - 315 = 90. As the robot turn to facing almost north say 359, your error would be 405 - 359 = 46. Once you reach north, all the sudden, the heading becomes 0 so now your error would be 405 - 0 = 405 and since the heading will never go beyond 359, the error will never be 0 and the robot will turn round and round non-stop. There are two ways to fix it. One is to use the compass as a absolute heading device as it is intended to be, meaning that you set your target as absolute heading instead of relative to your current heading. This is actually better than relative heading because you will eliminate cumulative error. Imagine if you are using relative heading, your robot either ran into something or hit by another robot and your heading got turned, the next time your code does "turn right 90 degree", it would end up pointing at 90 degree + whatever heading your robot was pointing at when the robot got hit. It will not be your intended heading. Whereas if you are taking advantage of an absolute pointing device, say your robot starts at pointing North and you try to turn the robot facing East, you would set your target to 90 (instead of currentHeading + 90), let's say before you start, another robot hits you and you are now pointing Northeast instead of North, since you are setting an absolute target to turn to East, the robot will turn only 45 degree (target - currHeading = East(90) - Northeast(45) = 45). So at the end you will be facing correctly to the East.
So let's use this year's game field as an example and assume the field is squarely aligned with the magnetic north (i.e. Your robot parked on the platform is facing Perfect North). You go down the ramp, grab the mid-height goal and determine that the parking zone is at approx. SSW (approx. 200), you either set your turn target to 200 so the robot will swing around to SSW or you turn your robot to NNE (approx. 20) and backup to the parking zone. Let's say your opponent was doing the center goal and ended up in the corridor you were travelling in to the parking zone but it did not completely block you, just clipped you at the corner of your robot. Your robot hit the opponent's robot at the corner and got turned slightly and was pointing to SW instead of SSW but since the target is set to SSW, the robot will correct itself. Now I am not saying that since you have the compass sensor, you can easily pull this off. This scenario requires very tricky programming but an absolute heading device makes it possible. In fact, even a relative heading device such as a gyro can do it if you understand the interaction. Our team did this with a gyro and it worked pretty well.
Now the second way is to make the compass device a continuous device (i.e. If it turns a full revolution, it will give you 360 and not reset back to 0). This is possible if you are using our compass module. If you look at the code, you will see it keeps track of a field called accRev (accumulated revolution). In other words, instead of calling CompassGetHeading, you should call CompassGetAccumHeading. This will give you a number that can pass 360 and beyond.
Regarding the top set of debug info, don't worry about it. The reason why "I" is changing is because I is the encoder reading. As the robot turns, the encoder reading will change but since your are doing turn only, PIDDrive ignores the yPidCtrl, so the top set of debug info is irrelevant in this scenario.


Oh ok that makes alot more sense. I believe I did try to use CompassGetAccumHeading + 90 and if I recall it still wouldn't work properly...I think it's because the compass wasn't working...any ideas on why it wouldn't be working or how I could fix it? I hope not to use the compass, but rather the gyro since it seems a lot easier for someone of my skill level. The only problem is that we ordered the gyro sometime mid last week with priority 2-day shipping, giving us a week of programming, but the people at hitechnic failed terribly, they didn't ship it until Tuesday this week, which in theory should arrive on thursday, but eventually we called them up and got a tracking number and found out that it won't be delivered until sometime today(friday) with the houston regional championship tomorrow...

I've done what I can to program the routes on RVW and I've also taped out the route on the field with measurements so I can just copy it all over.

To help with quick programming when our gyro gets here could you check this code and make sure it's correct? Since our gyro is coming in so late it'd be nice to just go on and tune it etc. without staying up all night confused on why it isn't working. Also if there is anything to this code you recommend to change or add for accuracy or simplicity or anything please let me know. Also keep in mind that the motor and sensor setup is set for RVW and our gyro will be on Port 3 unless another port is required which I don't believe is. The following is what the Motor and Sensor setup will be and after that the code i'm using in RVW (obviously i'll switch over the variable names and stuff)

Motor and Sensor Setup:
Code:
#pragma config(Hubs,  S1, HTMotor,  HTMotor,  HTServo,  none)
#pragma config(Sensor, S1,     ,               sensorI2CMuxController)
#pragma config(Sensor, S2,     irSeeker,       sensorI2CCustom)
#pragma config(Sensor, S3,     htComp,         sensorI2CHiTechnicCompass)
#pragma config(Sensor, S4,     htEOPD,         sensorI2CCustom)
#pragma config(Motor,  mtr_S1_C1_1,     rightWheel,    tmotorTetrix, openLoop, encoder)
#pragma config(Motor,  mtr_S1_C1_2,     leftWheel,     tmotorTetrix, openLoop, reversed, encoder)
#pragma config(Motor,  mtr_S1_C2_1,     rightLifter,   tmotorTetrix, PIDControl, reversed)
#pragma config(Motor,  mtr_S1_C2_2,     leftLifter,    tmotorTetrix, PIDControl)
#pragma config(Servo,  srvo_S1_C3_1,    servo1,               tServoNone)
#pragma config(Servo,  srvo_S1_C3_2,    servo2,               tServoNone)
#pragma config(Servo,  srvo_S1_C3_3,    irServo,              tServoStandard)
#pragma config(Servo,  srvo_S1_C3_4,    goalHook,             tServoStandard)
#pragma config(Servo,  srvo_S1_C3_5,    leftDoor,             tServoStandard)
#pragma config(Servo,  srvo_S1_C3_6,    rightDoor,            tServoStandard)


Gyro Program configured for RVW:
Code:
#pragma config(Hubs,  S1, HTMotor,  none,     none,     none)
#pragma config(Sensor, S2,     gyroSensor,     sensorI2CHiTechnicGyro)
#pragma config(Motor,  mtr_S1_C1_1,     leftMotor,     tmotorTetrix, openLoop, encoder)
#pragma config(Motor,  mtr_S1_C1_2,     rightMotor,    tmotorTetrix, openLoop, encoder, reversed)
//*!!Code automatically generated by 'ROBOTC' configuration wizard               !!*//
 
// include files
#include "ftclib/trcdefs.h"
#include "ftclib/dbgtrace.h"
#include "ftclib/sm.h"
#include "ftclib/gyro.h"
#include "ftclib/drive.h"
#include "ftclib/pidctrl.h"
#include "ftclib/piddrive.h"
 
// define distance with encoders
#define ENC_KP              12.0 // change this to meet target distance
#define ENC_KI              0.0
#define ENC_KD              0.0
#define ENC_TOLERANCE       1.0
#define ENC_SETTLING        200
 
// define turns with gyro
#define GYRO_KP             0.0 // change this to meet turn angle
#define GYRO_KI             0.0
#define GYRO_KD             0.0
#define GYRO_TOLERANCE      0.5
#define GYRO_SETTLING       200
 
#define CLICKS_PER_INCH     170.0 // change this to tune distance
 
#define EVTTYPE_PIDDRIVE        (EVTTYPE_NONE + 1)
 
GYRO        g_gyro;         //create the gyro object.
DRIVE       g_drive;        //create the drive object.
PIDCTRL     g_yPidCtrl;     //create the Y PID controller
PIDCTRL     g_turnPidCtrl;  //create the turn PID controller
PIDDRIVE    g_pidDrive;     //create the PID drive object
SM                  g_autoSM;               //create the autonomous state machine
 
// calculate all the math
float PIDCtrlGetInput(PIDCTRL &pidCtrl)
{
    float inputValue = 0.0;
 
    if (IsSameReference(pidCtrl, g_yPidCtrl))
    {
        inputValue = (nMotorEncoder[leftMotor] + nMotorEncoder[rightMotor])/2.0/CLICKS_PER_INCH;
    }
    else if (IsSameReference(pidCtrl, g_turnPidCtrl))
    {
        inputValue = GyroGetHeading(g_gyro);
    }
    return inputValue;
}
 
task main()
{
      // initializations
    GyroInit(g_gyro, gyroSensor);   //initialize gyro object.
    DriveInit(g_drive, leftMotor, rightMotor);  //initialize drive object
    PIDCtrlInit(g_yPidCtrl,         //initialize yPidCtrl object
                ENC_KP, ENC_KI, ENC_KD,
                ENC_TOLERANCE, ENC_SETTLING);
    PIDCtrlInit(g_turnPidCtrl,
                GYRO_KP, GYRO_KI, GYRO_KD,
                GYRO_TOLERANCE, GYRO_SETTLING);
    PIDDriveInit(g_pidDrive, g_drive, g_yPidCtrl, g_turnPidCtrl);
        // Initialize autonomous state machine and start it.
        SMInit(g_autoSM);
        SMStart(g_autoSM);
 
        while (true)
        {
            // display debug info on nxt
            displayTextLine(0, "state=%d", SMGetState(g_autoSM));
        PIDCtrlDisplayInfo(1, g_yPidCtrl);
        PIDCtrlDisplayInfo(3, g_turnPidCtrl);
        GyroTask(g_gyro);
 
        // Check if state machine is ready.
        if (SMIsReady(g_autoSM))
        {
            int currState = SMGetState(g_autoSM);
            switch (currState)
            {
                case SMSTATE_STARTED:
                    // Drive forward for 24 inches.
                    PIDDriveSetTarget(g_pidDrive,
                                      24.0,
                                      0.0,
                                      false,
                                      &g_autoSM,
                                      EVTTYPE_PIDDRIVE);
                    // Tell state machine to wait until done.
                    SMAddWaitEvent(g_autoSM, EVTTYPE_PIDDRIVE);
                    // Tell state machine to go to next state when done.
                    SMWaitEvents(g_autoSM, currState + 1);
                    break;
 
                case SMSTATE_STARTED + 1:
                    // Drive forward for 24 inches.
                    PIDDriveSetTarget(g_pidDrive,
                                      0.0,
                                      0.0,
                                      false,
                                      &g_autoSM,
                                      EVTTYPE_PIDDRIVE);
                    // Tell state machine to wait until done.
                    SMAddWaitEvent(g_autoSM, EVTTYPE_PIDDRIVE);
                    // Tell state machine to go to next state when done.
                    SMWaitEvents(g_autoSM, currState + 1);
                    break;
 
                default:
                    // We are done, stop the state machine.
                    SMStop(g_autoSM);
                    break;
            }
        }
        //
        // Execute all periodic tasks.
        //
        PIDDriveTask(g_pidDrive);
        DriveTask(g_drive);
        wait1Msec(20);
    }
}


Also while I've got my codes pulled up I was trying the PID with Encoders only for driving and turning and I got the drive tuned but the turns seems weird. For whatever reason every time I run a program that just says go 0.0 inches and turn right 90.0 degress and each time I run it the error is completely different, for example with random numbers but illustrates what was happening 1st run 81 2nd -10 3rd 68 4th 89 5th 75 I have no clue why it's doing this, The Robot seemed to turn roughly the right angle each time I believe but can't remember for sure. Here is the code

PID Enc:
Code:
#pragma config(Hubs,  S1, HTMotor,  HTMotor,  HTServo,  none)
#pragma config(Sensor, S2,     irSeeker,       sensorI2CCustom)
#pragma config(Sensor, S3,     htComp,         sensorI2CHiTechnicCompass)
#pragma config(Sensor, S4,     htEOPD,         sensorI2CCustom)
#pragma config(Motor,  mtr_S1_C1_1,     rightWheel,    tmotorTetrix, openLoop, encoder)
#pragma config(Motor,  mtr_S1_C1_2,     leftWheel,     tmotorTetrix, openLoop, reversed, encoder)
#pragma config(Motor,  mtr_S1_C2_1,     rightLifter,   tmotorTetrix, PIDControl, reversed)
#pragma config(Motor,  mtr_S1_C2_2,     leftLifter,    tmotorTetrix, PIDControl)
#pragma config(Servo,  srvo_S1_C3_1,    servo1,               tServoNone)
#pragma config(Servo,  srvo_S1_C3_2,    servo2,               tServoNone)
#pragma config(Servo,  srvo_S1_C3_3,    irServo,              tServoStandard)
#pragma config(Servo,  srvo_S1_C3_4,    goalHook,             tServoStandard)
#pragma config(Servo,  srvo_S1_C3_5,    leftDoor,             tServoStandard)
#pragma config(Servo,  srvo_S1_C3_6,    rightDoor,            tServoStandard)
//*!!Code automatically generated by 'ROBOTC' configuration wizard               !!*//
 
// include files
#include "ftclib/trcdefs.h"
#include "ftclib/dbgtrace.h"
#include "ftclib/sm.h"
#include "ftclib/drive.h"
#include "ftclib/pidctrl.h"
#include "ftclib/piddrive.h"
 
// define distance with encoders
#define ENC_KP              1.6 // change this to meet distance target
#define ENC_KI              0.0015
#define ENC_KD              0.5
#define ENC_TOLERANCE       1.0
#define ENC_SETTLING        200
 
// define turns with encoder
#define TURN_KP             1.0 // change this to meet turn target
#define TURN_KI             0.0
#define TURN_KD             0.5
#define TURN_TOLERANCE      1.0
#define TURN_SETTLING       200
 
#define CLICKS_PER_INCH     118.5 // change this to adjust actual distance
#define CLICKS_PER_DEGREE   10.0 // change this to adjust actual degree
 
#define EVTTYPE_PIDDRIVE    (EVTTYPE_NONE + 1)
 
DRIVE       g_drive;        //create the drive object.
PIDCTRL     g_yPidCtrl;     //create the Y PID controller
PIDCTRL     g_turnPidCtrl;  //create the turn PID controller
PIDDRIVE    g_pidDrive;     //create the PID drive object
SM          g_autoSM;       //create the autonomous state machine object
 
// calculate all the math
float PIDCtrlGetInput(PIDCTRL &pidCtrl)
{
    float inputValue = 0.0;
 
    if (IsSameReference(pidCtrl, g_yPidCtrl))
    {
        inputValue = (nMotorEncoder[leftWheel] + nMotorEncoder[rightWheel])/2.0/CLICKS_PER_INCH;
    }
    else if (IsSameReference(pidCtrl, g_turnPidCtrl))
    {
        inputValue = (nMotorEncoder[leftWheel] - nMotorEncoder[rightWheel])/2.0/CLICKS_PER_DEGREE;
    }
    return inputValue;
}
 
task main()
{
    // Initialize the drive object.
    DriveInit(g_drive, leftWheel, rightWheel);
    // Initialize the yPidCtrl object.
    PIDCtrlInit(g_yPidCtrl,
    ENC_KP, ENC_KI, ENC_KD,
    ENC_TOLERANCE, ENC_SETTLING);
    // Initialize the turnPidCtrl object.
    PIDCtrlInit(g_turnPidCtrl,
    TURN_KP, TURN_KI, TURN_KD,
    TURN_TOLERANCE, TURN_SETTLING);
    // Initialize the pidDrive object.
    PIDDriveInit(g_pidDrive, g_drive, g_yPidCtrl, g_turnPidCtrl);
    // Initialize the autonomous state machine and start it.
    SMInit(g_autoSM);
    SMStart(g_autoSM);
 
    while (true)
    {
        // display debug info on nxt
        displayTextLine(0, "state=%d", SMGetState(g_autoSM));
        PIDCtrlDisplayInfo(1, g_yPidCtrl);
        PIDCtrlDisplayInfo(3, g_turnPidCtrl);
 
        // Check if state machine is ready.
        if (SMIsReady(g_autoSM))
        {
            int currState = SMGetState(g_autoSM);
            switch (currState)
            {
            case SMSTATE_STARTED:
                // Drive forward for 24 inches.
                PIDDriveSetTarget(g_pidDrive,
                0.0,
                90.0,
                false,
                &g_autoSM,
                EVTTYPE_PIDDRIVE);
                // Tell state machine to wait until done.
                SMAddWaitEvent(g_autoSM, EVTTYPE_PIDDRIVE);
                // Tell state machine to go to next state when done.
                SMWaitEvents(g_autoSM, currState + 1);
                break;
 
            /*case SMSTATE_STARTED + 1:
                // Drive forward for 24 inches.
                PIDDriveSetTarget(g_pidDrive,
                0.0,
                90.0,
                false,
                &g_autoSM,
                EVTTYPE_PIDDRIVE);
                // Tell state machine to wait until done.
                SMAddWaitEvent(g_autoSM, EVTTYPE_PIDDRIVE);
                // Tell state machine to go to next state when done.
                SMWaitEvents(g_autoSM, currState + 1);
                break;*/
 
            default:
                // We are done, stop the state machine.
                SMStop(g_autoSM);
                break;
            }
        }
        //
        // Execute all periodic tasks.
        //
        PIDDriveTask(g_pidDrive);
        DriveTask(g_drive);
        wait1Msec(20);
    }
}


And one final question, is there a way to add in to the code under one of the SM States some code that pretty much says (in pseudo code)
Code:
if eopd < 5
{
   reverse
}
else if eopd > 5
{
   forward
{
else if eopd = 5
{
   return to main code
}

// new state

while encoder rightLifter < center goal height
{
   lift elevator
}


or something along those lines. Is that possible?


Fri Feb 20, 2015 1:14 pm
Profile
Guru
User avatar

Joined: Sun Nov 15, 2009 5:46 am
Posts: 1523
Post Re: PID Control
Since you decided to make your gyro code ready before you get the gyro sensor, did you try your code in RVW and see if it is working? The gyro code you have will not work because first, your GRYO_KP is zero and your turn target when calling PIDDriveSetTarget is also zero. I would recommend you try this out in RVW. You should be able to do everything in RVW granted that when you finally get the gyro sensor, you probably need to retune all the KP's and INCHES_PER_CLICK counts. Well, I suspect by the time you read my reply, you probably already receive the gyro sensor.
Regarding the compass sensor not working, did you try the compass test code I gave you? What about Xander's compass test code? If both are not working, I have no clue why. May be it needs calibration. We always avoid compass sensor because it is finicky. A lot of things can interfere with it and it's sensitive on its mounting position. You may want to play with where you mount the compass sensor (away from the motors, for example).
Regarding PID with encoders only, it should just work, not as accurate as gyro but when tuned correctly should do a decent job turning. Unfortunately, I don't understand your description of the issue. For example, what do you mean by "turns seems weird"? And what are the "random numbers"? The code looks fine but I am not sure about all the parameters are tuned correctly (e.g. TURN_KP, DEGREES_PER_CLICK... etc). If tuned correctly, it should work.
BTB1337 wrote:
And one final question, is there a way to add in to the code under one of the SM States some code that pretty much says (in pseudo code)
Code:
if eopd < 5
{
   reverse
}
else if eopd > 5
{
   forward
{
else if eopd = 5
{
   return to main code
}

// new state

while encoder rightLifter < center goal height
{
   lift elevator
}


or something along those lines. Is that possible?

Regarding the EOPD sensor code, you can do the following:
Code:
#pragma config(Hubs,  S1, HTMotor,  HTMotor,  HTServo,  none)
#pragma config(Sensor, S2,     irSeeker,       sensorI2CCustom)
#pragma config(Sensor, S3,     gyroSensor,     sensorI2CHiTechnicGyro)
#pragma config(Sensor, S4,     htEOPD,         sensorI2CCustom)
#pragma config(Motor,  mtr_S1_C1_1,     rightWheel,    tmotorTetrix, openLoop, encoder)
#pragma config(Motor,  mtr_S1_C1_2,     leftWheel,     tmotorTetrix, openLoop, reversed, encoder)
#pragma config(Motor,  mtr_S1_C2_1,     rightLifter,   tmotorTetrix, PIDControl, reversed)
#pragma config(Motor,  mtr_S1_C2_2,     leftLifter,    tmotorTetrix, PIDControl)
#pragma config(Servo,  srvo_S1_C3_1,    servo1,               tServoNone)
#pragma config(Servo,  srvo_S1_C3_2,    servo2,               tServoNone)
#pragma config(Servo,  srvo_S1_C3_3,    irServo,              tServoStandard)
#pragma config(Servo,  srvo_S1_C3_4,    goalHook,             tServoStandard)
#pragma config(Servo,  srvo_S1_C3_5,    leftDoor,             tServoStandard)
#pragma config(Servo,  srvo_S1_C3_6,    rightDoor,            tServoStandard)
//*!!Code automatically generated by 'ROBOTC' configuration wizard               !!*//
 
// include files
#include "ftclib/trcdefs.h"
#include "ftclib/dbgtrace.h"
#include "ftclib/gyro.h"
#include "ftclib/sm.h"
#include "ftclib/drive.h"
#include "ftclib/pidctrl.h"
#include "ftclib/piddrive.h"
 
// define distance with encoders
#define ENC_KP              1.6 // change this to meet distance target
#define ENC_KI              0.0015
#define ENC_KD              0.5
#define ENC_TOLERANCE       1.0
#define ENC_SETTLING        200
 
// define turns with encoder
#define GYRO_KP             1.0 // change this to meet turn target
#define GYRO_KI             0.0
#define GYRO_KD             0.5
#define GYRO_TOLERANCE      1.0
#define GYRO_SETTLING       200
 
// define distance with encoders
#define EOPD_KP             1.6 // MUST TUNE THIS FOR THE EOPD SENSOR
#define EOPD_KI             0.0
#define EOPD_KD             0.0
#define EOPD_TOLERANCE      0.5 // MUST TUNE THIS
#define EOPD_SETTLING       200
 
#define CLICKS_PER_INCH     118.5 // change this to adjust actual distance
 
#define EVTTYPE_PIDDRIVE    (EVTTYPE_NONE + 1)

GYRO        g_gyro;         //create the gyro object.
DRIVE       g_drive;        //create the drive object.
PIDCTRL     g_yPidCtrl;     //create the Y PID controller
PIDCTRL     g_eopdPidCtrl;  //create EOPD PID controller
PIDCTRL     g_turnPidCtrl;  //create the turn PID controller
PIDDRIVE    g_pidDrive;     //create the PID drive object
PIDDRIVE    g_eopdPidDrive; //create the PID drive object for EOPD
SM          g_autoSM;       //create the autonomous state machine object
 
// calculate all the math
float PIDCtrlGetInput(PIDCTRL &pidCtrl)
{
    float inputValue = 0.0;
 
    if (IsSameReference(pidCtrl, g_yPidCtrl))
    {
        inputValue = (nMotorEncoder[leftWheel] + nMotorEncoder[rightWheel])/2.0/CLICKS_PER_INCH;
    }
    else if (IsSameReference(pidCtrl, g_turnPidCtrl))
    {
        inputValue = GyroGetHeading(g_gyro);
    }
    else if (IsSameReference(pidCtrl, g_eopdPidCtrl))
    {
        inputValue = SensorValue[htEOPD]; // OR HOWEVER WAY YOU READ THE EOPD SENSOR
    }
    return inputValue;
}
 
task main()
{
    GyroInit(g_gyro, gyroSensor);
    // Initialize the drive object.
    DriveInit(g_drive, leftWheel, rightWheel);
    // Initialize the yPidCtrl object.
    PIDCtrlInit(g_yPidCtrl,
    ENC_KP, ENC_KI, ENC_KD,
    ENC_TOLERANCE, ENC_SETTLING);
    // Initialize the turnPidCtrl object.
    PIDCtrlInit(g_turnPidCtrl,
    GYRO_KP, GYRO_KI, GYRO_KD,
    GYRO_TOLERANCE, GYRO_SETTLING);
    // Initialize the pidDrive object.
    PIDDriveInit(g_pidDrive, g_drive, g_yPidCtrl, g_turnPidCtrl);
    PIDCtrlInit(g_eopdPidCtrl,
    EOPD_KP, EOPD_KI, EOPD_KD,
    EOPD_TOLERANCE, EOPD_SETTLING,
    PIDCTRLO_ABS_SETPT | PIDCTRLO_INVERSE_INPUT);
    PIDDriveInit(g_eopdPidDrive, g_drive, g_eopdPidCtrl, g_turnPidCtrl);
    // Initialize the autonomous state machine and start it.
    SMInit(g_autoSM);
    SMStart(g_autoSM);
 
    while (true)
    {
        GyroTask(g_gyro);
        // display debug info on nxt
        displayTextLine(0, "state=%d", SMGetState(g_autoSM));
        PIDCtrlDisplayInfo(1, g_eopdPidCtrl);
        PIDCtrlDisplayInfo(3, g_turnPidCtrl);
 
        // Check if state machine is ready.
        if (SMIsReady(g_autoSM))
        {
            int currState = SMGetState(g_autoSM);
            switch (currState)
            {
            case SMSTATE_STARTED:
                // Drive forward for 24 inches.
                PIDDriveSetTarget(g_eopdPidDrive,
                5.0,
                0.0,
                false,
                &g_autoSM,
                EVTTYPE_PIDDRIVE);
                // Tell state machine to wait until done.
                SMAddWaitEvent(g_autoSM, EVTTYPE_PIDDRIVE);
                // Tell state machine to go to next state when done.
                SMWaitEvents(g_autoSM, currState + 1);
                break;

            default:
                // We are done, stop the state machine.
                SMStop(g_autoSM);
                break;
            }
        }
        //
        // Execute all periodic tasks.
        //
        PIDDriveTask(g_pidDrive);
        DriveTask(g_drive);
        wait1Msec(20);
    }


Note that I just modified your code and did not compile it or do any testing, so there may be a few typos and such. This shows you the beauty of object oriented programming (i.e. you can create multiple instances of an object). In this case, not only you have a g_pidDrive object, you can create another PIDDrive object called g_eopdPidDrive. The EOPD piddrive object uses the EOPD sensor as your "Y direction control" and the gyro as your "Turn control". The only thing you have to be careful about is that when using the EOPD sensor, you must do Absolute SetPoint (i.e. when you set the target to 5.0, you don't want to do current EOPD distance + 5.0, you really just meant absolute 5.0). Secondly, unlike encoder distance which is increasing when the robot goes forward, the EOPD distance is decreasing when the robot goes forward. So you need to set the PIDCTRLO_INVERSE_INPUT flag.


Fri Feb 20, 2015 7:40 pm
Profile
Guru
User avatar

Joined: Sun Nov 15, 2009 5:46 am
Posts: 1523
Post Re: PID Control
Oh didn't see your elevator code. For lifting the elevator to the center goal, you can do it two ways. In our library, we have a module called pidmotor.h. This is a PID-controlled motor for arms, elevators etc. That's pretty cool because it is using PID control to hold the arm/elevator at the height you want. In addition, it also allows multi-tasking (i.e. the elevator going up at the same time as the robot is going forward towards the center goal). This is especially helpful consider autonomous is only 30 seconds. One of our autonomous routines not only score at the center goal but also knock down the kickstand. So if we do things one after another serially, we won't have time to complete all of it. So having the elevator going up in parallel to the robot seeking the center goal is very essential. For example, here is our code (http://proj.titanrobotics.net/hg/Ftc/20 ... Goal6541.h) that did this (https://www.youtube.com/watch?v=5aEbYVJQICo). But it may be a little bit complicated for you to learn another module so close to your competition. Plus a whole set of PID constants to tune. Here is a second way to do it without using our PIDMotor module:
Code:
#pragma config(Hubs,  S1, HTMotor,  HTMotor,  HTServo,  none)
#pragma config(Sensor, S2,     irSeeker,       sensorI2CCustom)
#pragma config(Sensor, S3,     gyroSensor,     sensorI2CHiTechnicGyro)
#pragma config(Sensor, S4,     htEOPD,         sensorI2CCustom)
#pragma config(Motor,  mtr_S1_C1_1,     rightWheel,    tmotorTetrix, openLoop, encoder)
#pragma config(Motor,  mtr_S1_C1_2,     leftWheel,     tmotorTetrix, openLoop, reversed, encoder)
#pragma config(Motor,  mtr_S1_C2_1,     rightLifter,   tmotorTetrix, PIDControl, reversed)
#pragma config(Motor,  mtr_S1_C2_2,     leftLifter,    tmotorTetrix, PIDControl)
#pragma config(Servo,  srvo_S1_C3_1,    servo1,               tServoNone)
#pragma config(Servo,  srvo_S1_C3_2,    servo2,               tServoNone)
#pragma config(Servo,  srvo_S1_C3_3,    irServo,              tServoStandard)
#pragma config(Servo,  srvo_S1_C3_4,    goalHook,             tServoStandard)
#pragma config(Servo,  srvo_S1_C3_5,    leftDoor,             tServoStandard)
#pragma config(Servo,  srvo_S1_C3_6,    rightDoor,            tServoStandard)
//*!!Code automatically generated by 'ROBOTC' configuration wizard               !!*//
 
// include files
#include "ftclib/trcdefs.h"
#include "ftclib/dbgtrace.h"
#include "ftclib/gyro.h"
#include "ftclib/sm.h"
#include "ftclib/drive.h"
#include "ftclib/pidctrl.h"
#include "ftclib/piddrive.h"
 
// define distance with encoders
#define ENC_KP              1.6 // change this to meet distance target
#define ENC_KI              0.0015
#define ENC_KD              0.5
#define ENC_TOLERANCE       1.0
#define ENC_SETTLING        200
 
// define turns with encoder
#define GYRO_KP             1.0 // change this to meet turn target
#define GYRO_KI             0.0
#define GYRO_KD             0.5
#define GYRO_TOLERANCE      1.0
#define GYRO_SETTLING       200
 
// define distance with encoders
#define EOPD_KP             1.6 // MUST TUNE THIS FOR THE EOPD SENSOR
#define EOPD_KI             0.0
#define EOPD_KD             0.0
#define EOPD_TOLERANCE      0.5 // MUST TUNE THIS
#define EOPD_SETTLING       200
 
#define CLICKS_PER_INCH     118.5 // change this to adjust actual distance
 
#define EVTTYPE_PIDDRIVE    (EVTTYPE_NONE + 1)

GYRO        g_gyro;         //create the gyro object.
DRIVE       g_drive;        //create the drive object.
PIDCTRL     g_yPidCtrl;     //create the Y PID controller
PIDCTRL     g_eopdPidCtrl;  //create EOPD PID controller
PIDCTRL     g_turnPidCtrl;  //create the turn PID controller
PIDDRIVE    g_pidDrive;     //create the PID drive object
PIDDRIVE    g_eopdPidDrive; //create the PID drive object for EOPD
SM          g_autoSM;       //create the autonomous state machine object
 
// calculate all the math
float PIDCtrlGetInput(PIDCTRL &pidCtrl)
{
    float inputValue = 0.0;
 
    if (IsSameReference(pidCtrl, g_yPidCtrl))
    {
        inputValue = (nMotorEncoder[leftWheel] + nMotorEncoder[rightWheel])/2.0/CLICKS_PER_INCH;
    }
    else if (IsSameReference(pidCtrl, g_turnPidCtrl))
    {
        inputValue = GyroGetHeading(g_gyro);
    }
    else if (IsSameReference(pidCtrl, g_eopdPidCtrl))
    {
        inputValue = SensorValue[htEOPD]; // OR HOWEVER WAY YOU READ THE EOPD SENSOR
    }
    return inputValue;
}
 
task main()
{
    GyroInit(g_gyro, gyroSensor);
    // Initialize the drive object.
    DriveInit(g_drive, leftWheel, rightWheel);
    // Initialize the yPidCtrl object.
    PIDCtrlInit(g_yPidCtrl,
    ENC_KP, ENC_KI, ENC_KD,
    ENC_TOLERANCE, ENC_SETTLING);
    // Initialize the turnPidCtrl object.
    PIDCtrlInit(g_turnPidCtrl,
    GYRO_KP, GYRO_KI, GYRO_KD,
    GYRO_TOLERANCE, GYRO_SETTLING);
    // Initialize the pidDrive object.
    PIDDriveInit(g_pidDrive, g_drive, g_yPidCtrl, g_turnPidCtrl);
    PIDCtrlInit(g_eopdPidCtrl,
    EOPD_KP, EOPD_KI, EOPD_KD,
    EOPD_TOLERANCE, EOPD_SETTLING,
    PIDCTRLO_ABS_SETPT | PIDCTRLO_INVERSE_INPUT);
    PIDDriveInit(g_eopdPidDrive, g_drive, g_eopdPidCtrl, g_turnPidCtrl);
    // Initialize the autonomous state machine and start it.
    SMInit(g_autoSM);
    SMStart(g_autoSM);
 
    while (true)
    {
        GyroTask(g_gyro);
        // display debug info on nxt
        displayTextLine(0, "state=%d", SMGetState(g_autoSM));
        PIDCtrlDisplayInfo(1, g_eopdPidCtrl);
        PIDCtrlDisplayInfo(3, g_turnPidCtrl);
 
        // Check if state machine is ready.
        if (SMIsReady(g_autoSM))
        {
            int currState = SMGetState(g_autoSM);
            switch (currState)
            {
            case SMSTATE_STARTED:
                // Drive forward for 24 inches.
                PIDDriveSetTarget(g_eopdPidDrive,
                5.0,
                0.0,
                false,
                &g_autoSM,
                EVTTYPE_PIDDRIVE);
                // Tell state machine to wait until done.
                SMAddWaitEvent(g_autoSM, EVTTYPE_PIDDRIVE);
                // Tell state machine to go to next state when done.
                SMWaitEvents(g_autoSM, currState + 1);
                break;

            case SMSTATE_STARTED + 1:
                if (rightLifter < cetner goal height)
                {
                    keep lifting;
                }
                else
                {
                    SMSetState(g_autoSM, currState + 1);
                }
                break;

            default:
                // We are done, stop the state machine.
                SMStop(g_autoSM);
                break;
            }
        }
        //
        // Execute all periodic tasks.
        //
        PIDDriveTask(g_pidDrive);
        DriveTask(g_drive);
        wait1Msec(20);
    }



Fri Feb 20, 2015 7:56 pm
Profile
Rookie

Joined: Sat Jan 31, 2015 1:14 am
Posts: 38
Post Re: PID Control
MHTS wrote:
Since you decided to make your gyro code ready before you get the gyro sensor, did you try your code in RVW and see if it is working? The gyro code you have will not work because first, your GRYO_KP is zero and your turn target when calling PIDDriveSetTarget is also zero. I would recommend you try this out in RVW. You should be able to do everything in RVW granted that when you finally get the gyro sensor, you probably need to retune all the KP's and INCHES_PER_CLICK counts. Well, I suspect by the time you read my reply, you probably already receive the gyro sensor.
Regarding the compass sensor not working, did you try the compass test code I gave you? What about Xander's compass test code? If both are not working, I have no clue why. May be it needs calibration. We always avoid compass sensor because it is finicky. A lot of things can interfere with it and it's sensitive on its mounting position. You may want to play with where you mount the compass sensor (away from the motors, for example).
Regarding PID with encoders only, it should just work, not as accurate as gyro but when tuned correctly should do a decent job turning. Unfortunately, I don't understand your description of the issue. For example, what do you mean by "turns seems weird"? And what are the "random numbers"? The code looks fine but I am not sure about all the parameters are tuned correctly (e.g. TURN_KP, DEGREES_PER_CLICK... etc). If tuned correctly, it should work.
BTB1337 wrote:
And one final question, is there a way to add in to the code under one of the SM States some code that pretty much says (in pseudo code)
Code:
if eopd < 5
{
   reverse
}
else if eopd > 5
{
   forward
{
else if eopd = 5
{
   return to main code
}

// new state

while encoder rightLifter < center goal height
{
   lift elevator
}


or something along those lines. Is that possible?

Regarding the EOPD sensor code, you can do the following:
Code:
#pragma config(Hubs,  S1, HTMotor,  HTMotor,  HTServo,  none)
#pragma config(Sensor, S2,     irSeeker,       sensorI2CCustom)
#pragma config(Sensor, S3,     gyroSensor,     sensorI2CHiTechnicGyro)
#pragma config(Sensor, S4,     htEOPD,         sensorI2CCustom)
#pragma config(Motor,  mtr_S1_C1_1,     rightWheel,    tmotorTetrix, openLoop, encoder)
#pragma config(Motor,  mtr_S1_C1_2,     leftWheel,     tmotorTetrix, openLoop, reversed, encoder)
#pragma config(Motor,  mtr_S1_C2_1,     rightLifter,   tmotorTetrix, PIDControl, reversed)
#pragma config(Motor,  mtr_S1_C2_2,     leftLifter,    tmotorTetrix, PIDControl)
#pragma config(Servo,  srvo_S1_C3_1,    servo1,               tServoNone)
#pragma config(Servo,  srvo_S1_C3_2,    servo2,               tServoNone)
#pragma config(Servo,  srvo_S1_C3_3,    irServo,              tServoStandard)
#pragma config(Servo,  srvo_S1_C3_4,    goalHook,             tServoStandard)
#pragma config(Servo,  srvo_S1_C3_5,    leftDoor,             tServoStandard)
#pragma config(Servo,  srvo_S1_C3_6,    rightDoor,            tServoStandard)
//*!!Code automatically generated by 'ROBOTC' configuration wizard               !!*//
 
// include files
#include "ftclib/trcdefs.h"
#include "ftclib/dbgtrace.h"
#include "ftclib/gyro.h"
#include "ftclib/sm.h"
#include "ftclib/drive.h"
#include "ftclib/pidctrl.h"
#include "ftclib/piddrive.h"
 
// define distance with encoders
#define ENC_KP              1.6 // change this to meet distance target
#define ENC_KI              0.0015
#define ENC_KD              0.5
#define ENC_TOLERANCE       1.0
#define ENC_SETTLING        200
 
// define turns with encoder
#define GYRO_KP             1.0 // change this to meet turn target
#define GYRO_KI             0.0
#define GYRO_KD             0.5
#define GYRO_TOLERANCE      1.0
#define GYRO_SETTLING       200
 
// define distance with encoders
#define EOPD_KP             1.6 // MUST TUNE THIS FOR THE EOPD SENSOR
#define EOPD_KI             0.0
#define EOPD_KD             0.0
#define EOPD_TOLERANCE      0.5 // MUST TUNE THIS
#define EOPD_SETTLING       200
 
#define CLICKS_PER_INCH     118.5 // change this to adjust actual distance
 
#define EVTTYPE_PIDDRIVE    (EVTTYPE_NONE + 1)

GYRO        g_gyro;         //create the gyro object.
DRIVE       g_drive;        //create the drive object.
PIDCTRL     g_yPidCtrl;     //create the Y PID controller
PIDCTRL     g_eopdPidCtrl;  //create EOPD PID controller
PIDCTRL     g_turnPidCtrl;  //create the turn PID controller
PIDDRIVE    g_pidDrive;     //create the PID drive object
PIDDRIVE    g_eopdPidDrive; //create the PID drive object for EOPD
SM          g_autoSM;       //create the autonomous state machine object
 
// calculate all the math
float PIDCtrlGetInput(PIDCTRL &pidCtrl)
{
    float inputValue = 0.0;
 
    if (IsSameReference(pidCtrl, g_yPidCtrl))
    {
        inputValue = (nMotorEncoder[leftWheel] + nMotorEncoder[rightWheel])/2.0/CLICKS_PER_INCH;
    }
    else if (IsSameReference(pidCtrl, g_turnPidCtrl))
    {
        inputValue = GyroGetHeading(g_gyro);
    }
    else if (IsSameReference(pidCtrl, g_eopdPidCtrl))
    {
        inputValue = SensorValue[htEOPD]; // OR HOWEVER WAY YOU READ THE EOPD SENSOR
    }
    return inputValue;
}
 
task main()
{
    GyroInit(g_gyro, gyroSensor);
    // Initialize the drive object.
    DriveInit(g_drive, leftWheel, rightWheel);
    // Initialize the yPidCtrl object.
    PIDCtrlInit(g_yPidCtrl,
    ENC_KP, ENC_KI, ENC_KD,
    ENC_TOLERANCE, ENC_SETTLING);
    // Initialize the turnPidCtrl object.
    PIDCtrlInit(g_turnPidCtrl,
    GYRO_KP, GYRO_KI, GYRO_KD,
    GYRO_TOLERANCE, GYRO_SETTLING);
    // Initialize the pidDrive object.
    PIDDriveInit(g_pidDrive, g_drive, g_yPidCtrl, g_turnPidCtrl);
    PIDCtrlInit(g_eopdPidCtrl,
    EOPD_KP, EOPD_KI, EOPD_KD,
    EOPD_TOLERANCE, EOPD_SETTLING,
    PIDCTRLO_ABS_SETPT | PIDCTRLO_INVERSE_INPUT);
    PIDDriveInit(g_eopdPidDrive, g_drive, g_eopdPidCtrl, g_turnPidCtrl);
    // Initialize the autonomous state machine and start it.
    SMInit(g_autoSM);
    SMStart(g_autoSM);
 
    while (true)
    {
        GyroTask(g_gyro);
        // display debug info on nxt
        displayTextLine(0, "state=%d", SMGetState(g_autoSM));
        PIDCtrlDisplayInfo(1, g_eopdPidCtrl);
        PIDCtrlDisplayInfo(3, g_turnPidCtrl);
 
        // Check if state machine is ready.
        if (SMIsReady(g_autoSM))
        {
            int currState = SMGetState(g_autoSM);
            switch (currState)
            {
            case SMSTATE_STARTED:
                // Drive forward for 24 inches.
                PIDDriveSetTarget(g_eopdPidDrive,
                5.0,
                0.0,
                false,
                &g_autoSM,
                EVTTYPE_PIDDRIVE);
                // Tell state machine to wait until done.
                SMAddWaitEvent(g_autoSM, EVTTYPE_PIDDRIVE);
                // Tell state machine to go to next state when done.
                SMWaitEvents(g_autoSM, currState + 1);
                break;

            default:
                // We are done, stop the state machine.
                SMStop(g_autoSM);
                break;
            }
        }
        //
        // Execute all periodic tasks.
        //
        PIDDriveTask(g_pidDrive);
        DriveTask(g_drive);
        wait1Msec(20);
    }


Note that I just modified your code and did not compile it or do any testing, so there may be a few typos and such. This shows you the beauty of object oriented programming (i.e. you can create multiple instances of an object). In this case, not only you have a g_pidDrive object, you can create another PIDDrive object called g_eopdPidDrive. The EOPD piddrive object uses the EOPD sensor as your "Y direction control" and the gyro as your "Turn control". The only thing you have to be careful about is that when using the EOPD sensor, you must do Absolute SetPoint (i.e. when you set the target to 5.0, you don't want to do current EOPD distance + 5.0, you really just meant absolute 5.0). Secondly, unlike encoder distance which is increasing when the robot goes forward, the EOPD distance is decreasing when the robot goes forward. So you need to set the PIDCTRLO_INVERSE_INPUT flag.



turns out our gyro seems to have gotten lost in the mail and has not arrived and USPS seems to have failed once again...We are without a gyro sensor, a broken compass sensor and left with encoder pid or motor on wait motor off code. in regards to the encoder turns, it honestly gives a different I value each time I run, sometimes it ends up with T = 90 I = 60, T = 90 I = 98, T = 90 I = 89, etc. each time i run, it's inconsistant on the ending target value which makes it impossible to tune without knowing a solid set ending point each time, does that make sense? I'm sorry i can't explain it any better i'm kinda stressed and tired trying to fix all the problems that came up with the robot and having next to no program for tomorrow. Thanks for all your help btw


Fri Feb 20, 2015 8:43 pm
Profile
Guru
User avatar

Joined: Sun Nov 15, 2009 5:46 am
Posts: 1523
Post Re: PID Control
I assume the encoders worked for going forward, right? Let me check your code one more time and see if I can spot anything.


Fri Feb 20, 2015 8:47 pm
Profile
Guru
User avatar

Joined: Sun Nov 15, 2009 5:46 am
Posts: 1523
Post Re: PID Control
Okay, would you do check if both encoders are actually working correctly? I suspect one of the encoders may not work properly. Do a "run straight forward 48 inches" or so. Add the following code right below the PIDCtrlDisplayInfo lines:
Code:
At the beginning outside the while loop, do:
nMotorEncoder[leftWheel] = 0;
nMotorEncoder[rightWheel] = 0;
Inside the while loop and after the PIDCtrlDisplayInfo lines, add the following lines:
nxtDisplayTextLine(5, "LEnc=%d", nMotorEncoder[leftWheel]);
nxtDisplayTextLine(6, "REnc=%d", nMotorEncoder[rightWheel]);

At the end of the run, check the Left and right encoder values. They should be pretty close in value. If not, one of them is malfunctioning. If that's really the case, you will not have a proper turn.


Fri Feb 20, 2015 8:57 pm
Profile
Guru
User avatar

Joined: Sun Nov 15, 2009 5:46 am
Posts: 1523
Post Re: PID Control
In the worst case scenario if you can't figure out how to turn with encoders consistently, you can go back to dead reckoning by using time. For example:
Code:
        if (SMIsReady(g_autoSM))
        {
            int currState = SMGetState(g_autoSM);
            switch (currState)
            {
            case SMSTATE_STARTED:
                // Drive forward for 24 inches.
                PIDDriveSetTarget(g_pidDrive,
                24.0,
                0.0,
                false,
                &g_autoSM,
                EVTTYPE_PIDDRIVE);
                // Tell state machine to wait until done.
                SMAddWaitEvent(g_autoSM, EVTTYPE_PIDDRIVE);
                // Tell state machine to go to next state when done.
                SMWaitEvents(g_autoSM, currState + 1);
                break;

            case SMSTATE_STARTED + 1:
                //
                // Turn right at half power for 1 second.
                //
                ClearTimer(T1);
                DriveTank(g_drive, 50, -50);
                SMSetState(g_autoSM, currState + 1);
                break;

            case SMSTATE_STARTED + 2:
                if (Timer1[T1] >= 1000)
                {
                    DriveTank(g_drive, 0, 0);
                    // do what's next...
                }
                break;

            default:
                // We are done, stop the state machine.
                SMStop(g_autoSM);
                break;
            }
        }
        //
        // Execute all periodic tasks.
        //
        PIDDriveTask(g_pidDrive);
        DriveTask(g_drive);
        wait1Msec(20);
    }


Fri Feb 20, 2015 9:34 pm
Profile
Rookie

Joined: Sat Jan 31, 2015 1:14 am
Posts: 38
Post Re: PID Control
MHTS wrote:
I assume the encoders worked for going forward, right? Let me check your code one more time and see if I can spot anything.


Yes it goes forward and is tuned perfectly for that


Fri Feb 20, 2015 9:55 pm
Profile
Rookie

Joined: Sat Jan 31, 2015 1:14 am
Posts: 38
Post Re: PID Control
BTB1337 wrote:
MHTS wrote:
I assume the encoders worked for going forward, right? Let me check your code one more time and see if I can spot anything.


Yes it goes forward and is tuned perfectly for that


I just rebooted everything changed out all batteries and stuff and now the turning seems to be fairly good with only 1 degree error rather than 20+ haha, still if you find what could have caused it to prevent it please let me know :)


Fri Feb 20, 2015 10:21 pm
Profile
Guru
User avatar

Joined: Sun Nov 15, 2009 5:46 am
Posts: 1523
Post Re: PID Control
BTB1337 wrote:
Yes it goes forward and is tuned perfectly for that

Well, you could have a perfect "go forward" routine even if one encoder is malfunctioning. You only need one encoder to do "go forward". But you need both encoders for "turn". That's why I have you print out the reading of both the left and right encoders while "going forward". The two encoders should have very close values. If they are very different, you will have problems turning.


Fri Feb 20, 2015 10:22 pm
Profile
Guru
User avatar

Joined: Sun Nov 15, 2009 5:46 am
Posts: 1523
Post Re: PID Control
BTB1337 wrote:
I just rebooted everything changed out all batteries and stuff and now the turning seems to be fairly good with only 1 degree error rather than 20+ haha, still if you find what could have caused it to prevent it please let me know :)

What could have caused what to prevent what?


Fri Feb 20, 2015 10:26 pm
Profile
Rookie

Joined: Sat Jan 31, 2015 1:14 am
Posts: 38
Post Re: PID Control
MHTS wrote:
BTB1337 wrote:
I just rebooted everything changed out all batteries and stuff and now the turning seems to be fairly good with only 1 degree error rather than 20+ haha, still if you find what could have caused it to prevent it please let me know :)

What could have caused what to prevent what?


sorry kinda jumped subjects, the weird inconsistant turn E and I i got with encoder turning. i got it from plus or minus 20 degrees to plus or minus 2 or so degrees by rebooting everything and replacing batteries, any idea on how to narrow the turn errors to like 1 or so with encoders?


Fri Feb 20, 2015 10:33 pm
Profile
Guru
User avatar

Joined: Sun Nov 15, 2009 5:46 am
Posts: 1523
Post Re: PID Control
There are two issues. When you said +/- 20 degrees, are they from the reading of E on the NXT display? Or are they the degrees error from your physical observation? If it's the E value from the NXT display, PID is supposed to make error down to within tolerance (whatever it was). If your tolerance was 1 degree, then E should read 1 or less at the end. If it doesn't, then you need to tune Kp to the correct value until E ended up within tolerance. If it is your physical observation that the robot is 20 degree from where you want it to be, then you need to tune the DEGREES_PER_CLICK until the robot physically turn 90 degree if you set the target to 90.
Am I understanding your issue correctly?
If by changing to a fresh battery that the robot turns correctly, that's just the way PID behaves. I think one of my previous replies have talked about this subject. You need to change to a fresh battery for competition. If the battery is too weak, then the robot can't reach target. You can fix this somewhat by increasing Kp to a larger number. +/- 20 degree seems to be a lot. Usually, a weak battery will leave the error around a few degrees. In any case, play around with Kp to see if it improves things.


Sat Feb 21, 2015 2:47 am
Profile
Display posts from previous:  Sort by  
Reply to topic   [ 80 posts ]  Go to page Previous  1, 2, 3, 4, 5, 6  Next

Who is online

Users browsing this forum: No registered users and 2 guests


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 post attachments in this forum

Search for:
Jump to:  



Powered by phpBB © 2000, 2002, 2005, 2007 phpBB Group.
Designed by ST Software for PTF.