从沙子到车辙(5.2):实时操作系统

5.2 实时操作系统

📚 本文内容摘自本人的开源书《从沙子到车辙 - 一个工程师的理解》

🔗 在线阅读/下载:from-sand-to-ruts

bash 复制代码
git clone https://github.com/Lularible/from-sand-to-ruts

⭐ 如果对您有帮助,欢迎 Star 支持,也欢迎通过 GitHub Issues 交流讨论。

离开了"一个人的部落"

裸机编程的世界是一个人的世界。你亲自调度一切,你亲自管理每一个寄存器。这种绝对的控制感让人上瘾------但也让人疲惫。

现在,想象你离开了这个部落,来到一个中型企业。

企业里有多个部门同时在运转。研发部必须在下午 3 点前交付图纸。质检部必须在每批产品下线后 10 分钟内完成抽检。安保部必须 24 小时不间断巡逻。

你不能像裸机编程那样"一个一个顺序做"------让研发部等质检部做完了再开始?不可能。

你需要一个管理者。它负责决定每一个时刻哪个任务获得 CPU,哪个任务挂起,哪个任务被抢占。

这个管理者就是 RTOS(Real-Time Operating System)内核


调度表:谁先跑,谁等着

RTOS 做的第一件事,就是把应用拆成多个任务(task),每个任务有自己的栈、自己的寄存器上下文、自己的优先级。你可以想象一张调度表:

复制代码
优先级 5 → 发动机扭矩控制 (100μs 周期,硬实时)
优先级 4 → ABS 轮速处理    (1ms 周期,硬实时)
优先级 3 → CAN 发送队列管理 (软实时)
优先级 2 → 诊断 UDS 响应   (软实时)
优先级 1 → 背景任务        (数据记录、状态监控)

这张表就是你的"企业组织架构图"。优先级越高的任务,对其时间约束的要求越严格------毫秒级的延迟是绝对不能接受的。

但光有优先级还不够。RTOS 的核心魔法是抢占


抢占:最高优先级的就绪任务总是赢

任何时候,最高优先级的就绪任务获得 CPU。这是 RTOS 调度的铁律。

场景是这样的:

  1. 低优先级的诊断任务 T2 正在跑。
  2. 突然,CAN 控制器的接收中断触发。ISR 把一帧底盘报文推进消息队列,唤醒高优先级的 ABS 任务 T4。
  3. ISR 返回时,调度器检查就绪队列------发现 T4 优先级高于 T2。
  4. 立即抢占。 T2 的寄存器上下文被保存到 T2 的栈里。T4 的上下文从 T4 的栈中恢复。T4 开始运行。

这个过程叫做零延迟抢占------高优先级任务从就绪到执行的时间 ≤ 调度器的上下文切换时间。在 Cortex-M4 上,这通常是几个微秒。

让我们放大看一次上下文切换到底发生了什么。


显微镜下的上下文切换

以 FreeRTOS on Cortex-M4 为例,一次完整上下文切换的时间线:

  1. SysTick 中断触发(或 PendSV 异常):12 cycles(中断 latency)。
  2. CPU 自动入栈 xPSR, PC, LR, R12, R3, R2, R1, R0:8 个字 = 8 个总线周期
  3. 中断向量表跳转到 PendSV_Handler。
  4. PendSV 手工入栈剩余的寄存器 R4-R11:8 个字 = 8 个总线周期
  5. 保存当前任务栈指针到 TCB(Task Control Block)。
  6. 选择最高优先级的就绪任务:读取就绪列表头指针(2-3 个总线周期)。
  7. 从新 TCB 恢复新任务栈指针。
  8. 出栈 R4-R11:8 个总线周期
  9. 异常返回:CPU 自动出栈 R0-R3, R12, LR, PC, xPSR。

总开销在 Cortex-M4@112MHz 下大约 1-2μs。

在 100μs 的控制周期内,这不到周期的 2%。你用一个周期 2% 的时间,换来了多任务并行的能力。这是极其划算的交易。


穿透 PendSV:上下文切换的真实汇编

FreeRTOS 在 Cortex-M 上使用 PendSV(可挂起的系统服务) 异常来实现上下文切换。PendSV 的优先级被设为最低------这确保了上下文切换永远不会打断正在运行的 ISR。当 SysTick 中断触发并发现有更高优先级的任务就绪时,它不直接切换------它只是挂起 PendSV 异常。SysTick ISR 退出后,CPU 在退出中断上下文回归任务上下文之前,才执行 PendSV。

