☰
U8g2 在 ARM Linux 上的用户态驱动移植:基于 c-periphery 的 GPIO 字符设备、I2C 与 SPI 实战指南
2026/10/7 2:04:56 网站建设 项目流程
  • 嵌入式
  • 嵌入式GUI
  • 驱动开发

【免费下载链接】u8g2

U8glib library for monochrome displays, version 2

项目地址:https://gitcode.com/gh_mirrors/u8/u8g2
点击查看免费下载

导读

U8g2 是面向单色 OLED/LCD 显示器的通用图形库,而本文讲解的是它在 ARM Linux(树莓派、NanoPi、OrangePi 等 SBC)上的移植分支:该移植不再依赖过时的 sysfs GPIO 接口,而是基于 c-periphery 用户态库直接操作/dev/gpiochip*字符设备,同时支持 I2C、SPI 总线访问。读完本文,你将掌握如何在 ARM Linux 上完成非 root 权限配置、修改并编译四种典型显示接口示例(硬件 SPI、软件 SPI、硬件 I2C、软件 I2C),以及如何使用 pthread 在同一进程内驱动多块显示屏。


一、移植背景:从 sysfs 到 GPIO 字符设备

Linux 内核的 GPIO 访问历史上经历过两个阶段:早期广泛使用的/sys/class/gpiosysfs 接口,以及内核 4.8 之后引入的gpiochip字符设备(/dev/gpiochip0、/dev/gpiochip1等)。本文对应的 arm-linux 移植分支(仓库中位于 sys/arm-linux/README.md)选择了后者,其核心变化如下:

  • 不再使用 sysfs 操作 GPIO:字符设备接口速度更快、稳定性更好,但为了兼容旧内核,仍保留 sysfs 编译选项;
  • GPIO、I2C、SPI 资源可关闭和释放:程序退出时可以close并归还句柄,避免资源泄漏;
  • 整体性能更好:字符设备 ioctl 调用比写 sysfs 伪文件系统效率更高;
  • 可以非 root 用户运行:配合 udev 规则或启动脚本授权设备节点即可;
  • 线程安全、支持多屏:每个显示屏拥有独立的用户数据结构与帧缓冲,可多线程并行驱动。

从源码结构看,这套移植被组织在 sys/arm-linux/ 目录下,分为port/(U8g2 与 Linux 设备对接的移植层)、examples/(C/C++ 示例)、drivers/(c-periphery 的 GPIO/I2C/SPI 驱动封装)以及tools/等部分。另外,官方还提供了基于 Java 的 Java UIO U8g2 方案复用该 arm-linux 移植。

二、环境准备:下载 U8g2 源码

在 ARM Linux 设备(或开发机)上先获取 U8g2 仓库。文档给出的步骤是:

cd ~/ git clone --depth 1 https://github.com/olikraus/u8g2.git

--depth 1表示浅克隆,只拉取最新提交,可以显著减少下载时间。克隆后,本文涉及的移植代码位于u8g2/sys/arm-linux/。如果你是在交叉编译环境中操作,请确认已安装对应的交叉工具链(详见下文“编译”一节)。

三、非 root 用户访问设备节点

默认情况下,/dev/gpiochip*、/dev/i2c-*、/dev/spidev*仅 root 可读写。若希望普通用户直接运行 U8g2 示例,需要做两件事:把用户加入专用组,并让设备节点对该组开放读写权限。

1. 创建用户组并加入用户

sudo groupadd u8g2 sudo usermod -a -G u8g2 username # username 换成你的非 root 用户名

2. 开机时设置设备节点权限(写入/etc/rc.local)

chown -R root:u8g2 /dev/gpiochip* chmod -R ug+rw /dev/gpiochip* chown -R root:u8g2 /dev/i2c* chmod -R ug+rw /dev/i2c* chown -R root:u8g2 /dev/spidev* chmod -R ug+rw /dev/spidev*

3. 重启生效

sudo reboot

更规范的做法是编写 udev 规则代替rc.local:udev 可以在设备节点创建时自动设置属主与权限,适合长期部署。无论哪种方式,核心目标都是让u8g2组成员对上述三类设备节点拥有读写权。

四、修改示例:以硬件 SPI 四线为例

文档推荐从四线硬件 SPI 示例入手,修改 sys/arm-linux/examples/c-examples/u8g2_4wire_hw_spi/u8g2_4wire_hw_spi.c。打开该文件后,需要按你的硬件调整三组宏:

// GPIO chip number for character device #define GPIO_CHIP_NUM 0 // SPI bus uses upper 4 bits and lower 4 bits, so 0x10 will be /dev/spidev1.0 #define SPI_BUS 0x10 #define OLED_SPI_PIN_RES 199 #define OLED_SPI_PIN_DC 198 // CS pin is controlled by linux spi driver, thus not defined here, but need to be wired #define OLED_SPI_PIN_CS U8X8_PIN_NONE

