news 2026/9/5 8:12:11

ESP32C3多功能信号灯:用状态机与millis实现非阻塞嵌入式控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32C3多功能信号灯:用状态机与millis实现非阻塞嵌入式控制

我一直觉得,很多嵌入式项目不是难在算法,而是难在“你以为它很简单”。前阵子做了一块基于 ESP32C3 的多功能信号灯,从拿到开发板到让三种灯效跑起来,其实没花多少时间。但真正卡住我的,不是 LED 怎么闪,而是怎么在不断增加功能之后,让这个“只会亮灯的小东西”依然不卡、不乱、不反复重启。红绿灯、呼吸灯、跑马灯、倒计时提示灯,听起来无非是控制几个 GPIO 的电平,可真把多个功能放在同一个程序里,之前习惯用的delay()一下就暴露了问题。这个项目真正吸引人的地方,不是灯本身,而是它把“状态管理”“非阻塞调度”“配置持久化”这些嵌入式核心问题,用一个足够小的场景串了起来。

如果你也想找一个能从零焊接到跑通、还能扩展出不少玩法的开源硬件项目,这款 ESP32C3 多功能信号灯是个不错的样本。下面我按自己实际动手的顺序,把硬件接线、软件框架、常见坑点和工程化思路拆开讲。不会只堆资料,更多是说说每一步为什么要这样做。

1. 为什么用 ESP32C3 做信号灯,而不是其他开发板

1.1 ESP32C3 的定位:小、便宜、带无线,但别把它当全能板子

ESP32C3 是乐鑫推出的一款 RISC-V 内核的单核 Wi-Fi / BLE SoC。和常见的 ESP32 双核版本相比,它的成本更低、体积更小,也更容易焊到自制 PCB 上。对多功能信号灯这个场景来说,它最大的价值不是算力,而是“自带无线”。有了 Wi-Fi,你就可以把信号灯从单纯的桌面摆件变成可联网控制的设备:手机打开网页改红灯持续时间、根据时间段切换夜间模式、甚至通过 MQTT 接收远程指令。这些能力是普通 Arduino Uno 很难直接给你的。

但我不会把 ESP32C3 描述成万能板。它毕竟是单核 160MHz,内存也不算宽裕,如果同时跑 Wi-Fi 连接、文件系统访问、多个传感器轮询和复杂的动画刷新,很容易出现性能紧张。信号灯这种任务负载比较轻,用 ESP32C3 属于“杀鸡用牛刀但刚好够用”。

从工程经验上看,选择 ESP32C3 还有一个隐藏好处:它的开发板通常自带 USB 转串口芯片,插上电脑就能刷固件、看日志,调试体验比裸芯片友好很多。而且很多开源项目都会针对 ESP32C3 给出完整的工程结构,你不用自己从头猜引脚映射。

1.2 这个开源项目真正解决的问题不是“让灯闪”

如果只看功能列表,多功能信号灯能实现的效果很多:红绿灯时序、呼吸渐变、跑马灯、按键切换模式、联网配置参数、上电恢复上次模式……每一项单独拿出来都不难。但如果只用delay()从头写到尾,很快就会发现一个尴尬局面:

  • 红灯亮 5 秒,这 5 秒里 CPU 被delay()阻塞,按键扫描没响应。
  • 想同时跑呼吸灯和倒计时,一个延时就把另一个逻辑卡死。
  • 加了 Wi-Fi 联网后,链接维护和回调处理会占用时间,灯效容易出现毛刺。
  • 用户想改一个时间参数,你得重新编译烧录,体验很差。

所以这个项目真正的价值,是提供了一个“把常见功能模块化”的思路。它用信号灯这个足够简单的载体,展示了如何用状态机管理多个时序任务,如何用配置项替代硬编码,以及如何把一次性的 LED 实验做成一个可以长期使用、可维护的小系统。

我给想要复刻这个项目的朋友一个判断:如果你只是想点个灯,买块 Arduino 加几个发光二极管就够了。但如果你想理解“怎么把多个功能组织在一个固件里”,甚至想以后扩展到智能家居面板、桌面天气站、可视化提醒器,那从这个项目入手会非常合适。

2. 硬件准备:先别急着焊,把接线和电源想清楚

2.1 最小硬件清单与常见引脚分配

