Translate this page

Controlling The Real World With Computers
::. Control And Embedded Systems .::

Experiment 6 - More Precise Control Of Motors

Home

                                                       Order

Let me know what you think

Previous: Experiment 5 - Controlling Motors
Next: Experiment 7 - Bi-directional Control Of Motors And The H-Bridge

Experiment 5 provided the basics of controlling motors, including a little on PWM. The goal in this experiment is to increase the usefulness of the system by configuring it in such a way as to permit a program to turn a bit on or off using simple function calls, and to do so with minimum memory usage and time-consuming overhead. Something like this:

TurnOn(WarningLED);
TurnOff(MainMotor);
TurnOn(LeftArmMotor);

We will then look into ways to automate the control of outputs by using a timer built into the computer.

Let's take another look at pointers since they will be used in the final code. Consider the following. It wouldn't hurt to compile, run and play with it to get a feel for what's going on:

 
 
// experi6a.c
 
#include <malloc.h>
#include <conio.h>
#include <stdio.h>
#include <bios.h>
 
void main(void)
{
 int x,y,*ptr1,*ptr2;
 
 // cause ptr1 to point to the memory location of x
 ptr1 = &x;
 
 // notice that x is being set equal to 5 AFTER ptr1 is made to point to x
 x = 5;
 
 // the * dereferences ptr1, which is to say it provides the value pointed to
 printf("ptr1 points to x's address: x = %d *prt1 = %d\n",x,*ptr1);
 
 y = 10;
 ptr2 = &y;
 
 printf("ptr2 points to y's address: y = %d *prt2 = %d\n",y,*ptr2);
 
 ptr2 = &x;
 
 printf("both ptr1 and ptr2 point to x's address: x = %d y = %d *prt1 = %d *prt2 = %d\n"
 ,x,y,*ptr1,*ptr2);
 
 *ptr2 = 123;
 
 printf("dereferenced ptr2 changed to 123: x = %d y = %d *prt1 = %d *prt2 = %d\n"
 ,x,y,*ptr1,*ptr2);
 
} // end experi6a.c


Click here to download experi6a.c

This is the output:

 
 
ptr1 points to x's address: x = 5 *prt1 = 5
ptr2 points to y's address: y = 10 *prt2 = 10
both ptr1 and ptr2 point to x's address: x = 5 y = 10 *prt1 = 5 *prt2 = 5
dereferenced ptr2 changed to 123: x = 123 y = 10 *prt1 = 123 *prt2 = 123

Notice that ptr1 is made to point to x's address before x is set equal to 5, and yet the dereferenced ptr1 also provides 5 (see Experiment 4 if you don't remember dereferencing). The same process applies when ptr2 is given the address of y and y is set to 10.

Finally, the program points both ptr1 and ptr2 to the address location of x. It then sets the dereferenced ptr2 to 123. Notice that x, the dereferenced ptr1 and the dereferenced ptr2 are now all equal to 123. Also note that no change was made directly to x. The change was made by changing the value pointed to by ptr2, which happens to be x and the value pointed to by ptr1. All values show the same because both pointers point to x. When the contents of the memory location are changed, all three get changed. That's the capability that's needed; the ability to control any output bit on any port by any of several control locations.

For basic on/off operation we need the port address, access to the current port value and something that will allow us to change an individual bit to turn it on or to turn it off, such as a couple of masks. It would be nice to be able to package all four elements in a single object so they could be treated as a unit. That's possible with something called a structure:

 
 
struct OC
{
 int PortAddress;
 char onmask;
 char offmask;
 int *PortData;
};

The name does not have to be OC. I used it because it's symbolic of Output Control. In fact, it just describes an object. It can't be used until you declare something as being an OC. That's done almost the same way you would declare anything else, such as "int x;." All you add is the struct keyword:
struct OC OutputControl;

OutputControl is now an instance of OC. You can refer to its members with a dot. For example:
OutputControl.PortAddress = 0x123;

Now try this piece of code:


// experi6b.c

#include <malloc.h>
#include <conio.h>
#include <stdio.h>
#include <bios.h>

struct OC
{
  int PortAddress;
  char onmask;
  char offmask;
  int *PortData;
};

void main(void)
{
  int pa=0x345,m=0xd,pd;
  struct OC OutputControl;

  OutputControl.PortAddress = pa;
  OutputControl.onmask = m;
  OutputControl.PortData = &pd;

  pd = 12345;

  printf("pa = %#X m = %#X pd = %d\n",pa,m,pd);

  printf("OutputControl.PortAddress = %#X\nOutputControl.onmask = %#X\n*OutputControl.PortData = %d\n"
  ,OutputControl.PortAddress,OutputControl.onmask,*OutputControl.PortData);


} // end experi6b.c


Click here to download experi6b.c

And the output:

 
 
pa = 0X345 m = 0XD pd = 12345
OutputControl.PortAddress = 0X345
OutputControl.onmask = 0XD
*OutputControl.PortData = 12345

A small note -- notice what the %#X does for you?

Everything works just like you'd expect, including the dereferenced pointer. The only change is that you use a dot to pick elements out of the object you have declared.

There are a maximum of 24 possible output lines on the board (which you can buy here -- just in case you were wondering). The OC structure allows us to establish the port address that will be used, mask off a bit and access the port data by means of a pointer.

Since we are going to need up to 24 such control structures, we could declare an array of them almost the same way we would any other data type:
struct OC OutputControl[24];

If we wanted to reference the 17th structure's address member for example, we would simply do something like this:
OutputControl[16].PortAddress = 0X345;
Remember, everything in C is zero-based, so the 17th structure is at [16]. The problem with using an array of structures however, is that it takes up memory that might never be used. You already know that each character uses 1 byte and that an integer uses 2 bytes in a DOS 16-bit system. A pointer needs 4 bytes. That makes the whole structure 8 bytes. At 8 bytes each, 24 of the OC structures need 192 bytes. That's not much for a multi-megabyte system, but it can get to be a problem with memory-starved control and embedded systems. In addition, the OC structure will be expanded before we get through, so it would be nice if we could use only the amount of memory that we need as we need it. Again, pointers come to the rescue. Rather than declare an array of structures, just declare an array of pointers to the structures:
struct OC *OutputControl[24];

This array reserves only enough memory for the pointers that will be used to point to the memory locations of the structures. Thus, it takes up 4 * 24 = 96 bytes. Try the following to illustrate the point. Here, the sizeof(..) operator will be used. As might be expected, it provides the size of an object:

 
 
// experi6c.c
 
#include <malloc.h>
#include <conio.h>
#include <stdio.h>
#include <bios.h>
 
struct OC
{
 int PortAddress;
 char onmask;
 char offmask;
 int *PortData;
};
 
void main(void)
{
 struct OC oc;
 struct OC *poc;
 struct OC OutputControl[24];
 struct OC *pOutputControl[24];
 
 printf("size of char = %d\n",sizeof(char));
 printf("size of int = %d\n",sizeof(int));
 printf("size of double = %d\n",sizeof(float));
 printf("size of double = %d\n",sizeof(double));
 printf("size of oc = %d\n",sizeof(oc));
 printf("size of poc = %d\n",sizeof(poc));
 printf("size of OutputControl = %d\n",sizeof(OutputControl));
 printf("size of pOutputControl = %d\n",sizeof(pOutputControl));
 
} // end experi6c.c


Click here to download experi6c.c

And the output:

 
 
size of char = 1
size of int = 2
size of double = 4
size of double = 8
size of oc = 8
size of poc = 4
size of OutputControl = 192
size of pOutputControl = 96

These object sizes are true only for the system the program was compiled and run on. The numbers will be accurate for most 16-bit DOS systems and compilers, but could be different for your system and compiler. Notice that the single oc structure has a size of 8 bytes, whereas the size of pointer poc is only 4 bytes. That's the reason the array of 24 pointers is 96 bytes smaller than the array of 24 structures. The difference will become even more significant as the size of the structure increases. Note that the pointer poc could have been given the address of oc the same way integer pointers were given the address of an integer:
poc = &oc;

The members of a pointer to a structure are referenced with "->". Thus, poc would reference the PortAddress thus:
poc->PortAddress = 0x345;
and dereferencing the pointer element looks like this:
*poc->PortData = 2;

That means we can do this -- try it, it's good for you:

 
 
// experi6d.c
 
#include <malloc.h>
#include <conio.h>
#include <stdio.h>
#include <bios.h>
 
struct OC
{
 int PortAddress;
 char onmask;
 char offmask;
 int *PortData;
};
 
