本文介绍开源项目 c-periphery,一个让嵌入式 Linux 开发者从"底层苦力"进化为"优雅工程师"的神器。
前言:你有没有这种经历
凌晨两点,你盯着屏幕,手里攥着数据手册,试图搞清楚为啥 /dev/spidev1.0 就是不给你好脸色。ioctl 的参数你已经查了三遍,errno 返回的是一个你背不出来的数字,Google 搜到的第一条结果还是2009年的 Stack Overflow……
恭喜你,这是嵌入式 Linux 应用层开发的标准入坑流程。
如果你在树莓派、BeagleBone、或者某块自研 ARM 板子上写过外设驱动的应用层代码,大概率体验过这种"原始人用打火石"的感觉:
- • 操作 GPIO?先去
/sys/class/gpio/export 里写个数字,再去找对应目录,再写 direction,再写 value…… - • 用 I2C?
ioctl(fd, I2C_RDWR, &rdwr_data) 你能一次性写对那个结构体? - • SPI?那几个 mode、bit_per_word、speed 的 ioctl 你每次都需要重查。
所有这些苦,都可以用 c-periphery 来结束。
c-periphery 是什么
一句话:一个把 Linux 外设原生 API 封装得干净利落的 C 库,支持 GPIO、LED、PWM、SPI、I2C、MMIO、Serial,MIT 协议,无任何外部依赖,编译成静态库直接链接,能跑在任何嵌入式 Linux 平台上。
GitHub 地址:https://github.com/vsergeev/c-periphery
目前 Star 数 1k+,Fork 数 293,持续维护,文档写得像 man page 一样规整(这在开源项目里真的是稀缺品质)。
更贴心的是,它还有"兄弟版本":
- • Python 版:
python-periphery(适合快速脚本验证)
所以不管你用啥语言,外设访问这块都有对应的封装。
实际上手:逐个击破
1. GPIO:再也不用对着 /sys 目录发呆
老方式(sysfs,已被内核标记为 deprecated):
echo 10 > /sys/class/gpio/exportecho"in" > /sys/class/gpio/gpio10/directioncat /sys/class/gpio/gpio10/value
这套写 Shell 还行,写 C 就得手动 open/write/read 一堆 fd,代码奇丑无比。
c-periphery 方式:
#include"gpio.h"gpio_t *gpio_in = gpio_new();gpio_t *gpio_out = gpio_new();// 打开 /dev/gpiochip0 的第10脚,输入方向gpio_open(gpio_in, "/dev/gpiochip0", 10, GPIO_DIR_IN);// 打开第12脚,输出方向gpio_open(gpio_out, "/dev/gpiochip0", 12, GPIO_DIR_OUT);bool value;gpio_read(gpio_in, &value); // 读输入gpio_write(gpio_out, !value); // 写输出(取反)gpio_close(gpio_in);gpio_close(gpio_out);gpio_free(gpio_in);gpio_free(gpio_out);
这套 API 背后用的是 Linux 4.8 引入的 character device GPIO(/dev/gpiochipN),彻底告别了 sysfs 那套早该退休的接口。代码逻辑清晰得就像在写伪代码。
真实项目案例:某工业网关项目需要用 GPIO 控制 RS485 收发切换(DE/~RE 引脚),原来写了一堆 open("/sys/class/gpio/...") 的代码,换用 c-periphery 后代码量减少了 60%,而且再也不会因为 sysfs 路径写错导致运行时崩溃。
2. I2C:告别 ioctl 噩梦
I2C 是嵌入式最常用的总线之一,连 EEPROM、温湿度传感器、OLED 屏幕全靠它。原生 Linux I2C 接口需要手工填充 struct i2c_rdwr_ioctl_data 和 struct i2c_msg,看着就头疼。
c-periphery 读取 EEPROM(地址 0x50,读第 0x100 字节):
#include"i2c.h"i2c_t *i2c = i2c_new();i2c_open(i2c, "/dev/i2c-0");uint8_t msg_addr[2] = { 0x01, 0x00 }; // 16位地址uint8_t msg_data[1] = { 0xff };structi2c_msgmsgs[2] = { { .addr = 0x50, .flags = 0, .len = 2, .buf = msg_addr }, // 写地址 { .addr = 0x50, .flags = I2C_M_RD, .len = 1, .buf = msg_data }, // 读数据};i2c_transfer(i2c, msgs, 2);printf("EEPROM[0x0100] = 0x%02x\n", msg_data[0]);i2c_close(i2c);i2c_free(i2c);
struct i2c_msg 是 Linux 内核原生结构体,c-periphery 直接复用,不做二次封装,保留了完整灵活性的同时免去了底层 ioctl 的繁琐。
真实项目案例:某楼宇环境监测设备挂了 SHT31 温湿度传感器(I2C 地址 0x44)。用 c-periphery 搭配传感器时序手册,半小时内就跑通了数据读取,之前用裸 ioctl 折腾了一整天。
3. SPI:刷屏不再是噩梦
SPI 常见于 Flash 存储、LCD 屏幕、ADC/DAC 芯片。原生 API 里的 SPI_IOC_MESSAGE ioctl 结构填起来绕死人。
c-periphery 示例:向 SPI Flash 发送 4 字节:
#include"spi.h"spi_t *spi = spi_new();// 打开 /dev/spidev1.0,模式0,最大速率 1MHzspi_open(spi, "/dev/spidev1.0", 0, 1000000);uint8_t buf[4] = { 0xaa, 0xbb, 0xcc, 0xdd };spi_transfer(spi, buf, buf, sizeof(buf)); // 全双工,发送同时接收printf("SPI 回环: 0x%02x 0x%02x 0x%02x 0x%02x\n", buf[0], buf[1], buf[2], buf[3]);spi_close(spi);spi_free(spi);
spi_transfer 一个函数搞定全双工传输,半双工可以传入 NULL 作为发送或接收缓冲区。SPI mode(CPOL/CPHA)直接在 spi_open 里指定,不用再查哪个 ioctl 对应哪个设置。
真实项目案例:某智能 POS 机的显示屏(ST7789 控制器,SPI 接口,时钟 40MHz,Mode 0),用 c-periphery 配合 DMA 缓冲区刷帧,驱动代码从 500 行裸 ioctl 精简到不到 100 行核心逻辑。
4. Serial:串口通信也可以很优雅
串口永远是嵌入式的硬通货:调试口、GNSS 模块、Zigbee 模组、工业 RS485……termios 那套 API 设计在上世纪,用起来像在做历史题。
c-periphery 串口收发:
#include"serial.h"serial_t *serial = serial_new();// 打开 /dev/ttyUSB0,115200波特率,默认 8N1,无流控serial_open(serial, "/dev/ttyUSB0", 115200);uint8_t tx[] = "AT\r\n";serial_write(serial, tx, sizeof(tx));uint8_t rx[128];int n = serial_read(serial, rx, sizeof(rx), 2000); // 2000ms 超时printf("收到 %d 字节: %s\n", n, rx);serial_close(serial);serial_free(serial);
超时参数直接在 serial_read 里指定,再也不用手动设置 VTIME、VMIN。需要非标准波特率或硬件流控?serial_open_advanced 都有。
真实项目案例:某 AGV 小车控制板通过串口和电机驱动器(Modbus RTU 协议)通信。用 c-periphery 做串口收发,上层再套一层 Modbus 帧解析,整个通信模块代码清晰到可以直接给客户看。
5. PWM:电机调速、蜂鸣器一把抓
#include"pwm.h"pwm_t *pwm = pwm_new();// 打开 PWM chip0 的通道10pwm_open(pwm, 0, 10);pwm_set_frequency(pwm, 1000.0); // 1kHzpwm_set_duty_cycle(pwm, 0.75); // 75% 占空比pwm_enable(pwm);// 动态调整占空比(比如马达调速)pwm_set_duty_cycle(pwm, 0.50); // 改为 50%pwm_close(pwm);pwm_free(pwm);
真实项目案例:某无人机地面站用 PWM 控制散热风扇转速,根据 CPU 温度动态调节。用 c-periphery 后,风扇控制逻辑写成了一个独立线程,50行代码搞定,还顺手加了过热保护。
6. MMIO:直接操作寄存器,老司机的快感
这个接口比较高阶,直接映射物理地址到用户空间,适合对特定 SoC 寄存器做精细操作的场景(比如读取 MAC 地址、操作 RTC 寄存器)。
#include"mmio.h"mmio_t *mmio = mmio_new();// 映射 AM335x 的 Control Module 基地址mmio_open(mmio, 0x44E10000, 0x1000);uint32_t mac_lo, mac_hi;mmio_read32(mmio, 0x630, &mac_lo);mmio_read32(mmio, 0x634, &mac_hi);printf("MAC: %08X%04X\n", __bswap_32(mac_hi), __bswap_16(mac_lo));mmio_close(mmio);mmio_free(mmio);
配合 volatile 结构体指针,还可以像操作硬件寄存器结构一样访问内存映射区域,读写速度和驱动层没有差别。
集成方式:三条路,各有千秋
方式一:CMake(推荐现代项目)
find_package(periphery REQUIRED)add_executable(my_app src/main.c)target_link_libraries(my_app PRIVATE periphery::periphery)
方式二:直接链接静态库
gcc -I/path/to/periphery/src main.c /path/to/periphery/periphery.a -o my_app
方式三:交叉编译(嵌入式主战场)
# 用 CMake 工具链文件export CC=arm-linux-gnueabihf-gccmkdir build && cd buildcmake -DCMAKE_TOOLCHAIN_FILE=/path/to/toolchain.cmake ..make# 或者 vanilla MakeCROSS_COMPILE=arm-linux-gnueabihf- make
静态库编译后就是个 .a 文件,丢进项目里就能用,没有运行时依赖,适合资源受限的嵌入式环境。
错误处理:这个库有点良心
每个函数返回负数表示错误,并提供两个辅助函数:
if (gpio_open(gpio, "/dev/gpiochip0", 10, GPIO_DIR_IN) < 0) {// gpio_errmsg():人类可读的错误描述fprintf(stderr, "GPIO 打开失败: %s\n", gpio_errmsg(gpio));// gpio_errno():底层 libc errnofprintf(stderr, "系统错误码: %d\n", gpio_errno(gpio));exit(1);}
这种设计让调试变得非常友好——不用猜,errmsg 直接告诉你哪里出了问题。I2C、SPI、Serial 等所有接口都有对应的 xxx_errmsg() 和 xxx_errno(),风格统一,一学全会。
适合什么场景用?
总结
c-periphery 解决的问题听起来不性感——"把 ioctl 封装一下"——但它把这件事做到了极致:
- • API 设计克制:不过度封装,保留了必要的灵活性
- • 文档质量高:每个接口都有 man page 级别的说明
- • 可移植性强:只依赖标准 C 库和 Linux,交叉编译无痛
如果你还在嵌入式 Linux 应用层裸写 ioctl,不妨试试 c-periphery。它不会让你的代码更快,但会让你的代码更值得被人类阅读。
毕竟,凌晨两点排查 bug 的人,值得一个更好用的库。
项目地址:https://github.com/vsergeev/c-periphery
Star 数:1k+ | License:MIT | 支持平台:任何嵌入式 Linux