第一个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, ¶m);
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. 核心知识体系
下面这张图总结了本章的核心逻辑:
个人经验:刚开始学RTOS时,我总想把所有功能塞到一个任务里。后来发现,拆成多个小任务,每个任务只做一件事,调试起来轻松得多。LED闪烁这个例子虽然简单,但它体现了任务创建的全部要素。把这个搞透了,后面学信号量、消息队列就顺了。
最后说一句:别小看点灯。我见过不少工程师,RTOS用得很溜,但问起任务栈怎么配、优先级怎么设,答不上来。基础打牢,后面才能走远。
公众号:蓝海资料掘金营,微信deep3321