深漂两年,一个单片机小白转职Linux的曲折之路

为Linux打call

Posted by Jerry Chen on September 20, 2019

说起梦想,我肯定是最飘的那个!

所以一个好的规划是必不可少的,2019的目标就是从单片机跳到Linux。说来有些讽刺,10个月前我有了从单片机转Linux的想法,今天我却依然做着单片机的工作。虽说如此,生活还是充满希望的,我也不会就此放弃。

「1-路漫漫其修远兮」

2018年12月,我重金购买了人生第一块Linux开发板,也是第一次看到了120G的学习资料。我记得当时我定了一个计划,每天看两节视频,一个多月可以看完。于是每天晚上9点到11点就是我躲在公司看视频的时间了。

执行了一个星期我就发现太难受了,每晚回去玩一把王者荣耀时间就没了,不玩又感觉没拿首胜心里不爽。而领导们都很勤奋,加班到10点是常事。这些因素都给我造成了极大的干扰,于是看视频的频率被放到每天晚上一节,再到晚上不看了,而上班无任务时摸鱼看一节。那一段时间刚好是我接手M3的时间,慢慢的Linux学习就被搁置了。

「2-不撞南墙不死心」

到了4月份,我下载了一个前程无忧,疯狂投那些嵌入式Linux岗位,大概100多份。前一个星期只收到了1个面试。去了之后面试官告诉我嵌入式Linux业务不赚钱,来这边搞M3吧,机会大大的。我当时一万只草泥马在奔腾,回来就在简历上加上了一句“只想转嵌入式Linux岗位”。后来又有两家面试,一家物联网一家图像处理,但是由于种种原因我不能或者不想去。这些失败的经历让我确信经验比能力更重要。

我事先调查了深圳最热门的几家嵌入式Linux培训机构,去了我才发现居然是从C基础和Linux基础开始教的,中间还会穿插教M3,真没有必要交2万去学全程。所以我做了一个大胆的决定,辞职自学2个月Linux。

「3-山重水复疑无路」

两个月期间我开了个博客,坚持每天一篇日记,终于把学习视频看完了,完成了初步的Linux实战,并且有了自己的知识库。我以为我终于可以找到心仪的工作,可是遇到的第一个面试官就很委婉的告诉我他们需要更有经验的人。没办法,只有继续找下去。后来联系我的企业感觉做Linux的机会很少,我都不确定他们有没有Linux岗位,主业是电子烟、紧急逃生指示牌之类的。

我的老主管建议我先去一家。我相信梦想还是要有的,万一实现了呢!

「4-三更灯火五更鸡」

为什么要做Linux?

  • 代码上主要用C语言实现,学习和使用成本低。结构上拥有最优秀的分层架构,学习这一套就好,不用忍受框架风格各异的代码;
  • 原厂自带稳定的基本驱动,拥有极其便利的开发条件,网上也可以找到大量的例程(PS:嵌入式Linux学习成本高但实际使用比单片机还简单);
  • 单片机薄弱的计算能力和单线程执行极大限制了应用的能力。未来Linux配AI算力芯片是将成为嵌入式开发主流。

「5-正是男儿发奋时」

魔鬼操作,从裸机到仿Linux的简易改造法?

一、做一个自己的命令行