这是 FreeRTOS 的 PendSV_Handler 汇编代码(简化但保留核心逻辑):

asm 复制代码
PendSV_Handler:
    /* 检查是否从异常中返回(CONTROL.FPCA等标志) */
    MRS  R0, PSP              ; 获取当前任务的进程栈指针
    CBZ  R0, no_save          ; 如果是首次运行,没有旧上下文

    /* 手工保存浮点寄存器(如果使用了FPU) */
    TST  R14, #0x10           ; 检查 EXC_RETURN 的 bit4
    IT   EQ
    VSTMDBeq R0!, {S16-S31}   ; 保存 FPU 高半区

    /* 保存整数寄存器 R4-R11 */
    STMDB R0!, {R4-R11}       ; 寄存器组入栈,R0递减

    /* 保存当前任务栈指针到TCB(通过R3间接) */
    LDR  R1, =pxCurrentTCB
    LDR  R1, [R1]             ; R1 = 当前TCB指针
    STR  R0, [R1]             ; 写回新栈顶到TCB

no_save:
    /* 调用调度器选下一个任务 */
    PUSH {R14}
    BL   vTaskSwitchContext
    POP  {R14}

    /* 加载新任务的栈指针 */
    LDR  R1, =pxCurrentTCB
    LDR  R1, [R1]             ; R1 = 新TCB指针
    LDR  R0, [R1]             ; R0 = 新任务的栈顶

    /* 恢复整数寄存器 R4-R11 */
    LDMIA R0!, {R4-R11}       ; 出栈,R0递增

    /* 恢复浮点寄存器(如果使用了FPU) */
    TST  R14, #0x10
    IT   EQ
    VLDMIAeq R0!, {S16-S31}

    /* 更新PSP为新栈指针,异常返回 */
    MSR  PSP, R0
    BX   R14                   ; 异常返回------CPU自动弹出R0-R3,R12,LR,PC,xPSR

这段汇编里有几个精妙的设计:

  1. FB CBZ R0, no_save:如果 PSP 为零,说明这是任务第一次被调度------没有旧上下文需要保存。直接跳到选新任务的逻辑。这省了一半的入栈操作。
  2. VB VSTMDBeq/VLDMIAeq :浮点寄存器的保存是懒加载------只有在当前任务确实用过 FPU 时才保存。EXC_RETURN 的 bit4 由硬件自动维护------如果任务执行过浮点指令,bit4=0;否则 bit4=1。不需要软件维护 FPU 使用标志。
  3. VB 异常返回的处理BX R14 使用的 EXC_RETURN 值自动告诉 CPU"返回后使用 PSP(进程栈指针)还是 MSP(主栈指针)"。任务上下文总是在 PSP 上。中断上下文总是在 MSP 上。这个硬件特性让 FreeRTOS 只需要管理一个 PSP。

物理连接:上下文切换的时间花在哪

上下文切换的 1-2μs 开销中,大部分时间花在总线周期上------出栈和入栈寄存器对 SRAM 的读写。 Cortex-M4 的 AHB 总线在 112MHz 下每个总线周期约 9ns。16 个字的入栈 + 16 个字的出栈 = 32 个总线周期 = 约 288ns。加上中断延迟(12 cycles = 约 107ns)、TCB 读写(几个总线周期)、就绪列表操作(查找最高优先级 = O(1) on Cortex-M with CLZ 指令)------加起来就是 1-2μs。

这里的每一个纳秒都是物理世界的约束。SRAM 的访问延迟不是无限小的------电容充电、信号传播、灵敏放大器稳定输出------每一个纳秒都是晶体管开关和金属连线 RC 延迟的总和。你在做操作系统设计,但你的约束来自 CMOS 物理。


创建任务:FreeRTOS 的任务生命

你不是自己管理上下文切换。FreeRTOS 帮你管。你只需要告诉它"创建这几个任务":

c 复制代码
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"

/* 任务句柄 */
TaskHandle_t xTorqueTask;
TaskHandle_t xCANProcessTask;
TaskHandle_t xDiagTask;
TaskHandle_t xBackgroundTask;

/* 同步对象 */
QueueHandle_t xCANRxQueue;
SemaphoreHandle_t xDTCSemaphore;

/* 任务栈------裸机时代你担心栈溢出,现在FreeRTOS帮你监控 */
static StackType_t torque_task_stack[512];
static StackType_t can_task_stack[1024];
static StackType_t diag_task_stack[512];
static StackType_t bg_task_stack[256];

