三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

RK3568之SPI子系统(协议)

RK3568之SPI子系统(协议)

第1章 基础知识

1.1 SPI介绍

SPI是一个同步的数据总线,也就是说它是用单独的数据线一个单独的时钟信号来保证发送端和接收端的完美同步

时钟是一个振荡信号,它告诉接收端在确切的时机对数据线上的信号进行采样。

产生时钟的一侧称为主机,另一侧称为从机。总是只有一个主机(一般来说可以是微控制器/MCU),但是可以有多个从机(后面详细介绍);

数据的采集时机可能是时钟信号上升沿(从低到高)或下降沿(从高到低)。

SPI是全双工且SPI没有定义速度限制,一般的实现通常能达到甚至超过10 Mbps

1.2 一主一从的连接方式

1.3 一主多从的连接方式

1.4 SPI的特性

SPI总线包括4条逻辑线,定义如下:

  • MISOMaster input slave output主机输入,从机输出(数据来自从机);
  • MOSIMaster output slave input主机输出,从机输入(数据来自主机);
  • SCLKSerial Clock串行时钟信号,由主机产生发送给从机;
  • SSSlave Select片选信号,由主机发送,以控制与哪个从机通信,通常是低电平有效信号。

其他制造商可能会遵循其他命名规则,但是最终他们指的相同的含义。以下是一些常用术语;

  • MISO也可以是SIMODOUTDOSDOSO(在主机端);
  • MOSI也可以是SOMIDINDISDISI(在主机端);
  • NSS也可以是CECSSSEL;
  • SCLK也可以是SCK;

本文将按照以下命名进行讲解[MISO, MOSI, SCK,NSS]。

1.5 SPI设备选择

SPI是[单主设备( single-master )]通信协议,这意味着总线中的只有一支中心设备能发起通信。当SPI主设备想读/写[从设备]时,它首先拉低[从设备]对应的SS线(SS是低电平有效),接着开始发送工作脉冲到时钟线上,在相应的脉冲时间上,[主设备]把信号发到MOSI实现“写”,同时可对MISO采样而实现“读”,如下图:

1.6 SPI数据发送接收

SPI主机和从机都有一个串行移位寄存器,主机通过向它的SPI串行寄存器写入一个字节来发起一次传输。

  1. 首先拉低对应SS信号线,表示与该设备进行通信
  2. 主机通过发送SCLK时钟信号,来告诉从机写数据或者读数据
  3. 这里要注意,SCLK时钟信号可能是低电平有效,也可能是高电平有效,因为SPI有四种模式,这个我们在下面会介绍
  4. 主机(Master)将要发送的数据写到发送数据缓存区(Menory),缓存区经过移位寄存器(0~7),串行移位寄存器通过MOSI信号线将字节一位一位的移出去传送给从机,,同时MISO接口接收到的数据经过移位寄存器一位一位的移到接收缓存区。
  5. 从机(Slave)也将自己的串行移位寄存器(0~7)中的内容通过MISO信号线返回给主机。同时通过MOSI信号线接收主机发送的数据,这样,两个移位寄存器中的内容就被交换。

SPI只有主模式和从模式之分,没有读和写的说法,外设的写操作和读操作是同步完成的。如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。也就是说,你发一个数据必然会收到一个数据;你要收一个数据必须也要先发一个数据。

每当主机产生一个时钟脉冲(SCK),就会发生以下同步动作:

  • 数据交换:主机的寄存器向从机推入 1 bit 数据(通过 MOSI),同时从机的寄存器也向主机推入 1 bit 数据(通过 MISO)。
  • 等价交换:在一个时钟周期内,双方必须同时发送和接收 1 bit。经过 8 个时钟周期,双方就完美地“互换”了一个字节的数据。

所以,SPI 在物理层面上确实是严格的“你发一个,我就得收一个;我想收一个,就必须发一个”的等价交换模式。

1.7 SPI通信的四种模式

SPI通信有4种不同的操作模式,不同的从设备可能在出厂是就是配置为某种模式,这是不能改变的;但我们的通信双方必须是工作在同一模式下,所以我们可以对我们的主设备的SPI模式进行配置,通过CPOL(时钟极性)和CPHA(时钟相位)来
控制我们主设备的通信模式,具体如下:

1.7.1 CPOL(时钟极性)

