18. 硬件SPI驱动OLED屏

一、OLED显示屏简介

  OLED,即有机发光二极管(Organic Light-Emitting Diode),又称为有机电激光显示(OrganicElectroluminesence Display,OLED)。OLED 可按发光材料分为两种:小分子 OLED 和高分子 OLED(也可称为 PLED)。OLED 是一种利用多层有机薄膜结构产生电致发光的器件,它很容易制作,而且只需要低的驱动电压,OLED 由于同时具备自发光(不需背光源)、对比度高、厚度薄、视角广、反应速度快、功耗低、柔性好等优异特性。

  这里,我们使用 7 针 0.96 寸的黄蓝双色 OLED 显示屏(分区域的双色,前 16 行为黄色,后 48 行为蓝色,且黄蓝色之间有一行不显示的间隔区)。该模块的分辨率为 128 * 64。并且它提供多种接口方式:I2C 接口,3 线 SPI 接口 和 4 线 SPI 接口。该模块默认为 4 线 SPI 接口,如果想要使用其它接口,请根据背面的提示自己焊接电阻。

OLED模块背面图

二、OLED模块接线

OLED模块正面图

引脚 含义
GND 接地
VCC 电源正极
D0 SPI中的时钟管脚——SCK
D1 SPI中的数据管脚——MOSI
RES 复位引脚(低电平有效)
DC 数据/命令控制脚
CS 片选引脚

三、SSD1306驱动芯片

  OLED 模块内部自带一个 GDDRAM。GDDRAM 是位映射静态 RAM,大小为 128 * 64 位。GDDRAM 分为8页(PAGE0 ~ PAGE7),每页内 1 个 SEG 对应 1Byte 数据,一页由 128 Byte 组成。一帧显示数据为 1024 Byte(1KB)。即屏幕每 8 行像素点(8 * PIXEL)记为一页(PAGE),64 行即为 8 页,则屏幕变为 128 列(ROW)8页(PAGE),若要显示整个屏幕,则需要 128 *8 个 1 字节数。

SSD1306显存与屏幕对应关系表

  为了方便控制,这里使用了 SSD1306 驱动芯片。当 GRAM 的写入模式为页模式时,需要设置低字节起始的列地址(0x00 ~ 0x0F)和高字节的起始列地址(0x10 ~ 0x1F),芯片手册中给出了写入 GRAM 与显示的对应关系,写入列地址在写完一字节后自动按列增长。

SSD1306页2显存写入字节与屏幕坐标的关系

  因为每次写入都是按字节写入的,这就存在一个问题,如果我们使用只写方式操作模块,那么,每次要写 8 个点,这样,我们在画点的时候,就必须把要设置的点所在的字节的每个位都搞清楚当前的状态,否则写入的数据就会覆盖掉之前的状态,结果就是有些不需要显示的点,显示出来了,或者该显示的没有显示了。这个问题在能读的模式下,我们可以先读出来要写入的那个字节,得到当前状况,在修改了要改写的位之后再写进 GRAM,这样就不会影响到之前的状况了。但对于 4 线 SPI 模式/IIC 模式,模块是不支持读 GRAM 的。

  因此,我们采用的办法是在单片机的内部建立一个虚拟的 OLED 的 GRAM(共 128 * 8=1024 个字节),每次修改时,只修改单片机上的 GRAM(实际上就是 SRAM),在修改完成后一次性把单片机上的 GRAM 写入到 OLED 的 GRAM。当然这个方法也有坏处,一个对于那些 SRAM 很小的单片机(比如 51 系列)不太友好,另一个是每次都写入全屏,屏幕刷新率会变低。

四、SSD1306常用命令

  SSD1306 的命令比较多,这里我们仅介绍几个比较常用的命令。

OLED指令

  第 0 个命令为 0X81,用于设置对比度的,这个命令包含了两个字节,第一个 0X81 为命令,随后发送的一个字节为要设置的对比度的值。这个值设置得越大屏幕就越亮。

  第 1 个命令为 0XAE/0XAF。0XAE 为关闭显示命令;0XAF 为开启显示命令。

  第 2 个命令为 0X8D,该指令也包含 2 个字节,第一个为命令字,第二个为设置值,第二个字节的 bit2 表示电荷泵的开关状态,该位为 1,则开启电荷泵,为 0 则关闭。在模块初始化的时候,这个必须要开启,否则是看不到屏幕显示的。

  第 3 个命令为 0XB0~B7,该命令用于设置页地址,其低三位的值对应着 GRAM 的页地址。

  第 4 个指令为 0X00~0X0F,该指令用于设置显示时的起始列地址低四位。

  第 5 个指令为 0X10~0X1F,该指令用于设置显示时的起始列地址高四位。

五、原理图

SPI1接口引脚

引脚 含义
GND 接地
VCC 电源正极
D0 SPI中的时钟管脚——SCK
D1 SPI中的数据管脚——MOSI
RES 复位引脚(低电平有效)
DC 数据/命令控制脚
CS 片选引脚

六、程序源码

6.1、SPI初始化函数

  SPI1 初始化函数内容如下:

SPI_HandleTypeDef g_spi1_handle;

/**
 * @brief SPI初始化
 * 
 * @param hspi SPI句柄
 * @param SPIx SPI基地址,可选值: SPIx, x可取: 1 ~ 3
 * @param CLKPolarity 时钟极性,可选值: [SPI_POLARITY_LOW, SPI_POLARITY_HIGH]
 * @param CLKPhase 时钟相位,可选值: [SPI_PHASE_1EDGE, SPI_PHASE_2EDGE]]
 * @param perscaler 时钟分频因子,可选值: SPI_BAUDRATEPRESCALER_x, x可取范围: [2, 4, 6, 8, 16, 32, 64, 128, 256]
 * @param SPI_FirstBit 数据有效性顺序,可选值: [SPI_FIRSTBIT_MSB, SPI_FIRSTBIT_LSB]
 */
