- 嵌入式
- 嵌入式GUI
- 驱动开发
【免费下载链接】u8g2
U8glib library for monochrome displays, version 2
导读
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_NUM | GPIO 字符设备编号 | 0对应/dev/gpiochip0 |
SPI_BUS | SPI 总线与片选设备号 | 高 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 Zero | arm-linux-gnueabi-gcc |
| Raspberry Pi 2、OrangePi Zero、NanoPi NEO、NanoPi NEO Plus 2 | arm-linux-gnueabihf-gcc |
| C-SKY Linux | csky-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
相关推荐
Serial-Studio与嵌入式Linux:在ARM设备上的移植指南
Serial Studio与嵌入式Linux:在ARM设备上的移植指南 引言:嵌入式开发中的数据可视化痛点 你是否在ARM嵌入式项目中遇到过这些问题?调试传感器
桌面应用数据可视化物联网Arduino-ESP32外设驱动详解:GPIO、ADC、I2C、SPI实战应用
Arduino ESP32外设驱动详解:GPIO、ADC、I2C、SPI实战应用 引言:为什么选择Arduino ESP32进行嵌入式开发? 还在为ESP32复
嵌入式物联网驱动开发Gobot 驱动 FriendlyELEC NanoPi(NEO)开发板:GPIO、PWM、I2C、SPI 完整实战指南
Gobot 驱动 FriendlyELEC NanoPi(NEO)开发板:GPIO、PWM、I2C、SPI 完整实战指南 导读 :本文以 platforms/f
物联网机器人嵌入式
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考