void main(void)
{
 struct OC oc;
 struct OC *poc;
 struct OC OutputControl[24];
 struct OC *pOutputControl[24];
 
 int x = 1000;
 
 oc.PortData = &x;
 
 poc = &oc;
 
 oc.PortAddress = 100;
 
 poc->PortAddress = 10;
 
 printf("poc->PortAddress = %d\noc.PortAddress = %d\nx = %d\n*poc->PortData = %d\n*oc.PortData = %d\n"
 ,poc->PortAddress
 ,oc.PortAddress
 ,x
 ,*poc->PortData
 ,*oc.PortData);
 
} // end experi6d.c


Click here to download experi6d.c

and get this as an output:

 
 
poc->PortAddress = 10
oc.PortAddress = 10
x = 1000
*poc->PortData = 1000
*oc.PortData = 1000

Notice that oc.PortAddress was set to 100, but got changed to 10. Changing poc->PortAddress didn't change poc->PortAddress alone. It also changed oc.PortAddress.

Also, since oc.PortData was made to point to the memory location of x, and since x had already been made equal to 1000, both *oc.PortData and *poc->PortData became 1000.

Let's take a look at x as it would appear in memory. Since 1000 is equal to the HEX value 03E8 and x is a two-byte integer, it would look like the following in memory. Note that we really don't care what the actual address location is. Lower memory in the table below is toward the left, and upper memory is toward the right. Notice that the bytes are reversed from the left-to-right order we might expect. This is typical of Intel processors:

  

  

  

  

E8

03

  

  

  

  

  

  

  

  

  

When oc.PortData is made to point to x, it points to the memory location containing E8. When oc.PortData is dereferenced, the value residing in that memory location, along with the value in the next location, are returned as a single integer; the 03E8 value (1000) is returned. If x is changed to another value, the same memory location is referenced, so the changed information is obtained.

The process also goes the other way. If the dereferenced oc.PortData is set equal to 100 (= 0x0064), then the above memory locations will change as well, and x will be made equal to 100:

  

  

  

  

64

00

  

  

  

  

  

  

  

  

  

The best way to understand what's going on is to play with the numbers then compile and run the program.

An array of pointers to OC structures is a good start, but that's all it is. It is still necessary to make the pointers point to an area in memory that can hold the structures. We need a way to make some room -- to do some memory allocation. No problem, we'll use malloc(..) . That's its job. The prototype for malloc(..) is:
void *malloc(size_t size);

The void simply means malloc(..) returns a pointer that is of any type you wish. It returns NULL if there is not enough memory. The size_t type is the same type that sizeof(..) returns. It's usually a long. Such types are usually declared using typdef . For example, size_t might be defined as follows:
typedef long size_t;

You can define virtually anything you like this way. Maybe you'd like to use the word integer in the place of int. OK:
typedef int integer;

How about OC? You would define it this way:

 
 
typedef struct
{
 int PortAddress;
 char onmask;
 char offmask;
 int *PortData;
} OC;

Now you can declare an instance without the struct keyword:
OC oc;

I like the non-typedef method because it reminds me that it's a structure that's being used. If you like typedef, go for it.

Back to the subject. Let's say that an array of pointers to OC structures has been defined like this:

struct OC *OutputControl[24];

All you would need to do to reserve memory for the 15th location is this:

OutputControl[14] = malloc(sizeof(struct OC));

Malloc will return the address of a block of memory for the 15th place that is the size of OC, providing the memory is available. A NULL will be returned if it's not.

Just as it is necessary to allocate memory for the structures, it is also necessary to free the memory when it is no longer being used. That is done by calling the free(..) function. Always free memory after you are through with it. Not doing so will give the computer severe heartburn. The following example frees the same memory that was allocated above:
free(OutputControl[14]);

The following enumeration will be used to set up each node of the array as needed. The most significant two bits [4:3] will be used to determine what port is to be used, and the least significant three bits [2:0] will determine the bit to be used in the port. It should be added to the constants header file:

 
 
 enum OutSetNums
 {
 PA0, // 0 00 000 Port A Bit 0
 PA1, // 1 00 001 Port A Bit 1
 PA2, // 2 00 010 Port A Bit 2
 PA3, // 3 00 011 Port A Bit 3
 PA4, // 4 00 100 Port A Bit 4
 PA5, // 5 00 101 Port A Bit 5
 PA6, // 6 00 110 Port A Bit 6
 PA7, // 7 00 111 Port A Bit 7
 PB0, // 8 01 000 Port B Bit 0
 PB1, // 9 01 001 Port B Bit 1
 PB2, // 10 01 010 Port B Bit 2
 PB3, // 11 01 011 Port B Bit 3
 PB4, // 12 01 100 Port B Bit 4
 PB5, // 13 01 101 Port B Bit 5
 PB6, // 14 01 110 Port B Bit 6
 PB7, // 15 01 111 Port B Bit 7
 PC0, // 16 10 000 Port C Bit 0
 PC1, // 17 10 001 Port C Bit 1
 PC2, // 18 10 010 Port C Bit 2
 PC3, // 19 10 011 Port C Bit 3
 PC4, // 20 10 100 Port C Bit 4
 PC5, // 21 10 101 Port C Bit 5
 PC6, // 22 10 110 Port C Bit 6
 PC7 // 23 10 111 Port C Bit 7
 };

Put the structure definitions somewhere close to the variable declarations in the digital C module. Near the top will be fine:

 
 
// The following are known only to the functions in this file.
// They can't be modified or even accessed by anything outside this
// file except through funtions in this file designed to provide access.
 
struct OC
{
 int PortAddress;
 char onmask;
 char offmask;
 int *PortData;
};
 
struct OC *OutputControl[24];
 
unsigned base;
unsigned switch_port;
unsigned ppi_porta;
unsigned ppi_portb;
unsigned ppi_portc;
 
int porta_val = 0xa; // port values are set for test
int porta_mask;
 
int portb_val = 0xb;
int portb_mask;
 
int portc_val = 0xc;
int portc_mask;

The port values are pre-set for test purposes only. If we are pointing to porta_val for example, the value read should be 0XA.

Now add the following configure and free routines to the digital C module and their prototypes to the extern header. Notice the slight change to set_up_ppi() as well:

 
 
// configure the array number location to a port
// and bit number dictated by portselect
int ConfigureOutput(int arraynumber, int portselect)
{
 int x;
 
 if(arraynumber < 0 || arraynumber > 23)
 return 0; // illegal number
 
 if(portselect < 0 || portselect > 23)
 return 0; // illegal number
 
 if(OutputControl[arraynumber] == NULL)
 {
 if((OutputControl[arraynumber] = malloc(sizeof(struct OC))) == NULL)
 {
 printf("Not enough memory\n");
 return 0;
 }
 }
 
 x = portselect >> 3; // the port number is in bits 3 and 4
 
 switch(x) // 0 = Port A, 1 = Port B, 2 = Port C
 {
 case 0:
 OutputControl[arraynumber]->PortAddress = ppi_porta; // address for Port A
 OutputControl[arraynumber]->PortData = &porta_val; // point to Port A data value
 break;
 
 case 1:
 OutputControl[arraynumber]->PortAddress = ppi_portb; // address for Port B
 OutputControl[arraynumber]->PortData = &portb_val; // point to Port B data value
 break;
 
 case 2:
 OutputControl[arraynumber]->PortAddress = ppi_portc; // address for Port C
 OutputControl[arraynumber]->PortData = &portc_val; // point to Port C data value
 break;
 }
 
 OutputControl[arraynumber]->onmask = 1 << (portselect & 7); // shift by bits[2:0]
 OutputControl[arraynumber]->offmask = ~OutputControl[arraynumber]->onmask;
 
// /* remove the double slash rem to skip debug print statements
 
 printf("arraynum=%02d port select=%02d ",arraynumber,portselect);
 
 printf("Addr=%#X "
 ,OutputControl[arraynumber]->PortAddress);
 
 printf("*Data=%X "
 ,*OutputControl[arraynumber]->Data);
 
 printf("onmsk=");
 for(x=128; x>0; x/=2)
 {
 if(x & OutputControl[arraynumber]->onmsk)
 printf("1");
 else printf("0");
 }
 
 printf(" offmsk=");
 for(x=128; x>0; x/=2)
 {
 if(x & OutputControl[arraynumber]->offmsk)
 printf("1");
 else printf("0");
 }
 printf("\n");
 
// remove the double slash rem to skip debug print statements */ 
 
 return 1;
}
 
// free the output control structures
void FreeOutputControl(void)
{
 int x;
 
 for(x=0; x<24; x++)
 {
 if(OutputControl[x] != NULL)
 free(OutputControl[x]);
 }
}
 
