COCKPIT SUITELIVE HUB /

工作坊 / AI 与编程

借助 AI 完成第一个驾驶舱模块:四个 LED 和一块 OLED

没有编程经验,也可以从一个范围明确的小项目开始。确定目标、认清元件、分步测试,才能真正用好 AI。这篇指南帮助你自己决定指示灯如何响应、屏幕显示什么,并理解每一次修改带来的结果。

完成规划后,你将得到一个可逐项验证的 Arduino Uno R3 学习项目:四个独立 LED、三个 OLED 数值、完整程序和可继续修改的提示词。

内容审核日期:2026年10月18日

先确定一个自己能够检查的目标

想象方向盘或飞行操纵杆旁边有一块小面板:四个灯表示不同状态,一块 OLED 显示速度、转速与燃油量。首次搭建时,这些数字刻意来自 Arduino IDE 的串口监视器。你不需要启动模拟器,可以亲自触发每一种变化,把接线问题与软件集成问题分开检查。

需求是四个 LED 可以独立开关,并清楚显示收到的数据。程序的任务是检查完整消息、一次性接受所有有效字段、标明已经过时的数据。最终得到的是一个自己能理解、能继续扩展的功能模块,而不是自动兼容任意模拟器的成品设备。

AI 可以解释接线、起草代码、帮助分析错误。你需要决定的是“应该发生什么”“什么时候发生”“怎样算成功”。即使还不认识变量、函数等词,也可以回答这几个问题。遇到新概念时,请 AI 结合你正在测试的那一段电路说明,不必一开始就学习整个编程体系。

明确这次使用的具体元件

准备 Arduino Uno R3、可传输数据的 USB 线、面包板、跳线、四个普通红色指示 LED,以及四个独立的 1 kΩ 电阻,额定功率至少 0.125 W。OLED 明确选用 Adafruit 4440:SSD1306 控制器、128 × 32 像素、I²C 地址 0x3C,并带自动复位。使用合适的 STEMMA QT 转接线,或者已经正确焊接的排针连接。

厂家明确支持这一具体模块使用 5 V 逻辑。这不代表所有外观相似的 OLED 都能直接接 Uno。必须核实控制器、引脚排列、供电与逻辑电平。SH1106 屏幕、需要独立复位线的旧版板卡,都不能照搬本例。先看产品编号和板上标识,再接通电源。

Uno R3 足以完成这次小型文本显示练习。有限的运行内存也是从简单内容开始的理由。如果以后需要更大的图形界面、更多设备或更复杂的数据处理,可查看微控制器对比,按照任务选择板卡,而不是只比较时钟频率。

连接四个 LED 与 OLED

搭建或改动接线前先拔掉 USB。先建立公共地,再一次连接一个功能。每个 LED 必须有自己的串联电阻,不能让四个 LED 共用一个限流电阻。通常长脚为阳极,外壳平边对应阴极一侧。如果引脚已经剪短,应根据标识或元件资料确认,不能只凭长度猜测。

红色 LED 的正向压降若约为 2 V,5 V 输出经过 1 kΩ 电阻,电流约为 3 mA。这是适合桌面实验的较低电流。本接法不适用于灯带、大功率照明或其他外部电源。需要扩大照明规模时,先阅读LED 照明,确认驱动与供电方式。

Uno R3 与 Adafruit 4440 的固定接线
元件 / 信号连接方式检查要点
LED 1D2 → 1 kΩ → 阳极;阴极 → GND每个 LED 独立电阻
LED 2D3 → 1 kΩ → 阳极;阴极 → GND不能直接接到 D3
LED 3D4 → 1 kΩ → 阳极;阴极 → GND确认极性
LED 4D5 → 1 kΩ → 阳极;阴极 → GND确认极性
OLED VINUno 5V仅限这里指定的模块
OLED GNDUno GND所有部分共地
OLED SDAUno A4 / SDA两种标识指同一信号
OLED SCLUno A5 / SCL两种标识指同一信号
OLED RST不连接4440 自带自动复位

先约定每一项数据的意义

我们自行约定学习格式:V1,速度,转速,燃油,灯掩码,最后添加 LF 换行,串口速度为 115200 波特。只接受非负整数:速度 0–999,转速 0–20000,燃油 0–100,灯掩码 0–15。发送实际消息时只写数字,不加空格、单位或小数。这个练习每秒最多发送五条完整消息即可。