CPOL决定了我们时钟线SCLK空闲时的状态。

CPOL=0的时候表示SCLK在空闲时为低电平。第1个跳变沿是上升沿,第2个跳变沿是下降沿;

CPOL=1的时候表示SCLK在空闲时为高电平。第1个跳变沿是下降沿,第2个跳变沿是上升沿;

1.7.2 CPHA(时钟相位)

CPHA是用来控制数据是在第一个跳变沿还是第二个跳变沿采集的

CPHA=0的时候,数据是在第一个跳变沿的时候就会被采集

CPHA=1的时候,数据是在第二个跳变沿的时候才会被采集

1.7.3 模式0(CPOL = 0,CPHA = 0)

CPOL = 0:时钟线空闲时是低电平,第1个跳变沿是上升沿,第2个跳变沿是下降沿;

CPHA = 0:数据在第1个跳变沿(上升沿)采样;

时序图如下:

1.7.4 模式1(CPOL = 0,CPHA = 1)

CPOL = 0:空闲时是低电平,第1个跳变沿是上升沿,第2个跳变沿是下降沿;

CPHA = 1:数据在第2个跳变沿(下降沿)采样;

时序图如下:

1.7.5 模式2(CPOL = 1,CPHA = 0)

CPOL = 1:空闲时是高电平,第1个跳变沿是下降沿,第2个跳变沿是上升沿;

CPHA = 0:数据在第1个跳变沿(下降沿)采样;

时序图如下:

1.7.6 模式3 (CPOL = 1,CPHA = 1)

CPOL = 1:空闲时是高电平;第1个跳变沿是下降沿,第2个跳变沿是上升沿;

CPHA = 1:数据在第2个跳变沿(上升沿)采样;

时序图如下:

1.8 SPI的优缺点

1.8.1 SPI通讯的优点

使SPI作为串行通信接口脱颖而出的原因很多;

  • 全双工串行通信;
  • 高速数据传输速率。
  • 简单的软件配置;
  • 极其灵活的数据传输,不限于8位,它可以是任意大小的字;
  • 非常简单的硬件结构。从站不需要唯一地址(与I2C不同)。从机使用主机时钟,不需要精密时钟振荡器/晶振(与UART不同)。不需要收发器(与CAN不同)。

1.8.2 SPI通讯的缺点

  • 没有硬件从机应答信号(主机可能在不知情的情况下无处发送);
  • 通常仅支持一个主设备;
  • 需要更多的引脚(与I2C不同);
  • 没有定义硬件级别的错误检查协议;
  • 与RS-232和CAN总线相比,只能支持非常短的距离;

1.9 SPI协议开发经验。

1. SPI可以是一主多从的协议。当有多个从机时,一个从机需要一个片选口。

2. MISO一定要配置为上拉输入或者浮空输入。

3. 片选口、时钟口和MOSI口要配置为推挽输出。

4. 由于时钟是几十兆数量级的,所以比I2C的几十K时钟高。导致SPI通讯比I2C通讯快。

(1) SPI支持全双工通讯,也就是主机同时发送数据,以及主机同时接收数据。

(2) 主机在发送一个字节给从机时,是先将这个字节的高位先发送出去。例如(0xA4 = 1010 0100。先发最高位的1,再发送次高位的0,接着发送1。这样从高到低的搬运数据)。

(3) 虽然SPI支持全双工通讯,但是同样支持一下两种情况。

  • 主机只是发送数据,但是主机不接收数据;主机不接收数据的本质是:主机接收到数据但是不处理。
  • 从机只是发送数据,但是从机不接收数据。

第2章 OLED(SPI协议)

2.1 OLED简介

OLED是(OLED,即有机发光二极管),又称为有机电激光显示(Organic Electroluminescence Display, OLED)。OLED由于同时具备自发光,不需背光源、对比度高、厚度薄、视角广、反应速度快、可用于挠曲性面板、使用温度范围广、构造及制程较简单等优异之特性,被认为是下一代的平面显示器新兴应用技术

以下是OLED显示屏模块的参数:

尺寸

0.96寸

模块电压

DC:3~5V

分辨率

128×64

驱动

SSD1306

管脚数

7针

接口

SPI/IIC

工作温度

-20℃~70℃

2.2 OLED的引脚描述

引脚名称

描述

GND

电源地

VCC

电源正3~5V

D0

