eefocus_3914144 发表于 2026-9-10 17:59:49

【瑞萨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]
查看完整版本: 【瑞萨BLE/WIFI模块测评】Zephyr 驱动OLED