灯掩码是数值相加:LED 1 对应 1,LED 2 对应 2,LED 3 对应 4,LED 4 对应 8。因此 5 表示 LED 1 和 3 亮,15 表示全部亮,0 表示全部灭。屏幕上的四个灯状态从左到右依次是 LED 1 到 LED 4,所以 1010 在这里表示 LED 1 和 3 亮,并非让你按二进制数字的通常书写顺序重新理解它。

启动时显示 WAIT。有效消息会在串口返回 OK,屏幕显示 DATA。无效消息返回 ERR,不会只更新其中几个字段。超过三秒没有有效消息时,所有 LED 熄灭,屏幕显示 STALE,保留最后的数字并明确标记其已经过时。这些行为应在最初需求中写清,而不是出现问题后再临时补上。

本例中,S 表示 km/h 速度,R 表示每分钟转数,F 表示燃油百分比。程序原样显示收到的数字,今后若需换算单位,应由发送端完成。四个 LED 位只是测试指示,不预先绑定某个模拟器功能。

给 AI 一份完整任务说明

有效的 Arduino 提示词需要同时说明硬件、数据和可观察到的行为。把厂家链接和确认过的产品编号交给 AI,先要求它列出接线与尚未明确的问题。如果你的元件不同,应先解决差异,再采用生成的接线方案。下面是一份可以直接修改的完整任务说明:

请帮助没有编程经验的初学者制作一个驾驶舱学习模块。逐步解释,不要编造引脚或模拟器接口。
硬件:Arduino Uno R3;四个红色 LED 接 D2、D3、D4、D5,每个串联 1 kOhm,阴极接 GND。OLED 为 Adafruit 4440,SSD1306 128x32 I2C 0x3C;VIN=5V、GND=GND、SDA=A4、SCL=A5,自动复位,不接 RST。
任务:提供使用 Wire、Adafruit GFX、Adafruit SSD1306 的完整 Arduino 程序。通过 USB 串口以 115200 波特接收我自己的测试消息,每秒最多 5 行:V1,speed,rpm,fuel,flags 加 LF;也接受 CRLF。范围:speed 0..999、rpm 0..20000、fuel 0..100、flags 0..15。1、2、4、8 分别控制 LED1..4。
结果:四行文本显示数值和灯状态。启动 WAIT;有效消息 DATA/OK;错误消息 ERR 且不部分更新。3000 ms 没有有效数据时熄灭所有 LED,显示 STALE,并保留最后数值。
限制:固定 64 字节接收缓冲区;超长行一直丢弃到 LF;不使用动态 String 对象,不在 loop 中使用 delay;显示刷新间隔至少 200 ms。计算 SRAM,包括 OLED 的 512 字节图像缓冲区。说明通信与内存分配失败。
给出接线表、完整代码、库清单,并设计每个 LED、全部 LED、错误数值、超长消息和断流测试。明确区分编译通过与实际硬件验证。

准备、上传并理解示例程序

在 Arduino IDE 选择“Arduino Uno”和对应端口。通过库管理器安装 Adafruit SSD1306、Adafruit GFX Library 及其依赖 Adafruit BusIO。Wire 随 AVR 开发板包提供。把下载的程序放在同名 uno-led-oled 文件夹中打开,先执行编译检查,再上传到自己的开发板。

下面保留完整代码,也提供下载文件。程序按小批次处理串口接收,不使用 delay() 让主循环等待,显示每秒最多更新五次。不过 I²C 传输本身仍然需要时间,因此这不是硬实时保证。较长连线、额外总线设备都需要单独检查。若想添加新的效果,先保留当前能运行的版本。

图像缓冲区需要 512 字节,此外还有变量、库缓冲区与栈。IDE 给出的静态 RAM 数值不会完整包含运行时的这次分配。代码用 F() 把固定文本保留在闪存中,不使用 Arduino String 对象。编译成功只能说明工具链接受了代码,不能证明实际接线正确,也不能证明屏幕已经显示。

本程序已在本地针对 arduino:avr:uno 编译通过,使用 AVR 1.8.7、Adafruit SSD1306 2.5.9、GFX 1.11.9 和 BusIO 1.15.0:占用 15,168 字节闪存、614 字节静态 RAM,仍需另计 512 字节图像缓冲区。这里未进行实物接线测试。

下载完整 Arduino 示例程序

展开完整示例代码
// Cockpit Suite workshop learning example. This is not a simulator protocol.
// Board: Arduino Uno R3. OLED: Adafruit 4440, SSD1306 128x32, I2C 0x3C.
// OLED VIN=5V, GND=GND, SDA=A4, SCL=A5; auto-reset, no reset wire.
// LEDs: D2/D3/D4/D5 -> individual 1k resistor -> anode; cathodes -> GND.
// Send at most 5 lines/second: V1,speed,rpm,fuel,flags followed by LF.
// Example: V1,120,3500,65,5 (LED1 and LED3 on). CR before LF is accepted.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <string.h>

