Head-Fi.org › Forums › Misc.-Category Forums › DIY (Do-It-Yourself) Discussions › PIC design tutorial
New Posts  All Forums:Forum Nav:

PIC design tutorial - Page 2

post #16 of 30
Thread Starter 

Assembly language, assemblers and compilers.

 
While, as I have said, it is possible to look up opcodes in a table and program a computer by entering them as numbers, this is a tedious process.
 
In order to lessen the effort, a system was devised where each opcode was assigned an easily remembered alias, called a mnemonic.
 
It then became possible to write a program as a series of lines of text (often stored in a PC 'file'), each line containing a mnemonic, and pass this 'assembly language' to a program called an assembler which reads the text line by line, recognizes the mnemonics and assembles a list of numbers which can be entered into memory in a machine controlled write process.
 
A typical PIC mnemonic is MOVLW. This means, move a literal to the w register. A literal is a number written in a program as opposed to MOVFW, which means, move the number contained in a file to the w register. 
 
These assembly language mnemonics would be used like this:-
 
    MOVLW, 3
 
... move (the literal) 3 to the w register
 
    MOVFW, XYZ
 
... move the contents of the file (RAM register) previously assigned the name XYZ to the w register. Since the value in the file register can vary between 0 and 255, it is called a variable, and this term is used to refer both to the number and to the name 'XYZ' given to it.
 
The use of spaces and other punctuation is critical in programming, and failure to follow the strict rules down to the last space, comma or semicolon will cause the assembler or compiler to throw an error.
 
The w register is the principal register in a PIC, where numbers are moved when it is intended to perform operations on them, such as addition or subtraction.
 
Microchip PICs are RISC processors. This is an acronym for Reduced Instruction Set (Computing).
 
As processors developed from 8-bit to 16-bit to 32-bit, the range of available instructions proliferated. This had its advantages in that programming could be efficient and programs compact, but it made learning to program difficult and increased the complexity of designing assemblers and other high-level language compilers, so a movement started to produce chips with more manageable numbers of opcodes. This means that there are comparatively few mnemonics to learn when learning to write assembly language for Microchip PIC.
 
One of the advantages of RISC processors is that instructions are quick to execute in terms of clock cycles. All PIC instructions except jumps take 4 clock cycles to execute as the processor repeatedly performs the steps; fetch, decode, store, execute. This 4-step cycle is called a machine cycle. Hence a PIC with a 4MHz clock executes one complete instruction every microsecond. A jump is where the processor fetches a number from memory and loads it into the program counter, forcing program execution to resume from a different address than the next one in sequence. The 8-bit PICs actually have 14-bit-wide program memory, enabling opcode and operand to be fetched simultaneously, but this is a fairly trivial detail, other than that it means that most instructions can be made to execute in 4 clock cycles.
 
While writing assembly language is considerably easier than opcode programming, further levels of abstraction are possible. Typically a compiler reads a text file containing a high-level language such as BASIC or C, and outputs a text file consisting of assembly language. This assembly language file is then assembled to produce a hex file which is used to program a device. There are in fact intermediate steps and intermediate files produced, depending on the exact implementation, but a compiler could be devised which produced a file which could be used directly to program a chip, and these intermediate details are of little concern to us in the context of this tutorial.
 

PIC 16F690 family Instruction Set:-

 

1000

 

Exact details of how to interpret this table can be found in the datasheet.

 

w


Edited by wakibaki - 12/14/12 at 6:55pm
post #17 of 30
Thread Starter 

Here's a simple TH layout.

 

700

 

This file has the gerbers:

 

PIC_voltmeter_th - CADCAM.ZIP 11k .ZIP file

 

Or for SMT, I'd modify the schematic a bit to make it easier to route:

 

700

 

Giving us this:

 

 

700

 

... which would be more convenient if, for example, you wanted to panel mount it, as the side facing has only the display and a few SMT res. on it. The display would need to be surface mounted, but this is not a big problem as it has long legs, I've used them like this.

 

I've retained a few thru-hole components, it's quite common to do this.

 

 

PIC_voltmeter_SMT - CADCAM.ZIP 8k .ZIP file

 

 

Or you can simply build it on a breadboard for purposes of the tutorial.

w


Edited by wakibaki - 12/17/12 at 2:33pm
post #18 of 30
Thread Starter 

How to read a datasheet.

 
Read the stuff in the large font first, then work your way down to the smallest font.
 
No, seriously.
 
You may think, 'What's this 4sshole on about? Telling us how to read.'
 
I didn't get where I am today, however, without learning what I can safely ignore.
 
It's common in engineering offices, and occasionally elsewhere, to hear muttering. '...Mutter, mutter, mutter, mutter   ...read the *ucking datasheet...'
 
You're going to be obliged to read ALL of it at some time or another, bearing in mind that the totality of detail of every chip in the range is going to prove redundant. Datasheets are an acquired taste. As one gets older one's appetites become more perverse. 
 
There is a lot of stuff in datasheets, almost inevitably there will be some you do not understand, some may be presented in an unfamiliar or difficult to interpret format, and this can be very intimidating and discouraging, but you just have to learn to move on to the next island of incomprehension. Often a subsequent part of the datasheet will provide a clue to the meaning of some preceding item, and your comprehension expands with time anyway.
 
Do not imagine, however, that the engineer(s) who wrote the datasheet may not have intended to confuse or mislead you, notwithstanding that they are constrained from outright lying by the threat of dismissal from their professional bodies, and that (in many instances most influentially) they bear considerable commercial liability, and ultimately have, in some jurisdictions, greater expectations imposed on them than the ordinary citizen in terms of the criminal law, or that in most instances they can actually be expected to behave in a reasonably honest fashion; but they are as fallible and prone to hubris and venality as the generality of the population.
 
So ultimately, don't trust anything until you've tested it, certainly not if there's a question of risk. We can send a man to the moon and bring him back alive, so surely we can trust software to run a nuclear power station...
 
Anyway, I digress.
 
Debugging. 
 
It's never a fault in the chip, except when it is, which it almost never is unless you blew it up.
 
Digital design only becomes esoteric when it starts to do something analogue. All the rest is a foregone conclusion. You just expect it to do what you want.
 
Let's focus on the analogue to digital converter in the 16F690.
 
We can readily find the relevant section in the datasheet.
 
We are going to use the A/D to get voltage data into the system. There is a section of code embedded in the datasheet. 
 
You can cut and paste the code directly from adobe reader into a text editor.
 
It's best to learn to use a programmer's text editor. PFE32 is good. We are also going to need Microchip's MPLab software installed on the computer to understand the assembly steps, it will run without any hardware attached, and it's a free download. We're also going to want the C compiler later on. Although the IDE (Integrated Development Environment) includes a text editor, there are many such text editors with slight variations in functionality, a familiar text editor with known performance has it's advantages.
 
;This code block configures the ADC
;for polling, Vdd reference, Frc clock
;and AN0 input.

;Conversion start & polling for completion
;are included.