参数含义与取值规则:

宏含义取值说明
GPIO_CHIP_NUMGPIO 字符设备编号0对应/dev/gpiochip0
SPI_BUSSPI 总线与片选设备号高 4 位是总线号、低 4 位是设备号:0x10表示/dev/spidev1.0,0x20表示/dev/spidev2.0
OLED_SPI_PIN_RES显示屏 RESET 引脚使用该 GPIO chip 内的绝对引脚编号(NanoPi Duo 上为 199)
OLED_SPI_PIN_DC显示屏 DC(数据/命令)引脚同样为 GPIO 绝对编号(NanoPi Duo 上为 198)
OLED_SPI_PIN_CS片选引脚硬件 SPI 模式下由 Linux SPI 驱动接管,故置为U8X8_PIN_NONE,但电路上仍需接线

接下来看main()中的初始化流程,它体现了该移植的标准用法:

u8g2_t u8g2; // Initialization u8g2_Setup_ssd1306_128x64_noname_f(&u8g2, U8G2_R0, u8x8_byte_arm_linux_hw_spi, u8x8_arm_linux_gpio_and_delay); init_spi_hw(&u8g2, GPIO_CHIP_NUM, SPI_BUS, OLED_SPI_PIN_DC, OLED_SPI_PIN_RES, OLED_SPI_PIN_CS); u8g2_InitDisplay(&u8g2); u8g2_ClearBuffer(&u8g2); u8g2_SetPowerSave(&u8g2, 0); u8g2_SetFont(&u8g2, u8g2_font_ncenB08_tr); u8g2_DrawStr(&u8g2, 1, 18, "U8g2 HW SPI"); u8g2_SetFont(&u8g2, u8g2_font_unifont_t_symbols); u8g2_DrawGlyph(&u8g2, 112, 56, 0x2603); u8g2_SendBuffer(&u8g2); printf("Initialized ...\n"); sleep_ms(5000); u8g2_SetPowerSave(&u8g2, 1); // Close and deallocate SPI resources done_spi(); // Close and deallocate GPIO resources done_user_data(&u8g2); printf("Done\n");

几个关键点:

  • u8g2_Setup_ssd1306_128x64_noname_f选择 SSD1306 128×64 全帧缓冲(_f后缀)初始化函数;如果你的屏是其他控制器,替换为对应的u8g2_Setup_*即可;
  • 字节回调u8x8_byte_arm_linux_hw_spi与 GPIO/延时回调u8x8_arm_linux_gpio_and_delay都是本移植在 sys/arm-linux/port/u8g2port.h 中导出的接口;
  • 结束时调用done_spi()与done_user_data(&u8g2)显式关闭并释放 SPI、GPIO 句柄,这正是文档强调的“资源可关闭和释放”特性。

五、编译构建:字符设备与 sysfs 两种方式

在 sys/arm-linux/ 目录下执行编译。Makefile 顶层会递归进入examples子目录,examples/c-examples/Makefile 再通过通配符逐个构建u8g2*子目录,产物统一输出到bin/。

1. 使用 GPIO 字符设备(默认推荐)

cd ~/u8g2/sys/arm-linux make clean make CPPFLAGS=-DPERIPHERY_GPIO_CDEV_SUPPORT=1 CC=gcc CXX=g++

PERIPHERY_GPIO_CDEV_SUPPORT=1是关键宏:它让 sys/arm-linux/port/u8g2port.c 中init_pin()走gpio_open()打开/dev/gpiochip%d字符设备的分支。

2. 使用 GPIO sysfs(兼容旧内核)

cd ~/u8g2/sys/arm-linux make clean make CC=gcc CXX=g++