// set up the ppi according to the dictates of the mode argument
void set_up_ppi(int mode)
{
 unsigned control = base + 0x23;
 int command;
 
 // make certain control locations start at NULL
 for(command=0; command<24; command++)
 OutputControl[command] = NULL;
 
 mode>>=6; // shift the mode value to the right 6 places
 
 command = (mode & 0x0c) << 1; // shift bits 2 and 3 into positions 4 and 5
 command += (mode & 3); // add in bits 0 and 2
 command |= 0x80; // OR in bit 7 for PPI set up
 
 outp(control, command); // set according to mode command
 
} // end set_up_ppi()

Let's take a look at ConfigureOutput(.......). You have already seen a lot of this type of code. The first thing it does is reject any output or port select number that's less than 0 or greater than 23 since that's what will be used as the index into the array of structure pointers, as well as to determine what port and bit to use. If OutputControl at the index location is NULL then memory is allocated. Be sure to add the loop in set_up_ppi(..) that sets all locations to NULL in the first place so you can be sure this will work.

Next, x is set to portselect shifted to the right 3 places. This will put bits 3 and 4 in bit locations 0 and 1. The original 3 bits will go in the "bit bucket" -- they will disappear as far as x is concerned. Remember however, that nothing will happen to portselect. It can and will be used later. The resulting x value will range from 0 to 2, representing Port A through Port C. The switch statement sets the appropriate port address and causes the data pointer to point to the correct data value variable.

The final thing to do is to set up the masks. The on mask is generated by shifting a 1 to the left by the bit number. This value can range from 0 to 7 and is in bits [2:0] of portselect. The maximum value the three bits can produce = 1 + 2 + 4 = 7, so portselect is ANDed with all 3 bits, or 7. The off mask is the inverted version of the on mask. It would be a very good idea to take another look at the boolean and data lines sections if you don't understand those last two sentences.

Here is the extern header so far:

 
 
// external prototypes
extern int ConfigureOutput(extern int arraynumber, extern int portselect);
extern int TurnOn(int arraynumber);
extern int TurnOff(int arraynumber);
extern int is_closure(int input);
extern void set_up_ppi(int mode);
extern void blinker(long on, long off);
extern void btoa(void);
extern void motor(long on, long off);
extern void motor2(long on, long off);
extern void portaon(void);
extern void portaoff(void);
extern void portbon(void);
extern void portboff(void);
extern void portcon(void);
extern void portcoff(void);

Use experi6e.c to test ConfigureOutput(..):

 
 
// experi6e.c
 
#include <malloc.h>
#include <conio.h>
#include <stdio.h>
#include <bios.h>
 
// include header with constants
#include "const6a.h"
 
// include header with external prototypes
#include "extern6a.h"
 
void main(void)
{
 int x,p,y;
 
 get_port(); // get the port number and establish register locations
 
 // make everything an output
 set_up_ppi(Aout_CUout_Bout_CLout);
 
 for(x=-1,p=PC7; p>=PA0; x++,p--)
 {
 if(!ConfigureOutput(x,p))
 printf("Error for arraynum = %d port select = %d\n",x,p);
 }
 
 for(x=0,p=PA0-1; p<=PC7+1; x++,p++)
 {
 if(!ConfigureOutput(x,p))
 printf("Error for arraynum = %d port select = %d\n",x,p);
 }
 
 // don't forget to free memory!
 FreeOutputControl();
 
} // end experi6e.c


Click here to download experi6e.c
Click here to download digi6e.c
Click here to download const6a.h
Click here to download extern6a.h

The x variable is used for the arraynum and p is used for port select. The arraynum goes up while port select goes down in the first loop, and they both travel up in the second. Errors are introduced to test the function.

This is the output you should see:

 
 
Error for arraynum = -1 port select = 23
arraynum=00 port select=22 Addr=0X262 *Data=C onmsk=01000000 offmsk=10111111
arraynum=01 port select=21 Addr=0X262 *Data=C onmsk=00100000 offmsk=11011111
arraynum=02 port select=20 Addr=0X262 *Data=C onmsk=00010000 offmsk=11101111
arraynum=03 port select=19 Addr=0X262 *Data=C onmsk=00001000 offmsk=11110111
arraynum=04 port select=18 Addr=0X262 *Data=C onmsk=00000100 offmsk=11111011
arraynum=05 port select=17 Addr=0X262 *Data=C onmsk=00000010 offmsk=11111101
arraynum=06 port select=16 Addr=0X262 *Data=C onmsk=00000001 offmsk=11111110
arraynum=07 port select=15 Addr=0X261 *Data=B onmsk=10000000 offmsk=01111111
arraynum=08 port select=14 Addr=0X261 *Data=B onmsk=01000000 offmsk=10111111
arraynum=09 port select=13 Addr=0X261 *Data=B onmsk=00100000 offmsk=11011111
arraynum=10 port select=12 Addr=0X261 *Data=B onmsk=00010000 offmsk=11101111
arraynum=11 port select=11 Addr=0X261 *Data=B onmsk=00001000 offmsk=11110111
arraynum=12 port select=10 Addr=0X261 *Data=B onmsk=00000100 offmsk=11111011
arraynum=13 port select=09 Addr=0X261 *Data=B onmsk=00000010 offmsk=11111101
arraynum=14 port select=08 Addr=0X261 *Data=B onmsk=00000001 offmsk=11111110
arraynum=15 port select=07 Addr=0X260 *Data=A onmsk=10000000 offmsk=01111111
arraynum=16 port select=06 Addr=0X260 *Data=A onmsk=01000000 offmsk=10111111
arraynum=17 port select=05 Addr=0X260 *Data=A onmsk=00100000 offmsk=11011111
arraynum=18 port select=04 Addr=0X260 *Data=A onmsk=00010000 offmsk=11101111
arraynum=19 port select=03 Addr=0X260 *Data=A onmsk=00001000 offmsk=11110111
arraynum=20 port select=02 Addr=0X260 *Data=A onmsk=00000100 offmsk=11111011
arraynum=21 port select=01 Addr=0X260 *Data=A onmsk=00000010 offmsk=11111101
arraynum=22 port select=00 Addr=0X260 *Data=A onmsk=00000001 offmsk=11111110
Error for arraynum = 0 port select = -1
arraynum=01 port select=00 Addr=0X260 *Data=A onmsk=00000001 offmsk=11111110
arraynum=02 port select=01 Addr=0X260 *Data=A onmsk=00000010 offmsk=11111101
arraynum=03 port select=02 Addr=0X260 *Data=A onmsk=00000100 offmsk=11111011
arraynum=04 port select=03 Addr=0X260 *Data=A onmsk=00001000 offmsk=11110111
arraynum=05 port select=04 Addr=0X260 *Data=A onmsk=00010000 offmsk=11101111
arraynum=06 port select=05 Addr=0X260 *Data=A onmsk=00100000 offmsk=11011111
arraynum=07 port select=06 Addr=0X260 *Data=A onmsk=01000000 offmsk=10111111
arraynum=08 port select=07 Addr=0X260 *Data=A onmsk=10000000 offmsk=01111111
arraynum=09 port select=08 Addr=0X261 *Data=B onmsk=00000001 offmsk=11111110
arraynum=10 port select=09 Addr=0X261 *Data=B onmsk=00000010 offmsk=11111101
arraynum=11 port select=10 Addr=0X261 *Data=B onmsk=00000100 offmsk=11111011
arraynum=12 port select=11 Addr=0X261 *Data=B onmsk=00001000 offmsk=11110111
arraynum=13 port select=12 Addr=0X261 *Data=B onmsk=00010000 offmsk=11101111
arraynum=14 port select=13 Addr=0X261 *Data=B onmsk=00100000 offmsk=11011111
arraynum=15 port select=14 Addr=0X261 *Data=B onmsk=01000000 offmsk=10111111
arraynum=16 port select=15 Addr=0X261 *Data=B onmsk=10000000 offmsk=01111111
arraynum=17 port select=16 Addr=0X262 *Data=C onmsk=00000001 offmsk=11111110
arraynum=18 port select=17 Addr=0X262 *Data=C onmsk=00000010 offmsk=11111101
arraynum=19 port select=18 Addr=0X262 *Data=C onmsk=00000100 offmsk=11111011
arraynum=20 port select=19 Addr=0X262 *Data=C onmsk=00001000 offmsk=11110111
arraynum=21 port select=20 Addr=0X262 *Data=C onmsk=00010000 offmsk=11101111
arraynum=22 port select=21 Addr=0X262 *Data=C onmsk=00100000 offmsk=11011111
arraynum=23 port select=22 Addr=0X262 *Data=C onmsk=01000000 offmsk=10111111
Error for arraynum = 24 port select = 23
Error for arraynum = 25 port select = 24

Setting up output control ahead of time in this manner provides for faster action when in control mode because no decisions have to be made. They are all made during setup. For example, to turn on an output just do the following:

 
 