BANKSEL ADCON1 
MOVLW B’01110000’ ;ADC Frc clock
MOVWF ADCON1
BANKSEL TRISA
BSF TRISA,0       ;Set RA0 to input
BANKSEL ANSEL
BSF ANSEL,0       ;Set RA0 to analog
BANKSEL ADCON0
MOVLW B’10000001’ ;Right justify,
MOVWF ADCON0      ;Vdd Vref, AN0, On
CALL SampleTime   ;Acquisiton delay
BSF ADCON0,GO     ;Start conversion
BTFSC ADCON0,GO   ;Is conversion done?
GOTO $-1          ;No, test again
BANKSEL ADRESH
MOVF ADRESH,W     ;Read upper 2 bits
MOVWF RESULTHI    ;store in GPR space
BANKSEL ADRESL
MOVF ADRESL,W     ;Read lower 8 bits
MOVWF RESULTLO    ;Store in GPR space
 
We're going to execute this code (with some modifications), continuously, in a loop, simply by giving a line at the beginning a label and writing a goto instruction referencing that label at the end of the program. Before issuing the loop instructions the program will render the 10-bit data from the A/D first into 3-digit BCD, and thence into 7-segment representation, active low configuration, leaving the updated value in a bank of 3 buffers where it is available for the ISR to paste on the display.
 
Before we can use this device we need to acknowledge some fundamental programming ideas.
 
The execution loop.
 
Almost every piece of firmware is intended to cause a chain of events taking place in an iterative loop. You can design something that starts up, does something, and then stops, quite easily, but this is not usually the objective.biggrin.gif
 
We're going to use assembly language to devise a control program to execute a limited range of objectives to meet the design criteria in as economic a fashion as possible, while identifying and modifying what are considered to be a reasonable set of design objectives, in an engineering compromise. Never proceed beyond a feasibility study without a clearly defined requirements specification agreed, even if only with yourself.
 
The program will get the voltage at the A/D input, waiting as long as the A/D may require to perform this sampling operation and process it down into appropriate digital memory-maps to drive the individual digits of the three, seven-segment LED outputs to display a value of 0-999, according to the assignment of the segments to the pins of the PIC port assigned for outputting this information. ICBS now that the choice to assign the bits controlling the segments to a single byte-wide port contributes to an efficient, comprehensible and easy to maintain code.
 
We are also going to drive the display using a timer-driven interrupt. This will look at the buffered digital values that represent the A/D readings and illuminate the segments sequentially and repeatedly, so fast as to give the impression that each of the digits is continuously illuminated, by exploiting the well-known phenomenon of persistence of vision.
 
The A/D has a precision of 10 bits. The values that can in theory be represented are hence 0-1023, but 0-999 is an acceptably economic resolution to obtain given the economies of effort obtained by imposing this constraint upon the design objective.
 
So one of the design objectives has hence been determined, to obtain an economic exploitation of the available precision of the 16F690 in terms of a visible digital display of 1000 levels encoded as multiplexed 7-segment display data on a 3 x 8-bit matrix.
 
So now we can see the objectives for the interrupt service routine.
 
The interrupt service routine will put the value of the appropriate digit on the PIC output port for each of the values, 0-9, activating the digit in the multiplexed display by driving the base of the appropriate PNP transistor LOW, and rotate the digits; hundreds, tens and units, maintaining a register for the synchronisation of active digit and displayed digit.
 
This desynchronizes the timing of the display updates from the execution of the A/D read/decode cycle, and helps guarantee the minimum processor time is devoted to each task, ensuring the best rate of update and minimum display flicker achievable.
 
This will become a bit clearer as we consider in detail what is taking place in practice with regard to the 7-segment display, and why it is being done. Perhaps as important, we will consider the limitations of what has been done in respect of the extensibility of this approach in terms of the comparative laxity of the timing constraints with regard to the display.
 
First however, we need to examine some of the basic precursors for an assembly language program. 
 
That seems to be an appropriate place to pause.
 
w

Edited by wakibaki - 12/21/12 at 7:44pm
post #19 of 30

This is all really good stuff so far.

I intend to dive right in in the new year.

 

Thank you again!

post #20 of 30
Thread Starter 

So.

 
We could have 4 digits and display values 0-1023 because the PIC is capable of resolving 1024 steps, but it makes things quite a lot simpler and cheaper if we just go for 3 digits. The fourth digit wouldn't contribute much, it would just be blank (or zero, 0) and on rare occasions show 1 when the value was 1000 - 1023. We'd have to blank the display etc. etc.,   ...the 4th. digit is just a lot of work for not very much result  
 
3-digit multiplexed displays are widely available. People have been doing this kind of stuff for quite a while, long enough for 3-digit displays to have been produced as a single-chip module with shared pins for the segments and individual pins to activate the digits, because it is known that if you switch digits fast enough your eye and brain are deceived into the impression that all 3 digits are continuously illuminated. This means that we can use 8 + 3 = 11 pins on the uProcessor instead of 3 * 8 = 24 pins. That's what we're going to do in this instance, because it makes a good introductory exercise to microprocessor implementations, and because this is what is commonly done commercially.
 
By manipulating the the decimal point and resistive divider at the input, we can display voltages from 0 - 0.999 volts to +/- 0.0005V (in theory) or 0 - 999 volts to +/- 0.5V.
 
Microcontroller special functions.
 
In addition to the microprocessor, memory and ports the PIC has a variety of special functions built in.
 
Many of the PIC pins can have functions assigned to them, some of which are under program control at run time and some of which can be assigned but are not under run-time control.
 
Input/output ports are assigned under run-time program control. This is to say, your program can start up and use a pin, or bank of pins, to output digital values, and later switch to using the same pins as inputs, to read in digital values.
 
The PIC also has a built-in multifunction clock module. This is controlled by a number of bits in configuration registers. Some PICs have 1 configuration word, some have 2. These words are not normally accessed programmatically during run-time, but they are set by programming tools at the time of programming according to instructions included in the assembly language program. The modes of operation include using external timing control components such as crystals or ceramic resonators in combination with an internal amplifier to create an accurate oscillator or using an internal RC network, which saves external components and pins, but is less critically accurate. Once a commitment to an external hardware configuration has been made there would be little point in reassigning the pins involved.
 
Included in the configuration-register assigned functions are a brown-out reset (BOR), a hardware watchdog timer (WDT) and numerous other functions. While the entire list can be discovered by reading the datasheet, a simpler, and in many ways more useful way of discovering the available functions is to examine the header (include) file for the chip in question. This file is installed when you download and install the MPLab software which is a free download from Microchip, the installer puts it in a location which will be known to the assembler when it requires it.
 
When we start to write a text file (.asm) to present to the assembler, commonly the first line will specify an 'include' file defining exactly which chip the design is intended for. This will be immediately followed by a line specifying the settings of the configuration bits. This will typically look like this:-
 
 
#include <p16F887.inc>


__CONFIG    _CONFIG1,  _IESO_OFF & _BOR_OFF & _CPD_OFF & _CP_OFF & _MCLRE_OFF & _PWRTE_ON & _WDT_OFF & _INTRC_OSC_NOCLKOUT


