用户故事 驰往:借镜观形,学以致用
你好,我是驰往,一个技术老菜鸟。很荣幸能被编辑小新邀请来写这篇用户故事。我看了其他大佬的用户故事,有资深技术大拿,也有刚接触(技术)的后起之秀。因此和编辑讨论以后,我决定结合自己的工作领域,从MCU工程师的视角和你聊一聊我学习这门课的收获。
不过,即便你不是MCU相关方向的同学,也可以听我聊一聊,说不定会有意外的启发。因为每个人的知识背景不同,有些词可能会有歧义,所以我先声明一下我在后面用到的术语。
MCU入门tips
先说下我的技术背景吧,我是一名MCU工程师(也就是我们常说的单片机),比较擅长的技术栈是Cortex-M核和RTOS(其实只是入行MCU很简单,掌握刚才说的技术就差不多了)。
Cortex-M核的知识是我在二线城市工作时摸鱼学的,主要参考了《Cortex-M3权威指南》这本书,算是一本MCU入门必读的基础图书。在我看来,想搞懂Cortex-M核最重要的就是理解它特有的硬件双栈切换。
而学习RTOS是出于工作需要,当时我主要学习的内容就是AliOS-Things的rhino内核,这个阿里也有些文档,结合文档学习也相对容易掌握。因为这个内核的任务调度是最简单的,核心仍然是栈切换,你同样可以参考《Cortex-M3权威指南》。如果你想更深入地学习RTOS,我比较推荐你选择一些流行的项目作为学习对象,比如freeRTOS、Zephyr。
前面提到的Cortex-M核和RTOS里,都需要我们理解栈的切换。也可以说不懂栈就相当于不入行。但现实情况是,不少同学对于栈的理解,只停留在“先进后出”这种书面定义上,并不知道其本质意义——保存上下文环境(调用者的下一条指令、参数等)。想要掌握这些,就要了解平台架构指令。
借镜观形
不过入行时间久了,我也能明显体会到这些技术门槛并不高,日常工作踩坑多了,无非也就是多积累一些如何提高性能降低功耗的思路,还有调试方面的经验。
想要寻求突破,有资本“拿更高的工资”,自然得理解更高级的内容,提高自己的技术深度,操作系统就成为了我的选择。一方面是深,细节要考虑很多;另一方面是涉及面广,下到驱动,上到应用都会涉及。
说起操作系统,学习的首选自然是Linux。但是现在的Linux内核源码太庞大了,感觉学不到头儿,很迷茫,这时恰好谢宝友老师在群里推荐LMOS的课,就加了进来(我作为伪Linux内核爱好者,在好几个群和LMOS、Alex都是群友)。就这样,我开启了Cosmos学习之旅。
开始是真的“蓝瘦”,因为我是比较憷X86的汇编的,也不懂x86架构。从MCU到CPU,复杂度简直是指数级别上升的。但是作为一个老菜鸟,没吃过猪肉还没见过“鸡”跑吗(如果CPU是猪,MCU就是更轻量的鸡)?
没错,我的学习方式就是用已掌握的知识来对比要学习的内容,我称之为“承上启下学习大法”。这样既可以让你不至于和新知一相见,就一脸茫然,又可以回过头检验之前掌握知识的深度。
比方说,GRUB部分类似于MCU的Bootloader,负责引导内核程序启动。当时我跟着课程讲解,并参考石墨上的分享,很容易就把系统启动起来了,也通过这个折腾的环节给自己建立了一点信心。
但是启动起来,归启动起来,里面很多跟x86相关的基础知识,对我来说还是很难学的。就拿相对重要的特权级切换来说吧,我就是用比较的视角来加深理解的。
对于Cortex-M核,特权级切换入口就是中断,同样我所了解到的x86平坦模式和长模式也是如此。只不过前者的中断只有特权模式,而X86的话要先判断CPL大小,只有它小于中断描述符的DPL,才能进入中断描述符里的段选择子所选的中断上下文中。
之后就是我学习过程中“啃”得最艰难的部分——内存了。后来我总结了一下,主要难在转换思路和理解具体代码这两部分。
先说转化思路。你可能觉得MMU(内存管理单元)无非就是把虚拟地址分成几段,每段都通过MMU对应的表项来找寻物理地址,没找到就进入缺页中断,然后更新各表项,建立虚拟地址到物理地址的映射。
单独来看这确实很简单,但是它改变了软件开发的思路,我们可以在用到特定内存的时候通过缺页中断从磁盘加载到物理内存,然后再进行MMU映射,相比于MCU在程序运行前物理地址就已经指定好的方式,这些延迟执行大大提高了程序的灵活性。
再聊聊操作系统里绕不开的伙伴系统和slab机制,这两个术语在开始看操作系统的时候就一直回荡在我的脑海中,但“只闻其声,不见其人”。这次我终于在Cosmos中抓住了它们,而且还是具体代码的形式。这部分学习技巧就是“Read The Source Code”。学完之后,感觉“轻舟已过万重山”。
进程调度的流程其实和RTOS的总体思路一样,一般比较简单的RTOS(rhino)的调度流程如下。
- 当前任务运行。
- 到达切换条件时,把当前任务的上下文保存到当前任务的栈。
- 把当前任务加入到就绪队列或者等待队列。
- 从就绪队列选择合适的进程作为当前任务。
- 出栈返回该任务上次运行的位置,继续往后运行。
Cosmos与rhino最大的不同,还是在用户空间与内核空间的切换和权限上,这里也是我学习的难点。
用户态进程和内核态进程之间的切换,首先是用户态进程通过中断切到内核态,同时用户栈保留用户空间的上下文然后切换到内核栈,接着内核栈压入用户栈的栈顶地址,最后完成内核态进程的切换。至于普通进程“独占用户空间,共享内核空间”,本质上是不同的用户态进程用的MMU映射表不同,但切到内核态会使用相同的内核态的MMU映射。至此,我的进程学完了,彻底松了一口气。
之后设备驱动我对比了之前自学的Linux设备驱动模型,文件系统对比了FatFs,还没总结出能分享出来的发现,就不一一展开了。
学以致用
既然学了,不用岂不亏了?虽然手头没有MPU相关任务,甚至没有系统相关的工作任务,但是可以把学到的思想用于其中,或者用到其他的项目组。
我们做的是音频相关的项目,我早就看我们现有的音频代码不顺眼了,也看驱动的接口不顺眼,所以在我负责移植的一个任务中,按Cosmos的驱动架构重写了一遍我们驱动的架构。然后汲取Linux的ASoC架构思想,在我们项目的一个分支上实现了一个简要版本的ASoC框架(嗯,重新梳理以后看起来顺眼多了)。
/* bsp audio.c */
unsigned char BSP_AUDIO_IN_Init(uint32_t AudioFreq, uint32_t BitRes, uint32_t ChnlNbr)
{
ts_id_printf("[BSP_AUDIO_IN_Init] freq:%d\n", AudioFreq);
Tsm_Cfg_Codec_Info_t *codec_info = tsm_cfg_codec_group_get_info();
pcm_param_t param = {0};
param.dir = PCM_AUDIO_DIR_IN;
param.anc_adc_map = codec_info->adc_mic_mux;
param.pcm_cfg.format = PCM_FORMAT_S32_LE;
param.pcm_cfg.channels = 1;
param.pcm_cfg.sample_rate = AudioFreq;
// param.pcm_cfg.gain = 0;
if (codec_hdl < 0) {
codec_hdl = snd_codec_open("tsing_codec_dai", CODEC_OPEN_FLAG_ANC_PATH);
if (codec_hdl < 0) {
ts_id_printf("fail to open snd codec\n");
return 1;
}
}
snd_codec_ioctl(codec_hdl, TSING_CODEC_IOCTL_CMD_SET_PCM_PARAM, (unsigned long)¶m);
}
/* snd_soc_codec.c */
int snd_codec_open(char *dai_name, int flag)
{
drivcallfun_t open_func;
for (int snd_hdl = 0; snd_hdl < SND_CODEC_MAX; snd_hdl++) {
if (snd_codec[snd_hdl].codec_dai && snd_codec[snd_hdl].codec_dai->dai_name &&
!strcmp(snd_codec[snd_hdl].codec_dai->dai_name, dai_name)) {
open_func = snd_codec[snd_hdl].codec_dai->ops[IOIF_CODE_OPEN];
if (open_func)
return open_func(&snd_codec[snd_hdl], (void *)flag);
return 0;
}
}
return -1;
}
int snd_codec_ioctl(int snd_hdl, unsigned int cmd, unsigned long arg)
{
int ret = 0;
void *context = snd_codec[snd_hdl].context;
drivcallfun_t ioctl_func;
ioctl_t param;
param.cmd = cmd;
param.arg = arg;
ioctl_func = snd_codec[snd_hdl].codec_dai->ops[IOIF_CODE_IOCTRL];
if (ioctl_func)
ret = ioctl_func(context, ¶m);
return ret;
}
/* drvfuncall.h */
#ifndef __DRV_CALLFUNC_H
#define __DRV_CALLFUNC_H
#define IOIF_CODE_OPEN 0
#define IOIF_CODE_CLOSE 1
#define IOIF_CODE_READ 2
#define IOIF_CODE_WRITE 3
#define IOIF_CODE_LSEEK 4
#define IOIF_CODE_IOCTRL 5
#define IOIF_CODE_DEV_START 6
#define IOIF_CODE_DEV_STOP 7
#define IOIF_CODE_SET_POWERSTUS 8
#define IOIF_CODE_ENUM_DEV 9
#define IOIF_CODE_FLUSH 10
#define IOIF_CODE_SHUTDOWN 11
#define IOIF_CODE_MAX 12
typedef struct {
unsigned int cmd;
unsigned long arg;
} ioctl_t;
typedef int (*drivcallfun_t)(void *context, void *param);
#endif
/* snd_tsing_codec.c */
static ioctl_func_talble_t ioctl_func_table[] = {
{TSING_CODEC_IOCTL_CMD_SET_PCM_PARAM, tsm_codec_set_pcm_param},
{TSING_CODEC_IOCTL_CMD_SET_GAIN, snd_set_codec_gain},
{TSING_CODEC_IOCTL_CMD_SET_ANC_PARAM, tsm_codec_set_anc_param},
};
static int find_ioctl_func_index(unsigned int cmd)
{
for (int i = 0; i < sizeof(ioctl_func_table); i++) {
if (ioctl_func_table[i].cmd == cmd)
return i;
}
return -1;
}
int tsing_codec_dai_ioctl(void *context, void *param)
{
ts_id_printf("[tsing_codec_dai_ioctl]\n");
ioctl_t *ctrl = (ioctl_t *)param;
int idx = find_ioctl_func_index(ctrl->cmd);
return ioctl_func_table[idx].ioctl_cmd_func(context, ctrl->arg);
}
int tsing_codec_dai_open(void *handle, void *param)
{
ts_id_printf("[tsing_codec_dai_open]\n");
snd_soc_codec_t *codec = (snd_soc_codec_t *)handle;
int flag = (int)param;
// init codec context
codec_context.pcm_cfg[0].format = PCM_FORMAT_S32_LE;
codec_context.pcm_cfg[0].sample_rate = 16000;
codec_context.pcm_cfg[1].format = PCM_FORMAT_S32_LE;
codec_context.pcm_cfg[1].sample_rate = 16000;
codec_context.anc_path = flag & 1;
codec->context = &codec_context;
return 0;
}
static drivcallfun_t tsing_codec_dai_ops[IOIF_CODE_MAX] = {
[IOIF_CODE_OPEN] = tsing_codec_dai_open,
[IOIF_CODE_IOCTRL] = tsing_codec_dai_ioctl,
[IOIF_CODE_DEV_START] = tsing_codec_dai_start,
[IOIF_CODE_DEV_STOP] = tsing_codec_dai_stop,
};
static codec_context_t codec_context = {
.dai.dai_name = "tsing_codec_dai",
.dai.ops = tsing_codec_dai_ops
};
还有一个例子,另一个项目的Linux应用组同事问了我这样一个问题:定义了一个非常大AI算法权重数组,然后在算法初始化的时候传参进去,为什么执行这个算法初始化程序的时候要用2秒这么长的时间?
这个问题激起了我的兴趣。经过我们讨论,结合在课程里学到的缺页中断的知识,做出了后面的分析——该数组的访问导致缺页中断,而缺页中断会在elf文件中该数组的偏移处读取数据,然后建立虚拟地址到物理地址的映射。这个过程耗费时间太长了。再加上该文件系统的硬件介质是nor-flash,很可能读取也慢。
经过调试我们得出了结论,正是nor-flash读取速率太低导致初始化比较慢。询问了他们项目驱动组的同事,果然是没优化这块,甩锅过去。现在我还能记起来当时帮忙参谋、一起破案的成就感,这就是学以致用的快乐。
写在最后
最后闲谈几句,聊点人生路上的“承上启下”。
人到中年,正是承上启下的年纪。上有老,下顾小,然才疏学浅,债负五车,于风雨飘摇的经济环境中,战战兢兢,如履薄冰。走过了轻松的前半生,还有未知的后半生。直面迷惘,是中年人的必修课,也是对家庭的责任。
只有行动起来,主动掌握更多的知识,才能更容易地承上启下,借助过往的经验沉淀来学习和运用新知识。比如你要用你所学的等比数列来计算提前还房贷合不合算,要用风险对冲的理念买份保险……
我想大部分读者也不是专门做操作系统的,但是我们都有一颗充满好奇的心,想一探究竟。于理想,操作系统是我的羁绊,我已断断续续地看了若干操作系统的相关文章;于现实,操作系统知识是不失业、甚至工资更上一层楼的必要条件。学完这门课,你会在所在行业更加沉稳,甚至有想进一步突破的冲动。
最后,感谢各位大佬耐心阅读,由于水平有限,如有疏漏欢迎在评论区指正。也祝我们都可以通过学习Cosmos,在自己的领域里有所突破。
- 艾恩凝 👍(3) 💬(1)
终于又来新的用户故事了!只有行动起来,拒绝做思想的巨人!我学完这个都过去一年半了,时间过得真快
2023-11-13