int TurnOn(int arraynumber)
{
 if(OutputControl[arraynumber] == NULL)
 return 0; // node not set up
 
 // keep existing bits and OR this one in
 *OutputControl[arraynumber]->PortData |= OutputControl[arraynumber]->onmask;
 
 // put the result in this node's port register
 outp(OutputControl[arraynumber]->PortAddress, *OutputControl[arraynumber]->PortData);
 
 return 1;
}

And to turn it off:

 
 
int TurnOff(int arraynumber)
{
 if(OutputControl[arraynumber] == NULL)
 return 0; // node not set up
 
 // keep existing bits but remove this one
 *OutputControl[arraynumber]->PortData &= OutputControl[arraynumber]->offmask;
 
 // put the result in this node's port register
 outp(OutputControl[arraynumber]->PortAddress, *OutputControl[arraynumber]->PortData);
 
 return 1;
}

Taking a closer look:

 
 Store to the Port Data variable, 
 
 through the at location its current value with the on
 dereferenced pointer arraynumber, ORed mask value.
 | | | |
 *OutputControl [arraynumber]->PortData |= OutputControl[arraynumber]->onmask;
 
 Then output it to the port at arraynumber
 
 outp(OutputControl[arraynumber]->PortAddress, *OutputControl[arraynumber]->PortData);
 

Turning off a bit works the same way except that the port data value is ANDed with the off mask in order to turn off the desired bit. It is important to note that up to 8 nodes can access a single port data variable. That's what the pointers do for us. Each manipulates only the bit it uses to turn on or off its particular line.

Now add the On/Off routines to the digital C module. You will need to change the starting values of the port data variables to 0 so things will work correctly:

 
// digi6f.c
 
#include <stdio.h>
 
// The following are known only to the functions in this file.
// They can't be modified or even accessed by anything outside this
// file except through funtions in this file designed to provide access.
 
struct OC
{
 int PortAddress;
 char onmask;
 char offmask;
 int *PortData;
};
 
struct OC *OutputControl[24];
 
unsigned base;
unsigned switch_port;
unsigned ppi_porta;
unsigned ppi_portb;
unsigned ppi_portc;
 
int porta_val = 0;
int porta_mask;
 
int portb_val = 0;
int portb_mask;
 
int portc_val = 0;
int portc_mask;
 
// configure the array number location to a port
// and bit number dictated by portselect
int ConfigureOutput(int arraynumber, int portselect)
{
 int x;
 
 if(arraynumber < 0 || arraynumber > 23)
 {
 printf("arraynumber error --- %d\n",arraynumber);
 return 0; // illegal number
 }
 
 if(portselect < 0 || portselect > 23)
 {
 printf("portselect error --- %d\n",portselect);
 return 0; // illegal number
 }
 
 if(OutputControl[arraynumber] == NULL)
 {
 if((OutputControl[arraynumber] = malloc(sizeof(struct OC))) == NULL)
 {
 printf("Not enough memory\n");
 return 0;
 }
 }
 
 x = portselect >> 3; // the port number is in bits 3 and 4
 
 switch(x) // 0 = Port A, 1 = Port B, 2 = Port C
 {
 case 0:
 OutputControl[arraynumber]->PortAddress = ppi_porta; // address for Port A
 OutputControl[arraynumber]->PortData = &porta_val; // point to Port A data value
 break;
 
 case 1:
 OutputControl[arraynumber]->PortAddress = ppi_portb; // address for Port B
 OutputControl[arraynumber]->PortData = &portb_val; // point to Port B data value
 break;
 
 case 2:
 OutputControl[arraynumber]->PortAddress = ppi_portc; // address for Port C
 OutputControl[arraynumber]->PortData = &portc_val; // point to Port C data value
 break;
 }
 
 OutputControl[arraynumber]->onmask = 1 << (portselect & 7); // shift by bits[2:0]
 OutputControl[arraynumber]->offmask = ~OutputControl[arraynumber]->onmask;
 
// /* add double slash rem to print debug statements
 
 printf("arraynum=%02d port select=%02d ",arraynumber,portselect);
 
 printf("Addr=%#X "
 ,OutputControl[arraynumber]->PortAddress);
 
 printf("*Data=%X "
 ,*OutputControl[arraynumber]->PortData);
 
 printf("onmsk=");
 for(x=128; x>0; x/=2)
 {
 if(x & OutputControl[arraynumber]->onmask)
 printf("1");
 else printf("0");
 }
 
 printf(" offmsk=");
 for(x=128; x>0; x/=2)
 {
 if(x & OutputControl[arraynumber]->offmask)
 printf("1");
 else printf("0");
 }
 printf("\n");
 
// add double slash rem to print debug statements */ 
 
 return 1;
}
 
 
// free the output control structures
void FreeOutputControl(void)
{
 int x;
 
 for(x=0; x<24; x++)
 {
 if(OutputControl[x] != NULL)
 free(OutputControl[x]);
 }
}
 
 
// turn on an output node
int TurnOn(int arraynumber)
{
 if(OutputControl[arraynumber] == NULL)
 {
 printf("can't turn on -- location %d not set up\n",arraynumber);
 return 0; // node not set up
 }
 
 // keep existing bits and OR this one in
 *OutputControl[arraynumber]->PortData |= OutputControl[arraynumber]->onmask;
 
 // put the result in this node's port register
 outp(OutputControl[arraynumber]->PortAddress, *OutputControl[arraynumber]->PortData);
 
 return 1;
}
 
 
// turn off an output node
int TurnOff(int arraynumber)
{
 if(OutputControl[arraynumber] == NULL)
 {
 printf("can't turn off -- location %d not set up\n",arraynumber);
 return 0; // node not set up
 }
 
 // keep existing bits but remove this one
 *OutputControl[arraynumber]->PortData &= OutputControl[arraynumber]->offmask;
 
 // put the result in this node's port register
 outp(OutputControl[arraynumber]->PortAddress, *OutputControl[arraynumber]->PortData);
 
 return 1;
}
 
// set up the ppi according to the dictates of the mode argument
void set_up_ppi(int mode)
{
 unsigned control = base + 0x23;
 int command;
 
 // make certain control locations start at NULL
 for(command=0; command<24; command++)
 OutputControl[command] = NULL;
 
 command = (mode & 0x0c) << 1; // shift bits 2 and 3 into positions 4 and 5
 command += (mode & 3); // add in bits 0 and 2
 command |= 0x80; // OR in bit 7 for PPI set up
 
 outp(control, command); // set according to mode command
 
} // end set_up_ppi()

Use experi6f.c to test the routines:

NOTE: Please be sure to read the Warranty And Disclaimer before working with the hardware!

 
 
// experi6f.c
 
void waitalittlewhile(void); // timer for test only
 
#include <malloc.h>
#include <conio.h>
#include <stdio.h>
#include <bios.h>
 
// include header with constants
#include "constant.h"
 
// include header with external prototypes
#include "extern6f.h"
 
enum
{
 WarningLED,
 MainMotor,
 LeftArmMotor
};
 
void main(void)
{
 int x,y,*ptr1,*ptr2;
 
 get_port(); // get the port number and establish register locations
 
 // make everthing an output
 set_up_ppi(Aout_CUout_Bout_CLout);
 
 if(!ConfigureOutput(WarningLED,PA0))
 printf("Error setting up Warning LED = %d port select = %d\n"
 ,WarningLED,PA0);
 
 if(!ConfigureOutput(MainMotor,PA1))
 printf("Error setting up Main Motor = %d port select = %d\n"
 ,MainMotor,PA1);
 
 if(!ConfigureOutput(LeftArmMotor,PA2))
 printf("Error setting up Left Arm Motor = %d port select = %d\n"
 ,LeftArmMotor,PA2);
 
 while(!kbhit())
 {
 if(!TurnOn(WarningLED))
 printf("Can't turn on the Warning LED\n");
 else printf("Turned on the Warning LED\n");
 waitalittlewhile();
 
 if(!TurnOff(WarningLED))
 printf("Can't turn off the Warning LED\n");
 else printf("Turned off the Warning LED\n");
 waitalittlewhile();
 
 if(!TurnOn(LeftArmMotor))
 printf("Can't turn on the Left Arm Motor\n");
 else printf("Turned on the Left Arm Motor\n");
 waitalittlewhile();
 
 if(!TurnOff(LeftArmMotor))
 printf("Can't turn off the Left Arm Motor\n");
 else printf("Turned off the Left Arm Motor\n");
 waitalittlewhile();
 
 if(!TurnOn(MainMotor))
 printf("Can't turn on the Main Motor\n");
 else printf("Turned on the Main Motor\n");
 waitalittlewhile();
 
 if(!TurnOff(MainMotor))
 printf("Can't turn off the Main Motor\n");
 else printf("Turned off the Main Motor\n");
 waitalittlewhile();
 }
 
 // don't forget to free memory!
 FreeOutputControl();
 
 portaoff();
 
} // end experi6f.c
 