__CONFIG    _CONFIG2, _WRT_OFF & _BOR21V
 
 
The # sign simply tells the assembler to look out for an 'include' or other directive following immediately after. Remember though that the assembler is incredibly fussy about such little details. These lines are taken from a program for 16F887, I haven't written the one for the 16F690 yet.
 
There's quite a lot to be learned from these few lines.
 
Firstly the #include <> directive tells the assembler to read the file 'p16F887.inc' and insert its contents wholesale into the text it is processing.
 
That file is a section of text, itself written in assembly language, which defines the values of the mnemonics you can see in the following two lines, such as _BOR_OFF, _MCLRE_OFF etc.
 
Underscore, underscore, CONFIG (__CONFIG) says that there is a config word coming.  
 
Underscore, CONFIG1 (_CONFIG1) says that the word in question is config word 1.
 
The comma ',' says the value of the word follows now...
 
and the rest - _IESO_OFF & _BOR_OFF & _CPD_OFF etc., etc., defines the numerical value of the config word. How does it do this?
 
Let's look at the contents of 'p16F690.inc' in the next post.
 
w

Edited by wakibaki - 12/21/12 at 5:28pm
post #21 of 30
Thread Starter 

If we search the Microchip directory tree (under Program Files) for p16F690.inc, we will soon discover the file in \MPASM Suite (depending on exact version). We can just click on the file, which should cause it to open in Notepad, or drop it into any text editor of our choice.

 
Searching through the file for 'CONFIG' will soon reveal the following:-
 
;==========================================================================


; The following is an assignment of address values for all of the
; configuration registers for the purpose of table reads

_CONFIG          EQU  H'2007'


;----- CONFIG Options --------------------------------------------------

_FOSC_LP             EQU  H'3FF8'    ; LP oscillator: Low-power crystal on RA4/OSC2/CLKOUT and RA5/OSC1/CLKIN
_LP_OSC              EQU  H'3FF8'    ; LP oscillator: Low-power crystal on RA4/OSC2/CLKOUT and RA5/OSC1/CLKIN
_FOSC_XT             EQU  H'3FF9'    ; XT oscillator: Crystal/resonator on RA4/OSC2/CLKOUT and RA5/OSC1/CLKIN
_XT_OSC              EQU  H'3FF9'    ; XT oscillator: Crystal/resonator on RA4/OSC2/CLKOUT and RA5/OSC1/CLKIN
_FOSC_HS             EQU  H'3FFA'    ; HS oscillator: High-speed crystal/resonator on RA4/OSC2/CLKOUT and RA5/OSC1/CLKIN
_HS_OSC              EQU  H'3FFA'    ; HS oscillator: High-speed crystal/resonator on RA4/OSC2/CLKOUT and RA5/OSC1/CLKIN
_FOSC_EC             EQU  H'3FFB'    ; EC: I/O function on RA4/OSC2/CLKOUT pin, CLKIN on RA5/OSC1/CLKIN
_EC_OSC              EQU  H'3FFB'    ; EC: I/O function on RA4/OSC2/CLKOUT pin, CLKIN on RA5/OSC1/CLKIN
_FOSC_INTRCIO        EQU  H'3FFC'    ; INTOSCIO oscillator: I/O function on RA4/OSC2/CLKOUT pin, I/O function on RA5/OSC1/CLKIN
_INTRC_OSC_NOCLKOUT  EQU  H'3FFC'    ; INTOSCIO oscillator: I/O function on RA4/OSC2/CLKOUT pin, I/O function on RA5/OSC1/CLKIN
_INTOSCIO            EQU  H'3FFC'    ; INTOSCIO oscillator: I/O function on RA4/OSC2/CLKOUT pin, I/O function on RA5/OSC1/CLKIN
_FOSC_INTRCCLK       EQU  H'3FFD'    ; INTOSC oscillator: CLKOUT function on RA4/OSC2/CLKOUT pin, I/O function on RA5/OSC1/CLKIN
_INTRC_OSC_CLKOUT    EQU  H'3FFD'    ; INTOSC oscillator: CLKOUT function on RA4/OSC2/CLKOUT pin, I/O function on RA5/OSC1/CLKIN
_INTOSC              EQU  H'3FFD'    ; INTOSC oscillator: CLKOUT function on RA4/OSC2/CLKOUT pin, I/O function on RA5/OSC1/CLKIN
_FOSC_EXTRCIO        EQU  H'3FFE'    ; RCIO oscillator: I/O function on RA4/OSC2/CLKOUT pin, RC on RA5/OSC1/CLKIN
_EXTRC_OSC_NOCLKOUT  EQU  H'3FFE'    ; RCIO oscillator: I/O function on RA4/OSC2/CLKOUT pin, RC on RA5/OSC1/CLKIN
_EXTRCIO             EQU  H'3FFE'    ; RCIO oscillator: I/O function on RA4/OSC2/CLKOUT pin, RC on RA5/OSC1/CLKIN
_FOSC_EXTRCCLK       EQU  H'3FFF'    ; RC oscillator: CLKOUT function on RA4/OSC2/CLKOUT pin, RC on RA5/OSC1/CLKIN
_EXTRC_OSC_CLKOUT    EQU  H'3FFF'    ; RC oscillator: CLKOUT function on RA4/OSC2/CLKOUT pin, RC on RA5/OSC1/CLKIN
_EXTRC               EQU  H'3FFF'    ; RC oscillator: CLKOUT function on RA4/OSC2/CLKOUT pin, RC on RA5/OSC1/CLKIN

_WDTE_OFF            EQU  H'3FF7'    ; WDT disabled and can be enabled by SWDTEN bit of the WDTCON register
_WDT_OFF             EQU  H'3FF7'    ; WDT disabled and can be enabled by SWDTEN bit of the WDTCON register
_WDTE_ON             EQU  H'3FFF'    ; WDT enabled

_WDT_ON              EQU  H'3FFF'    ; WDT enabled

_PWRTE_ON            EQU  H'3FEF'    ; PWRT enabled
_PWRTE_OFF           EQU  H'3FFF'    ; PWRT disabled

_MCLRE_OFF           EQU  H'3FDF'    ; MCLR pin function is digital input, MCLR internally tied to VDD
_MCLRE_ON            EQU  H'3FFF'    ; MCLR pin function is MCLR

_CP_ON               EQU  H'3FBF'    ; Program memory code protection is enabled
_CP_OFF              EQU  H'3FFF'    ; Program memory code protection is disabled

_CPD_ON              EQU  H'3F7F'    ; Data memory code protection is enabled
_CPD_OFF             EQU  H'3FFF'    ; Data memory code protection is disabled

_BOREN_OFF           EQU  H'3CFF'    ; BOR disabled
_BOD_OFF             EQU  H'3CFF'    ; BOR disabled
_BOR_OFF             EQU  H'3CFF'    ; BOR disabled
_BOREN_SBODEN        EQU  H'3DFF'    ; BOR controlled by SBOREN bit of the PCON register
_BOD_SBODEN          EQU  H'3DFF'    ; BOR controlled by SBOREN bit of the PCON register
_BOR_SBODEN          EQU  H'3DFF'    ; BOR controlled by SBOREN bit of the PCON register
_BOREN_NSLEEP        EQU  H'3EFF'    ; BOR enabled during operation and disabled in Sleep
_BOD_NSLEEP          EQU  H'3EFF'    ; BOR enabled during operation and disabled in Sleep
_BOR_NSLEEP          EQU  H'3EFF'    ; BOR enabled during operation and disabled in Sleep
_BOREN_ON            EQU  H'3FFF'    ; BOR enabled
_BOD_ON              EQU  H'3FFF'    ; BOR enabled
_BOR_ON              EQU  H'3FFF'    ; BOR enabled

