快速設計簡單嵌入式操作系統(3):動手實操,基于STC8編寫單任務執行程序,感悟MCU指令的執行過程

引言

前面我們陸續學習了操作系統常見的基礎概念,接著簡單了解了一下8051單片機的內存結構和執行順序切換的相關概念。接下來,我們就開始進行實操,基于8051單片機STC8來編寫一個簡單的操作系統,這里我們先實現一個單任務的執行程序,體會MCU中指令的執行過程。


一、新建工程

1.1 本地新建工程目錄

1. 選擇一個合適的地方新建一個文件夾os_demo

2. 在os_demo目錄下新建user目錄,用于存放main.c

詳細地說,第一步,新建os_demo文件夾,用于存放工程。筆者在本地的STC目錄下直接創建一個空的os_demo文件夾;第二步,進入os_demo1目錄,繼續創建一個空文件夾user,用于存放main.c源文件。

最后效果如下

1.2 keil5新建工程文件

1. 進入project,點擊新的uVision工程,進入本地創建好的os_demo1目錄下,輸入工程文件名稱os_demo1,保存即可;

2. 接著選擇使用的8051單片機芯片;

3. 在keil中建立邏輯目錄結構;

4. keil中的簡單配置。

具體步驟如下圖所示

第一步,選擇目錄新建keil工程文件,生成相關配置文件

第二步,選擇芯片型號,筆者使用的是STC8H8K64U這款8051單片機芯片

不用添加這個啟動文件,因為后面我們正要編寫的就是這種類似文件

第三步,在keil中建立邏輯目錄結構,盡可能選擇與本地目錄名相同去創建目錄user,然后將本地對于的文件main.c添加進來,OK即可

第四步,配置一下,進入魔法棒,接著進入C51,如下圖所示,將本地新建的user目錄的路徑在這里包含一下

至此,該工程就創建完畢了。


二、程序編寫

在keil中,打開前面創建好的工程文件,然后打開main.c開始編寫代碼。

首先是引入STC8的頭文件,其中定義了該單片機中的一些常用的寄存器地址,包括但不限于特殊功能寄存器等

#include <stc8h.h> // 定義一些寄存器的地址

2.1 任務棧管理

每個MCU要進行的需求可以認為是一個任務,而每個任務要執行的話都有對應的指針或者說地址去便于找到該任務執行的入口。

這里我們需要先定義一個任務堆棧指針數組,用于存放不同任務的堆棧指針SP。關于SP前面也重點提過,用來記錄程序執行時的下一條指令地址的特殊功能寄存器。這里定義它是為了去記錄任務(我們希望執行的程序)的堆棧指針的。

接著定義一個二維數組,用來存放每個任務的堆棧信息,這個堆棧信息可以理解為每個任務的執行入口地址,其實可以見到理解為SP去記錄的那個堆棧指針。

然后這倆數組都是用于存放一定數量任務的堆棧相關內容的,所以要規定最大任務數量和堆棧深度(可先理解為堆棧大小)。同時為了避免魔法數字的出現,筆者在這里便使用宏定義去分別定義最大任務數以及最大堆棧深度。當然為了簡化程序復雜程度,筆者先定義最大任務數量為2,后續再慢慢擴充。

然后我們還需要定義一個任務id,用于表示任務的名字

因此,針對上述描述的邏輯,給出關于任務堆棧管理的定義部分示例代碼如下:

#define MAX_TASKS	2		// 簡化任務數為2
#define	MAX_TASK_DEPTH	32	// 堆棧深度// idata 表明信息定義在STC8訪問最快的內部內存空間里面unsigned char idata task_sp[MAX_TASKS];	// 任務的堆棧指針
unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEPTH];  // 每個tasks任務的堆棧信息unsigned char idata task_id;		// 當前任務號,從0開始

2.2 任務的創建

接著,我們開始創建幾個任務,前面筆者為簡化任務數量,將任務最大數定義為2,所以我們就先創建兩個任務。

大家可能疑惑:任務?這怎么創建??實際上,前面說過任務就是咱CPU或MCU要去完成的某項工作,實際就是一個具有特定功能或需求的程序,因此所謂創建倆任務在這里我們直接創建倆函數用來模擬兩個任務的創建就好了。