void waitalittlewhile(void)
{
 long x;
 
 for(x=0L; x<100000L; x++);
 
} // end waitalittlewhile(..)


Click here to download experi6f.c
Click here to download digi6f.c
Click here to download constant.h
Click here to download extern6f.h

You will need to hook up some LEDs and/or motors using the additional circuitry in Experiment 5 to test the program. That would be a very good idea, since what follows depends on what has past (quick, somebody write that down!). Seriously though, you need to make certain ConfigureOutput(..) works as it should before moving forward, and you won't really know that until you are sure the proper port lines are activated when commanded to do so. The example only uses Port A. It would be a very good idea to try the others as well. You might need to play with the number in the delay loop a little to get it to work right on your computer. Take out the // remark signs so ConfigureOutput(..) won't print any debug information once you get it working properly. Speaking of bugs, Professor/Admiral Grace Murray Hopper found the first computer bug in the Mark II computer on September 9th, 1945 -- a moth got caught in one of the relays. (read more about this computer science pioneer)

The delay loop is still a problem. A more reliable way of timing events is needed. Fortunately, the computer has a built-in timer. It is a very accurate crystal-controlled device that produces an interrupt at reliable intervals.

An interrupt is analogous to the bell on a telephone. A telephone with no bell would require a person to periodically pick it up to see if anyone was on the other end, which would be debatably more annoying than having to answer its ring.

Devices such as the timer can interrupt the microprocessor to cause it to sevice the interrupt as required. Special functions called interrupt handlers or interrupt service routines ( ISR ) are set up for that purpose. Each interrupt is assigned a number which is a reference to the location of a pointer in the first 1024 bytes of memory in a PC, much like the index to the array of pointers we used earlier. The table is called an interrupt vector table or dispatch table.

Our only concern here will be the first 16 locations. They are used for hardware interrupt requests ( IRQ ). Each pointer uses four bytes. The following program will display the first 16 pointers or vectors of a machine. It first sets a pointer to character to NULL, which means it points to location 0 in memory. The for() loop starts both x and n at 0, but increments x by 4 while incrementing n by 1. Thus, n keeps track of the interrupt number while x keeps track of the memory location of the vector. The dereferenced ptr shows what is in memory.

 
 
// experi6g.c
 
#include <conio.h>
#include <stdio.h>
 
void main(void)
{
 int x,y,n;
 char *ptr;
 ptr = NULL;
 
 for(x=0,n=0; n<16; x+=4,n++)
 {
 printf("Number = %03d Memory Location = %04d Vector = ",n,x);
 for(y=0; y<2; y++)
 {
 printf("%02X",*ptr);
 ptr++;
 }
 printf(":");
 for(y=0; y<2; y++)
 {
 printf("%02X",*ptr);
 ptr++;
 }
 printf("\n");
 }
 
}
 
// experi6g.c
 


Click here to download experi6g.c

The program should produce the first 16 locations as shown below, although the pointer values will vary among machines. The pointers are found in memory in a segment:offset format. The segment/offset scheme is a way to address more memory than 16 bit registers would normally allow. It divides memory into 65536 16-byte segments or paragraphs, for a total of 65536 * 16 = 1048576 bytes. That means the starting point of the segment is easy to get -- just multiply the segment value by 16. The offset is the number of bytes into the segment. Thus, to determine where something actually is in memory, simply multiply the segment by 16 then add the offset.

The timer uses interrupt 8. It can be seen from the table below that the vector is at 3120:3032 for the machine the program was run on. Remember from above however, that the bytes are reversed in memory. Thus, the segment = 0x2031 and the offset = 0x3230. First, Multiply the segment by 16. That is the same as shifting it to the left 4 bits, which gives us the HEX segment number with a 0 stuck on the right end of it -- 0x20310 -- now add the offset:
0x20310 + 0x3230 = 0x23540 = 144704 decimal

 
 
Number = 000 Vector Table Location = 0000 Vector = 2449:643A
Number = 001 Vector Table Location = 0004 Vector = 2072:7438
Number = 002 Vector Table Location = 0008 Vector = 362D:2D64
Number = 003 Vector Table Location = 0012 Vector = 732E:6173
Number = 004 Vector Table Location = 0016 Vector = 2C76:2031
Number = 005 Vector Table Location = 0020 Vector = 2E31:2031
Number = 006 Vector Table Location = 0024 Vector = 3939:382F
Number = 007 Vector Table Location = 0028 Vector = 3038:2F32
Number = 008 Vector Table Location = 0032 Vector = 3120:3032 (timer interrupt)
Number = 009 Vector Table Location = 0036 Vector = 3A32:333A
Number = 010 Vector Table Location = 0040 Vector = 3535:2063
Number = 011 Vector Table Location = 0044 Vector = 6C79:6465
Number = 012 Vector Table Location = 0048 Vector = 2045:7870
Number = 013 Vector Table Location = 0052 Vector = 2024:0043
Number = 014 Vector Table Location = 0056 Vector = 204C:6962
Number = 015 Vector Table Location = 0060 Vector = 7261:7279

Ralf Browns Interrupt List will provide you with more detail on vectors if you are interested. The routine for 8 actually invokes interrupt 1C which can be used by programs other than the operating system. That would be us. A special routine is written to handle the interrupt. The compiler knows what it is by the interrupt keyword:

 
 
// this happens on a timer interrupt
interrupt new_timer()
{
 disable(); // disable interrupts
 
 timer_counter++; // increment the counter
 
 enable(); // enable interrupts
 
} // end interrupt new_timer()

Interrupt handlers have no return value and no arguments. Notice how little the routine does. Interrupt handlers should always do as little as possible due to the fact that they might prevent other important processes from taking place if they do too much. That's because they usually turn off interrupts before doing their work. The compiler I use uses the call to disable() to turn off interrupts, as do many others. You might have to check to find out what yours uses.

Here is the updated extern.h with a few things not yet discussed:

 
 
// extern6h.h
 
// external prototypes
 
// digital routines -- also put at the top of digital.c without "extern"
extern int ConfigureOutput(int arraynumber, int portselect);
extern int TurnOn(int arraynumber);
extern int TurnOff(int arraynumber);
extern int is_closure(int input);
extern void set_up_ppi(int mode);
extern void blinker(long on, long off);
extern void btoa(void);
extern void motor(long on, long off);
extern void motor2(long on, long off);
extern void portaon(void);
extern void portaoff(void);
extern void portbon(void);
extern void portboff(void);
extern void portcon(void);
extern void portcoff(void);
 
// timer routines -- also put at the top of timer.c without "extern"
extern double get_frequency(void);
extern long get_timer_counter(void);
extern void set_up_new_timer(void);
extern void wait(double seconds);
extern void restore_old_timer(void);
 
// end extern6h.h


Click here to download extern6h.h

The location of the new interrupt service routine must be placed in the vector table so the system can find it. The location of the old routine must also be saved so the vector can be restored when the program is through with it. A call to getvect(...) will get the pointer to the former location so it can be saved, and a call to setvect(...) will put the new one in the table. Since the former routine is probably not located in this program's segment, it is declared as a far pointer. A routine to get the value of the timer counter will also be added. Notice the two prototypes just above the timer routines in the following. One is the new interrupt handler. The other is a place holder for the old handler.

The following is the timer module. Notice that the prototypes contained in this module are declared here as well as in extern6h.h. Here however, they are not declared as externals, since they are not external to this file. The digital prototypes should be placed at the top of digital.c in a like manner. It helps keep the confusion down for the compiler. With the compiler I use, the timer test would not run properly until I included the prototypes in both places.

Notice how the far pointer to the old timer routine is declared. The name is surrounded by parenthesis due to the precedence problems that would occur without them (see Experiment 3). Also notice the call to disable() and enable() in almost all of the timer routines. They disable interrupts for the short time needed to perform various tasks, then enable interrupts. It's not a good idea to have an interrupt occur while you are trying to work with something that is or will become part of an interrupt routine.

Recall that we will be dealing with the vector information at location 0x1C in the vector table. To set up the new routine, the old timer routine location is saved in the old_timer pointer using getvect(0x1c). The new routine is then recorded in the vector table using setvect(0x1c, new_timer). Restoring the old routine is simply a matter of using setvect() with the old timer pointer.

 
 
// timer6h.c
 
#include <dos.h>
#include <stdio.h>
#include <bios.h>
#include "extern6h.h"
 
