Nuttx是一种实时操作系统,被用在PX4飞控上。国内关于nuttx的介绍很少,所以写这篇博客和大家分享,并且记录自己的学习历程。
关于Nuttx的配置方法在 https://www.youtube.com/channel/UC0QciIlcUnjJkL5yJJBmluw 。最开始的两篇视频里有,大家请先把这个视频的前五篇看一遍。否则真的一点看不懂我接下来要说的。
我这里从第三篇视频也就是第一个例程中开始讲开来。视频中用的是stm32C8T6,可是我手头只有stm32VET6的开发板,而且LED的引脚不同。所以为了亮灯,我们需要修改LED引脚定义。
这个视频有一点不好的地方是喜欢用makefile menuconfig,这种傻瓜式操作很方便,但是其实不利于代码的理解。视频中进入Application然后选择LED Example,所以我们去apps里面找到examples下的leds_main。打开文件过后在文件的底端发现了貌似主函数的leds_main,这个函数的作用就是创建一个线程,也就是那句task_create。然后创建的task的代码是led_daemon,这个线程的代码在文件的上方。虽然很多参数不知道什么用处的,但是闪烁灯的逻辑无非是亮灯等待一定时间然后灭灯。所以我们可以把循环的部分扣出来毛估估一下,猜个大概
for (; ; )
{
userled_set_t newset;
userled_set_t tmp;
//等待一定时间,通过递增到一个数字,亮灯,然后再从这个数字递减到0,灭灯
if (incrementing)
{
tmp = ledset;
do
{
tmp++;
newset = tmp & supported;
}
while (newset == ledset);
/* REVISIT: There are flaws in this logic. It would not work
* correctly if there were spaces in the supported mask.
*/
if (newset == 0)
{
incrementing = false;
continue;
}
}
else
{
/* REVISIT: There are flaws in this logic. It would not work
* correctly if there were spaces in the supported mask.
*/
if (ledset == 0)
{
incrementing = true;
continue;
}
tmp = ledset;
do
{
tmp--;
newset = tmp & supported;
}
while (newset == ledset);
}
ledset = newset;
printf("led_daemon: LED set 0x%02x\n", (unsigned int)ledset);
//设置亮灯还是灭灯
ret = ioctl(fd, ULEDIOC_SETALL, ledset);
if (ret < 0)
{
int errcode = errno;
printf("led_daemon: ERROR: ioctl(ULEDIOC_SUPPORTED) failed: %d\n",
errcode);
goto errout_with_fd;
}
usleep(500*1000L);
}
我原本以为前面创建的userled_set_t会是具体指向某个led,但是这条思路无果而终,这时我看到了ioctl,我一开始以为是打印点东西,后来注意到上面有printf了,不会这么矛盾吧,然后百度一下才发现有门道,这时linux用用户空间到设备驱动的接口,虽然nuttx不是linux系统,但是还是借鉴了posix规范。第一个参数是指向设备,第二个参数是命令,第三个参数是前面那个命令的参数。
而我们需要知道指向设备的的参数具体指向哪里,所以找到fd赋值的地方
fd = open(CONFIG_EXAMPLES_LEDS_DEVPATH, O_WRONLY);其中重要的参数是CONFIG_EXAMPLES_LEDS_DEVPATH
怎么做呢?当然是全局搜索关键词,我全局搜索的范围包括了git上面pull下来的三个工程(apps,nuttx,tools)。但是诡异的是搜索到另一个文件samv71-xult.h里面有定义这个宏定义之外没有别的地方用了这个宏定义。samv71-xult.h明显是另一个芯片的代码,虽然看里面的内容我们知道在这个芯片,CONFIG_EXAMPLES_LEDS_DEVPATH会指向/dev/userleds,但是我还是想找到在哪定义了这个宏定义。我突然想到之前在看makefile的时候,看到很多宏定义其实是拆开来定义的,什么意思?就是,CONFIG_EXAMPLES_LEDS_DEVPATH的定义方式是比如CONFIG_EXAMPLES_ + LEDS_DEVPATH。这么做的好处就是格式可以统一,而且可以一个个扫描地去定义,如果是SPI的例程,我估计就是CONFIG_EXAMPLES_ + SPI_DEVPATH。所以在这里我全局搜索了LEDS_DEVPATH。然后在apps/examples/leds/kconfig里面找到了
config EXAMPLES_LEDS_DEVPATH
string "LED device path"
default "/dev/userleds"
看来我上面猜错了不是LEDS_DEVPATH而是EXAMPLES_LEDS_DEVPATH是可变的。但是我还是找到了我想要的。
我不知道为什么在samv71-xult.h里面会再次定义这个变量,但是kconfig是在我们使用make menuconfig的时候被调用的,所以我比较确信是在这里定义了LED的设备定义。所以我们需要去找/dev/userleds。
找这个文件,我的第一反应是这应该是一个文件夹下面对应的一个文件,所以我在文件夹里面搜索了一下,没找到userleds这个文件,然后又搜索了一下dev,也没找到这个文件夹。然后只能在代码中全局搜索了一下,发现在NuttxPortingGuide.html里面有解释,Nuttx的文件系统是个伪文件系统,而且和一般的文件系统不一样,一般文件系统是伪文件去对应真实文件,但是nuttx里面是真实文件去对应伪文件,我也不知道这个是啥意思,但能明确的是按照目录名字去找/dev/userleds是找不通的。
查了大半天,结果什么都没有发现,自然是有点气馁。但是NuttxPortingGuide.html确实是个好东西,之前用VSCode看,没展开成网页形式,全是代码,就无视掉了,用网页打开后就舒服很多了。目录里面专门有一个led的模块,里面讲了这不是默认模块,所以不会写在核心驱动里面,但是还是按照核心驱动的模板写了这么一个模块。它有两个重要的头文件。一个是
我们一开始会使用./configure.sh stm32minimun/userleds来设置环境变量,所以找到这个文件夹,发现这个文件夹下面只有一个defconfig,打开这个文件搜索,只有一个跟LED相关的,就是CONFIG_ARCH_LEDS这个变量。然后在Makefile里面找到这个变量的用途
ifeq ($(CONFIG_ARCH_LEDS),y)
CSRCS += stm32_autoleds.c
else
CSRCS += stm32_userleds.c
endif
CSRCS经过大致看了下别的文件的使用方式,发现应该是C_SRCS的意思,就是具体.c文件的意思。在defconfig里面写了CONFIG_ARCH_LEDS is not set,所以明显是使用了stm32_userleds.c这个文件。然后打开这个文件发现这里面具体实现了上面在\nuttx\include\nuttx\board.h定义过的一堆函数。然后其中stm32_gpiowrite是不是很熟悉,然后这个函数的参数是GPIO_LED1,所以我就去找这个参数的具体定义,在include的一堆文件里面,我觉得定义最可能在stm32f103_minimum.h里面,打开后发现,我们梦寐以求的变量就在这里,我使用的板子的灯在B0口,所以改成
#define GPIO_LED1 (GPIO_OUTPUT|GPIO_CNF_OUTPP|GPIO_MODE_50MHz|\
GPIO_OUTPUT_CLEAR|GPIO_PORTB|GPIO_PIN0)
然后编译一下,烧进板子里。然后按照最开始提到的视频里面做一遍,然后就让灯闪烁起来了。
总结一下,大致猜测一下nuttx的流程。首先使用configure.sh stm32f103-minimum/userleds来指明这是个stm32f103-minimum最小系统,然后userleds里面包含了为了闪灯所需要定义的全局变量。然后make将\nuttx\include\nuttx\board.h作为头文件,stm32f103-minimum/src文件夹里面的文件作为c文件,完成灯的闪烁代码的编译。编译完过后,会被当成一个设备,放在dev下面,等待被配置。