比如創建兩個不斷進行加法運算的功能任務

void task0()
{// 0號任務,代表第0個小朋友做的事情unsigned int a = 3;// 死循環,表示該任務永遠不會執行完while(1){a = a + 3;}
}void task1()
{// 1號任務,代表第1個小朋友做的事情unsigned int b = 5;// 死循環,表示該任務永遠不會執行完while(1){b = b + 5;}
}

如上代碼所示,我們創建了兩個任務,分別叫任務0和任務1,都是進行一個不斷累積加的任務。

同時我們知道,對于STC8單片機來說,其MCU為單核MCU,執行程序一般是從上至下依次執行,當碰見這種循環的程序時,更是會一直卡在那里反復運行,如果不給跳出循環的函數可能就無休止了。比如咱現在這個加法累積運算,咱MCU里面的ALU運算邏輯單元就要出馬,但單核僅一個,所以正常情況一次只能處理一個運算,如果需要兩個任務都執行,勢必會產生資源爭搶的問題。

恰巧后續咱會一步一步將這個問題解決,不過為了理解為上,我們循序漸進一步一步來看。因此,我們先從執行一個任務入手,看看單核MCU是如何執行這個任務的。

2.3 任務的加載

我們這里所謂的運行一個任務其實和普通單片機運行有一點點區別,正常情況如果點個燈,那就直接在main函數里面寫段相關邏輯編譯運行就行,但這里不一樣,大家是否還記得前面提到的堆棧指針以及切換程序執行順序的內容?本次我們就是要結合切換程序執行順序的思路去利用這個堆棧指針來修改程序運行的順序,將我們的單個任務嵌入中間運行,等運行完后再去執行原程序內容。

因此這里可能就需要先簡單提一下我們單片機上電后做的事情了:

對于STC8單片機來說,其硬件上電復位后會自動將PC程序計數器強制為復位向量地址,然后從這里開始執行啟動程序,
當啟動程序完成初始化后,就會執行LCALL main指令調用main函數。
由于LCALL指令會在跳轉前先將當前執行指令的下一條指令地址(即PC程序計數器對應的指向)壓入堆棧,
然后等該指令調用的函數執行完后,再從堆棧中彈出返回地址(先前存的原來執行的下一條指令地址)賦值給當前的PC,
接著就能繼續執行原來后面的程序指令了。

對于這段話,可能剛看會有一點理解不過來哈,咱也不求一看就懂,所以沒事,多思考多查總能理解的。

好,我們細品上面這段話,里面有幾個關鍵點:PC程序計數器、(PC指向壓入)堆棧、后彈出給PC繼續執行原程序。同時會發現,LCALL指令就是我們前面所說的call指令的邏輯,可以跳轉,然后實現一個程序順序切換的功能。也就是說,我們可以通過修改壓入堆棧內容,從而使返回原程序的地址變成我們希望執行的任務地址,這樣當跳轉后返回時就能執行咱自己的程序了。具體邏輯如下:

首先,我們肯定要有存放自己任務入口指針的堆棧和自己的堆棧指針,用于管理我們各個任務的堆棧指針和堆棧信息,當前前面我們已經定義好這倆東西了,即task_stack和task_ip;

其次,我們還需要將我們現有的任務的堆棧信息放到模擬的堆棧空間中,同時把任務地址所在地址給我們定義的堆棧指針記錄。其實咱這里就是將各個任務的地址信息存進task_stack,不過需要注意的是,當前任務是函數定義,所以其地址信息是函數指針,其類型int,16位,然后把這塊堆棧信息所在的地址給自定義的task_sp里面;

最后,我們將堆棧指針壓入SP,覆蓋原本PC指向的下一個程序地址,也就是將前面記錄的地址的指針(地址)task_sp壓入SP即可。

從上述邏輯看,接下來要做的應該是“其次...”這部分,也就是讓任務的堆棧信息都放到相關位置,這相當于是真正開始前的初始化,所以這個內容我們當做任務的加載,定義task_load函數,函數原型為void task_load(unsigned int fn, unsigned char tid)

代碼示例如下:

// fn 函數指針,注意數據類型int,16bit
// tid task id,8bit  0, 1
// 函數功能: 將一個task的函數指針放入對應的堆棧空間
void task_load(unsigned int fn, unsigned char tid)
{// 1. task的堆棧指針記錄相應taskId堆棧信息地址task_sp[tid] = task_stack[tid] + 1;// 2. 使用兩個空間存放task的函數指針task_stack[tid][0] = fn & 0xFF;		// 低8位task_stack[tid][1] = fn >> 8;		// 高8位
}

從代碼上看,由于task_sp是用于記錄堆棧指針,所以我們讓存放對應id任務的堆棧空間所在地址給task_sp;然后由于定義的堆棧是8位的,所以每8位依次將對應id的任務堆棧信息存到自定義的堆棧task_stack中。

2.4 main函數執行

好了,任務加載完后,就可以開始運行咱們得任務了,按照前面所說,接下來就是覆蓋SP的內容即可,所以main函數中我們先加載任務,然后指定任務id(前面定義過記錄任務id的變量task_id),最后覆蓋SP即可

代碼如下:

void main()
{task_load(task0, 0);	// 裝載任務0到對應堆棧內存task_id = 0;SP = task_sp[0];		// 將當前的堆棧指針壓入SP中
}

從代碼上看,我們是希望將任務0的程序運行出來,首先加載了任務0,將其堆棧信息和id傳入了任務加載函數中,完成了堆棧信息的存儲和堆棧指針的記錄,接著指明任務id為0,最后將任務堆棧指針賦值給SP,覆蓋原SP,實現main執行后返回執行的程序為任務0的程序。


三、調試驗證

前面我們已經完成了這個單任務運行代碼的編寫,邏輯是通過修改SP來間接改變程序執行順序,嵌入了自定義的程序從而完成指定程序的執行,但還未實測。因此接下來,我們在keil中測試一下:

首先編譯一下

可以看出,編譯沒有錯誤。接著我們來調試一下,在任務0的累加處打斷點一步一步看看

單任務執行測試

可以看出,我們指定的任務0確實按照指定邏輯被執行。當前,整個程序的執行情況為以下狀態:

硬件上電復位后會先執行一段啟動程序,然后啟動程序完成初始化后會自動發出LCALL main的指令。

接著在調用main時確實是先將LCALL指令之后的下一條指令的地址(PC當前指向的地址)壓入SP中,然后再開始執行的main中的程序。不過在執行main程序時,按順序先執行task_load函數中的內容時,函數中會將task0的函數指針的低8位和高8位依次存儲自定義堆棧中。
然后讓SP堆棧指針指向自定義堆棧棧頂地址,進而覆蓋了原先SP記錄的LCALL之后實際的棧頂地址。
所以,main執行完后繼續執行ret指令出現的結果就是:實際通過SP記錄的堆棧棧頂地址從堆棧依次彈出給到PC的返回地址是我們自定義的task0函數的高8位和低8位地址,
最后程序就會跳回此時PC所指向的地址即task0函數的位置去執行task0函數的內容了。

當然,這只是一個簡單的測試,還有很多可以擴展的地方,比如可以更換其他任務邏輯進行測試、開空調時信息中的內容是否正確等,可自己多探索一下。


四、小結

本次我們理解了基于STC8單片機的單任務執行的原理,使用keil進行工程創建、代碼編寫和實際測試的實操,深入理解了單核MCU的程序指令運行過程和其單任務運行的局限性。


筆者小白,能力有限,以上內容難免存在不足和紕漏,僅供參考,各位閱讀時請帶著批判性思維學習,遇到問題多查查。同時歡迎各位評論區批評指正。謝謝。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/92882.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/92882.shtml
英文地址,請注明出處:http://en.pswp.cn/web/92882.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Spring AI Alibaba - 聊天機器人快速上手

本節對應 Github&#xff1a;https://github.com/JCodeNest/JCodeNest-AI-Alibaba/tree/master/spring-ai-alibaba-helloworld 本文將以阿里巴巴的通義大模型為例&#xff0c;通過 Spring AI Alibaba 組件&#xff0c;手把手帶你完成從零到一的構建過程&#xff1a;首先&#…

串口通信學習