SPI时钟线

D1

SPI数据线

RES

复位

DC

SPI数据/命令选择线

CS

SPI片选

2.3工作原理:SSD1306显存与命令

SSD1306的显存总共为128*64bit大小,其将这些显存分为了8页。每页包含了128个字节,总共8页,这样刚好是128*64的点阵大小。

  • 命令0X81:设置对比度。包含两个字节,第一个0X81为命令,随后发送的一个字节为要设置的对比度的值。这个值设置得越大屏幕就越亮。
  • 命令0XAE/0XAF:0XAE为关闭显示命令;0XAF为开启显示命令。
  • 命令0XB0~B7:用于设置页地址,其低三位的值对应着GRAM的页地址。
  • 命令0X00~0X0F:用于设置显示时的起始列地址低四位。
  • 命令0X10~0X1F:用于设置显示时的起始列地址高四位。

2.4 SSD1306的4线SPI通讯时序、

(1)参考手册1

(2)参考手册2

从这个时序图可以看出几个关键点:

1. 在发送数据之前,要把CS引脚拉低。

2. D/C一般是用来控制发送命令还是数据的。

3. 数据在第二个SCLK跳变开始传输。

4. 数据在SCLK上升沿的时候进行传输,每次发送一个Bit,高位在前。

所以,由这个时序图可以判断出来,用的是SPI模式3的通讯方式。

知道这个之后,你就知道单片机的硬件SPI模式要怎么配置了(下图以STM32为例)。

2.5 硬件原理图

OLED的引脚名称

描述

RK3568底板网络标号RK3568的GPIO口引脚编号

GND

电源地

VCC

电源正3~5V

D0

SPI时钟线

SPI0_CLK_M1GPIO2_D391

D1

SPI数据线

SPI0_MOSI_M1GPIO2_D189

RES

复位

PDM_SDI3_M0_ADCGPIO1_B040

DC

DC 就是Data/Command 选择引脚,SSD1306 用它来判断你通过 SPI 发过来的这个字节是"命令"还是"数据"DVP_PWREN0_H_GPIO0_B0GPIO0_B08

CS

SPI片选

SPI0_CS0_M1GPIO2_D290

注意:

SPI 总线标准只有 4 根线:CLK、MOSI、MISO、CS。但是OLED SPI并没有使用MISO这个引脚。

但 SSD1306 额外需要一根DC(Data/Command)引脚来区分你发的是命令还是数据:

主机往该引脚上写0。即 DC = 0 → 发送的是命令(如清屏、设置对比度)
主机往该引脚上写1。即 DC = 1 → 发送的是显存数据

这根线不属于 SPI 控制器管理,所以必须用一个普通 GPIO 来控制,在设备树里单独声明。这里我选择GPIO0_B0作为DC引脚。

2.6 查看引脚复用(编写设备树之前)

cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins
OLED的引脚名称RK3568的引脚编号复用功能
CLK91
CS90
MOSI89
MISO88(配置上,但是不使用)
DC8
RES40

2.7 设备树编写

在SPI0控制器节点下编写从机设备节点

(1)具有SPI模式配置和大小端配置

&spi0{ /*覆盖原有spi0控制器中的属性配置*/ status = "okay"; pinctrl-names = "default", "high_speed"; pinctrl-0 = <&spi0m1_pins &spi0m1_cs0>; pinctrl-1 = <&spi0m1_pins_hs &spi0m1_cs0_hs>; /*spi0m1的从机1*/ ssd1306_oled: ssd1306@0 { compatible = "solomon,ssd1306-spi"; status = "okay"; reg = <0>; /* CS0 */ spi-max-frequency = <10000000>; /* 10MHz */ /*spi的工作模式*/ spi-cpol; /* CPOL=1 */ spi-cpha; /* CPHA=1 */ /*spi先从低位开始传递*/ spi-lsb-first; /*特有的dc引脚和复位引脚*/ dc-gpios = <&gpio0 RK_PB0 GPIO_ACTIVE_HIGH>; rst-gpios = <&gpio1 RK_PB0 GPIO_ACTIVE_LOW>; }; };

(2) 使用SPI的模式0