_IESO_OFF            EQU  H'3BFF'    ; Internal External Switchover mode is disabled
_IESO_ON             EQU  H'3FFF'    ; Internal External Switchover mode is enabled

_FCMEN_OFF           EQU  H'37FF'    ; Fail-Safe Clock Monitor is disabled
_FCMEN_ON            EQU  H'3FFF'    ; Fail-Safe Clock Monitor is enabled
 
You will notice immediately that;
 
_CONFIG          EQU  H'2007'
 
this is called an equ or equate. It's a way of assigning a number to a mnemonic in assembly language.
 
In this case it it is telling the assembler that when it sees _CONFIG, it can stuff in "H'2007'"
 
H'xxxx' means that the number is in hexadecimal. If it were B'xxxxxxxx', that would mean a binary number. The assembler (in the case of MPASM) defaults to decimal, if you don't explicitly tell it otherwise it will treat any number as decimal.
 
Note that H'2007' in this case refers to a memory address, but one which is inaccessible to a normal program. As stated earlier, the values in this address can only be set by the assembler at program time.
 
Also notice the helpful explanations in most lines following the semicolon (;). Anything after the semicolon is ignored by the assembler until the next newline. These are called remarks, and you find the same thing in most programs, in BASIC the semicolon is replaced by 'REM', in C the remarks are enclosed like this:-
 
/* This is a C remark */
 
Putting explanatory remarks in your program is part of what is called documentation.
 
Following the _CONFIG equate, we see an number of other equates. These are expressed in hex, but they are designed to control bits in the register. They are carefully arranged so that they can be combined into a single byte with which to configure multiple options simultaneously.
 
The operator '&' performs a bitwise AND of 2 (bytewide) values presented to it.
 
Hence a line like this:-
 
__CONFIG    _CONFIG1,  _IESO_OFF & _BOR_OFF & _CPD_OFF & _CP_OFF & _MCLRE_OFF & _PWRTE_ON & _WDT_OFF & _INTRC_OSC_NOCLKOUT
 
...combines the settings _IESO_OFF, _BOR_OFF, _CPD_OFF, _CP_OFF, _MCLRE_OFF, _PWRTE_ON, _WDT_OFF, _INTRC_OSC_NOCLKOUT into a single byte, which gets loaded into CONFIG word 1.
 
If you don't understand the idea of 'bitwise AND', that's not too important at this stage, what's important is to understand that we can set up the PIC to perform in the way we want by following the template and writing the mnemonics for the conditions we want into a line similar to the example. So including '& _WDT_ON' in the line will enable the watchdog timer and including '& _WDT_OFF' will disable the watchdog timer.
 
Bitwise AND means that if bit 7 of one input byte is '1' and bit 7 of the other input byte is '1' then bit 7 of the output byte is set to '1', but if either input bit is '0', then the output bit gets '0', and so on, down through bits 6,5... to bit 0.
 
So the file p16F690.inc can tell us some useful stuff about the available options in the PIC 16F690, without the necessity to read the datasheet. It also provides us with a lot of mnemonics for various other features of the 16F690, which we can use instead of numeric values to make our program more readable (comprehensible), which can be important when you come back to it in a year or so's time, and you can vaguely remember writing it, but you haven't got the faintest idea how it works.  
 
w

Edited by wakibaki - 12/21/12 at 7:53pm
post #22 of 30
Thread Starter 

Bytes, data types and ASCII

 

Before we move on to writing a program, there is another topic that I want to touch on.

 

This is related to the format for data, the byte, 8 bits.

 

The byte is very important as a fundamental unit for storage and processing, and an appreciation of why this is the case is important in programming of all kinds, so although we are not going to use the ASCII code here, I think it is worth a mention, because I'm trying to give an introduction to programming here that will have wider application than just in PIC micros.

 

I touched on assemblers, compilers and text files earlier. These are the tools that make it possible for us to program a computer or control system using a computer, rather than using paper tables of instructions and a pen and paper.

 

In order to store text in a computer memory it is necessary to turn the alphabet into numbers. We need to do this in a standard way if information is to be transferred from one system to another and still remain comprehensible.

 

The basic (worldwide) system for doing this is called the American Standard Code for Information Interchange, ASCII for short.

 

There are several more complex ways of encoding text, to include the large number of characters, accents, Cyrillic alphabet, ideograms (Japanese, Chinese), Hebrew, Farsi and other written forms of communication, but the fundamental basis for computer text is the ASCII code.

 

It takes the 256 values available in a byte and assigns the digits, 0-9 and the alphabet A-Z and a-z (called alphanumerics) an individual number.

 

You can see here A is 65, B is 66 and so on. Don't ask me why these particular numbers were chosen to represent these particular letters, I don't know, but these are used, almost without exception, in all the computers all over the world, other than when data is encrypted.

 

The 256 values available in a byte mean that it is possible to encode any number, all the upper and lower case alphabet and quite a few punctuation marks and other formatting commands such as return and newline, which date back to the days of teletype (teleprinter, TTY) machines most of which used a 5-bit code called BAUDOT.

 

In fact the code shown below only requires 128 values, but you can look up the extended ASCII code, which includes graphics primitives which enable the drawing of onscreen boxes and other useful features which will be familiar to anybody who used the previous generation of IBM PCs which ran DOS, the predecessor of Windows.

 

1000

 

Hence when talking about bytes we sometimes call them characters and compilers call character variables char.

 

A one dimensional array of char is called a string.

 

A variable declaration for a chunk of memory (called mystring) to hold 30 characters might be declared like this

 

unsigned char[31] mystring;

 

31 bytes are used because, by convention, character strings in memory are terminated with a zero byte (B'00000000'), so that the system knows where the string ends.

 

unsigned char, because char variables can still be used to store numbers. We can reuse these variables for multiple purposes as we choose if memory is short, but where memory is not severely limited, we normally assign variables to a single purpose, as then we don't have to do as much work keeping track of what use the memory is being put to at any given time.

 

signed and unsigned, when used to refer to variables, denotes whether they are used to contain numbers with a positive or negative value (signed) or a value alone (unsigned).

 

When a variable is used to store signed values, the MSB (Most Significant Bit) is given over to indicating positive (zero, 0) or negative (one, 1). Hence a signed char can contain numeric values between -128 and +127.

 

An integer is normally a 16-bit (2-byte) value, and a long integer is a 32-bit value, although these definitions are flexible to a degree, depending on the system in question. Values with a decimal point are called float (floating point) and have special compiler libraries or header files (math.h in C) to deal with processing them which take the burden of manipulating these values off the programmer, and may make use of a hardware maths co-processor.

 

w