不需要校驗位就選8位&#xff0c;需要校驗位就選9位&#xff01;USRTUSART框圖STM32的外設引腳這是USART的基本結構。數據幀&#xff0c;八位是這個公式還是很重要的&#xff01;如果在編輯器里面使用printf打印漢字的話&#xff0c;會出現亂碼的話&#xff0c;前提是你的編碼格…

面試經典150題[001]:合并兩個有序數組(LeetCode 88)

合并兩個有序數組&#xff08;LeetCode 88&#xff09; https://leetcode.cn/problems/merge-sorted-array/?envTypestudy-plan-v2&envIdtop-interview-150 1. 題目背景 你有兩個已經排好序的數組&#xff1a; nums1&#xff1a;前面是有效數字&#xff0c;后面是空位&…

快速安裝達夢8測試庫

計劃&#xff1a;數據庫名實例名PORT_NUMMAL_INST_DW_PORTMAL_HOSTMAL_PORTMAL_DW_PORTDMDWDBINST_1533615101192.168.207.612510135101*****[2025-08-11 15:14:34]***** Last login: Fri Jul 25 17:36:04 2025 from 192.168.88.48 [rootdm01 ~]# ip a 1: lo: <LOOPBACK,UP,…

Hive中優化問題

一、小文件合并優化Hive中的小文件分為Map端的小文件和Reduce端的小文件。(1)、Map端的小文件優化是通過CombineHiveInputFormat操作。相關的參數是&#xff1a;set hive.input.formatorg.apache.hadoop.hive.ql.io.CombineHiveInputFormat;(2)、Reduce端的小文件合并Map端的小…

tlias智能學習輔助系統--Maven高級-繼承

目錄 一、打包方式與應用場景 二、父子工程繼承關系 1. 父工程配置 2. 子工程配置 三、自定義屬性與引用屬性 1. 定義屬性 2. 在 dependencyManagement 中引用 3. 子工程中引用 四、dependencyManagement 與 dependencies 的區別 五、項目結構示例 六、小結 在實際開…

把 AI 押進“小黑屋”——基于 LLM 的隱私對話沙盒設計與落地

標簽&#xff1a;隱私計算、可信執行環境、LLM、沙盒、內存加密、TEE、SGX、Gramine ---- 1. 背景&#xff1a;甲方爸爸一句話&#xff0c;“數據不能出機房” 我們給某三甲醫院做智能問診助手&#xff0c;模型 70 B、知識庫 300 GB。 甲方只給了兩條鐵律&#xff1a; 1. 患者…

Java 大視界 -- Java 大數據在智能教育學習效果評估指標體系構建與精準評估中的應用(394)

Java 大視界 -- Java 大數據在智能教育學習效果評估指標體系構建與精準評估中的應用&#xff08;394&#xff09;引言&#xff1a;正文&#xff1a;一、傳統學習評估的 “數字陷阱”&#xff1a;看不全、說不清、跟不上1.1 評估維度的 “單行道”1.1.1 分數掩蓋的 “學習真相”…

Dubbo 3.x源碼(33)—Dubbo Consumer接收服務調用響應

基于Dubbo 3.1&#xff0c;詳細介紹了Dubbo Consumer接收服務調用響應 此前我們學習了Dubbo Provider處理服務調用請求的流程&#xff0c;現在我們來學習Dubbo Consumer接收服務調用響應流程。 實際上接收請求和接收響應同屬于接收消息&#xff0c;它們的流程的很多步驟是一樣…

棧和隊列:數據結構中的基礎與應用?

棧和隊列&#xff1a;數據結構中的基礎與應用在計算機科學的領域中&#xff0c;數據結構猶如大廈的基石&#xff0c;支撐著各類復雜軟件系統的構建。而棧和隊列作為兩種基礎且重要的數據結構&#xff0c;以其獨特的特性和廣泛的應用&#xff0c;在程序設計的舞臺上扮演著不可或…

服務端配置 CORS解決跨域問題的原理

服務端配置 CORS&#xff08;跨域資源共享&#xff09;的原理本質是 瀏覽器與服務器之間的安全協商機制。其核心在于服務器通過特定的 HTTP 響應頭聲明允許哪些外部源&#xff08;Origin&#xff09;訪問資源&#xff0c;瀏覽器根據這些響應頭決定是否放行跨域請求。以下是詳細…