&spi0{ /*覆盖原有spi0控制器中的属性配置*/ status = "okay"; pinctrl-names = "default", "high_speed"; pinctrl-0 = <&spi0m1_pins &spi0m1_cs0>; pinctrl-1 = <&spi0m1_pins_hs &spi0m1_cs0_hs>; /*spi0m1的从机1*/ ssd1306_oled: ssd1306@0 { compatible = "solomon,ssd1306-spi"; status = "okay"; reg = <0>; /* CS0 */ spi-max-frequency = <10000000>; /* 10MHz */ /*特有的dc引脚和复位引脚*/ dc-gpios = <&gpio0 RK_PB0 GPIO_ACTIVE_HIGH>; rst-gpios = <&gpio1 RK_PB0 GPIO_ACTIVE_LOW>; }; };

编译并烧录内核

2.8 查看引脚复用(编写设备树之后,但是没有加载驱动)

cat /sys/kernel/debug/pinctrl/pinctrl-rockchip-pinctrl/pinmux-pins
OLED的引脚名称RK3568的引脚编号复用功能
CLK91
CS90
MOSI89
MISO88(配置上,但是不使用)
DC8
RES40

2.9 查看设备树

(1)查看SPI控制器0

cd /proc/device-tree/spi@fe610000/ ls -alh cat compatible

(2)查看自定义的oled节点

cd /proc/device-tree/spi@fe610000/ssd1306@0/ ls -alh cat compatible && echo "\n"

第3章 stm32f103c8t6的spi协议驱动oled

3.1 硬件接线

OLED引脚STM32引脚
D0 (CLK)PA5
D1 (MOSI)PA7
RESPA4
DCPA3
CSPA2
VCC3.3V
GNDGND

3.2 驱动代码

