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模块接线
引脚 | 含义 |
---|---|
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 驱动芯片。当 GRAM 的写入模式为页模式时,需要设置低字节起始的列地址(0x00 ~ 0x0F)和高字节的起始列地址(0x10 ~ 0x1F),芯片手册中给出了写入 GRAM 与显示的对应关系,写入列地址在写完一字节后自动按列增长。
因为每次写入都是按字节写入的,这就存在一个问题,如果我们使用只写方式操作模块,那么,每次要写 8 个点,这样,我们在画点的时候,就必须把要设置的点所在的字节的每个位都搞清楚当前的状态,否则写入的数据就会覆盖掉之前的状态,结果就是有些不需要显示的点,显示出来了,或者该显示的没有显示了。这个问题在能读的模式下,我们可以先读出来要写入的那个字节,得到当前状况,在修改了要改写的位之后再写进 GRAM,这样就不会影响到之前的状况了。但对于 4 线 SPI 模式/IIC 模式,模块是不支持读 GRAM 的。
因此,我们采用的办法是在单片机的内部建立一个虚拟的 OLED 的 GRAM(共 128 * 8=1024 个字节),每次修改时,只修改单片机上的 GRAM(实际上就是 SRAM),在修改完成后一次性把单片机上的 GRAM 写入到 OLED 的 GRAM。当然这个方法也有坏处,一个对于那些 SRAM 很小的单片机(比如 51 系列)不太友好,另一个是每次都写入全屏,屏幕刷新率会变低。
四、SSD1306常用命令
SSD1306 的命令比较多,这里我们仅介绍几个比较常用的命令。
第 0 个命令为 0X81,用于设置对比度的,这个命令包含了两个字节,第一个 0X81 为命令,随后发送的一个字节为要设置的对比度的值。这个值设置得越大屏幕就越亮。
第 1 个命令为 0XAE/0XAF。0XAE 为关闭显示命令;0XAF 为开启显示命令。
第 2 个命令为 0X8D,该指令也包含 2 个字节,第一个为命令字,第二个为设置值,第二个字节的 bit2 表示电荷泵的开关状态,该位为 1,则开启电荷泵,为 0 则关闭。在模块初始化的时候,这个必须要开启,否则是看不到屏幕显示的。
第 3 个命令为 0XB0~B7,该命令用于设置页地址,其低三位的值对应着 GRAM 的页地址。
第 4 个指令为 0X00~0X0F,该指令用于设置显示时的起始列地址低四位。
第 5 个指令为 0X10~0X1F,该指令用于设置显示时的起始列地址高四位。
五、原理图
引脚 | 含义 |
---|---|
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;
}