Edited by wakibaki - 1/1/13 at 11:07am
post #23 of 30

Good stuff Fred; a clear and concise cover of the topic. Since this is my 'wheelhouse' so to speak, I'm anticipating the application. Hope you had a good New Years and again - thanks for your time and efforts!

post #24 of 30
Thread Starter 

Back again.

 

I said I would show a couple of applications using 7-segment displays, and I will, but in the meantime I needed to develop something for myself, so I'm going to show it first.

 

A friend of mine recently suggested using a stepper motor to drive a pot or stepped attenuator for a remote, powered volume control.

 

As it happened I had just learned a new technique for PCB layout.

 

Some components are circular, such as tubes. Previously, when developing footprints (pad layouts) for circular components, I placed the pads on a grid to within a thousandth of an inch using trigonometry to calculate the positions. The layout tool draws circles no problem, so the component outlines were no problem.

 

Recently I started to use the layout tool to rotate components and tracks as a block. The menu feature which permits this is different from the rotate facility for standalone components which works in 90 degree increments. The block rotate facility works in degrees and fractions of a degree. Thus it's possible to use it to make circular component footprints by placing one pad, rotating the drawing and placing another pad, and so on...

 

So making circular layouts of all kinds has become a lot simpler for me. You can use a tool for years without discovering all its features.

 

The next obvious thing to do was to make a circular layout with SMT resistors, make a custom mechanism (a wiper) to fit a stepper motor, and bolt the stepper + wiper directly to the PCB. Hey presto, a custom stepped attenuator.

 

So I looked on ebay, and quickly found these, stepper motors + gearboxes:-

 

 

 

You can see my test logic-level board with 16F690 and a 5V regulator. It has 3 sets of header pins. One to connect a PicKit2 programmer, one 4-output header to drive the motor coils A, B, C and D and a 2-pin carrying +5V and GND. Each stepper came with a driver board with a ULN2003 transistor array chip, with indicator LEDs for the stepper coils and header pins for connections..

 

The driver board means that the motor can be driven using logic levels if DC power is supplied to the driver board, in this case 5-12V DC.

 

A stepper motor has a rotor which resembles a toothed gearwheel. The coils are arranged so that the rotor moves a fraction of a whole rotation when one coil after another is driven. 200 steps per revolution is common. To get half steps sometimes adjacent coils are energised simultaneously: A, A+B, B, B+C, C, C+D, D, D+A, A, A+B and so on. This gives a smoother rotation. It's possible to drive a stepper motor with orthogonal sinewaves to get even smoother motion, but the using the cogged motion it's possible to reposition the motor accurately very successfully. These motors have a built-in gearbox, permitting even finer positioning.

 

A peculiarity of stepper motors is that they have a maximum speed of rotation when starting up, otherwise the magnetic fields can move ahead of the rotor motion, causing the motor to stall. Once the motor is started the stepping speed can be ramped up, if so desired.

 

Here's the ebay ad.

 

http://www.ebay.co.uk/itm/Two-Arduino-5V-4-Phase-Stepper-Motors-with-2-ULN2003-Control-Boards-Pair-/120904974870?pt=UK_Sound_Vision_Other&hash=item1c267f8216

 

...and the stepping sequence.

 

 

The first objective here is to determine a reasonable starting (stepping) speed using the PICs internal clock. To do that we create a simple program to output the stepping sequence on one of the PICs ports, PORTC in this case. We can determine the switching rate empirically by putting a delay in between steps and changing its duration.

 

That's probably enough for one post...

 

w

post #25 of 30
Thread Starter 

Lest anybody get impatient to see some code, here's the first, basic program in its entirety:-

 

 

list      p=16F690

#include <p16F690.inc>

__CONFIG    _FCMEN_OFF & _IESO_OFF & _BOR_OFF & _CPD_OFF & _CP_OFF & _MCLRE_OFF & _PWRTE_OFF & _WDT_OFF & _INTRC_OSC_NOCLKOUT

cblock h'20'

rflag
sflag
dflag

endc

        ORG     0x000

        bcf     STATUS,RP1
        bcf     STATUS,RP0     ; 00, BANK 0
        movlw   b'11111111' 
        movwf   PORTA          ; PORTA all ones
        movwf   PORTB          ; PORTB all ones
        clrf    PORTC          ; PORTC all zero
        bsf     STATUS,RP1
        bcf     STATUS,RP0     ; 00, BANK 2
        clrf    ANSELH         ; make PORTB 4,5 inputs digital
        bcf     STATUS,RP1
        bsf     STATUS,RP0     ; 01, BANK 1
        movwf   TRISB          ; PORTB inputs
        movwf   TRISA          ; PORTA inputs
        clrf    TRISC          ; PORTC outputs
        bcf     STATUS,RP1
        bcf     STATUS,RP0     ;00, BANK 0
        movlw   b'00000001' 
        movwf   rflag
        movwf   PORTC
        clrf    dflag  

mainloop:

        btfsc   PORTB,4
        goto    main_anti
        call    clockwize
        call    do_your_thing
        call    delay
        call    delay
        call    delay
        call    delay
        call    delay
        call    delay
        call    delay
        call    delay
        call    delay
        call    delay
        goto    mainloop

main_anti:

        btfsc   PORTB,5
        goto    mainloop
        call    aclockwize
        call    do_your_thing
        call    delay
        call    delay
        call    delay
        call    delay
        call    delay
        call    delay
        call    delay
        call    delay
        call    delay
        call    delay
        goto    mainloop

clockwize:

        rrf     rflag,f
        btfss   STATUS,C
        goto    clk
        bsf     rflag,7
clk:
        bcf     STATUS,C
        return

aclockwize:                    ;6

        rlf     rflag,f
        btfss   STATUS,C
        goto    aclk
        bsf     rflag,0
aclk:
        bcf     STATUS,C
        return

do_your_thing:

        movfw   rflag
        movwf   sflag
        rrf     sflag,f
        btfsc   STATUS,C
        goto    s0
        rrf     sflag,f
        btfsc   STATUS,C
        goto    s1
        rrf     sflag,f
        btfsc   STATUS,C
        goto    s2
        rrf     sflag,f
        btfsc   STATUS,C
        goto    s3
        rrf     sflag,f
        btfsc   STATUS,C
        goto    s4
        rrf     sflag,f
        btfsc   STATUS,C
        goto    s5
        rrf     sflag,f
        btfsc   STATUS,C
        goto    s6
        rrf     sflag,f
        btfsc   STATUS,C
        goto    s7
s0:
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        bcf     STATUS,C
        movlw   b'00000001'
        movwf   PORTC
        return
s1:
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        bcf     STATUS,C
        movlw   b'00000011'
        movwf   PORTC
        return
s2:
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        bcf     STATUS,C
        movlw   b'00000010'
        movwf   PORTC
        return
s3:
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        bcf     STATUS,C
        movlw   b'00000110'
        movwf   PORTC
        return
s4:
        nop
        nop
        nop
        nop
        nop
        nop
        bcf     STATUS,C
        movlw   b'00000100'
        movwf   PORTC
        return