void SPI_Init(SPI_HandleTypeDef *hspi, SPI_TypeDef *SPIx,uint32_t CLKPolarity, uint32_t CLKPhase, uint32_t perscaler,uint32_t firstBit)
{
    hspi->Instance = SPIx;                                                      // SPI基地址
    hspi->Init.Mode = SPI_MODE_MASTER;                                          // SPI主机模式
    hspi->Init.Direction = SPI_DIRECTION_2LINES;                                // SPI全双工模式
    hspi->Init.DataSize = SPI_DATASIZE_8BIT;                                    // SPI帧格式
    hspi->Init.CLKPolarity = CLKPolarity;                                       // SPI时钟极性
    hspi->Init.CLKPhase = CLKPhase;                                             // SPI时钟相位
    hspi->Init.NSS = SPI_NSS_SOFT;                                              // SPI软件NSS控制
    hspi->Init.BaudRatePrescaler = perscaler;                                   // SPI时钟分频因子
    hspi->Init.FirstBit = firstBit;                                             // SPI数据高位先发送
    hspi->Init.TIMode = SPI_TIMODE_DISABLE;                                     // SPI不使用TI模式
    hspi->Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;                     // SPI不使用CRC校验
    hspi->Init.CRCPolynomial = 7;                                               // 设置CRC校验多项式

    HAL_SPI_Init(hspi);
}

  SPI 底层初始化函数内容如下:

/**
 * @brief SPI底层初始化函数
 * 
 * @param hspi SPI句柄
 */
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    if (hspi->Instance == SPI1)
    {
        __HAL_RCC_SPI1_CLK_ENABLE();                                            // 使能SPI1时钟
        __HAL_RCC_GPIOB_CLK_ENABLE();                                           // 使能SPI1对应的GPIO时钟

        GPIO_InitStruct.Pin = GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 ;            // SPI1的SCK引脚、MISO引脚和MOSI引脚
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;                                 // 复用推完输出
        GPIO_InitStruct.Pull = GPIO_NOPULL;                                     // 不使用上下拉电阻
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;                           // 输出速度
        GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;                              // 复用功能
        HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    }
}

  SPI 发送接收一个字节函数内容如下:

/**
 * @brief SPI发送接收一个字节函数
 * 
 * @param hspi SPI句柄
 * @param data 要发送的数据
 * @return uint8_t 接收的数据
 */
uint8_t SPI_SwapOneByte(SPI_HandleTypeDef *hspi, uint8_t data)
{
    uint8_t receive = 0;

    HAL_SPI_TransmitReceive(hspi, &data, &receive, 1, 1000);

    return receive;
}

6.2、OLED初始化函数

6.2.1、OLED初始化函数

/**
 * @brief OLED初始化函数
 * 
 */
void OLED_Init(void)
{
    OLED_GPIO_Init();

    OLED_CS(1);
    OLED_DC(1);

    OLED_RESET(0);
    HAL_Delay(10);
    OLED_RESET(1);

    OLED_SSD1306_Init();

    OLED_Clear();
}

6.2.2、OLED的GPIO引脚初始化

#define OLED_CS_GPIO_PORT                   GPIOD
#define OLED_CS_GPIO_PIN                    GPIO_PIN_8
#define RCC_OLED_CS_GPIO_CLK_ENABLE()       __HAL_RCC_GPIOD_CLK_ENABLE()

#define OLED_CS(x)                          do{ x ? \
                                                HAL_GPIO_WritePin(OLED_CS_GPIO_PORT, OLED_CS_GPIO_PIN, GPIO_PIN_SET):\
                                                HAL_GPIO_WritePin(OLED_CS_GPIO_PORT, OLED_CS_GPIO_PIN, GPIO_PIN_RESET);\
                                            }while(0)

#define OLED_DC_GPIO_PORT                   GPIOD
#define OLED_DC_GPIO_PIN                    GPIO_PIN_9
#define RCC_OLED_DC_GPIO_CLK_ENABLE()       __HAL_RCC_GPIOD_CLK_ENABLE()

#define OLED_DC(x)                          do{ x ? \
                                                HAL_GPIO_WritePin(OLED_DC_GPIO_PORT, OLED_DC_GPIO_PIN, GPIO_PIN_SET):\
                                                HAL_GPIO_WritePin(OLED_DC_GPIO_PORT, OLED_DC_GPIO_PIN, GPIO_PIN_RESET);\
                                            }while(0)

#define OLED_RESET_GPIO_PORT                GPIOD
#define OLED_RESET_GPIO_PIN                 GPIO_PIN_10
#define RCC_OLED_RESET_GPIO_CLK_ENABLE()    __HAL_RCC_GPIOD_CLK_ENABLE()

#define OLED_RESET(x)                       do{ x ? \
                                                HAL_GPIO_WritePin(OLED_RESET_GPIO_PORT, OLED_RESET_GPIO_PIN, GPIO_PIN_SET):\
                                                HAL_GPIO_WritePin(OLED_RESET_GPIO_PORT, OLED_RESET_GPIO_PIN, GPIO_PIN_RESET);\
                                            }while(0)
/**
 * @brief OLED的GPIO引脚初始化函数
 * 
 */
