随着时代的进步,OLED显示屏成为了继LCD显示屏之后的新一代显示屏技术,OLED具有可视角高,功耗低,厚度薄,耐冲击、振动能力强,像素响应时间低等优点,在嵌入式开发中,OLED显示器也是一个主要的部分,制作OLED显示模块的驱动也是学习STM32路上的重要一部分,本篇将从零开始,一步一步教你编写属于自己的OLED驱动,全部源码放在交流群,有需要的可以入群拿,喜欢的不要忘了点赞以及关注博主哦
交流Q_qun:659512171
目录
一,基础知识:
二,STM32CubeMX配置:
1,新建工程:
2,配置工程:
(1)配置RCC时钟:
(3)配置调试:
(4)配置 IIC / SPI:
SPI:
IIC:
(5)配置工程:
(6)生成文件:
三,在 Keil 中编写OLED驱动:
1,新建oled.c,oled.h,oledfont.h:
2,编写程序:
画点,画线:
显示ASCII字符:
显示汉字,图片:
四,测试驱动:
效果展示:
SPI,IIC通信的基本原理以及OLED的基本知识
SPI,IIC:通信:传送门
下面介绍OLED的基本知识(鉴于OLED数据手册全是英文,初学者或者英语基础不好的朋友读起来很吃力,在这里就不贴数据手册了,有兴趣的可以进群下载,在这里我就分享一些我自己的经验以及理解):
先来简单介绍一下显示方式,OLED屏幕并不是一个像素一个数据,而是8个像素一个数据,所以实际数据数组(也可以不用数组,后面会讲)总共有64 / 8 = 8行,大小为 128 * 8 = 1024bit。下面给一张图片:
然后来说一下OLED的数据格式,规定当像素亮的时候表示为1,灭的时候表示为0,则一个包括8像素的数据就可以用一个8位数据来表示,一般使用16进制数。对于字体和图像的取模可以使用取模软件 PCtoLCD2002,这个软件的教程在博客上也有很多,大家可以自行下载,也可以在我的Q群中下载,软件的配置按照下图就可以了
其中的左下角的每行显示数据中的点阵大小根据你取的字模横向像素长度来设置,有多长就输入多长,例如:一个图片是64*32的,那么就输入64。索引不用管
接着来说一下OLED的初始化数据,这一段包含了包括显示方向,亮度,显示模式等等诸多显示相关的重要数据,一般而言使用厂家提供的默认初始化代码就可以,这里贴上:
OLED_WR_CMD(0xAE);//--display off
OLED_WR_CMD(0x00);//---set low column address
OLED_WR_CMD(0x10);//---set high column address
OLED_WR_CMD(0x40);//--set start line address
OLED_WR_CMD(0x20);//--set page address
OLED_WR_CMD(0x00);
OLED_WR_CMD(0x81);// contract control
OLED_WR_CMD(0xFF);//--设置亮度
OLED_WR_CMD(0xA1);//set segment remap
OLED_WR_CMD(0xA6);//--normal / reverse
OLED_WR_CMD(0xA8);//--set multiplex ratio(1 to 64)
OLED_WR_CMD(0x3F);//--1/32 duty
OLED_WR_CMD(0xC8);//Com scan direction
OLED_WR_CMD(0xD3);//-set display offset
OLED_WR_CMD(0x00);//
OLED_WR_CMD(0xD5);//set osc division
OLED_WR_CMD(0x80);//
OLED_WR_CMD(0xD8);//set area color mode off
OLED_WR_CMD(0x05);//
OLED_WR_CMD(0xD9);//Set Pre-Charge Period
OLED_WR_CMD(0xF1);//
OLED_WR_CMD(0xDA);//set com pin configuartion
OLED_WR_CMD(0x12);//
OLED_WR_CMD(0xDB);//set Vcomh
OLED_WR_CMD(0x30);//
OLED_WR_CMD(0x8D);//set charge pump enable
OLED_WR_CMD(0x14);//
OLED_WR_CMD(0xAF);//--turn on oled panel
在这里提供了OLED的初始化程序,后期可以通过向SSD1306的寄存器写入数据来更改设置,在这段语句中,OLED_WR_CMD 函数的定义是:向OLED写入 1 字节的命令。
另外也要说一下两种通信方式分别是怎么区分指令和数据的:
SPI:在SPI屏上有一个D/C接口(即Data/Command),SPI屏幕即是通过读取这个脚的电平来区别命令和数据的,D/C为低电平是命令,D/C为高电平时是数据
IIC:IIC屏幕通过控制字节来区分,命令是0x00,数据是0x40
下面我们来看一下HAL库提供的SPI/IIC通信函数:
/*这是HAL库的SPI通信示例*/
HAL_SPI_Transmit(&hspi1, data, 1024, 1000);
/*这是HAL库的IIC通信示例*/
HAL_I2C_Mem_Write(&hi2c1, 0x78, 0x00, 1024, CMD, 1000);
可以看到,在SPI通信中,没有提供从机地址和寄存器地址两个参数,如果你看了我之前写的解析通信协议的那篇文章,你一定知道,这是由于SPI本身不是地址选择从机的缘故,SPI通过片选(CS)选择从机,IIC通过地址选择从机,聪明的你可能又要问:那SPI怎么访问从机的寄存器呢?关于这个问题,我们可以借鉴一下IIC的通信时序(我前面一篇也讲了),在IIC中,地址在起始位之后,数据之前,所以在SPI中也就是在向寄存器发送数据前发送这个寄存器的地址,例如:现在有一个OLED显示屏,我想要调整他的亮度(对于黑白屏即为对比度),由数据手册可以得到亮度寄存器地址是 0x81h,那么我只需要先发送一个命令为 0x81,然后再发送一个命令为 0x80(亮度,取值在0x00 ~ 0xFF,即0 ~ 255)就可以实现亮度调整了
OLED命令的部分说的差不多了,下面来看一下显示数据:
关于OLED的显示,有三种方式,一种是页面寻址模式,即每个页是独立的,想要显示全屏需要操作完8行,示意图如下
此种方式的优点是抗干扰能力强,不会因一个数据错乱而影响全屏,缺点是需要传输多次数据,如果是性能较弱的单片机速度会不够快
另一种是水平寻址模式,即全屏作为一个整体,在一行输入完后跳转到下一行,结束后再跳到第一行,垂直寻址模式与此类似,也是全屏作为一个整体,但是在一列的数据输入完后跳入下一列,结束后跳到第一列,示意图如下:
水平寻址:
垂直寻址:
此种模式的优点是速度快,一次性传输完所有数据,缺点是抗干扰能力较弱,少一个数据会使全屏显示错位
这几种显示模式由SSD1306的0x20h寄存器决定,默认为0x10页面寻址模式,上面提供的初始化代码就是使用页面寻址模式,如果需要使用水平寻址(0x00)/垂直寻址(0x01),更改寄存器值为对应值即可,要注意的是,如果是页面寻址就需要设置高列和低列,其余的不用(也不用记,后面我会给出三种的初始化)
还有一点要注意,即是否使用显存,如果使用则代表在STM32上创建一个缓冲数组来存放下一次显示的数据,优点就是操作方便,后期的画点,滚屏等操作会很简单,但缺点就是占用空间较大(其实也不完全算缺点),而不使用显存就是直接把要显示的数据从字库中提取出来或者创建,然后一个一个的写入,优点就是占用小,对低性能,小SRAM的单片机很友好,但缺点就是操作不方便,而且速度进一步降低
当然,我还需要介绍一下IIC接口OLED和SPI接口OLED的区别,它们两个的区别就是几乎没有区别,因为它们本来用的就是同一个显示屏,只不过引出了不同的接口,做成了两个模块而已。由于只有接口不同,两种屏幕模块只在速度和写入函数上有所不同,体现在驱动程序上就是 OLED_WR_CMD 和 OLED_WR_Data 两个函数不太一样,其他的原理都是一样的
现在,想必你已经掌握了基础知识(是不是累了?收藏文章休息会接着看吧),可以开敲代码了,这里使用 STM32CubeMX 生成配置文件以减少工作量
打开STM32CubeMX,选择 Start My Project from MCU,选择你的MCU,这里我就拿最大众的 F103C8T6 举例:
这里输入主频敲击回车后就可以自动配置
这里要选好,不然会导致芯片自锁
为了方便后面的工作,我这里直接把IIC和SPI两个都配置了
这里如果报错(即红色的叉),调整下面的分频系数 Prescaler 即可
如果你看过我前面的文章,就会知道还需要配置一些GPIO来实现对从机的选择等操作,这里需要3个,分别是RES,DC,CS:
这里使用了临近的3个GPIO,都定义为浮空输出,为了方便区分,在下面的 User Label 对它进行命名
IIC无需进行任何额外设置
这里按照图中配置就好了
点击右上角 GENERATE CODE 生成文件,然后等待进度条跑完后点击 Open Project(前提是keil是工程文件默认打开程序)进入keil
注意!“.c” 文件一定要保存到文件的Core中的Src文件夹,位置如图:
如果是 “.h” 文件,则要保存到Core目录中的Inc文件夹(主要是看着整洁,而且Keil中有这两个路径)
可以通过右击左边的工程目录中的文件夹图标,然后在菜单中选择 Add New Item Group 来新建:
按照这个模式,再新建 oled.h 和 oledfont.h 即可,下面对这3个文件进行简单说明:
oled.c:关键程序都写在这里,比如函数的定义,是大脑
oled.h:函数的声明以及宏的定义都在这里,保证了oled.c的函数可以在主程序中调用,是神经系统
oledfont.h:字库,基本的英文ASCII库和自己取模的汉字字模都在这里,是记忆库
上述的3个文件共同实现了对OLED屏幕的驱动,但现在只是刚刚建好,所以下面就要为它们 “注入灵魂”,写程序
我一般习惯于先把头文件结构写好,这样在编写源文件的时候就可以只调用头文件(这一部分是给C语言基础不好的朋友们写的,基础好的可以直接跳过)
oled.h:
#ifndef __OLED_H__
#define __OLED_H__
#include "i2c.h"
#include "spi.h"
#include "main.h"
//在下面写函数声明
#endif
上面给出了头文件中的一般结构,在C语言中是很基础的东西,在此不做赘述
oledfont.h:
#ifndef __OLEDFONT_H__
#define __OLEDFONT_H__
//在下面写字库
#endif
好了,头文件结构都写好了,接下来总算该写程序了,下面我就边讲边贴代码,一个函数一个函数的讲解
首先,引用头文件:
#include "oled.h"
#include "oledfont.h"
如果这里报错就检查一下前面头文件是否写对了,以及两个头文件是否在Keil的路径中
然后,按照一般的程序,现在就该写初始化函数了,但是在这里面,还需要先把写命令和写数据的函数写好,这里先在头文件中定义几个宏方便调用RES,DC,CS引脚:
#define OLED_RES_Clr HAL_GPIO_WritePin(RES_GPIO_Port, RES_Pin, GPIO_PIN_RESET);
#define OLED_RES_Set HAL_GPIO_WritePin(RES_GPIO_Port, RES_Pin, GPIO_PIN_SET);
#define OLED_DC_Clr HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_RESET);
#define OLED_DC_Set HAL_GPIO_WritePin(DC_GPIO_Port, DC_Pin, GPIO_PIN_SET);
#define OLED_CS_Clr HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);
#define OLED_CS_Set HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
将上面的宏定义添加到oled.h或者是oled.c的靠上部分(至少位置在写命令和写数据函数之前,不然都会调用错误),我一般习惯于写到头文件里,然后就可以回到oled.c中编写函数了
unsigned char GRAM[1024]; //定义显存,适用于SRAM较大的单片机
/*页寻址显存:
unsigned char GRAM[8][128];
*/
//写命令
void OLED_WR_CMD(unsigned char cmd){
OLED_DC_Clr; //把 D/C 引脚拉低以表示命令
HAL_SPI_Transmit(&hspi1,&cmd,1,1000);
/*IIC:
HAL_I2C_Mem_Write(&hi2c1 ,0x78,0x00,I2C_MEMADD_SIZE_8BIT,&cmd,1,1000);
*/
}
//写数据
void OLED_WR_Data(unsigned char* data){
/*SPI:*/
OLED_DC_Set; //把 D/C 引脚拉高以表示数据
HAL_SPI_Transmit(&hspi1,data,1024,1000);
/*IIC:
HAL_I2C_Mem_Write(&hi2c1 ,0x78,0x40,I2C_MEMADD_SIZE_8BIT,data,1024,1000);
*/
}
/*页寻址:
//void OLED_WR_Data(unsigned char* data[8]){
// OLED_DC_Set; //把 D/C 引脚拉高以表示数据
// for(int i=0;i<8;i++){
SPI:
// HAL_SPI_Transmit(&hspi1,data[i],128,1000);
IIC:
HAL_I2C_Mem_Write(&hi2c1 ,0x78,0x40,I2C_MEMADD_SIZE_8BIT,data[i],128,1000);
// }
//}
*/
/*
如果不使用显存,则使用以下代码:
void OLED_WR_Data(unsigned char data){
OLED_DC_Set; //把 D/C 引脚拉高以表示数据
HAL_SPI_Transmit(&hspi1,&data,1,1000);
}
*/
上面我写了页寻址,水平寻址/垂直寻址的程序,后面就不再写了,要修改只需对显存的操作进行更改,例如:
for(int i=0;i<8;i++){
for(int t=0;t<128;t++){
GRAM[i*128 + t] = 0x00;
}
}
//可以化成
for(int i=0;i<8;i++){
for(int t=0;t<128;t++){
GRAM[i][t] = 0x00;
}
}
此后对于显存的操作就不再一一展示,参照上面的程序更改即可
下面就是初始化函数了,这里使用的是水平寻址模式
void OLED_Init(void){
HAL_Delay(200); //延时防止卡死
/*使用水平寻址模式*/
OLED_RES_Clr;
HAL_Delay(80);
OLED_RES_Set; //复位OLED
OLED_WR_CMD(0xAE); //display off
OLED_WR_CMD(0x20); //Set Memory Addressing Mode
OLED_WR_CMD(0x00); //00,Horizontal Addressing Mode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,Invalid
OLED_WR_CMD(0xB0); //Set Page Start Address for Page Addressing Mode,0-7
OLED_WR_CMD(0xC8); //Set COM Output Scan Direction
OLED_WR_CMD(0x00); //---set low column address
OLED_WR_CMD(0x10); //---set high column address
OLED_WR_CMD(0x40); //--set start line address
OLED_WR_CMD(0x81); //--set contrast control register
OLED_WR_CMD(0xFF); //亮度调节 0x00~0xff
OLED_WR_CMD(0xA1); //--set segment re-map 0 to 127
OLED_WR_CMD(0xA6); //--set normal display
OLED_WR_CMD(0xA8); //--set multiplex ratio(1 to 64)
OLED_WR_CMD(0x3F); //
OLED_WR_CMD(0xA4); //0xa4,Output follows RAM content;0xa5,Output ignores RAM content
OLED_WR_CMD(0xD3); //-set display offset
OLED_WR_CMD(0x00); //-not offset
OLED_WR_CMD(0xD5); //--set display clock divide ratio/oscillator frequency
OLED_WR_CMD(0xF0); //--set divide ratio
OLED_WR_CMD(0xD9); //--set pre-charge period
OLED_WR_CMD(0x22); //
OLED_WR_CMD(0xDA); //--set com pins hardware configuration
OLED_WR_CMD(0x12);
OLED_WR_CMD(0xDB); //--set vcomh
OLED_WR_CMD(0x20); //0x20,0.77xVcc
OLED_WR_CMD(0x8D); //--set DC-DC enable
OLED_WR_CMD(0x14); //
OLED_WR_CMD(0xAF); //--turn on oled panel
/*使用页面寻址模式
OLED_WR_CMD(0xAE);//--display off
OLED_WR_CMD(0x00);//---set low column address
OLED_WR_CMD(0x10);//---set high column address
OLED_WR_CMD(0x40);//--set start line address
OLED_WR_CMD(0xB0);//--set page address
OLED_WR_CMD(0x81); // contract control
OLED_WR_CMD(0xFF);//--128
OLED_WR_CMD(0xA1);//set segment remap
OLED_WR_CMD(0xA6);//--normal / reverse
OLED_WR_CMD(0xA8);//--set multiplex ratio(1 to 64)
OLED_WR_CMD(0x3F);//--1/32 duty
OLED_WR_CMD(0xC8);//Com scan direction
OLED_WR_CMD(0xD3);//-set display offset
OLED_WR_CMD(0x00);//
OLED_WR_CMD(0xD5);//set osc division
OLED_WR_CMD(0x80);//
OLED_WR_CMD(0xD8);//set area color mode off
OLED_WR_CMD(0x05);//
OLED_WR_CMD(0xD9);//Set Pre-Charge Period
OLED_WR_CMD(0xF1);//
OLED_WR_CMD(0xDA);//set com pin configuartion
OLED_WR_CMD(0x12);//
OLED_WR_CMD(0xDB);//set Vcomh
OLED_WR_CMD(0x30);//
OLED_WR_CMD(0x8D);//set charge pump enable
OLED_WR_CMD(0x14);//
OLED_WR_CMD(0xAF);//--turn on oled panel
*/
}
初始化函数无需过多深入研究,就是很无聊的一个寄存器一个寄存器的配置,没有什么可以学习的,直接使用厂家提供的就可以了
现在,我们成功的将OLED初始化了,但就像一个场地,如果都是人,还怎么做实验?所以我们需要一个函数来 “清场”
/*
功能描述:清屏
*/
void OLED_Clear(void){
for(int n=0;n<1024;n++){
GRAM[n]=0x00;
}
}
现在,你的oled.c中已经具备了发送命令/数据、初始化以及清屏的能力,但这只是准备好了场地,要想实现显示,还需要后面的函数:
画点可以说是最基础的一个函数了,有了画点函数,就相当于有了画线,画圆,画矩形,画三角等等函数,只需要利用一些数学知识,把OLED显示屏看作一个坐标系,代入数学函数即可画出各种图案,各位可以自己探索,这里只给出最基本的画点,画线函数:
/*
功能描述:在OLED中画点
参数:x:x坐标;y:y坐标;mode:1,反白显示;0,正常显示
*/
void OLED_DrawPoint(int x,int y,int mode){
int line_y,pixel_y,temp; //定义临时变量
if(x>127||y>63){
return;
} //判断数据合法性
line_y = y / 8; //计算对应显存行数
pixel_y = y % 8; //计算对应行的像素
temp = 0x01 << pixel_y; //通过移位得到数据
if(mode==0){
GRAM[line_y*128 + x] |= temp; //通过或运算来更新显存
}else{
GRAM[line_y*128 + x] &= temp; //通过与运算来更新显存
}
}
/*
功能描述:在OLED中画线
参数:x1,y1:起始坐标;x2,y2:终止坐标;mode:1,反白显示;0,正常显示
*/
void OLED_DrawLine(unsigned char x1,unsigned char y1,unsigned char x2,unsigned char y2,int mode)
{
unsigned char i = 0; //先计算增量Δy和Δx
char DeltaY = 0,DeltaX = 0;
float k = 0,b = 0; //考虑到斜率有小数的情况,所以b也写成浮点型
if(x1>x2) { //保持Δx为正,方便后面使用
i = x2;x2 = x1;x1 = i;
i = y2;y2 = y1;y1 = i;
i = 0;
}
if (y1 <= y2){
DeltaY = y2 - y1;
DeltaX = x2 - x1;
if(DeltaX == 0) { //斜率k不存在时的画法
if(y1 <= y2) {
for(y1 = y1; y1<=y2; y1++) {
OLED_DrawPoint(x1,y1,mode);
}
}else if(y1 > y2) {
for(y2 = y2; y2<=y1; y2++) {
OLED_DrawPoint(x1,y2,mode);
}
}
}else if(DeltaY == 0) { //斜率k为0时的画法
for(x1 = x1; x1<=x2; x1++) {
OLED_DrawPoint(x1,y1,mode);
}
}else { //斜率正常存在时的画法
k = ((float)DeltaY)/((float)DeltaX); //计算斜率
b = y2 - k * x2; //计算截距
if(k > -1 && k < 1) {
for(x1 = x1; x1 <= x2; x1++) {
OLED_DrawPoint(x1,(int)(k * x1 + b),mode);
}
}else if (k >= 1 || k <= -1){
for(y1 = y1; y1<=y2; y1++) {
OLED_DrawPoint((int)((y1 - b) / k),y1,mode);
}
}
}
}else if(y1 > y2){
int m = y1;
y1 = y2;
y2 = m;
DeltaY = y2 - y1;
DeltaX = x2 - x1;
if(DeltaX == 0) { //斜率k不存在时的画法
if(y1 <= y2) {
for(y1 = y1; y1<=y2; y1++) {
OLED_DrawPoint(x1,y1,mode);
}
}else if(y1 > y2) {
for(y2 = y2; y2<=y1; y2++) {
OLED_DrawPoint(x1,y2,mode);
}
}
}else { //斜率正常存在时的画法
k = ((float)DeltaY)/((float)DeltaX); //计算斜率
b = y2 - k * x2; //计算截距
int n = y1;
if(k > -1 && k < 1) {
for(x1 = x1; x1 <= x2; x1++) {
OLED_DrawPoint(x1,(int)(y2 - (k * x1 + b) + n),mode);
}
}else if (k >= 1 || k <= -1){
for(y1 = y1; y1<=y2; y1++) {
OLED_DrawPoint((int)(y1 - b) / k,y2 - y1 + n,mode);
}
}
}
}
}
现在,oled.c中已经具备画图的能力了,但是很明显,我们现代人不能和老祖先一样只用图像交流,所以我们还需要文字,需要文字就需要字库,下面给出两个尺寸ASCII字库(6*8和8*16尺寸),放在oledfont.h中,当然,你也可以直接使用系统字库,CSDN上也是有相关教程的,不过其实大同小异
PS:因为字库有点多,所以就挂上网盘下载链接,12k大小,很快就下好了
OLED字库https://pan.baidu.com/s/1Fi_6sSSdaICBMJB5QH1lQg?pwd=ao0d提取码 ao0d
现在字库有了,下一步就是提取数据到显存使用,为了方便使用,我将此功能分为三个部分,基础的单字符提取,数字显示,字符串显示,下面是代码:
/*
功能描述:显示一个字符
参数:x,y:坐标;chr:字符;Size:尺寸12(6*8)、16(8*16);mode:1,反白显示;0,正常显示
*/
void OLED_ShowChar(int x,int y,char chr,int Size,int mode)
{
if (mode == 0){
unsigned char c=0,i=0;
c=chr-' '; //得到偏移后的值
if(x>127){x=0;y=y+2;}
if(Size ==16){
for(i=0;i<8;i++){
GRAM[y*128 + x + i] = F8X16[c*16+i];
}
for(i=0;i<8;i++){
GRAM[(y+1) * 128 + x + i] = F8X16[c*16+i+8];
}
}else {
for(i=0;i<6;i++){
GRAM[y*28 + x + i] = F6x8[c*6+i];
}
}
}else if(mode == 1){
unsigned char c=0,i=0;
c=chr-' ';
if(x>127){x=0;y=y+2;}
if(Size ==16){
for(i=0;i<8;i++){
GRAM[y*128 + x + i] = ~F8X16[c*16+i];
}
for(i=0;i<8;i++){
GRAM[(y+1) * 128 + x + i] = ~F8X16[c*16+i+8];
}
}else {
for(i=0;i<6;i++){
GRAM[y* 128 + x + i] = ~F6x8[c*6+i];
}
}
}
}
/*m^n*/
int oled_pow(int x,int y)
{
int result=1;
for(int n = y;n > 0;n--){
result *= x;
}
return result;
}
/*
功能描述:显示数字
参数:x,y:起点坐标;size:字体大小:num:数值(0~4294967295);mode:1,反白显示;0,正常显示
*/
void OLED_ShowNum(int x,int y,int num,int size,int mode)
{
int t,te;
int enshow,len=0;
int temp = num;
do {
len++;
temp /= 10;
}while(temp != 0);
for(t=0;t120){
if (Char_Size == 16){
x=0;y+=2;
}else{
x=0;y+=1;
}
}
j++;
}
}
现在好了,有文字,能画图,应该可以了吧,但我们中国人怎么能不显示汉字呢?而且现在只能画图,要是想要显示一张图片该怎么做呢?所以还需要两个字库和函数分别存储,显示汉字和图片(不同尺寸的要分开,同种尺寸可以放在一起)
char Hzk16[][16]={
{0x00,0x00,0x1E,0x10,0x10,0x90,0xF0,0x9F,0x90,0x90,0x90,0x90,0x1E,0x00,0x00,0x00},
{0x80,0x80,0x84,0x42,0x41,0x42,0x24,0x28,0x10,0x08,0x04,0x03,0x00,0x00,0x00,0x00},/*"岁",0*/
{0x00,0x00,0x80,0x00,0x00,0xE0,0x02,0x04,0x18,0x00,0x00,0x00,0x40,0x80,0x00,0x00},
{0x10,0x0C,0x03,0x00,0x00,0x3F,0x40,0x40,0x40,0x40,0x40,0x78,0x00,0x01,0x0E,0x00},/*"心",1*/
};
char BMP32[][128]={
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x80,0xC0,0xA0,0x80,0x10,0x48,0xA4,0xD4,0xC8,0xC0,0xE0,0xE0,0xC0,0x80,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x03,0x0B,0x00,0x18,0x31,0x33,0x0B,0x03,0x03,0x07,0x03,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x66,0x66,0x24,0x24,0xA7,0x27,0x24,0x24,0xA4,0xE6,0x00,0x00,0x00,0xFC,0xFC,0x00,0x04,0xFE,0x00,0x00,0xFE,0x00,0x00,0xFE,0x00,0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x06,0x02,0x02,0x02,0x03,0x03,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x01,0x03,0x00,0x06,0x07,0x06,0x06,0x06,0x06,0x00,0x01,0x00,0x00,0x00,0x00,0x00},
/*"F:\JiJin\Pictures\Saved Pictures\LOGO.bmp",0*/
};
这个是我用PCtoLCD取的字模,大家可以自己取模,同样是放在oledfont.h中,当然,汉字也可以直接使用系统字库,这样就可以把汉字显示融合到显示字符串的函数里了,但本文作为演示,只使用单独字模。如果你仔细观察图片和汉字的字模会发现,他们两个简直就是一家人,除了尺寸不太一样,其他都是一样的,这也就是我把它们俩放在一起讲的原因,显示汉字实际上就是显示图片(其实对于除画图外的所有显示都是),现在有了前面程序的经验,可能你已经会写这个函数了,下面把我的函数贴上:
/*
功能描述:显示汉字
参数:x,y:起点坐标;no:对应数组位置;mode:1,反白显示;0,正常显示
*/
void OLED_ShowChinese(int x,int y,int no,int mode)
{
for (int i = 0; i < 2; i++) {
for(int t=0;t<16;t++)
{
if (mode == 0){
GRAM[y * 128 + x + t] = Hzk16[2*no+i][t];
}else if(mode == 1){
GRAM[y * 128 + x + t] = ~Hzk16[2*no+i][t];
}
}
y++;
}
}
/*
功能描述:显示显示BMP图片
参数:x,y:起点坐标;no:对应数组位置;mode:1,反白显示;0,正常显示
*/
void OLED_ShowBMP(int x,int y,int no,int mode)
{
int BMP_High = 32;
int BMP_Long = 32;
int temp;
if(BMP_High%8 != 0){
temp = BMP_High/8 + 1;
}else{
temp = BMP_High/8;
}
for(int i=0;i < temp;i++){
for(int t=0;t
呼~,终于写完了,但是这就可以了吗?想想我们还差什么?聪明的你应该已经注意到了,我们全程都在操作显存,但是目前显存还在STM32中没有发出去!所以我们需要一个函数发送显存(如果在每个函数后面加上发送会一卡一卡的,所以单独写出)
/*
功能描述:发送显存
*/
void OLED_GRAM_Transmit(void){
OLED_WR_Data(GRAM);
}
这下结束了……吗?没有,再想想,在C语言中,写好函数后需要干什么?没错,就是函数声明,接着只需要在头文件 oled.h 中声明一下函数,然后主程序调用就可以测试了
void OLED_WR_CMD(unsigned char cmd);
void OLED_WR_Data(unsigned char* data);
void OLED_Init(void);
void OLED_Clear(void);
void OLED_DrawPoint(int x,int y,int mode);
void OLED_DrawLine(unsigned char x1,unsigned char y1,unsigned char x2,unsigned char y2,int mode);
void OLED_ShowChar(int x,int y,char chr,int Size,int mode);
int oled_pow(int x,int y);
void OLED_ShowNum(int x,int y,int num,int size,int mode);
void OLED_ShowString(int x,int y,char *chr,int Char_Size,int mode);
void OLED_ShowChinese(int x,int y,int no,int mode);
void OLED_ShowBMP(int x,int y,int no,int mode);
void OLED_GRAM_Transmit(void);
声明完后,在main.c中调用oled.h,如下图
这里要写在USER CODE BEGIN Includes 和 USER CODE END Includes 之间,这样下次生成配置文件就不会删掉,
然后在初始化区域初始化OLED显示屏以及清屏,主循环写入测试程序:
程序都要写在用户代码区,然后编译,下载……咦,怎么报错了?
如果出现这种情况,就在魔术棒里配置一下
配置完以后,再次下载,成功下载入单片机
硬件连接:
SPI:
OLED ------- STM32
GND GND
VCC 3.3v
D0(SCLK) SCLK(PA5)
D1(SDA) MOSI(PA7)
RES RES(PA4)
DC DC(PB0)
CS CS(PB1)
IIC:
OLED ------- STM32
GND GND
VCC 3.3v
SCL SCL(PB6)
SDA SDA(PB7)
到这里,你就拥有了一个属于你自己的最基本的OLED驱动程序,当然,还有很多效果如滚屏,缩放没有做出,以后有机会我再出一篇讲讲。
既然都看到这里了,不给个关注吗?