不定义PERIPHERY_GPIO_CDEV_SUPPORT时,init_pin()会调用gpio_open_sysfs()走/sys/class/gpio路径(见 u8g2port.c 中#if PERIPHERY_GPIO_CDEV_SUPPORT的分支选择)。老内核没有 gpiochip 字符设备时使用此方式。

3. 交叉编译工具链选择

如果直接在树莓派等设备本机编译,CC=gcc CXX=g++即可。交叉编译时需替换工具链前缀,各示例子目录的 Makefile 顶部给出了对照表:

目标平台编译器前缀
NanoPi、Raspberry Pi Zeroarm-linux-gnueabi-gcc
Raspberry Pi 2、OrangePi Zero、NanoPi NEO、NanoPi NEO Plus 2arm-linux-gnueabihf-gcc
C-SKY Linuxcsky-linux-gcc

子目录 Makefile 还会把 csrc/ 下的全部.c编译进目标,并链接 sys/arm-linux/port/u8g2port.o 与drivers/下的gpio.o、spi.o、i2c.o。构建完成后,可执行文件位于sys/arm-linux/bin/。

六、源码级原理:port 移植层如何工作

这一节深入 sys/arm-linux/port/u8g2port.c 与 sys/arm-linux/port/u8g2port.h,理解移植层的内核。

6.1 用户数据结构与多屏/线程安全

移植层用user_data_struct保存每个显示屏的独立状态:

struct user_data_struct { uint8_t gpio_chip; // /dev/gpiochip%d gpio_t *pins[U8X8_PIN_CNT]; // c-periphery GPIO 句柄 uint8_t bus; // I2C /dev/i2c-%d;SPI 高 4 位总线号 + 低 4 位设备号 uint8_t index; // 回调缓冲索引 uint8_t buffer[256]; // I2C 一次最多 32 字节、SPI 最多 128 字节 unsigned long delay; // I2C 延时(纳秒) unsigned int spi_mode; // SPI 模式 uint32_t max_speed; // SPI 最大速率 uint8_t *int_buf; // 内部帧缓冲 };

init_user_data()(u8g2port.c)会为每个显示实例动态分配独立的帧缓冲并调用u8g2_SetBufferPtr()绑定,同时把整个结构挂到u8g2_SetUserPtr()。注释明确指出“每个显示屏需要独立的缓冲才能线程友好”——这是多屏示例能够并行渲染的基础。而 I2C、SPI 句柄是全局静态数组(i2c_handles[8]、spi_handles[256]),因为同一总线可被多个设备共享(例如同一条 I2C 总线上地址不同的多块屏),且init_i2c()/init_spi()只在句柄为空时打开一次,天然实现“总线只开一次”。

6.2 GPIO 字符设备打开与写引脚

在PERIPHERY_GPIO_CDEV_SUPPORT开启时,init_pin()拼接/dev/gpiochip%d文件名并用gpio_open()以GPIO_DIR_OUT_HIGH模式打开引脚;未开启时退化为gpio_open_sysfs()。write_pin()对非U8X8_PIN_NONE的引脚执行gpio_write()。GPIO 与延时的统一回调u8x8_arm_linux_gpio_and_delay则把 U8x8 协议层的各类消息(U8X8_MSG_GPIO_*、U8X8_MSG_DELAY_*)映射到引脚写操作与nanosleep实现的毫秒/微秒/纳秒延时上。

6.3 硬件 I2C 与硬件 SPI 回调

  • u8x8_byte_arm_linux_hw_i2c:把U8X8_MSG_BYTE_SEND的字节暂存进user_data->buffer,在U8X8_MSG_BYTE_END_TRANSFER时组装成一条写消息i2c_transfer()发出,地址由u8x8_GetI2CAddress(u8g2) >> 1得出;
  • u8x8_byte_arm_linux_hw_spi:每次U8X8_MSG_BYTE_SEND直接调用spi_transfer(),并通过U8X8_MSG_BYTE_SET_DC联动u8x8_gpio_SetDC()切换数据/命令线;init_spi()按bus >> 4与bus & 0x0f拆出/dev/spidevX.Y,以spi_mode和max_speed打开设备。

关于 SPI 速率:init_spi_hw()调用init_spi_hw_advanced()时默认max_speed = 500000(500 kHz),同时 SPI 模式取自display_info->spi_mode。如果你的屏需要更高速度,可以改用init_spi_hw_advanced()自行指定模式与速率。

七、多屏多线程示例:三块屏并行渲染

多屏能力是这套移植的突出卖点,示例位于 sys/arm-linux/examples/c-examples/u8g2_sw_i2c_thread/u8g2_sw_i2c_thread.c。它的思路是:为每块屏建立一个 pthread,线程内各自初始化、绘制并刷新。

struct display { uint8_t gpio_chip; uint8_t scl; uint8_t sda; uint8_t res; unsigned long delay; };

main()中定义三个显示实例:

display_t display_1 = { 0, 14, 13, U8X8_PIN_NONE, 0 }; display_t display_2 = { 0, 15, 16, U8X8_PIN_NONE, 0 }; display_t display_3 = { 0, 198, 199, U8X8_PIN_NONE, 0 };

即三组gpio_chip / scl / sda / res / delay,随后用pthread_create()为每块屏创建线程,pthread_join()等待结束。每个线程执行do_display():

u8g2_Setup_ssd1306_i2c_128x32_univision_f(&u8g2, U8G2_R0, u8x8_byte_sw_i2c, u8x8_arm_linux_gpio_and_delay); init_i2c_sw(&u8g2, disp.gpio_chip, disp.scl, disp.sda, disp.res, disp.delay); u8g2_InitDisplay(&u8g2); h = u8g2_GetDisplayHeight(&u8g2); w = u8g2_GetDisplayWidth(&u8g2); u8g2_SetPowerSave(&u8g2, 0); for (int i = 1; i < 200; ++i) { u8g2_ClearBuffer(&u8g2); y1 = rand() % h; y2 = rand() % h; u8g2_DrawLine(&u8g2, 0, y1, w - 1, y2); u8g2_SendBuffer(&u8g2); sleep_ms(50); }

每 50 ms 画一条随机直线并刷新,循环 200 次后关闭电源、释放 GPIO 资源退出。文档特别提醒:

  • 修改display_1/2/3的引脚定义以匹配你的实际接线;如果只有两块屏,删掉对应的线程创建与 join 代码即可;
  • 示例刻意使用软件 I2C(u8x8_byte_sw_i2c),因为多数 SBC 没有三路硬件 I2C 控制器,而靠修改显示屏 I2C 地址又往往需要焊接电阻——软件 I2C 只需任意 GPIO 即可扩展出多路总线。该移植也因此支持最多 3 块软件 I2C 屏并行工作(见 u8g2port.c 头部注释)。

编译并运行:

cd ~/u8g2/sys/arm-linux make CPPFLAGS=-DPERIPHERY_GPIO_CDEV_SUPPORT=1 CC=gcc CXX=g++ cd bin ./u8g2_sw_i2c_thread

八、其他接口模式与 C++ 封装

除硬件 SPI 与多线程软件 I2C 外,examples/c-examples/ 还提供四套等价示例,覆盖主流连接方式:

示例目录接口说明
u8g2_4wire_hw_spi四线硬件 SPI见上文,CS 由内核 SPI 驱动接管
u8g2_4wire_sw_spi四线软件 SPI通过init_spi_sw()用 GPIO 模拟 SPI 时钟与数据
u8g2_hw_i2c硬件 I2C通过init_i2c_hw()指定/dev/i2c-%d总线号
u8g2_sw_i2c软件 I2C通过init_i2c_sw()指定 SCL/SDA 两个 GPIO
u8g2_sw_i2c_thread多线程软件 I2C三屏并行渲染示范

以 u8g2_sw_i2c/u8g2_sw_i2c.c 为例,软件 I2C 只需三个宏:

#define GPIO_CHIP_NUM 0 #define OLED_I2C_PIN_SCL 11 #define OLED_I2C_PIN_SDA 12 #define OLED_I2C_PIN_RESET U8X8_PIN_NONE

初始化时使用init_i2c_sw(&u8g2, GPIO_CHIP_NUM, SCL, SDA, RESET, 0),最后一个参数是 I2C 延时(纳秒),0表示不额外延时;若你的屏需要更慢的时钟,可传入非零值。

C++ 用户可参考 examples/cpp-examples/ 下对应的*_cpp示例,它们基于 sys/arm-linux/port/U8g2lib.h 的 C++ 封装,接口风格与 Arduino 版 U8g2lib 一致。

九、常见问题与注意事项

  • gpio_open()/i2c_open()报权限错误:多半是设备节点权限未配置,回到第三节完成u8g2组授权,或临时用 root 验证;
  • 设备节点不存在:确认内核已启用对应外设,例如树莓派需在config.txt开启dtparam=spi=on/dtparam=i2c_arm=on并重启;
  • SPI 速率不够:init_spi_hw()默认 500 kHz,可改用init_spi_hw_advanced()并传入更高max_speed与合适spi_mode;
  • 多屏线程数:示例默认三块屏,少于三块时删减线程即可;i2c_handles上限 8、spi_handles上限 256,理论可扩展更多;
  • 旧内核兼容:内核不含/dev/gpiochip*时,编译去掉PERIPHERY_GPIO_CDEV_SUPPORT=1走 sysfs 路径即可。

这套 arm-linux 移植让 U8g2 摆脱了对 Arduino/单片机环境的依赖,直接在 Linux 用户态以标准 C 结构驱动单色屏,配合 c-periphery 库在 GPIO 字符设备、I2C、SPI 三条通路上的封装,做到了非 root 运行、资源可回收、线程安全与多屏并行,适合在树莓派、NanoPi 等嵌入式 Linux 设备的仪表盘、信息屏类应用中落地。

  • 嵌入式
  • 嵌入式GUI
  • 驱动开发

【免费下载链接】u8g2

U8glib library for monochrome displays, version 2

项目地址:https://gitcode.com/gh_mirrors/u8/u8g2
点击查看免费下载
上一篇:3步实现Word文档智能整合:告别多文件管理混乱
下一篇:FlutterFire查询缓存策略指南文档:Firebase缓存步骤说明

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询