第一个NuttX应用程序:编写一个简单的LED闪烁任务

说实话,从裸机跳到RTOS,很多人第一反应是「我连点个灯都不会了?」。别急,点灯恰恰是最好的切入点。今天我们就用NuttX写一个LED闪烁任务,把任务创建、优先级、栈配置这几个核心概念一次性讲透。

1. 任务到底是什么?

在裸机时代,我们写个while(1)循环,里面放个延时、翻转IO口,完事。但在RTOS里,这个循环变成了一个「任务」。你可以把任务想象成一个独立的小程序,它有自己的栈、自己的优先级、自己的生命周期。

我刚开始接触RTOS时,总觉得任务和函数调用差不多。后来踩了个坑才明白——函数调用是「你叫我,我执行完就回去」,而任务是「我一直在,调度器决定我什么时候跑」。这个区别很关键。

2. 创建第一个任务:LED闪烁

先看代码,再解释。这是NuttX标准写法:

#include <nuttx/config.h>
#include <nuttx/arch.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <debug.h>

/* LED引脚定义,以STM32F4为例 */
#define LED_GPIO        GPIO_PIN_13
#define LED_PORT        GPIO_PORT_D

static int led_thread(int argc, char *argv[])
{
    /* 初始化GPIO */
    stm32_configgpio(LED_GPIO | GPIO_OUTPUT | GPIO_OUTPUT_SET);
    
    while(1)
    {
        /* 翻转LED */
        stm32_gpiowrite(LED_GPIO, 0);
        usleep(500000);  /* 500ms */
        
        stm32_gpiowrite(LED_GPIO, 1);
        usleep(500000);
    }
    
    return 0;  /* 实际上不会执行到这里 */
}

void led_app_main(void)
{
    int ret;
    pthread_t thread_id;
    pthread_attr_t attr;
    struct sched_param param;
    
    /* 配置任务属性 */
    pthread_attr_init(&attr);
    param.sched_priority = 100;  /* 优先级 */
    pthread_attr_setschedparam(&attr, &param);
    pthread_attr_setstacksize(&attr, 2048);  /* 栈大小 */
    
    /* 创建任务 */
    ret = pthread_create(&thread_id, &attr, 
                         (pthread_startroutine_t)led_thread, 
                         NULL);
    if(ret != 0)
    {
        _err("Failed to create LED thread: %d\n", errno);
    }
    
    /* 分离线程,让系统自动回收资源 */
    pthread_detach(thread_id);
}

嗯,这里要注意几个点。NuttX的任务创建用的是POSIX线程接口,也就是pthread。这和Linux上的写法几乎一样。如果你有Linux编程经验,上手会非常快。

3. 任务优先级:谁先跑?

优先级是个好东西,也是个坑。NuttX的优先级范围通常是0到255,数值越大优先级越高。0是空闲任务用的,一般应用任务用50到200之间。

优先级设计原则:

  • 实时性要求高的任务,优先级高(比如电机控制)
  • 计算量大但不紧急的任务,优先级低(比如日志输出)
  • 优先级不要超过200,给系统任务留空间

我曾经在一个项目里把两个任务都设成同一个优先级,结果发现它们轮流跑,但其中一个任务偶尔会卡住。排查了半天,原来是优先级相同导致调度器按时间片轮转,而那个任务里有个阻塞操作没处理好。从那以后,我习惯给关键任务留出至少10个优先级的差距。

4. 任务栈配置:给多少才够?

栈大小是新手最容易忽视的问题。给大了浪费内存,给小了一启动就崩。怎么估算?

任务类型 推荐栈大小 说明
简单LED闪烁 1024 ~ 2048 几乎没有局部变量
带简单协议处理 2048 ~ 4096 比如UART接收+解析
带文件操作或网络 4096 ~ 8192 文件系统、socket会消耗栈
复杂业务逻辑 8192以上 递归、大量局部变量

避坑指南:我曾经把一个任务的栈设成512字节,结果任务一启动就进HardFault。查了半天,发现是printf函数内部用了不少栈空间。记住:printf、sprintf这类函数是栈消耗大户,调试时尤其注意。

怎么确认栈够不够?NuttX提供了栈溢出检测机制。编译时加上CONFIG_STACK_COLORATION=y,运行后用pthread_stacksize或者up_check_tcbstack查看栈使用峰值。我习惯在产品定型前跑一遍所有功能,然后看栈使用量,留出30%的余量。

5. 任务的生命周期

任务创建后,会经历这几个状态:

  • 就绪(Ready):任务已创建,等待调度器分配CPU
  • 运行(Running):正在执行
  • 阻塞(Blocked):等待某个事件(比如延时、信号量)
  • 挂起(Suspended):被其他任务或自己暂停
  • 终止(Terminated):任务函数返回或被强制删除

我们的LED任务大部分时间在usleep里,处于阻塞状态。这时候CPU会去跑其他就绪的任务。这就是RTOS的魅力——CPU利用率最大化。

6. 编译和运行

把上面的代码放到apps/examples/led目录下,然后在menuconfig里启用:

Application Configuration → Examples → LED Example

编译后烧录,你会看到LED以500ms的间隔闪烁。如果没闪,先检查GPIO配置对不对。我遇到过好几次,明明是PD13,结果配成了PC13,灯死活不亮。

7. 核心知识体系

下面这张图总结了本章的核心逻辑:

NuttX任务创建核心流程 pthread_create() 任务优先级 sched_priority 任务栈配置 stacksize 任务函数 led_thread() 就绪 → 运行 阻塞(usleep) GPIO翻转 while(1) 无限循环 调度器切换

个人经验:刚开始学RTOS时,我总想把所有功能塞到一个任务里。后来发现,拆成多个小任务,每个任务只做一件事,调试起来轻松得多。LED闪烁这个例子虽然简单,但它体现了任务创建的全部要素。把这个搞透了,后面学信号量、消息队列就顺了。

最后说一句:别小看点灯。我见过不少工程师,RTOS用得很溜,但问起任务栈怎么配、优先级怎么设,答不上来。基础打牢,后面才能走远。


公众号:蓝海资料掘金营,微信deep3321