Low Level C Programming
Low Level C Programming
Variables __________________________________________________________________ 3
Types, Declarations, and Format Specifiers _____________________________________________ 3 Keyboard Input / Screen Output ______________________________________________________ 4 Type Conversions and Typecasting____________________________________________________ 5 Variable Scope ___________________________________________________________________ 6 Storage Class of Variables___________________________________________________________ 6
Functions __________________________________________________________________ 8
Writing and Using Functions ________________________________________________________ 8 Function Prototypes________________________________________________________________ 9 Pointers, Addresses, and Function Arguments ___________________________________________ 9
Operators_________________________________________________________________ 20
Arithmetic, Logical, and Bitwise Operators ____________________________________________ 20 Masking ________________________________________________________________________ 22
Cryptic ___________________________________________________________________ 24 Misc. Functions in Borland __________________________________________________ 25 PC/AT Computer Architecture _______________________________________________ 26
Introduction History
C was originally written in the early 1970s and implemented in 1978 by Dennis Richie at Bell Telephone Laboratories, Inc. It is associated with UNIX because the operating system was actually written in C. The language is sometimes called a system programming language, because of its usefulness in writing operating systems. Operating systems were originally written in low level languages like assembly language. C is a high level language with the necessary low level resources to deal with hardware. This property makes the modern C language a versatile tool. In 1983, the American National Standard Institute (ANSI) created a committee to provide a standard definition of the C programming language. They wrote the ANSI standard for C. Most C compilers implement the ANSI standard and more. Later, C++ was developed. It is a superset of the C programming language. It has all the features of C as well as additional ones. Most importantly C++ is uses object-oriented programming (OOP). Writing an OOP program is very different from writing a procedural program. Most common languages, including C, are procedural. You may however, write C programs for a C++ compiler, which is what we will do in this class.
Compilers
Programming languages can be divided into two different types: compiled and interpreted. Interpreters (e.g. original BASIC and MATLAB) proceed through a program by translating and then executing single instructions, one at a time. This is very easy when writing the code and debugging. In fact, many compiled languages will run in an interpreted mode in the integrated development environment (IDE) for debugging purposes. Many of you have probably used the single step mode of some IDE (e.g. the IDE for Visual Basic). Compiled programs must be translated into machine language before they can be executed. Compilers (such as C and JAVA) translate an entire program into machine language before executing any of the instructions. A compiler or interpreter is itself a computer program that accepts written code for an editor as input and generates a corresponding machine-language program as output. The original written code is called the source code, and the resulting machine-language program is called the object code. Another program, called a linker, operates on object code files and combines them into a resulting executable file (program.exe). This is known as linking. Since the program doesnt execute single instructions, one at a time, it is much faster and can be run outside of the IDE. When you generate an executable from source code you go through both the compiling and linking processes.
Some C Basics
C is case sensitive. This means that Dog, dog, DOG, dOg, and doG are all different identifiers in C. The word "main" is very important. It must appear once, and only once in every C program. It defines the main routine. This is the point where execution is begun when the program is executed. It does not have to be the first statement in the program but it must exist as the entry point for the program.
#include <stdio.h> void main(void) { printf(Here is your first C program) } // This is also a comment
Comments are delineated by /* */ and by // See the previous example code for examples of comments. A comment is any additional text that you add to your code for clarification of what is taking place. All comments are ignored by the compiler, so they do not add to the file size of the executable program. Neither do they affect the execution of the program. In ANSI C, comments begin with the sequence /* and are terminated by the sequence */. Some compilers also implement the // sequence. Everything to the right of the // sequence, on the same line, is a comment. Braces, { }, define blocks There are many types of blocks (i.e. groups of code) in C. They all are defined by { }. In the previous example the { } define the main function block. In the next example the if block is nested in (i.e. defined inside of) the main block. In other words the if block of code is part of the main routine. main() { int data; data = 2; if (data == 2) { // this is the start of main the if block printf("Data is now equal to 2\n"); data = 3; data = 2*3 + 5; } // this is the end of the if block } /* this is the end the main block */ // this is the start of the main block
The semicolon, ;, defines the end of a single statement In the previous example there are several statements that are followed by a semicolon. In C a statement does not have to be on a single line. Its end is determined by the semicolon rather than by the end of the line. For example, the following two statements are the same as far as the compiler is concerned. data = 2*3 + 5; data = 2*3 + 5;
This next example contains several declarations and format specifiers for printing. The printf function is the standard print function used to output to the display. A format specifier is preceded by the % sign, and the value following the quotes is substituted in its place in the printed output. main(){ / ************************ VARIABLE DECLARATIONS *************/ / *************************************************************/ int a; /* simple integer type */ long int b; /* long integer type */ short int c; /* short integer type */ unsigned int d; /* unsigned integer type */ char e; /* character type */ float f; /* floating point type */ double g; /* double precision floating point */ int int_array[5]; /* 5 element array of ints */ float float_array[4][4] /* 4 by 4 matrix of floats */ a = 1023; b = 2222; c = 123; d = 1234; e = 'X'; f = 3.14159; g = 3.1415926535898; int_array[0] = 18; flaot_array[0][3] = 55; printf("a = %d\n",a); /* decimal output */ printf("a = %x\n",a); /* hexadecimal output */ printf("b = %ld\n",b); /* decimal long output */ printf("c = %d\n",c); /* decimal short output */ printf("d = %u\n",d); /* unsigned output */ printf("e = %c\n",e); /* character output */ printf("f = %f\n",f); /* floating output */ printf("g = %f\n",g); /* double float output */ printf("a = %d\n",a); /* simple int output */ printf("a = %7d\n",a); /* use a field width of 7 */ printf("a = %-7d\n",a); /* left justify in field of 7 */ printf("\n"); printf("f = %f\n",f); /* simple float output */ printf("f = %12f\n",f); /* use field width of 12 */ printf("f = %12.3f\n",f); /* use 3 decimal places */ printf("f = %12.5f\n",f); /* use 5 decimal places */ printf("f = %-12.5f\n",f); /* left justify in field */
In the preceding program arrays were also introduced. Arrays of any variable type can be used in C. Multidimensional arrays are also used. Array indices in C range from 0 to one less than the length of the array.
Input data can be entered into the computer from a standard input device (keyboard) by means of the C library function scanf(). This function can be used to enter any combination of numerical values and single characters. When a program uses the scanf() function to get data, the user must press the enter key after the data is entered. This is different from getche() or getch(), which wait for a single key to be pressed. The general scanf() function has the following prarmeters: scanf(control string, arg1, arg2, arg3,..., argx); Table 1-3. Scanf() conversion codes. %c %e %h %s single character floating point value in exponential format hexadecimal integer string pointer %d %f %i %u single decimal integer floating point value Integer unsigned decimal integer
void main() { float age; float days; printf(How many years old are you? ); scanf(%f , &age); days = age*365; printf(\nYou are %.1f days old.\n,days);
As explained earlier getche() is another function for inputting data, but only one character. This function is similar to getch() which inputs a character but does not echo it back to the screen.
//declarations with initializations // // // // // floatval inval is floatval floatval floatval is 1 is is is 1.833 6.5 (5.5 + 1) 6.5 (5.5 + 1) 7.0 (5.5 + 1.5)
Notice the difference between the last two results. In one result the division is an integer division because both values are ints. In the other the result, the division is floating point division because one of the operands is a float value. Type casting can be used to change the type of variable or expression during the evaluation of operators and when values are passed to functions. To type cast a variable or expression the desired type is put in parentheses before the variable or expression. Type casting is demonstrated in the program below. #include <stdio.h> void main(void) { int two = 2, three = 3; float fiveptfive = 5.5, floatval; floatval floatval floatval floatval floatval = = = = = fiveptfive/three; // floatval is 1.833 (int)fiveptfive/three; // floatval is 1.0 fiveptfive + three/two; // floatval is 6.5 (5.5 + 1) fiveptfive + (float)three/two; //floatval is 7.0 (5.5+1.5) fiveptfive + (float)(three/two); //floatval is 6.5 (5.5+1)
In the second statement, integer division is used because both operands are integers after the typecast is applied. In the fourth statement floating point division is used because one of the operands is a float. In the last statement, integer division is used because the type cast is applied to the expression after a division with two ints is performed.
Variable Scope
The scope of a variable is the part(s) of the program in which it is known. For example, variables that are declared in a function are only known in that function. These are local variables, and are not known in other functions. Actually the scope a local variable is the block in which it is declared. This includes main function block. A global variable is known throughout the program. It is declared outside of all blocks. The following program and discussion should help you understand scope. int g = 1; int func1(void); int func1(void); main() { int x; x = func1(); x = func2(); } int func1(void) { int output; output = g; return output; } int func2 (void) { int g = 3, x, y; x = 4; y = x + g; return y; } In the preceding code the variable g is a global variable. It is known in main and in func1. It can be modified and/or used in these these two functions. It is declared as a local variable in the func2 however. Therefore it cannot be modified or used here. The use of g in func2 refers to its own local variable. The variables x, y, and output are local variables. They are only known in their respective functions. The statement x = 4 in func2 does not affect the x in the main function.
// x is 1 // x is 7
#include <stdio.h> int increment(void); void main(void) { printf(%d\n, increment() ); printf(%d\n, increment() ); printf(%d\n, increment() ); } int increment(void) { static int number = 0; number = number +1; return (number); }
Functions
Subroutines, or functions as they are called in C, are used in almost all programming languages. A modular program is one that consists of subprograms that, when combined, create a working program. All subroutines are functions in C. There is no distinction made between those that do and those that do not return a value through the function name itself, like there is in Basic and Fortran.
The functiontype is the type of variable that is returned through the functions name (e.g. x = func1()). If the functiontype is omitted then the default is int. If no value is returned through the function name then it should be declared void and the return statement at the end omitted. The functiontype is any valid variable type including arrays, structures, etc. The argument list is a list of the variables passed to the function including their type. Whether or not the argument is passed back to the calling function by changing the variable that is passed, depends on whether it is a pointer or not. This is discussed in the next section. If no arguments are passed then the word void can be substituted for the argument list. We have been using functions already and will continue to do so. Your understanding of them will be increased through the examples in this tutorial. The following is a simple example of the use of functions. #include <stdio.h> void nothing(void); float second_function(int a, int b); main(){ int x = 1; unsigned char y = 2; float one_half; nothing(); one_half = second_function(x,y); printf("Second_function made one_half = %f = %d / %d\n",one_half,x,y); // function prototypes
void nothing(void){ printf("\n\nThis is a function that does nothing but\n"); printf("print some lines of code.\n\n\n\n"); } float second_function(int a, int b){ // there is no code because this is a simple example return ((float)a/b); }
Function Prototypes
A prototype is a declaration of a function and its argument list without the actual code for the function. The example programs to this point have included prototypes. All functions should be prototyped before they are used for the first time. It is customary to do this at the top of the program or in a header file. We will discuss header files in a later section. If the function is declared (written) in the same file that it is used in, then the prototype is not required. However, prototyping in this case will increase the rigor of the compilers type checking and reduce the chances for problems. If the function being used is in another file or in library then a prototype must be included in the file that it is called in. Even standard functions, like the printf function, must be prototyped before they are used. You may have questioned the purpose line #include <stdio.h> in many of the previous examples. The header file stdio.h contains the prototypes of the standard input/output functions of C.
// index is an int var, pt1 and pt2 are pointers //to integers
The following two rules are very important when using pointers and must be thoroughly understood. 1. A variable name with an ampersand in front of it defines the address of the variable and therefore points to the variable. 2. A pointer with a "star", (i.e. asterick) in front of it refers to the value of the variable pointed to by the pointer. The pointer by itself is the memory address where the value is stored. In the example program, since we have assigned pointer, pt1, to address location of index, we can manipulate the value of index by using either the variable name itself, or by using *pt1. Anywhere in the program where we want to use the variable index, we could use the name *pt1 instead, since they are identical in meaning until pt1 is reassigned to the address of some other variable. To add a little intrigue to the system, we have another pointer defined in the example program, pt2. Since pt2 has not been assigned a value, it contains garbage. Assigning a value to the integer variable *pt2 changes an arbitrary memory location. This is, of course, very dangerous since we do not what this address is being used for. Pointers are admittedly a difficult concept. However, they are used extensively in all but the most trivial C programs. It is well worth your time to read the previous material until you understand it thoroughly.
A pointer must be defined to point to some type of variable. Following a proper definition, it cannot be used to point to any other type of variable or it will result in a "type incompatibility" error. The reason for this is easily understood if it is considered that different variable types use different amounts of memory. For example an int uses two bytes of memory whereas a float uses four bytes. Not all forms of arithmetic are permissible on the pointer value itself, only those things that make sense, considering that a pointer is an address somewhere in the computer. It would make sense to add a constant to an address, thereby moving it ahead in memory that number of places. Likewise, subtraction is permissible, moving it back some number of locations. Adding two pointers together would not make sense because absolute memory addresses are not additive. Pointer multiplication is also not allowed, as that would be a funny number. Pointers as Function Arguments Thus far we have been discussing pointers and addresses without demonstrating their usefulness. The most common use of pointers is in the passing of arguments to functions. In C, if a simple variable name is passed to a function then its value cannot be changed by the function. Only its value is passed (this is called pass by value). However, if the address of a variable is passed to the function then the variable can be changed by the function. If you want a function to change the value of a variable in its arrqument list then you must pass the address of the variable. The following program demonstrates the use of pointers as function arguments. #include <stdio.h> void first_function(int a, int b); void second_function(int *pa, int *pb); void main(){ int x = 0; int y = 0; first_function(x,y); second_function(&x, &y); }
// function prototypes
// x and y are not changed by this call // x and y are two after this call
void first_function(int a, int b){ int c; c = a + b; // c is zero (x + y from main) a = 1; // this does not change x and y b = 1; // in the main function } void second_function(int *pa, int *pb){ int c; c = *pa + *pb; // c is zero (x + y from main) *pa = 2; // this does changes x and y *pb = 2; // in the main function } Array Names as Pointers In C, an array variable name without any indices is a pointer to the the beginning of the array. For example, if an array is declared as char string[20] then string is the pointer (the address) to the first element of the array, a pointer to string[0]. However string[0] is the actual value contained in the first element. For all practical purposes string is a pointer. It does, however, have one restriction that a true pointer does not have; it cannot be changed like other pointers, and therefore always points to the same location. Given the preceding discussion it might be apparent that whenever an array is used as an argument to a function, it is a pass by reference argument. This means that the address is passed. The
10
following example should help illustrate the use of arrays as function arguments. Any of the three methods for declaring the array in the argument list that are shown below are valid. void first_function(int pa[10], int b); void second_function(int pa[], int b); void third_function(int *pa, int b); void main(){ int x[10], y; first_function(x,x[0]); // the pointer to x is passed and the value // of the tenth element is passed second_function(x,y); third_function(x,y); // function prototypes
void first_function(int pa[10], int b){ pa[1] = 1; // this changes the second element of x b = 1; // this does not change the first element of x } void second_function(int pa[], int b){ pa[1] = 1; // this changes the second element of x } void third_function(int *pa, int b){ pa[9] = 1; // this changes the last element of x } Memory-Mapped Register Access using Pointers Although it is nonstandard, and dangerous, it is possible to set a pointer to point at a specific memory address. This can be useful in computer architectures such as single-board computers and other similarly simple architectures where hardware devices are memory-mapped. This is one of the beauties (and responsibilities) of using C rather than a language that does not allow low-level programming. For example, suppose that a dsp (digital signal processor) card is being programmed and it has a digital output port whose register is mapped to the address 0x500017 by the hardware on the card. Then the following is an example of how to write a value out on this output port. Also, suppose that the card has an 4 counters that are used to keep track of 4 encoder inputs in hardware. These counter registers are mapped contiguously in memory at addresses 0x500030 to 0x500033. Then the following might is also an example of how to read the encoder counts. void main(){ int *DIGOUTD; // declare a pointer to be used to point the register int *QDCOUNT; // declare a pointer to be to point to the first counter int encoder_val[4]; // array to hold the encoder counts DIGOUTD=(int *)0x500017;//set pointer to the add of the register *DIGOUTD = 0x05; //set bits 0 and 2 of the output port to one QDCOUNT=(int *)0x500030; //set pointer to address of first counter for (i=0;i<4;i++) encoder_val[i] = QDCOUNT[i]; // read the encoders
In the example above it is very important that the programmer know the address of the register so that the pointer does not point to some important location other than the register. It is also important the programmer makes sure that the data type used for the pointer and the register are the same size.
11
#include
The #include statement is a preprocessor directive that tells the compiler to merge the specified file into the current file. The contents of that file become part of the current file. This is like a big copy and paste. #include is most commonly used for header files, such as stdio.h. In previous programs the angle brackets have been used around the name of the file (e.g. #include <stdio.h> ). This tells the compiler to look in the standard library directory for the file. If you are including a file that is not located here, then quotations should be used and the full path should be specified. The following is an example of this. #include c:\mydir\myfile.h
Header Files
Header files typically contain function prototypes, global variables, and constants used by a library or program. For example the function prototype for the printf() is in stdio.h header file. Many compilers are smart enough to include the right standard header files in your program. Often however you may get an error of the effect, function funcname should have a prototype. This should tell you that you probably have not included the header file for the library containing the function, funcname. If you do not know which header file it is in, then the easiest solution is to use the context sensitive help that is available in most IDEs.
#define
The #define directive (sometimes called a macro) can be used to define symbolic constants within a C program. Constants are useful because they save memory for variables and also speed up the program in many cases. After you define an expression you may use it as often as you like. By convention, C programmers use uppercase characters for #define identifiers. Macro definitions are usually placed at the beginning of a file. The macro definition can be accessed from its point of definition to the end of the file. The following rules apply when you create macros: The name for the macro must follow the rules set aside for any other identifier in C. Most importantly the macro name cannot contain spaces. The macro definition should not be terminated by a semicolon unless you want the semicolon included in your replacement string. The following example demonstrates the use of constants. #define PI 3.1415 #define TWOPI 2*PI void main(void) { float circumference, area, radius = 5.0; circumference = TWOPI * radius; area = PI * radius * radius;
12
We will cover the conditional expression, the one in parentheses, later. Until then, simply accept the expressions for what you think they should do, and you will probably be correct. Several things must be pointed out regarding the while loop. First, if the variable count were initially set to any number greater than 5, the statements within the loop would not be executed at all, so it is possible to have a while loop that never executes. Secondly, if the variable were not incremented in the loop the loop would never terminate. Finally, if there is only one statement to be executed within the loop, it does not need braces but can stand alone, directly after the conditional.
Several things must be pointed out regarding this statement. Since the test is done at the end of the loop, the statements in the braces will always be executed at least once. Secondly, if "i" were not
13
changed within the loop, the loop would never terminate, and hence the program would never terminate. Finally, just like for the while loop, if only one statement will be executed within the loop, no braces are required. It should come as no surprise to you that these loops can be nested. That is, one loop can be included within the compound statement of another loop. The nesting level has no limit.
The first field, the initializing field, contains the expression "index = 0. Any expressions in this field are executed prior to the first pass through the loop. There is essentially no limit as to what can go here, but good programming practice is to keep it simple. Several initializing statements can be placed in this field, separated by commas. The second field, in this case containing "index < 6", is the conditional. The test is done at the beginning of each pass through the loop. It can be any expression which will evaluate to a true or false. (More will be said about the actual value of true and false later.) The expression contained in the third field is executed each time the loop is executed, but it is not executed until after those statements in the main body of the loop are executed. This field, like the first, can also be composed of several operations separated by commas. Following the for() expression is any single or compound statement which will be executed as the body of the loop. In nearly any context in C, a simple statement can be replaced by a compound statement that will be treated as if it were a single statement as far as program control goes.
The if statement
The following program is an example of our first conditional branching statement, the if. Notice first, that there is a for loop with a compound statement containing two if statements. This is an example of how statements can be nested. It should be clear to you that each of the if statements will be tested 10 times. /* This is an example of the if and the if-else statements */ main(){ int data; for(data = 0;data < 10;data = data + 1) { if (data == 2) printf("Data is now equal to %d\n",data); if (data < 5) printf("Data is now %d, which is less than 5\n",data); else printf("Data is now %d, which is greater than 4\n",data); } } /* end of for loop */
14
Consider the first if statement. It starts with the keyword "if" followed by an expression in parentheses, the conditional. If the expression is evaluated and found to be true, the single statement following the if is executed, and if false, the statement is skipped. Here too, the single statement can be replaced by a compound statement. The expression "data == 2" is simply asking if the value of data is equal to 2, this will be explained in detail in the next chapter.
15
#include <stdio.h> #include <conio.h> void dog(void); void main(void) { char ch; printf("*************Main Menu***************\n"); printf("1 Go to number one\n"); printf("2 Go to number two\n"); printf("3 Enter the function dog\n"); printf("Enter you choice now "); ch=getche(); switch (ch) { case '1': printf("\nYou have chosen number one\n"); break; case '2': printf("\nYou have chosen number two\n"); break; case '3': dog(); break; } } void dog() { printf("\nThis is the function dog called from case 3"); } Once an entry point is found, statements will be executed until a break is found or until the program drops through the bottom of the switch braces. In case 3 the program goes into the function dog(), then the program returns to main().
16
The notation for a base 16 number is a subscript of 16 after the number or an h behind the last digit. For distinction, sometimes base 10 is delineated with subscript of 10 behind or a d behind the last digit. Often no distinction is made for a base 10 number. Binary numbers use a b at the end of the number or the subscript 2. Examples: 35 = 35d = 3510 = 23h = 2316 107 = 107d = 10710 = 6Bh = 6B16
Example: 6 = 6d = 610 = 0110b = 01102 The key to understanding different base systems is in the placeholders. The first digit in Decimal is the 1s spot, the second is the 10s spot, the third is the 100s spot, etc. The following figure demonstrates the concept of placeholders using the same number in the three different bases.
3674
1000s = 10 100s = 102 10s = 101 1s = 100
3
E5A16
3674 = 2048+1024+512+64+16+8+2
17
Hex to Decimal and Binary to Decimal This is demonstrated in Figure 1-1. It simply involves multiplication of the digit in the placeholder by the value of the placeholder and summing each of these values. Binary to Hex and Hex to Binary Binary and Hex have a simple relationship because 16 is 24. Each Hex digit corresponds to 4 bits. To convert from Binary to Hex, four bits at a time are converted to single Hex digit, starting with the right most four bits. Example: Convert 1110 0101 10102 to Hex 01012 = 4 + 1 = 5 = 516 10102 = 8 + 2 = 10 = A16 1110 0101 10102 = E5A16 11102 = 8 + 4 + 2 = 14 = E16
To convert from Hex to Binary this process is reversed. Each Hex digit is converted to four bits. Example: Convert E5A16 to Binary 516 = 5 = 4 + 1 = 01012 A16 = 10 = 8 + 2 = 10102 E5A16 = 1110 0101 10102 E16 = 12 = 8 + 4 + 2 = 11102
To convert from Binary to Decimal it is often easier to convert to Hex, and then to Decimal. Decimal to Hex and Decimal to Binary Converting Decimal to Hex and Decimal to Binary is the most difficult, you must use modulo division. Example: Convert 3674 to Hex 3674 / 163 = 3674/4096 = 0 with remainder of 3674 3674 / 162 = 3674/256 = 14 with remainder of 90 90 / 161 = 90/16) =5 with remainder 10 10/160 = 16/1 = 10 with remainder 0 3674 = E5A16
Decimal to Binary conversions can be accomplished in a similar manner or by converting Decimal to Hexadecimal and then Hexadecimal to Binary.
You may have noticed that the -127 is not simply 127 with the 16th bit set to 1. Integer math is usually faster in a computer if 2s compliments are used for negative numbers. The 2s compliment of a number is the negative of that number. To get a 2s compliment of a number, take the compliment (reverse all the bits) and add one.
18
Example: 2s compliment of 127 is -127 1111 1111 1000 0000 + 1 1111 1111 1000 0001 Example: 2s compliment of -127 is 127 0000 0000 0111 1110 + 1 0000 0000 0111 1111
Compliment of 127 Add one -127 Compliment of -127 Add one 127
A 360 KB disk has 360*1024 = 368,640 bytes (not 360,000) Commonly A/D and D/As are 12 bit Some A/D and D/As are 16 bit M = K*K = 10242 = 1,048,576 (sometimes 1,000,000) The 286, which has 24 bit addressing, has 16MB limit on Ram G = K*M = 10243= 1,073,741,824 (sometimes 1,000,000,000)
19
Relational operators are symbols used to compare two values. If the values compare correctly according to the relational operator, the expression is considered true: otherwise it is considered false. #include <stdio.h> void main() { int i=7; printf(i printf(i printf(i printf(i printf(i printf(i printf(i }
is equal to: %d\n\n,i); < 5 is %d\n, i<5); > 4 is %d\n, i>4); == 6 is %d\n, i==6); != 7 is %d\n, i!=7); <= 10 is %d\n, i<=10); >= 6 is %d\n, i>=6);
0 1 0 0 1 1
Before continuing with operators, a clarification of true and false in C is useful. Any nonzero value is considered true in C, including negative numbers and character values. Only a zero value is considered false. Therefore if a number is used as a conditional, the conditional is considered true for any value other than zero. The following program should help to demonstrate this. #include <stdio.h> void main() { int i=7, j=0; if (i) printf(true); if (j) printf(false); if (j-i) printf(true); if (0.000000001) printf(true); while (1); }
/*prints true*/ /*prints nothing */ /*prints true*/ /*prints true*/ /*endless loop */
20
In C a distinction is made between logical and bitwise operators. Logical operators operate on the logical value of an expression while bitwise operators operate on each bit of a value. This should become clear in the examples that follow.
Table 1-7. Logical and Bitwise Operators. && || ! Logical Operators Logical AND Logical OR Logical NOT & | ~ >> << ^ Bitwise Operators AND OR Compliment Shift Right Shift Left Exclusive OR (XOR)
The logical AND, as well as logical OR operators work on two operands to return a logical value based on the operands. The logical NOT operator works on a single operand. #include <stdio.h> main() { int i=3; int j=0; printf("Examples of logical expressions\n"); printf("-------------------------------\n"); printf("i && j %d\n",i&&j); printf("i || j %d\n",i||j); printf("!I %d\n",!i); printf("!j %d\n",!j); printf("i>0) && (j<7) %d\n",(i>0)&&(j<7)); printf("(i<0) || (j<7) %d\n",(i<0)||(j<7)); printf("!(i>5) || (j>0) %d\n",!(i>5)||(j>0));
The bitwise operators grant you low level control of values through C. Bitwise operators refer to the testing, setting, or shifting of the actual bits in a number. Even though the operations are performed on a single bit basis, an entire byte or integer variable is operated on in one instruction. The following examples are bitwise operations on unsigned char (byte) values. The same concepts apply to all integer type variables. Example: Bitwise AND 1010 0110 1100 0111 1000 0110 Example: Bitwise OR 1010 0110 1100 0111 1110 0111 Example: Compliment 1010 0110 0101 1001 Example: Right Shift 1010 0110 0000 1010 166 AND 199 equals 134 166 OR 199 equals 231 NOT 166 equals 89 166 right shift 4 equals 10
21
main() { unsigned char i=166, j=199; printf("Examples of bitwise operations \n"); printf("-------------------------------\n"); printf("i & j %d\n",i&j); printf("i | j %d\n",i|j); printf("~i %d\n",~i); printf("i>>4 %d\n",i>>4); printf("i<<20) %d\n",i<<2);
Masking
The general concept of masking is used in many I/O programming problems because hardware data is often manipulated at the bit level. For the following examples, suppose that a dsp (digital signal processor) card is being programmed and it has a digital output port whose register is mapped to the address 0x500017 by the hardware on the card. Also suppose that it has a digital input port whose register is mapped to the address 0x500015. Suppose that we wish to make bit 0 of the digital output port a zero, without changing any of the other bits. Then the following is an example of how that might be accomplished. Also, suppose that there are limit switches connected to the digital input port and we wanted to determine if switches connected to bits 4 or 5 of were being pushed (assume pushed=1). void main(){ int *DIGOUTD; // pointer to point the output ports register int *DIGINB; // pointer to point the input ports register int port_val; // temporary storage variable DIGOUTD=(int *)0x500017;//set pointer to the add of the output port DIGINB=(int *)0x500017; //set pointer to the add of the input port port_val = *DIGOUTD; // read current value on output port port_val = port_val & 0xfffffffe; // make bit 0 a zero *DIGOUTD = port_val; // write the new value out to the port port_val = *DIGINB; // read the input port if (port_val & 0x18) {perform some action}
In this example the mask, 0xfffe, is used to manipulate bit 0 specifically, without changing any of the other bits. This is done with the bitwise AND operator. xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx1 1111 1111 1111 1111 1111 1111 1111 1110 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx0 or xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx0 1111 1111 1111 1111 1111 1111 1111 1110 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx0 port_val AND 0xfffffffe will also sets bit 0 to 0 port_val AND 0xfffffffe will only sets bit 0 to 0
22
It doesnt matter what bit 0 was to begin with, zero or one, it is zero after the masking operation of the bitwise and with 0xfe. Furthermore, the other 31 bits are not affected by this operation. If they were one, then they are still one. If they were zero, then they are still zero. The second masking operation can be viewed as follows. Remember we want to determine if either bit 4 or bit 5 is one? Here we use a bitwise AND with 0x18. Remember that anything other than zero is true in C. xxxx xxxx xxxx xxxx xxxx xxxx xxx1 0xxx 0000 0000 0000 0000 0000 0000 0001 1000 0000 0000 0000 0000 0000 0000 0001 0000 or xxxx xxxx xxxx xxxx xxxx xxxx xxx0 0xxx 0000 0000 0000 0000 0000 0000 0001 1000 0000 0000 0000 0000 0000 0000 0000 0000 port_val AND 0x18 equals false port_val AND 0x18 equals true
As another example suppose we wanted to set bits 4 and 5 to one? We could use a bitwise OR with 0x18. It will set bits 4 and 5 to one, no matter what they are currently, without affecting the other bits. xxxx xxxx xxxx xxxx xxxx xxxx xxx1 1xxx 0000 0000 0000 0000 0000 0000 0001 1000 xxxx xxxx xxxx xxxx xxxx xxxx xxx1 1xxx or xxxx xxxx xxxx xxxx xxxx xxxx xxx0 0xxx 0000 0000 0000 0000 0000 0000 0001 1000 xxxx xxxx xxxx xxxx xxxx xxxx xxx1 1xxx port_val OR 0x18 sets bits 4 and 5 = 1 port_val OR 0x18 sets bits 4 and 5 = 1
There are many other examples of how to use masking. It just requires careful thought about what your are trying to achieve. How would you determine if either bit 4 or bit 8 were zero?
23
Cryptic
There are a few constructs used in C that may seem a little strange, but they greatly increase the efficiency of the compiled code and are used extensively by experienced C programmers. In the following program, several examples of these are given. main() { int x = 0,y = 2,z = 1025; float a = 0.0,b = 3.14159,c = -37.234; /* incrementing */ x = x + 1; x++; ++x; z = y++; z = ++y; /* decrementing */ y = y - 1; y--; --y; y = 3; z = y--; z = --y; /* arithmetic op */ a = a + 12; a += 12; a *= 3.2; a -= b; a /= 10.0; /* conditional expression */ a = (b >= 3.0 ? 2.0 : 10.5 ); if (b >= 3.0) a = 2.0; else a = 10.5; c = (a > b?a:b); c = (a > b?b:a); /* c will have the max of a or b */ /* c will have the min of a or b */ /* This increments x */ /* This increments x */ /* This increments x */ /* z = 2, y = 3 */ /* z = 4, y = 4 */ /* This decrements y */ /* This decrements y */ /* This decrements y */ /* z = 3, y = 2 */ /* z = 1, y = 1 */ /* /* /* /* /* This This This This This adds 12 to a */ adds 12 more to a */ multiplies a by 3.2 */ subtracts b from a */ divides a by 10.0 */
24
25
CPU
NMI
...
Memory & Fast I/O Devices
Memory Bus
IRQ Lines
BUS
I/O Bus
I/0 Devices
...
(8237) DMA Controller
Figure 1-2. I/O Architecture of the PC/AT
DRQ Lines
Hold
When installing an I/O card (such as a data acquisition card) a base address must be chosen for it. For the PC/AT I/O addresses fall into the following three ranges: 016 - 20016 20116 - 3FF16 40016 - 7FF16 Reserved for use on motherboard most common (compatible with the PC/XT I/O cards) less common (not compatible with PC/XT I/O cards)
26
The card will take a certain number of registers (I/O bytes). These addresses must not conflict with any other device. If only one I/O card, other than the standard I/O devices like the serial ports, is installed, then the most common base address is 30016. Table 1-8 shows the typical I/O address map for the PC/AT. The highlighted spaces are the best bets in choosing the addresses for an I/O card.
Table 1-8. Typical PC/AT Address Map Hex Address Range 000-01F 020-03F 040-05F 060-06F 070-07F 080-09F 0A0-0BF 0C0-0DF 0F0 0F1 0F8-0FF 1F0-1F8 200-207 208-277 (112 bytes) 278-27F 280-2F7 (120 bytes) 2F8-2FF 300-31F (32 bytes) 320-35F (64 bytes) 360-36F 378-37F 380-38F 390-39F (16 bytes) 3A0-3AF 3B0-3BF 3C0-3CF 3D0-3DF 3E0-3EF (16 bytes) 3F0-3F7 3F8-3FF 400-7FF (1024 bytes) Use DMA controller 1, 8237A-5 Interrupt controller 1, 8259A, master Timer, 8254, 2 8042 (keyboard) Real-time clock, NMI mask DMA page registers, 74LS612 Interrupt controller 2, 8259A DMA controller 2, 8237A-5 Clear math coprocessor busy Reset math coprocessor Math coprocessor Fixed disk Game I/O Not Used Parallel printer port 2 Not Used Serial port 2 Prototype card Not Used Reserved Parallel printer port 1 SDLC, bisynchronous 2 Not Used Bisynchronous Monochrome display and printer adapter Reserved Color/graphics monitor adapter Not Used Diskette controller Serial port 1 Not Used if all I/O devices decode the tenth address bit
27
AT Extension to the XT Card Slots 8 Additional Data Lines IRQ Lines 10,11,14,15 DRQ Lines 0, 5-7
XT Card Slots 8 Bit Data Lines 32 Address Lines IRQ Lines 3-7, 9 DRQ Lines 1-3
Figure 1-3. Card Edge Connectors in the PC/AT Two features of the PC/AT bus standard that are often used by I/O peripherals are interrupts and DMA transfers. When you are installing an I/O device, you are often forced to choose an interrupt number and/or a DMA number. INTERRUPTS Interrupts are used by I/O devices to get the attention of the CPU. When the interrupt controller receives an interrupt signal on one the interrupt request (IRQ) lines, it interrupts the CPU, which then stops what it is doing, and services the interrupt. This is a very powerful tool in I/O applications. For example, the CPU may set up the I/O device to perform a task, like collecting data. When the I/O device is done, it will interrupt the CPU, and the CPU will get the data from the device. This allows the CPU to perform other tasks while the I/O device is at work, rather than setting and twiddling its thumbs. On the card edge connectors of the PC/AT there are 10 IRQ available, numbered 3-7, 9-11, and 14-15. If only the 8 bit data bus is used then 3-7 and 9 are available. The 8 IRQ lines added in the PC/AT extension of the PC/XT bus, are cascaded through IRQ 2. The interrupt structure of the PC/AT is shown in Table 1-9. The IRQ that are likely candidates to be used when inserting a peripheral device are highlighted. DMA Direct Memory Access is the transfer of data directly between the I/O devices and memory, bypassing the CPU. In DMA transfers the DMA controller takes control of the bus after receiving a DMA request from the I/O device on a specific DRQ line. This transfer can be faster than using the CPU to transfer data if large chunks of data are being moved to/from memory. However, if the I/O device uses the full 16 bit I/O data bus of the PC/AT then it is faster (and easier) to use the REP INSW assembly language command to transfer data to memory. DMA transfers are becoming a thing of the past.
On the card edge connectors of the PC/AT there are 7 DRQ available, numbered 0-3, and 5-7. If only the 8 bit data bus is used then 1-3 are available. The 4 DRQ lines of the older PC/XT standard are cascaded through through DRQ 4 added in the PC/AT extension. The DMA structure of the PC/AT is
28
shown in Table 1-10. The DRQ that are likely candidates to be used when inserting a peripheral device are highlighted. Table 1-9. PC/AT Interrupt Structure Interrupt Controller and IRQ # 8259 #1 8259 #2 IRQ 0 IRQ 1 IRQ 2 IRQ 8 IRQ 9 IRQ 10 IRQ 11 IRQ 12 IRQ 13 IRQ 14 IRQ 15 IRQ 3 IRQ 4 IRQ 5 IRQ 6 IRQ 7 Vector Number 8 9 NA 16 17 18 19 20 21 22 23 11 12 13 14 15 Use Timer 0 (Time of Day Clock - 18.2 Hz) Keyboard Cascade Interrupt from 8259 #2 CMOS real-time clock Replaces IRQ 2 Not Used Not Used Not Used Math Coprocessor Fixed Disk Controller Not Used Serial Port 2 if installed Serial Port 1 Parallel Port 2 if installed Diskette Controller Parallel Port 1
Table 1-10. PC/AT Interrupt Structure DMA Controller and DRQ # 8237 #2 8237 #1 DRQ 4 DRQ 0 DRQ 1 DRQ 2 DRQ 3 DRQ 5 DRQ 6 DRQ 7 Page Register Address NA 8716 8316 8116 8216 8B16 8916 9A16 Use Cascade Input from 8237 #1 Not Used SDLC adapter (if installed) Diskette controller Not Used Not Used Not Used Not Used
29