嵌入式开发新选择:Squirrel脚本语言移植与应用实战
2026/8/18 1:35:56 网站建设 项目流程

1. 项目概述:当“松鼠”跑进嵌入式世界

如果你在嵌入式开发领域摸爬滚打了一段时间,可能会对C语言的严谨高效又爱又恨,也会对Python在资源受限环境下的无力感到无奈。今天要聊的这个“小东西”——Squirrel,恰恰是在这两者之间寻找平衡点的一个有趣选择。它不是那种动辄要改变行业格局的明星语言,但在特定的嵌入式场景下,却像一只灵巧的松鼠,能在资源有限的“树枝”间轻盈跳跃,完成一些传统脚本语言难以胜任的任务。

Squirrel是一门轻量级、面向对象、类C语法的脚本语言,专为嵌入到主机应用程序(尤其是游戏和嵌入式系统)中而设计。它的核心卖点非常明确:在保持脚本语言快速开发、动态灵活优势的同时,极力压榨运行时开销,使其能够运行在内存可能只有几十KB的微控制器(MCU)上。当你看到网络热词里反复出现“embedded”与“Squirrel”的组合,甚至具体到“embedded board array”或“linux移植”时,这背后反映的正是开发者们对一种“中间态”解决方案的渴求——既不想完全陷入底层C的复杂性,又受制于硬件资源无法使用完整的Python或Lua。

这篇文章,我就以一个嵌入式老鸟的视角,带你彻底拆解这只“嵌入式松鼠”。我们会从它为何能在夹缝中生存讲起,一步步深入到如何把它“移植”到你的板子上,并写出高效可靠的脚本。无论你是在为下一个IoT设备寻找灵活的配置与逻辑层,还是厌倦了每次修改参数都要重新编译烧录的繁琐,Squirrel都可能是一个值得放进工具箱的备选方案。

2. Squirrel语言核心特性与嵌入式适配性解析

2.1 生于游戏,长于嵌入:设计哲学溯源

要理解Squirrel为何适合嵌入式,得先看看它的出身。它最初的设计目标是为视频游戏提供高性能的脚本系统。游戏脚本需要什么?快速原型迭代、复杂的对象交互、相对简单的逻辑控制,同时绝不能拖累核心引擎的性能。这些需求与许多嵌入式应用场景(如设备控制逻辑、用户交互脚本、动态配置解析)不谋而合。

Squirrel在语言设计上做了大量权衡,这些权衡正是其嵌入式适配性的基石:

  1. 动态类型与轻量级对象模型:和Python、Lua一样,Squirrel是动态类型的,这带来了开发的灵活性。但其对象模型比Python轻量得多,更接近Lua的table,却又提供了类(class)、继承等更传统的OOP支持。这对于嵌入式开发者来说是个甜点:你可以用熟悉的方式组织代码,但运行时负担却小得多。
  2. 增量式垃圾回收(GC):这是Squirrel区别于很多嵌入式脚本语言的关键。它的GC是增量、非压缩的。这意味着垃圾回收过程可以被分成很多小步骤,穿插在脚本执行中,避免了传统“停止-世界”式GC带来的不可预测的停顿。在实时性要求不极端苛刻的嵌入式系统中,这种可预测的微小延迟比一次数百毫秒的卡顿要友好得多。
  3. 类C的语法与寄存器式虚拟机:如果你会C、C++或Java,看Squirrel代码几乎毫无障碍。它减少了学习成本。其虚拟机采用寄存器架构,而非栈式架构,指令集设计紧凑,执行效率通常更高,生成的字节码也更小。

2.2 资源消耗:与Lua、MicroPython的硬核对比

光说特点不够,我们得看数字。Squirrel在资源上的节俭是它立足的根本。

  • 内存占用:一个最小的Squirrel虚拟机(VM)本身,在不包含任何标准库的情况下,内存占用可以控制在50-100KB的ROM/Flash空间和几KB的RAM(用于VM运行时状态)左右。这比完整的MicroPython(通常需要几百KB ROM和几十KB RAM)要苗条得多,与经典的Lua 5.x在伯仲之间,甚至在某些配置下更优。
  • 执行性能:由于其寄存器式VM和精简的设计,Squirrel的字节码执行速度通常优于解释执行的MicroPython,与高度优化的Lua JIT版本相比在纯计算任务上可能有差距,但考虑到嵌入式MCU通常没有JIT环境,Squirrel的解释器性能表现是相当扎实的。
  • 可裁剪性:Squirrel的标准库是可选的。你可以只编译进你需要的部分(比如只留数学库和基础字符串操作),进一步缩减体积。这对于只有64KB或128KB Flash的STM32G0系列或ESP8266等芯片至关重要。