static StaticTask_t torque_task_tcb;
static StaticTask_t can_task_tcb;
static StaticTask_t diag_task_tcb;
static StaticTask_t bg_task_tcb;

void app_create_tasks(void)
{
    /* 创建消息队列------CAN ISR → CAN处理任务 */
    xCANRxQueue = xQueueCreate(32, sizeof(can_frame_t));

    /* 创建信号量------DTC更新互斥 */
    xDTCSemaphore = xSemaphoreCreateMutex();

    /* 创建任务------最高优先级(5): 扭矩控制,100μs周期 */
    xTorqueTask = xTaskCreateStatic(
        vTorqueControlTask,
        "Torque",
        torque_task_stack, 512,
        NULL,
        tskIDLE_PRIORITY + 5,
        torque_task_stack,
        &torque_task_tcb);

    /* 优先级(4): CAN报文处理,由队列唤醒 */
    xCANProcessTask = xTaskCreateStatic(
        vCANProcessTask,
        "CANProc",
        can_task_stack, 1024,
        NULL,
        tskIDLE_PRIORITY + 4,
        can_task_stack,
        &can_task_tcb);

    /* 优先级(2): 诊断任务,由UDS请求唤醒 */
    xDiagTask = xTaskCreateStatic(
        vDiagTask,
        "Diag",
        diag_task_stack, 512,
        NULL,
        tskIDLE_PRIORITY + 2,
        diag_task_stack,
        &diag_task_tcb);

    /* 优先级(1): 后台任务------最低优先级 */
    xBackgroundTask = xTaskCreateStatic(
        vBackgroundTask,
        "BG",
        bg_task_stack, 256,
        NULL,
        tskIDLE_PRIORITY + 1,
        bg_task_stack,
        &bg_task_tcb);

    vTaskStartScheduler();
    /* 调度器启动后,这行代码永远不会执行到 */
}

从 ISR 到任务:CAN 处理的完整 FreeRTOS 流程

你看到了在裸机章节中 CAN ISR 直接操作环形缓冲。在 FreeRTOS 下,ISR 和任务之间的通信通过消息队列解耦:

