为什么需要自定义库
Arduino 的原生示例代码适合快速验证,但真实项目中,传感器驱动、通信协议、业务逻辑通常需要跨项目复用。将代码封装为库,不仅能够隐藏硬件细节,还能通过 Arduino 库管理器实现一键安装。库的本质是一个带元数据的 C++ 模块集合,它被 Arduino IDE 或 Arduino CLI 自动发现并编译。
库的标准目录结构
一个标准的 Arduino 库必须包含以下内容(名称可选但有强烈约定):
MyKeypad/
├── src/ // 所有源文件(非必须,但推荐)
│ ├── MyKeypad.h
│ └── MyKeypad.cpp
├── examples/ // 示例程序
│ └── BasicRead/
│ └── BasicRead.ino
├── extras/ // 额外资源(数据表、图纸)
├── docs/ // 文档
├── keywords.txt // 语法高亮定义
├── library.properties // 库元数据(必需)
├── LICENSE // 开源许可证(发布必需)
└── README.md
两个关键文件:
-
library.properties:描述库名、版本、依赖、架构等,是库管理器识别库的“身份证”。 -
keywords.txt:纯文本格式,让 IDE 识别类、方法、常量并高亮。
构建系统如何工作
Arduino 构建器在编译前会扫描 src/ 下的所有 .h 和 .cpp 文件,并自动加入编译队列。它遵循两条重要规则:
-
每个
.cpp会被单独编译,但头文件不会被提前包含。因此你的库代码中,每个.cpp必须显式#include "MyKeypad.h"。 -
库依赖的其它库需要在
library.properties的depends字段中声明,构建器会按拓扑顺序编译。
此外,src/ 子目录中的文件会被递归扫描,推荐使用子目录划分模块(如 src/driver/、src/utils/)。
library.properties 详解
name=MyKeypad
version=1.0.0
author=Zhang San <zhangsan@example.com>
maintainer=Zhang San <zhangsan@example.com>
sentence=Arduino library for reading matrix keypads
paragraph=Support 4x4 and 4x3 keypads with internal pull-up.
category=Device Control
url=https://github.com/zhangsan/MyKeypad
architectures=avr,esp32,esp8266,stm32,sam
depends=Wire,SPI
-
category必须是官方枚举值:Device Control、Sensors、Signal Input/Output等。 -
architectures用逗号分隔支持的平台,*表示所有平台。 -
depends可留空,但如果有外部依赖,必须写明。
编写高质量库代码的要点
h和 cpp 分离,头文件防御
MyKeypad.h 中必须使用 #pragma once 或传统 include guard。
// src/MyKeypad.h
#pragma once
#include "Arduino.h"
enum KeyState : uint8_t {
RELEASED = 0,
PRESSED,
HOLD
};
class MyKeypad {
public:
MyKeypad(uint8_t* rowPins, uint8_t* colPins, uint8_t rows, uint8_t cols);
void begin();
char getKey();
KeyState getState(uint8_t row, uint8_t col);
private:
uint8_t* _rowPins;
uint8_t* _colPins;
uint8_t _rows;
uint8_t _cols;
void scan();
};
// src/MyKeypad.cpp
#include "MyKeypad.h"
MyKeypad::MyKeypad(uint8_t* rowPins, uint8_t* colPins, uint8_t rows, uint8_t cols)
: _rowPins(rowPins), _colPins(colPins), _rows(rows), _cols(cols) {
}
void MyKeypad::begin() {
for (uint8_t i = 0; i < _rows; i++) {
pinMode(_rowPins[i], INPUT_PULLUP);
}
for (uint8_t i = 0; i < _cols; i++) {
pinMode(_colPins[i], OUTPUT);
digitalWrite(_colPins[i], HIGH);
}
}
void MyKeypad::scan() {
// 实现矩阵扫描逻辑
}
char MyKeypad::getKey() {
scan();
// 返回按键字符
return '\0';
}
KeyState MyKeypad::getState(uint8_t row, uint8_t col) {
// 返回该位置状态
return RELEASED;
}
使用标准类型与命名规范
- 使用
uint8_t、int16_t等固定宽度类型,避免byte、int在 AVR 与 ARM 上的差异。 - 方法名采用驼峰,文件名为库名,类名与文件名保持一致。
- 内部成员用下划线前缀(
_rowPins),避免与外部变量冲突。
避免全局静态对象
不要在库中定义全局对象,除非它是单例且明确说明。否则容易导致内存浪费和初始化顺序问题。
编写示例程序
每个 examples/ 子目录对应一个 .ino 文件,它会被构建器识别为独立示例。示例必须能开箱即用,并演示最常用的 API:
// examples/BasicRead/BasicRead.ino
#include <MyKeypad.h>
uint8_t rowPins[4] = {5, 4, 3, 2};
uint8_t colPins[4] = {8, 7, 6, 9};
MyKeypad keypad(rowPins, colPins, 4, 4);
void setup() {
Serial.begin(115200);
keypad.begin();
}
void loop() {
char key = keypad.getKey();
if (key) {
Serial.println(key);
}
}
keywords.txt 格式
每行一注:关键字 类型,类型可取 KEYWORD1(类名)、KEYWORD2(方法或函数)、LITERAL1(常量)。
MyKeypad KEYWORD1
begin KEYWORD2
getKey KEYWORD2
getState KEYWORD2
RELEASED LITERAL1
PRESSED LITERAL1
HOLD LITERAL1
注意:关键字两侧必须用制表符 Tab 分隔,键后不能有空格。
本地测试库
将库文件夹放在 Arduino 的 libraries 目录下(~/Documents/Arduino/libraries),重启 IDE。在“项目 -> 加载库 -> 库管理器”中应能看到;编译示例,观察是否有错误。快速迭代建议使用命令行工具 arduino-cli:
arduino-cli compile --fqbn arduino:avr:uno --library /path/to/MyKeypad examples/BasicRead/BasicRead.ino
发布到官方库管理器
提交前准备
- 库名唯一,且与源码目录名一致。
-
library.properties中version符合语义化版本x.y.z。 - 必须包含
LICENSE文件(推荐 MIT 或 LGPL)。 - 所有路径和文件名不能含有空格或特殊字符。
主动提交
到 Arduino Library Registry 指南 按模板创建 Pull Request,需要提供 library.properties 仓库的 URL。审核通过后,库将会被库管理器收录。
保持更新的建议
- 每次在 GitHub 上打 tag(如
v1.0.1),库管理器会通过 Release 侦测自动更新。 - 修改
library.properties中的 version 后,必须同步提交发布。 - 在 README 中写明 API 变更日志。
常见注意事项
- 不要使用
delay()阻塞扫描,应基于millis()实现非阻塞逻辑,否则会干扰其他任务。 - 头文件不要使用非标准 C 库,如
avr/pgmspace.h,除非你的库仅面向 AVR。 - 库内资源(定时器、中断)必须提供
end()或deinit()方法,便于释放。 - 若库依赖 Arduino 核心,必须在每个
.cpp中#include "Arduino.h",且不能放在extern "C"中。 - 测试库时请在至少两个不同架构(如 AVR 和 ESP32)上编译,保证可移植性。
结语
从写一个普通 .ino 到设计一个可发布的库,意味着思维从“实现功能”转向“定义契约”。好的库应具备清晰接口、稳定行为和完备示例。遵循本文的目录结构与发布流程,你的库就能被全球开发者使用,也可作为个人技术积累的里程碑。关键在于:设计时多考虑重入性和可移植性,发布前多测试多打磨。