注意:这里说的“占用”是VM核心的占用。实际内存消耗主要取决于你运行的脚本本身:创建的对象数量、字符串长度、数组大小等。Squirrel的GC虽然增量,但依然需要预留一定的堆空间供其运作。

2.3 为何选择Squirrel?典型应用场景画像

你不会在所有的嵌入式项目里都用Squirrel。它的用武之地非常具体:

  1. 设备控制逻辑脚本化:想象一个智能家居控制器。灯光切换、窗帘开合的复杂场景(“观影模式”、“离家模式”)如果硬编码在C里,每次修改都要固件升级。用Squirrel写成脚本,可以通过手机App直接下发、更新,实现了逻辑与固件的解耦。
  2. 产品参数动态配置与校准:工业传感器需要现场校准,参数(量程、滤波系数、报警阈值)可能多达数十个。将这些参数和校准流程用Squirrel脚本定义,现场工程师通过一个简单的串口命令行就能交互式地完成配置,脚本还能包含配置验证逻辑。
  3. 原型验证与快速迭代:在硬件开发早期,用Squirrel快速实现核心算法或业务逻辑的模拟,比用C调试更快。验证无误后,可以再手工移植为C代码,或者在某些性能不敏感的环节直接保留脚本。
  4. 作为高级用户接口或插件系统:为你的嵌入式设备提供一个简单的脚本控制台,允许高级用户或集成商编写自定义的自动化任务。Squirrel相对安全的执行环境(可通过沙箱限制访问)比直接暴露一个C解释器要安全。

当你的项目同时面临“需要灵活性”和“资源非常紧张”两个条件时,Squirrel的评估价值就大大提升了。

3. 将Squirrel移植到嵌入式平台:从源码到可执行文件

3.1 获取源码与理解目录结构

Squirrel的官方源码托管在SourceForge上,项目名就是“squirrel”。通常,你需要关注两个核心部分:

  • squirrel: 语言核心,包含编译器(sqc)和虚拟机(VM)的源码。
  • sqstdlib: 标准库,提供数学、字符串、IO等基础功能。

拿到源码后,别急着编译。先浏览一下目录,重点看:

  • include/: 你需要集成的头文件,主要是squirrel.hsqstdblob.h等。
  • sqconfig.h: 这是黄金配置文件。几乎所有与平台特性、功能裁剪相关的宏定义都在这里。比如,你可以通过SQ_USE_MATH_LIBRARY来决定是否启用数学库。

3.2 针对嵌入式环境的编译配置与裁剪

嵌入式编译的核心思想是:只带走你需要的

  1. 选择编译模式:通常你需要编译出静态库(.a.lib)以便链接到你的固件中。修改你的构建系统(如Makefile、CMakeLists.txt或IAR/Keil的工程文件),将Squirrel的.cpp文件(注意,Squirrel是C++写的,但接口是C风格的)加入编译,并定义正确的宏。
  2. 关键宏配置(sqconfig.h)
    • SQ_USE_MATH_LIBRARY: 如果不需要sin,cos,sqrt等函数,果断关掉。
    • SQ_USE_IO_LIBRARY: 嵌入式环境通常没有文件系统,这个库(提供file相关操作)基本可以禁用。
    • SQ_USE_OS_LIBRARY: 提供系统调用,如获取时间。在无操作系统的裸机环境,你需要自己实现或关闭它,然后提供自定义的钩子函数。
    • SQ_USE_DEFAULT_STDLIB: 如果你连最基础的print都不需要(可能通过自定义绑定函数来输出日志),可以关闭整个标准库,极致精简。
    • SQ_MEMORY_DEBUG/SQ_COLLECT_MEMORY_INFO调试时非常有用,但正式发布务必关闭,它们会增加额外开销。
  3. 内存管理定制:默认Squirrel使用malloc/free。在嵌入式系统,尤其是无操作系统的场合,你可能希望使用静态内存池或RTOS提供的内存管理。你需要重写sq_vm_mallocsq_vm_free等函数。这通常通过修改源码或提供特定的宏定义来实现,是移植中最需要小心的一步,因为它直接关系到系统的稳定性和内存碎片问题。