s5:
        nop
        nop
        nop
        nop
        bcf     STATUS,C
        movlw   b'00001100'
        movwf   PORTC
        return
s6:
        nop
        nop
        bcf     STATUS,C
        movlw   b'00001000'
        movwf   PORTC
        return
s7:
        bcf     STATUS,C
        movlw   b'00001001'
        movwf   PORTC
        return

delay:  decf    dflag,f
        btfss   STATUS,Z
        goto    delay     
        return

END
 
You will see that there are a lot of NOPs in it, these are to maintain equal timing between steps, because the program takes a different length of time to get to different steps in the sequence, as we shall see.
 
Also note the use of white space. It's important to space out your programs to make them readable. They can very easily become unintelligible if you don't do this.
 
w
post #26 of 30
Thread Starter 

Here's the program reduced to a template for PIC 16F690:

 

 

list      p=16F690

#include <p16F690.inc>

__CONFIG    _FCMEN_OFF & _IESO_OFF & _BOR_OFF & _CPD_OFF & _CP_OFF & _MCLRE_OFF & _PWRTE_OFF & _WDT_OFF & _INTRC_OSC_NOCLKOUT

cblock h'20'

endc

        ORG     0x000

mainloop:

        goto    mainloop

END
 
These few lines are what we call the programming overhead. The bits that need to be in the program, but that aren't really part of the program itself. Not all these lines are essential in every program, but they're used in most programs.
 
list      p=16F690
 
This tells the assembler which PIC the program is targetted on. This is necessary in that the assembler needs to know which mnemonics to expect and which opcodes to use. It's also possible to inform the assembler of this by using command line switches, or by selecting the type in the MPLab IDE (Integrated Development Environment). The command line is where you run the assembler from a DOS box in Windows. We won't go into that... 
 
#include <p16F690.inc>
 
This tells the assembler to read the file p16F690.inc and treat it as a prefix to this file. p16F690.inc, as discussed before, contains the definitions that let the assembler interpret the mnemonics that follow, such as '_FCMEN_OFF'.
 
__CONFIG    _FCMEN_OFF & _IESO_OFF & _BOR_OFF & _CPD_OFF & _CP_OFF & _MCLRE_OFF & _PWRTE_OFF & _WDT_OFF & _INTRC_OSC_NOCLKOUT
 
This line instructs the assembler to embed the instructions for programming the configuration word(s) in the file (hex file) that is passed to the programmer. The programmer is a separate program to the assembler. It takes the assembler's output and sends it to the hardware programmer which manipulates the PIC chip's pins to program the internal (flash) memory. This is done behind your back in the IDE. You just click the widget or select 'Program' from the 'Programmer' menu.
 
I'll run through the whole operating procedure for the IDE later.
 
A word can be an 8-, 16-, 32- or 64-bit value, depending on the system in question.
 
PIC micros can have many useful built-in features, depending on the individual chip.
 
These include, for example, a fail-safe clock monitor, a watchdog timer, a brown-out reset, code protection and an internal RC clock generator. All these are set up at programming time by writing to the configuration word or words. These features are not accessible at any other time, you cannot control them from your program.
 
We can see all the features of the 16F690 accessible via the configuration word in this line. All are turned OFF, except the internal RC clock oscillator. The clock oscillator can be exported to other devices via a dedicated pin on the chip, but this feature is turned off.
 
cblock h'20'

var1...
var2...
var3...

endc
 
The cblock directive allows the definition of a block of contiguous constants. var1, var2, var3, in this case are the names of the constants, so var1 will be h'20', var2 will be h'21', var3 will be h'22'.
 
endc indicates that we are done defining contiguous constants.
 
This may appear a bit confusing at first, because we are defining constants, but at the same time I am referring to them as variables (vars).
 
To understand this we need to look at the PIC data memory map.
 
The PIC has a bytewide chunk of memory (RAM) dedicated to the temporary storage of data. In some processors (systems) there is no distinction between program memory and data memory, but this is not the case in the PIC.
 
Here is the 16F690 memory map:-
 

 

You can see that this diagram is titled 'PIC 16F690 SPECIAL FUNCTION REGISTERS'. This is because the data memory is dual-purpose. There are 4 byte-wide banks. The lower 32 (0-31) registers (bytes) of each bank are used for controlling special functions of the PIC which are accessible at runtime from your program.

 

The bytes from 32-128 are called General Purpose Registers and are available for temporary storage of data for the use in programs. If you use a different processor you have to look at the memory map, because the GP registers in some start at h'40' or elsewhere.

 

So now we can see that the constant h'20' which we called var1 can be used to address the memory at address h'20', so that when we write an assembler instruction to perform an operation on a file, which is what the assembler calls an 8-bit register, and we use the name var1 in that line, the assembler knows that we mean that the register on which to perform the operation is the one at h'20'.

 

If this all seems a bit long-winded, it will become clearer as we examine the program. It's just a way of making the program easier to understand when reading it, because we could call the variable 'kilograms', or 'miles' and we don't have to remember that register h'20' contains the number of miles or kilograms or volts or whatever.

 

 

        ORG     0x000
 
This tells the assembler where in program memory the code is to be located. 
 
When the PIC executes a POR (Power On Reset), it goes to address 0 to find it's first instruction. The 0x prefix is just another way of indicating a hex number, we could just as easily write h'0', or h'00', or h'000', the two formats are used indistinguishably, better get used to seeing both.
 
The assembler will locate the code at address 0x000 by default, but sometimes we don't want this, as we will see when we come to deal with interrupts, but that is getting ahead of ourselves, so just accept this as part of the template for now.
 
mainloop:


        goto    mainloop
 
These lines are not part of any assembler requirement.
 
The first line, mainloop: is called a label. It's distinguished by being terminated by a colon ':'. The assembler will actually recognise a label without a colon, but it's better (IMO) to learn some discipline in typing, so I recommend that you always use the colon, if only for the benefit of human readers.
 
The label just identifies a location in the program, so that the directive 'goto mainloop' has a known destination. If you don't supply a known destination the assembler will throw an error.
 
Almost all programs, certainly those that interact with a user, operate in a loop. Windows programs have a central loop. DOS applications, Linux applications operate in a loop, until, when required to quit, they drop out of the loop, or in the case of a PIC application, the power is turned off.
 
I just habitually call the label at the start of the main loop in my PIC programs 'mainloop'. C programs have a routine where program operation commences, it's called:
 
main()
{

}
 
We'll encounter this again later.
 
END
 
The assembler likes to be told where the code ends, even though it should be obvious because the file has come to an end.
 
It'll moan if you don't put this in.
 
w
 
 
 
 
 
post #27 of 30
Thread Starter 
Here's the schematic for the little board I built.
 

 

It means I can connect up a 9V battery and have a power (5V) output for the stepper board. Then there are 4 pins brought out to a header to drive the stepper motor coils, and a programming header so that I can reprogram the chip in situ.
 
There are 2 switches to ground with pull-ups to Vcc connected to PORTB, bits 4 and 5. The pullups keep the pins high, except when the switches are closed, when the pins are pulled low. This is a common arrangement for reading switches with PICs. Sometimes the resistors go to ground and the switches pull up to Vcc, it doesn't make much difference, except when you come to read them. Sometimes the switch/resistor circuit has a cap for hardware debounce, sometimes the debounce is done in software, but we don't care about switch bounce (on/off jittering when pressed) here, this is a crude test circuit.
 