long get_timer_counter(void);
void set_up_new_timer(void);
void wait(double seconds);
void restore_old_timer(void);
double get_frequency(void);
 
void interrupt new_timer(), interrupt (far *old_timer)();
 
unsigned long timer_counter;
 
// save the old vector, set up new vector, zero out counter
void set_up_new_timer(void)
{
 disable(); // turn off interrupts
 
 old_timer = getvect(0x1c);
 
 setvect(0x1c, new_timer);
 
 timer_counter = 0L;
 
 enable(); // turn interrupts back on
}
 
// restore former table entry and rate
void restore_old_timer()
{
 disable();
 
 setvect(0x1c, old_timer);
 
 enable();
}
 
// return the value of the counter to the caller
long get_timer_counter(void)
{
 return timer_counter;
}
 
// the interrupt handler
interrupt new_timer()
{
 disable();
 
 timer_counter++;
 
 enable();
}
 
// end timer6h.c


Click here to download timer6h.c

Now the new routine, rather than the old routine will be called when a timer interrupt occurs. All the new_timer() routine does is increment the timer counter which was set to 0 when the routine was placed in the vector table. It will do a little more latter, but should never do too much. It especially should not call such things as printf().

The reason is that many routines are not reentrant , which is to say they can't be reliably re-entered while they are in the middle of something. That is often because they use the same piece of memory each time for what they do. For example, something in main() could be using printf() when an interrupt occurs. If the ISR then uses printf(), the fixed-memory location would be clobbered and the results would probably look like anything but what was expected. Similar situations can cause much worse things to happen. See this article by David K. Every if you are interested in more detail about reentrant code.

The test program is simplicity personified:

 
 
// experi6h.c
 
#include <dos.h>
#include <stdio.h>
#include <bios.h>
#include "extern6h.h";
 
void main(void)
{
 set_up_new_timer();
 
while(!kbhit())
 {
 printf("%ld\n",get_timer_counter());
 }
 
 restore_old_timer();
}


Click here to download experi6h.c
Click here to download extern6h.h

Now compile experi6h and timer6h and link them. The end product should be experi6h.exe. Run experi6h and you should see the timer counter changing. The only thing that's changing the variable however, is the timer interrupt service routine. Nothing in experi6h has access to the counter.

The timer in the PC is driven by a 1193180 Hz (cycles per second) clock. It looks a lot like the square wave in Experiment 5 when viewed on an oscilloscope . There is a 16-bit divider register in the timer that is set to 0. The register counts down from wherever it's set -- 0 in this case -- with the first pulse rolling the register over from 0 to 65535. The timer will issue an interrupt when the register reaches 0 (but not on the first, loaded 0). It will then re-load the register with the original value, which it retains internally. The result is to divide the clock signal by 65536, producing an effective output of 18.20648193 Hz, and one interrupt every .054925493 seconds, or about 55ms.

It would be nice to be able to set up the timer for any rate desired. No problem. Just set up the register with 1193180/frequency. Let's say 1000 Hz is desired so the system will produce 1000 interrupts per second. 1193180/1000 = 1193.180. Digital register can't be loaded with a floating point number, but 1193 gets pretty close since 1193180/1193 = 1000.15088 Hz. The port address for the timer register is 0x40. The same location is used for the ms and ls values. It's the order that determines what will go where -- least significant then most significant. The new set_up_new_timer() gets a double as an argument. That's the same as a float, but with higher precision. The argument tells set_up_new_timer() what frequency to use in setting up the timer. Another double called divideby is used to determine what the divider would be by setting it equal to the input frequency divided into 1193180. Since only the result less the fraction will be used, divideby is rounded by adding .5 to it. Any fraction >= .5 will cause the whole number part to move up by one. Recall that we need to end up with is a 16-bit number for the timer's counter register. The most signifcant portion is the upper 8 bits, and the least significant portion is the lower 8 bits. The ms portion is divideby shifted to the right 8 bits, with divideby cast as an unsigned integer. Forgotten what some of that is all about?:
most significant and least significant: Data Lines
shift operators: Experiment 1
casting: Experiment 5

The actual frequency that results is then calculated for access by other routines through get_frequency().

 
 
// timer6i.c
 
#include <dos.h>
#include <stdio.h>
#include <bios.h>
#include "extern6i.h"
 
long get_timer_counter(void);
int set_up_new_timer(double freq);
void wait(double seconds);
void restore_old_timer(void);
double get_frequency(void);
 
void interrupt new_timer(), interrupt (far *old_timer)();
 
unsigned long timer_counter;
double frequency;
 
// save the old vector, set up new vector, zero out counter
// set up timer rate
int set_up_new_timer(double freq)
{
 unsigned ms,ls;
 double divideby;
 
 if(freq < (1193180.0/65536.0))
 return 0; // can't go below this
 
 if(freq > 1193180.0)
 return 0; // or above this
 
 divideby = 1193180.0/freq;
 
 divideby+=0.5; // causes a round above .5
 
 ms = (unsigned)divideby >> 8; // get upper 8 for ms
 ls = (unsigned)divideby & 0xff; // mask off lower 8 for ls
 
 frequency = 1193180.0/(double)((ms << 8) + ls);
 
 timer_counter = 0L;
 
 disable(); // turn off interrupts
 
 outp(0x40, ls); // least significant byte of timer count
 outp(0x40, ms); // most significant byte of timer count
 
 old_timer = getvect(0x1c);
 
 setvect(0x1c, new_timer);
 
 enable(); // turn interrupts back on
 
 return 1;
}
 
// restore former table entry and rate
void restore_old_timer()
{
 disable();
 
 outp(0x40, 0); // least significant byte of timer count
 outp(0x40, 0); // most significant byte of timer count
 
 setvect(0x1c, old_timer);
 
 enable();
}
 
// return the frequency to the caller
double get_frequency(void)
{
 return frequency;
}
 
// return the value of the counter to the caller
long get_timer_counter(void)
{
 return timer_counter;
}
 
// wait for seconds and/or fractions of a second
void wait(double seconds)
{
 long wait_count, start_count;
 
 if(!seconds)
 return;
 
 if(timer_counter < 0L)
 return;
 
 wait_count = (long)((seconds * frequency) + 0.5); // round at .5
 
 start_count = timer_counter;
 
 while((timer_counter - start_count) < wait_count);
}
 
// the interrupt handler
interrupt new_timer()
{
 disable();
 
 timer_counter++;
 
 enable();
}
 
// end timer6i.c


Click here to download timer6i.c

The wait() routine takes advantage of the new timer setup. It is sent a double argument called seconds, representing the number of seconds of delay desired, which can include decimal fractions. The number of counts to wait will be the number of interrupts per second times the number of seconds to wait. Since the number of interrupts per second is the frequency calculated during setup, the count needed is the number of seconds desired times the frequency. This is rounded then cast to a long variable called wait_time. Another long called start_count gets the current counter value timer_counter. From then on, the number of counts elapsed will be equal to timer_counter - start_count. A while loop waits for that difference to be >= wait_count. Please note that set_up_new_timer() must be called or none of this will work. Also, it would probably be a good idea not to try to set the frequency too high. My test machine didn't like it when I tried 10K Hz. It worked up to 5K Hz just fine.

The new extern header:

 
 
// extern6i.h
 
// external prototypes
 
// digital routines -- also put at the top of digital.c without "extern"
extern int ConfigureOutput(int arraynumber, int portselect);
extern int TurnOn(int arraynumber);
extern int TurnOff(int arraynumber);
extern int is_closure(int input);
extern void set_up_ppi(int mode);
extern void blinker(long on, long off);
extern void btoa(void);
extern void motor(long on, long off);
extern void motor2(long on, long off);
extern void portaon(void);
extern void portaoff(void);
extern void portbon(void);
extern void portboff(void);
extern void portcon(void);
extern void portcoff(void);
 
// timer routines -- also put at the top of timer.c without "extern"
extern double get_frequency(void);
extern long get_timer_counter(void);
extern int set_up_new_timer(double freq);
extern void wait(double seconds);
extern void restore_old_timer(void);
 
// extern6i.h

The test program sets the timer up for 4K Hz then attempts a 60 second timer at 1 second intervals. It waits for a keystroke to start so a watch can be used to check accuracy. The call to getch() waits for a single character from the keyboard. It would be a very good idea to compile timer and experi6i, link them, then run this test since the routines will be used in the future:

 
 
// experi6i.c
 
#include <dos.h>
#include <stdio.h>
#include <bios.h>
#include "extern6i.h"
 
