Pages

Tuesday, 13 May 2014

How It Works #1: How does the PIC16F887 calculator work?

Introduction

When I first decided to put up this video showing a simple calculator made from PIC16F887, I did not expect a lot of viewership and comments. The first reason is that the programming language I used, CCS C, is not very popular. The second reason is that, compared to other videos on YouTube, my project can be considered child's play. In the months following the upload, I had received numerous people asking for the code. Whatever their purpose for requesting the code is, be it for school assignments or hobby projects, I decided to share it publicly after some editing to include sharing policy, clear comments, etc.

In this long (please bear with me) blog post, I will attempt to explain how the calculator works. I will assume you have at least basic knowledge of general C programming syntax and how to compile and program a PIC in CCS C.

Getting Information before Calculating

In order to perform calculation, the microcontroller (MCU) needs to gather some data. The simplest arithmetic calculations, i.e. addition, subtraction, multiplication, and division, require 3 things:
  1. First Argument
  2. Second Argument
  3. Operator
For addition and multiplication, the order of the arguments is not important, but for subtraction and division, it is important. Therefore, the MCU must know which one is the first and which one is the second. To understand this, first you need to know how the LCD module works.

The LCD has a cursor that points to a specific coordinate at any given time. After initializing, the cursor is moved to (1,1) using the following code
lcd_gotoxy(1,1);
Coordinate (1,1) is upper left corner of the screen. After initialization, the MCU will scan for any key press using
k = kbd_getc();
This function is defined in the flex_keypad_AE.c file. It is repeated in a while loop. When a key press is detected, the MCU will then check which key is pressed. Any key will just print out the corresponding number or symbol, except the "*" key and the "=" key. When "*" key is detected, it will initiate a clear screen function. There is nothing to talk about.

The real "magic" happens when "=" key is pressed. When detected, the MCU will start a chain of instructions.

The first step is (obviously) printing the "=" character, and the cursor is reset back to (1,1) via variables definition
posX = 1;
posY
= 1;
Another variable called index is also reset to 0. I will explain what this variable is later.

After the cursor reset, the LCD starts to read the numbers on the screen in a do-while loop. What this means is the LCD will keep reading as long as a condition is satisfied. In this case, the condition is a check of a flag called calcF (calculation finished). This flag checks whether the calculation is done. Hence, as long as the calculation is not done, the LCD will keep reading.

Typically, numbers are separated by an operator, for example
24+275=
Reading from the left, if the LCD detected any numbers, it will keep scanning to the right. At some point the cursor will point to the operator (if there is one). Note the underline in the statement below.
24+275=
The LCD will send the current cursor coordinate to the MCU to record the operator position in a variable called opPos. This coordinate will serve as the separation point between the two arguments. After that, the LCD will keep scanning to the right again, until it detected the "=" character. At this point, we can safely say that the cursor has reached the end of the mathematical statement and the argument separation process can be started.

NB: Weight (Index)

Roman numeric system has a very odd feature. In words and sentences, the alphabets are arranged from left to right; but in numbers, the weight of numbers increases from right to left. This simply means the number "4" in 540 and 1400 carries different weight (10 and 100 respectively). For this reason and simplicity, the argument is read from right to left. It is easier to understand if you look at the following calculations, using the statement above as example here:
275 = 5 + (7 x 10) + (2 x 100)

Determining the Second Arguments

Starting from the "=" sign, the cursor moves left. The weight is increased by a multiplication of 10, as shown by the statement
index++;
But there is a problem: the LCD returns any data it reads in char type. In programming, it is impossible to perform arithmetic using this type. Therefore the data is typecasted into int type and stored in a buffer.
bufBc = lcd_getc(posX,posY); bufB = (int)bufBc;
There is also another problem here: char 4 does not correspond to int 4. The LCD modules uses ASCII table to translate numbers into alphabets, numbers, symbols, etc.
Click to enlarge in a new window
Click to enlarge in a separate window
Luckily, or rather, intentionally, the ASCII table is designed in such a way that the numbers are shifted by hex 30. For example, in order to display the number "4", the MCU will send a hexadecimal 34 to the LCD.  Therefore to store the actual number, a hex 30 is subtracted from the data before multiplying the weight (in the power of 10s).
bufB = ( (bufB-0x30) * pow(10,index-1) ) + 0.1;
The 0.1 at the end of the statement is required due to the way computers perform logarithmic calculations. At the moment, I cannot remember where I got that information, but rest assure that without it, the result will be very slightly different from the actual number (i,e, 0.9999999...).

