尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

NuttX嵌入式开发实战:从类Linux视角理解RTOS与设备驱动

NuttX嵌入式开发实战:从类Linux视角理解RTOS与设备驱动 1. “NuttX工程师”不是一个头衔而是一套工作方式第一次在一张完全陌生的板卡上刷入NuttX固件控制台跳出NSH提示符我随手敲下help滚出来一大串命令——ls、cat、free、ps、ifconfig、mount。那一瞬间我甚至怀疑自己是不是连错了串口、连到了某个残缺的Linux实例上。这种“既视感”不是错觉。NuttX从设计之初就把POSIX兼容当成了头等大事而不是像很多RTOS那样把POSIX当作一个可选的“兼容层”往后放。也正是因为这个定位NuttX在无人机、工业控制器、车载网关和物联网设备里越来越常见。PX4飞控固件就是跑在NuttX上的我当年就是从PX4的代码开始被迫认识它结果一发不可收拾后来几乎所有需要“正经操作系统”的嵌入式项目我都会优先考虑它。所谓“NuttX工程师”不是招聘网站上那种写进JD的头衔更像是一种工作方式。你要同时干几件事搭板级支持包、裁剪内核配置、写设备驱动、移植中间件、调多线程和实时性、排查栈溢出和内存泄漏偶尔还要帮应用工程师解释为什么他的进程模型在单片机上不成立。这套工作方式的核心不是“会用某个API”而是知道系统在什么配置下会变成什么样、挂在什么地方、怎么从日志和内存里把线索挖出来。这篇文章就按我日常做事的顺序把NuttX工程师真正会碰到的那些东西过一遍为什么选它、怎么把它跑起来、驱动怎么写、多任务现场怎么翻车、Linux应用怎么搬过来以及最终怎么从“会编译固件”进化到“能搞定一块新板卡”。中间会穿插我踩过的坑和验证过的做法给想入坑或者已经入坑的同行一个参考。2. 理解NuttX的第一把钥匙把RTOS当Linux看很多从裸机或FreeRTOS转过来的工程师第一次接触NuttX都会有一个共同的困惑为什么一个跑在MCU上的RTOS要弄得这么“重”答案藏在它的设计目标里。NuttX不只是想做一个调度器加信号量的集合它希望提供一个接近Linux的编程环境让那些已经非常成熟的POSIX软件生态可以低成本地落到嵌入式设备上。这个目标决定了它的一切。2.1 设备即文件这个抽象救了多少命NuttX的设备模型和Linux神似设备通过register_driver()注册成一个/dev/xxx节点应用层用open()、read()、write()、ioctl()就能操作它。这个设计看起来平平无奇实际工程价值非常大。我举一个真实场景裸机项目里驱动代码直接暴露函数接口应用层直接调用。如果你想在电脑上模拟测试应用逻辑要么把驱动编译进模拟器要么改一堆接口。但在NuttX里驱动就是文件描述符应用根本不知道后面是真实硬件还是模拟数据源——写一个虚拟设备应用代码一行都不用动。更重要的是调试方式变了。Linux工程师习惯用命令行去看系统和设备NuttX也把同一套思路搬了过来。板子跑起来之后你可以直接cat /dev/ttyS0、ls /dev甚至自己写一个小工具去ioctl某个设备。这种“系统感”是裸机环境完全给不了的。2.2 Flat、Protected、Kernel三种模式怎么选NuttX另一个容易被忽视的设计是运行模式。它支持三种内存保护级别模式内存保护典型场景代价FLAT无资源紧张的小MCU全部代码共享地址空间应用出错可能带崩系统PROTECTEDMPU实现用户/内核隔离中等MCU希望挡住应用乱写内核数据需要划分内存区域配置稍复杂KERNELMMU完整隔离带MMU的Cortex-A系列复杂度最高接近“嵌入式Linux”的体验大多数STM32、i.MX RT这类芯片跑的是FLAT模式。这个模式最简单但必须明白应用任务和内核跑在同一个地址空间里一个野指针就可能把整个系统打挂。所以FLAT模式下写应用心里始终要绷着一根弦——在这里没有“进程崩溃不要紧起一个新的就行”这回事。如果想用PROTECTED或KERNEL模式配置量会明显上升涉及到CONFIG_BUILD_PROTECTED、内存布局、特权级切换等。我的建议是项目初期先用FLAT把功能跑通把模式切换当成后期优化项否则一开始就陷进内存隔离的细节里很容易把整个项目拖垮。2.3 和FreeRTOS、Zephyr放在一起比每次聊NuttX必然会有人拿FreeRTOS和Zephyr来对比。我的看法是它们虽然都是“嵌入式操作系统”但设计哲学差得很远。FreeRTOS的定位是“尽量小的实时内核”它的API简洁、学习曲线平缓、生态极其庞大。但它的系统服务不成体系网络栈、文件系统、设备模型大多依赖厂商或第三方扩展项目一旦复杂起来到处都是要把各家代码缝在一起的粘合活。Zephyr走的是现代风格有设备树有模块化管理社区活跃度也很高。它的抽象层做得不错但稳定性在不同架构和不同版本之间差异比较大而且它对POSIX的支持主要面向应用层底层驱模型和Linux并不一致。NuttX的独特之处在于它对“类Linux”体验的坚持程度是三者中最高的。文件系统接口、Socket接口、pthread、消息队列、信号、共享内存这些都尽量按POSIX标准来。这意味着你的工程师团队里任何一个写过Linux应用的人都能快速上手写NuttX应用反过来你在NuttX上写的应用代码将来想迁移到Linux上改动成本也低得多。当然代价也有NuttX的配置项多到吓人Kconfig菜单层层嵌套初学者很容易迷失。这个问题后面专门讲。3. 从零拉起一块板子配置、编译、上电一线我见过太多人倒在“第一步”上源码克隆下来了工具链装好了结果编译出来的固件刷进去没反应。所以我把整个流程按我自己的习惯重新捋一遍每一步都讲清楚为什么这么做。3.1 源码结构和工具链NuttX的代码分布在两个仓库里apache/nuttx是内核与驱动主体apache/nuttx-apps是应用和工具集。两块缺一不可构建系统会把它们拼到同一个固件里。代码根目录有几个关键地方先记住它们arch/芯片架构相关代码ARM、RISC-V、Xtensa等都在这里boards/具体板卡的BSP工程启动代码、时钟配置、板级外设初始化都在这drivers/通用驱动框架和各类外设驱动include/nuttx/内核对外暴露的头文件apps/应用目录里面是按模块组织的应用源码工具链方面不同架构用的编译器不同。ARM用arm-none-eabi-gccRISC-V用riscv-none-elf-gcc如果目标平台是带Linux用户态的就不用看了那是另一条路线。工具链版本建议和NuttX官方文档或板卡README里推荐的保持一致版本太新有时反而会踩到编译选项不兼容的坑。3.2 configure.sh和Kconfig的真实工作方式NuttX沿用了Linux内核的Kconfig配置体系但比Linux多了一个defconfig的概念每块板卡的每种配置都有对应的默认配置文件放在boards/架构/厂商/板卡名/configs/下面。比如boards/arm/stm32/stm32f4discovery/ ├── configs/ │ ├── nsh/ │ │ └── defconfig │ ├── ostest/ │ │ └── defconfig │ └── ... ├── include/ ├── src/ └── ...选好配置后用脚本生成构建环境cd nuttx ./tools/configure.sh stm32f4discovery:nsh make menuconfigconfigure.sh会自动识别你当前是Linux还是Windows构建环境某些老版本可能需要显式加上-LLinux或-WWindows参数具体看./tools/configure.sh -h输出。make menuconfig这个步骤非常关键。它读取defconfig生成了一个名为.config的当前配置你可以在这里逐个打开或关掉功能项。我习惯在开发阶段至少确保以下几项是开的配置项作用CONFIG_DEBUG_ERROR/CONFIG_DEBUG_WARN/CONFIG_DEBUG_INFO内核调试日志分级输出CONFIG_ARCH_STACKDUMP任务崩溃时打印栈回溯CONFIG_STACK_CHECK周期性检查任务栈是否溢出CONFIG_SYSLOG日志输出通道通常指向串口或RAMCONFIG_FS_PROCFS提供/proc运行时查看系统信息这些选项不是性能最优的配法但开发期不开排障会非常痛苦。调试功能会带来少量CPU和内存开销可这换来的排查效率提升绝对值得。配置完成直接编译make -j$(nproc)生成的固件通常在nuttx根目录下名字就叫nuttx.bin或nuttxELF格式。烧录方式看具体板卡STM32可以用OpenOCD加st-flashi.MX RT可以用其专用烧写工具ESP32需要用esptool。烧进去之后串口上应该能看到NSH的启动日志和提示符。3.3 第一次看到NSH提示符之后该做什么很多人看到NSH提示符就以为大功告成其实真正的板级调试才刚刚开始。我每次点亮一块新板卡会按固定顺序敲这些命令确认系统健康uname -a确认系统版本、CPU架构、构建时间ps看当前有哪些线程在跑栈分配是否正常free看堆内存总量和剩余量判断内存是否紧张ls /dev确认外设驱动有没有注册成功mount确认根文件系统和各挂载点是否就绪如果free显示的内存跟预期差很多先检查配置里是不是打开了太多不必要的功能如果ls /dev少了某个节点说明对应驱动没注册要么是Kconfig没开要么是启动初始化没调用。我在这个阶段一定会做的一件事在NSH里执行helloapps里自带的示例程序。这个程序会创建一个任务、打印一句话、然后退出。它能验证最基础的任务创建、调度、标准输出链路是否正常。如果连hello都跑不起来先别急着调驱动调度器的问题排完再说。4. 写一个字符驱动顺便弄懂NuttX的设备模型设备驱动是NuttX工程师最常写的代码。我不讲复杂的网络驱动或USB驱动就从一个最简单的字符设备入手把整条链路打通。4.1 register_driver背后的设计意图NuttX的设备节点通过register_driver()创建它的核心参数是file_operations结构体。这个结构体里开放、关闭、读、写、ioctl、poll这些方法看起来和Linux的file_operations几乎一模一样这不是巧合而是刻意为之。它的好处在于驱动作者只需要实现自己需要的操作方法剩下的设为NULL即可。比如一个只读设备只需要实现.read一个控制类设备只实现.ioctl即可。框架层会帮你处理那些没实现的方法返回ENOTTY之类的合理错误。驱动注册时机也值得注意。NuttX提供了几个层面的初始化钩子最常用的是板级初始化函数board_app_initialize()它在系统启动早期、文件系统就绪后被调用。大多数自研驱动都在这里注册。4.2 一个假传感器驱动的完整代码下面这个例子创建一个虚拟传感器设备/dev/fakesensor每次读取返回一个递增的整数同时支持通过ioctl设置初值。代码故意保持最简但流程是完整的。#include nuttx/config.h #include nuttx/fs/fs.h #include nuttx/kmalloc.h #include errno.h #include debug.h #define FAKE_IOCTL_SET_VALUE 0x0001 struct fake_sensor_dev_s { int32_t value; }; static ssize_t fake_read(FAR struct file *filep, FAR char *buffer, size_t buflen) { FAR struct fake_sensor_dev_s *dev filep-f_priv; FAR int32_t *ptr (FAR int32_t *)buffer; if (buflen sizeof(int32_t)) { return -EINVAL; } *ptr dev-value; return sizeof(int32_t); } static int fake_ioctl(FAR struct file *filep, int cmd, unsigned long arg) { FAR struct fake_sensor_dev_s *dev filep-f_priv; switch (cmd) { case FAKE_IOCTL_SET_VALUE: dev-value (int32_t)arg; return OK; default: return -ENOTTY; } } static const struct file_operations g_fake_fops { .read fake_read, .ioctl fake_ioctl, }; int fake_sensor_register(void) { FAR struct fake_sensor_dev_s *dev; dev kmm_zalloc(sizeof(*dev)); if (dev NULL) { return -ENOMEM; } dev-value 42; return register_driver(/dev/fakesensor, g_fake_fops, 0666, dev); }这里有几个必须注意的点filep-f_priv保存了设备私有数据它是在register_driver()时传进去的第四个参数。驱动内部状态全部放这里不要用全局变量除非你确定这个设备只有一个实例。内存用kmm_zalloc分配它是NuttX内核堆的分配函数从系统堆里取内存。如果这个驱动在受保护模式下运行需要区分用户堆和内核堆FLAT模式下可以直接用。read回调返回的是实际读取的字节数返回0表示文件结束返回负值表示错误。这个约定和Linux完全一致写错会导致应用层行为异常。4.3 在NSH里验证这个驱动驱动注册函数写好了要把它挂到系统初始化流程里。最简单的办法是在board_app_initialize()里调用#ifdef CONFIG_EXAMPLES_FAKESENSOR ret fake_sensor_register(); if (ret 0) { _err(fake_sensor_register failed: %d\n, ret); } #endif然后在应用的Kconfig里定义一个开关对应的初始化代码只有在开关打开时才编译。这是NuttX工程里的标准套路避免无关驱动影响其他项目。验证方式很简单NSH命令行直接操作设备nsh ls /dev找到fakesensor节点后写一个小的测试任务调用readint main(int argc, FAR char *argv[]) { int fd open(/dev/fakesensor, O_RDONLY); int32_t v 0; ssize_t n; if (fd 0) { printf(open failed, errno%d\n, errno); return 1; } n read(fd, v, sizeof(v)); if (n sizeof(v)) { printf(sensor value %ld\n, (long)v); } close(fd); return 0; }如果看到输出42说明从驱动注册、设备节点创建、应用层打开、最终读到数据整条链路完全打通了。这个“最小闭环”验证法我强烈建议每个新驱动都走一遍。不要一上来就接真实硬件调先用逻辑设备把框架打通再替换成真正的硬件访问代码排障范围能小一大半。5. 多任务现场翻车NuttX工程师的排障三板斧驱动写完了系统就进入“多任务”阶段。真正把NuttX搞崩溃的往往不是驱动本身而是任务之间的协作问题。这一节讲的都是我或者身边同事真实踩过的坑。5.1 优先级反转NuttX如何把锅背走经典的优先级反转场景低优先级任务A持有互斥锁高优先级任务C在等锁此时中等优先级任务B抢占CPU导致C迟迟得不到运行。如果这个互斥锁还保护着某个Ethernet帧缓冲网络栈就会表现出一阵阵诡异的卡顿。NuttX的解决办法是优先级继承机制对应的配置项是CONFIG_PRIORITY_INHERITANCE。打开它之后当高优先级任务在等一把被低优先级任务持有的锁时系统会把低优先级任务的优先级临时提升到高优先级任务的级别让它尽快执行完并释放锁锁释放后再恢复原值。我见过不少项目为了省一点调度开销把这个功能关掉结果遇到网络和串口同时高负载时系统行为变得极不稳定。我的建议是除非你能明确证明优先级反转不会发生比如锁的持有时间极短且持有者优先级固定否则保持打开。这个开关带来的调度开销远比排查诡异卡顿的时间成本低。还有一个容易忽略的细节NuttX的互斥锁默认就有优先级继承但信号量sem_t没有。如果你用信号量做互斥需要自己处理优先级反转问题。所以互斥场景请始终使用pthread_mutex而不是裸信号量这是代码审查时我必查的一项。5.2 中断上下文别调延时这是铁律在中断服务程序里调用usleep()、sem_wait()、malloc()这类可能引发调度或阻塞的函数是嵌入式开发最经典的翻车姿势。NuttX在这方面和Linux一样严格但NuttX的错误容忍度更低——Linux里中断里做错事往往会打印警告然后凑合继续NuttX的某些设备驱动则会直接卡死或触发断言。正确做法是把中断里要处理的数据先放进缓冲区或队列设置一个标志然后唤醒一个专用于处理数据的线程。NuttX提供的work_queue机制就是干这个的。它把延后执行的工作挂到内核工作队列上由系统线程在普通上下文里执行安全性和实时性都有保障。我遇到过一个实际案例某外部中断驱动直接在ISR里调用了一个I2C读取函数而I2C控制器在慢速时钟下传输过程需要等待DMA完成。结果只要传感器触发频率一高系统就周期性地“冻结”几百毫秒。把I2C读取改成work queue方式后问题彻底消失。这类问题在代码审查里几乎看不出来必须靠对硬件时序和调度机制的理解才能定位。5.3 栈溢出与内存泄漏的经典症状栈溢出有两个典型症状一是任务运行一段时间后随机崩溃崩溃时的PC指针经常指向一些莫名其妙的地址二是开启CONFIG_STACK_CHECK后系统会周期性打印Stack overflow警告。这个警告出现时任务可能还能跑但栈已经踩到安全区域之外了必须立即处理。排查手段很简单ps命令会显示每个任务的栈大小和当前栈使用峰值。如果某个任务的峰值已经接近分配的栈大小果断把这个任务的栈开大——在FLAT模式下栈是从公共堆里分配的开大一些通常不会伤筋动骨。内存泄漏的定位更麻烦一些。NuttX没有像Linux那样的完整valgrind但有一些实用手段配置CONFIG_DEBUG_MM可以打印每次内存分配和释放的调用点在应用层定期记录free命令显示的空闲堆大小观察是否有持续下降趋势用CONFIG_SCHED_INSTRUMENTATION做任务级别的调度和内存跟踪我最常用的是第一种开发阶段打开CONFIG_DEBUG_MM配合串口日志哪个模块在持续申请内存不释放代码里一看便知。生产版本再关掉这个选项性能影响就没了。5.4 NSH、gdb和内核dump的搭配用法系统崩溃后第一件事不是急着看代码而是把崩溃现场的dump保存下来。NuttX在发生硬件异常时会打印当前任务名、栈回溯和寄存器值。这个信息量非常大顺着栈回溯基本能定位到是在哪个函数的哪一行触发的。有时候串口上只有一段残缺的dump看不出所以然这时就要上gdb。NuttX支持标准的gdb调试协议配合OpenOCD或J-Link可以做到arm-none-eabi-gdb nuttx target remote localhost:3333 monitor reset halt continue板子崩了之后用bt命令查看栈回溯效果远比手动分析dump输出直观。我一般会在开发板预留一个SWD调试口虽然平时用不到一旦出现难以复现的偶发崩溃这个口就是救命稻草。还有一个心得NSH的devmem命令可以读写任意内存地址排查硬件寄存器配置问题时非常好用但也是破坏力最大的工具之一一个地址写错就可能让整个系统重启。建议只在明确知道寄存器地址和位域含义的情况下使用。6. 把Linux应用挪到NuttX哪些红利能吃哪些要放弃很多团队选择NuttX的初衷就是想把成熟的Linux应用逻辑迁移到MCU上。但“接近Linux”不等于“就是Linux”两者之间有几道必须迈过的坎。这一节我把移植经验按优先级讲透。6.1 可以直接照搬的POSIX面先说好消息纯计算逻辑、数据结构、算法、文件IO、Socket网络编程、线程和同步处理这些代码在NuttX上几乎能原样编译运行。以网络为例NuttX支持完整的BSD Socket接口。下面这段TCP服务器核心代码在Linux和NuttX上基本不用改#include sys/socket.h #include netinet/in.h #include arpa/inet.h #include string.h #include unistd.h #include stdio.h #define PORT 8000 int main(int argc, FAR char *argv[]) { int listenfd socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in addr; memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_port htons(PORT); addr.sin_addr.s_addr INADDR_ANY; bind(listenfd, (struct sockaddr *)addr, sizeof(addr)); listen(listenfd, 5); for (;;) { int connfd accept(listenfd, NULL, NULL); const char msg[] hello from nuttx\n; send(connfd, msg, sizeof(msg), 0); close(connfd); } }前提是NuttX配置里打开了CONFIG_NET_TCP、CONFIG_NET_SOCKETS这些网络选项。打开之后这个程序的行为和Linux下的表现几乎一致——这正是NuttX选择POSIX路线最值钱的地方。6.2 第一个要改的东西进程模型Linux应用和NuttX应用最大的分水岭是进程模型。Linux下fork()可以轻松复制出一个进程NuttX的FLAT模式下没有进程概念只有任务task和线程thread。fork()在大多数NuttX配置下不可用或者行为与Linux完全不同。移植时首先要做的就是把所有依赖fork()的代码改成pthread_create()或NuttX的task_create()。换成线程之后原来进程间靠fork()天然隔离的内存就变成了共享内存需要自己加互斥锁保护。这是移植工程量最大的地方也是最容易引入隐藏bug的地方。另一个容易踩坑的是waitpid()。Linux下waitpid()可以等待任意子进程退出NuttX里如果配置了CONFIG_SCHED_WAITPID语义上也支持但某些老版本里对“等待任意子进程”的处理并不完整。移植时最好明确waitpid(pid)传具体的任务ID而不是传-1。6.3 网络与异步IO的移植实例移植TCP/UDP服务时最常遇到的问题不是Socket API本身而是select()和poll()。好消息是NuttX对这两个接口支持得相当完整select()可以同时监控多个socket的可读可写状态这正好弥补了“没有多进程”的短板——一个线程用select()管多路连接是NuttX下最优雅的并发模型。我在实际项目里移植过一个基于select()的Modbus TCP网关程序原代码写的是Linux版本。移植工作量主要集中在三处原Linux代码NuttX中的替代原因fork()处理并发连接select() 单线程循环NuttX无进程模型动态链接库dlopen()编译期静态链接FLAT模式不支持动态加载/proc文件读取改用CONFIG_FS_PROCFS导出的接口路径和字段略有差异改动不大最终程序跑起来后网络抓包对比Linux版本行为一致。这算是我个人近几年迁移体验最好的一次。6.4 移植边界与替代方案有一些Linux能力在NuttX上是“有条件可用”需要提前摸底。比如mmapNuttX部分配置支持mmap但主要面向文件映射和外设地址映射语义和Linux差异较大不要假设能用。动态链接FLAT模式下整个系统是一个固件镜像所有应用静态链接进内核。想动态加载可执行文件可以选择NXFLAT这种老机制但配置和工具链复杂度都比较高新项目建议直接放弃动态加载。shell脚本NSH支持基本的脚本语法但不支持完整bash语法。复杂脚本建议改成C程序里实现或者用简单的NSH脚本跑跑启动流程就行。总结下来移植前先做一次“资产普查”哪些代码是纯POSIX的哪些依赖了Linux特有机制。纯POSIX代码直接进依赖Linux特有机制的代码必须提前规划改写方案别等到联调阶段才发现。7. 成为像样的NuttX工程师我的最后几条心得文章快结束的时候我想把一些没法归类到具体章节、但确实影响工作质量的体会单独写一写。这些经验不来自某一份文档而是来自这几年在项目里反复折腾出来的。7.1 把配置项吃透比背API更值NuttX最劝退新人的地方是Kconfig配置项太多太杂。我在一次内部培训里数过顶层菜单项就有几十个子项加起来上千。但真正决定系统行为的核心配置其实就那一两百项。我的做法是把board目录下的defconfig当成“系统规格书”来读。每次拿到一块新板卡先花一个小时把它的defconfig从头到尾看一遍搞清楚这个板子默认开了哪些功能、每项配置为什么这样设。这块板子的设计意图、外设启用情况、调试手段全藏在这个文件里。这种方式比零散地翻文档高效得多。7.2 社区和代码库是最高效的老师NuttX的中文资料相比FreeRTOS稀缺得多很多问题去网上搜答案往往是空的。但它的源码结构非常清晰尤其是boards/和drivers/目录下每块板卡、每个驱动的实现都是活的文档。我遇到不理解的内核机制时习惯先找一个使用该机制的驱动或板子初始化代码通读一遍再回到内核头文件里看接口定义。这个“从示例倒推原理”的方法比直接啃源码快得多。Apache社区的邮件列表和GitHub Issues里也有很多值得翻的老帖子那些讨论里常常藏着文档里没写清楚的设计权衡。7.3 一个被忽略的习惯为你的板卡做基线回退每个项目到后期都会面临同一个问题配置越调越多系统越来越膨胀某天突然出现一个莫名的问题却想不起来是哪个配置改坏了。我从一个老同事那里学到的方法是给每块板卡建一个“最小可用配置”只包含能跑通基础功能的选项把它固化到版本控制里。平时调配置都是在它的基础上增量修改一旦系统行为异常退回基线配置用二分法定位是哪个配置项导致的。这个习惯在NuttX这种配置项繁多的系统里效率提升非常明显。至于新板卡选型我的建议是先查一下官方boards/目录里有没有同系列已经支持的板子。有同系列参考板从零适配的时间能从几周缩短到几天完全没有参考板的话从芯片原厂的SDK硬切到NuttX的工程量要按一个月起步来估算这个预期管理对项目排期非常重要。最后再分享一个小技巧每次适配完新板卡或者写完新驱动我会把“硬件连接、配置项、踩坑记录”整理成一份Markdown笔记放在板卡目录的doc/子目录里。NuttX的板卡代码更新迭代很快代码会过时但当时解决问题的思路不会。几个月后自己回来找答案或者接手的人来问问题这份笔记比任何代码注释都管用。
返回列表