All we want from the program is that the stepper motor should move clockwise when one switch is closed, and anticlockwise when the other is closed.
 
Looking at the stepping sequence from a few posts back, we can see that there are 8 states:-
 
0001
0011
0010
0110
0100
1100
1000
1001
 
We just need to output these patterns (on PORTC). Depending on the order we output them, the motor will move in one direction or the other. There are 8 pins on PORTC, so it's easier to treat the output patterns as byte wide
 
00000001
00000011
00000010
00000110
00000100
00001100
00001000
00001001
00000001
00000011
00000010
00000110
00000100
00001100
00001000
00001001
 
...and so on.
 
There being 8 states we can store the states in a single byte, encoded as the position of a 1.
 
00000001 means output 00000001
00000010 means output 00000011
00000100 means output 00000010
00001000 means output 00000110
00010000 means output 00000100
00100000 means output 00001100
01000000 means output 00001000
10000000 means output 00001000
 
...all we have to do is move the bit in one direction or the other, depending on which switch is closed (if any), and send the pattern for the corresponding state to PORTC. Or do nothing if no switch is closed.
 
At its most primitive what we're doing is:
 
Test switch 1
If closed, move clockwise, go back to the beginning and start over
If not, test switch 2
If closed, move anticlockwise, go back to the beginning and start over
If not, go back to the beginning and start over
 
This is the program loop I spoke about previously.

 

 

In the cblock section of the stepper program I create (name) three variables.

 

 

cblock h'20'

rflag
sflag
dflag

endc
 
rflag contains the state register (the one with the bit that moves left and right)

 

sflag because I need a shadow register to move rflag into so I can do operations on it to discover where the one is without affecting the state of rflag, and s comes after r.

dflag because I need a register to use for counting, and it was less typing to copy rflag and change the r to d for delay.

 

Now let's look at the first section of the program, the initialisation:-

 

 

        bcf     STATUS,RP1
        bcf     STATUS,RP0     ; 00, BANK 0
        movlw   b'11111111' 
        movwf   PORTA          ; PORTA all ones
        movwf   PORTB          ; PORTB all ones
        clrf    PORTC          ; PORTC all zero
        bsf     STATUS,RP1
        bcf     STATUS,RP0     ; 00, BANK 2
        clrf    ANSELH         ; make PORTB 4,5 inputs digital
        bcf     STATUS,RP1
        bsf     STATUS,RP0     ; 01, BANK 1
        movwf   TRISB          ; PORTB inputs
        movwf   TRISA          ; PORTA inputs
        clrf    TRISC          ; PORTC outputs
        bcf     STATUS,RP1
        bcf     STATUS,RP0     ;00, BANK 0
        movlw   b'00000001' 
        movwf   rflag
        movwf   PORTC
        clrf    dflag  
 
This section is often started with a label, init:, but we don't need one in this case.
 
What we're doing here is setting up the ports as input or output, presetting their values, initialising the state register, and initialising a register that I use as a counter to create a delay (dflag).
 
The first thing to notice is the use of RP0 and RP1, which are bits in the STATUS register.
 
The STATUS register is an important register, so important that if you look in the memory map, you'll see that the STATUS register is mapped across all four memory banks. This means that whichever memory bank is selected (you can only address one at a time), you always have access to the STATUS register. This is good because bits RP0 and RP1 of the STATUS register control which memory bank is currently active.
 
PORTA, PORTB and PORTC are all in bank 0.
 
TRISA, TRISB and TRISC are all in bank 1. TRIS stands for tristate, and the TRIS registers control whether ports are used as input ports or output ports. The individual bits of each port can be input or output, depending on the state of the bits in the TRIS registers. If bit 0 in TRISC is zero, bit 0 of PORTC is an output, if bit 0 in TRISC is one, bit 0 of PORTC is an input and so on...
 
RP1 and RP0 make up a 2-bit binary number that you can set to a value between 0 and 3. RP0 is the rightmost (least significant bit). This number controls which memory bank, 0, 1, 2 or 3 is active. RP0 is actually bit 5 of the STATUS register, and RP1 bit 6, but because of <p16F690.inc> we can refer to them by name.
 
You set (1) the bits like this:
 
bsf    STATUS, RP0
 
bit set file   (whitespace)  filename, bit
 
You clear (0) the bits like this:
 
bcf    STATUS, RP1
 
bit clear file   (whitespace)  filename, bit
 

 

The first thing we want to do is initialise the port values, because when we assign them as inputs or outputs they immediately output their current value (if outputs), and the set value can influence what is read if they are inputs.

 

 

        bcf     STATUS,RP1
        bcf     STATUS,RP0     ; 00, BANK 0
        movlw   b'11111111' 
        movwf   PORTA          ; PORTA all ones
        movwf   PORTB          ; PORTB all ones
        clrf    PORTC          ; PORTC all zero
 
Select bank 0
Move a literal (b'11111111') to the 'w' register
Move the 'w' register to PORTA
Move the 'w' register to PORTB
Clear (0) file register, filename (PORTC)
 
You should be able to understand what is going on with TRISA, B and C and the variables rflag and dflag. The only thing remaining in this section to explain is this line
 
clrf      ANSELH
 
The ANSEL and ANSELH registers perform a secondary configuration of port bits that are set as inputs by the TRIS registers. If the appropriate bits in the ANSEL or ANSELH registers are set (1) then those inputs are used as analog inputs and can be demultiplexed to the A/D converter. They are set by default on POR. Therefore if we wish to read the digital values on PORTB 4 and 5, we need to unset (clear, 0) the appropriate bits in the ANSELH register. The easiest way to do this is to clear the whole register.
 
w

Edited by wakibaki - 1/9/13 at 4:15pm
post #28 of 30
Thread Starter 

Now let's take a look at the program's main loop.

 

This is the section between the label, 'mainloop:' and the last 'goto mainloop'.

 

 

mainloop:

        btfsc   PORTB,4
        goto    main_anti
        call    clockwize
        call    do_your_thing
        call    delay
        goto    mainloop

main_anti:
        btfsc   PORTB,5
        goto    mainloop
        call    aclockwize
        call    do_your_thing
        call    delay
        goto    mainloop
 
I've taken out several lines that just say 'call delay', over and over, because they don't contribute to understanding the program. I notice I've spelled clockwise wrong, but it's too late to worry about that now.
 
        btfsc   PORTB,4
 
bit test file skip (if) clear     filename, bit_to_test
 
What this means is; look at bit 4 of PORTB, if it's clear (zero, 0), skip one instruction and carry on.
 
So if bit 4 of PORTB is one (1, high) execute the next instruction (goto the label main_anti:)
 
If bit 4 of PORTB is zero (0, low), the skip takes us to the instruction to call the subroutine clockwize.
 
We need to understand the idea of a subroutine and the calling procedure.
 