void main(void)
{
 int x;
 
 set_up_new_timer(4000);
 
 printf("frequency = %f\n",get_frequency());
 
 printf("Press any key to begin test: ");
 getch(); // wait for key
 puts(""); // print a blank line
 
 for(x=0; x<60; x++)
 {
 if(kbhit())
 break;
 printf("x = %2d counter = %6ld\n",x,get_timer_counter());
 wait(1);
 }
 
 // don't forget this one -- your computer could freeze!!!
 restore_old_timer();
}
 
// end experi6i.c


Click here to download experi6i.c
Click here to download extern6i.h

The following test program combines output control with the interrupt-based timing routines. It is put together by linking the three object files after compiling. In MIX PowerC that would be:
pcl experi6j digital timer

This will produce an executible called experi6j.exe. Notice how the timer was set up. The desired interval is .1 second. In order to get good precision, the timer was set up to be about 100 times that fast. This was done by setting the timer close to 1000 Hz, but not exactly. The idea is to get close to the precision needed, but to try to avoid rounding in the counter calculations. To do that, divide the clock by the desired frequency, then use the nearest non-fractional number. For example, 1193180/1000 = 1193.180. Another one: the goal is to get close to 5K Hz. 1193180/5000 = 238.636. Use 1193180/239. Here, the closest non-fractional number to 1193.180 is 1000, so use 1193180/1000 as the argument for set_up_new_timer().

 
 
// experi6j.c
 
#include <dos.h>
#include <stdio.h>
#include <bios.h>
 
// include header with constants
#include "constant.h"
 
// include header with external prototypes
#include "extern.h"
 
enum
{
 WarningLED,
 MainMotor,
 LeftArmMotor
};
 
void main(void)
{
 int x;
 
 get_port(); // get the port number and establish register locations
 
 // make everthing an output
 set_up_ppi(Aout_CUout_Bout_CLout);
 
 printf("1193180/1000 = %f\n"
 ,1193180/1000);
 
 set_up_new_timer(1193180/1000);
 
 printf("frequency = %f\n",get_frequency());
 
 if(!ConfigureOutput(WarningLED,PA0))
 printf("Error setting up Warning LED = %d port select = %d\n"
 ,WarningLED,PA0);
 
 if(!ConfigureOutput(MainMotor,PA1))
 printf("Error setting up Main Motor = %d port select = %d\n"
 ,MainMotor,PA1);
 
 if(!ConfigureOutput(LeftArmMotor,PA2))
 printf("Error setting up Left Arm Motor = %d port select = %d\n"
 ,LeftArmMotor,PA2);
 
 while(!kbhit())
 {
 if(!TurnOn(WarningLED))
 printf("Can't turn on the Warning LED\n");
 else printf("Turned on the Warning LED\n");
 wait(.1);
 
 if(!TurnOff(WarningLED))
 printf("Can't turn off the Warning LED\n");
 else printf("Turned off the Warning LED\n");
 wait(.1);
 
 if(!TurnOn(LeftArmMotor))
 printf("Can't turn on the Left Arm Motor\n");
 else printf("Turned on the Left Arm Motor\n");
 wait(.1);
 
 if(!TurnOff(LeftArmMotor))
 printf("Can't turn off the Left Arm Motor\n");
 else printf("Turned off the Left Arm Motor\n");
 wait(.1);
 
 if(!TurnOn(MainMotor))
 printf("Can't turn on the Main Motor\n");
 else printf("Turned on the Main Motor\n");
 wait(.1);
 
 if(!TurnOff(MainMotor))
 printf("Can't turn off the Main Motor\n");
 else printf("Turned off the Main Motor\n");
 wait(.1);
 }
 
 portaoff();
 
 // always free memory!
 FreeOutputControl();
 
 // always restore the old timer!
 restore_old_timer();
}
 
// end experi6j.c


Click here to download experi6j.c

The timer module can use the output control structure if it is provided with its description and declares it as an external. To do this, move the structure definition to outcont.h. A few more members have been added for future use. The name here is outcnt6k.h:

 
 
// outcnt6k.h
 
struct OC
{
 int PortAddress; // address of the port with this output line
 
 char onmask; // mask that will turn this line on when
 // ORed with the data value and stored back
 
 char offmask; // mask that will turn this line off when
 // ANDed with the data value and stored back
 
 int *PortData; // pointer to the data value for the port
 
 long seton; // on-time setting for this line
 // a -1 means run continuously
 
 long setoff; // off-time setting for this line
 
 long oncount; // counts left for on time
 
 long offcount; // counts left for off time
};
 
// end outcnt6k.h


Click here to download outcnt6k.h

The digital C file module no longer contains the declaration for the OC structure. It now includes outcont.h which does that. It does however, declare an instance of the structure:

struct OC *OutputControl[24];

The timer C module file also includes outcnt6k.h, but shows the instance of OC as external:

extern struct OC *OutputControl[24];

The OutputControl array of OC pointers can be viewed as being in digital and accessable to both the digital and timer modules. Here are the tops of each.

Digital -- OutputControl belongs to the digital C module:

 
 
// digi6k.c
 
#include <dos.h>
#include <stdio.h>
#include <bios.h>
#include "outcnt6k.h" // defines output control structure
 
struct OC *OutputControl[24];
 
// prototypes
int ConfigureOutput(int arraynumber, int portselect);
int TurnOn(int arraynumber);
int TurnOff(int arraynumber);
int is_closure(int input);
void set_up_ppi(int mode);
void blinker(long on, long off);
void btoa(void);
void motor(long on, long off);
void motor2(long on, long off);
void portaon(void);
void portaoff(void);
void portbon(void);
void portboff(void);
void portcon(void);
void portcoff(void);
 
// The following are known only to the functions in this file.
// They can't be modified or even accessed by anything outside this
// file except through funtions in this file designed to provide access.
 .
 .
 .
 .
 .
 
// end digit6k.c

Timer -- OutputControl is external but can be accessed by the timer C module:

 
// timer6k.c
 
#include <dos.h>
#include <stdio.h>
#include <bios.h>
#include "outcnt6k.h" // defines output control structure
 
// in digi6k.c
extern int ConfigureOutput(int arraynumber, int portselect);
extern struct OC *OutputControl[24];
 
// local prototypes
long get_timer_counter(void);
int set_up_new_timer(double freq);
void wait(double seconds);
void restore_old_timer(void);
double get_frequency(void);
 
void interrupt new_timer(), interrupt (far *old_timer)();
 
unsigned long timer_counter;
double frequency;
 .
 .
 .
 .
 .
 
// end timer6k.c

It would actually be a lot cleaner to put everything at the top of the files into corresponding header files, then include the header files. Everything above that goes at the top of digi6k.c would be put in digi6k.h, then #include "digi6k.h" entered at the top of digi6k.c Everything above that goes at the top of timer6k.c would be put in timer6k.h, then #include "timer6k.h" entered at the top of timer6k.c It makes no difference how you do it. It cleans up the C file to have the header, but it's easier to see what items are if they are declared at the top of the file being worked on.