c 复制代码
/* CAN 接收中断 ------ 只在 ISR 中做最小工作量 */
void CAN1_RX0_IRQHandler(void)
{
    can_frame_t frame;
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    /* 从硬件FIFO读出一帧------越快越好 */
    frame.id  = (CAN1->sFIFOMailBox[0].RIR >> 21);
    frame.dlc = CAN1->sFIFOMailBox[0].RDTR & 0x0F;
    frame.data[0] = CAN1->sFIFOMailBox[0].RDLR & 0xFF;
    frame.data[1] = (CAN1->sFIFOMailBox[0].RDLR >> 8) & 0xFF;
    frame.data[2] = (CAN1->sFIFOMailBox[0].RDLR >> 16) & 0xFF;
    frame.data[3] = (CAN1->sFIFOMailBox[0].RDLR >> 24) & 0xFF;
    frame.data[4] = CAN1->sFIFOMailBox[0].RDHR & 0xFF;
    frame.data[5] = (CAN1->sFIFOMailBox[0].RDHR >> 8) & 0xFF;
    frame.data[6] = (CAN1->sFIFOMailBox[0].RDHR >> 16) & 0xFF;
    frame.data[7] = (CAN1->sFIFOMailBox[0].RDHR >> 24) & 0xFF;

    CAN1->RF0R |= CAN_RF0R_RFOM0;

    /* 把帧推进队列 ------ 唤醒等待该队列的CAN处理任务 */
    xQueueSendFromISR(xCANRxQueue, &frame, &xHigherPriorityTaskWoken);

    /* 如果队列发送唤醒了一个更高优先级的任务,
     * 请求在ISR退出后立即进行上下文切换 */
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

/* CAN 处理任务 ------ 在任务上下文中处理 */
void vCANProcessTask(void *pvParameters)
{
    can_frame_t frame;

    while (1) {
        /* 阻塞等待队列中的数据 ------ ISR把帧推进来后立即唤醒 */
        if (xQueueReceive(xCANRxQueue, &frame, portMAX_DELAY) == pdTRUE) {
            switch (frame.id) {
            case 0x100: /* ABS轮速 */
                process_abs_wheel_speed(&frame);
                break;
            case 0x200: /* 发动机转速 */
                process_engine_speed(&frame);
                break;
            case 0x300: /* 进气温度 */
                process_intake_temp(&frame);
                break;
            default:
                break;
            }
        }
    }
}

ISR 的延迟被降到最低------只把帧推进队列然后立即退出。任务处理在任务上下文中执行------可以被抢占,可以阻塞在别的队列上,可以使用浮点运算而不用担心 FPU 上下文保存(FreeRTOS 自动管理)。


调度理论:Rate Monotonic Scheduling

你给任务分配优先级,不能凭感觉。速率单调调度(Rate Monotonic Scheduling, RMS) 是实时系统调度理论的基石。

RMS 的规则极其简单:周期越短的任务,优先级越高。 不根据"重要性"------根据"频率"。理由很直接:周期短的任务 deadline 也短,必须优先调度。

但你给所有任务都分配了优先级,总得确保它们都能在截止时间前完成。这里有一个可调度性测试------利用率上界

对于 n 个周期任务,如果它们的 CPU 利用率总和 U 满足:

复制代码
U = Σ(Ci/Ti) ≤ n × (2^(1/n) - 1)

其中 Ci 是任务 i 的最坏执行时间,Ti 是它的周期。那么这组任务在 RMS 下是一定可调度的------不管它们的相对相位关系如何。

这个公式的渐近值:当 n → ∞ 时,上界趋近于 ln(2) ≈ 0.693。也就是说,在 RMS 下,CPU 利用率只要不超过 69.3%,任务集就一定可调度。这不是"大概可以"------是数学定理。

回到你的发动机 ECU。扭矩控制 100μs 周期、50μs 执行(利用率 0.5)。ABS 轮速 1ms 周期、200μs 执行(利用率 0.2)。CAN 处理 5ms 周期、1ms 执行(利用率 0.2)。总利用率 0.9 > 0.693------RMS 可调度性定理不能保证。你需要更精确的可调度性分析------或者用 EDF(最早截止时间优先)调度,它在利用率 ≤ 1.0 时保证可调度------但 EDF 在过载时的行为不稳定,在汽车 ECU 中很少使用。

RMS 是你在裸机时直觉做的事------"最重要的任务跑得最频繁"------但它给了你这个直觉一个数学上严格的形式化。


RTOS 不是免费的午餐------三个经典的"炸膛"场景

调度器是你最忠实的助手,也可以是你最危险的定时炸弹。你迟早会遇到下面这三个问题。

场景一:优先级反转

这是 RTOS 世界最臭名昭著的 bug。想象一个具体的时间线:

复制代码
t=0ms:   T3 (低优先级, prio=2) 开始执行。
t=1ms:   T3 获取 Mutex M(保护DTC记录表)。T3 仍在执行。
t=2ms:   T1 (高优先级, prio=5) 就绪。抢占 T3。T1 开始执行。
t=3ms:   T1 尝试获取 Mutex M。发现已被 T3 持有。T1 阻塞。
         T3 恢复执行------T3 持有 Mutex,需要完成工作才能释放。
t=4ms:   T2 (中优先级, prio=3) 就绪。T2 不需要 Mutex。
         调度器看到 T2 优先级高于当前运行的 T3。抢占 T3。T2 开始执行。
t=10ms:  T2 完成。T3 恢复。
t=11ms:  T3 完成工作,释放 Mutex M。
t=11ms:  T1 获得 Mutex M,恢复执行。

T1 从 t=3ms 阻塞到 t=11ms------8 毫秒。它的 deadline 可能只有 500μs。高优先级任务被饿死了------不是因为 T3(持有锁的人)太慢,而是因为 T2(不持有锁、不需要锁的人)总是抢占 T3。T2 和 T1 之间没有竞争关系------T2 是真正无辜的一方------但它造成了灾难性的后果。

解决:优先级继承(Priority Inheritance)。 当 T1 阻塞在 T3 持有的 Mutex 上时,RTOS 暂时把 T3 的优先级提升到 T1 的级别(prio=5)。T2 只有 prio=3,无法再抢占 T3。T3 以高优先级快速完成工作、释放 Mutex,T1 得以继续。T3 完成释放后优先级降回原始设置。

FreeRTOS 默认的 Mutex 不支持优先级继承------你需要用特殊的互斥量类型。而 OSEK/VDX(AUTOSAR CP 使用的 OS)在规范层面强制要求优先级继承。这不是可选功能------是安全关键系统的基本保障。

这个 bug 在裸机代码中不可能存在------裸机没有抢占式多任务。RTOS 在提供便利的同时,也引入了全新类别的问题。

场景二:任务栈溢出------HardFault 从何而来

症状:系统不定期 HardFault。无法复现。

你给 T3 分配了 512 字节的栈空间。正常控制周期中只用 420 字节------看起来绰绰有余。但在特定条件------比如故障处理(记录 DTC 到 NVM + 发送故障通知 CAN 报文)------调用链深入 5 层,局部变量和函数参数把栈使用量推到 560 字节。栈溢出到相邻任务(T4)的栈空间------覆盖了 T4 的栈帧。T4 在下一次调度时恢复到一个被破坏的上下文 → HardFault。

栈溢出检测------魔法数字法:

c 复制代码
/* FreeRTOS 栈溢出检测钩子------魔法数字技术 */
#define STACK_MAGIC_NUMBER  0xA5A5A5A5UL

void vApplicationStackOverflowHook(TaskHandle_t xTask,
                                    char *pcTaskName)
{
    /* 栈溢出发生了!记录故障,尝试安全关闭 */
    record_dtc(DTC_STACK_OVERFLOW);

    /* 打印任务名------至少知道是谁越界了 */
    while (1) {
        /* 无法安全恢复------不要再调度 */
    }
}

/* FreeRTOS 内部:每次上下文切换时检查栈顶的魔法数字 */
/* 这段逻辑在 FreeRTOS 内核中(port.c),但你可以理解它 */
void vPortCheckStack(void)
{
    TaskHandle_t task = pxCurrentTCB;
    StackType_t *stack_start = task->pxStack;
    StackType_t *stack_top;

    /* 检查栈顶的魔法数字是否被覆盖 */
    if (*stack_start != STACK_MAGIC_NUMBER) {
        /* 魔法数字被破坏了------栈溢出 */
        vApplicationStackOverflowHook(task, task->pcTaskName);
    }
}

/* 在 FreeRTOSConfig.h 中启用 */
#define configCHECK_FOR_STACK_OVERFLOW    2
/* 方法2:检查栈顶魔法数字 + 检查当前栈指针是否越界 */

在任务栈创建时,FreeRTOS 把整个栈空间填充为 0xA5(方法1),或在栈起始位置写入 0xA5A5A5A5(魔法数字方法)。每次上下文切换时检查这个数字是否被破坏。同时,uxTaskGetStackHighWaterMark() 返回栈使用峰值------高水位标记越接近零,越危险。低于零------已经溢出了。

栈溢出的排查有个残酷的现实:栈溢出的后果(HardFault)往往发生在栈溢出之后很久------在另一个完全无关的任务被调度时。 你看到的故障现场(T4 的 HardFault)和故障根源(T3 的栈溢出)之间可能隔了数十次上下文切换。这是嵌入式调试中最让人崩溃的场面之一。

场景三:空闲任务饿死

症状:系统心跳定时器(SysTick)的中断处理时间逐渐变长,最终超过 SysTick 周期(典型 1ms),出现 SysTick 溢出。

你引以为傲的高优先级任务 T1(100μs 周期)在每次运行时获取一个自旋锁。T1 周期 100μs,每次运行 50μs------CPU 利用率 50%。剩下的 50μs 被中低优先级任务占满。但空闲任务(Idle Task)永远不会获得 CPU------因为至少有一个任务一直就绪。

空闲任务有一项重要职责:释放被删除任务的内存。当你调用 vTaskDelete(NULL) 删除任务自身时,任务的内存不是立即释放的------任务还在运行中,不能释放自己的栈。被删除任务的 TCB 和栈被放进一个"待回收列表"中。是空闲任务------当 CPU 没有其他事可做时------逐一释放这些内存。如果空闲任务永远得不到 CPU,内存越积越多,最终堆耗尽、pvPortMalloc 返回 NULL、系统崩溃。

空闲任务不是浪费。它是系统的垃圾回收工。 如果你发现空闲任务 CPU 利用率永远为 0%,你的系统设计有问题------要么有任务在忙等(busy-wait),要么总利用率超过 100%。


调度:计算机系统最底层也最核心的问题

实时操作系统的本质是:在有限资源下,对时间做最优分配。

你有 1 个 CPU(或在多核上是 N 个 CPU),但有很多任务都在抢它。RTOS 的任务是确保每个任务在它的 deadline 之前得到必要的 CPU 时间。

这和 SPI/CAN 的仲裁、AMBA 总线的仲裁、以太网交换机的优先级队列调度------是同一个问题的不同实例。就是:如何在多方竞争中,按优先级和最坏延迟来调度共享资源。

从逻辑门在硅片上的信号竞争,到 CAN 总线的 ID 仲裁,到 RTOS 的任务调度,到互联网路由器的 QoS 策略------这些都是调度的不同面貌。 你解决了一个,理解了它们全部。

但自己做 RTOS 可以,自定义调度策略很灵活------整个汽车行业却需要一个标准。如果一个 Tier-1 为标致-雪铁龙写的发动机控制器,和一个为大众写的发动机控制器,硬件一样但软件完全不能共用------同样的 CAN 驱动要重写五遍,同样的诊断栈要重写五遍。软件的规模效应被完全吞噬了。

2003 年,一群欧洲 OEM 和 Tier-1 的代表坐在会议室里,决定改变这一切。


本篇小结

今天我们做了一件事:理解实时操作系统的本质------在有限资源下对时间做最优分配,以及这种分配出错时系统如何崩溃。

关键结论:

  1. RTOS的本质是调度问题------和SPI仲裁、CAN ID仲裁、AMBA总线仲裁是同一个问题的不同实例:如何在多方竞争中按优先级和最坏延迟分配共享资源。你解决了一个,理解了它们全部。
  2. 信号量、互斥量、优先级继承不是"设计模式"------是对物理竞争条件的精确建模:优先级反转让高优先级任务无限期等待------优先级继承通过在持锁时临时提高优先级来打破等锁链。
  3. 栈溢出是RTOS世界里最危险的定时炸弹:故障现场(某任务的HardFault)和故障根源(另一任务的栈溢出)之间可能隔了数十次上下文切换。栈水印检测和高水位标记是唯一的侦查手段。

下一节,当整个汽车行业需要一个统一的ECU软件标准------AUTOSAR应运而生。一个Rte_Read()调用穿越5层软件栈,最终变成CAN总线上8字节的物理帧。

【下集预告】

"我们需要一个标准。一个汽车 ECU 软件的通用框架。"

这个标准叫做 AUTOSAR。它把 ECU 软件拆成分层、分模块、可配置的组件------就像 ISO 把螺丝钉的螺纹规格标准化一样。CAN 驱动写一次。诊断栈写一次。安全组件封装一次。软件不再依附于特定的芯片平台或 OEM 架构------它是可配置、可复用、可交换的。

下一节,你将看到 AUTOSAR 的两张面孔:CP(经典平台,面向实时 ECU)和 AP(自适应平台,面向自动驾驶)。一个 SWC 发出的 Rte_Read() 调用,会在 5 层软件栈中穿越 MCAL、CanIf、PduR、COM、RTE------最终变成一个 CAN 总线上 8 字节的物理帧。你会看到这段旅程的每一站,包括那些在 ARXML 配置文件中被定义的信号布局。

从裸机到 RTOS 再到 AUTOSAR------这是汽车软件工程的三个进化阶段。你已经走完了前两个。现在是第三个。

相关推荐
Soari4 小时前
GitHub 开源项目解析:rk‑llama.cpp —— 基于 llama.cpp 的 Rockchip NPU 加速本地推理引擎
开源·github·llama·llm 推理·npu 本地模型推理·加速 c/c++ 开源项目
2023自学中4 小时前
Linux虚拟机 CMakeLists.txt:x86 与 ARM 双架构编译脚本
linux·c语言·c++·嵌入式
webmote334 小时前
从零打造虚拟小智:用浏览器模拟 IoT 设备的实践之路
aigc·.net·嵌入式
Hommy885 小时前
【开源剪映小助手】核心功能特性
开源·github·视频剪辑自动化·剪映api
X54先生(人文科技)6 小时前
《元创力》纪实录·卷宗2.1边界测绘:一枚信标的沉没与一张舆图的诞生
人工智能·深度学习·开源·ai写作
FreakStudio7 小时前
大话电容传感器和电容SOC芯片,看这一篇就够了
python·单片机·嵌入式·面向对象·并行计算·电子diy·电子计算机
brycegao3218 小时前
Tauri2+Vue3+Ollama 实战|依托 AI 协同开发全离线隐私记账桌面软件(开源)
人工智能·开源·vue·ai编程·tauri·ollama·桌面开发
亥时科技8 小时前
无人机利用率看不清?一块 BI 仪表盘,能把“设备台账”变成“经营驾驶舱”
开源·无人机·ai巡检
八目蛛9 小时前
八目蛛网络(免费工具网站导航)
css·vue.js·开源·vue3·html5·ai编程