快速设计简单嵌入式操作系统(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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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 …