const uint8_t LED_PINS[4] = {2, 3, 4, 5};
const uint8_t OLED_ADDRESS = 0x3C;
const uint32_t DATA_TIMEOUT_MS = 3000;
const uint32_t SCREEN_INTERVAL_MS = 200;
Adafruit_SSD1306 display(128, 32, &Wire, -1);
char lineBuffer[64];
uint8_t lineLength = 0;
bool discardLine = false;
bool haveData = false;
bool oledReady = false;
uint16_t speedValue = 0, rpmValue = 0, fuelValue = 0, flagValue = 0;
uint32_t lastDataMs = 0, lastScreenMs = 0;

bool parseNumber(const char *&cursor, uint16_t limit, char separator,
                 uint16_t &result) {
  if (*cursor < '0' || *cursor > '9') return false;
  uint16_t value = 0;
  while (*cursor >= '0' && *cursor <= '9') {
    uint8_t digit = *cursor++ - '0';
    if (value > limit / 10 ||
        (value == limit / 10 && digit > limit % 10)) return false;
    value = value * 10 + digit;
  }
  if (*cursor != separator) return false;
  if (separator != '\0') ++cursor;
  result = value;
  return true;
}

bool parseLine(const char *line) {
  if (strncmp(line, "V1,", 3) != 0) return false;
  const char *cursor = line + 3;
  uint16_t speed, rpm, fuel, flags;
  if (!parseNumber(cursor, 999, ',', speed) ||
      !parseNumber(cursor, 20000, ',', rpm) ||
      !parseNumber(cursor, 100, ',', fuel) ||
      !parseNumber(cursor, 15, '\0', flags)) return false;
  speedValue = speed;
  rpmValue = rpm;
  fuelValue = fuel;
  flagValue = flags;
  lastDataMs = millis();
  haveData = true;
  return true;
}

void readSerial() {
  // Bound each pass so a continuous stream cannot starve the outputs.
  for (uint8_t count = 0; count < 32 && Serial.available(); ++count) {
    char character = (char)Serial.read();
    if (character == '\r') continue;
    if (character == '\n') {
      lineBuffer[lineLength] = '\0';
      if (!discardLine && parseLine(lineBuffer)) Serial.println(F("OK"));
      else Serial.println(F("ERR"));
      lineLength = 0;
      discardLine = false;
    } else if (!discardLine) {
      // NUL and non-printable input are not part of this text protocol.
      if (character < 32 || character > 126 ||
          lineLength >= sizeof(lineBuffer) - 1) {
        discardLine = true;
      } else {
        lineBuffer[lineLength++] = character;
      }
    }
  }
}

void renderScreen(bool fresh) {
  display.clearDisplay();
  display.setCursor(0, 0);
  display.print(F("S:")); display.print(speedValue);
  display.print(F(" R:")); display.println(rpmValue);
  display.print(F("F:")); display.print(fuelValue);
  display.print(F("% LED:"));
  for (uint8_t index = 0; index < 4; ++index)
    display.print(fresh && (flagValue & (1U << index)) ? '1' : '0');
  display.println();
  display.println(!haveData ? F("WAIT") : (fresh ? F("DATA") : F("STALE")));
  display.print(F("DEMO SERIAL"));
  display.display();
}

void setup() {
  for (uint8_t index = 0; index < 4; ++index) {
    digitalWrite(LED_PINS[index], LOW);
    pinMode(LED_PINS[index], OUTPUT);
  }
  Serial.begin(115200);
  Wire.begin();
#if defined(WIRE_HAS_TIMEOUT)
  Wire.setWireTimeout(25000, true);
#endif
  Wire.beginTransmission(OLED_ADDRESS);
  bool addressResponds = (Wire.endTransmission() == 0);
  oledReady = addressResponds &&
      display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS, false, false);
  if (oledReady) {
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    display.setTextWrap(false);
    renderScreen(false);
  } else {
    Serial.println(F("OLED ERROR: check model, wiring, address, RAM"));
  }
  Serial.println(F("READY: V1,speed,rpm,fuel,flags"));
}