Unity筆記(五)知識補充——場景切換、退出游戲、鼠標隱藏鎖定、隨機數、委托

寫在前面&#xff1a;寫本系列(自用)的目的是回顧已經學過的知識、記錄新學習的知識或是記錄心得理解&#xff0c;方便自己以后快速復習&#xff0c;減少遺忘。主要是C#代碼部分。十七、場景切換和退出游戲1、場景切換場景切換使用方法&#xff1a; SceneManager.LoadScene()&a…

用 Spring 思維快速上手 DDD——以 Kratos 為例的分層解讀

用 Spring 思維理解 DDD —— 以 Kratos 為參照 ? 在此前的學習工作中&#xff0c;使用的開發框架一直都是 SpringBoot&#xff0c;對 MVC 架構幾乎是肌肉記憶&#xff1a;Controller 接請求&#xff0c;Service 寫業務邏輯&#xff0c;Mapper 操作數據庫&#xff0c;這套套路…

docspace|Linux|使用docker完全離線化部署onlyoffice之docspace文檔協作系統(全網首發)

一、 前言 書接上回&#xff0c;Linux|實用工具|onlyoffice workspace使用docker快速部署&#xff08;離線和定制化部署&#xff09;-CSDN博客&#xff0c;如果是小公司或者比如某個項目組內部使用&#xff0c;那么&#xff0c;使用docspace這個文檔協同系統是非常合適的&…

【教程】如何高效提取胡蘿卜塊根形態和顏色特征?

胡蘿卜是全球不可或缺的健康食材和重要的經濟作物&#xff0c; 從田間到餐桌&#xff0c;從鮮食到深加工&#xff0c;胡蘿卜在現代人的飲食和健康中扮演著極其重要的角色&#xff0c;通過量化塊根形態和色澤均勻性&#xff0c;可實現對高產優質胡蘿卜品種的快速篩選。工具/材料…

Python初學者筆記第二十四期 -- (面向對象編程)

第33節課 面向對象編程 1. 面向對象編程基礎 1.1 什么是面向對象編程面向過程&#xff1a;執行者 耗時 費力 結果也不一定完美 面向對象&#xff1a;指揮者 省時 省力 結果比較完美面向對象編程(Object-Oriented Programming, OOP)是一種編程范式&#xff0c;它使用"對象&…

Go 語言 里 `var`、`make`、`new`、`:=` 的區別

把 Go 語言 里 var、make、new、: 的區別徹底梳理一下。1?? var 作用&#xff1a;聲明變量&#xff08;可以帶初始值&#xff0c;也可以不帶&#xff09;。語法&#xff1a; var a int // 聲明整型變量&#xff0c;默認值為 0 var b string // 默認值 ""…

計算機網絡---IP(互聯網協議)

一、IP協議概述 互聯網協議&#xff08;Internet Protocol&#xff0c;IP&#xff09;是TCP/IP協議族的核心成員&#xff0c;位于OSI模型的網絡層&#xff08;第三層&#xff09;&#xff0c;負責將數據包從源主機傳輸到目標主機。它是一種無連接、不可靠的協議&#xff0c;提供…

DataFun聯合開源AllData社區和開源Gravitino社區將在8月9日相聚數據治理峰會論壇

&#x1f525;&#x1f525; AllData大數據產品是可定義數據中臺&#xff0c;以數據平臺為底座&#xff0c;以數據中臺為橋梁&#xff0c;以機器學習平臺為中層框架&#xff0c;以大模型應用為上游產品&#xff0c;提供全鏈路數字化解決方案。 ?杭州奧零數據科技官網&#xff…

【工具】通用文檔轉換器 推薦 Markdown 轉為 Word 或者 Pdf格式 可以批量或者通過代碼調用

【工具】通用文檔轉換器 推薦 可以批量或者通過代碼調用 通用文檔轉換器 https://github.com/jgm/pandoc/ Pandoc - index 下載地址 https://github.com/jgm/pandoc/releases 使用方法: 比如 Markdown 轉為 Word 或者 Pdf格式 pandoc -s MANUAL.txt -o example29.docx …