- 图形学
- 音视频
【免费下载链接】openFrameworks
openFrameworks is a community-developed cross platform toolkit for creative coding in C++.
导读
本文以 openFrameworks 仓库中的 firmataExample 为骨架,完整讲解如何在 openFrameworks 应用中通过 Firmata 协议与 Arduino 开发板通信:从烧录 StandardFirmata 固件、指定串口与波特率建立连接,到配置数字/模拟引脚、监听引脚变化事件,再到通过sendPwm()、sendServo()、sendDigital()、sendAnalog()等 API 驱动 LED、呼吸灯和舵机。读完本文你将能够基于 ofArduino 类搭建一套完整的「C++ 创意编程 + 硬件控制」链路,并理解其底层 Firmata 消息机制。
一、示例概述与学习目标
firmataExample 位于 examples/communication/firmataExample,是 openFrameworks 通信(communication)类目下的硬件互联示例。它演示了使用Firmata 协议与 Arduino 通信的完整流程,对应的官方 README 见 examples/communication/firmataExample/README.md。
该示例覆盖以下核心能力:
- 连接 Arduino:设置串口设备路径与波特率(baud rate);
- 收发数字与模拟信号:读取数字输入引脚、模拟输入引脚,输出数字高低电平与 PWM 波形;
- 事件驱动:在 Arduino 端口上添加(
ofAddListener)与移除(ofRemoveListener)事件监听器。
在代码阅读与改写时,需要重点留意:
- 连接开发板的参数写法:
ard.connect("/dev/tty.usbmodemfd121", 57600),其中第一个参数是串口设备名,第二个是波特率; - 用
ofAddListener()/ofRemoveListener()配合setupArduino()处理连接就绪事件; - 控制台日志中连接状态的输出方式;
- 面向不同 Arduino 外设的输出函数,例如:
sendPwm()—— 脉冲宽度调制,用于 LED 呼吸/调光;sendServo()—— 控制舵机角度;sendDigital()—— 发送数字脉冲(HIGH / LOW);sendAnalog()—— 发送模拟量(0–255 范围)。
二、运行前提:为 Arduino 烧录 StandardFirmata
运行该示例前必须先在 Arduino 上烧录 Firmata 固件。官方文档给出的步骤如下:
- 连接 Arduino 到电脑;
- 打开 Arduino IDE,依次进入File -> Examples -> Firmata,选择StandardFirmata;
- 针对你的开发板编译并上传 StandardFirmata;
- 上传完成后关闭 Arduino IDE(避免其独占串口),再运行 openFrameworks 应用。
这一点在源码注释中也有明确说明(见 ofApp.cpp 头部注释),并且建议优先使用 Arduino 1.0 及以上版本的 IDE,因为 Firmata 引脚编号从 2.3 版本(随 Arduino 1.0 发布)起发生了变化。
同时需要注意硬件兼容性边界:ofArduino当前只支持基于ATMega168 / ATMega328微控制器的标准 Arduino 板(UNO、Duemilanove、Diecimila、NG 等),Arduino FIO 与 Arduino Mini 也可以工作;而基于其他微控制器的Arduino MEGA 等板型目前不被支持。这一限制同样记录在 ofApp.cpp 的源码注释中,选板前务必确认。
三、连接流程:串口、波特率与初始化事件
3.1 connect():建立串口连接
示例在setup()中完成连接(ofApp.cpp):
void ofApp::setup(){ ofSetVerticalSync(true); ofSetFrameRate(60); ofBackground(255,0,130); buttonState = "digital pin:"; potValue = "analog pin:"; bgImage.load("background.png"); font.load("franklinGothic.otf", 20); smallFont.load("franklinGothic.otf", 14); // replace the string below with the serial port for your Arduino board // you can get this from the Arduino application or via command line // for OSX, in your terminal type "ls /dev/tty.*" to get a list of serial devices ard.connect("/dev/tty.usbmodemfd121", 57600); // listen for EInitialized notification. this indicates that // the arduino is ready to receive commands and it is safe to // call setupArduino() ofAddListener(ard.EInitialized, this, &ofApp::setupArduino); bSetupArduino = false; // flag so we setup arduino when its ready, you don't need to touch this :) }connect()的声明位于 ofArduino.h:
bool connect(const std::string & device, int baud = 57600);device:串口设备路径,可从 Arduino IDE 或命令行获取。macOS 上可用ls /dev/tty.*查看串口设备列表,Linux 下通常是/dev/ttyACM0或/dev/ttyUSB0,Windows 下则是COM3之类的端口名;baud:波特率,默认值为 57600。StandardFirmata 默认的串口波特率即 57600,一般无需修改。
从底层实现看,connect()会调用ofSerial::setup()打开串口,随后立刻发送sendFirmwareVersionRequest()请求固件版本(ofArduino.cpp)。Arduino 上电/复位后会自动上报版本信息,ofArduino在解析到版本号后完成引脚初始化,并触发EInitialized事件。
3.2 EInitialized 事件:连接就绪的信号
由于串口数据到达存在延迟,不能在connect()返回后立即发送引脚配置命令。正确的姿势是监听EInitialized事件:
ofAddListener(ard.EInitialized, this, &ofApp::setupArduino);EInitialized在 ofArduino.h 中定义:
/// \brief Triggered when the firmware version is received upon connect, /// the major firmware version is passed as an argument. From this point /// it's safe to send to the Arduino. ofEvent <const int> EInitialized;它的触发点在 ofArduino.cpp 的 initPins():ofArduino收到 Arduino 启动时自动上报的 Firmata 版本后,会初始化引脚数据结构并调用ofNotifyEvent(EInitialized, _majorFirmwareVersion, this)。此时才「可以安全地向 Arduino 发送命令」。
示例中的处理函数setupArduino()在回调中做了三件事:
void ofApp::setupArduino(const int & version) { // remove listener because we don't need it anymore ofRemoveListener(ard.EInitialized, this, &ofApp::setupArduino); // it is now safe to send commands to the Arduino bSetupArduino = true; // print firmware name and version to the console ofLogNotice() << ard.getFirmwareName(); ofLogNotice() << "firmata v" << ard.getMajorFirmwareVersion() << "." << ard.getMinorFirmwareVersion(); ... }值得注意:回调执行完后立即ofRemoveListener()移除监听器,避免重复触发;同时通过bSetupArduino = true标志位,配合updateArduino()中的判断,保证「连接就绪前不发送任何命令」。固件名与版本号则通过getFirmwareName()、getMajorFirmwareVersion()、getMinorFirmwareVersion()打印到控制台,作为连接状态的日志依据。
ofArduino还提供轮询式判断isArduinoReady()(ofArduino.h),但头文件注释明确说明它「不推荐使用」,首选方式就是监听EInitialized事件。
四、引脚配置:数字、模拟、PWM 与舵机
setupArduino()是整套引脚配置的核心(ofApp.cpp):
void ofApp::setupArduino(const int & version) { ofRemoveListener(ard.EInitialized, this, &ofApp::setupArduino); bSetupArduino = true; ofLogNotice() << ard.getFirmwareName(); ofLogNotice() << "firmata v" << ard.getMajorFirmwareVersion() << "." << ard.getMinorFirmwareVersion(); // set pins D2 and A5 to digital input ard.sendDigitalPinMode(2, ARD_INPUT); ard.sendDigitalPinMode(19, ARD_INPUT); // pin 21 if using StandardFirmata from Arduino 0022 or older // set pin A0 to analog input ard.sendAnalogPinReporting(0, ARD_ANALOG); // set pin D13 as digital output ard.sendDigitalPinMode(13, ARD_OUTPUT); // set pin A4 as digital output ard.sendDigitalPinMode(18, ARD_OUTPUT); // pin 20 if using StandardFirmata from Arduino 0022 or older // set pin D11 as PWM (analog output) ard.sendDigitalPinMode(11, ARD_PWM); // attach a servo to pin D9 // servo motors can only be attached to pin D3, D5, D6, D9, D10, or D11 ard.sendServoAttach(9); // Listen for changes on the digital and analog pins ofAddListener(ard.EDigitalPinChanged, this, &ofApp::digitalPinChanged); ofAddListener(ard.EAnalogPinChanged, this, &ofApp::analogPinChanged); }4.1 引脚编号规则:Firmata 2.3 之后的关键变化
源码注释给出了一个极易踩坑的规则(ofApp.cpp):
Note: pins A0 - A5 can be used as digital input and output. Refer to them as pins 14 - 19 if using StandardFirmata from Arduino 1.0. If using Arduino 0022 or older, then use 16 - 21. Firmata pin numbering changed in version 2.3 (which is included in Arduino 1.0)
也就是说:
- Arduino 1.0 / Firmata 2.3+:模拟引脚 A0–A5 作为数字引脚使用时编号为14–19;
- Arduino 0022 及更早版本:对应编号为16–21。
因此示例中sendDigitalPinMode(19, ARD_INPUT)配置的是 A5 引脚,sendDigitalPinMode(18, ARD_OUTPUT)配置的是 A4 引脚,并都在注释中标注了旧版固件的备选编号(21 / 20)。
4.2 引脚模式常量(ARD_*)
模式常量定义在 ofArduino.h:
| 常量 | 值 | 含义 |
|---|---|---|
ARD_INPUT | 0x00 | 数字输入 |
ARD_OUTPUT | 0x01 | 数字输出 |
ARD_ANALOG | 0x02 | 模拟输入模式 |
ARD_PWM | 0x03 | 数字引脚 PWM 输出模式 |
ARD_SERVO | 0x04 | 舵机输出模式 |
ARD_SHIFT | 0x05 | 移位寄存器模式 |
ARD_I2C | 0x06 | I2C 模式 |
ARD_ONEWIRE | 0x07 | 1-Wire 模式 |
ARD_STEPPER | 0x08 | 步进电机模式 |
ARD_ENCODER | 0x09 | 旋转编码器模式 |
ARD_SERIAL | 0x0A | 串口通信模式 |
ARD_INPUT_PULLUP | 0x0B | 数字输入(启用内部上拉电阻) |
数字高低电平另有ARD_HIGH(1)与ARD_LOW(0)两个常量(ofArduino.h)。
sendDigitalPinMode()底层会先检查目标引脚能力表pinCapabilities(该表由 Capability Query 握手获得),确认该引脚支持所选模式后才发送SET_PIN_MODE消息;若引脚被设置为ARD_INPUT/ARD_INPUT_PULLUP,还会自动开启该端口的数字上报(ofArduino.cpp)。需要留意头文件中的一条警告:如果任意一个模拟引脚被设为ARD_INPUT,所有模拟引脚的模拟上报都会被关闭——数字与模拟模式之间存在互斥关系。
4.3 PWM 与舵机的硬件限制
- PWM:示例将 D11 设为
ARD_PWM。根据 ofArduino.h 的说明,Arduino Uno 上支持 PWM 的引脚为3、5、6、9、10、11; - 舵机:
sendServoAttach(9)将舵机挂载到 D9。源码注释(ofApp.cpp)明确指出舵机只能挂载到D3、D5、D6、D9、D10、D11引脚。
sendServoAttach()的完整签名是:
void sendServoAttach(int pin, int minPulse = 544, int maxPulse = 2400);其实现会发送SERVO_CONFIG的 SysEx 消息,包含引脚号以及最小/最大脉冲宽度(默认 544µs / 2400µs),随后将引脚模式置为ARD_SERVO(ofArduino.cpp)。
五、主循环与事件驱动:update() 与引脚变化回调
5.1 每帧必须调用 ard.update()
update()每帧调用updateArduino(),而updateArduino()的第一步就是:
void ofApp::updateArduino(){ // update the arduino, get any data or messages. // the call to ard.update() is required ard.update(); // do not send anything until the arduino has been set up if (bSetupArduino) { // fade the led connected to pin D11 ard.sendPwm(11, (int)(128 + 128 * sin(ofGetElapsedTimef()))); // pwm... } }ard.update()是必须的:它读取串口缓冲区中所有可用字节并交给processData()解析(ofArduino.cpp)。不调用它,Arduino 上报的引脚数据就永远不会被处理,事件也不会触发。
连接就绪后,示例用一个正弦波驱动 D11 引脚做 PWM 呼吸灯效果:
ard.sendPwm(11, (int)(128 + 128 * sin(ofGetElapsedTimef())));sin()输出 -1~1,映射后 PWM 值在0–255之间平滑变化,ofGetElapsedTimef()提供时间基准。sendPwm()底层发送的是ANALOG_MESSAGE | pin消息,并把 0–255 的数值拆成两个 7-bit 字节传输(ofArduino.cpp)。
5.2 引脚变化事件:EDigitalPinChanged / EAnalogPinChanged
setupArduino()的最后注册了两个事件监听器:
ofAddListener(ard.EDigitalPinChanged, this, &ofApp::digitalPinChanged); ofAddListener(ard.EAnalogPinChanged, this, &ofApp::analogPinChanged);对应的事件定义在 ofArduino.h:
EDigitalPinChanged:数字引脚值变化时触发,参数为变化的引脚号;EAnalogPinChanged:模拟引脚值变化时触发,参数为变化的引脚号。
回调实现(ofApp.cpp):
void ofApp::digitalPinChanged(const int & pinNum) { buttonState = "digital pin: " + ofToString(pinNum) + " = " + ofToString(ard.getDigital(pinNum)); } void ofApp::analogPinChanged(const int & pinNum) { potValue = "analog pin: " + ofToString(pinNum) + " = " + ofToString(ard.getAnalog(pinNum)); }配套的读取 API:
getDigital(pin):数字引脚当前值(0/1)。若引脚为ARD_INPUT返回最近接收到的值,若为ARD_OUTPUT返回最近写入的值(ofArduino.cpp);getAnalog(pin):模拟引脚当前读数。Arduino 内置10-bit ADC,因此返回范围是0–1023(ofArduino.h)。
需要注意的细节:被当作数字引脚使用的模拟引脚(如 A5),其变化由digitalPinChanged处理,而不是analogPinChanged(ofApp.cpp 注释明确说明)。
六、交互控制:鼠标开关 LED、键盘旋转舵机
6.1 鼠标点击:控制板载 LED
void ofApp::mousePressed(int x, int y, int button){ // turn on the onboard LED when the application window is clicked ard.sendDigital(13, ARD_HIGH); } void ofApp::mouseReleased(int x, int y, int button){ // turn off the onboard LED when the application window is clicked ard.sendDigital(13, ARD_LOW); }鼠标按下时向 D13(Arduino 板载 LED 所在引脚)发送ARD_HIGH,松开时发送ARD_LOW,实现「点击点亮、松开熄灭」。sendDigital()的实现按端口位运算维护端口值(port = (pin >> 3) & 0x0F),值变化时才发送DIGITAL_MESSAGE | port消息(ofArduino.cpp)。
6.2 左右方向键:控制舵机与 A4 引脚
void ofApp::keyPressed (int key){ switch (key) { case OF_KEY_RIGHT: // rotate servo head to 180 degrees ard.sendServo(9, 180, false); ard.sendDigital(18, ARD_HIGH); // pin 20 if using StandardFirmata from Arduino 0022 or older break; case OF_KEY_LEFT: // rotate servo head to 0 degrees ard.sendServo(9, 0, false); ard.sendDigital(18, ARD_LOW); // pin 20 if using StandardFirmata from Arduino 0022 or older break; default: break; } }- 右方向键:
sendServo(9, 180, false)将 D9 舵机转到 180°,同时 A4(数字编号 18)输出高电平; - 左方向键:
sendServo(9, 0, false)将舵机转到 0°,A4 输出低电平。
sendServo()的第三个参数force为false,表示只在目标值变化时才发送消息(避免重复占用串口带宽)。其底层实现中,pin ≤ 15 时走ANALOG_MESSAGE通道,pin > 15 时走EXTENDED_ANALOGSysEx 消息(ofArduino.cpp)。调用前必须已经执行过sendServoAttach(),否则会输出 "Servo Control is not configured for pin" 错误日志。
七、界面反馈:连接状态与数据可视化
draw()负责把硬件状态实时绘制到窗口(ofApp.cpp):
void ofApp::draw(){ bgImage.draw(0,0); ofEnableAlphaBlending(); ofSetColor(0, 0, 0, 127); ofDrawRectangle(510, 15, 275, 150); ofDisableAlphaBlending(); ofSetColor(255, 255, 255); if (!bSetupArduino){ font.drawString("arduino not ready...\n", 515, 40); } else { font.drawString(potValue + "\n" + buttonState + "\nsending pwm: " + ofToString((int)(128 + 128 * sin(ofGetElapsedTimef()))), 515, 40); ... } }- 背景加载
background.png(Arduino 与面包板示意图),启动后即可看到示例截图中的界面; - 连接未就绪时显示"arduino not ready...";
- 连接就绪后,左上角信息面板实时显示:当前模拟引脚读数(
potValue)、数字引脚状态(buttonState)以及正在发送的 PWM 值; - 底部小字提示操作方式:「如果连接了舵机,使用左/右方向键分别逆时针/顺时针旋转」。
八、Firmata 协议底层:ofArduino 的消息机制
理解示例背后的协议层,有助于排查串口通信问题。ofArduino对 Firmata 协议的实现在 ofArduino.cpp 中,关键常量定义于 ofArduino.h。
8.1 协议版本与消息命令
- 协议版本:
FIRMATA_MAJOR_VERSION2、FIRMATA_MINOR_VERSION5(即 Firmata 2.5,兼容 2.x 固件,ofArduino.h); - 单条消息最大数据字节数:
FIRMATA_MAX_DATA_BYTES64。
示例中每条 API 对应的底层命令字节:
| ofArduino API | 底层命令 | 说明 |
|---|---|---|
connect()→sendFirmwareVersionRequest() | REPORT_FIRMWARE(0x79, SysEx) | 请求固件名与版本 |
sendDigitalPinMode() | SET_PIN_MODE(0xF4) | 设置引脚模式 |
sendDigital() | DIGITAL_MESSAGE(0x90) | 按端口(8 个引脚一组)发送数字值 |
sendPwm()/sendServo()(pin≤15) | ANALOG_MESSAGE(0xE0) | 按引脚发送 PWM/舵机值 |
sendAnalogPinReporting() | REPORT_ANALOG(0xC0) | 开启模拟引脚上报 |
sendServoAttach() | SERVO_CONFIG(0x70, SysEx) | 配置舵机脉冲宽度 |
sendServo()(pin>15) | EXTENDED_ANALOG(0x6F, SysEx) | 扩展模拟输出 |
其中 0x80–0xFF 是普通命令字节,0x00–0x7F 范围的数据通过 SysEx 扩展命令集(START_SYSEX0xF0 /END_SYSEX0xF7 包裹)传输。
8.2 数据收发与解析管线
- 发送:所有数值都以「两个 7-bit 字节」形式编码传输(
sendValueAsTwo7bitBytes),保证数据字节最高位始终为 0,从而与命令字节(最高位为 1)区分开; - 接收:
update()每帧轮询ofSerial缓冲区,processData()按字节解析多字节消息,SysEx 消息累积后交给processSysExData()分发,最终转化为EDigitalPinChanged、EAnalogPinChanged、EInitialized等事件抛出(ofArduino.cpp 起为 SysEx 解析逻辑)。
这意味着事件监听回调并不是在update()的同一栈上同步、即时触发,而是「先收数据、再解析、再通知」的异步事件模型——这正是示例把ard.update()放在每帧最前面的原因。
九、主程序入口与移植到自己的项目
示例的入口 main.cpp 是标准的 openFrameworks 窗口程序:
int main( ){ ofGLWindowSettings settings; settings.setSize(800, 600); settings.windowMode = OF_WINDOW; //can also be OF_FULLSCREEN auto window = ofCreateWindow(settings); ofRunApp(window, std::make_shared<ofApp>()); ofRunMainLoop(); }窗口尺寸 800×600,窗口模式运行。ofApp类的成员声明位于 ofApp.h,核心成员包括:
ofArduino ard;—— 通信对象;bool bSetupArduino;—— 连接就绪标志位;string buttonState / potValue;—— 界面显示用的状态字符串;- 私有回调:
setupArduino()、digitalPinChanged()、analogPinChanged()、updateArduino()。
将本示例移植到自己的项目时,需要改动的最小集合是:
- 把
ard.connect()的串口设备名替换为你自己开发板的端口; - 按实际接线修改
setupArduino()中的引脚配置(输入/输出/PWM/舵机); - 保留
ard.update()的每帧调用与bSetupArduino就绪判断; - 需要更深一层串口调试时,可参考同目录下的 serialExample 了解
ofSerial的底层用法。
十、常见问题排查
- 一直显示 "arduino not ready...":检查 StandardFirmata 是否已成功烧录、串口设备名是否正确、Arduino IDE 是否仍占用串口(务必关闭 IDE 再运行应用);
- 舵机不动:确认舵机挂在 D3/D5/D6/D9/D10/D11 之一,且调用过
sendServoAttach();控制台出现 "Servo Control is not configured for pin" 即说明未先 attach; - PWM 无效:确认引脚在 Uno 的 PWM 引脚列表(3、5、6、9、10、11)内,且模式设为
ARD_PWM;getPwm()会校验pinCapabilities,不支持时输出错误日志; - 模拟读数异常:确认
sendAnalogPinReporting(0, ARD_ANALOG)已开启上报,且没有其他模拟引脚被设为ARD_INPUT(会导致全部模拟上报关闭); - 使用了 MEGA 等板型:当前
ofArduino仅支持 ATMega168/328 系列,换板前先确认兼容性; - 引脚编号对不上:区分 Firmata 2.3+(A0–A5 = 数字 14–19)与旧版固件(16–21)两套编号。
结语
firmataExample 是 openFrameworks 与 Arduino 硬件交互的入门典范:通过ofArduino这个对 Firmata 协议的完整封装,开发者可以像操作普通对象一样读写数字/模拟引脚、输出 PWM、控制舵机,并通过ofEvent事件模型获得实时的引脚变化通知。理解了本文的连接流程、引脚编号规则、事件模型与底层消息机制,你就能在此基础上构建更复杂的创意硬件项目——例如传感器数据驱动的实时可视化、多路舵机交互装置等,而无需关心串口协议的细节。
- 图形学
- 音视频
【免费下载链接】openFrameworks
openFrameworks is a community-developed cross platform toolkit for creative coding in C++.
相关推荐
为什么你需要Weave Router:一个端点接管全部LLM,40-70%成本削减的完整逻辑
为什么你需要Weave Router:一个端点接管全部LLM,40 70%成本削减的完整逻辑 Weave Router 是一个开源的 LLM 智能路由代理(Mo
后端LLM 网关大模型MicroPython 在 ESP8266 上的 PWM 脉冲宽度调制实战指南:LED 呼吸灯与舵机控制
MicroPython 在 ESP8266 上的 PWM 脉冲宽度调制实战指南:LED 呼吸灯与舵机控制 导读 本文围绕官方教程 docs/esp8266/tu
嵌入式语言运行时编程语言解释器编译器物联网系统编程openFrameworks 串口通信实战:用 ofSerial 与 Arduino 建立双向通信
openFrameworks 串口通信实战:用 ofSerial 与 Arduino 建立双向通信 openFrameworks 提供了跨平台的 ofSeria
图形学音视频
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考