Blinking LEDs is all very well but it would be really nice if we could talk to our microcomputers. Unlike their big desktop cousins, microcomputers don't have keyboards and screens to make communication easy. It would be nice if the desktops could help out their little cousins, sort of lend them their keyboards and screens. This is often not practical for a finished embedded computer. After all, if it looks like a computer then it is not really an embedded computer. But a keyboard and screen can be extremely powerful tools during development and debugging. So C, with a little help from our library, provides tools to let the microcomputer share the keyboard and screen of its desktop host.
The key to this trick is a simple digital interconnection called a serial port. As we shall see in more detail later (Chapter 11 of the Computer book), a serial port uses two signal wires and a ground wire to carry binary data between two computers. Our 9S08GT microcomputers have a couple of such ports and our desktop PCs have one (you can add more using USB if you have to). The first thing that we need is a program to talk to the serial port on the PC. A good choice is a free program called PuttyTel. This opens a window on the PC screen and arranges that any keystroke typed into the window is sent out the serial port and any characters that arrive through the serial port are displayed on the screen.
When you first launch PuttyTel you will get a setup window that looks like this
I have circled the only bits that we care about. We start by clicking the Serial button. This should make the program fill in COM1 in the left-hand text box and the number 9600 in the right-hand text box. We will look at these values a bit more later. For now just remember the 9600 and can start the program by clicking the OPEN button. That should make the dialog disappear and leave us with an empty black window, like this.
When this is the frontmost window, we can type on the PC keyboard and know that every character is being sent out the serial port. If you try this then you will find that nothing seems to happen; no characters appear in the window. This is because there is nothing on the other end of the serial connection. The only characters that appear in the window are ones that are arrive over the serial port. Since there is nothing connected to the serial port, nothing will arrive.
Although our serial connection only uses three wires, most serial connections are made using cables with 9-pin DB-9 connectors on their ends. They look like this.
Each connector has an outer metal shell that also serves as a ground connection. One end has a series of 9 metal pins arranged in two rows. We call this the male end. The other end has a black plastic block with 9 holes, also arranged in two rows. This is the female end. Your desktop PC should have a male DB-9 connector on the back and you need to connect plug the female end of your cable in to that. It helps to tighten the locking screws so that the plug won't fall out.
Your 9S08GT computer does not have a DB-9 connector on it. Instead, you have a little PC board with such connector on one side and 4 pins on the other side. Connect this to the male end of the cable and plug the pins into the breadboard so that the pin names on the computer match those on your little adapter board. The result should look like this.
Even with everything turned on, this will still do nothing. We need a program at the 9S08 end to talk to its serial port.
The standard C library provides a range of routines to handle text input and output. On
a desktop computer these routines would use the window and keyboard of the computer on
which the program was running. On our little embedded processors the library is set
up to use the serial port to send and receive character data. However, we can't quite
leave this to the standard library because we have to establish the connection to
the PC before we can actually do any I/O. So our first simple program will make the
connection and then sned a character to the PC. For this we shall need two routines,
Serial_begin
and putchar
.
NOTE This is one place where we are going to depart from the real Arduino. The real Arduino library does not use the standard C input/output code but provides its own alternate forms. Since our compiler provides excellent support for standard C I/O I thought that we would stick to that. The Arduino forms are rather odd and, incidentally, require C++, which our compiler does not support.
The first thing that we will need is
This routine steals two pins from port E (PTE0 and PTE1) and converts them into the two serial pins. It needs one extra piece of information, the speed at which to send data, what is known as the Baud rate. Back when we started PuttyTel we accepted the default speed of 9600 so we have to use that same number now. So the first line of our program is
Serial_begin(9600);
Our program is going to be simplest possible. It will sit sending a single 'a' character
out the serial port. After the call to Serial_begin
the 9S08 is can send characters
over the serial line to PuttyTel and receive characters from it. We send a character out
with the routine putchar
which must be passed the ASCII code for the character. We
looked at the ASCII code back in Chapter 1. There we found that the binary code for the
lower case letter 'a' is 11000012.
C allows us to write numbers in any of the forms that we met in
chapter 1, decimal (97), hexadecimal (0x61), binary (0b01100001), but it also provides a
special form for ASCII characters. We can simply put the character in single quotes and
C will replace it by the correct ASCII code. Thus we can write our lower case a with any
of the lines
putchar('a');
putchar(97);
putchar(0x61);
putchar(0b01100001);
This gives us our first program.
void main (void) {
//
// Put any one-time setup code here.
//
Serial_begin(9600);
//
// Code below here runs in an infinite loop until something external stops it.
//
while (1) {
putchar('a');
}
}
If you run this program then the PuttyTel window should start to fill up with 'a's.
You can experiment with putting different values in the call to Serial_write
and
seeing what letters they produce.
It would be very tedious if we had to use one putchar
call for each letter that
we want to write so the library provides printf(<string>)
, which sends an
entire string of character in one call. As we shall shall see later, this printf
command is quite powerful and can do a lot more than just print simple strings but
this is where we start.
Strings in C are in general somewhat complicated and we shall learn a lot more about them later (??ref??). Constant strings, however, are very easy. Just as we can make a single character constant by surrounding a letter with single quotes, so we can make a string constant by surrounding the string with double quotes. Thus we have strings such as
"Hello"
"Hello, world!"
"How's it going?"
We can send these to the PC by passing them to printf
, like these
printf("Hello!");
printf("Hello, Hello!\r\n");
The last of those is a bit weird. It has some extra stuff at the end. If you cast your mind back to Chapter 1 you may remember that the first 32 ASCII characters are not not printable symbols but represent directions to the display to do fancy things. In particular the character with ASCII value 0x0D is a carriage return character while 0x0A is a linefeed. C provides a way to put such special characters into string using special two-character sequences called escape sequences such as these. '\r' is the ASCII value 0x0D that tells the display program to move the cursor back to the start of the line. It is the carriage return sequence ('r' for return). '\n' is the ASCII value 0x0A that moves the cursor down one line ('n' for newline). If we want to move the cursor down to the start of the next line then we need both of these special characters so it is very common to end a string intended as output with the "\r\n" sequence.
Here is a the classic first C program, HelloWorld. It opens a serial port and prints a string to PuttyTel and then just sits doing nothing until you turn it off.
void main (void) {
//
// Put any one-time setup code here.
//
Serial_begin(9600);
printf("Hello, world!\r\n");
//
// Code below here runs in an infinite loop until something external stops it.
//
while (1) {
}
}
You may wish to experiment with different combinations of printable strings and escape sequences and see what sorts of effect you can achieve.
So far this has been a one-way conversation. The microcomputer can talk to the PC but
we can't say anything back. It is time to meet getchar
. When you call this routine
waits until a character has arrived from the PC and returns its ASCII code. If
it has. Otherwise it returns the value -1, which does not correspond to any ASCII
character (it is an 8-bit value and all the ASCII codes fit in 7 bits and so have the
msb set to zero).
NOTE This is called a blocking read. It will block the execution of the program until a character arrives. This is often what we want but there will later be occasions when we want to do something else until there is a character ready. We will have to find a way to provide a non-blocking read that will return immediately if there isn't a character so that the program can go on working while it waits.
getchar
brings back an ASCII value that we will need to keep around so that we can examine
it and do something with it. For this we need some new pieces of C including the ideas of
a variable and an assignment statement. The end will result will look like this.
int theChar;
...
theChar = getchar();
Let's examine these statements a bit at a time. We start with the first line which declares our variable.
A variable is place to store a value, a name for a piece of memory. Inside the computer, the variable corresponds to the address of some byte in memory, but that is too messy for us poor humans to worry about. C allows us to represent the address by a name. A name is a just what it sounds like, a string of letters and numbers that we use to name the memory location. We call such an object a variable because its value can change, vary, over the lifetime of the program. Valid names in C start with a letter and can contain any mixture of letters, numbers, and the underscore character (_). We have already met some names
pinMode
digitalWrite
Serial_begin
putchar
getchar
are all names for objects we have met. Somebody else chose those names and we just have
to learn them. When we want to use a variable we get to invent the name. It is a really
good idea to choose names that tell you what the variable is used for. For example we
currently need a name for the variable that holds the character returned by getchar
.
I am going to call it theChar (where Char is pronounced car NOT tchar because it
is short for character).
C allows us to name various different kinds of object. We have already seen names that represent library routines, such as pinMode, and now have names that represent numbers whose values can change. We call these different kinds of objects different types. Before we can use a name we must tell C what type of object we are naming, using a special kind of statement called a declaration that we describe like this
<type> <name> [= <value>];
Examples include
char ch;
int nLetter = 15;
The funny description with its weird brackets is written in a standard form for describing
syntax; that is, the rules for constructing legal programs. In this form an element in
angle brackets, such as <type>
, tells us what is allowed in that position in the statement.
Somewhere else we must be told what are the legal things that can go there (see below).
Thus <type>
, <name>
, and <value>
are all to be replaced by strings with some special form.
The square brackets surround optional elements. Thus in this declaration you must
have a
BEWARE: C has a weird restriction (not shared by the real Arduino compiler). You must declare all of your variables at the start of the program, before any other kind of statement is used. If you forget then your program will not compile and you will get an error message. Unfortunately, you almost always get a totally meaningless error message that you will have to learn to recognize.
The simplest type in C is a 2-byte, signed, integer value. As you learned in Chapter 1, such a value can range from -32768 to +32767. In C it is just called
int
The full definition of Serial_read() tells us that it returns such an int
so this will
be the right type of variable for us to use.
16-bits is a generally useful size for a number but it is often larger than we need. If we know that we won't need values bigger than 127 then we can use an 8-bit integer that will fit in one byte. Since this is the ideal size to store a character, this type is called
char
again, pronounced like car not like tchar. It can store values from -128 to +127.
The 9S08 computer that we are using can only internally manipulate 8-bit values so that
we should use char
variables when we can. The CPU can add or subtract two 16-bit
numbers about twice the time it takes with 8-bit numbers but it takes about 10 times as
long to multiply or divide int
's as it does char
's.
Sometimes we know that a number cannot possibly be negative. In such a case we can add
the prefix unsigned
to the type. So we have the following four types of integer
char
int
unsigned char
unsigned int
There are types for longer integers and even for numbers with decimal points (floating point numbers) but they are much more costly and we will not normally use them.
Once we have declared our variable, we can use its value and change its value. We change the value with an assignment statement that looks like this
<var> = <expression>;
Here <var>
is the name of the variable that you wish to use and <expression>
an
ordinary mathematical expression using addition (+), subtraction (-), multiplication (*),
and division (/). This use of the =
sign is quite different from the usual mathematical
meaning. It is best read becomes rather than equals. It changes the value associated
with <var>
. The old value is lost without trace and the new one will remain until
another assignment alters it. Thus, we get a perfectly legal statement like
count = count + 1;
In mathematics this would be a complete impossibility since it would imply that one was the same as zero. In C it tells the computer to increase the value stored in count by one.
We want to use the assignment statement to put the value returned by the call to
getchar()
into our variable. That is a particularly simple case and looks like this
theChar = getchar();
We want to write a program that will sit waiting for characters to come from the PC and, every time it finds one, send it back. It needs to do this repeatedly, so we will put the code into the loop of our program. We know have enough ideas to try writing our echo program. Our first try looks like this.
main() {
int theChar;
Serial_begin(9600);
while (1) {
theChar = getchar();
putchar(theChar);
}
}
If you run this code with you 9S08 connected to PuttyTel then you should find that any key that you type on the keyboard will show up in the window. You can check that this is due to your computer either by stopping the program or by turning off the power to your 9S08GT.
The three I/O routines that we have met so far allow us to move characters between the PC and our 9S08 computer. These are the basis of all serial interaction. However, we often want to deal with numeric information instead of character information. We need ways to write numbers out and read them in.
The printf
routine can do more than just print fixed character strings. The slightly
odd name actually stands for "print formatted". printf
does not just send whatever
characters it finds in its first argument to the PC, it actually looks at the characters
and interprets some of them as commands to print something else. The first such command
tells it to print a decimal number. It looks like this "%d" and is called a
format string. Here is an example of the use of a format string.
int age = 21;
printf("Your age is %d.\r\n", age);
If you put this into a program then it will print out
Your age is 21.
We can print more than one value in a single printf
. Each item that we want print
needs its own format string to tell printf
how to print it and then needs an
expression to tell it what to print. Here is another example
int age = 3;
printf("Your current age is %d. It will be %d years before you may drink alcohol.\r\n",
age, 21-age);
This demonstrates several things.
printf
matches format strings with values by position. The first format string it
encounters will use the first argument after the string, the second format will use
the second additional argument. Obviously something will go wrong if the number of
arguments and the number of format strings doesn't match.21-age
is an expression that will evaluate to a number of years to
print.Because we are mostly going to be thinking about values inside our microcontrollers as bit patterns we often want to see them in binary. Unfortunately, C does not provide direct binary printing but it does support printing in hex. Since we can learn to quickly translate hex into binary that will have to do.
We print a number in hex using the format string %x or %X. The only difference is how C prints the digits that are represented by letters. If you use the lower case %s then C will use lower case in its printing. If you use %X then C will use upper case too. So we have
printf("It says %x, or %X.\r\n", 0x1abcd, 0x1abcd);
that prints
It says 1abcd or 1ABCD.
Note that there is no relation beween the way we write the number in the program and
what printf
prints. The format string decides whether to use lower or upper case.
C also makes it easy to read numbers from the input. However, there are a number of tricks to this. We will stick to simple uses. To read a single decimal number from the input we do something like this
scanf("%d", &theValue);
Just like printf
, scanf
takes a string that tells it what sort of input to expect.
In this case the "%d" tells it to expect an integer. scanf
will go off and expect the
user to type a series of digits in and will convert them into an integer. It will
quit when it finds the first character that cannot be in the current type of value. So
in this case it will quite when it sees anything but a digit. scanf
will store the
resulting value into variable theValue
, which must be of type int
.
NOTE 1 I am hiding a lot of stuff here, not least the reason for that strange ampersand. It must be there but its exact meaning is a bit beyond us at the moment. We may explore this in a later chapter.
NOTE 2 The best way to end a number when you are typing one in is to press the 'return' key.
Just like printf
, scanf
can also handle numbers in hex. If you use a "%x" format
string then scanf will allow the incoming number to consist of any valid hex digit,
0-9 or a-f, and will convert the value accordingly. Unlike printf
, scanf
does not
care about the case of the letters.
We can put all this together to make a simple program that converts decimal numbers to their hex format.
main() {
int value;
Serial_begin(9600);
while (1) {
printf("Enter a decimal number ");
scanf("%d", &value);
printf("%d in hex is %x.\r\n", value, value);
}
}
NOTE 3 It is possible for scanf to read more than one item at a time but there are a number of significant complications. I have presented this version that will read single numbers because it should be all that you will need for a long time.
We can write numeric values in C using decimal (100) hex (0x64), binary (0b01100100), and ASCII ('d') representations. We store values that change in variables of types such as
char
8-bit number (range -128 -> 127)int
16-bit signed number (-32768 -> 32767)unsigned int
16-bit unsigned number (0 -> 65535)All variables must be declared before any other statements in a program.
We change the values of variables using assignment statements of the form
<var> = <expression>;
We can talk over a serial cable to a PC running a terminal program such as PuttyTel using the routines
Serial_begin(int baudRate); // Makes the connection and sets the speed.
putchar(int theChar); // Writes a single character to the PC
getchar(); // Waits for a character and returns it
printf(char* theString) // Writes a complete string to the PC
printf(char* fmt, arg1, arg2, ...) // Writes a string and some values to the PC
scanf(char* fmt, &varName) // Reads a single number into a variable
// which **must** be an int.
We can print or read numbers in decimal notation using the "%d" format and numbers in hex using the "%x" or "%X" formats.