void OLED_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;

    RCC_OLED_CS_GPIO_CLK_ENABLE();
    RCC_OLED_DC_GPIO_CLK_ENABLE();
    RCC_OLED_RESET_GPIO_CLK_ENABLE();

    GPIO_InitStruct.Pin = OLED_CS_GPIO_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(OLED_CS_GPIO_PORT, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = OLED_DC_GPIO_PIN;
    HAL_GPIO_Init(OLED_DC_GPIO_PORT, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = OLED_RESET_GPIO_PIN;
    HAL_GPIO_Init(OLED_RESET_GPIO_PORT, &GPIO_InitStruct);
}

6.2.3、SSD1306初始化

/**
 * @brief SSD1306初始化函数
 * 
 */
void OLED_SSD1306_Init(void)
{
    OLED_WriteByte(0xAE, OLED_MODE_CMD);                                        // 关闭显示

    OLED_WriteByte(0xD5, OLED_MODE_CMD);                                        // 设置显示时钟分频比/振荡器频率
    OLED_WriteByte(0x80, OLED_MODE_CMD);  

    OLED_WriteByte(0xA8, OLED_MODE_CMD);                                        // 设置多路复用率
    OLED_WriteByte(0x3F, OLED_MODE_CMD);  

    OLED_WriteByte(0xD3, OLED_MODE_CMD);                                        // 设置显示偏移
    OLED_WriteByte(0x00, OLED_MODE_CMD);  

    OLED_WriteByte(0x40, OLED_MODE_CMD);                                        // 设置显示开始行

    OLED_WriteByte(0xA1, OLED_MODE_CMD);                                        // 设置左右方向,0xA1正常 0xA0左右反置
    OLED_WriteByte(0xC8, OLED_MODE_CMD);                                        // 设置上下方向,0xC8正常 0xC0上下反置

    OLED_WriteByte(0xDA, OLED_MODE_CMD);                                        // 设置COM引脚硬件配置
    OLED_WriteByte(0x12, OLED_MODE_CMD);  

    OLED_WriteByte(0x81, OLED_MODE_CMD);                                        // 设置对比度控制
    OLED_WriteByte(0xCF, OLED_MODE_CMD);  

    OLED_WriteByte(0xD9, OLED_MODE_CMD);                                        // 设置预充电周期
    OLED_WriteByte(0xF1, OLED_MODE_CMD);

    OLED_WriteByte(0xDB, OLED_MODE_CMD);                                        // 设置VCOMH取消选择级别
    OLED_WriteByte(0x30, OLED_MODE_CMD);

    OLED_WriteByte(0xA4, OLED_MODE_CMD);                                        // 设置整个显示打开/关闭
    OLED_WriteByte(0xA6, OLED_MODE_CMD);                                        // 设置正常/倒转显示

    OLED_WriteByte(0x8D, OLED_MODE_CMD);                                        // 设置充电泵
    OLED_WriteByte(0x14, OLED_MODE_CMD);

    OLED_WriteByte(0xAF, OLED_MODE_CMD);                                        // 开启显示
}

6.2.4、写寄存器函数

typedef enum OLED_WriteMode
{
    OLED_MODE_CMD = 0,
    OLED_MODE_DATA = 1
} OLED_WriteMode;
SPI_HandleTypeDef *g_oled_spi_handle_ptr;

/**
 * @brief OELD往寄存器写入一个字节
 * 
 * @param data 一字节数据
 * @param mode OLED状态的枚举值
 */
void OLED_WriteByte(uint8_t data, OLED_WriteMode mode)
{
    OLED_CS(0);                                                                 // 片选
    OLED_DC(mode);                                                              // 数据/命令选择
    SPI_SwapOneByte(g_oled_spi_handle_ptr, data);                               // 发送数据
    OLED_CS(1);                                                                 // 取消片选
}

6.2.5、OLED刷新GRAMM函数

static uint8_t g_oled_gram[8][128];                                             // GRAM缓冲区

/**
 * @brief OLED刷新GRAM函数
 * 
 * @param hspi SPI句柄
 */
void OLED_RefreshGRAM(void)
{
    for (int page = 0; page < 8; page++)
    {
        OLED_SetCursor(0, page);

        for (int n = 0; n < 128; n++)
        {
            OLED_WriteByte(g_oled_gram[page][n], OLED_MODE_DATA);
        }
    }
}

6.3、常用功能

6.3.1、开启OLED

/**
 * @brief 开启OLED
 * 
 */
void OLED_DisplayOn(void)
{
    OLED_WriteByte(0x8D, OLED_MODE_CMD);                                        // SET DCDC命令
    OLED_WriteByte(0x14, OLED_MODE_CMD);                                        // DCDC ON
    OLED_WriteByte(0xAF, OLED_MODE_CMD);                                        // DISPLAY ON
}

6.3.2、关闭OLED

/**
 * @brief 关闭OLED
 * 
 */
void OLED_DisplayOff(void)
{
    OLED_WriteByte(0X8D, OLED_MODE_CMD);                                        // SET DCDC命令
    OLED_WriteByte(0X10, OLED_MODE_CMD);                                        // DCDC OFF
    OLED_WriteByte(0XAE, OLED_MODE_CMD);                                        // DISPLAY OFF
}

6.3.3、OLED设置坐标函数

/**
 * @brief OLED设置坐标函数
 * 
 * @param x 坐标所在的列,范围: 0 ~ 127
 * @param page 坐标所在的页,范围: 0 ~ 7
 */
void OLED_SetCursor(uint8_t x, uint8_t page)
{
    if (x < 0 || x > 127 || page < 0 || page > 8)                               // 超出范围
    {
        return;
    }

    OLED_WriteByte(0xB0 + page, OLED_MODE_CMD);                                 // 设置页地址
    OLED_WriteByte((0x10 | (x & 0xF0) >> 4), OLED_MODE_CMD);                    // 设置显示列低地址
    OLED_WriteByte((x & 0x0F), OLED_MODE_CMD);                                  // 设置显示列高地址
}

6.3.4、OLED清屏函数

/**
 * @brief OLED清屏函数
 * 
 */
void OLED_Clear(void)
{
    for (int page = 0; page < 8; page++)
    {
        for (int n = 0; n < 128; n++)
        {
            g_oled_gram[page][n] = 0x00;
        }
    }
    OLED_RefreshGRAM();                                                         // 更新显示
}

6.3.5、OLED局部清屏函数

/**
 * @brief OLED局部清屏函数
 * 
 * @param x 要清空的区域的左上角的列坐标
 * @param y 要清空的区域的左上角的行坐标
 * @param width 要清空的区域的宽度
 * @param height 要清空的区域的高度
 */
void OLED_ClearArea(uint8_t x, uint8_t y, uint8_t width, uint8_t height)
{
    for (uint8_t i = y; i < y + height; i++)
    {
        for (uint8_t j = x; j < x + width; j++)
        {
            g_oled_gram[i >> 3][j] &= ~(0x01 << (i & 0x07));
        }
    }
}

一个数右移 n 位(未溢出的情况),相当于这个数整除 2 的 n 次方,即 a >> n == a / 2^n

一个数左移 n 位(未溢出的情况),相当于这个数乘以 2 的 n 次方,即 a << n == a * 2^n

如果想要计算 x % 2^n,其中 n 是一个非负整数。可以通过简单的位运算 x & (2^n - 1) 来完成。这是因为 2^n - 1 在二进制下是一个所有低位都为 1,高位为 0 的数。所以,当你使用 & 运算符与 x 进行运算时,它会保留 x 的最低 n 位,这正好是你想要的模运算结果。

6.3.6、画点函数

/**
 * @brief OLED画点函数
 * 
 * @param x 要画的点的x坐标,范围: 0 ~ 127
 * @param y 要画的点的y坐标,范围: 0 ~ 63
 */
void OLED_DrawPoint(uint8_t x, uint8_t y)
{
    if (x < 0 || x > 127 || y < 0 || y > 63)                                    // 超出范围
    {
        return;
    }

    g_oled_gram[y >> 3][x] |= 0x01 << (y & 0x07);                               // 画点
}

6.4、显示字符

6.4.2、OLED显示字符函数

  这里,我们用取模软件生成字符的数据,这里采用:阴码 + 列行式 + 逆向 + C51 的格式。

字符取模

/* 常用ASCII表
 * 偏移量32 
 * ASCII字符集: !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
 * PC2LCD2002取模方式设置:阴码 + 列行式 + 逆向 + C51
 * 总共:4个字符集(12*12、16*16、24*24和32*32),用户可以自行新增其他分辨率的字符集。
 * 每个字符所占用的字节数为:(size/8+((size%8)?1:0))*(size/2),其中size:是字库生成时的点阵大小(12/16/24/32...)
 */
/**
 * @brief OLED显示字符函数
 * 
 * @param x 要显示的字符所在的列,范围: 0 ~ 127
 * @param y 要显示的字符所在的页,范围: 0 ~ 7
 * @param chr 要显示的字符
 * @param size 要显示的字符大小
 */
void OLED_ShowChar(uint8_t x, uint8_t y, char chr, uint8_t size)
{
    uint8_t *pfont = NULL;

    if (x < 0 || x > 127 || y < 0 || y > 63)                                    // 超出范围
    {
        return;
    }

    switch (size)
    {
    case 12:
        pfont = (uint8_t *)ascii_06x12[chr - ' '];
        break;

    case 16:
        pfont = (uint8_t *)ascii_08x16[chr - ' '];
        break;
  
    default:
        return;
    }

    for (uint8_t i = 0; i < size / 2; i++)
    {
        g_oled_gram[y][x + i] = pfont[i];
        g_oled_gram[y + 1][x + i] = pfont[i + size / 2];
    }
}

6.4.3、OLED显示字符串函数

/**
 * @brief OLED显示字符串函数
 * 
 * @param x 要显示的字符串所在的列,范围: 0 ~ 127
 * @param y 要显示的字符串所在的页,范围: 0 ~ 7
 * @param str 要显示的字符串
 * @param size 要显示的字符串的大小
 */
void OLED_ShowString(uint8_t x, uint8_t y, char * str, uint8_t size)
{
    if (x < 0 || x > 127 || y < 0 || y > 63)                                    // 超出范围
    {
        return;
    }

    for (uint8_t i = 0; str[i] != '\0'; i++)
    {
        OLED_ShowChar(x + i * size / 2, y, str[i], size);
    }
}

6.4.4、OLED显示汉字函数

  这里,我们定义一个结构体数组用来存放汉字的取模数据。其中,单个结构体变量的成员定义如下:

typedef struct ChineseCell_16x16
{
    char index[4];
    unsigned char data[32];
} ChineseCell_16x16;
const ChineseCell_16x16 chinese_16x16[] = {
{"小", {0x00,0x00,0x00,0xE0,0x00,0x00,0x00,0xFF,0x00,0x00,0x00,0x20,0x40,0x80,0x00,0x00,0x08,0x04,0x03,0x00,0x00,0x40,0x80,0x7F,0x00,0x00,0x00,0x00,0x00,0x01,0x0E,0x00},/*"小",0*/},
{"樱", {0x10,0x90,0xFF,0x90,0xBE,0x42,0x3A,0x42,0xBE,0x00,0xBE,0x42,0x3A,0x42,0xBE,0x00,0x06,0x01,0xFF,0x00,0x83,0x82,0x9A,0x56,0x53,0x22,0x32,0x4E,0x42,0x82,0x02,0x0},/*"樱",1*/},

{"", {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF}},
};

  这里,我们采用一种映射关系来存储汉字的取模数据。其中,index 存放的是单个汉字,例如:樱,而 data 用来存储对应汉字取模的数据。我们在显示汉字时,首先会依次取出每个汉字(UTF-8 编码一般用 3 个字节存储汉字,GBK 编码一般会使用 2 个字节存储汉字),然后与结构体数组中的 index 数组存放的单个汉字进行比较(C 语言中,可以用 string.h 库文件提供的 strcmp() 函数进行比较),如果找到则记录对应的索引,然后根据此索引取出对应的 data 中数据。如果没有找到,则不显示或显示默认字符。

/**
 * @brief OLED显示汉字函数
 * 
 * @param x 要显示的汉字所在的列,范围: 0 ~ 127
 * @param y 要显示的汉字所在的页,范围: 0 ~ 7
 * @param chinese 要显示的汉字
 * @param size 要显示的汉字大小
 */
void OLED_ShowChinese(uint8_t x, uint8_t y, char *chinese, uint8_t size)
{
    char sigleChinese[4] = {0};                                                 // 存储单个汉字
    uint8_t index = 0;                                                          // 汉字索引
    uint8_t j = 0;

    if (x < 0 || x > 127 || y < 0 || y > 63)                                    // 超出范围
    {
        return;
    }

    switch (size)
    {
    case 16:
        for (uint8_t i = 0; chinese[i] != '\0'; i++)                            // 遍历汉字字符串
        {
            // 获取单个汉字,一般UTF-8编码使用3个字节存储汉字,GBK编码使用2个字节存储汉字
            sigleChinese[index] = chinese[i];
            index++;
            index = index % 3;

            if (index == 0)                                                     // 汉字索引为0,说明已经获取了一个汉字
            {
                for (j = 0; strcmp(chinese_16x16[j].index, "") != 0; j++)       // 遍历汉字数组
                {
                    if (strcmp(chinese_16x16[j].index, sigleChinese) == 0)      // 找到汉字
                    {
                        break;
                    } 
                }

                for (uint8_t k = 0; k < size; k++)
                {
                    // ((i + 1) / 3)定位到第几个汉字
                    g_oled_gram[y][x + k + ((i + 1) / 3 - 1) * size] = chinese_16x16[j].data[k];
                    g_oled_gram[y + 1][x + k + ((i + 1) / 3 - 1) * size] = chinese_16x16[j].data[k + size];
                }
            }
        }
    }
}

6.4.5、OLED显示数字函数

typedef struct DataFormat
{
    uint8_t digit;                                                              // 位数
    uint8_t decimal;                                                            // 小数位数
    uint8_t symbol : 1;                                                         // 是否显示符号
    uint8_t align_right : 1;                                                    // 是否右对齐
} DataFormat;
/**
 * @brief OLED显示数字函数
 * 
 * @param x 要显示的数字的列,范围: 0 ~ 127
 * @param y 要显示的数字的行,范围: 0 ~ 63
 * @param num 要显示的数字
 * @param size 要显示的数字大小
 * @param dataFormat 格式化结构体
 */
void OLED_ShowNumber(uint8_t x, uint8_t y, int num, uint8_t size, DataFormat dataFormat)
{
    char str[20] = {0};

    if (x < 0 || x > 127 || y < 0 || y > 63)                                    // 超出范围
    {
        return;
    }

    if (dataFormat.align_right)
    {
        if (dataFormat.symbol)
        {
            sprintf(str, "%+*d", dataFormat.digit, num);                        // 显示符号右对齐
        }
        else
        {
            sprintf(str, "%*d", dataFormat.digit, num);                         // 不显示符号右对齐
        }
    }
    else
    {
        if (dataFormat.symbol)                
        {
            sprintf(str, "%+d", num);                                           // 显示符号左对齐
        }
        else                                  
        {
            sprintf(str, "%d", num);                                            // 不显示符号左对齐
        }
    }

    OLED_ShowString(x, y, str, size);
}

6.4.5、OLED显示浮点数

/**
 * @brief OLED显示浮点数函数
 * 
 * @param x 要显示的数字的列,范围: 0 ~ 127
 * @param y 要显示的数字的行,范围: 0 ~ 63
 * @param num 要显示的数字
 * @param size 要显示的数字大小
 * @param dataFormat 格式化结构体
 */
void OLED_ShowDecimal(uint8_t x, uint8_t y, double decimal, uint8_t size, DataFormat dataFormat)
{
    int num = (int)decimal;
    int temp = num;
    int len = 0;
    int x1 = x;

    if (x < 0 || x > 127 || y < 0 || y > 63)                                    // 超出范围
    {
        return;
    }

    dataFormat.decimal = dataFormat.decimal ? dataFormat.decimal : 2;           // 小数部分的位数默认为两位

    // 获取整数的位数
    while (temp)
    {
        temp /= 10;
        len++;
    }
  

    // 获取整数的位数
    if (dataFormat.digit > len)
    {
        dataFormat.digit -= dataFormat.decimal;                                     // 获取整数的位数
        dataFormat.align_right ? dataFormat.digit-- : dataFormat.digit;             // 如果右对齐的话,还要左移一位,显示小数点
        dataFormat.digit = (dataFormat.digit > len) ? dataFormat.digit : len;       // 确保整数的位数大于等于指定值的整数位
    }
  
    OLED_ShowNumber(x, y, num, size, dataFormat);

    // 显示小数点
    if (dataFormat.digit > len)
    {
        if (dataFormat.align_right)
        {
            x1 += (dataFormat.digit * (size / 2));
        }
        else
        {
            x1 += (len * (size / 2));
            x1 = ((num < 0) || (dataFormat.symbol)) ? x1 + (size / 2) : x1;
        }
    }
    else
    {
        x1 += (len * (size / 2));
        x1 = ((num < 0) || (dataFormat.symbol)) ? x1 + (size / 2) : x1;
    }
  
  
  
    OLED_ShowChar(x1, y, '.', size);
    x1 += (size / 2);

    // 显示小数部分
    dataFormat.symbol = 0;                                                      // 小数部分不显示符号
    dataFormat.digit = dataFormat.decimal;                                      // 放大后,重新设置要显示的位数
    dataFormat.align_right = 0;                                                 // 小数部分为左对齐

    // 放大小数位,并且确保得到的结果为整数
    temp = 1;
    for (int i = 0; i < dataFormat.decimal; i++)
    {
        temp *= 10;
    }
    num = decimal * temp - num * temp;
    num = num > 0 ? num : -num;

    OLED_ShowNumber(x1, y, num, size, dataFormat);
}

6.4.6、OLED打印格式化字符串

/**
 * @brief OLED打印函数
 * 
 * @param x 要显示的字符串的列,范围: 0 ~ 127
 * @param y 要显示的字符串的行,范围: 0 ~ 63
 * @param size 要显示的字符串大小
 * @param format 指定要显示的格式化字符串
 * @param ... 格式化字符串参数列表
 * 
 * @note 此函数对浮点数的支持不好,使用浮点数时无法显示
 */
void OLED_Printf(uint8_t x, uint8_t y, uint8_t size, char *format, ...)
{
    char str[256];                                                              // 定义字符数组
    va_list arg;                                                                // 定义可变参数列表数据类型的变量arg
    va_start(arg, format);                                                      // 从format开始,接收参数列表到arg变量
    vsprintf(str, format, arg);                                                 // 使用vsprintf打印格式化字符串和参数列表到字符数组中
    va_end(arg);                                                                // 结束变量arg
    OLED_ShowString(x, y, str, size);                                           // OLED显示字符数组(字符串)
}

6.5、任意位置显示字符

6.5.1、任意位置显示图像函数

/**
 * @brief OLED在任意位置绘制图像
 * 
 * @param x 要绘制图像所在的列,范围: 0 ~ 127
 * @param y 要绘制图像所在的列,范围: 0 ~ 63
 * @param width 要绘制图像的宽
 * @param height 要绘制图像的高
 * @param image 要绘制图像的数据数组
 */
void OLED_ShowImage(uint8_t x, uint8_t y, uint8_t width, uint8_t height, const uint8_t *image)
{
    if (x < 0 || x > 127 || y < 0 || y > 63)                                    // 超出范围
    {
        return;
    }

    // 清除区域后覆盖显示,如果不清空指定区域为叠加显示
    OLED_ClearArea(x, y, width, height);

    for (uint8_t i = 0; i < ((height - 1) >> 3) + 1; i++)
    {
        for (int j = 0; j < width; j++)
        {
            g_oled_gram[(y >> 3) + i][x + j] |= image[i * width + j] << (y & 0x07);
            g_oled_gram[(y >> 3)+ i + 1][x + j] |= image[i * width + j] >> (8 - (y & 0x07));
        }
    }
}

6.5.2、任意位置显示字符函数

/**
 * @brief OLED显示字符函数
 * 
 * @param x 要显示的字符所在的列,范围: 0 ~ 127
 * @param y 要显示的字符所在的页,范围: 0 ~ 63
 * @param chr 要显示的字符
 * @param size 要显示的字符大小
 */
void OLED_ShowChar(uint8_t x, uint8_t y, char chr, uint8_t size)
{
    uint8_t *pfont = NULL;

    if (x < 0 || x > 127 || y < 0 || y > 63)                                    // 超出范围
    {
        return;
    }

    switch (size)
    {
    case 12:
        pfont = (uint8_t *)ascii_06x12[chr - ' '];
        break;

    case 16:
        pfont = (uint8_t *)ascii_08x16[chr - ' '];
        break;
  
    default:
        return;
    }

    OLED_ShowImage(x, y, size / 2, size, pfont);
}

6.5.3、任意位置显示汉字函数

/**
 * @brief OLED显示汉字函数
 * 
 * @param x 要显示的汉字所在的列,范围: 0 ~ 127
 * @param y 要显示的汉字所在的页,范围: 0 ~ 63
 * @param chinese 要显示的汉字
 * @param size 要显示的汉字大小
 */
void OLED_ShowChinese(uint8_t x, uint8_t y, char *chinese, uint8_t size)
{
    char sigleChinese[4] = {0};                                                 // 存储单个汉字
    uint8_t index = 0;                                                          // 汉字索引
    uint8_t j = 0;

    if (x < 0 || x > 127 || y < 0 || y > 63)                                    // 超出范围
    {
        return;
    }

    switch (size)
    {
    case 16:
        for (uint8_t i = 0; chinese[i] != '\0'; i++)                            // 遍历汉字字符串
        {
            // 获取单个汉字,一般UTF-8编码使用3个字节存储汉字,GBK编码使用2个字节存储汉字
            sigleChinese[index] = chinese[i];
            index++;
            index = index % 3;

            if (index == 0)                                                     // 汉字索引为0,说明已经获取了一个汉字
            {
                for (j = 0; strcmp(chinese_16x16[j].index, "") != 0; j++)       // 遍历汉字数组
                {
                    if (strcmp(chinese_16x16[j].index, sigleChinese) == 0)      // 找到汉字
                    {
                        break;
                    } 
                }

                // ((i + 1) / 3)定位到第几个汉字
                OLED_ShowImage(x + ((i + 1) / 3 - 1) * size y, size, size, chinese_16x16[j].data);
            }
        }
    }
}

6.6、绘制几何图形

6.6.1、绘制直线

/**
 * @brief OLED绘制直线函数
 * 
 * @param x0 起始点所在的列,范围: 0 ~ 127
 * @param y0 起始点所在的行,范围: 0 ~ 63
 * @param x1 终点所在的列,范围: 0 ~ 127
 * @param y1 终点所在的行,范围: 0 ~ 63
 */
void OLED_DrawLine(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1)
{
    int16_t x = 0, y = 0, dx = 0, dy = 0, d = 0, incrE = 0, incrNE = 0, temp = 0;
    int16_t x0_t = x0, y0_t = y0, x1_t = x1, y1_t = y1;
    uint8_t yFlag = 0, xyFlag = 0;

    if (y0_t == y1_t)                                                           // 横线单独处理
    {
        if (x0_t > x1_t)                                                        // 0号点x坐标大于1号点x坐标,则交换两点x坐标
        {
            temp = x0_t; 
            x0_t = x1_t; 
            x1_t = temp;
        }
  
        for (x = x0_t; x <= x1_t; x++)                                          // 遍历x坐标,依次画点
        {
            OLED_DrawPoint(x, y0);
        }
    }
    else if (x0_t == x1_t)                                                      // 竖线单独处理
    {
        if (y0_t > y1_t)                                                        // 0号点y坐标大于1号点y坐标,则交换两点y坐标
        {
            temp = y0_t;
            y0_t = y1_t;
            y1_t = temp;
        }
  
        for (y = y0_t; y <= y1_t; y++)                                          // 遍历y坐标,依次画点
        {
            OLED_DrawPoint(x0, y);
        }
    }
    else                                                                        // 斜线
    {
        // 使用Bresenham算法画直线,可以避免耗时的浮点运算,效率更高
        if (x0_t > x1_t)                                                        // 0号点x坐标大于1号点x坐标,交换两点坐标
        {
            // 交换后不影响画线,但是画线方向由第一、二、三、四象限变为第一、四象限
            temp = x0_t; x0_t = x1_t; x1_t = temp;
            temp = y0_t; y0_t = y1_t; y1_t = temp;
        }
  
        if (y0_t > y1_t)                                                        // 0号点y坐标大于1号点y坐标,将y坐标取负
        {
            // 取负后影响画线,但是画线方向由第一、四象限变为第一象限
            y0_t = -y0_t;
            y1_t = -y1_t;
  
            yFlag = 1;                                                          // 置标志位yflag,记住当前变换,在后续实际画线时,再将坐标换回来
        }
  
        if (y1_t - y0_t > x1_t - x0_t)	                                        // 画线斜率大于1,将x坐标与y坐标互换
        {
            // 互换后影响画线,但是画线方向由第一象限0~90度范围变为第一象限0~45度范围
            temp = x0_t; x0_t = y0_t; y0_t = temp;
            temp = x1_t; x1_t = y1_t; y1_t = temp;
  
            xyFlag = 1;                                                         // 置标志位xyflag,记住当前变换,在后续实际画线时,再将坐标换回来
        }
  
        // 以下为Bresenham算法画直线,算法要求,画线方向必须为第一象限0~45度范围
        dx = x1_t - x0_t;
        dy = y1_t - y0_t;
        incrE = 2 * dy;
        incrNE = 2 * (dy - dx);
        d = 2 * dy - dx;
        x = x0_t;
        y = y0_t;

        // 画起始点,同时判断标志位,将坐标换回来
        if (yFlag && xyFlag) { 
            OLED_DrawPoint(y, -x); 
        } else if (yFlag) { 
            OLED_DrawPoint(x, -y); 
        } else if (xyFlag) { 
            OLED_DrawPoint(y, x); 
        } else { 
            OLED_DrawPoint(x, y); 
        }

        while (x < x1_t)                                                        // 遍历x轴的每个点
        {
            x++;
            if (d < 0)                                                          // 下一个点在当前点东方
            {
                d += incrE;
            }
            else                                                                // 下一个点在当前点东北方
            {
                y++;
                d += incrNE;
            }

            // 画每一个点,同时判断标志位,将坐标换回来
            if (yFlag && xyFlag) { 
                OLED_DrawPoint(y, -x); 
            } else if (yFlag) { 
                OLED_DrawPoint(x, -y); 
            } else if (xyFlag) { 
                OLED_DrawPoint(y, x); 
            } else { 
                OLED_DrawPoint(x, y); 
            }
        }
    }
}

6.6.2、绘制矩形

/**
 * @brief OLED绘制矩形
 * 
 * @param x 要绘制矩形所在的列,范围: 0 ~ 127
 * @param y 要绘制矩形所在的行,范围: 0 ~ 63
 * @param width 要绘制矩形的宽,范围: 1 ~ 127
 * @param height 要绘制矩形的高,范围: 1 ~ 63
 * @param isFilled 是否要填充 1: 填充,0: 不填充
 */
void OLED_DrawRectangle(uint8_t x, uint8_t y, uint8_t width, uint8_t height, uint8_t isFilled)
{
    int16_t i = 0, j = 0;

    if (!isFilled)		                                                        // 指定矩形不填充
    {
        for (i = x; i < x + width; i++)                                         // 遍历上下x坐标,画矩形上下两条线
        {
            OLED_DrawPoint(i, y);
            OLED_DrawPoint(i, y + height - 1);
        }

        for (i = y; i < y + height; i++)                                        // 遍历左右y坐标,画矩形左右两条线
        {
            OLED_DrawPoint(x, i);
            OLED_DrawPoint(x + width - 1, i);
        }
    }
    else                                                                        // 指定矩形填充
    {
        for (i = x; i < x + width; i++)                                         // 遍历x坐标
        {
            for (j = y; j < y + height; j++)                                    // 遍历y坐标
            {
                OLED_DrawPoint(i, j);                                           // 在指定区域画点,填充满矩形
            }
        }
    }
}

6.6.3、绘制三角形

/**
 * @brief LCD绘制三角形
 * 
 * @param x0 第一个点所在的列,范围: 0 ~ 127
 * @param y0 第一个点所在的行,范围: 0 ~ 63
 * @param x1 第二个点所在的列,范围: 0 ~ 127
 * @param y1 第二个点所在的行,范围: 0 ~ 63
 * @param x2 第三个点所在的列,范围: 0 ~ 127
 * @param y2 第三个点所在的行,范围: 0 ~ 63
 * @param isFilled 是否要填充 1: 填充,0: 不填充
 */
void OLED_DrawTriangle(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2, uint8_t isFilled)
{
    int16_t minX = x0, minY = y0, maxX = x0, maxY = y0;
    int16_t i = 0, j = 0;
    int vertexX[] = {x0, x1, x2};
    int vertexY[] = {y0, y1, y2};

    if (!isFilled)                                                              // 指定三角形不填充
    {
        // 调用画线函数,将三个点用直线连接
        OLED_DrawLine(x0, y0, x1, y1);
        OLED_DrawLine(x0, y0, x2, y2);
        OLED_DrawLine(x1, y1, x2, y2);
    }
    else                                                                        // 指定三角形填充
    {
        // 找到三个点最小的x、y坐标
        if (x1 < minX) {minX = x1;}
        if (x2 < minX) {minX = x2;}
        if (y1 < minY) {minY = y1;}
        if (y2 < minY) {minY = y2;}
  
        // 找到三个点最大的x、y坐标
        if (x1 > maxX) {maxX = x1;}
        if (x2 > maxX) {maxX = x2;}
        if (y1 > maxY) {maxY = y1;}
        if (y2 > maxY) {maxY = y2;}
  
  
        // 最小最大坐标之间的矩形为可能需要填充的区域
        for (i = minX; i <= maxX; i++)
        {
            for (j = minY; j <= maxY; j++)
            {
                // 调用pnpoly算法,判断指定点是否在指定三角形之中,如果在,则画点,如果不在,则不做处理
                if (pnpoly(3, vertexX, vertexY, i, j)) {OLED_DrawPoint(i, j);}
            }
        }
    }
}
 /**
  * @brief 判断指定点是否在指定多边形内部
  * 
  * @param nVertex 多边形的顶点数
  * @param vertexX 多边形的顶点x坐标集合
  * @param vertexY 多边形的顶点y坐标集合
  * @param x 指定点的x坐标
  * @param y 指定点的y坐标
  * @return uint8_t 0: 不在多边形内部; 1: 在多边形内部;
  */
uint8_t pnpoly(uint8_t nVertex, int *vertexX, int *vertexY, int x, int y) 
{
    int i = 0, j = 0;
    int c = 0;

    for (i = 0, j = nVertex-1; i < nVertex; j = i++) 
    {
        if (((vertexY[i] > y) != (vertexY[j] > y)) &&
            (x < (vertexX[j] - vertexX[i]) * (y - vertexY[i]) / (vertexY[j] - vertexY[i]) + vertexX[i])) {
            c = !c;
        }
    }
    return c;
}

6.6.4、绘制圆

/**
 * @brief OLED绘制圆
 * 
 * @param x 圆心所在的列,范围: 0 ~ 127
 * @param y 圆心所在的行,范围: 0 ~ 63
 * @param radius 半径
 * @param isFilled 是否要填充 1: 填充,0: 不填充
 */
void OLED_DrawCircle(uint8_t x, uint8_t y, uint8_t radius, uint8_t isFilled)
{
    // 使用Bresenham算法画圆,可以避免耗时的浮点运算,效率更高

    int16_t x_t = 0, y_t = radius;
    int16_t d = 1 - radius;
    int16_t j = 0;

    // 画每个八分之一圆弧的起始点
    OLED_DrawPoint(x + x_t, y + y_t);
    OLED_DrawPoint(x - x_t, y - y_t);
    OLED_DrawPoint(x + y_t, y + x_t);
    OLED_DrawPoint(x - y_t, y - x_t);

    if (isFilled)		                                                        // 指定圆填充
    {
        // 遍历起始点y坐标,在指定区域画点,填充部分圆
        for (j = -y_t; j < y_t; j++)
        {
            OLED_DrawPoint(x, y + j);
        }
    }

    while (x_t < y_t)		                                                    // 遍历x轴的每个点
    {
        x_t++;
        if (d < 0)		                                                        // 下一个点在当前点东方
        {
            d += 2 * x_t + 1;
        }
        else			                                                        // 下一个点在当前点东南方
        {
            y_t--;
            d += 2 * (x_t - y_t) + 1;
        }
  
        // 画每个八分之一圆弧的点
        OLED_DrawPoint(x + x_t, y + y_t);
        OLED_DrawPoint(x + y_t, y + x_t);
        OLED_DrawPoint(x - x_t, y - y_t);
        OLED_DrawPoint(x - y_t, y - x_t);
        OLED_DrawPoint(x + x_t, y - y_t);
        OLED_DrawPoint(x + y_t, y - x_t);
        OLED_DrawPoint(x - x_t, y + y_t);
        OLED_DrawPoint(x - y_t, y + x_t);
  
        if (isFilled)	                                                        // 指定圆填充
        {
            // 遍历中间部分,在指定区域画点,填充部分圆
            for (j = -y_t; j < y_t; j++)
            {
                OLED_DrawPoint(x + x_t, y + j);
                OLED_DrawPoint(x - x_t, y + j);
            }
  
            // 遍历两侧部分
            for (j = -x_t; j < x_t; j++)
            {
                // 在指定区域画点,填充部分圆
                OLED_DrawPoint(x - y_t, y + j);
                OLED_DrawPoint(x + y_t, y + j);
            }
        }
    }
}

6.6.5、绘制椭圆

/**
 * @brief OLED绘制椭圆函数
 * 
 * @param x 圆心所在的列,范围: 0 ~ 127
 * @param y 圆心所在的行,范围: 0 ~ 63
 * @param a 椭圆的横向半轴长度,范围: 0 ~ 127
 * @param b 椭圆的纵向半轴长度,范围: 0 ~ 63
 * @param isFilled 是否要填充 1: 填充,0: 不填充
 */
void OLED_DrawEllipse(uint8_t x, uint8_t y, uint8_t a, uint8_t b, uint8_t isFilled)
{
    int16_t x_t = 0;
    int16_t y_t = b;
    int16_t a_t = a, b_t = b;
    int16_t j = 0;
    float  d1 = b_t * b_t + a_t * a_t * (-b_t + 0.5);
    float d2 = 0;

    // 使用Bresenham算法画椭圆,可以避免部分耗时的浮点运算,效率更高
    if (isFilled)	//指定椭圆填充
    {
        // 遍历起始点y坐标在指定区域画点,填充部分椭圆
        for (j = -y_t; j < y_t; j++)
        {
            OLED_DrawPoint(x, y + j);
            OLED_DrawPoint(x, y + j);
        }
    }

    // 画椭圆弧的起始点
    OLED_DrawPoint(x + x_t, y + y_t);
    OLED_DrawPoint(x - x_t, y - y_t);
    OLED_DrawPoint(x - x_t, y + y_t);
    OLED_DrawPoint(x + x_t, y - y_t);

    // 画椭圆中间部分
    while (b_t * b_t * (x_t + 1) < a_t * a_t * (y_t - 0.5))
    {
        if (d1 <= 0)                                                            // 下一个点在当前点东方
        {
            d1 += b_t * b_t * (2 * x_t + 3);
        }
        else                                                                    // 下一个点在当前点东南方
        {
            d1 += b_t * b_t * (2 * x_t + 3) + a_t * a_t * (-2 * y_t + 2);
            y_t--;
        }
        x_t++;
  
        if (isFilled)                                                           // 指定椭圆填充
        {
            // 遍历中间部分
            for (j = -y_t; j < y_t; j++)
            {
                // 在指定区域画点,填充部分椭圆
                OLED_DrawPoint(x + x_t, y + j);
                OLED_DrawPoint(x - x_t, y + j);
            }
        }
  
        // 画椭圆中间部分圆弧
        OLED_DrawPoint(x + x_t, y + y_t);
        OLED_DrawPoint(x - x_t, y - y_t);
        OLED_DrawPoint(x - x_t, y + y_t);
        OLED_DrawPoint(x + x_t, y - y_t);
    }

    // 画椭圆两侧部分
    d2 = b_t * b_t * (x_t + 0.5) * (x_t + 0.5) + a_t * a_t * (y_t - 1) * (y_t - 1) - a_t * a_t * b_t * b_t;

    while (y_t > 0)
    {
        if (d2 <= 0)		                                                    // 下一个点在当前点东方
        {
            d2 += b_t * b_t * (2 * x_t + 2) + a_t * a_t * (-2 * y_t + 3);
            x_t++;
  
        }
        else				                                                    // 下一个点在当前点东南方
        {
            d2 += a_t * a_t * (-2 * y_t + 3);
        }
        y_t--;
  
        if (isFilled)	                                                        // 指定椭圆填充
        {
            // 遍历两侧部分,在指定区域画点,填充部分椭圆
            for (j = -y_t; j < y_t; j++)
            {
                OLED_DrawPoint(x + x_t, y + j);
                OLED_DrawPoint(x - x_t, y + j);
            }
        }
  
        /*画椭圆两侧部分圆弧*/
        OLED_DrawPoint(x + x_t, y + y_t);
        OLED_DrawPoint(x - x_t, y - y_t);
        OLED_DrawPoint(x - x_t, y + y_t);
        OLED_DrawPoint(x + x_t, y - y_t);
    }
}

6.7、main()函数

int main(void)
{
    HAL_Init();
    System_Clock_Init(8, 336, 2, 7);
    Delay_Init(168);

    SPI_Init(&g_spi1_handle, SPI1, SPI_POLARITY_HIGH, SPI_PHASE_2EDGE, SPI_BAUDRATEPRESCALER_2, SPI_FIRSTBIT_MSB);

    g_oled_spi_handle_ptr = &g_spi1_handle;

    OLED_Init();

    OLED_ShowChar(0, 0, 'A', 12);
    OLED_ShowChar(10, 0, 'A', 16);
    OLED_ShowString(10, 20, "Hello World!", 12);
    OLED_ShowString(20, 40, "Hello Sakura!", 16);
    OLED_ShowChinese(80, 60, "小樱", 16);

    OLED_RefreshGRAM();

    while (1)
    {
  
    }
  
    return 0;
}
posted @ 2023-11-24 22:23  星光映梦  阅读(198)  评论(0编辑  收藏  举报