首先需要做到可显字符和部分特殊字符回显,比如输入a返回a,输入退格返回退格(显示上会删掉上一个字符)。收到字符(建议FIFO)进行拼包,收到回车才认为收到了完整的一帧。逐字节解析使用一个数组缓存有效字符,然后去掉帧前所有的空格。此时就可以进入帧解析,如果长度为0就直接返回,如果不为0就手动在全帧后面补充一个结束符’\0’。帧解析后面建议统一发送”# “。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void ConsoleService(void)
{
	执行一次:打印欢迎信息(xxx\r\n# );
	if(FIFO有数据)
	{
		while(从0位单字节读取,读到长度结束)
		{
			if(字符可显)
				输出:字符原返;字符加入到帧数组(长度允许的情况);
			if(需显特殊字符,比如退格)
				输出:字符原返;如果是退格就从帧数组中去掉最后一位(如果可去);
			if(快捷功能字符,比如上下键、Tab键) //上下键是两字符转义实现的
				执行特殊操作,如获取备份;
			if(字符是回车){ //认为收到一帧
				去掉帧前所有空格;
				执行特殊操作,如建立备份;
				输出:换行"\r\n";
				DoFrame(FrameBuf, FrameLen); //长度为0就返回,否则补'\0',进行解析
				输出:命令输入头"# ";
				清空FIFO长度;清空帧Buf和长度;
			}
		}
	}
}

为了实现最大支持64个字符串参数,需要定义64个指针指向以空格分隔后的下一个字符地址,特别注意多个空格连起来的情况。然后将所有的空格替换为结束字符’\0’,这样就可以确保每一个有效指针可以作为字符串参数传入,未用到的指针都指向NULL。建议给处理函数的传参是”int argc, char *argv[]”,根据第一个argv[0]的字符串执行不同的函数,命令别名也可以在这里实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void DoFrame(char *Str, int Len)
{
	int argc = 0;
	char *argv[64];
	如果Len为0就return,不为0就最后补一个'\0'(长度不允许就return);
	argc = GetSegNum(Str); //查找' '得到总段数,注意多空格连接的情况
	for(int i = 0; i < 64; i++)
		argv[i] = GetSegStartAddr(str, i); //查找' '关联段的地址,注意多空格连接,未用指向NULL
	/* 下面是处理示例 */
	// 建议help中提示可使用goto、custom0命令等;
	// custom0中实现别名的提示,比如“cpu reset”,cpu是custom0别名。
	if(0 == strcmp(argv[0], "help")) //help命令
		GoHelpCmd(argc, argv);
	else if(0 == strcmp(argv[0], "goto")) //goto命令
		GoGotoCmd(argc, argv);
	else if((0 == strcmp(argv[0], "custom0")) //自定义命令
		|| (0 == strcmp(argv[0], Aliases[0]))) //事先定义好别名数组
		GoCustom0Cmd(argc, argv);
}

如果要实现密码控制,需要在帧解析函数最上方就判断缓存数组里的值是否和密码一致,基于此对“通过变量”进行操作。如果“通过变量”为否,密码一致就将“通过变量”置起打印欢迎信息,始终返回。如果“通过变量”为真就跳过这一步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void ConsoleService(void)
{
	执行一次:打印欢迎信息(xxx\r\nPlease enter your password:\r\n# );
	...
}
void DoFrame(char *Str, int Len)
{
	if(!PassValid)
	{
		if(0 != strcmp(Str, PASSWORD)){
			PassValid = TRUE;
			打印错误信息(Wrong password!\r\n\r\nPlease enter your password:);
		}
		else{
			PassValid = TRUE;
			打印欢迎信息(Welcome Linuxer!\r\n);
		}
        return;
	}
	如果Len为0就return,不为0就最后补一个'\0'(长度不允许就return);
	...
}

前台功能占用控制台需要可抢占多任务实现,比如ucosii上设定整个命令行操作的实现作为一个任务,跳转的处理函数中间含有while(1)。

1
2
3
4
5
6
7
8
9
10
void ConsoleTask(void) //如使用任务,串口接收必须FIFO实现
{
	while(1){
	...
	if(0 == strcmp(argv[0], "cmd1")) //这部分在DoFrame函数里面
		GoCmd1(argc, argv); //继续在里面加入while(1)就实现了控制台前台抢占,接收必须FIFO实现
	...
	};
}
OSTaskCreate(ConsoleTask, (void *)0, &TaskStk[TaskStkSize-1], 10);
二、让我们手动管理内存

准备:如果你使用IAR,就用Link文件限制自动分配使用的ROM和RAM;如果是CCS,就用Cmd文件进行限制。

划分:规划一段空间作为手动管理空间,规划每页大小比如4K(为什么要分页?便于空间管理和加快执行。可类比遍历和二分法)。比如有128页,针对每页都建立一个结构体(建议链表串起来),结构体初始化页起始地址、最大可用连续空间4k、变量链表。变量链表中存放变量起始地址和大小。

申请:假设需要申请大小为n字节的空间;

  1. 首先从第1页开始查询该页最大可用连续空间是否满足要求;都不满足就返回错误;

  2. 满足条件NeedLen < NextNode->StartAddr - (CurrNode->StartAddr + CurrNode->Size)就在该页中查询第一个可以放下的位置并将信息加入到变量链表中(中插保证链表按照变量地址从小到大排序)。请注意特殊情况的处理;

  3. 然后遍历该页所有变量节点,根据后节点和当前节点的信息更新当前页最大可用连续空间;

  4. 最后返回上述分配的起始地址。

释放:假设释放地址为A的变量;

  1. 根据地址判断存在的页;
  2. 遍历该页的变量节点,找到该地址的节点并删除;
  3. 然后遍历该页所有变量节点,根据后节点和当前节点的信息更新当前页最大可用连续空间;

通过上面的方法可以实现最大大小为页空间的变量分配管理(不一定非要是变量分配,需要使用到空间的都可以使用这一类分配方法)。如果要实现跨页的大小,引入一个块管理,比如规划每块大小为128页。假设有16块,针对每块都建立一个结构体(建议链表),结构体初始化块起始地址、最大可用连续页数128、块内页链表。块内页链表中存放页起始地址和页个数。这样就可以实现最大大小为块大小的变量分配管理,当然实现起来并不轻松,各部分细节自行思考完善。

三、简单实现个文件系统

假设我有一个硬盘,认为根目录(/),在它的最上方分配128K+128字节的空间,前128字节存放类型’d’(文件夹)以及名称“”,后128k存放根目录下每个文件/文件夹的地址等。假设每个文件/文件夹固定使用128字节的大小,具体存放地址、文件类型(文件/文件夹)、删除标志;

当写入一个文件夹,就分配128k+128字节的空间,返回地址A。地址A的前128字节存放类型’d’以及名称xxx。后128k同样存放目录下的所有文件/文件夹的地址等。

当写入一个文件,就分配FileSize+128字节的空间,返回地址B。地址B的前128字节存放类型(’s’文本类型;’b’二进制类型)、文件长度、名称xxx。后FileSize空间存放文件的内容。

格式化:将硬盘前128字节写入类型’d’和名称“”,后128k空间清空;

写入1:写入一个”/test1.txt”;

首先分配test1.txt的空间+128字节,在返回的地址C前128字节写入’s’、长度FileSize、名称”test1.txt”;

然后在返回地址的后FileSize空间里写入内容;

最后在目录”/”上方偏移128字节的后128k空间里按照格式写入地址C,类型’s’。

写入2:写入一个”/demo/test2.bin”;

首先在”/”偏移128字节的后128k空间里遍历类型为’d’的文件夹,进入文件夹地址找到名称,对比名称”demo”,如果找不到就返回错误。

直到找到后,分配test2.bin的空间+128字节,在返回的地址D前128字节写入’b’、长度FileSize、名称”test2.bin;

然后在返回地址的后FileSize空间里写入内容;

最后在目录”/demo/”上方偏移128字节的后128k空间里按照格式写入地址D,类型’b’。

删除:删除文件”/test1.txt”;

在”/”偏移128字节的后128k空间里遍历类型为’s’的文件,进入文件地址找到名称,对比名称”test1.txt”,如果找不到就返回错误。找到后就把在”/”偏移128字节的后128k空间里对应行的标志改为已删除。

复制:复制文件”/demo/test2.bin”到”/output/my.bin”;

首先确认demo目录存在,test2.bin文件存在,output目录存在;

接着分配test2.bin的空间+128字节,在返回的地址E前128字节写入’b’、长度FileSize、名称”test2.bin;

然后在返回地址的后FileSize空间里copy来源的内容;

最后在output目录信息中新增test2.bin文件的信息。

链接:Link文件”/demo/test2.bin”到”/output/my.link”;

首先确认demo目录存在,test2.bin文件存在,output目录存在;

接着分配64+128字节,在返回的地址F前128字节写入’l’(链接类型)、长度0、名称”my.link;

然后在返回地址的后64字节空间里写入链接的文件地址;

最后在output目录信息中新增my.link文件的信息。

移动:移动文件”/demo/test2.bin”到”/output/test3.bin”;

首先确认demo目录存在,test2.bin文件存在,output目录存在;

然后判断demo目录地址和output目录地址是否相同:

如果相同就认为是重命名,则直接改前者目录中”test2.bin”目标的地址链接信息中的文件名为”test3.bin”;

如果不相同就先执行复制然后删除原文件。

四、写在最后

进入新公司的几点收获?

  1. 模块化的思路

    以前我觉得模块化就是传出一个模块控制指针,包含读、写、ioctrl、数据。

    来到公司我才发现还可以用宏定义独立出模块接口函数,比如模块中用到串口接收函数,就可以定义一个接口头文件写上#define MODULE_RX(id, buf, len) UartReceiveFiFo(id, buf, len),这样移植到其他工程只用改掉该接口文件宏定义后面的实现函数名即可。

  2. Iap跳转的思路

    了解到了一种万能的IAP跳转方法,不需要中断向量表偏移、不需要ram启动。直接关中断然后把中断向量表区域擦除再重新写入跳转的向量表,最后重启即可。

下一步的计划?

嵌入式Linux + 基于国产AI芯片的深度学习/强化学习。