void loop() {
  readSerial();
  uint32_t now = millis();
  bool fresh = haveData && (uint32_t)(now - lastDataMs) < DATA_TIMEOUT_MS;
  for (uint8_t index = 0; index < 4; ++index)
    digitalWrite(LED_PINS[index],
                 fresh && (flagValue & (1U << index)) ? HIGH : LOW);
  if (oledReady && (uint32_t)(now - lastScreenMs) >= SCREEN_INTERVAL_MS) {
    lastScreenMs = now;
    renderScreen(fresh);
  }
}

用明确的输入检查每一种结果

上传后,把串口监视器设置为 115200 波特,并选择“换行”或“NL & CR”。打开串口可能使 Uno 复位,应先等到 READY。首次启动时 OLED 应显示 WAIT,四个 LED 都应熄灭。若这一阶段就不符合预期,先不要连接模拟器。

发送 V1,120,3500,65,1,应只有 LED 1 亮。依次把最后一个值改成 2、4、8,单独检查每路。发送 V1,120,3500,65,15 应全部亮;最后一项为 0 时应全部灭。核对屏幕三个数字与发送值相同,再测试边界 V1,999,20000,100,15 和 V1,0,0,0,0。

故意发送 V1,120,3500,101,5 或少一个字段的消息,应返回 ERR,不能部分更新。超过 63 个字符的一整行应被丢弃,但下一条正确消息应恢复正常。随后等待超过三秒:所有灯灭,显示 STALE。再次发送有效消息,正常状态应恢复。把“预期结果”和“实际结果”分开记录,才能向别人准确说明问题。

直接对比:V1,120,3500,65,15 让四个灯全亮,V1,120,3500,65,0 让四个灯全灭,V1,120,3500,65,5 只点亮 LED 1 和 3。

出现问题时,先缩小范围

只有某个 LED 不亮时,先检查极性、该灯自己的电阻和指定引脚。整个 OLED 不亮时,先检查型号、电源、公共地、SDA/SCL 和地址。OLED ERROR 可能来自 I²C 无响应或图像内存分配失败。即使地址有响应,也还不能证明画面正确,仍需观察实际显示。

向 AI 提问时描述事实,例如:“Uno R3,4440 模块,程序未修改。出现 READY,发送 V1,120,3500,65,1 返回 OK,LED1 亮,但 OLED 黑屏。首先应该断电检查哪些连接?”编译错误要提供完整报错和库版本。要求每次只提出一项有理由的修改,不要同时更换五根线和整段程序。

扩展前保存可工作的程序,要求解释修改位置,之后重复测试。可参考故障排查建立固定流程。密码、私人配置、无关项目目录不应放进求助提示词。逐步理解一个小修改,比在不明原因的情况下反复替换整个程序更有助于掌握项目。

从学习数据走向模拟器数据

要使用真实模拟器数值,电脑端还需要有文档依据的数据路径:读取来源、换算单位、检查范围,再发送约定消息。这里的 V1 只是教学格式,不是承诺已经存在的 Cockpit Suite 设备接口。规划自动桥接之前,先确认设备接入方式。串口监视器和另一个程序通常不能同时独占同一串口。

每个灯都应有明确含义:它表示用户提出的开关请求,还是模拟器已经确认的状态?“没有数据”应怎样区别于“状态关闭”?数据映射和显示与仪表会继续说明这些问题。需要增加字段时,先修改数据约定,再改发送端和接收端,并增加对应测试。

如果主要目标是把按钮、轴做成 USB 游戏控制器,FreeJoy 是适用于相应硬件的另一条路线。这里的 Uno 程序使用 USB 串口,不会因此自动成为原生摇杆设备。根据任务选择方法,先让一条完整信号路径可靠工作,再扩充整个面板。

下一步可以和社区一起完成

Cockpit Suite 把支持的模拟器和设备汇集到同一软件环境中。项目虽然年轻,但已经有可用软件与真实搭建作为基础。适配好的组合可以简化准备步骤;自行编写的 Arduino 模块仍是需要学习和集成的项目。可复现的使用经验能帮助我们把这些过程做得更清楚。

欢迎在Cockpit Suite 论坛说明目标、元件清单和最后一次成功测试。我们希望你遇到问题时能够获得下一步帮助,并尽可能当天回复;较晚提出的问题可能稍后答复。工作坊也讨论 3D 打印、龙门 CNC 铣削以及 LightBurn 激光加工。欧洲及其他地区的活跃社区,可以把个人的小成功变成更多驾驶舱爱好者能使用的经验。

资料来源与技术依据

资料核查日期:2026 年 10 月 18 日。步骤、数据格式与程序为本教程自行设计;厂家资料不能替代实际接线测试。

继续完成下一个模块