# nonos **Repository Path**: awander/nonos ## Basic Information - **Project Name**: nonos - **Description**: NONOS (读作 None-OS,意思是“不是操作系统的操作系统”) 是[awander](41204@qq.com)研发的一个简单的操作系统架构,可以在几乎所有资源非常有限的单片机上实现“非抢占式多任务”处理。 - **Primary Language**: C - **License**: GPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2022-02-25 - **Last Updated**: 2025-11-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # NONOS 操作系统 NONOS (读作 None-OS,意思是“不是操作系统的操作系统”) 是[awander](41204@qq.com)研发的一个简单的操作系统架构,可以在几乎所有资源非常有限的单片机上实现“非抢占式多任务”处理。 ## 概述 ### NONOS 的设计宗旨 * 尽量简单,提供给应用层的核心 API 接口只有不到 10 个 * 尽量少占 RAM,即使在只有 512字节 RAM 的 MCU 上也可以运行 * Footprint 非常小,核心代码编译后只占 4KB 左右 ROM 空间 * 不使用动态内存分配(malloc/free)(除非SDK本身就使用了动态内存管理),但却有动态内存分配的效果,避免内存碎片,保证长期运行的可靠性 * 非抢占式任务调度,不追求苛刻的实时性,但尽可能的实时 * 尽量屏蔽 MCU 的差异,强调在多种CPU上可移植 * 提供了很多通用 HAL 硬件驱动封装(可在多种 MCU 间共享代码) ### 为什么要搞 NONOS ? * 低不成:很多单片机 SDK 没有提供或移植任何操作系统,或因资源太少根本无法支持任何操作系统。这就导致系统开发非常困难,或无法实现复杂多功能应用,或程序架构非常混乱(比如很多人直接基于中断驱动,中断长时间无法返回,导致系统响应性非常差) * 高不就:很多高级 CPU 提供了非常复杂的多任务操作系统(比如 uCOS、FreeRTOS、RT-Thread、VxWorks等),如果没有一定的功底,很难快速掌握;即便掌握了,每个人实现一个功能也有很多种方案可选择(比如 ESP-IDF 中的中断延时处理,就可以使用Task、消息队列、RingBuffer、EventGroup、Semaphore、FreeRTOS-Timer、esp_timer...等等机制实现),从而导致程序的架构非常复杂,编程模式的一致性也很差! * 很多其它的号称“万能”的 IoT 操作系统,如 阿里AliOS、华为LiteOS 等,其实能适配的 MCU 非常有限(对于像 STM8L/C51/MSP430 这样的非常低端的 MCU,几乎无人能适配);即使号称适配了,很多功能也没有实现(比如 AliOS 只能兼容部分 STM32 芯片,并不提供关于 GPIO 等外设的封装) * 很多 MCU 厂家自己的几个不同系列、不同型号的 MCU SDK 都无法保证一致(如 STM 的 STM8S/STM8L/STM32),更不用说不同厂家的 MCU 了,这就导致 MCU 适配工作非常的艰巨!程序之间的可移植性非常差! * NONOS 的目的之一就是要在保留原生 SDK 的基础上,统一编程模型,简化程序结构,同时灵活适配各个不同厂家的低/中/高端 MCU ### NONOS 解决了如下主要问题 * 在任何裸机上支持非抢占式多任务处理 * 可运行在其它操作系统之上,简化操作系统的应用层复杂性,统一接口 * 无碎片的动态数据分配(数据保存在TCB(任务控制块)中),无需SDK提供动态内存管理(malloc/free) * 信号量机制 * 软定时器 * 中断延时处理,与中断机制结合,可以实现很好的实时性 * 消息队列:使得基于消息驱动的异步编程模式成为可能 * 接收数据异步处理:可实现非常好的并发通信处理能力 * 消息异步发送及重发机制 * 命名化GPIO接口封装机制:极大地增加程序可读性、可维护性、可移植性 * 常见的可移植的硬件驱动HAL接口:可便捷地实现在不同MCU及PCB之间移植和共享代码 * 一些常用的工具库、算法等 ### NONS 的核心原理 * 最底层基于 "Shift Block Queue(滑动块队列)"(自己开发的一种简单的基于块内存搬移的非线性队列);不需要MCU有任何上下文切换等多任务支持能力 * 把TCB(任务控制块)做为一种Block数据,放入Shift Block Queue,形成任务队列 * 多任务调度、定时任务、消息队列、动态内存分配等机制共用同一个滑动块队列,可非常高效地利用有限的 RAM * 基于主程序循环的队列任务处理,非常可靠 * 精巧利用 C 语言宏技术,实现GPIO的命名化 * 通过驱动分层封装,实现可移植的硬件驱动HAL接口 ### NONOS 的使用效果 * [深圳市智辰科技有限公司](https://www.szwistar.com)自 2017 年开始构建 NONOS 之后,已经陆续移植到了 STM8S/STM8L/STM32F0/STM32F1/MSP430/C51/ESP8266/ESP32/PHY62XX/FreqChip 等芯片上,并且兼容 裸机/FreeRTOS/Windows/Linux 等操作系统 * 在此基础上[深圳市智辰科技有限公司](https://www.szwistar.com)已经快速构建了上百个单片机应用程序,研发效率提升了 500% 以上,系统可维护性也大大提升 -------------------------------------------------------------------------------- ## 关于 FreeRTOS 接口 Events can be sent to a task using an intermediary object. Examples of such objects are queues, semaphores, mutexes and event groups. FreeRTOS 常用的几个接口: ### Critical Section 临界区机制 * FreeRTOS@ESP32 有几种层次的临界区: * 硬件级:硬件(仅可屏蔽的)中断开关: portDISABLE_INTERRUPTS() / portENABLE_INTERRUPTS() =实现=> taskDISABLE_INTERRUPTS() / taskENABLE_INTERRUPTS() * 任务级:可在任务执行过程中禁止任务切换: vTaskSuspendAll() / xTaskResumeAll() * 资源级: 仅针对指定资源进行保护: vTaskEnterCritical() / vTaskExitCritical() =实现=> portENTER_CRITICAL() / portEXIT_CRITICAL() =代替=> taskENTER_CRITICAL() / taskEXIT_CRITICAL() * NONOS 中的 osCritical 是针对单个资源级的 * 关于 vTaskEnterCritical() 可能导致的问题,请参见:[Critical Sections & Disabling Interrupts](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/api-guides/freertos-smp.html#critical-sections-disabling-interrupts) ### FreeRTOS::EventGroup <==> NONOS::Signal 多路复用机制 ### FreeRTOS::Task ### FreeRTOS::TaskNotify 多路复用机制(推荐) Task notifications are a method of sending an event directly to a task without the need for such an intermediary object. A notification sent to a task can optionally perform an action, such as update, overwrite or increment the task’s notification value. In that way task notifications can be used to send data to a task, or be used as light weight and fast binary or counting semaphores. A task can use xTaskNotifyWait() to [optionally] block to wait for a notification to be pending, or ulTaskNotifyTake() to [optionally] block to wait for its notification value to have a non-zero value. The task does not consume any CPU time while it is in the Blocked state. xTaskNotifyGive() is a macro that calls xTaskNotify() with the eAction parameter set to eIncrement - so pdPASS is always returned. * xTaskNotify() / xTaskNotifyFromISR() / xTaskNotifyGive() / vTaskNotifyGiveFromISR(): 提供 Notify * xTaskNotifyWait() / ulTaskNotifyTake(): 阻塞等待 Notify ### FreeRTOS::Queue * 发送:xQueueSendToBack()=xQueueSend() / xQueueSendToFront() / xQueueGenericSend() * 接收:xQueueReceive() / xQueuePeek() / xQueueGenericReceive() * ISR发送:xQueueSendToBackFromISR()=xQueueSendFromISR() / xQueueSendToFrontFromISR() / xQueueGenericSendFromISR() / xQueueGiveFromISR() * ISR接收:xQueueReceiveFromISR() / xQueuePeekFromISR() ### FreeRTOS::QueueSelect 多路复用机制(很少用) Standard FreeRTOS queues and semaphores can be added to the set using calls to xQueueAddToSet(). xQueueSelectFromSet() is then used to determine which, if any, of the queues or semaphores contained in the set is in a state where a queue read or semaphore take operation would be successful. * xQueueCreateSet() / xQueueAddToSet() / xQueueSelectFromSet() / xQueueRemoveFromSet() * xQueueSelectFromSetFromISR() ### FreeRTOS::Semaphore FreeRTOS 的 Semaphore(信号量) 又分为 4 种: Binary, Counting, Mutex(互斥量), RecursiveMutex, #### Binary / Counting: * 不带优先级继承,可以用作“纯同步”功能,即 take/give 可以分别在不同的任务/ISR中进行。如:可以持续在中断 ISR 中 give,在任务中 take * Counting 是有计数的信号量,有多少就可以 take 多少,take 完了再 take 就会阻塞;可以在另外一个任务中通过 give 补充 #### Mutex / RecursiveMutex: * 因为带优先级继承,不能在中断 ISR 中使用!必须在同一个任务中 take/give 成对使用,take 了之后必须 give 交还! * RecursiveMutex 可以 take 几次再 give 几次 * Mutex 可以用作实现 CriticalSection 机制 #### APIs * xSemaphoreGive() / xSemaphoreTake(): 适用于 Binary, Counting, Mutex * xSemaphoreGiveFromISR() / xSemaphoreTakeFromISR(): 适用于 Binary, Counting * xSemaphoreGiveRecursive() / xSemaphoreTakeRecursive(): 适用于 RecursiveMutex * uxSemaphoreGetCount(): 获取信号量的数量 -------------------------------------------------------------------------------- ## NONOS 任务管理机制(for ESP-IDF RTOS) NONOS 任务管理机制在 ESP32/8266 上除了程序入口 main()/app_main() 以外,其它的所有代码均由几种事件驱动: * Signal 简单信号驱动 * Timer 定时器驱动 * AppEvent 应用事件驱动 * DataIO 硬件设备收发数据驱动 * Task 单次执行的任务 * IRQ 硬件中断驱动 * SystemEvent 系统事件驱动 NONOS 任务管理机制在 ESP32/8266 上基于 ESP-IDF/FreeRTOS 系统架构进行了封装,应用程序一般不推荐直接使用 FreeRTOS 的多任务接口,以保持可移植性。 NONOS 任务管理机制在 ESP32/8266 上的关键实现见下表: | 多任务机制 | 关键实现原理 | 提供的关键应用接口 | | -------------- | ---------------------- | ---------------------------------- | | Signal 简单信号驱动 | * 底层使用 xEventGroupCreate() 创建 EventGroup 做为主信号表,并使用 xTaskCreate() 创建主信号循环处理任务。
* 硬件驱动(如在ISR中断中)或应用层可以通过 osTaskFireSignal()触发信号。
* 如果有信号被触发,则调用 osTaskRunOnSignal() 注册的消息处理回调函数处理。 | osTaskRunOnSignal()
osTaskFireSignal()
osTaskFireSignalFromISR() | | Timer 定时器驱动 | * 底层使用 esp_timer_create() 创建的 Timer 做为定时器,并使用 esp_timer_start_periodic()/esp_timer_start_once() 创建循环定时和单次定时。
* 应用层可以调用 osTaskRunEvery() 等接口注册定时回调函数。 | OSTaskRunEvery()
osTaskRunEvery()
OSTaskRunPeriodically()
osTaskRunPeriodically()
OSTaskRunAfter()
osTaskRunAfter()
OSTaskRunAt() | | AppEvent 应用事件驱动 | * 底层使用 xQueueCreate() 创建主消息队列,并使用 xTaskCreate() 创建主消息循环处理任务。
* 应用层可以调用 osTaskDispatchEvent() 接口发送应用层消息。
* 应用程序可以在 osInit() 时注册自己的事件处理回调函数。 | osTaskDispatchEvent()
osInit() | | DataIO 硬件设备收发数据驱动 | * 底层使用 "AppEvent 应用事件驱动" 中创建的主消息队列和主消息循环处理任务。
* 硬件驱动在收到数据后,使用 OSTaskCreateRxMessage() 将数据放入消息队列;主消息循环发现有收到数据,则调用 osTaskRunOnRxMessage() 注册的消息处理回调函数处理;
* 应用需要发送数据时,使用 OSTaskCreateTxMessage() 将要发送的数据放入队列;主消息循环发现有数据需要发送,则调用发送消息中指定的 sender 进行数据发送。
* 如果需要定时多次循环发送数据,则使用 OSTaskCreateTxMessageXn() 进行发送,它是由 "Timer 定时器驱动" 实现的,不使用消息队列。 | OSTaskRunOnRxMessage()
OSTaskAllocRxMessage()
OSTaskCreateRxMessage()
OSTaskAllocTxMessage()
OSTaskCreateTxMessage()
OSTaskCreateTxMessageXn() | | Task 单次执行的任务 | * 底层使用 "AppEvent 应用事件驱动" 中创建的主消息队列和主消息循环处理任务。
* 应用层需要执行任务(如中断延时调用)时可通过 osTaskRunLater() 生成一个任务消息,并发送到主消息队列的头部;主消息循环发现此消息后,就会调用它。 | osTaskRunLater() | | IRQ 硬件中断驱动 | * 底层在 bsp/isr_table.cpp 中为各个中断源提供了默认共享的中断服务程序(ISR)。
* 应用层如果需要响应某个 ISR,需要:
1)在相应的 ISR 函数中调用自己的 ISR 代码;
2)在 pcb_conf.c/pcb_start() 中注册此默认共享的 ISR (如 gpio_isr_handler_add());因为很多中断可能存在复用的情况,应用程序在此注册默认共享的 ISR。不要直接注册自己的 ISR,否则可能导致中断无法共享;也不要在别处注册 ISR,否则 ISR 代码分散在应用程序的各处,很管理和定位错误!
3)在硬件驱动中,使能硬件中断(如在 pcb_conf.c/pcb_init() 把 GPIO 口初始化为中断使能)。
* 在 ISR 代码中,可以:
1)(推荐)通过 osTaskFireSignalFromISR() 触发信号,从而驱动信号对应的回调函数;
2)通过 osTaskRunLater() 执行中断延时调用。
3)接收数据并通过 OSTaskCreateRxMessage() 将数据放入主消息队列。 | [bsp/isr_table.cpp](../bsp/isr_table.cpp)
[bsp/xxx/pcb_conf.c](../bsp/ESP-WROVER-KIT-V4.1/pcb_conf.c)/pcb_init()
[bsp/xxx/pcb_conf.c](../bsp/ESP-WROVER-KIT-V4.1/pcb_conf.c)/pcb_start()
osTaskFireSignalFromISR()
osTaskRunLater()
OSTaskCreateRxMessage() | | SystemEvent 系统事件驱动 | * 底层直接沿用 ESP_IDF 的 [Legacy Event Loop](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/api-reference/system/esp_event_legacy.html) 机制,未进行任何封装。
* 应用层可以提供自己的系统事件处理函数,并在 main.cpp/app_system_event_handler() 中进行调用。 | [main.cpp](../main.cpp)/app_system_event_handler() | -------------------------------------------------------------------------------- ## 关于延时/时钟/看门狗 -------------------------------------------------------------------------------- ## 关于 GPIO -------------------------------------------------------------------------------- ## 关于 PCB 与 BSP -------------------------------------------------------------------------------- ## 关于中断 中断处理是所有 MCU 中都要特别小心的地方,特别是在像 ESP32 这样多 CPU 并且使用抢占式多任务操作系统 FreeRTOS 的情况下,情况更是非常复杂! ### 在中断处理函数中不能干的事 * 不要 gpio_config() ### 关于 FreeRTOS 中 xxxFromISR 函数 -------------------------------------------------------------------------------- ## 关于数据收发处理中的几个术语 NONOS 在数据收发处理中,有几个层次的概念: * **sender(数据发送者)** 是指设备驱动层的,用于数据发送的 C++ 接口或 C 语言函数。它往往出现在待发送数据事件中,指示本数据应由哪个进行发送。 * **receiver(数据接收者)** 是指设备驱动层的,用于数据接收的 C++ 接口。它往往出现在接收数据事件中,指示本数据是由哪个接口接收到的。目的是当应用层在数据处理完毕后,可以通过这个接口发送回应 ACK 等。 * **processor(数据处理者)** 是指应用层的,用于处理接收到的数据的 C++ 接口或 C 语言函数。 ## 关于异步收发数据处理 NONOS 中支持异步数据接收和发送,下面说明如何正确使用它。 ### 异步接收数据 #### 1. 异步接收数据类型定义 * 异步接收数据处理需要使用消息队列,应用程序应先定义异步接收数据消息类型,如定义一个串口消息: > `#define APP_EVENT_RX_UART_CMD ((OSEventType)(OS_EVENT_RX_BASE + 0x01)))` #### 2. 设备驱动程序异步接收数据 * 对于时间很短的 IO,设备驱动程序可以在中断 ISR 中直接接收数据,然后放到一个固定的接收缓冲区中(如使用 u8 buf[]、ByteBuffer等)。 * 对于时间较长的 IO,设备驱动程序应该先用 osTaskRunOnSignal() 注册一个数据接收信号处理函数;然后在中断 ISR 中当有数据需要接收时,通过 osTaskFireSignal() 触发对应的数据接收函数;数据接收函数在中断外完成数据的接收处理。 #### 3. 设备驱动程序发布“接收数据”事件 * 设备驱动程序在收数据完成后,应通过 osTaskDispatchRxEvent() 把数据放入消息队列。 * **注意**:osTaskDispatchRxEvent() 会另外分配内存并拷贝数据到消息队列,所以在它返回之后,缓冲区中的数据即可清除。 #### 4. 应用层异步处理接收数据 * 应用层通过 osRunOnRxMessage() 注册自己关心的接收消息处理函数,当队列中有对应的消息时就会调用它。接收消息处理器原型为: > `typedef void (*OSRxMessageHandler)(OSEventRx *msg, void* cookie);` * **注意**:在接收消息处理函数中,当消息处理完成之后,必须返回 len 以告知调用都释放接收数据内存! * 至此,异步接收数据流程结束。 ### 异步发送数据(单次,使用消息队列实现) #### 1. 应用层异步发布“发送数据”事件 * 应用层通过 osTaskDispatchTxEvent() 把要发送的数据放入消息队列。osTaskDispatchTxEvent() 的原型为:
    esp_err_t osTaskDispatchEventTx(const OSTxMessageSender sender, const void* cookie, u8 len, const void* data);

    // 其中,sender 是驱动程序接口:
    void (*OSTxMessageSender)(void *cookie, u8* data, u8 len);