Since the timer module now has access to the output control array, it can be used to set some of the variables for a node of the array. The following sets up pulse width modulation for a node. It first checks to make certain that offtime is reasonable, then calls ConfigureOutput(...). The seton member is set to -1 if the on time is less than 0, indicating the node stays on. Otherwise, seton is calculated to be the frequency (which is the number of interrupts per second) times the number of seconds desired. If ontime is greater than 0, the oncount member is set equal to the seton member, the setoff member is calculated and the node is turned on. The setoff and oncount members are set to 0 if ontime is less than or equal to 0. Offcount is always set to 0:

 
// Set up Pulse Width Modulation for an output
//
// arraynumber is the position in the output control array
//
// type:
// 0 = unidirectional, no brake
// 1 = unidirectional with brake
// 2 = pwm line, directional line, no brake
// 3 = pwm line, directional line, with brake
// 4 = dual pwm lines -- both high = brake
// 5 = pwm line and two direction lines as for L298
// 255 = last slot -- leave
//
// Forward and Reverse port numbers are pwm lines for each
//
// The Direction port number is provided for bridges that have a reverse line
// Set to anything if not used
//
// The Brake port number is provided for circuits that have a brake line
// Set to anything if not used
//
int pwm(int arraynumber, int type,
 int ForwardPortNumber, int ReversePortNumber,
 int DirectionPortNumber, int BrakePortNumber,
 double ForwardOnTime, double ForwardOffTime,
 double ReverseOnTime, double ReverseOffTime,
 int StartDirection)
{
 if(StartDirection < 0 || StartDirection > 2)
 return 0;
 
 if(ForwardOnTime <= MinTime || ForwardOnTime >= MaxTime
 || ForwardOffTime <= MinTime || ForwardOffTime >= MaxTime)
 return 0;
 
 disable(); // no interrupts while setting up 
 
 if(!ConfigureOutput(arraynumber, type, 
ForwardPortNumber, 
ReversePortNumber,
 DirectionPortNumber,
 BrakePortNumber))
 {
 enable();
 return 0;
 }
 
OutputControlActive = 1;
 
 OutputControl[arraynumber]->ForwardSetOn =
 (long)((frequency * ForwardOnTime) + 0.5); // round up at .5
 
 OutputControl[arraynumber]->ForwardSetOff 
= (long)((frequency * ForwardOffTime) + 0.5);
 
 OutputControl[arraynumber]->ForwardOnCount
 = OutputControl[arraynumber]->ForwardSetOn;
 
 OutputControl[arraynumber]->ForwardOffCount
 = OutputControl[arraynumber]->ForwardSetOff;
 
 OutputControl[arraynumber]->direction = StartDirection;
 
 OutputControl[arraynumber]->type = type;
 
 if(!type) // uni directional
 {
 enable();
 return 1;
 }
 
 if(type == 1 || type == 3) // 1 and 3 have a brake
 {
 *OutputControl[arraynumber]->BrakePortData
 &= OutputControl[arraynumber]->BrakeOffMask; // turn off the brake
 
 outp(OutputControl[arraynumber]->BrakePortAddress, 
*OutputControl[arraynumber]->BrakePortData);
 }
 
 if(type > 1) // 2,3,4,5 use reverse pwm line, 2,3,5 use direction line
 {
 if(ReverseOffTime <= MinTime || ReverseOffTime >= MaxTime
 || ReverseOffTime <= MinTime || ReverseOffTime >= MaxTime)
 {
 free(OutputControl[arraynumber]);
 OutputControl[arraynumber] = NULL;
 enable();
 return 0;
 }
 
OutputControl[arraynumber]->ReverseSetOn =
 (long)((frequency * ReverseOnTime) + 0.5); // round up at .5
 
 OutputControl[arraynumber]->ReverseOnCount
 = OutputControl[arraynumber]->ReverseSetOn;
 
 OutputControl[arraynumber]->ReverseSetOff =
 (long)((frequency * ReverseOffTime) + 0.5);
 
 OutputControl[arraynumber]->ReverseOffCount
 = OutputControl[arraynumber]->ReverseSetOff;
 
 if(type == 2 || type == 3 || type == 5) // 2,3,5 use a direction line
 {
 if(StartDirection == 1)
 *OutputControl[arraynumber]->DirectionPortData
 != OutputControl[arraynumber]->DirectionOnMask; // set for forward
 
 else if(StartDirection == 2)
 *OutputControl[arraynumber]->DirectionPortData
 &= OutputControl[arraynumber]->DirectionOffMask; // clear for reverse
 
 outp(OutputControl[arraynumber]->DirectionPortAddress, 
*OutputControl[arraynumber]->DirectionPortData);
 
 if(type == 5)
 {
 if(StartDirection == 1)
 *OutputControl[arraynumber]->BrakePortData
 &= OutputControl[arraynumber]->BrakeOffMask; // turn off the brake
 
 else if(StartDirection == 2)
 *OutputControl[arraynumber]->BrakePortData
 |= OutputControl[arraynumber]->BrakeOnMask; // turn on the brake
 
 outp(OutputControl[arraynumber]->BrakePortAddress, 
*OutputControl[arraynumber]->BrakePortData);
 }
 }
 
 } // end if(type > 1)
 
 enable();
 
 return 1;
 
} // end int pwm(..)

This information is used by the updated timer interrupt service routine to determine when an output should be turned on or off.

The routine now runs through the 24 Output Control nodes. It skips all nodes that have not been set up or ones that run all the time.

Notice that the on count was set to seton in pwm(..), and that the off count was set to 0. Thus, the first time around, new_timer() will find that the on counter is greater than 0 and decrement it. If the on counter is made zero by the decrement, the node will be turned off and the off counter loaded with setoff. If, on the other hand, the off counter is greater than zero, it will be decremented. If it is made zero by the decrement, the node will be turned back on and the on counter loaded with seton again.

 
 
// the timer interrupt handler
interrupt new_timer()
{
 int x;
 
 disable();
 
 timer_counter++;
 
 for(x=0; x<24; x++)
 {
 if(OutputControl[x] == NULL) // not set up
 continue;
 
 if(OutputControl[x]->seton <= 0L) // stay on or not set
 continue;
 
 if(OutputControl[x]->oncount > 0L)
 {
 OutputControl[x]->oncount--;
 
 if(!OutputControl[x]->oncount)
 {
 // keep existing bits but remove this one
 *OutputControl[x]->PortData &= OutputControl[x]->offmask;
 
 // put the result in this node's port register
 outp(OutputControl[x]->PortAddress, *OutputControl[x]->PortData);
 
 OutputControl[x]->offcount = OutputControl[x]->setoff;
 }
 
 } // end if(OutputControl[x]->oncount > 0L)
 
 // note that this will not decrement as soon as set
 // above, but will wait until the next interrupt
 else if(OutputControl[x]->offcount > 0L)
 {
 OutputControl[x]->offcount--;
 
 if(!OutputControl[x]->offcount)
 {
 // keep existing bits and OR this one in
 *OutputControl[x]->PortData |= OutputControl[x]->onmask;
 
 // put the result in this node's port register
 outp(OutputControl[x]->PortAddress, *OutputControl[x]->PortData);
 
 OutputControl[x]->oncount = OutputControl[x]->seton;
 }
 
 } // end else if(OutputControl[x]->offcount > 0L)
 
 } // end for(x=0; x<24; x++)
 
 enable();
}

A node is continually turned on and off automatically in the new_timer() ISR. All main() has to do is call pwm(..) to set it up. It can then go about its business. On and off times can range from sub-milliseconds to years! That makes it good for everything from controlling the speed of a motor to flashing an LED to turning on and off such things as air-conditioning systems (providing output devices are added that can take the current and voltage).

Here is the test program. There is no need to copy it as it and the other files can be downloaded below:

 
 
// experi6k.c
 
// test
extern void show(void);
 
#include <dos.h>
#include <stdio.h>
#include <bios.h>
 
// include header with constants
#include "const6a.h"
 
// include header with external prototypes
#include "extern6a.h"
 
enum
{
 MainMotor,
 WarningLED,
 LeftArmMotor
};
 
void main(void)
{
 int x;
 double ontime;
 
 get_port(); // get the port number and establish register locations
 
 // make everthing an output
 set_up_ppi(Aout_CUout_Bout_CLout);
 
 printf("1193180/1000 = %f\n",1193180.0/1000.0);
 
 set_up_new_timer(1193180.0/1000.0);
 
 printf("frequency = %f\n",get_frequency());
 
 if(!pwm(MainMotor,PA0, .003, .002))
 printf("Error setting up Warning LED = %d port select = %d\n"
 ,WarningLED,PA0);
 
 if(!pwm(WarningLED,PA1, .03, .02))
 printf("Error setting up Main Motor = %d port select = %d\n"
 ,MainMotor,PA1);
 
 if(!pwm(LeftArmMotor,PA2, .3, .2))
 printf("Error setting up Left Arm Motor = %d port select = %d\n"
 ,LeftArmMotor,PA2);
 
 show(); // a little test routine in timer that shows contents of nodes
 
 printf("press any key to end test\n");
 
 getch();
 
 // don't forget to free memory!
 FreeOutputControl();
 
 portaoff();
 
 restore_old_timer();
}
 
// end experi6k.c

To make life a little easier, you can download all of the files for this experiment below. On most machines, just right-click then do a save-as:

experi6k.c

digi6f.c

timer6k.c

timer6k.h

const6a.h

extern6a.h

outcnt6k.h

 

Now compile experi6k, timer6k and digi6f, then link them to form experi6k.exe. Notice the difference between this program and the previous ones? There is no infinite loop such as while(!kbhit()). Everything is set up, then there is a getch() which simply sits there and waits for a keystroke. Other activities could just as easily take place. The timer is taking care of pulse width modulation tasks in the background.

Using the proper interfacing and driver devices (see Experiment 5), hook up LEDs and/or motors to the outputs. Notice the difference in rates. The .3 seconds on and .2 seconds off for PA2 provides a normal-looking flasing LED cycle. PA1 on the other hand, is too fast for an indicator but could be used for motor speed control. PA0's 3ms on and 2ms off moves too fast to tell it's blinking at all, but works well for speed control. In other words, it's the choice of numbers that determines what an output will do.

Previous: Experiment 5 - Controlling Motors
Next: Experiment 7 - Bi-directional Control Of Motors And The H-Bridge

Problems, comments, ideas? Please Let me know what you think
Copyright 2001, Joe D. Reeder. All Rights Reserved.