3.3 与裸机或RTOS的集成实践

集成Squirrel VM到你的工程,主要做三件事:

  1. 初始化VM

    #include “squirrel.h” #include “sqstdblob.h” HSQUIRRELVM v; v = sq_open(1024); // 参数是初始堆栈大小,根据实际情况调整 // 注册标准库(如果启用) sq_pushroottable(v); sqstd_register_bloblib(v); // 例如注册blob库 sq_pop(v, 1);
  2. 提供“原生绑定”:这是嵌入式脚本的核心价值。将你的硬件功能(如读GPIO、写PWM、发送串口数据)暴露给脚本。

    // 一个简单的绑定函数示例:让脚本可以控制LED SQInteger led_set(HSQUIRRELVM v) { SQInteger pin, state; sq_getinteger(v, 2, &pin); // 获取第二个参数 sq_getinteger(v, 3, &state); // 获取第三个参数 HAL_GPIO_WritePin(GPIOA, pin, (GPIO_PinState)state); return 0; // 无返回值 } // 注册这个函数到Squirrel的全局表 sq_pushroottable(v); sq_pushstring(v, “setLED”, -1); sq_newclosure(v, led_set, 0); // 创建闭包 sq_newslot(v, -3, SQFalse); // 在根表中创建新条目 sq_pop(v, 1);

    现在,在Squirrel脚本中你就可以直接调用setLED(5, 1)来点亮GPIOA.5了。

  3. 脚本加载与执行

    • 编译执行:如果你的脚本存储在外部Flash或文件系统中,可以先读入内存,然后用sq_compilebuffer编译,再调用sq_pushroottablesq_call来执行。
    • 预编译字节码:更常见的嵌入式做法是,在PC上先用Squirrel编译器(sqc)将.nut脚本文件编译成字节码文件(.cnut),然后将这个字节码数组直接作为常量数组编译进固件。运行时直接用sq_readclosure加载这个数组并执行。这节省了MCU上编译的开销,也保护了脚本源码。
  4. 处理Tick:Squirrel的增量式GC需要定期被“推动”。你需要在你的主循环或RTOS的某个低优先级任务中,定期调用sq_collectgarbage(v)。调用频率决定了GC的“粒度”,频率越高,单次调用耗时越短,对实时任务的影响越小。

4. Squirrel脚本编程精要与嵌入式实践

4.1 语法快速上手与嵌入式风格约定