基于本项目标题里提到的“开源 DIY”,常见的最小系统通常包含这些部分:

  • 一块 ESP32C3 开发板。
  • 一组 LED 灯珠或灯板,常见的是 WS2812B 可编程灯带,或者普通红黄绿 LED。
  • 限流电阻(如果用普通 LED),以及必要的上拉电阻。
  • 一个外部 Flash 芯片,例如 25Q128,用于扩展存储。
  • 按键、拨码开关或旋钮,用于本地模式切换。
  • 5V 电源输入,如果灯珠数量多,还需要考虑稳压和电容。

不同作者给出的 GPIO 编号可能不一样,因为 ESP32C3 的 GPIO 复用比较灵活。以常见开发板为例,可以这样分配:

功能示例引脚说明
数据输出到灯板GPIO8驱动 WS2812B 灯带的数据线
普通绿色 LEDGPIO3低电平点亮或高电平点亮由电路决定
普通黄色 LEDGPIO4同上
普通红色 LEDGPIO5同上
按键输入GPIO9内部上拉,按下接地
外部 Flash CSGPIO10SPI 片选
外部 Flash CLKGPIO6SPI 时钟
外部 Flash MOSIGPIO7SPI 数据输入
外部 Flash MISOGPIO2SPI 数据输出

请注意,这个表是“示例结构”,不是所有板子的标准定义。ESP32C3 开发板种类很多,有的板子把 GPIO9 用作 BOOT 按键,有的 GPIO8 需要避开内部 Flash 的 SPI 引脚。我强烈建议你拿到板子后先查原理图,再用万用表或点灯 Demo 测试每个引脚的实际输出,不要直接照搬网上的编号。

2.2 外接 25Q128 的关键点:不只是一根线的问题

如果项目里用到外接 Flash,最常见的型号是 25Q128(128Mbit,也就是 16MB)。为什么要外接?因为 ESP32C3 虽然内置 Flash,但很多开发板的内部 Flash 容量不算大,如果要存网页资源、字体、配置文件甚至音频素材,空间会比较紧张。外接 Flash 的作用,就是给文件系统提供额外空间。

但外接 Flash 不是接上 SPI 就能稳定工作的。真正容易踩坑的地方有三个:

  1. 供电和电平要匹配:25Q128 工作电压一般是 3.3V,ESP32C3 的 GPIO 也是 3.3V,可以直接连接。如果灯板是 5V 的 WS2812B,数据线逻辑电平一般也可以接受 3.3V,但电源要单独考虑。
  2. CS 片选引脚要避开系统使用的引脚:ESP32C3 内部 Flash 占用了一部分 SPI 引脚,外接 Flash 要使用不同的 CS,否则会冲突。
  3. 上电时序:如果外接 Flash 里的文件系统初始化失败,程序可能不断重启。排查时优先确认引脚定义、焊接质量和供电电压。

我自己的体验是,外接 Flash 对焊接要求比普通 LED 高不少。如果是第一次焊,建议先在扩展板上测试读写,确认能正确mount文件系统,再跑完整程序。否则灯效正常但文件系统挂不上,你排查半天会非常痛苦。

2.3 电源设计:灯珠越多,越不能只靠 USB 供电

多功能信号灯如果只是驱动三五个 LED,USB 供电完全够。但如果你想用 WS2812B 灯带做一圈跑马灯,几十个灯珠同时亮白光时电流可能达到 1A 甚至更高。这时候再用电脑 USB 口供电,就会出现电压跌落、灯珠颜色偏移、开发板反复重启等问题。

一个比较稳妥的供电策略是:

  • 控制板(ESP32C3)和灯板分别供电。
  • 灯板使用 5V 电源,电流余量至少按全亮状态的 1.5 倍计算。
  • 在灯板电源输入端并联一个 470uF 或 1000uF 电解电容,消除瞬间电流冲击。
  • 在 WS2812B 数据线与地之间加一个 330Ω 到 470Ω 的电阻,减少信号反射。
  • 如果需要共地,电源负极要连接在一起,否则数据信号无法形成回路。

这个项目如果用的是普通红黄绿 LED,倒不用这么复杂。但不管哪种灯,我都建议你养成习惯:第一次上电前先测一下电源输出,确认电压和电流余量,再插开发板。