* **注意**:osTaskDispatchTxEvent() 会另外分配内存并拷贝数据到消息队列,所以在它返回之后,缓冲区中的数据即可清除。 #### 2. NONOS 在主消息循环中自动发送数据 * NONOS 在主消息循环中发现要发送的消息后,会自动调用 `((ICommIf*)sender)->send(data, len)` 发送数据。 * 在发送完成之后,NONOS 会自动释放发送数据内存。 ### 异步发送数据(多次,使用定时任务实现) NONOS 的多次异步数据发送功能可以很好地解决多次、重复发送数据的问题。 #### 1. 应用层创建定时多次发送任务 * 应用层通过 osTaskTxMessageXn() 创建定时多次发送任务。osTaskTxMessageXn() 的原型为: > `OSTimerTaskId osTaskTxMessageXn(const void* sender, u16 interval, u8 repeatCount, u8 len, const void* data, BOOL fireFirstNow);` * **注意**:osTaskTxMessageXn() 会另外分配内存并拷贝数据到消息队列,所以在它返回之后,缓冲区中的数据即可清除。 #### 2. NONOS 在定时任务中自动发送数据 * NONOS 在定时任务中,会自动调用 `((ICommIf*)sender)->send(data, len)` 发送数据。 * 在发送完成之后,NONOS 会自动释放发送数据内存。 ### 设备驱动程序自己实现异步发送 设备驱动程序实现异步数据发送时,因为不是在中断程序中运行,所以编程模式比较简单,用户可以不使用以上两种方式,而是自己实现。比如: * 可直接使用阻塞式发送,直到数据发送完成; * 如果发送时间很长,可以把数据放入驱动程序自己的发送数据缓冲区中(如使用 u8 buf[]、ByteBuffer等),以中断方式进行数据发送; -------------------------------------------------------------------------------- ## 关于 HAL 设备驱动 -------------------------------------------------------------------------------- ## 关于 C/C++ 混合编程模式 NONOS 的核心层、PCB层、BSP层、HAL层、协议层、应用层混合了 C/C++ 编程,大致如下: * 核心层:C 语言 * PCB层:C 语言 * BSP层:C 语言,但因要调用 HAL 层 C++ 代码,所以扩展名为 .cpp * HAL层:C/C++,设备抽象层驱动程序部分采用 C++ 接口,部分采用 C 接口 * 协议层:C/C++ * 应用层:C 语言,但因要调用 C++ 代码,所以扩展名为 .cpp ### 为什么要这么做? * HAL 硬件抽象层代码非常琐碎和繁杂,如果不使用 C++ 进行封装,会导致程序结构非常复杂,一是容易出错,二是代码不易共享,三是不易使用,四是不易维护 * 协议层涉及到协议接口的标准化和统一化,所以 C++ 是很好的选择 ### C++ 的使用原则 C++ 的特性非常多,使用起来有非常复杂的技巧和“花样”,这会导致:1、代码非常难以阅读;2、问题很难定位;3、大量代为行为不可知;... 所以,有必要对在 NONOS 中使用 C++ 编程的原则进行约定: * 禁止使用系统的 C++ 类库 * 禁止重载运算符 * 禁止使用多继承 * 少使用模板类 * 基本不使用动态对象创建 * 不要“玩花样”,不要图“奇技淫巧”! ### C++ 调用 C 代码 C++ 可以直接调用 C 代码,只是在引用 C 函数的声明时,要加上 extern "C"。 ### C 调用 C++ 代码 C 是不能直接调用 C++ 代码的,但可以通过“桥接/bridge”(或称“封装/wrapper”)实现,如:c_call_cpp_bridge 模块提供了核心层 C 代码调用 C++ 代码的桥接。 -------------------------------------------------------------------------------- ## 关于 OTA --------------------------------------------------------------------------------