A subroutine is a section of code, generally one that performs a complete function in it's own right, that is split from the main routine for one reason or another, often because it is used repeatedly, perhaps in different sections of the code. This both saves time (in rewriting the code) and saves program memory.
 
In MPasm assembler a subroutine is bounded by a label, indicating the start, and a 'return' instruction, which indicates the end and causes program execution to resume at the next command in the calling routine.
 
When the program encounters a 'call' instruction it puts (pushes, in conventional system terms) the address of the next instruction on the stack. The stack is a block of memory organised on a lifo basis (last in first out). In the 16F690 the stack is 8 deep. The program then loads the address of the subroutine label into the program counter, causing execution to continue at the first instruction in the subroutine. When the subroutine execution encounters a 'return' instruction, the processor extracts (pops) the stored address from the stack, loads it into the program counter, and execution continues from the instruction after the call in the calling routine.
 
The 8-deep stack imposes a limit which means that subroutines can conventionally only be 'nested' 8 deep on the 16F690. Special precautions may need to be taken when executing subroutines, as values in processor registers, such as the 'w' register, may need to be preserved explicitly, but we will not deal with that exigency here.
 
Three subroutines, clockwize, do_your_thing and delay are called in this branch of the program before 'goto mainloop' is encountered and program execution returns to the beginning.
 
If the program has branched to main_anti a similar test is performed on PORTB, bit 5, and depending on the state of this bit, a similar call sequence (but this time for anticlockwise) is executed, and in either case the program eventually resumes at the label 'mainloop:'
 
Now we just need to understand the 4 subroutines, clockwize, anticlockwize, do_your_thing and delay.
 
w
post #29 of 30
Thread Starter 

OK, here are the 2 subroutines, clockwize and anticlockwize, they're practically identical, so there's no reason not to just describe one.

 
clockwize:
        rrf     rflag,f
        btfss   STATUS,C
        goto    clk
        bsf     rflag,7
clk:
        bcf     STATUS,C
        return

aclockwize:
        rlf     rflag,f
        btfss   STATUS,C
        goto    aclk
        bsf     rflag,0
aclk:
        bcf     STATUS,C
        return
 
We have the state register rflag, containing b'00000001'. We need to move the 1 left or right. Fortunately there are PIC commands that do just that. The mnemonics for these are rrf and rlf, rotate right (or left) through carry. What this means is that the contents of the rightmost bit, or leftmost bit, get moved into the Carry bit (STATUS, C) while all then other bits get shuffled over one place. The carry bit also gets rotated into the bit which would otherwise be left empty, but we don't need that so we just clear the carry bit, after testing it to see if anything has rotated into it. If anything has we set the appropriate bit of rflag, either bit 7 or bit 0, depending on the direction of rotation.
 
Rotate the register
Check the carry bit
If it's set, set bit 7 or bit 0 as appropriate
Either way, clear the carry bit
Return to caller.
 
Now the delay routine.
 
delay:  
        decf    dflag,f
        btfss   STATUS,Z
        goto    delay     
        return
 
Take one away from dflag, leave the result in dflag (decrement file, filename, destination)
If Zero bit set, return
Else, go back to the beginning.
 
Since dflag starts out as b'00000000', taking one away leaves b'11111111' or 255 decimal. The routine doesn't return until dflag reaches 0, which is what we want it to be when the routine is called again.
 
Finally:
 
do_your_thing:
        movfw   rflag
        movwf   sflag
        rrf     sflag,f
        btfsc   STATUS,C
        goto    s0
        rrf     sflag,f
        btfsc   STATUS,C
        goto    s1
        rrf     sflag,f
        btfsc   STATUS,C
        goto    s2
        rrf     sflag,f
        btfsc   STATUS,C
        goto    s3
        rrf     sflag,f
        btfsc   STATUS,C
        goto    s4
        rrf     sflag,f
        btfsc   STATUS,C
        goto    s5
        rrf     sflag,f
        btfsc   STATUS,C
        goto    s6
        rrf     sflag,f
        btfsc   STATUS,C
        goto    s7
s0:
        nop
        bcf     STATUS,C
        movlw   b'00000001'
        movwf   PORTC
        return
s1:
        nop
        bcf     STATUS,C
        movlw   b'00000011'
        movwf   PORTC
        return
s2:
        nop
        bcf     STATUS,C
        movlw   b'00000010'
        movwf   PORTC
        return
s3:
        nop
        bcf     STATUS,C
        movlw   b'00000110'
        movwf   PORTC
        return
s4:
        nop
        bcf     STATUS,C
        movlw   b'00000100'
        movwf   PORTC
        return
s5:
        nop
        bcf     STATUS,C
        movlw   b'00001100'
        movwf   PORTC
        return
s6:
        nop
        bcf     STATUS,C
        movlw   b'00001000'
        movwf   PORTC
        return
s7:
        bcf     STATUS,C
        movlw   b'00001001'
        movwf   PORTC
        return
 
I've taken out a lot of nop's for clarity, they're just there to keep the timing straight, because it takes longer to detect the 1 in b'10000000' than it does in b'00000001'
 
So what's going on here? We're looking for the 1 bit. When we find out which position it's in, we jump to the appropriate point to output the right pattern to PORTC, then we return to the calling routine. We find the bit by shifting all the bits right and checking to see if it's popped out into the carry bit. We just have to make a copy of rflag in sflag to work on, because otherwise we'd destroy rflag by shifting it. Move (copy) rflag into the 'w' register, move (copy) the 'w' register into sflag. That's it, apart from we have to count the machine cycles to reach each stage, and pad each stage with the right number of nop's so that the time between transitions is the same.
 
When I first tried the program, all the indicator LEDs for the coils lit up and the stepper motor didn't move, it didn't even buzz. So I put the delay in between calls to the clockwize or anticlockwize routines. No change. So I called the delay routine 10 times between movements - everything sprang into life.
 
Obviously this program is incomplete. It needs further work to make it seek the start position, maybe 2 speed movement, slow at first, then faster. It does, however, demonstrate the very basics of driving a stepper motor, and all without any serious calculation of gearing, steps per revolution or rate of revolution or anything else, it's all empirical (suck it and see).
 
I'll get on with building the meter PCB...
 
w

Edited by wakibaki - 1/9/13 at 7:38pm
post #30 of 30
Thread Starter 

Sorry I haven't written anything here for a while, I'm having some trouble with the 16F690 voltmeter.

 

I've got the meter working with a 16F887 with a non-multiplexed display, i.e the A/D code works, I've got the display working multiplexed with the 16F690, but I can't get the A/D to work with the multiplexed display. It's occupied several days now without significant progress. The minute I enable the A/D the display goes haywire. Something in the interrupt service routine.

 

I may simply move on to implementing the PGA2310 volume control, I have this working with a 16F887 with a multiplexed display, it should transfer to the 16F690 no problem, but that's what I thought about the meter. Anyway, having got my teeth into this particular problem, I'm reluctant to let it go, so I'm going to put a few more hours into it...

 

w. 

New Posts  All Forums:Forum Nav:
  Return Home
Head-Fi.org › Forums › Misc.-Category Forums › DIY (Do-It-Yourself) Discussions › PIC design tutorial