The result of bufB is stored in a variable called B, which, technically, means second argument.
B += bufB
This is done in a cumulative way, i.e. 5 + 70 + 200 + ... , as shown in the example in NB above. All the steps above are repeated in a loop until the cursor reaches the operator coordinate opPos.
while( posX > opPos )
Once the cursor reaches the operator (if there is one), we can safely say that the second argument is completely captured.

To Operate or Not to Operate?

Now you may be asking, "What happens if there is no operator? What if the statement is just '275='? "

It may sound weird, but it does not really matter whether there is an operator. Right after the second argument capture, the final result is immediately set to the second argument.
result = B
The next step is to check whether the cursor is at position 0.
if ( posX != 0 )
If it is, then it means there is no operation and the loop closes. The final result is simply the second argument.
If it is not, then it means there is an operator and another argument, hence the loop continues.

Now that the cursor is at the operator position, it is the best time to determine the operator type. This is simply done by storing the char data read by the LCD at this point.
operator = lcd_getc(posX,posY);
After that, the program continues to capture the first argument in a similar fashion to the second argument capturing process.
bufAc = lcd_getc(posX,posY);
bufA = (int)bufAc;
bufA = ( (bufA-0x30) * pow(10,index-1) ) + 0.1;
A += bufA;
All the steps above are repeated in a loop until the cursor reaches the origin.
while( posX >= 1 )

Calculate!

Now that the program has captured the first argument, second argument, and operator type, it has enough information to perform the actual calculation. Remember that, in the previous step, we stored the operator type in a variable? This is the time to put that information to use. By determining the operator type, the corresponding arithmetic calculation can be performed.

Addition is simply A + B : result = A + B;
Subtraction is simply A - B : result = A - B; (remember, the order of the arguments is important! A - B does not equal to B - A)
Multiplication is simply A x B : result = A * B;

In the case of division, additional steps are required. All the other operations are done using integers, i.e. numbers without decimal points; but in division, decimal is often required. Hence, to correctly perform division, the arguments need to be typecasted into floating points.
resultF = (float)A / (float)B;
After the calculation is done, the final step is to print the result. Note that there are two different variables for result: result and resultF. This difference is important to change the result display format via a variable called divFlag (division flag). It is often not necessary to print the result in decimal format if only integers are involved, such as in addition, subtraction, and multiplication.

Once the difference is determined, the final result can be printed, conveniently at the second line of the LCD.
lcd_gotoxy(1,2);
printf(lcd_putc, "%s", string);
Also, the calcF flag is checked to indicate that the calculation is finished, thus ending the main loop. Before ending, all the variables are reset to initial values to prepare for the next calculation.

Any key press after the loop ends will initiate a clear screen, providing an empty screen for the next calculation.

Conclusion

While this program may look simple, it was one of the proudest moment in my short life as an embedded system programmer. I am not an expert in this, that is why completing this task is considered a milestone for me. As shown in the YouTube video description, this project was actually a cancelled assignment when I was studying in a university back in 2010. This calculator only supports 2 arguments and basic arithmetic operations. Complex operations such as trigonometry and logarithm are being considered.

If you are still reading at this point, I sincerely thank you and hope you enjoyed reading the working principle behind my simple calculator. If you have any feedback, question, or suggestion, please do not hesitate to leave a comment.

Happy programming!

7 comments:

  1. Is it possible with this circuit 7 segment display?

    ReplyDelete
    Replies
    1. Hi,

      Yes it is possible to do it with 7 segment displays, but it will be quite complicated.

      You will need to remove everything that is related to LCD in the code (eg lcd_putc, lcd_gotoxy, etc) and replace them with corresponding pins that drive the 7 segments (eg pin A0 to A6 for first digit, B0 to B6 for second, etc).

      I wouldn't advise you to use 7 segments unless you have a driver chip, because in order to do that, you would need at least 14 I/O pins to drive only 2 displays.

      So, yes, it is possible to use 7 segments if you're fine with the complication that might arise. :)

      Delete
  2. Hi, I'm trying out your code with proteus but It doesn't work and I don't know why. Whatever I type in, the result is wrong. For example 1+1 = 1 or 7-7 =-7. If you could help me I would be very grateful. Thank You

    ReplyDelete
    Replies
    1. There could be a lot of reasons for this. Base on what you have written, I am assuming the program only recognises the second argument plus the sign. Check the code to see what happens after getting the second argument. The program should check if cursor position is not 0, i.e. "posX != 0". If you can debug it, look at what happens to the posX variable step-by-step.

      Delete
    2. can you give me code this project??

      Delete
  3. can u send me the code? thank u in advance!!

    ReplyDelete
  4. Please can I get the code and schematics for this? I'd love to try it out.

    ReplyDelete