一、前言
為什么要研究單片機函數切換的過程?實際上是我在20年暑假時給51單片機寫了一個簡單的實時操作系統,具有簡單的搶占式內核調度功能,雖然很簡單,但我還是想把實現的過程分享出來,這篇文章是其中的內容之一,有興趣的同學可以先了解一下,點個關注收藏,后面持續更新!
二、函數切換原理
在使用C語言編寫51單片機的程序時,如果我們在函數一中調用另外一個函數,只需要添加一行 函數名+括號及參數 就可以執行另外一個函數,就就像下面的例子:
int main(void){ int a=0; Fun1(a); Fun2(a); return 0;}
在main函數中直接調用Fun1,Fun2函數,然后程序就會跳轉。但是問題來了,函數是怎么跳轉的呢?在函數跳轉的過程中51單片機的寄存器是如何變換的呢?
實際上,函數的切換過程其實就是將當前函數的運行狀態和數據以及返回地址等保存到堆棧,然后讀取新函數的運行狀態和數據,PC(程序計數器)再跳轉到調用函數的地址執行對應的函數,這些操作其實都是在對51單片機的寄存器進行操作,具體用到的幾個寄存器如下:
寄存器 | 功能 |
---|---|
R0-R7 | 工作寄存器R0~R7:存儲當前程序的 “環境“ |
DPH | 數據地址指針(高8位):DPH和DPL組合在一起使用,用它來訪問外部數據存儲器中的任一單元,也可以作為通用寄存器來用 |
DPL | 數據地址指針(低8位):DPH和DPL組合在一起使用,用它來訪問外部數據存儲器中的任一單元,也可以作為通用寄存器來用 |
PSW | 程序狀態字:里面放了CPU工作時的很多狀態,可以了解CPU的當前狀態 |
B | B寄存器:在做乘、除法時放乘數或除數 |
ACC | 累加器:運算寄存器 |
SP | 堆棧指針:指向堆棧操作的棧頂地址,是8位計數器 |
PC | 程序計數器:指向下一條待執行的指令 |
下面我們來用匯編手動編寫一個函數切換函數,然后在定時器中斷中調用,不停的切換兩個函數,編寫前先了解一下切換框架和使用到的匯編代碼
POP出棧指令
彈出堆棧數據到data,然后SP指針減一
POP data
PUSH壓棧指令
先把SP指針加一,然后將data數據壓入堆棧
PUSH data
RET返回指令
把彈出堆棧兩個字節的數據到PC,指向下一個程序的執行地址
三、函數切換代碼實現
函數代碼我們使用51單片機作為運行平臺,在主函數中通過切換函數1切換到函數1,函數1是一個死循環,之后我們在函數1里面調用函數切換2切換到函數2運行,函數2延時一段時間后再切換回1,一直循環下去;代碼如下:
定義用到的函數:
void task1(void); //函數1
void task2(void); //函數2
void delay(unsigned short time);//延時函數
定義用到的變量和類型
unsigned char a; //函數一運行的標志
unsigned char b; //函數二運行的標志
unsigned char task1_stack[20]; //函數堆棧
unsigned char task2_stack[20]; //函數堆棧
//聲明函數控制塊結構體
typedef struct
{
unsigned char Task_SP; //函數堆棧指針
}TASK_TCB;
//定義TCB
TASK_TCB task1_tcb;
TASK_TCB task2_tcb;
編寫main函數主體初始化,此處定義兩個函數控制塊tcb,用來存放函數的堆棧指針(函數的堆棧其實就是一個數組,用來保存函數的運行數據),然后我們在將函數的入口地址保存在堆棧的最低兩位,接著將SP指針向上偏移14位,因為我們要保存的寄存器加起來有13位,同時在一開始要把函數入口保存在堆棧所以是14位
而切換到函數的時候是要先從函數堆棧出棧,所以預先偏移14位地址,main函數代碼如下:
void main(void){ //保存堆棧指針和函數入口 task1_tcb.Task_SP = task1_stack; task1_stack[0]= (unsigned char)task1; task1_stack[0]= (unsigned char)task1>>8; //偏移堆棧 task1_tcb.Task_SP += 14; //保存堆棧指針和函數入口 task2_tcb.Task_SP = task2_stack; task2_stack[0]= (unsigned char)task2; task2_stack[0]= (unsigned char)task2>>8; //偏移堆棧 task2_tcb.Task_SP += 14; //切換到函數1 Task_Sched_1(); while(1);}
編寫函數1和函數2實體
void task1(void) { while(1) { a=1; b=0; delay(100); //延時 Task_Sched_2();//切換到函數2 }}void task2(void){ while(1) { a=0; b=1; delay(100);//延時 Task_Sched_1();//切換到函數1 }}
編寫函數切換函數
切換到函數1
void Task_Sched_1(void){ __asm PUSH ACC //保護當前寄存器,壓棧 __asm PUSH B __asm PUSH PSW __asm PUSH DPL __asm PUSH DPH __asm PUSH 0 //0-7為工作寄存器 __asm PUSH 1 __asm PUSH 2 __asm PUSH 3 __asm PUSH 4 __asm PUSH 5 __asm PUSH 6 __asm PUSH 7 SP = (task1_tcb.Task_SP); __asm POP 7 //恢復目標函數寄存器 __asm POP 6 __asm POP 5 __asm POP 4 __asm POP 3 __asm POP 2 __asm POP 1 __asm POP 0 __asm POP DPH __asm POP DPL __asm POP PSW __asm POP B __asm POP ACC}
切換到函數2
void Task_Sched_2(void){ __asm PUSH ACC //保護當前寄存器,壓棧 __asm PUSH B __asm PUSH PSW __asm PUSH DPL __asm PUSH DPH __asm PUSH 0 //0-7為工作寄存器 __asm PUSH 1 __asm PUSH 2 __asm PUSH 3 __asm PUSH 4 __asm PUSH 5 __asm PUSH 6 __asm PUSH 7 SP = (task2_tcb.Task_SP); __asm POP 7 //恢復目標函數寄存器 __asm POP 6 __asm POP 5 __asm POP 4 __asm POP 3 __asm POP 2 __asm POP 1 __asm POP 0 __asm POP DPH __asm POP DPL __asm POP PSW __asm POP B __asm POP ACC}
注意此處的切換函數使用匯編編譯,主要內容就是保存當前函數的運行環境到函數堆棧,然后從下一個函數的堆棧讀取其運行環境,切換代碼我寫在一個os.c文件里面,編譯前需要匯編編譯,步驟如下:
右擊文件->options
開啟嵌入匯編程序,使C語言中可以編譯匯編代碼,加__asm聲明一下是匯編就行
四、實驗現象
函數1中把a取1,b取0,而函數2相反,當這兩個函數交叉運行時a和b的波形應該相反,所以仿真后結果如下,手動切換函數完成
上一篇:51單片機定時器、串口、中斷
下一篇:51單片機串口應用實例(匯編)
推薦閱讀最新更新時間:2025-06-07 23:41


