The following training material is intended to provide the student with a software development process capable of robust, good quality software.
People who don't use revision control tend to keep several copies of their software, each from a different date. There are several problems with that approach. It is awkward to see what changed, difficult to manage collaboratively, and takes more disk space. By contrast, revision control software gives these features:
There are numerous programs around for revision control. The ones we are interested in are CVS and Subversion. CVS is older and more crusty. If you have access to Subversion, I recommend it. Collaboration software has been maintained under CVS. Cresent Valley software has been under Subversion. I understand that migrating from CVS to Subversion is not difficult since the commands are similar.
At this point I have only written instructions for CVS: CvsInstructions.html . Subversion should be similar.
The job of the programmer is communication. You must communicate to the computer the code to implement your intention. But just as important is to communicate your intention to anyone reading your code. Thus clear code must be your objective. It is easier to get clear code to be correct. And it is easier to understand and debug code that is written clearly.
Innovation First Default Code tries to perform math directly on the 8-bit sensor and PWM values. This is greatly complicated by the fact that these variables do not implement sign correctly. Hence many math operations which should be straightforward give erroneous results.
In these "unsigned" numbers the zero value is in the middle of the range, or about 127. The value 0 is the most negative, or -127. The value 255 is the most positive, or 128.
Let's say you want to invert a sensor. If it were a regular "signed" variable you could simply say
myvar=-myvarBut this doesn't work with unsigned variables. In fact, such a result is not defined. The required formula is insteadmyvar=255-myvarBut this formula obscures the purpose you are trying to accomplish: inverting the number. And in addition to obfuscation, the formula contains a "magic number", 255.Let's consider another example, joysticks. You may want to use the y joystick to control forward/backward power and the x joystick to control turning. Assuming you are using tank/skid steering you will want to use an equation that does the following action:
left_pwm = joy_y+joy_xright_pwm = joy_y-joy_xBut those straightforward equations don't work correctly with unsigned values. Instead, to make the numbers function Innovation First provides an example like this:
left_pwm = Limit_Mix(2000 + p1_y + p1_x - 127)right_pwm = Limit_Mix(2000 + p1_y - p1_x + 127)How amazingly obscure! And compounding the ugliness is that these funky equations with their several magic numbers are repeated in the code. There has to be a better way.
We can see from this example that the Innovation First Default Code fails to provide clearly written code. So, how can we improve on this?
One way is to convert these unsigned variables into signed variables. And since we have several sensors and several PWMs, we will want to do this repeatedly. This is a job for functions.
With functions we can hide and minimize the ugliness.
int byte2int(unsigned char ifibyte);This function will take as input an unsigned 8-bit value and return a signed int with equivalent values. Thus most of our code can have simple, straightforward equations. We can't eliminate the ugliness. But we stuff all of it into the byte2int() function. And then we document and test byte2int().
In summary, we should consider using a function
Everything should be made as simple as possible, but not simpler. --- Albert Einstein
Sometimes we have to put magic numbers into functions. But the caller should not have to know that magic number to make use of the function. A well designed function should be easy to use.
Consider Limit_Mix() from the Innovation First Default Code. The caller is expected to add 2000 (a magic number) to add 2000 to the sensor expression before calling Limit_Mix(). The 2000 will then be subtracted back out before the function returns.
The number 2000 is magic and is completely arbitrary. There is nothing in the name of the function that suggests it. So this function has not clarified the code, but made it even more obscure. In short, the number 2000 does not belong in this function.
Why did the author use 2000 in Limit_Mix()? The purpose was to prevent expressions of unsigned values from going negative. By adding it before subtracting p1_x, he ensured that the expression would always be positive.
He did that because unsigned numbers cannot represent negatives. In fact there are several approaches he could have used to avoid that problem. For instance consider a function we will call Limit_Diff():
unsigned char Limit_Diff(unsigned char positive, unsigned char negative);In this function we can choose to return a PWM value that adds "positive", subtracts "negative", and translates the result to the 0 to 255 range that the pwms expect. Finally it should prevent the result from overflowing. The code could look something like this:
unsigned char Limit_Diff(unsigned char positive, unsigned char negative)
{
int diff;
diff = (int)positive - (int)negative + ZERO;
diff = (diff<0) ? 0 : diff; // Limit negative bound
diff = (diff>MAXNUM) ? MAXNUM : diff; // Limit positive bound
return((unsigned char)diff);
}
This function largely takes the place of Limit_Mix(). But it eliminates that ugly number 2000. It does require the caller to split up the expression into its positive and negative parts and pass them separately. Thus the equivalent calls become:
left_pwm=Limit_Diff(p1_y, p1_x)
right_pwm=Limit_Diff(p1_y, 255-p1_x)
That is better. But it is rather specialized and doesn't help us with anything other than taking differences of these funky numbers. What if we want to reduce the strength of the x dimension, so as to make the steering less touchy? This function doesn't help at all. And you can't simply say
p1_x/2The result would be only being able to turn in one direction. There's gotta be another way.
We reduce the pollution from magic numbers in going from Limit_Mix() to Limit_Diff(). But we remained in that awkward number system of sensors and PWMs. We get more generality by converting to signed variables before doing our math.
int joy_x, joy_y;
joy_x = byte2int(p1_x);
joy_y = byte2int(p1_y);
Now we can do simple arithmetic on joy_x and joy_y.
int left_power, right_power;
left_power = joy_y + joy_x/2;
right_power = joy_y - joy_x/2;
This is not complete since we haven't limited the range of left_power and right_power. That code could look something like:
left_power = (left_power < -127) ? -127 : left_power;
left_power = (left_power > 127) ? 127 : left_power;
right_power = (right_power < -127) ? -127 : right_power;
right_power = (right_power > 127) ? 127 : right_power;
Kinda ugly, huh? It displays magic numbers and is repetitive. Sounds like an opportunity for another function.
int Int_Range_Limit(int unlimited)
{
int limited;
limited = (unlimited < MINLIMIT) ? MINLIMIT : unlimited;
limited = (unlimited > MAXLIMIT) ? MAXLIMIT : unlimited;
return(limited);
}
And now we can replace those left_power and right_power limiting expressions with this:
left_power = Int_Range_Limit(left_power);
right_power = Int_Range_Limit(right_power);
which is a lot more readable. Alas, we are not yet done. We are now manipulating signed int's. We can't simply write these to PWMs and expect it to work since when an int goes negative the PWM will not represent that correctly.
The most obvious solution is
left_pwm = (unsigned char)(left_power+128);
right_pwm = (unsigned char)(right_power+128);
That works. But once again we have a magic number popping up. If we put this into a function we can hide the magic number.
unsigned char int2byte(int value)
{
unsigned char ifibyte;
ifibyte = 0xFF & (value + ZERO);
return (ifibyte);
}
In summary, the calling code will look like this:
int joy_x, joy_y;
int left_power, right_power;
joy_x = byte2int(p1_x);
joy_y = byte2int(p1_y);
left_power = joy_y + joy_x/2;
right_power = joy_y - joy_x/2;
left_pwm = int2byte( Int_Range_Limit(left_power) );
right_pwm = int2byte( Int_Range_Limit(right_power) );
What have we accomplished?
Is there anything else we would want to include into these functions? Maybe. We could move the Int_Range_Limit() code into int2byte() and so eliminate the need for Int_Range_Limit(). But we need to be careful that we aren't obscuring bugs. If our code was supposed to keep the PWM value in range before calling int2byte() then any use of the Int_Range_Limit() code inside it would be obscuring a bug in our calling code. You need to decide whether you consider this a bug.
In the 2005 collaboration library the Int_Range_Limit() code is incorporated into int2byte(), but when it is invoked it is considered an error and a message is printed.
Another capability that could be put into byte2int() and int2byte() would be inversion. By providing an "invert" parameter these functions could translate positive to negative when going to or from "unsigned" bytes. The 2005 collaboration library has these functions implemented that way.
The functions shown above were all pure: they didn't look at any global variables and didn't affect any globals. Most of the time this is what you will want to do.
The functions shown above were also reentrant. With the FRC this is not required since the FRC doesn't support threads and the functions are not recursive. But part of what we are doing here is teaching good programming style. Writing your functions so that they are reentrant will mean you avoid troubles later in your programming career.
There are several ways to ensure your functions are reentrant. The easiest is:
To put it another way, in most cases when beginning programmers access global variables or static variables from within their functions this is happening because the function is poorly written.
But sometimes we want to maintain state in a function. I would not recommend this for beginner programmers, but you should be aware of the possibility. When we do this we give up reentrancy.
When would we want to maintain state? One possibility is a function that manipulates a robotic arm. You might want that function to refuse to raise the arm when the arm is in an already fully raised position. Then we need for the function to know the position of the arm. For now I just want you to be aware of this possibility. I will cover the details in a future lesson on Object Programming in C.