从柴胡,目前从事物联网智能设备的开发和单片机及嵌入式驱动的开发工作,是RT-Thread和嵌入式技术爱好者。
写在前面:如何阅读和学习一个较大的工程,每个人都有不同的方法。我倾向于首先“不求甚解”地使用API,将整个工程跑起来,对该工程建立一个整体的认识,然后在使用中,从最感兴趣的模块入手,从上层开始,像剥洋葱一样,逐层分析。本文就是按照这样的方法,为研究一个小功能,从RT-Thread中bsp的串口开始,最终深入到RT-Thread的内核,初步探究内核中的基本元素——内核对象。由于作者手中只有stm32f103的开发板,所以本文在涉及到bsp的代码部分,都是指RT-Thread的github仓库中最新的bsp/stm32f10x内的代码。
使用RT-Thread的第一步就是通过ENV工具和scons来构建一个工程。我们可以看到在rtconfig.h中,RT-Thread通过一个宏RT_CONSOLE_DEVICE_NAME(在stm32f10x内,其默认值为“uart1”),就可以完成console所使用串口的设置。这个宏本质就是一个字符串,也就是串口的名字,将这个宏修改为“uart1”,console就会使用串口1。RT-Thread是如何通过串口的名字就可以实际在硬件上控制该串口了呢?
在stm32f10x的bsp中,main函数内的rtthread_startup()将会完成RT-Thread的初始化。根据代码,整理出与我们此次研究有关的代码层次结构图,如下图所示,在该图中,下级表示被上级调用的子函数,同级之间表示并列关系,即同级的函数都是被上级函数所调用的子函数。
根据上图,我们可以推断出在rt_hw_usart_init中,就完成了字符串(即串口名称)与硬件串口的绑定,所以在接下来调用rt_console_set_device时,就可以直接通过RT_CONSOLE_DEVICE_NAME使用该串口。
下面我们先分析rt_hw_usart_init,其中核心代码的调用结构如下图所示:
串口设备数据结构如下图所示。
从调用层次和串口设备的数据结构中可以发现,RT-Thread将串口封装成一个结构体,其名字(char *name,在stm32f10x中,uart1的名字为“uart1”)最终赋值给结构体子成员的rt_object parent的name数组中。
根据上述分析,当rt_hw_usart_init运行完毕后,串口设备就被注册至内核了。实际上,只是串口设备的“孙”成员(子成员的子成员)rt_object parent,被注册到了内核中。而所谓注册到内核,就是指内核将其地址存入一个链表中。为什么只需要注册其中一个成员呢?这里运用了一个C语言的小技巧,即结构体首个成员的地址就是该结构体的地址,所以当我们获取到了结构体首个成员的地址时,也就相当于我们获取到了该结构体的地址。
下面我们分析rt_console_set_device,其核心代码调用结构如下图所示:
其代码如下图所示
与我们之前的分析一致。rt_console_set_device中维护了一个全局变量_console_device,保存console所使用的串口设备句柄(即串口设备结构体指针)。通过rt_device_find遍历内核中相应的链表,匹配与传入的字符串名称(char *name,在stm32f10x中,默认传入为“uart1”)一致的对象,即可获取到相应的句柄,将其赋值给_console_device,然后console模块就可以通过_console_device控制和使用该串口了。
后记:C语言的对象化和分层思想并不是为了“炫技”和模仿C++,而是为了用最少的代码干最多的事儿,并且减少耦合,更利于大规模的多人开发。Linux、RT-Thread以及其他大的C语言项目中,都运用了这种思想。不论使用什么语言做程序设计,这种思想都是开发大工程的一种最优解(当然这句话在目前“函数式编程”蓬勃发展的情况下显得比较主观)。在阅读优秀的代码时,相比于读懂具体的代码实现,我认为弄清楚为什么要这样实现反而更重要。
添加微信13924608367 为好友,注明rt-thread,拉进RT-Thread微信交流群,与RT-Thread官方团队直接交流。
RT-Thread
构筑物联网产业的基石,让物联网终端的开发变得简单、快速,芯片的价值得到最大化发挥。
长按二维码,关注我们
微信扫一扫
关注该公众号