/** * STM32F103C8T6 + SH1106 OLED 软件模拟SPI 测试程序 * * 接线: * OLED D0(CLK) -> PA5 * OLED D1(MOSI) -> PA7 * OLED RES -> PA4 * OLED DC -> PA3 * OLED CS -> PA2 * OLED VCC -> 3.3V * OLED GND -> GND * * 使用标准库,main.c 直接替换即可 * 不需要任何其他文件,纯 GPIO 模拟 SPI */ #include "stm32f10x.h" /* ------------------------------------------------------------------ */ /* GPIO 引脚定义 */ /* ------------------------------------------------------------------ */ #define OLED_GPIO GPIOA #define OLED_CLK_PIN GPIO_Pin_5 /* D0: SPI时钟 */ #define OLED_MOSI_PIN GPIO_Pin_7 /* D1: SPI数据 */ #define OLED_RST_PIN GPIO_Pin_4 /* RES: 复位 */ #define OLED_DC_PIN GPIO_Pin_3 /* DC: 命令/数据 */ #define OLED_CS_PIN GPIO_Pin_2 /* CS: 片选 */ /* GPIO 操作宏 */ #define OLED_CLK_H() GPIO_SetBits(OLED_GPIO, OLED_CLK_PIN) #define OLED_CLK_L() GPIO_ResetBits(OLED_GPIO, OLED_CLK_PIN) #define OLED_MOSI_H() GPIO_SetBits(OLED_GPIO, OLED_MOSI_PIN) #define OLED_MOSI_L() GPIO_ResetBits(OLED_GPIO, OLED_MOSI_PIN) #define OLED_RST_H() GPIO_SetBits(OLED_GPIO, OLED_RST_PIN) #define OLED_RST_L() GPIO_ResetBits(OLED_GPIO, OLED_RST_PIN) #define OLED_DC_H() GPIO_SetBits(OLED_GPIO, OLED_DC_PIN) #define OLED_DC_L() GPIO_ResetBits(OLED_GPIO, OLED_DC_PIN) #define OLED_CS_H() GPIO_SetBits(OLED_GPIO, OLED_CS_PIN) #define OLED_CS_L() GPIO_ResetBits(OLED_GPIO, OLED_CS_PIN) /* ------------------------------------------------------------------ */ /* 6x8 ASCII 字模(0x20开始) */ /* ------------------------------------------------------------------ */ static const uint8_t ASCII6x8[][6] = { { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, // sp { 0x00, 0x00, 0x00, 0x2f, 0x00, 0x00 }, // ! { 0x00, 0x00, 0x07, 0x00, 0x07, 0x00 }, // " { 0x00, 0x14, 0x7f, 0x14, 0x7f, 0x14 }, // # { 0x00, 0x24, 0x2a, 0x7f, 0x2a, 0x12 }, // $ { 0x00, 0x62, 0x64, 0x08, 0x13, 0x23 }, // % { 0x00, 0x36, 0x49, 0x55, 0x22, 0x50 }, // & { 0x00, 0x00, 0x05, 0x03, 0x00, 0x00 }, // ' { 0x00, 0x00, 0x1c, 0x22, 0x41, 0x00 }, // ( { 0x00, 0x00, 0x41, 0x22, 0x1c, 0x00 }, // ) { 0x00, 0x14, 0x08, 0x3E, 0x08, 0x14 }, // * { 0x00, 0x08, 0x08, 0x3E, 0x08, 0x08 }, // + { 0x00, 0x00, 0x00, 0xA0, 0x60, 0x00 }, // , { 0x00, 0x08, 0x08, 0x08, 0x08, 0x08 }, // - { 0x00, 0x00, 0x60, 0x60, 0x00, 0x00 }, // . { 0x00, 0x20, 0x10, 0x08, 0x04, 0x02 }, // / { 0x00, 0x3E, 0x51, 0x49, 0x45, 0x3E }, // 0 { 0x00, 0x00, 0x42, 0x7F, 0x40, 0x00 }, // 1 { 0x00, 0x42, 0x61, 0x51, 0x49, 0x46 }, // 2 { 0x00, 0x21, 0x41, 0x45, 0x4B, 0x31 }, // 3 { 0x00, 0x18, 0x14, 0x12, 0x7F, 0x10 }, // 4 { 0x00, 0x27, 0x45, 0x45, 0x45, 0x39 }, // 5 { 0x00, 0x3C, 0x4A, 0x49, 0x49, 0x30 }, // 6 { 0x00, 0x01, 0x71, 0x09, 0x05, 0x03 }, // 7 { 0x00, 0x36, 0x49, 0x49, 0x49, 0x36 }, // 8 { 0x00, 0x06, 0x49, 0x49, 0x29, 0x1E }, // 9 { 0x00, 0x00, 0x36, 0x36, 0x00, 0x00 }, // : { 0x00, 0x00, 0x56, 0x36, 0x00, 0x00 }, // ; { 0x00, 0x08, 0x14, 0x22, 0x41, 0x00 }, // < { 0x00, 0x14, 0x14, 0x14, 0x14, 0x14 }, // = { 0x00, 0x00, 0x41, 0x22, 0x14, 0x08 }, // > { 0x00, 0x02, 0x01, 0x51, 0x09, 0x06 }, // ? { 0x00, 0x32, 0x49, 0x59, 0x51, 0x3E }, // @ { 0x00, 0x7C, 0x12, 0x11, 0x12, 0x7C }, // A { 0x00, 0x7F, 0x49, 0x49, 0x49, 0x36 }, // B { 0x00, 0x3E, 0x41, 0x41, 0x41, 0x22 }, // C { 0x00, 0x7F, 0x41, 0x41, 0x22, 0x1C }, // D { 0x00, 0x7F, 0x49, 0x49, 0x49, 0x41 }, // E { 0x00, 0x7F, 0x09, 0x09, 0x09, 0x01 }, // F { 0x00, 0x3E, 0x41, 0x49, 0x49, 0x7A }, // G { 0x00, 0x7F, 0x08, 0x08, 0x08, 0x7F }, // H { 0x00, 0x00, 0x41, 0x7F, 0x41, 0x00 }, // I { 0x00, 0x20, 0x40, 0x41, 0x3F, 0x01 }, // J { 0x00, 0x7F, 0x08, 0x14, 0x22, 0x41 }, // K { 0x00, 0x7F, 0x40, 0x40, 0x40, 0x40 }, // L { 0x00, 0x7F, 0x02, 0x0C, 0x02, 0x7F }, // M { 0x00, 0x7F, 0x04, 0x08, 0x10, 0x7F }, // N { 0x00, 0x3E, 0x41, 0x41, 0x41, 0x3E }, // O { 0x00, 0x7F, 0x09, 0x09, 0x09, 0x06 }, // P { 0x00, 0x3E, 0x41, 0x51, 0x21, 0x5E }, // Q { 0x00, 0x7F, 0x09, 0x19, 0x29, 0x46 }, // R { 0x00, 0x46, 0x49, 0x49, 0x49, 0x31 }, // S { 0x00, 0x01, 0x01, 0x7F, 0x01, 0x01 }, // T { 0x00, 0x3F, 0x40, 0x40, 0x40, 0x3F }, // U { 0x00, 0x1F, 0x20, 0x40, 0x20, 0x1F }, // V { 0x00, 0x3F, 0x40, 0x38, 0x40, 0x3F }, // W { 0x00, 0x63, 0x14, 0x08, 0x14, 0x63 }, // X { 0x00, 0x07, 0x08, 0x70, 0x08, 0x07 }, // Y { 0x00, 0x61, 0x51, 0x49, 0x45, 0x43 }, // Z }; /* ------------------------------------------------------------------ */ /* 延时函数 */ /* ------------------------------------------------------------------ */ static void delay_us(uint32_t us) { us *= 8; while (us--); } static void delay_ms(uint32_t ms) { while (ms--) delay_us(1000); } /* ------------------------------------------------------------------ */ /* GPIO 初始化 */ /* ------------------------------------------------------------------ */ static void oled_gpio_init(void) { GPIO_InitTypeDef GPIO_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStruct.GPIO_Pin = OLED_CLK_PIN | OLED_MOSI_PIN | OLED_RST_PIN | OLED_DC_PIN | OLED_CS_PIN; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; /* 推挽输出 */ GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(OLED_GPIO, &GPIO_InitStruct); /* 初始状态 */ OLED_CS_H(); OLED_CLK_H(); OLED_RST_H(); OLED_DC_H(); } /* ------------------------------------------------------------------ */ /* 软件模拟 SPI 发送一字节(MSB first,Mode3:CPOL=1 CPHA=1) */ /* ------------------------------------------------------------------ */ static void spi_write_byte(uint8_t byte) { uint8_t i; for (i = 0; i < 8; i++) { OLED_CLK_L(); if (byte & 0x80) OLED_MOSI_H(); else OLED_MOSI_L(); byte <<= 1; OLED_CLK_H(); } } /* ------------------------------------------------------------------ */ /* 发送命令 / 数据 */ /* ------------------------------------------------------------------ */ static void oled_write_cmd(uint8_t cmd) { OLED_CS_L(); OLED_DC_L(); /* DC=0: 命令 */ spi_write_byte(cmd); OLED_CS_H(); } static void oled_write_data(uint8_t data) { OLED_CS_L(); OLED_DC_H(); /* DC=1: 数据 */ spi_write_byte(data); OLED_CS_H(); } /* ------------------------------------------------------------------ */ /* 硬件复位 */ /* ------------------------------------------------------------------ */ static void oled_reset(void) { OLED_RST_H(); delay_ms(1); OLED_RST_L(); delay_ms(20); OLED_RST_H(); delay_ms(20); } /* ------------------------------------------------------------------ */ /* SH1106 初始化序列 */ /* ------------------------------------------------------------------ */ static void oled_init(void) { oled_reset(); oled_write_cmd(0xAE); /* 关显示 */ oled_write_cmd(0x02); /* 列低4位=2(SH1106偏移) */ oled_write_cmd(0x10); /* 列高4位=0 */ oled_write_cmd(0x40); /* 起始行=0 */ oled_write_cmd(0x81); /* 对比度 */ oled_write_cmd(0xCF); oled_write_cmd(0xA1); /* 列正向 */ oled_write_cmd(0xC8); /* 行正向 */ oled_write_cmd(0xA6); /* 正常显示 */ oled_write_cmd(0xA8); /* 多路复用率 */ oled_write_cmd(0x3F); /* 64行 */ oled_write_cmd(0xD3); /* 显示偏移 */ oled_write_cmd(0x00); oled_write_cmd(0xD5); /* 时钟分频 */ oled_write_cmd(0x80); oled_write_cmd(0xD9); /* 预充电 */ oled_write_cmd(0xF1); oled_write_cmd(0xDA); /* COM引脚配置 */ oled_write_cmd(0x12); oled_write_cmd(0xDB); /* VCOMH */ oled_write_cmd(0x40); oled_write_cmd(0x8D); /* 电荷泵 */ oled_write_cmd(0x14); /* 开启 */ oled_write_cmd(0xA4); /* 输出跟随RAM */ oled_write_cmd(0xA6); /* 正常显示 */ oled_write_cmd(0xAF); /* 开显示 */ } /* ------------------------------------------------------------------ */ /* 设置坐标(SH1106 列偏移2) */ /* ------------------------------------------------------------------ */ static void oled_pos(uint8_t x, uint8_t y) { oled_write_cmd(0xB0 + y); /* 页地址 */ oled_write_cmd(((x & 0xF0) >> 4) | 0x10); /* 列高4位 */ oled_write_cmd((x & 0x0F) | 0x02); /* 列低4位+偏移2 */ } /* ------------------------------------------------------------------ */ /* 清屏 */ /* ------------------------------------------------------------------ */ static void oled_clear(void) { uint8_t x, y; for (y = 0; y < 8; y++) { oled_write_cmd(0xB0 + y); oled_write_cmd(0x02); oled_write_cmd(0x10); for (x = 0; x < 128; x++) oled_write_data(0x00); } } /* ------------------------------------------------------------------ */ /* 全亮 */ /* ------------------------------------------------------------------ */ static void oled_full_bright(void) { uint8_t x, y; for (y = 0; y < 8; y++) { oled_write_cmd(0xB0 + y); oled_write_cmd(0x02); oled_write_cmd(0x10); for (x = 0; x < 128; x++) oled_write_data(0xFF); } } /* ------------------------------------------------------------------ */ /* 显示字符串(6x8) */ /* ------------------------------------------------------------------ */ static void oled_show_string(uint8_t x, uint8_t y, const char *str) { uint8_t i, k; while (*str) { if (*str < 0x20 || *str > 0x7A) { str++; continue; } k = *str - 0x20; if (x > 121) { x = 0; y++; } oled_pos(x, y); for (i = 0; i < 6; i++) oled_write_data(ASCII6x8[k][i]); x += 6; str++; } } /* ------------------------------------------------------------------ */ /* main */ /* ------------------------------------------------------------------ */ int main(void) { /* 系统时钟初始化(使用默认72MHz) */ SystemInit(); /* GPIO 初始化 */ oled_gpio_init(); delay_ms(100); /* OLED 初始化 */ oled_init(); /* 步骤1: 全亮测试 - 验证硬件通信是否正常 */ oled_full_bright(); delay_ms(2000); /* 步骤2: 清屏 */ oled_clear(); delay_ms(1000); /* 步骤3: 显示文字 */ oled_show_string(0, 0, "HELLO STM32"); oled_show_string(0, 2, "SH1106 TEST"); oled_show_string(0, 4, "SPI OK!"); oled_show_string(0, 6, "128x64 OLED"); delay_ms(3000); /* 步骤4: 循环滚动显示 */ while (1) { oled_clear(); oled_show_string(0, 0, "Hardware Test"); oled_show_string(0, 2, "CLK -> PA5"); oled_show_string(0, 4, "MOSI -> PA7"); oled_show_string(0, 6, "CS -> PA2"); delay_ms(2000); oled_clear(); oled_show_string(0, 0, "Hardware Test"); oled_show_string(0, 2, "DC -> PA3"); oled_show_string(0, 4, "RES -> PA4"); oled_show_string(0, 6, "VCC -> 3.3V"); delay_ms(2000); /* 全亮闪烁 */ oled_full_bright(); delay_ms(500); oled_clear(); delay_ms(500); oled_full_bright(); delay_ms(500); oled_clear(); delay_ms(500); } }

参考资料

Linux手写FrameBuffer任意引脚驱动spi屏幕https://blog.csdn.net/plmm__/article/details/145193029?spm=1001.2014.3001.5502

STM32模块例程介绍(包含SPI)https://doc.embedfire.com/module/module_tutorial/zh/latest/README.html

野火RK3568的SPI模块资料https://doc.embedfire.com/linux/rk356x/driver/zh/latest/linux_driver/subsystem_spi_subsystem.html#

rk3588s驱动oledhttps://zhuanlan.zhihu.com/p/1906008511390935030

NXP i.MX 系列芯片驱动OLEDhttps://www.heiweilu.top/index.php/linuxqudongmokuaiqioled-spi/

第 12 篇 RK 平台安卓驱动实战 5:SPI 设备驱动开发,以 SPI 屏 / Flash 为例https://blog.csdn.net/alspd_zhangpan/article/details/159164992?spm=1001.2014.3001.5502

← 返回列表