Squirrel语法非常直观。这里强调几个在嵌入式编程中需要特别注意的点:

  • 变量与类型:局部变量用local声明。虽然动态类型,但主动管理类型对性能有好处。
    local pin = 5; // 整数 local name = “LED1”; // 字符串(注意嵌入式中字符串是内存消耗大户) local isOn = true; // 布尔值
  • 表(Table)是万能结构:表可以当数组、字典、甚至简单的对象来用。在嵌入式脚本中,用它来组织配置参数非常方便。
    local config = { “sampling_rate” = 100, // Hz “threshold” = 2.5, “channels” = [1, 3, 5] // 数组也是表 }; local rate = config[“sampling_rate”];
  • 函数与闭包:函数是一等公民。闭包可以捕获外部变量,用于实现回调机制非常强大。
    function onButtonPress(pin) { local counter = 0; // 被闭包捕获 return function() { counter++; ::print(“Button on pin ” + pin + “ pressed ” + counter + “ times\n”); } } local callback = onButtonPress(12); // 将callback绑定到中断服务例程(通过原生绑定)
  • 类与继承:对于稍复杂的设备抽象,可以使用类。
    class Sensor { constructor(id) { this.id = id; } function read() { return ::readSensorNative(this.id); // 调用绑定的原生函数 } } local tempSensor = Sensor(0); local value = tempSensor.read();

嵌入式编程风格建议

  1. 避免全局变量:多使用局部变量和函数参数。Squirrel的全局变量查找比局部变量慢。
  2. 谨慎使用字符串操作:连接(+)或切片操作在内存有限的系统可能引发频繁的分配和GC。尽量在原生C侧处理复杂字符串。
  3. 预计算与常量:将循环中不变的表达式提到循环外。使用const定义真正的常量。
  4. 控制对象创建:在长期运行的循环中,避免不断创建新的表或数组对象,尽量复用。

4.2 原生绑定(Native Binding)深度指南

绑定是将C/C++世界的能力安全、高效地暴露给脚本的关键。除了简单的函数,还有更复杂的模式:

  1. 绑定类(Class Binding): 让你在Squirrel中创建的对象,背后对应一个C++的结构体实例。
    • 你需要定义“创建”、“销毁”、“成员方法”、“成员属性”等一系列回调函数。
    • 这允许脚本像操作普通Squirrel对象一样操作一个硬件设备句柄或复杂的数据结构,当脚本对象被GC回收时,对应的C++资源也会通过析构函数被释放,防止内存泄漏。
  2. 绑定回调与异步事件: 嵌入式系统充满异步事件(中断、定时器、通信接收)。一种模式是:在C侧,当事件发生时,将对应的Squirrel函数(闭包)压入一个待执行队列。然后,在主循环或专用脚本任务中,从队列取出并执行这些函数。这实现了非阻塞的事件驱动脚本编程。
  3. 安全考量: 永远不要相信脚本传入的参数。在绑定函数内部,必须严格检查参数的数量、类型和取值范围。一个错误的脚本调用setLED(999, 999)可能会导致内存越界写入,使系统崩溃。

4.3 内存管理、GC调优与防泄漏策略

在内存捉襟见肘的嵌入式环境,脚本引擎的内存行为必须受控。

  1. 设置合理的堆大小sq_open(stack_size)中的stack_size是虚拟机堆的初始大小。你需要根据脚本的复杂程度估算一个值,并留有余量。太小会导致内存不足错误,太大会浪费宝贵RAM。通常从16KB或32KB开始测试。
  2. 监控内存使用:利用Squirrel提供的调试接口(如sq_getallocatedmemory)定期在日志中输出内存使用情况,观察在典型操作下内存的峰值和稳态值。
  3. 理解GC触发:Squirrel的GC并非仅在调用sq_collectgarbage时工作。当内存分配失败时,VM也会尝试强制进行一轮完整的GC来回收内存。因此,确保你的sq_vm_malloc在失败时返回NULL,而不是自己死循环或崩溃。
  4. 避免循环引用:Squirrel的GC是引用计数的(辅以标记清除处理循环引用)。但循环引用会导致对象无法被及时回收。在脚本中,如果两个对象互相持有引用,且不再被外部使用,需要手动将引用置为null来打破循环。
  5. 静态生命周期对象:对于从脚本初始化到结束始终存在的对象(如全局配置表),可以考虑在C侧创建并通过绑定使其在Squirrel中可见,而不是完全在脚本中创建。这样可以减少对脚本堆的压力。

5. 实战:构建一个基于Squirrel的智能灯控固件

5.1 系统架构设计

我们假设一个基于STM32F407的智能RGB灯控板,硬件上有PWM输出控制RGB LED,几个按键,一个WS2812B灯带接口,以及Wi-Fi模块(如ESP8266 AT固件)。固件需要支持多种灯光模式(纯色、渐变、音乐律动等),且这些模式希望可以通过手机App动态更新。

架构设计如下

  • 底层驱动层(C语言): HAL库或寄存器操作,控制GPIO、PWM、定时器、串口(连接Wi-Fi模块)。
  • 硬件抽象层(HAL Binding, C): 将底层功能封装成一系列安全的Squirrel原生绑定函数,例如pwmSet(channel, duty)neopixelSet(strip_id, color_array)uartSend(wifi_uart, data)readKey()
  • Squirrel虚拟机核心: 集成裁剪后的Squirrel VM。
  • 脚本逻辑层(Squirrel)
    • main.nut: 脚本入口,初始化硬件,加载模式,运行主循环。
    • modes/: 目录(在概念上),存储各种灯光模式脚本,如solid_color.nut,gradient.nut,music_beat.nut。每个模式脚本导出一个统一的接口(如start(params),stop(),tick())。
    • network.nut: 处理通过Wi-Fi接收到的控制命令(如JSON),解析后调用相应模式。
  • 通信与存储: Wi-Fi接收到的新脚本模式脚本,先存储在外部SPI Flash的一个特定区域。系统在启动或收到更新指令后,从Flash读取字节码并加载到VM执行。

5.2 关键绑定函数实现示例

以PWM绑定为例,展示更健壮的实现:

// pwm_binding.c SQInteger pwm_set_duty(HSQUIRRELVM v) { // 1. 检查参数数量 if(sq_gettop(v) < 3) { return sq_throwerror(v, “缺少参数: pwm_set(channel, duty)”); } // 2. 获取并检查参数1:通道号 SQInteger channel; if(SQ_FAILED(sq_getinteger(v, 2, &channel))) { return sq_throwerror(v, “参数1(通道号)必须为整数”); } if(channel < 0 || channel >= PWM_CHANNEL_MAX) { return sq_throwerror(v, “通道号超出范围”); } // 3. 获取并检查参数2:占空比 SQFloat duty; if(SQ_FAILED(sq_getfloat(v, 3, &duty))) { // 也允许传入整数 SQInteger duty_int; if(SQ_FAILED(sq_getinteger(v, 3, &duty_int))) { return sq_throwerror(v, “参数2(占空比)必须为数字”); } duty = (SQFloat)duty_int; } if(duty < 0.0 || duty > 100.0) { return sq_throwerror(v, “占空比必须在0-100之间”); } // 4. 调用实际硬件操作(需自行实现) if(HAL_PWM_SetDuty(channel, (float)duty) != HAL_OK) { return sq_throwerror(v, “硬件PWM设置失败”); } // 5. 函数无返回值 return 0; } // 注册函数 void register_pwm_lib(HSQUIRRELVM v) { sq_pushroottable(v); sq_pushstring(v, “pwm_set”, -1); sq_newclosure(v, pwm_set_duty, 0); sq_newslot(v, -3, SQFalse); sq_pop(v, 1); }

5.3 脚本示例与动态加载机制

一个简单的渐变模式脚本modes/gradient.nut

// 渐变模式 class GradientMode { speed = 10.0; hue = 0.0; constructor(startHue, speedMs) { this.hue = startHue.tofloat(); this.speed = speedMs.tofloat(); } function start() { ::print(“Gradient mode started\n”); this.hue = 0.0; } function stop() { ::print(“Gradient mode stopped\n”); } function tick(deltaTimeMs) { // deltaTimeMs由主循环传入 this.hue += (this.speed * deltaTimeMs / 1000.0); if(this.hue > 360.0) this.hue -= 360.0; // 将HSV颜色转换为RGB(这里假设有一个绑定的工具函数) local rgb = ::hsvToRgb(this.hue, 1.0, 1.0); // 设置到LED(假设strip_id为0) ::neopixelSet(0, rgb); } } // 工厂函数,用于动态创建模式实例 function create_mode(params) { local startHue = params.get(“start_hue”, 0); local speed = params.get(“speed_ms”, 10); return GradientMode(startHue, speed); } // 将工厂函数导出到全局命名空间,方便主脚本查找 ::_mode_factories[“gradient”] <- create_mode;

在主脚本main.nut中动态加载模式:

// 主脚本 _mode_factories <- {}; // 全局表,用于存放所有模式工厂函数 // 加载模式模块(假设字节码已预加载或从Flash读取) // 这里模拟动态编译(实际产品中应加载预编译字节码) dofile(“modes/gradient.nut”); // 用户选择模式 local selectedMode = “gradient”; local params = { “start_hue” = 120, “speed_ms” = 5 }; if(selectedMode in ::_mode_factories) { local factory = ::_mode_factories[selectedMode]; currentMode = factory(params); currentMode.start(); } // 主循环(由C侧定时调用) function main_loop_tick(deltaTimeMs) { if(currentMode != null) { currentMode.tick(deltaTimeMs); } }

动态加载机制:在实际产品中,dofileloadfile可能不可用(因为文件系统)。更常见的做法是:

  1. 在PC端用sqcgradient.nut编译成gradient.cnut(字节码)。
  2. 使用一个二进制转换工具,将gradient.cnut转换为C语言数组,例如gradient_code.h,里面包含const unsigned char gradient_bytecode[] = {…};
  3. 将此数组编译进固件,存储于Flash的只读区域。
  4. 在C侧,提供一个绑定函数loadModule(name),内部根据名字找到对应的字节码数组,然后调用sq_readclosure从内存加载并执行。

6. 调试、性能分析与常见问题排查

6.1 调试手段:日志、断言与远程交互

在没有图形化调试器的脚本环境中,日志是你的眼睛。

  1. 增强print函数: 重写Squirrel的默认print函数,将其重定向到你的串口日志系统,并添加时间戳、脚本文件名和行号信息(Squirrel运行时可以获取当前执行位置)。
  2. 使用编译指令: Squirrel支持#line指令,可以确保编译错误信息指向原始的.nut文件,而不是编译后的字节码位置。
  3. 实现一个简单的交互式控制台: 通过串口提供一个REPL(Read-Eval-Print Loop)环境。这需要你实现一个行编辑器,读取用户输入,传递给sq_compilebuffersq_call执行,并打印结果。这是极其强大的调试和现场诊断工具。
  4. 断言与错误处理: 在绑定函数和关键脚本逻辑中使用sq_assert或自定义的检查。确保脚本错误能被捕获并通过sq_geterror获取详细的错误信息,记录到日志中。

6.2 性能分析与优化点

当觉得脚本运行慢时,可以按以下顺序排查:

  1. 基准测试: 写一个纯计算的循环脚本,与等价的C实现对比,估算出脚本执行的固有开销。这有助于你判断性能瓶颈是否在脚本本身。
  2. 热点分析: 在关键函数前后调用sq_getmicroseconds(如果实现了该绑定)来测量耗时。通常,性能瓶颈出现在:
    • 频繁的跨语言调用: 脚本中大量调用非常简单的原生函数(如getPin())。考虑将一组操作合并到一个原生调用中。
    • 密集的字符串处理: 如前所述,尽量避免在脚本循环中进行复杂的字符串拼接或分割。
    • 巨大的表遍历: 如果表很大,线性查找会变慢。考虑是否能用数组替代,或者将关键查找移到C侧。
  3. 字节码 vs 源码: 始终使用预编译的字节码,避免在设备上编译。
  4. GC频率调整: 调整sq_collectgarbage的调用频率。太频繁会增加开销,太稀少会导致单次GC停顿变长、内存峰值升高。需要通过压力测试找到一个平衡点。

6.3 常见问题速查与解决方案

问题现象可能原因排查步骤与解决方案
系统运行一段时间后死机或重启内存泄漏、堆溢出1. 开启内存调试信息 (SQ_COLLECT_MEMORY_INFO),监控内存增长。
2. 检查绑定函数中,是否为Squirrel对象分配了C资源但未正确设置析构回调。
3. 检查脚本中是否存在全局变量无限制累积数据(如不断向一个全局数组追加数据)。
4. 减小VM堆大小看是否提前崩溃,帮助定位。
脚本执行时报“内存不足”VM堆空间不足、内存碎片1. 增加sq_open的堆大小。
2. 检查脚本中是否有单次分配巨大对象(如一个超长字符串或超大数组)。
3. 优化GC调用策略,更频繁地触发增量GC。
调用原生函数后系统行为异常绑定函数参数检查不严、栈操作错误1. 在绑定函数开头严格检查参数数量和类型。
2. 检查所有sq_get*sq_push*函数调用是否成对,是否破坏了VM栈平衡。
3. 确保对硬件寄存器的访问是原子的或受保护的(特别是中断上下文中)。
脚本加载失败字节码文件损坏、版本不兼容1. 确认PC上编译字节码的Squirrel版本与嵌入式端的VM版本完全一致。
2. 计算字节码文件的CRC校验和,加载前先验证。
3. 如果是动态加载,检查存储介质(如Flash)的读写是否正确。
实时任务受到干扰GC停顿、脚本循环执行时间过长1. 将sq_collectgarbage调用放在低优先级任务中,并限制其单次最大执行时间(通过分片)。
2. 将冗长的脚本操作分解成多个短小的函数,通过事件驱动的方式分步执行,避免独占CPU。

最后一点心得:引入Squirrel这类脚本引擎,本质是在开发效率运行时开销/确定性之间做权衡。它非常适合作为嵌入式系统的“上层建筑”,处理那些变化频繁、逻辑复杂但对实时性要求不极端的任务。对于底层的驱动、时序要求严格的中断服务、高性能信号处理,还是应该交给C/汇编。让合适的工具做合适的事,这才是嵌入式系统设计的精髓。当你需要那块“灵活的橡皮泥”时,Squirrel这只小巧而健壮的“松鼠”,值得你考虑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询