给单片机编操作系统——第0篇
:::align{right} 能给单片机编那么多东西,编操作系统也不过分吧?
第0篇——概述~和瞎扯~ :::
0.前言
0.前言
:::info{open} 将文章下载至本地,以获取最佳阅读体验:WinterOS(Password:0324)
作者:glen:
BiliBili:glengmr
E-mail:[email protected] :::
1.正文
对,你没看错。这是前言的正文,那既然你没看错,我们就开始吧。
先说说我是怎么有这个想法的:
2023 年的一个冬天,我发现基本所有能用单片机(整文特指 Arduino 及其系列产品)做到的东西都已经被做出来了,实在没有什么新奇的东西,于是我便想到了一个基本不可能用单片机做到的事——跑操作系统。
声明:本人在单片机方面见识短浅,文章中难免会有不足和错误之处,请各位大佬多多指教。
2.后记
对,你又没看错。这是前言的后记。
既然要做这么一个“项目”,那么就给她取个名字吧。就叫“Winter OS”吧,图标我还要再设计一段时间。
然后来说一下这个项目的大概内容:
Winter OS
├───Winter Kernel
│ ├───Winter Kernel for Arduino for Main_Controller
│ ├───Winter Kernel for Arduino for Graphics_Processor
│ ├───Winter Kernel for Arduino for Audio_Processor
│ ├───Winter Kernel for Arduino for Device_Manager
│ └───Winter Kernel for Arduino for Keyboard_Processor
└───Winter System
└───Winter System for Arduino
也许你会问:为什么要搞这么多东西?
看完这篇文章,你一定会明白。
废话不多说,我们开始正文吧。
1.架构
1.软件
众所周知,操作系统(后文称“OS”)的启动是由 Bootloader 或 Bios/EFI 引导磁盘中 OS 文件,然后将整设备的硬件控制权限移交给 OS。但是单片机的 Bootloader 只会引导固件中的代码,并不能直接引导 SD(或外部储存器,如磁盘)中的 OS 文件,而且不管是固件代码还是外部储存文件,都不能直接控制整个单片机的 CPU、内存、电源等重要硬件。那么问题来了,要怎么才能使 Bootloader 引导 SD 中的文件并且把权限移交出来呢?
答案是:不要引导和权限了。我们用单片机的固件代码(后文称“内核”或“Kernel”)来处理 OS,如图 1.1-0:
我们来举一个最简单的 Kernel 的例子:
void setup () {
pinMode ( 0 , OUTPUT ) ;
}
void loop () {
digitalWrite ( 0 , HIGH ) ;
}
这个 Kernel 让第 0 个数字引脚设置成了高电平,是不是很简单?
那我们来个稍微难一点的(有亿点长):
先是 Kernel:
//Kernel(本段代码部分由AI生成):
#include <SPI.h>
#include <SD.h>
#include <String.h>
const int chipSelect = 4; // SD卡模块的片选引脚
void setup() {
Serial.begin(9600);
Serial.println("Initializing SD card...");
if (!SD.begin(chipSelect)) {
Serial.println("SD card initialization failed!");
return;
}
Serial.println("SD card initialized.");
// 打开文件"command.pgm"
File dataFile = SD.open("command.pgm");
if (dataFile) {
while (dataFile.available()) {
// 读取一行直到遇到换行符
String line = dataFile.readStringUntil('\n');
// 打印行内容到串口
Serial.print("Line: ");
Serial.println(line);
processLine(line);
}
dataFile.close(); // 关闭文件
} else {
Serial.println("File Error");
}
}
void loop() { // 空置(只需读取并执行一次) }
// 行处理函数的示例
void processLine(String line) {
if (line.startsWith("pinset")) {
// 引脚控制指令
int pin=(line[7]-'0');
String str=line[7]+line[8]+line[9];
for (int i = 0; i < str.length(); i++) { //一个可控制0~999的引脚,绝对够用
pin = pin * 10 + (str.charAt(i) - '0');
}
char mode=line[11];
switch (mode){
case 'L':
pinMode(pin, OUTPUT); //'L'命令为低电平,'H'命令为高电平
digitalWrite(pin,LOW);
break;
case 'H':
pinMode(pin, OUTPUT);
digitalWrite(pin,HIGH);
break;
}
return;
}else if (line.startsWith(";")){ //';'是注释符
return;
}else {
Serial.println("Unknown command");
}
}
然后是 command.pgm:
; command.pgm:
; 这是”command.pgm”,在刚刚Kernel的行处理函数(void processLine(String line))中,我们已经定义了分号(';')为注释符
pinset 001 L; ;1号引脚设为低电平
pinset 002 H; ;2号引脚设为高电平
pinset 009 L; ;9号引脚设为低电平
这段就是让 Kernel 执行 command.pgm 这个程序中的命令来控制引脚的模式。
由于我家里没有现成的 SD 模块,没办法实战。检查了代码,看起来应该没有问题。如果有问题的话请在评论区中指出。
是不是也很简单?我们还可以给它再嵌套亿层,大概理论如下:
Kernel 负责引导、控制、处理 OS(指处理 OS 所要进行的文件读写、计算、打印等),OS 来查找、处理程序(“查找”指的是按照文件后缀来选择合适的程序,“处理”指处理程序所要进行的文件读写、计算、打印等),程序来处理文件(指根据程序的功能来读写文件、识别文件内容等)。
完美无缺吧?
由于UP时间太少了,暑假还要上课,上完课还想玩原神,所以这里只写一个示例(图 1.1-1):
这张流程图是什么意思呢?
我们拿出亿些数据来举个栗子:
先说一说这个《图片》文件:
图片文件的第
1 行,即文件头:~IMG#2#2$其中
'~'表示这一行有数据;"IMG"表示这是一个图片文件;')'代表括号前的字符是系统定义符,如'\n';'#'是数据分割符,表示前后是两个数据且数据类型(指数据的种类和意义)不同;两个'2'分别表示长2 像素,宽2 像素;'$'表示该行结束且下一行与该行数据类型(指数据的种类和意义)不同。第
2 行,就是文件主数据:~255*255*255%0*0*0&不难看出,这是图片第一行的两个像素的颜色,按
R,G,B的顺序排列;其中'*'代表前一个数据与后一个数据是一组数据;'%'表示前一个(一组)数据和后一个(一组)数据的类型(指数据的种类和意义)相同;'&'表示该行结束,下一行与该行数据类型(指数据的种类和意义)相同。第
3 行与第2 行一样。第
4 行,"FED"表示文件结束。再来说说这个“程序”是如何处理文件的:
首先,程序读到了文件头
~IMG#2#2$,判断这是一个图片,分辨率为2\times2 。然后,程序读到了文件主数据,即
~255*255*255%0*0*0& \n ~127*127*127%155*155*155$,知道了这个图片文件的所有像素,那么程序现在就要向操作系统发出屏幕打印的请求了。按照文件给出的顺序和屏幕打印函数的格式,程序依次向操作系统发送了以下数据,直到读到"FED":pgm.print(lotx=0,loty=0,type=pt,r=255,g=255,b=255); pgm.print(lotx=0,loty=1,type=pt,r=0,g=0,b=0); pgm.print(lotx=1,loty=0,type=pt,r=127,g=127,b=127); pgm.print(lotx=1,loty=1,type=pt,r=155,g=155,b=155);我们规定,
"pgm.print"代表程序打印,即程序申请在允许的范围内打印;"lotx"和"loty"代表打印内容的坐标;"type"是打印内容类型,可以是"pt"(像素),也可以是"ic"(内置图标),或"tx"(文本)等等;后面的'r','g','b'是像素的颜色;如果要打印内置图标或文本的话,还要再后面加上icon=…或text=…。然后,OS 读到了程序的申请,于是处理了程序发来的数据,再发给 Kernel。
通过程序的申请,OS 知道了每个像素的相对程序原点的位置和颜色。而程序原点
x 位置就是屏幕原点的位置加上程序的左边框宽度,程序原点y 位置就是屏幕原点的位置加上程序的上边框宽度。我们现在的程序强制最大化,要不然处理起来比较麻烦。我们在这里规定:程序的左边框宽度2 像素,上边框宽10 像素,所以 OS 能得到图片所有像素的绝对位置为(2,10),(3,10),(2,11),(3,11) 。那么 OS 可以给 Kernel 发数据了。我们规定:向 Kernel 发送的屏幕打印指令如下:
Function_Name:scrprt Print something on the screen.This Command is sent to KERNEL. Usage: scrprt $(Value_Type) *(Value_Name) &(Value_Value) #(Value_Function); Value_Type:[Available,There MUST BE VALUE(S)], Allowed value(s): spt, sic, stx; Value_Name:[Disable,There must be "~NULL@)"]; Vaule_Value:[Available,There MUST BE A SET OF FORMATTED VALUES HERE], The format of the value(s): ~@^[pos_x]@^[pos_y]@^[R]@^[G]@^[B]@^[icon]@^[text]@) Note:The "@^[icon]" and "@^[text]" MUST BE OMITTED when not in use. Value_Function:[Disable,There must be "~NULL@)"];\~插叙始\~
如果不出意外的话(我是说不出意外啊!),以后向 Kernel 的发送指令都将采用以下格式:
(Function_Name) $(Value_Type) *(Value_Name) &(Value_Value) #(Value_Function);且
"Function_Name"必须是六个字符。多余的我就不解释了,应该懂得都懂,看不懂的可以把上面那段放到翻译软件里。
顺带提一下:因为程序的代码、OS 的代码和 Kernel 对 OS 的兼容层有亿点复杂,所以这些东西我们下篇再写吧。
说一下我写“屏幕打印指令说明”时为什么要用英文。
可以看出来,这个“说明”的格式很“规整”,那么也不难猜到,这个“说明”就是—— 要放在系统里的说明
我们可以看到,在现在的操作系统中,“终端”里的命令都有说明,打开你的终端,输入
exit /?(Windows)或apt --help(Linux)都能看到这些说明。那么,我们的操作系统的“终端”里的命令也要有说明。
而且,用 Arduino 在屏幕上打印中文是一件很难的事,所以,我们干脆用英文得了。
对了,你没听错。在我们的操作系统中,这些给 OS 用于发送给 Kernel 的命令,也可以直接用在“终端”里。
好了,多的不说,我们继续。
\~插叙终\~
现在,Kernel 接收到了 OS 的指令,那么 Kernel 如何把这些指令让 Arduino 执行呢?
很简单,直接对要给 Arduino 烧录的 Kernel 加几句话不就对了:
//Kernel(本段代码全部由UP所写,故有可能有错误,望谅解): #include <SPI.h> #include <U8g2_for_Adafruit_GFX.h> #include <Adafruit_GFX.h> #include <Adafruit_ST7735.h> {...} //其他 Adafruit_ST7735 tft = Adafruit_ST7735(9, 10, 8); U8G2_FOR_ADAFRUIT_GFX u8g2_for_adafruit_gfx; void setup(){ //初始化函数 {...} //一些其他代码 tft.initR(INITR_18GREENTAB); tft.fillScreen(ST7735_BLACK); u8g2_for_adafruit_gfx.begin(tft); {...} //一些其他代码 } void loop(){ //主函数 {...} //一些其他代码 readCommand(); {...} //一些其他代码 } {...} //一些其他代码 void readCommand(){ {...} //读取指令的其他代码,其中读到的指令的变量名为:"command",类型为String。 processCommand(command); {...} //其他代码 } void processCommand(String c){ //处理指令: String name=c.substr(0,6); c.erase(0,7); //删除像"scrprt "这类的字符串,注意此次删除了空格,后文也有这种情况,不再说明 switch (name){ case "scrprt": //读取数值: c.erase(0,1); //删除'$'字符 String type=c.substr(0,3); c.erase(0,4); //删除"spt "或"sic ","stx "字符串 c.erase(0,9); //删除"*~NULL@) " //处理屏幕打印核心部分,参考 ~@^[pos_x]@^[pos_y]@^[R]@^[G]@^[B]@^[icon]@^[text]@) int tmp=c.find_first_of(' '); String value=c.substr(0,tmp); int pos=1; //用来标记现在读的值的意义,如[pos_x]对应的值为1,[G]对应的值为4,如上 int px,py,r,g,b; String tx; //所有的值 int ic; //现在先不对内置图标做处理,放到以后再说 value.erase(0,1) //删除开头的'~' while (pos<8){ value.erase(0,2); //删除分隔符"@^" int p=c.find_first_of('@'); String tmp2=c.substr(0,p); c.erase(0,p); switch (pos){ case 1: int num=0; for (int i=0;i<tmp2.length();i++){ //我知道把"for"放进"case"有些啰嗦,但是基本不影响时间复杂度,甚至在"case 6"和"case 7"里还能省时间 num=num*10+(tmp2.charAt(i)-'0'); } px=num; pos++; break; case 2: int num=0; for (int i=0;i<tmp2.length();i++){ num=num*10+(tmp2.charAt(i)-'0'); } py=num; pos++; break; case 3: int num=0; for (int i=0;i<tmp2.length();i++){ num=num*10+(tmp2.charAt(i)-'0'); } r=num; pos++; break; case 4: int num=0; for (int i=0;i<tmp2.length();i++){ num=num*10+(tmp2.charAt(i)-'0'); } g=num; pos++; break; case 5: int num=0; for (int i=0;i<tmp2.length();i++){ num=num*10+(tmp2.charAt(i)-'0'); } b=num; pos++; break; case 6: ic=0; pos++; break; case 7: tx=tmp2; pos++; break; default: break; } } //判断并打印: if (type=="spt"){ tft.drawPixel(px,py,(r*256+g*8+b/8)); //画点,其中颜色的值的计算公式如下: //int COLOR=(int(RED)*256)+(int(GREEN)*8)+int(int(BULE)/8); //我也不知道为什么要这样算,很抽象的算法 } if (type=="stx"){ u8g2_for_adafruit_gfx.setFont(u8g2_font_timR08_tf);//设置英文字体为TimesNewRoman u8g2_for_adafruit_gfx.setCursor(px, py); //设置光标位置 u8g2_for_adafruit_gfx.setForegroundColor((r*256+g*8+b/8));//设置颜色 u8g2_for_adafruit_gfx.print(tx); //打印文本 } if (type=="sic"){ //现在不处理内置图标的打印任务 } break; //处理来自OS的打印请求的部分结束 case {...} //处理其他来自OS的请求 {...} } //最外面的"switch"结束 {...} // "processCommand"函数的其他内容 } //"processCommand"函数结束 {...} //Kernel的其他部分 //Kernel结束你看,这样我们就成功的做出了 Winter OS 的大概软件架构。
当然,我知道这里 Kernel 的处理“scrprt”的方式带过于慢了,所以后面会尽量会将参数改成二进制,但格式基本不变。
现在你知道文章前言里的“Winter OS的内容”为何要分“Winter Kernel”和“Winter System”了吧。
可以说,Winter Kernel 是真正执行命令的地方,程序则是发起命令的地方,而 Winter System 是 Winter Kernel 与程序之间的“转译层”,是将一个复杂命令转化成多条简单命令的地方。
我们举完例子了,相信大家都能理解。如果这部分有任何问题,请毫不保留的在评论区~
哇……软件架构是真的难写,我从开始写这篇文章到现在写到这里已经快一个月过去了。(这小段抱怨是我在本地写的时候抱怨的。我是先在本地写文章,然后存成 PDF 放在蓝奏云上,再复制到 B 站和你谷上,但是在你谷还要再整一遍 MarkDown 和 LaTeX。MarkDown 是真难整啊啊啊服了 QwQ)
终于能到下一段了啊啊啊啊啊啊啊啊……
2. 硬件
软件架构说完了,我们来简单聊聊硬件架构。
我自己想了半学期,觉得如果一个 Arduino 想要担起处理整个硬件需求的重担,那是肯定不行的。因为 Arduino 自身本来就没有那么多硬件,没有显卡,没有声卡,没有电源管理器,它除了有 CPU、内存和储存外,其他什么也没有,就连它自身的那点储存还只能用来存内核代码,放不了操作系统文件。
所以,硬件架构不可能只有一块 Arduino,而是把多个 Arduino 拼起来,组成一个计算机。
那么该如何“拼”呢?
来看图 1.2-0:
这张图很好的解释了我们的硬件架构,其中,立方体代表该部件至少需要一个 Arduino,连接线代表数据的传输方向。
这些东西我们放在下一篇文章,因为东西实在有亿点多。
其中我们得说一下电源控制器的结构,如图 1.2-1:
说明一下,这张电路图中的 GC1 和 GC2 分别是 Main_Controller(以后简称“MNCL”)和 Device_Manager(以后简称“DMR”),它们在此的作用是保持 Q3 的基极为高电平,以确保整个计算机通电, Q3 是控制整个计算机电源的重要控件,它的基极为高电平时,整个计算机才可能被通电。
那么,如何让 Q3 的基极由低电平变成高电平呢?
这里采用的方法是——按钮。用户按下 S2 后,来自 DC(前提是 SPDT1 的开关拨到上面)或 C1(前提是 SPDT1 的开关拨到下面)的电流会流入 Q3 基极,而且 Q3 集电极已经有来自 DC 或 C1 的电流了,所以 Q3 发射极有电流,然后 GC1(MNCL)和 GC2(DMR)被通电,DMR 被通电后干的第一件事就是给 Q3 基极持续通高电平,这样就能维持计算机的开机状态。如果想关机,那么 MNCL 给 DMR 一个信号,然后 DMR 停止 Q3 基极的高电平,整个计算机就断电了。
但是,有个问题:万一 DMR 有延迟怎么办?
很简单,在电路中加一个法拉电容,用几个三极管控制它,使它在 S2 按下时放电给 DMR ,直到放完电时锁止它,使它充电,直到下一次 S2 按下。
还有,DC 是充电器,C1 是内置电池,D6,D7,D8 是LED灯,用来指示当前电源模式,电源模式有两个,第一个是充电器只给计算机供电,不给电池供电,对应 SPDT1 拨到上面;第二个是充电器给计算机供电,又给电池供电,对应 SPDT1 拨到下面,但是,如果想真正更改电源模式,必须接入充电器。
完美啊~
好了,硬件架构部分我们先聊到这,其他的下一篇我们来详细说。
2.后记
这篇是我们“给单片机编操作系统”的开篇,别看这短短几页,它浓缩了我在校两个学期,至少一半对于这个“项目”的思考、学习、探索和内耗。就如前言中说的,我从 2022 年冬就有这个想法,我于 2023 年 6 月小学毕业,于 8 月升入初中,然后在 2023 年冬决定正式开始的搞这个东西,在纸上和脑子里构建了第一个框架,但是,过了半年,我发现当时想的方案和方向根本不可行,于是改版,就有了你们现在在前文“架构”里看到的东西。这个东西写出来基本不费时间和精力,但是要把它想出来,基本是比登天还难。自从我改版以来,就不断的看现代计算机的各种构造和设计,来触发我的灵感。要想想出来这种东西,有时候真的只能靠“灵感”了,我有时几天都想不出来的东西,一天早上骑车去学校时突然就想到了解决方案,就是你们看到的那个“向 Kernel 的发送指令的格式”,这东西是真的搞人啊,比喻一下,如果我想这些东西时,每想出来一个“灵感点”,就要薅掉一缕头发,那么我的头现在已经能当反光镜了。而且,这个“项目”只有我一人在搞,你们看到的封面图片、各种配色、图片的内容设计、代码和伪代码、流程图等等,除了有极少一部分的简弹代码由 AI 写,其他全是我写、想或者画的,这不仅考验一个人的思维,还有他的审美、文笔、耐心、专业知识等等,对了,还有打字速度和视力。我觉得我能去干这个事,只有一点:热爱。
最后,附几张我的“手稿”吧
好了,以上就是本篇的全部内容了,如果你对这个“项目”感兴趣,不妨在 B 站给我留下你的三连和关注,我们下一篇再见吧~