Stack underflow problem using PIC assembly on a PIC16F84A

336 Views Asked by At

I have a small project for a course I am doing that requires us to produce a PWM signal using PIC assembly language. To try and simplify things I have set the high time to 5ms and the low time to 15ms so I can call the same delay sub routine multiple times. Calling this delay multiple times seems to be causing the problem with the stack underflow.

I am not really sure what I can try to resolve this as I am very fresh to programming. I have tried searching on this site as well as generally but haven't been able to find a solution. I am using MPLab 9.82 as well.

The code is as follows:

list        p=16F84A      
#include    <p16F84A.inc>

    __CONFIG _CP_OFF & _XT_OSC & _PWRTE_ON & _WDT_OFF ;turn off watchdog timer

org 0x00 ; program starts at 0x00

counter equ 4Fh ; create a counter at position 4Fh in RAM

    BSF STATUS, RP0 ; select bank 1
    BCF TRISB, D'1' ; set port B pin 1 to output
    BCF STATUS, RP0 ; select bank 0

    goto main

main

    BSF PORTB,1 ; turn port B pin 1 on
    call delay_5ms ; calls sub routine for 5ms delay
    BCF PORTB,1 ; turn port B pin 1 off
    call delay_5ms ; calls sub routine for 5ms delay
    call delay_5ms ; calls sub routine for 5ms delay
    call delay_5ms ; calls sub routine for 5ms delay

delay_5ms 

    movlw D'200' ; put decimal number 200 into working register
    movwf counter ; move 200 from working register into counter

lp  nop ; no operation. just take 1 instruction
    nop  ; 1 instruction
    decfsz counter ; 1 instruction and decreases counter by 1
    goto lp ; 2 instructions (goto takes 2 instructions)
    return

end 

mplab 9.82 Simulator code

2

There are 2 best solutions below

1
On

Every embedded system need an infinite loop. The infinite loop is necessary because the embedded software's job is never done. It is intended to be run until either the world comes to an end or the board is reset, whichever happens first. In addition, most embedded systems have just one piece of software running on them. In an non-embedded systems, when main() finishes it returns to the operating system and the program is removed from memory. In embedded systems there's no operating system to return to, and the program can't be removed from memory.

In assembler the structure could look like:

reset:
 ; do init part

main:
  ; do the job in infinite loop
goto main    

or in C

void main ()
{
    init();
    while(1)
    {
        do_job();
    }
}
0
On

As Hans Passant pointed out, main needs to have some way to prevent fallthrough. For a detailed explanation of what's going on here, let's look at a C function:

void doNothing()
{
    asm("nop");
}

Not the best example, but I needed something that didn't have a return statement. At least, in C there is no return statement, but in assembly this function (assuming it doesn't get inlined) would look like this:

doNothing:
nop
return

A function written in C will always compile to an assembly function with a return instruction (unless the compiler inlines the function), even if your function doesn't have a C return statement.

Why is this necessary? Because ASM labels don't exist.

A labeled line of code in an assembly source file is just a reference to a specific memory address. When I have the following code:

main:
   nop
   nop
   nop
   goto main

The goto main is just a hardware abstraction which means "goto whatever memory address main happens to be. Whether the branch is relative or absolute, it matters not-- the assembler does the math for you and replaces the main in goto main with the required offset to make the branch take you where you want. However, when your executable is created, the main: (note the colon after, I'm talking about the label itself) is gone. This leads to a few quirks with assembly that don't happen in other languages:

  • First, I can have multiple labels that point to the same instruction, and doing so does not increase the file size of the executable.
foo:
bar:
baz:
nop

Second, and most importantly, there is nothing stopping execution from falling through a lable. The CPU sees

main

    BSF PORTB,1 ; turn port B pin 1 on
    call delay_5ms ; calls sub routine for 5ms delay
    BCF PORTB,1 ; turn port B pin 1 off
    call delay_5ms ; calls sub routine for 5ms delay
    call delay_5ms ; calls sub routine for 5ms delay
    call delay_5ms ; calls sub routine for 5ms delay

delay_5ms
    movlw D'200' ; put decimal number 200 into working register

exactly the same as

main

    BSF PORTB,1 ; turn port B pin 1 on
    call delay_5ms ; calls sub routine for 5ms delay
    BCF PORTB,1 ; turn port B pin 1 off
    call delay_5ms ; calls sub routine for 5ms delay
    call delay_5ms ; calls sub routine for 5ms delay
    call delay_5ms ; calls sub routine for 5ms delay
    movlw D'200' ; put decimal number 200 into working register

That being said, you can use fallthrough to your advantage, if for example you want to execute some function a fixed number of times, without the overhead of a loop.

foo:
;execute "bar" four times

call bar
call bar
call bar

bar:
   nop
   return