【瑞萨BLE/WIFI模块测评】Zephyr 驱动OLED
# Zephyr 驱动 OLEDSCI 简易 I2C 点亮 SSD1306> 瑞萨 RA4M2 开发板(RA-Eco-RA4M2-100PIN)上有个 4pin I2C 插座,接的是 **SCI3 的简易 I2C**(SCL3/SDA3),不是专用 IIC 外设。本文记录如何用 Zephyr 的 `renesas,ra-i2c-sci` 驱动在这对引脚上点亮 SSD1306 OLED,并把它封装成可复用模块。全文含实机 RTT 验证日志和两个实测踩出的驱动坑。
>
> 环境:Zephyr 4.4.0 / Zephyr SDK 1.0.1 / J-Link(RTT 日志)/ 板卡 `ek_ra4m2/r7fa4m2ad3cfp`。
!(https://www.eefocus.com/forum/data/attachment/forum/202609/10/175826aa14bnrxfax1r4x3.png)
## 1. 硬件:I2C 插座是 SCI3,不是 IIC
RA 系列 MCU 有两类 I2C:
- **IIC 外设**(专用 I2C 控制器,Zephyr 驱动 `renesas,ra-iic`)
- **SCI 简易 I2C**(串口外设兼任,Zephyr 驱动 `renesas,ra-i2c-sci`,底层是 FSP 的 `r_sci_i2c`)
本板的 I2C 插座原理图标 "SCI3":
| 插座引脚 | 信号 | MCU 引脚 |
|---|---|---|
| 1 | 3.3V | — |
| 2 | GND | — |
| 3 | SCL3 | P408 |
| 4 | SDA3 | P409 |
板载 **1.5K 上拉**,全板 3.3V 电平,OLED 模块(SSD1306,0.96 寸 128x64,I2C 地址 0x3C)直插即可。
## 2. 设备树配置(关键)
三件事:pinctrl 配开漏、使能 sci3 的 i2c 子节点、给个 alias 方便代码引用。
```dts
/* boards/ek_ra4m2.overlay */
#include <zephyr/dt-bindings/i2c/i2c.h>
/* SCL3=P408, SDA3=P409,必须开漏(外部已有 1.5K 上拉) */
&pinctrl {
sci3_i2c_default: sci3_i2c_default {
group1 {
psels = <RA_PSEL(RA_PSEL_SCI_3, 4, 8)>,
<RA_PSEL(RA_PSEL_SCI_3, 4, 9)>;
drive-open-drain;
};
};
};
/* pinctrl 挂在父节点 sci3 上(和 uart 用法一致),i2c 子节点单独使能 */
&sci3 {
pinctrl-0 = <&sci3_i2c_default>;
pinctrl-names = "default";
status = "okay";
i2c {
clock-frequency = <I2C_BITRATE_FAST>; /* 400kHz */
status = "okay";
};
};
/ {
aliases {
/* i2c 子节点没有 label,用路径 alias 引用 */
oled-i2c = &{/soc/sci3@40118300/i2c};
};
};
```
要点:
- **PSEL 值**:SCI3 的功能选择值是 `RA_PSEL_SCI_3`(=0x5,奇数 SCI 都是 0x5;偶数是 0x4,见 `include/zephyr/dt-bindings/pinctrl/renesas/pinctrl-ra.h`)。同一 PSEL 覆盖该 SCI 的所有角色(TX/RX/SCL/SDA),引脚角色由引脚本身决定。
- **`drive-open-drain` 不能少**:I2C 是线与结构,推挽输出会顶坏总线。
- **驱动自动使能**:Kconfig `I2C_RENESAS_RA_SCI` 是 `default y` + `depends on DT_HAS_RENESAS_RA_I2C_SCI_ENABLED`,设备树一使能驱动就编进来了,`prj.conf` 只需 `CONFIG_I2C=y`。
## 3. 驱动模块设计(src/oled_ssd1306.c/.h)
没有用 Zephyr 的显示子系统,而是应用层裸 I2C 驱动:
- SSD1306 协议很简单:I2C 写 `0x00 + 命令字节` 发命令,写 `0x40 + 数据` 写显存;
- 128x64 = 8 页 x 128 列,显存正好 1KB,整块缓存、整块刷;
- 字体直接复用 **Zephyr 内置 CFB 字体**(`${ZEPHYR_BASE}/subsys/fb/cfb_fonts.c` 里的 `cfb_font_1016`,10x16),extern 声明即可用,128x64 正好是 **12 列 x 4 行** 的字符界面。
对外 API 只有 5 个:
```c
intoled_init(void); /* 初始化,返回 0 或 i2c 错误码 */
intoled_flush(void); /* 1KB 显存整块刷屏 */
void oled_clear(void); /* 清显存(不刷屏) */
void oled_print(const char *s, uint8_t col, uint8_t row);/* 写字符格 */
const struct device *oled_i2c_bus(void); /* 总线句柄,诊断用 */
```
CMake 加两行:
```cmake
target_sources(app PRIVATE src/main.c src/oled_ssd1306.c
${ZEPHYR_BASE}/subsys/fb/cfb_fonts.c)
```
使用示例(src/main.c):
```c
int ret = oled_init();
printk("oled init ret=%d\n", ret);
oled_clear();
oled_print("RA4M2 OLED", 0, 0);
oled_print("SCI3 P408/P409", 0, 1);
oled_flush();
/* 主循环:每秒刷新一行 uptime */
snprintf(line, sizeof(line), "uptime %lus", sec++);
oled_print(line, 0, 2);
oled_flush();
```
## 4. 上板验证(实机 RTT 日志)
为了方便诊断,`main()` 里加了一段 **I2C 总线扫描**(逐个地址发 1 字节读,打印 ACK 的地址):
```c
for (uint16_t addr = 0x08; addr < 0x78; addr++) {
if (i2c_read(bus, &dummy, 1, addr) == 0) {
printk("i2c scan: 0x%02x ACK\n", addr);
}
}
```
烧录复位后 RTT 实抓:
```
*** Booting Zephyr OS build v4.4.0-24-g58069e646a84 ***
app start
oled init ret=0
i2c scan: 0x3c ACK
```
OLED 在 **0x3C** 应答,初始化成功,屏幕显示三行文字 + 每秒刷新的 uptime,LED 同时闪烁。编译体积:FLASH 48.6KB(9.3%)/ RAM 9.1KB。
## 5. 踩坑记录(都是实测)
| 坑 | 现象 | 解法 |
|---|---|---|
| **零长度写卡死** | 用 `i2c_write(bus, NULL, 0, addr)` 做总线扫描,程序直接挂住(FSP `r_sci_i2c` 等不到完成信号) | 扫描改用 **1 字节读** `i2c_read(bus, &b, 1, addr)` |
| **-5(-EIO)** | `oled init ret=-5` | 这是标准 **NACK**:设备没插、接触不良或地址错。不是驱动/配置问题。先看接线,再考虑 0x3D 地址 |
| pinctrl 没配开漏 | 总线电平异常、通信不可靠 | pinctrl 组加 `drive-open-drain` |
| i2c 子节点无法引用 | SoC dtsi 里 `sci3` 的 `i2c` 子节点没有 label | alias 用路径引用:`&{/soc/sci3@40118300/i2c}` |
| RTT Logger 无输出 | 0 Bytes/s | Zephyr 有 3 个 up-channel,日志在通道 0,加 `-RTTChannel 0` |
排查思路总结:**先扫描,再初始化**。扫描能 ACK 说明供电、接线、上拉、地址全对,剩下就是软件协议问题;扫描全 NACK 就是物理层问题,别在代码里浪费时间。
## 6. 复用清单
换项目/换板子要用这个 OLED 模块,只需要:
1. 拷 `src/oled_ssd1306.c` + `src/oled_ssd1306.h`;
2. overlay 里配好 I2C 控制器(IIC 或 SCI 都行,Zephyr 统一 `i2c` API)+ alias `oled-i2c`;
3. CMake 加源文件和 `cfb_fonts.c`;
4. `prj.conf` 加 `CONFIG_I2C=y`。
如果 OLED 是 0x3D 地址,改头文件里的 `SSD1306_ADDR` 一处即可。
## 7. OLED驱动源码
oled_ssd1306.c
```
/*
* SSD1306 OLED(128x64,I2C)裸驱动,接口见 oled_ssd1306.h
* 字体复用 Zephyr 内置 CFB 字体 10x16(CMakeLists 里编译 cfb_fonts.c)
*/
#include <string.h>
#include <zephyr/drivers/i2c.h>
#include "oled_ssd1306.h"
#define OLED_PAGES (OLED_HEIGHT / 8)
/* 复用 Zephyr 内置 CFB 字体:10x16,字符 32~126,列优先,bit0=顶行 */
extern const uint8_t cfb_font_1016;
#define FONT_W 10
#define FONT_FIRST 32
#define FONT_LAST126
static const struct device *const i2c_bus = DEVICE_DT_GET(DT_ALIAS(oled_i2c));
static uint8_t oled_fb;
/* ctrl: 0x00=命令,0x40=显存数据 */
static int oled_write(uint8_t ctrl, const uint8_t *data, size_t len)
{
uint8_t buf;
if (len > OLED_WIDTH) {
return -EINVAL;
}
buf = ctrl;
memcpy(&buf, data, len);
return i2c_write(i2c_bus, buf, len + 1, SSD1306_ADDR);
}
static const uint8_t oled_init_seq[] = {
0xAE, /* display off */
0xD5, 0x80, /* display clock */
0xA8, 0x3F, /* multiplex ratio 63 */
0xD3, 0x00, /* display offset 0 */
0x40, /* start line 0 */
0x8D, 0x14, /* charge pump on */
0x20, 0x00, /* horizontal addressing mode */
0xA1, /* segment remap */
0xC8, /* COM scan descending */
0xDA, 0x12, /* COM pins: alternative */
0x81, 0xCF, /* contrast */
0xD9, 0xF1, /* precharge */
0xDB, 0x40, /* VCOMH level */
0xA4, /* display from RAM */
0xA6, /* normal (not inverted) */
0x2E, /* scroll off */
0xAF, /* display on */
};
int oled_init(void)
{
if (!device_is_ready(i2c_bus)) {
return -ENODEV;
}
return oled_write(0x00, oled_init_seq, sizeof(oled_init_seq));
}
int oled_flush(void)
{
static const uint8_t window[] = { 0x21, 0, 127, 0x22, 0, 7 };
int ret;
ret = oled_write(0x00, window, sizeof(window));
if (ret != 0) {
return ret;
}
for (int p = 0; p < OLED_PAGES; p++) {
ret = oled_write(0x40, oled_fb, OLED_WIDTH);
if (ret != 0) {
return ret;
}
}
return 0;
}
void oled_clear(void)
{
memset(oled_fb, 0, sizeof(oled_fb));
}
static void oled_draw_char(char c, uint8_t x, uint8_t row)
{
const uint8_t *g;
if (c < FONT_FIRST || c > FONT_LAST) {
c = ' ';
}
g = cfb_font_1016;
for (int cx = 0; cx < FONT_W; cx++) {
oled_fb = g;
oled_fb = g;
}
}
void oled_print(const char *s, uint8_t col, uint8_t row)
{
while (*s != '\0' && col < OLED_COLS && row < OLED_ROWS) {
oled_draw_char(*s++, col * FONT_W, row);
col++;
}
}
const struct device *oled_i2c_bus(void)
{
return i2c_bus;
}
```
oled_ssd1306.h
```
/*
* SSD1306 OLED(128x64,I2C)裸驱动,字符界面 12 列 x 4 行(10x16 字体)
* 接线:SCI3 简易 I2C,SCL3=P408, SDA3=P409(板载 1.5K 上拉)
* 协议:命令 = 地址+0x00+命令字节;显存 = 地址+0x40+数据字节
*/
#ifndef OLED_SSD1306_H
#define OLED_SSD1306_H
#include <stdint.h>
#include <zephyr/device.h>
#define SSD1306_ADDR 0x3c /* 部分模块是 0x3d */
#define OLED_WIDTH 128
#define OLED_HEIGHT64
/* 字符格:128/10 = 12 列,64/16 = 4 行 */
#define OLED_COLS (OLED_WIDTH / 10)
#define OLED_ROWS (OLED_HEIGHT / 16)
/* 初始化 SSD1306,返回 0 或 i2c 错误码(-5 = NACK,检查接线/地址) */
int oled_init(void);
/* 整块显存刷到屏上,返回 0 或 i2c 错误码 */
int oled_flush(void);
/* 清空显存(不刷屏,需再调 oled_flush) */
void oled_clear(void);
/* 在字符格 (col, row) 处写字符串,col 0~11,row 0~3(只写显存) */
void oled_print(const char *s, uint8_t col, uint8_t row);
/* I2C 总线设备(诊断用,如总线扫描) */
const struct device *oled_i2c_bus(void);
#endif /* OLED_SSD1306_H */
```
页:
[1]