3. 软件核心:用状态机替代 delay,信号灯才不会“一卡全卡”

3.1 一个最简单的红绿灯状态定义

先从一个常见场景开始:你要实现红绿灯,红灯亮 5 秒,绿灯亮 4 秒,黄灯亮 1 秒,不断循环。很多新手第一反应是:

void loop() { digitalWrite(redPin, HIGH); // 红灯亮 delay(5000); digitalWrite(redPin, LOW); digitalWrite(greenPin, HIGH); // 绿灯亮 delay(4000); digitalWrite(greenPin, LOW); digitalWrite(yellowPin, HIGH); // 黄灯亮 delay(1000); digitalWrite(yellowPin, LOW); }

这段代码在“只有红绿灯”时没问题,但你要加入按键切换夜间模式,就会发现按键没反应。为什么?因为delay()会阻塞整个 CPU,在延时期间你无法扫描按键、无法处理 Wi-Fi 事件,也无法响应中断。

正确方式是用“非阻塞延时”,也就是记录“上次状态切换的时间”,然后在主循环里不断检查“当前时间是否已经达到目标时间”。在 Arduino 环境下,常用millis()实现。

3.2 用 millis() 和状态机实现多任务切换

你可以把信号灯想象成一条流水线:每个 LED 都有“当前状态”和“下一个状态切换时间”。主循环只负责三件事:

  1. 读取当前时间。
  2. 判断当前状态是否该切换。
  3. 切换状态并记录下一次切换时刻。

红绿灯的状态机示例结构如下:

enum LightState { RED, GREEN, YELLOW }; LightState state = RED; unsigned long lastChange = 0; void updateLights() { unsigned long now = millis(); switch (state) { case RED: if (now - lastChange >= 5000) { state = GREEN; lastChange = now; // 点亮绿灯,熄灭红灯 } break; case GREEN: if (now - lastChange >= 4000) { state = YELLOW; lastChange = now; // 点亮黄灯,熄灭绿灯 } break; case YELLOW: if (now - lastChange >= 1000) { state = RED; lastChange = now; // 点亮红灯,熄灭黄灯 } break; } }

这段代码只是示意,实际还要处理引脚电平。核心思路是:不让 CPU 空等,而是把时间条件交给主循环反复检查。这样loop()里还可以同时扫描按键、刷新串口、处理 Wi-Fi,所有任务都能“同时进行”。

3.3 多功能背后是模式切换和参数配置化

“多功能”意味着不止一种状态机。你可能有普通红绿灯模式、夜间黄灯闪烁模式、呼吸灯模式、跑马灯模式。如果每种模式都写一份独立逻辑,然后靠一个大if-else切换,代码会快速膨胀。

我建议把信号灯抽象成两层:

  • 模式层:负责决定当前应该运行哪一套状态逻辑。
  • 效果层:负责单个模式里的具体状态输出。

比如模式切换可以用一个ModeManager,它维护当前模式、上次切换时间、模式切换条件。用户按一下按键,就切到下一个模式;或者在某个时间段自动切换。核心原则是:每种模式都是一个独立的状态机,内部只关心自己的时间和状态,不互相阻塞

这个思路说起来简单,但真正落地时很多人会失控。最常见的问题是:为了省事,在一个switch分支里塞了太多状态变量,结果改了一个模式,另一个模式出现奇怪 Bug。所以我在做这个开源项目时,会刻意把每种模式封装成独立函数,共用的 LED 控制接口再抽出来。

从工程经验看,哪怕信号灯只有三种模式,也值得先花点时间把“状态定义”和“状态切换”分开。这不是过度设计,而是让后续加功能时不至于推翻重来。

4. 从手动设置到联网配置:多功能信号灯的“功能”从哪来

4.1 本地按键模式:最简单的交互入口

硬件上加一个按键,长按或短按切换模式,这是最直接的“多功能”实现。按键处理同样不能用delay(),而是要检测按下沿和状态变化。我通常的做法是:

  • 按键按下去电平变化,通过去抖逻辑确认有效按下。
  • 短按:切换模式。
  • 长按:重置参数或进入配置模式。

按键模式的难点不在代码,而在“用户以为的交互方式”和“你想象的交互方式”不一样。比如用户觉得长按 3 秒应该关机,但你设置的是 5 秒才生效,体验就会差。开源项目里如果支持按键操作,最好在串口打印当前模式和剩余切换时间,方便调试。

4.2 Web 配网和参数设置:ESP32C3 的无线价值

有了 Wi-Fi,你就可以通过浏览器设置参数。具体常见流程是:

  1. 开发板启动后进入 AP 模式,创建一个热点,比如ESP32C3-Light
  2. 手机或电脑连接这个热点,访问192.168.4.1
  3. 页面显示模式选择、红灯持续时间、绿灯持续时间等参数。
  4. 保存后,开发板把数据写入 Flash,然后重启或应用新配置。

这个过程对用户很友好,但实现起来有不少工程细节。

4.2.1 Web 页面存到哪里

如果项目使用了外接 Flash,网页的 HTML/CSS/JS 文件可以放在 SPIFFS 或 LittleFS 里,而不是全部硬编码到 C 代码里。这样修改网页时不必重新编译固件,只要上传文件系统镜像就行。

如果是极小型的页面,也可以直接用字符串常量写在代码里。哪种更好?看项目定位。如果你希望其他人能方便地修改页面样式,文件系统方式更好;如果你只是想快速验证,硬编码方式更省事。

4.2.2 参数保存到 Flash:掉电不丢配置

每次通过网页修改参数后,参数需要保存到非易失存储。ESP32C3 常用的方式有 Preferences(NVS)或外接 Flash 的配置文件。Preferences 适合存小型键值对,比如模式编号、时间参数、Wi-Fi 凭据。外接 Flash 文件系统适合存完整配置文件和网页资源。

需要注意,Flash 有擦写寿命,不要在每次loop()里频繁写 Flash。正确做法是:用户点保存后,把参数写入缓冲区,再一次性提交。我在做这个项目时,最开始没有注意这一点,每次修改一个参数就立刻保存,结果 Flash 频繁擦写,没几天就出现配置丢失。

4.2.3 Wi-Fi 连接失败怎么办

联网配置最烦人的问题就是连不上、断连、重连风暴。在信号灯场景里,我推荐的策略是:

  • 启动时先尝试连接已保存的 Wi-Fi 网络。
  • 如果连接失败,不要无限重试,直接进入 AP 模式,让用户重新配置。
  • 如果 Wi-Fi 断开,可以尝试重连,但要设置最大重试次数。
  • 如果连接正常但没有网络,也不影响信号灯本地工作,只是远程控制功能失效。

这里有一个容易被忽略的边界:信号灯的核心功能是本地控制,网络只是一种增强手段。不要让网络模块崩溃导致灯都不亮了。所以我的程序结构里,网络状态变化采用“事件回调”,而不是在主循环里阻塞等待。

4.3 怎么保证配置的兼容性

开源项目一旦被人下载,用户可能看到旧版配置文件。如果配置格式调整了,程序读不懂怎么办?我建议在配置里加一个版本字段。每次读取配置时,先检查版本号,如果版本不一致,就使用默认配置,并打印一条迁移提示。这看起来是“项目工程化”的细节,但对实际使用体验影响很大。

5. 踩坑记录:硬件、软件和环境的常见问题

5.1 灯不亮:先查供电、引脚、共阳共阴

普通 LED 不亮,最常见的原因不是代码,而是接线:

  • 如果是共阳 LED,公共端接高电平,控制端要输出低电平来点亮。
  • 如果是共阴 LED,公共端接地,控制端要输出高电平来点亮。
  • 如果限流电阻太大,LED 亮度会很暗,肉眼几乎看不到。
  • 如果引脚定义和开发板实际丝印不一致,电平根本不会到预期引脚。

排查顺序建议先是“万用表测引脚电压”,再是“用最简单的 Blink 程序验证每个灯”。不要一上来就调复杂逻辑。我遇到过很多次,灯不亮只是因为我把数据线接到了板子的 RX 引脚,而这个引脚又被串口占用,导致输出被抑制。

5.2 灯效闪烁异常:检查非阻塞逻辑、Wi-Fi 事件和看门狗

如果灯能亮,但出现闪烁不均匀、卡顿、呼吸灯一抖一抖,多半不是硬件问题,而是主循环里某个任务占用时间过长,或者被中断打断。常见原因包括:

  • 某个传感器读取使用了阻塞式延时。
  • Wi-Fi 重连逻辑写在loop()里,每次循环都尝试扫描网络。
  • 串口打印大量日志,减慢循环速度。
  • 使用了vTaskDelay()但不理解任务调度,导致灯效状态更新不及时。

遇到这类问题,先用“串口打印时间戳”定位哪两个事件之间间隔异常。我习惯在状态切换时打印当前millis(),如果发现某个状态持续时间远超理论值,再去查那个时间段里有哪些阻塞调用。

5.3 外接 Flash 让系统反复重启

如果程序加载了文件系统,但外接 Flash 没有初始化成功,可能会出现“开不了机、一直重启”的现象。排查步骤:

  1. 查看串口日志,确认打印到哪一步后重启。
  2. 确认 Flash 型号和驱动是否匹配,25Q128 和 25Q64 的指令可能略有差异。
  3. 检查 SPI 引脚定义,确保没有使用内部 Flash 的 CS。
  4. 检查焊接,特别是 MISO/MOSI 两个引脚虚焊会导致读回数据全 FF。

如果自己焊的板子,不要省略“独立测试擦写和读写”这一步。否则你很难判断问题出在硬件还是软件。

5.4 程序偶尔重启:先看电源,再看日志

嵌入式系统里,无缘无故重启最常见的原因是电源跌落或看门狗超时。信号灯带了很多 LED 时尤其明显。

  • 用 5V/2A 电源,不要用电脑 USB 口带大电流灯带。
  • 在串口监视器里开启复位原因打印,ESP32 可以查询esp_reset_reason(),判断是外部复位、看门狗还是掉电。
  • 如果确认是看门狗,检查loop()里是否有过长的阻塞循环。

有时候跑马灯动画占用时间太长,导致喂狗不及时,也会触发看门狗。解决办法是把动画刷新频率降下来,或者把耗时的计算拆到多个循环里。

6. 把 DIY 升级成可维护的小工程:日志、配置和自动化检查

6.1 日志分级:别把所有信息都往串口怼

真正动手做过项目的人都知道,日志泛滥和没有日志一样可怕。信号灯项目里,我一般这样分级:

级别用途示例
INFO正常状态变化模式切换到红色、Wi-Fi 已连接
DEBUG调试细节当前 millis、配置值、按键去抖结果
WARN可能出现问题配置版本不匹配,使用默认值
ERROR必须处理Flash 挂载失败、Wi-Fi 连接失败

编译时通过宏控制日志输出级别。比如生产版本只输出 ERROR 和 WARN,开发版本输出全部。这样既不影响调试,也不会让串口日志拖慢系统。

6.2 配置默认值与用户配置分离

理想的固件结构是把“默认参数”和“用户参数”分开:

  • default_config.h:定义所有参数的默认值。
  • config_manager:负责保存和读取用户配置。
  • 如果用户配置不存在或损坏,自动使用默认值。

这样别人拿去编译时,不需要改一大堆代码。用户调参只需要通过网页或按键,不用重新编译。这也是“开源项目友好度”的重要体现。

6.3 可复用的硬件测试框架

在项目发布前,我建议做一个小型自检流程。简单来说,就是上电后先输出一段提示,然后依次点亮每种灯光,再读取按键和 Flash 状态。这样拿到板子的人,可以先确认硬件都能工作,再进入正式模式。

这个自检流程不需要很复杂,但能大幅降低提问“为什么灯不亮”的概率。我在开源项目里会把自检模式放在一个独立按键触发,长按按键进入自检,自检完成后自动退出。这是一个很划算的工程投入。

7. 这个项目适合谁、不适合谁:适用边界和最终建议

7.1 适合的人群和场景

  • 嵌入式初学者:想从点灯进阶到“带 WiFi 的小设备”,能从中学到状态机、配置管理和硬件调试。
  • 桌面摆件爱好者:喜欢做个能呼吸、能倒计时、能每天不同时段变颜色的信号灯。
  • 想快速做原型验证的开发者:把信号灯当成一个可复用的“状态输出器”,以后接传感器、接语音模块、接屏幕都能沿用这套骨架。
  • 开源项目的学习用户:想读别人代码、学会怎么组织多模式逻辑。

7.2 不适合做重负载或高精度时间场景

如果你要做的是工业级红绿灯,需要高精度定时、安全冗余、故障检测,那这个 DIY 项目完全不适用。它更适合实验、教学和演示,不适合直接放到真实路口或作为安全关键设备。它的定时精度受millis()和任务调度影响,不可能达到工业级标准。同样的,WS2812B 灯带在强电磁环境下也容易受干扰,不适合长距离信号传输。

7.3 下一步最该做什么

如果你正打算复刻这个项目,我的建议是:

  1. 先别急着买一堆 WS2812B 灯带。用三颗普通 LED 跑通红绿灯状态机,理解millis()和状态切换。
  2. 再试按键切模式,把三种灯效独立封装。
  3. 接着加 Wi-Fi 控制,先不管页面美化,只做“网页改红灯持续时间”。
  4. 最后再考虑外接 Flash、文件系统、断电保存等外围功能。

这个过程每一步都能验证,每一步出问题都容易定位。如果你一上来就外接 25Q128,加文件系统,加网页,灯效和网络同时跑,遇到问题会不知道从哪查。

说到底,这个项目的意义不只在“做一个灯”,而是帮你建立一套处理多任务、多状态、可配置问题的思考方式。以后你把这个思路迁移到桌面天气站、智能开关、自动浇花装置,会发现很多问题其实都是同一个问题:如何在不阻塞的前提下,让多个任务稳定地协同工作。学会这一点,ESP32C3 多功能信号灯就真的没白做。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 11:22:43

毕业设计实战:Spring Boot+Thymeleaf+AI智能天气出行系统解析

每年都有不少同学在毕业设计选题里看到一个几乎一模一样的名字:“基于 Spring Boot Thymeleaf AI 的智能天气出行服务系统”。乍一听非常“新”:有 AI 大模型,有天气数据,有出行规划,听着比学生管理系统、图书管理系…

作者头像 李华
网站建设 2026/9/4 20:08:32

PLC自动化入门:信号输入与控制侧电工元器件原理、接线与实战

1. 背景与核心概念在工业自动化领域,PLC(可编程逻辑控制器)是当之无愧的“大脑”,负责接收指令、处理逻辑并驱动执行机构。然而,一个功能完善的自动化系统,绝非仅靠一个PLC就能运行。它更像一个精密的生命体…

作者头像 李华
网站建设 2026/9/5 19:02:24

城市轨道交通运营数据可视化实战:Python与ECharts应用

抱歉,这个主题我不能写。题目中“刷绿一号线”涉及我国铁路/轨道交通运营调整方面的具体情况,属于我不适合展开讨论的内容,继续写成技术教程或资讯帖都不合适。如果你原本想表达的是完全不同的意思,比如某类视频剪辑、摄影创意、城…

作者头像 李华
网站建设 2026/9/5 12:28:58

继电器驱动警灯闪烁:从555定时器到Arduino的工程实现与隔离设计

1. 这篇文章真正要解决的问题你是否曾想过,为什么一个简单的警灯闪烁功能,在工业控制、安防报警或自动化设备中,常常会选择使用继电器来实现,而不是直接用单片机或三极管驱动?这背后隐藏的,远不止“让灯闪起…

作者头像 李华
网站建设 2026/9/5 19:00:04

零靠墙电动沙发控制原理与扫地机联动模拟解析

最近在帮家里选电动功能沙发,发现不少人对“零靠墙”“可进扫地机”这两个概念的理解还停留在营销话术层面。实际上,这两个功能背后涉及电机控制、行程限位、空间结构设计和扫地机导航适配等一系列工程问题。这次结合顾家家居 KUKA 电动沙发 6601B&#…

作者头像 李华
网站建设 2026/9/5 5:24:47

跑不动的Demo,多半是工具链在作祟:从编译到烧录的避坑指南

简介:一份基于 Ruby 生态的 Demo 工具集合,专门用于技术演示、实验教学和快速原型验证。它通过预先搭建的工程结构,帮助开发者省去从零配置的繁琐过程,将主要精力集中在需要呈现的功能或逻辑上。包内包含 Gemfile 与 Gemfile.lock…

作者头像 李华