正点原子 发表于 2022-11-11 12:40:47

《STM32MP1 M4裸机HAL库开发指南》第二十六章 OLED实验

本帖最后由 正点原子 于 2022-11-11 12:40 编辑

1)实验平台:正点原子STM32MP157开发板
2)购买链接:https://item.taobao.com/item.htm?&id=629270721801
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-318813-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子STM32MP157技术交流群:691905614




第二十六章 OLED实验

      本章我们来学习使用OLED液晶显示屏,在开发板上我们预留了OLED模块接口,需要准备一个OLED显示模块。下面我们一起来点亮OLED,并实现显示字符和图片。
      本章分为如下几个小节:
      26.1、字符编码;
      26.2、制作字模;
      26.3、OLED简介;
      26.4、OLED字符、数字显示实验;
      26.5、OLED显示图片实验;
      26.6、OLED显示动图实验;
      26.7、OLED显示中文字体;


26.1 字符编码
      计算机中存储的信息是以二进制的0或1来表示的,我们在屏幕上看到的汉字、英文和数字等信息是经过二进制转换后的结果。按照某种规则将字符存储在计算机中,例如ASCII 字符集中,字符’a’用十进制的97来表示,字符’A’用十进制的65来表示,我们称为“编码”,反之,将计算机中的二进制数据解释出来,我们称为“解码”。
      字符集是各种文字和符号的集合,常见的字符集有ASCII字符集、GB2312字符集、BIG5字符集、Unicode字符集等。下面我们会介绍ASCII字符集,关于其它字符集,如果想深入了解,大家可以参考百度百科详细说明。计算机要准确的处理各种字符集的文字,就需要进行字符编码。字符编码也称作字集码,它是一套编码规则,是信息处理的一项基本技术,其在符号集与数字系统之间建立对应关系,将符号转换为计算机可以够识别和存储的数字。
      ASCII码使用7位2进制数表示一个字符,7位2进制数可以表示出27个字符,共128个字符,其中有 96 个可打印字符,包括常用的字母、数字、标点符号等,另外还有 32 个控制字符,控制字符中,如LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BS(退格)、BEL(振铃)等,如下图中是ASCII字符代码表:
图26.1. 1 ASCII字符代码表ASCII字符代码表
      本章实验,我们会使用以下ASCII字符集来显示英文字符和数字(第一个字符是空格): !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~      
26.2 制作字模
26.2.1 像素
      图像显示是用点阵的方式来显示的,屏幕上一个个很细小的点就是像素,像素点就类似一个灯(在 OLED 显示器中,像素点就是一个小灯),通过控制这些小灯的亮和灭可以显示不同的图像。像素是图像最基本的单位,屏幕上像素点越小,像素密度越高的,图像的像素越高,显示的效果就越清晰。高分辨率的图像要比低分辨率的图像包含更多的像素,所以同样大小的图像,使用高分辨率的屏幕比低分辨率的屏幕来显示更清晰。提起显示器,我们都会听到 720P、1080P、2K 或 4K 这样的字眼,1080P 的意思就是一个屏幕上的像素数量是1920*1080 个,也就是这个屏幕一列 1080 个像素点,一共 1920 列,如下图所示:
图26.2.1. 1像素点和分辨率关系示意图
      上图中,X 轴就是1080P显示器的横轴,Y 轴就是1080P显示器的竖轴。图中的小方块就是像素点,一共有 1920*1080=2073600 个像素点。左上角的 A 点是第一个像素点,右下角的 C 点就是最后一个像素点。
26.2.2 字模
1. 字模简介
      字模就是字符或者图像在点阵上显示时对应的编码,字模也就是我们要计算机识别的符号或者图像的数据,以数字来表示,因为计算机只能识别数字0和1,所以我们要先把图像或者符号通过字模软件转化成字模。如下图,如果我们要显示一个英文字母“B”,以8*16个二进制数据位来表示,每个二进制数据位记录一个像素点的状态,则一个英文字母需要8*16/8=16个字节的二进制数据位来表示。字符的宽为1个字节,高为2个字节:
图26.2.2. 1 显示英文字符
      如果是汉字,汉字字符是英文字符的两倍,一个汉字则需要16*16/8=32个字节的二进制数据位来表示,字符的宽为2个字节,高也为2个字节,即汉字字符的宽是英文字符的宽的2倍:
图26.2.2. 2显示中文字体
2. 制作字模
      制作字模,有很多的优秀的软件,本实验我们使用“开发板光盘A-基础资料\3、软件\PCtoLCD2002完美版”字模软件来制作字模,该软件可以提供各种字符包括汉字(字体和大小都可以自己设置)点阵提取,且取模方式可以设置好几种,常用的取模方式,该软件都支持。该软件除了支持字符模式,还支持图形模式,也就是用户如果要显示一张图片的话,可以自己定义图片的大小,然后手动画图,或者可以导入一张已有的图片,该软件根据图片提取出点阵数据。下面我们来介绍此字模软件的使用方法。
(1)字符模式
      ①双击打开字模软件PCtoLCD2002完美版:
图26.2.2. 3 PCtoLCD2002完美版
      ②对于要显示字符,我们先把要显示的字符输入字符输入框中,然后点击设置选项设置取模方式:
图26.2.2. 4 输入字符
      ③然后我们设置取模方式,如下图,红框处可以配置,其它部分我们先保持默认:
图26.2.2. 5设置取模方式
      点阵格式可以选择阴码或者阳码,字模也就是像素点的数据,每一个点都需要一个bit位来存储,该位为0代表该像素点不显示,为1代表显示。字模点阵中有笔迹像素位的状态是“1”(亮),没有笔记像素位的状态是“0”(灭)的方式为阴码;字模点阵中有笔迹像素位的状态是“0”(灭),没有笔记像素位的状态是“1”(亮)的方式为阳码。本节实验中选择阴码。
      取模走向可以选择顺向或者逆向,也就是屏幕上同列中上行与下行哪个行对应生成字节的高位还是低位的问题,顺向就是屏幕下行属于高位,逆向就是屏幕上行属于低位。本节试验选择顺向。
      每行显示数据可以自行配置,点阵表示生成的字模中,每行最大可以显示多少个数据,索引则表示产生的索引中,每行显示多少个索引值。
图26.2.2. 6点阵数据
      例如配置为“点阵:12 索引:12”,以下字模每行最大为12个数据:
{0x00,0x04,0x00,0x3C,0x03,0xC4,0x1C,0x40,0x07,0x40,0x00,0xE4},
{0x00,0x1C,0x00,0x04},/*"A",0*/
      字宽和字高,字宽和字高可以自行配置,会有英文的字宽和字宽以及中文的字宽和字宽,我们前面也说过,汉字的字宽是英文字宽的两倍,如果配置汉字的字宽和字高都是16,那么英文的字宽和字高分别是8和16:
图26.2.2. 7设置字宽和字高
      自定义格式中,A51按汇编生成,C51按c格式生成,显然我们是c编程,选择C51即可。自定义格式处如果勾选后,我们可以配置生成的字模数据的格式。例如,如果生成的数据中不想要大括号“{}”,可以自定义去掉大括号:
图26.2.2. 8自定义点阵数据的前缀和后缀
      生成的点阵数据中就没有了大括号:
0x10,0x04,0x1F,0xFC,0x10,0x04,0x00,0x04,0x00,0x04,0x00,0x04,
0x00,0x0C,0x00,0x00,/*"L",1*/
      取模方式有逐列式、逐行式、列行式和行列式。不同的取模方式需要结合不同的算法。在右上角的取模说明里面有,即:从第一列开始向下每取8个点作为一个字节,如果最后不足8个点就补满8位。取模顺序是从高到低,即第一个点作为最高位。如*-------取为10000000。其实就是按如下图所示路径的这种方式:
图26.2.2. 9取模方式图解
从上到下,从左到右,高位在前。我们按这样的取模方式,然后把ASCII字符集按12*6大小、16*8和24*12大小取模出来(对应汉字大小为12*12、16*16和24*24,字符的只有汉字的一半大!),每个12*6的字符占用12个字节,每个16*8的字符占用16个字节,每个24*12的字符占用36个字节。
④点击生成字模,再保存字模,可以选择保存生成的字模:
图26.2.2. 10生成和保存字模
(2)图形模式
      PCtoLCD2002完美版字模软件支持BMP格式的图片,如果要对某一张点阵图片取模,需要将此图片转化成.bmp格式的图片才可以。
图26.2.2. 11图形模式
      如下图,选择图形模式后,点击文件打开,选择打开一张.bmp格式的文件即可:
图26.2.2. 12选择打开图形
      或者可以选择文件新建,新建一张大小规定的图像(如果屏幕很小,建议设置的长和宽要比屏幕小,这样才可以显示完全),然后手动画图:
图26.2.2. 13手动画图
26.2.3 根据取模方式控制点阵显示字符
      下面,我们先以显示简单的字符为例子,对字符显示做一个简单的讲解,为了方便讲解,此处讲解的代码先不在开发板上运行,我们先在以前学习C语言的工具(例如Visual C++ 6.0)上操作实现。此处的代码非常简单,如果没有安装此软件的也无关紧要,理解一遍代码即可。
1. 显示英文字符
(1)字模提取
      如果我们要显示一个英文字符“A”,如下图,设置字体为宋体,字宽和字高都为16,这个是汉字格式,那么对应的英文格式字长就是8,字宽就是16。如果A这个字符按阴码、顺向、逐列式、十六进制方式取模(即从下到上、从左到右、从低位到高位取模),如下图,因为点阵格式是阴码,为1的地方表示亮,为0的地方表示灭:
26.2.3. 1设置取模方式
26.2.3. 2英文字符A
26.2.3. 3字符A对应的点阵数据
      那么,取模得到的十六进制数据为:
0x00,0x04,0x00,0x3C,0x03,0xC4,0x1C,0x40,0x07,0x40,0x00,0xE4,0x00,0x1C,0x00,0x04,/*"A",0*/
      不同的取模方式得到的数据不一样,如果设置字体为宋体,字宽和字高都为16,我们的取模方式为阴码、顺向、行列式、十六进制方式提取(即从上到下、从右到左、从),则取模得到的十六进制的数据为:
26.2.3. 4设置取模方式
26.2.3. 5字符A对应的点阵数据
      取模得到的点阵数据如下:
0x00,0x00,0x00,0x10,0x10,0x18,0x28,0x28,0x24,0x3C,0x44,0x42,0x42,0xE7,0x00,0x00,/*"A",0*/
(2)程序实现
以上字模提取过程称为编码,不同的取模方式,算法会有些差别,我们使用程序将字符“A”打印出来,使用以上阴码、顺向、行列式、十六进制取模方式的编码数据,在Visual C++ 6.0下的程序如下:
1   #include <stdio.h>
2   #include <stdlib.h>
3   
4   unsigned char ch[] = {0x00,0x00,0x00,0x10,0x10,0x18,0x28,0x28,0x24,0x3C,0x44,0x42,0x42,0xE7,0x00,0x00};
5
6   void showA(){
7       int i,j;
8       unsigned char t;
9       for (i = 0; i < 16; ++i)             /* 总共16个十六进制数据 */
10      {
11          t = ch;                               /* 依次取出以上数组的数据*/
12          for (j = 0; j < 8; ++j)          /* 对于某行中的每个点*/
13          {   
14            if (0x80 & t)
15            {                  
16                  printf("*");/* 从左到右如果最左位为1,则显示*号 */
17            }
18            else
19            {
20                  printf(" ");/* 从左到右如果最左位为0,则显示空格 */
21            }
22            t <<= 1;          /* 将右边的数据往左移动*/
23          }
24          printf("\n");
25      }
26}
27   
28int main(void) {
29      showA();
30      return EXIT_SUCCESS;
31}
      我们简单分析以上代码的实现逻辑。
      第4行是讲取模得到的数据按照顺序排列到一个一维数组中;
      第9行,一维数组共16个数据,每个数据代表一行(一行有8位),使用for循环依次取出这16个数据;
      第11行,依次取出的数据存放到变量t中;
      第12行,每个数据是8bit的, j表示这8个位的第几位,有0~7个位,0表示第0位,每个位表示一个像素点。
      第14~17行,将这8位的每个位进行判断,如果某位为1,则打印*号;
      第18~21行,如果某位为0,则打印空格;
      第22行,t左移,从第0个数据开始,直到将这16个数据都判断完毕为止。
      第28~30行是固定格式,使用控制台输出。
      以上代码使用for循环嵌套,对数据逐位进行判断。
      编译无报错,执行后控制台打印出A,效果如下:
26.2.3. 6运行结果
2. 显示中文字符
      我们前面说过,一个中文字符的字宽是一个英文字符的两倍,每个汉字每行有16个像素点,即2个字节的像素点,所以每行需要对2个字节的二进制数据进行打印,参考前面英文字符的显示方法,程序中我们将汉字分为左半部分和右半部分来实现,所以要再增加一个for循环。
      (1)汉字取模
      前面我们说了,取模方式的不同,算法会有差别,前面的英文字符显示我们是采用阴码、顺向、行列式、十六进制取模方式。这里我们设置字体为宋体,字宽和字高都为16,使用阴码、顺向、逐行式、十六进制取模方式:
26.2.3. 7设置取模方式
26.2.3. 8字体效果
26.2.3. 9点阵数据排列
      取模后点阵的数据为:
0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0xFF,0xFE,0x01,0x00,0x01,0x00,
0x02,0x80,0x02,0x80,0x04,0x40,0x04,0x40,0x08,0x20,0x10,0x10,0x20,0x08,0xC0,0x06,/*"大",0*/
(2)程序实现
      取模的数据中有两行十六进制数据,先以第一行的数据为例,第0个数据是左半边,第1个数据是右半边,第2个数据是左半边,第3个数据是右半边,也就是排序中,偶数对应左半边,奇数对应右半边,依此类推,数据交替存放。程序实现的代码如下:
1   #include <stdio.h>
2   #include <stdlib.h>
3   
4   unsigned char ch[] = {
5   0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0xFF,0xFE,0x01,0x00,0x01,0x00,
6   0x02,0x80,0x02,0x80,0x04,0x40,0x04,0x40,0x08,0x20,0x10,0x10,0x20,0x08,0xC0,0x06
7   };
8   
9   void show()
10{
11      int i, j;
12      unsigned char f, s;   /*表示一个汉字每行的左半边的8像素和右半边的8像素 */
13      for (i = 0; i < 16; ++i)/* 16个组合 */
14      {   
15          f = ch;      /* 取出左半边的数据 */
16          s = ch;    /* 取出右半边的数据 */
17
18          /* 先判断中文字符的左半边的数据 */
19
20          for (j = 0; j < 8; ++j) /* 每个数据8位 */
21          {   
22             if (0x80 & f)
23            {      
24                  printf("*");/* 从左到右,如果某位为1,则显示*号 */
25            }
26            else
27            {
28                  printf(" ");/* 从左到右,如果某位为0,则显示空格 */
29            }
30            f <<= 1;          /* 将右边的数据往左移动 */
31          }
32
33          /* 再判断中文字符右半边的数据 */
34
35          for (j = 0; j < 8; ++j)/* 每个数据8位 */
36          {   
37             if (0x80 & s)
38            {      
39                  printf("*");/* 从左到右,如果某位为1,则显示*号 */
40            }
41            else
42            {
43                  printf(" ");/* 从左到右,如果某位为0,则显示空格 */
44            }
45            s <<= 1;         /* 将右边的数据往左移动 */
46          }
47          printf("\n");
48      }
49}
50   
51int main(void) {
52      show();
53      return EXIT_SUCCESS;
54}
      第12行,定义两个变量,f用于左半边的数据计算,f是偶数;s用于右半边的数据计算,s是奇数;
      第13行,一维数组中有两行,每行16个十六进制的数据;
      第15行,依次取出左半边的数据,
      第16行,依次取出右半边的数据;
      第20~31行,像前面显示英文字符那样,将汉字的左半边显示出来;
      第35行~46行,像前面显示英文字符一样,将汉字的右半边显示出来;
      编译不报错,运行程序,结果如下:
26.2.3. 10编译运行效果
3. 显示图片或者动图
      关于显示图片和动图,这里就不列出代码了,我们后面会有专门的实验。
      (1)显示一张图片
      如果只是想显示一张图片,只需要将此图片转化成.bmp格式的图片,再取模即可。要注意的是显示屏幕的分辨率,如果屏幕的分辨率比图片的分辨率要小,则屏幕上无法显示完全图片,可以修改图片的分辨率以后再进行取模。可以使用windows自带的画图工具先打开要修改的.bmp格式的文件,打开以后再手动修改像素:
26.2.3. 11小改像素
      (2)显示动图
      如果要显示动图,如果只有一张.bmp格式的图片,可以通过程序将图片移动位置,如果是.gif格式的动图文件,可以使用gif分离器软件,例如开发板光盘A-基础资料\3、软件下的GIF2BMP软件,将动图拆分成一张张的.bmp格式的文件,然后再对每张图片取模。其实动图也就是由一帧帧的图片组合成的,拆分出的每一张.bmp格式的图片都是一帧图。
      双击打开Gif分离器zhs9.exe,然后选择要分离的.gif格式文件以及分离后的文件保存路径,再点击开始分离,软件则进行分离。
26.2.3. 12分离图片
      为了方便大家,在开发板光盘A-基础资料\3、软件\GIF2BMP下有放一张动图文件,文件名为2323.gif,像素为120*60帧。如下图,将2323.gif的动图分离后,最后得到58张.bmp格式的文件,即此动图有58帧:
26.2.3. 13分离后的图片
      后面的实验中,我们会选出其中的10张图片在OLED上显示出一个动图。
26.3 OLED简介
26.3.1 OLED简介
OLED,即有机发光二极管(Organic Light-Emitting Diode),又称为有机电激光显示(Organic Electroluminesence Display, OELD)。OLED由于同时具备自发光,不需背光源、对比度高、厚度薄、视角广、反应速度快、可用于挠曲性面板、使用温度范围广、构造及制程较简单等优异之特性,被认为是下一代的平面显示器新兴应用技术。
LCD都需要背光,而OLED不需要,因为它是自发光的。这样同样的显示,OLED效果要来得好一些。以目前的技术,OLED的尺寸还难以大型化,但是分辨率确可以做到很高。在本章中,我们使用的是ALINETEK的OLED显示模块,该模块有以下特点:
1)模块有单色和双色两种可选,单色为纯蓝色,而双色则为黄蓝双色。
2)尺寸小,显示尺寸为0.96寸,而模块的尺寸仅为27mm*26mm大小。
3)高分辨率,该模块的分辨率为128*64。
4)多种接口方式,该模块提供了总共4种接口包括:6800、8080两种并行接口方式、4线SPI接口方式以及IIC接口方式(只需要2根线就可以控制OLED了!)。
5)不需要高压,直接接3.3V就可以工作了。
26.3.2 OLED的模式简介
这里要提醒大家的是,该模块不和5.0V接口兼容,所以请大家在使用的时候一定要小心,别直接接到5V的系统上去,否则可能烧坏模块。以下4种模式通过模块的BS1和BS2设置,BS1和BS2的设置与模块接口模式的关系如下表所示:
表26.3.2. 1 OLED模块接口方式设置表
表中:“1”代表接VCC,而“0”代表接GND。
该模块的外观图如下图所示:
图26.3.2. 1 ALIENTEK OLED模块外观图
ALIENTEK OLED模块默认设置是:BS1和BS2接VCC ,即使用8080并口方式,如果你想要设置为其他模式,则需要在OLED的背面,用烙铁修改BS1和BS2的设置。
模块的原理图如下图所示:
图26.3.2. 2 ALIENTEK OLED模块原理图
该模块采用8*2的2.54排针与外部连接,总共有16个管脚,在16条线中,我们只用了15条,有一个是悬空的。15条线中,电源和地线占了2条,还剩下13条信号线。
在不同模式下,我们需要的信号线数量是不同的,在8080模式下,需要全部13条,而在IIC模式下,仅需要2条线就够了!这其中有一条是共同的,那就是复位线RST(RES),RST上的低电平,将导致OLED复位,在每次初始化之前,都应该复位一下OLED模块。
ALIENTEK OLED模块的控制器是SSD1306,本章,我们将学习如何通过STM32H750来控制该模块显示字符和数字,本章的实例代码将可以支持两种方式与OLED模块连接,一种是8080的并口方式,另外一种是4线SPI方式。
1. 8080并行接口方式
首先我们介绍一下模块的8080并行接口,8080并行接口的发明者是INTEL,该总线也被广泛应用于各类液晶显示器,ALIENTEK OLED模块也提供了这种接口,使得MCU可以快速的访问OLED。ALIENTEK OLED模块的8080接口方式需要如下一些信号线:
      CS:OLED片选信号。
      WR:向OLED写入数据。
      RD:从OLED读取数据。
      D:8位双向数据线。
      RST(RES):硬复位OLED。
      DC:命令/数据标志(0,读写命令;1,读写数据)。
模块的8080并口读/写的过程为:先根据要写入/读取的数据的类型,设置DC为高(数据)/低(命令),然后拉低片选,选中SSD1306,接着我们根据是读数据,还是要写数据置RD/WR为低,然后:
在RD的上升沿, 使数据锁存到数据线(D)上;
在WR的上升沿,使数据写入到SSD1306里面;
SSD1306的8080并口写时序图如下图所示:
图26.3.2. 38080并口写时序图
SSD1306的8080并口读时序图如下图所示:
图26.3.2. 4 8080并口读时序图
SSD1306的8080接口方式下,控制脚的信号状态所对应的功能如下表所示:
表26.3.2. 2控制脚信号状态功能表
在8080方式下读数据操作的时候,我们有时候(例如读显存的时候)需要一个假读命(Dummy Read),以使得微控制器的操作频率和显存的操作频率相匹配。在读取真正的数据之前,由一个的假读的过程。这里的假读,其实就是第一个读到的字节丢弃不要,从第二个开始,才是我们真正要读的数据。
一个典型的读显存的时序图,如下图所示:
图26.3.2. 5读显存时序图
可以看到,在发送了列地址之后,开始读数据,第一个是Dummy Read,也就是假读,我们从第二个开始,才算是真正有效的数据。
2. 4线串行(SPI)方式
我们的代码同时兼容SPI方式的驱动,如果你使用的是这种驱动方式,则应该把代码中的宏OLED_MODE设置为:
#define OLED_MODE       0   /* 0: 4线串行模式 */
接下来介绍一下4线串行(SPI)方式,4线串口模式使用的信号线有如下几条:
CS:OLED片选信号。
RST(RES):硬复位OLED。
DC:命令/数据标志(0,读写命令;1,读写数据)。
SCLK:串行时钟线。在4线串行模式下,D0信号线作为串行时钟线SCLK。
SDIN:串行数据线。在4线串行模式下,D1信号线作为串行数据线SDIN。
模块的D2需要悬空,其他引脚可以接到GND。在4线串行模式下,只能往模块写数据而不能读数据。
在4线SPI模式下,每个数据长度均为8位,在SCLK的上升沿,数据从SDIN移入到SSD1306,并且是高位在前的。DC线还是用作命令/数据的标志线。在4线SPI模式下,写操作的时序如下图所示:
图26.3.2. 6 4线SPI写操作时序图
4线串行模式就为大家介绍到这里。其他还有几种模式,在SSD1306的数据手册《SSD1306-Revision 1.1 (Charge Pump)》上都有详细的介绍,如果要使用这些方式,请大家参考该手册,手册位于“开发板光盘A-基础资料\6、硬件资料\1、芯片资料\【正点原子】0.96寸OLED模块12864资料”路径下。
3. SSD1306的显存
接下来,我们介绍一下模块的显存,SSD1306的显存总共为128*64bit大小,SSD1306将这些显存分为了8页,其对应关系如下表所示:
图26.3.2. 7 SSD1306显存与屏幕对应关系表
可以看出,SSD1306的每页包含了128个字节,总共8页,这样刚好是128*64的点阵大小。当GRAM的写入模式为页模式时,需要设置低字节起始的列地址(0x00~0x0F)和高字节的起始列地址(0x10~0x1F),芯片手册中给出了写入GRAM与显示的对应关系,写入列地址在写完一字节后自动按列增长,如下图所示:
图26.3.2. 8 GRAM与显示的对应关系
因为每次写入都是按字节写入的,这就存在一个问题,如果我们使用只写方式操作模块,那么,每次要写8个点,这样,我们在画点的时候,就必须把要设置的点所在的字节的每个位都搞清楚当前的状态(0/1?),否则写入的数据就会覆盖掉之前的状态,结果就是有些不需要显示的点,显示出来了,或者该显示的没有显示了。这个问题在能读的模式下,我们可以先读出来要写入的那个字节,得到当前状况,在修改了要改写的位之后再写进GRAM,这样就不会影响到之前的状况了。但是这样需要能读GRAM,对于4线SPI模式/IIC模式,模块是不支持读的,而且读改写的方式速度也比较慢。
所以我们采用的办法是在STM32MP157的内部建立一个虚拟的OLED的GRAM(共128*8=1024个字节),在每次修改的时候,只是修改STM32MP157上的GRAM(实际上就是SRAM),在修改完了之后,一次性把STM3MP157上的GRAM写入到OLED的GRAM。当然这个方法也有坏处,一个是对于那些SRAM很小的单片机(比如51系列)不太友好,另一个是每次都写入全屏,屏幕刷新率会变低。
4. SSD1306的命令
SSD1306的命令比较多,这里我们仅介绍几个比较常用的命令,这些命令如下表所示:
图26.3.2. 9 SSD1306常用命令表
第一个命令为0X81,用于设置对比度的,这个命令包含了两个字节,第一个0X81为命令,随后发送的一个字节为要设置的对比度的值。这个值设置得越大屏幕就越亮。
第二个命令为0XAE/0XAF。0XAE为关闭显示命令;0XAF为开启显示命令。
第三个命令为0X8D,该指令也包含2个字节,第一个为命令字,第二个为设置值,第二个字节的BIT2表示电荷泵的开关状态,该位为1,则开启电荷泵,为0则关闭。在模块初始化的时候,这个必须要开启,否则是看不到屏幕显示的。
第四个命令为0XB0~B7,该命令用于设置页地址,其低三位的值对应着GRAM的页地址。
第五个指令为0X00~0X0F,该指令用于设置显示时的起始列地址低四位。
第六个指令为0X10~0X1F,该指令用于设置显示时的起始列地址高四位。
其他命令,我们就不在这里一一介绍了,大家可以参考SSD1306 datasheet的第28页。从这页开始,对SSD1306的指令有详细的介绍。
5. OLED初始化过程
最后,我们再来介绍一下OLED模块的初始化过程,SSD1306的典型初始化框图如下图所示:
图26.3.2. 10 SSD1306初始化框图
驱动IC的初始化代码,我们直接使用厂家推荐的设置就可以了,只要对细节部分进行一些修改,使其满足我们自己的要求即可,其他不需要变动。
26.4 OLED字符、数字显示实验
      本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\3、M4裸机驱动例程\ MP157-M4 HAL库V1.2\实验15-1 OLED字符、数字显示实验。
前面我们重点向大家介绍了ALIENTEK OLED模块的相关知识,接下来我们将使用这个模块来显示字符和数字。通过以上介绍,我们可以得出OLED显示需要的相关设置步骤如下:
1)设置STM32MP157与OLED模块相连接的IO
这一步,先将我们与OLED模块相连的IO口设置为输出,具体使用哪些IO口,这里需要根据连接电路以及OLED模块所设置的通讯模式来确定。这些将在硬件设计部分向大家介绍。
2)初始化OLED模块
其实这里就是上面的初始化框图的内容,通过对OLED相关寄存器的初始化,来启动OLED的显示。为后续显示字符和数字做准备。
3)通过函数将字符和数字显示到OLED模块上
这里就是通过我们设计的程序,将要显示的字符送到OLED模块就可以了,这些函数将在软件设计部分向大家介绍。
通过以上三步,我们就可以使用ALIENTEK OLED模块来显示字符和数字了。
26.4.1 硬件设计
1. 例程功能
使用8080并口模式驱动或者使用4线SPI串口模式,驱动OLED模块,不停的显示ASCII码和码值。LED0闪烁,提示程序运行。
2. 硬件资源
1)LED灯
    LED0 - PI0
2)ALIENTEK 0.96寸OLED模块,在硬件上,OLED与开发板的IO口对应关系如下:
      OLED_RS(OLED 模块上的丝印是DC)对应12C5_SCL,即:PA11;
      OLED模块上悬空/不接的引脚是DCMI_PIXCLK,即PA6;
表26.4.1. 1 IO口对应表
注意,这里的OLED_D因为不是接的连续的IO,所以后面的程序中得用拼凑的方式去组合一下,后续的程序部分会介绍到。
3. 原理图
OLED模块插在开发板底板的CAMERA接口上,接口的原理图如下,对应的IO口在前面已有详细说明了:
表26.4.1. 2 CAMERA接口部分原理图
下面我们介绍OLED模块与我们开发板的连接,开发板上有一个OLED/CAMERA的接口(P2接口)可以和ALIENTEK OLED模块直接对插(靠左插!),这些线的连接,开发板的内部已经连接好了,我们只需要将OLED模块插上去就好了,连接如下图所示:
表26.4.1. 3 OLED模块连接示意图
26.4.2 程序设计
OLED只是用到HAL库中GPIO外设的驱动代码,配置步骤在前面跑马灯实验已经介绍了。
1. 程序流程图
下面看看本实验的程序流程图:
图26.4.2. 1程序流程图
OLED驱动代码需要我们手动添加,由三个文件构成:oled.c、oled.h和oledfont.h。oledfont.h头文件存放的是ASCII字符集,oled.h存放的是引脚接口宏定义和函数声明等,oled.c则是驱动代码。
2. 添加oledfont.h文件代码
      oledfont.h文件用于存放ASICII字符集点阵数据,也就是将ASICII字符集取模后得到的数据,这里我们把 ASCII 字符集按字宽和字高为12*12、16*16和24*24的大小取模出来。这里我们以按字宽和字高为12*12为例子做讲解。设置字体为隶书,字宽和字高都为12,取模方式设置:阴码+逐列式+顺向+C51格式,最后将以下ASICII码字符拷贝到输入框,注意,第一个字符是空格,ASCII字符集: !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
图19.4.2. 2设置取模方式
      最后将数据保存在一个.tex文件中,得到的数据如下:
图19.4.2. 3取模得到的点阵数据
      其它字宽和字高为16*16和24*24的数据生成也是同样的方法。oledfont.h文件的代码我们已经在本实验的工程中给出,大家可以直接使用。
3. 添加oled.h文件代码
1   #ifndef __OLED_H
2   #define __OLED_H
3
4   #include "stdlib.h"
5   #include "./SYSTEM/sys/sys.h"
6   
7   /* OLED模式设置
8    * 0: 4线串行模式(模块的BS1,BS2均接GND)
9    * 1: 并行8080模式 (模块的BS1,BS2均接VCC)
10   */
11#define OLED_MODE       1   /* 默认使用8080并口模式 */
12
13/************************OLED SPI模式引脚定义*********************/
14/*
15   * 注意:这里仅定义了 OLED 4线SPI模式驱动时的引脚定义.
16   * 8080并口访问,由于引脚太多,就不单独定义了
17*/
18/* PE1引脚和时钟使能定义 */
19#define OLED_SPI_RST_PORT               GPIOE
20#define OLED_SPI_RST_PIN                GPIO_PIN_1
21#define OLED_SPI_RST_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)   
22/* PB7引脚和时钟使能定义 */
23#define OLED_SPI_CS_PORT                GPIOB
24#define OLED_SPI_CS_PIN               GPIO_PIN_7
25#define OLED_SPI_CS_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   
26/* PA11引脚和时钟使能定义 */
27#define OLED_SPI_RS_PORT                GPIOA
28#define OLED_SPI_RS_PIN               GPIO_PIN_11
29#define OLED_SPI_RS_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)   
30/* H9引脚和时钟使能定义 */
31#define OLED_SPI_SCLK_PORT            GPIOH
32#define OLED_SPI_SCLK_PIN               GPIO_PIN_9
33#define OLED_SPI_SCLK_CLK_ENABLE() do{ __HAL_RCC_GPIOH_CLK_ENABLE(); }while(0)   
34/* H10引脚和时钟使能定义 */
35#define OLED_SPI_SDIN_PORT            GPIOH
36#define OLED_SPI_SDIN_PIN               GPIO_PIN_10
37#define OLED_SPI_SDIN_CLK_ENABLE() do{ __HAL_RCC_GPIOH_CLK_ENABLE(); }while(0)   
38
39/********OLED SPI和8080并口模式相关端口控制函数定义*****************/
40/*
41   * 注意:OLED_RST/OLED_CS/OLED_RS,这三个是和80并口模式
42   * 共用的,即80模式也必须实现这3个函数!
43   */
44#define OLED_RST(x)   do{ x ? \
45       HAL_GPIO_WritePin(OLED_SPI_RST_PORT, OLED_SPI_RST_PIN, GPIO_PIN_SET) : \
46       HAL_GPIO_WritePin(OLED_SPI_RST_PORT, OLED_SPI_RST_PIN, GPIO_PIN_RESET); \
47                        }while(0)       /* 设置RST引脚 */
48
49#define OLED_CS(x)   do{ x ? \
50          HAL_GPIO_WritePin(OLED_SPI_CS_PORT, OLED_SPI_CS_PIN, GPIO_PIN_SET) : \
51          HAL_GPIO_WritePin(OLED_SPI_CS_PORT, OLED_SPI_CS_PIN, GPIO_PIN_RESET); \
52                        }while(0)       /* 设置CS引脚 */
53                        
54#define OLED_RS(x)   do{ x ? \
55          HAL_GPIO_WritePin(OLED_SPI_RS_PORT, OLED_SPI_RS_PIN, GPIO_PIN_SET) : \
56          HAL_GPIO_WritePin(OLED_SPI_RS_PORT, OLED_SPI_RS_PIN, GPIO_PIN_RESET); \
57                        }while(0)       /* 设置RS引脚 */
58                        
59#define OLED_SCLK(x)   do{ x ? \
60      HAL_GPIO_WritePin(OLED_SPI_SCLK_PORT, OLED_SPI_SCLK_PIN, GPIO_PIN_SET) : \
61   HAL_GPIO_WritePin(OLED_SPI_SCLK_PORT, OLED_SPI_SCLK_PIN, GPIO_PIN_RESET); \
62                        }while(0)       /* 设置SCLK引脚 */
63                        
64#define OLED_SDIN(x)   do{ x ? \
65   HAL_GPIO_WritePin(OLED_SPI_SDIN_PORT, OLED_SPI_SDIN_PIN, GPIO_PIN_SET) : \
66   HAL_GPIO_WritePin(OLED_SPI_SDIN_PORT, OLED_SPI_SDIN_PIN, GPIO_PIN_RESET); \
67                        }while(0)       /* 设置SDIN引脚 */
68
69/* OLED 80并口模式WR,RD端口控制函数 定义 */
70#define OLED_WR(x)   do{ x ? \
71          HAL_GPIO_WritePin(GPIOH, GPIO_PIN_8, GPIO_PIN_SET) : \
72          HAL_GPIO_WritePin(GPIOH, GPIO_PIN_8, GPIO_PIN_RESET); \
73                        }while(0)       /* 设置WR引脚 */
74
75#define OLED_RD(x)   do{ x ? \
76          HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_SET) : \
77          HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_RESET); \
78                        }while(0)       /* 设置RD引脚 */
79
80/***************************命令/数据 定义*************************/                                          
81
82#define OLED_CMD      0       /* 写命令 */
83#define OLED_DATA       1       /* 写数据 */
84
85/*******************************函数声明**************************/
86      
87static void oled_wr_byte(uint8_t data, uint8_t cmd);    /* 写一个字节到OLED */
88static uint32_t oled_pow(uint8_t m, uint8_t n);         /* OLED求平方函数 */
89void oled_init(void);         /* OLED初始化 */
90void oled_clear(void);          /* OLED清屏 */
91void oled_display_on(void);   /* 开启OLED显示 */
92void oled_display_off(void);    /* 关闭OLED显示 */
93void oled_refresh_gram(void);   /* 更新显存到OLED */
94/* OLED画点函数 */
95void oled_draw_point(uint8_t x, uint8_t y, uint8_t dot);
96/* OLED区域填充函数 */                                                   
97void oled_fill(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2, uint8_t dot);
98/* OLED显示字符函数 */      
99void oled_show_char(uint8_t x, uint8_t y, uint8_t chr, uint8_t size, uint8_t mode);
100 /* OLED显示数字函数 */
101 void oled_show_num(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size);
102 /* OLED显示字符串函数 */
103 void oled_show_string(uint8_t x, uint8_t y, const char *p, uint8_t size);         
104
105 #endif
      第19~37行,为了和8080并口方式区别开来,重新定义了SPI模式的引脚;
      第44~67行,SPI和8080并口模式相关端口控制函数的定义;
      第70~78行,8080并口模式相关端口控制函数定义;
      第82~83,写命令和写数据宏定义;
      第87~103行,一些函数的声明。
4. 添加oled.c文件代码
      下面我们来重点分析oled.c文件的代码,此文件中有很多函数,我们选择分析几个重要的函数。所有函数的代码清查看工程文件中的oled.c文件。
(1)oled_init初始化函数
1   /**
2    * @brief       初始化OLED(SSD1306)
3    * @param       无
4    * @retval      无
5    */
6   void oled_init(void)
7   {
8       GPIO_InitTypeDefgpio_init_struct;
9   /* 使能GPIOA、GPIOB、GPIOC、GPIOD、GPIOE、GPIOH和GPIOI时钟 */
10      __HAL_RCC_GPIOA_CLK_ENABLE();
11      __HAL_RCC_GPIOB_CLK_ENABLE();
12      __HAL_RCC_GPIOC_CLK_ENABLE();
13      __HAL_RCC_GPIOD_CLK_ENABLE();
14      __HAL_RCC_GPIOE_CLK_ENABLE();
15      __HAL_RCC_GPIOH_CLK_ENABLE();
16      __HAL_RCC_GPIOI_CLK_ENABLE();
17
18#if OLED_MODE==1                                 /* 使用8080并口模式 */
19      /* 设置PA6,11,12*/
20      gpio_init_struct.Pin=GPIO_PIN_6|GPIO_PIN_11|GPIO_PIN_12;   
21      gpio_init_struct.Mode=GPIO_MODE_OUTPUT_PP;                  /* 推挽输出 */
22      gpio_init_struct.Pull=GPIO_PULLUP;                        /* 上拉 */
23      gpio_init_struct.Speed=GPIO_SPEED_FREQ_VERY_HIGH;   /* 高速 */
24      HAL_GPIO_Init(GPIOA,&gpio_init_struct);                        /* 初始化GPIOA */
25
26      /* 设置PB7,8 */
27      gpio_init_struct.Pin=GPIO_PIN_7|GPIO_PIN_8;
28      HAL_GPIO_Init(GPIOB,&gpio_init_struct);             /* 初始化GPIOB */
29
30      /* 设置PE1,6*/
31      gpio_init_struct.Pin=GPIO_PIN_1 | GPIO_PIN_6;
32      HAL_GPIO_Init(GPIOE,&gpio_init_struct);            /* 初始化GPIOE */
33
34      /* 设置PH8,9,10,11,12,14*/
35      gpio_init_struct.Pin=GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | \
36            GPIO_PIN_11 |GPIO_PIN_12 | GPIO_PIN_14;
37      HAL_GPIO_Init(GPIOH,&gpio_init_struct);         /* 初始化GPIOH */
38
39      /* 设置PHI4 */
40      gpio_init_struct.Pin=GPIO_PIN_4;
41      HAL_GPIO_Init(GPIOI,&gpio_init_struct);          /* 初始化GPIOI */
42
43      OLED_WR(1);/* WR电平拉高 */
44      OLED_RD(1);/* RD电平拉高 */
45
46#else/* 使用4线SPI 串口模式 */
47      gpio_init_struct.Pin=OLED_SPI_RST_PIN;
48      gpio_init_struct.Mode=GPIO_MODE_OUTPUT_PP;                  /* 推挽输出 */
49      gpio_init_struct.Pull=GPIO_PULLUP;                        /* 上拉 */
50      gpio_init_struct.Speed=GPIO_SPEED_FREQ_VERY_HIGH;   /* 高速 */
51      /* RST引脚模式设置 */
52      HAL_GPIO_Init(OLED_SPI_RST_PORT,&gpio_init_struct);
53
54      gpio_init_struct.Pin=OLED_SPI_CS_PIN;
55      /* CS引脚模式设置 */
56      HAL_GPIO_Init(OLED_SPI_CS_PORT,&gpio_init_struct);
57
58      gpio_init_struct.Pin=OLED_SPI_RS_PIN;
59      /* RS引脚模式设置 */
60      HAL_GPIO_Init(OLED_SPI_RS_PORT,&gpio_init_struct);
61
62      gpio_init_struct.Pin=OLED_SPI_SCLK_PIN;
63      /* SCLK引脚模式设置 */
64      HAL_GPIO_Init(OLED_SPI_SCLK_PORT,&gpio_init_struct);
65
66      gpio_init_struct.Pin=OLED_SPI_SDIN_PIN;
67      /* SDIN引脚模式设置 */
68      HAL_GPIO_Init(OLED_SPI_SDIN_PORT,&gpio_init_struct);
69
70      OLED_SDIN(1);   /* 设置SDIN引脚 */
71      OLED_SCLK(1);   /* 设置SCLK引脚 */
72#endif
73      OLED_CS(1);
74      OLED_RS(1);
75
76      OLED_RST(0);
77      delay_ms(100);
78      OLED_RST(1);
79
80      oled_wr_byte(0xAE, OLED_CMD);   /* 关闭显示 */
81      oled_wr_byte(0xD5, OLED_CMD);   /* 设置时钟分频因子,震荡频率 */
82      oled_wr_byte(80, OLED_CMD);   /* ,分频因子;,震荡频率 */
83      oled_wr_byte(0xA8, OLED_CMD);   /* 设置驱动路数 */
84      oled_wr_byte(0X3F, OLED_CMD);   /* 默认0X3F(1/64) */
85      oled_wr_byte(0xD3, OLED_CMD);   /* 设置显示偏移 */
86      oled_wr_byte(0X00, OLED_CMD);   /* 默认为0 */
87
88      oled_wr_byte(0x40, OLED_CMD);   /* 设置显示开始行 ,行数. */
89
90      oled_wr_byte(0x8D, OLED_CMD);   /* 电荷泵设置 */
91      oled_wr_byte(0x14, OLED_CMD);   /* bit2,开启/关闭 */
92      oled_wr_byte(0x20, OLED_CMD);   /* 设置内存地址模式 */
93      /* ,00,列地址模式;01,行地址模式;10,页地址模式;默认10; */
94      oled_wr_byte(0x02, OLED_CMD);
95      /* 段重定义设置,bit0:0,0->0;1,0->127; */
96      oled_wr_byte(0xA1, OLED_CMD);
97      /* 设置COM扫描方向;bit3:0,普通模式;1,重定义模式 COM->COM0;N:驱动路数 */
98      oled_wr_byte(0xC0, OLED_CMD);   
99      oled_wr_byte(0xDA, OLED_CMD);   /* 设置COM硬件引脚配置 */
100   oled_wr_byte(0x12, OLED_CMD);   /* 配置 */
101
102   oled_wr_byte(0x81, OLED_CMD);   /* 对比度设置 */
103   oled_wr_byte(0xEF, OLED_CMD);   /* 1~255;默认0X7F (亮度设置,越大越亮) */
104   oled_wr_byte(0xD9, OLED_CMD);   /* 设置预充电周期 */
105   oled_wr_byte(0xf1, OLED_CMD);   /* ,PHASE 1;,PHASE 2; */
106   oled_wr_byte(0xDB, OLED_CMD);   /* 设置VCOMH 电压倍率 */
107   /* 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc; */
108   oled_wr_byte(0x30, OLED_CMD);   
109   /* 全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏) */
110   oled_wr_byte(0xA4, OLED_CMD);
111   /* 设置显示方式;bit0:1,反相显示;0,正常显示 */
112   oled_wr_byte(0xA6, OLED_CMD);   
113   oled_wr_byte(0xAF, OLED_CMD);   /* 开启显示 */
114   oled_clear();
115 }
该函数的结构比较简单,开始是对GPIO口的初始化,这里我们用了宏定义OLED_MODE来决定要设置的IO口,默认使用8080并口模式,后面的就是一些初始化序列了,我们按照厂家提供的资料来做就可以。值得注意一点的是,因为OLED是无背光的,在初始化之后,我们把显存都清空了,所以我们在屏幕上是看不到任何内容的,就像没通电一样,不要以为这就是初始化失败,要写入数据模块才会显示的。
(2)oled_data_out和oled_wr_byte函数
      oled_data_out函数是通过拼凑的方法向OLED输出一个8位数据。因为数据引脚使用到的IO口并不是连续的IO口,一个8位的数据要通过这8个不连续的IO口发送出去的话,我们使用拼凑的方式,即:
      对于8080并口方式:
表26.4.2. 18位数据线接口方式
      对于4线SPI串口方式,D0信号线作为串行时钟线SCLK,D1信号线作为串行数据线SDIN,数据通过D1一位一位地发送出去。
1   #if OLED_MODE == 1    /* 使用8080并口驱动OLED */
2   /**
3    * @brief       通过拼凑的方法向OLED输出一个8位数据
4    * @param       data: 要输出的数据
5    * @retval      无
6    */
7   static void oled_data_out(uint8_t data)
8   {
9       uint16_t dat = data & 0X0F;
10      
11      GPIOH->ODR &= ~(0XF << 9);    /* 清空PH的第12~9位 */
12      GPIOH->ODR |= dat << 9;      /* 将D分配给PH */
13      
14      GPIOH->ODR &= ~(0X1 << 14);   /* 清空PH的第14位 */
15      GPIOH->ODR |= ((data >> 4) & 0x01) << 14;         /* 将D4给PH14 */
16      
17      GPIOI->ODR &= ~(0X1 << 4);                                  /* 清空PI4 */
18      GPIOI->ODR |= ((data >> 5) & 0x01) << 4;            /* 将D5给PI4 */
19      
20      GPIOB->ODR &= ~(0X1 << 8);                                  /* 清空PB8 */
21      GPIOB->ODR |= ((data >> 6) & 0x01) << 8;
22
23      GPIOE->ODR &= ~(0X1 << 6);                                  /* 清空PE6 */
24      GPIOE->ODR |= ((data >> 7) & 0x01) << 6;            /* 将D7给PE6 */
25#if 0
26      GPIOC->ODR&=~(0XF<<6);                      //清空6~9
27      GPIOC->ODR|=dat<<6;                         //D-->PC
28      
29      GPIOC->ODR&=~(0X1<<11);                     //清空11
30      GPIOC->ODR|=((data>>4)&0x01)<<11;
31      
32      GPIOD->ODR&=~(0X1<<3);                      //清空3
33      GPIOD->ODR|=((data>>5)&0x01)<<3;
34      
35      GPIOB->ODR&=~(0X3<<8);                      //清空8,9
36      GPIOB->ODR|=((data>>6)&0x01)<<8;
37      GPIOB->ODR|=((data>>7)&0x01)<<9;
38#endif
39      
40}
41/**
42   * @brief       向OLED写入一个字节
43   * @param       data: 要输出的数据
44   * @param       cmd: 数据/命令标志 0,表示命令;1,表示数据;
45   * @retval      无
46   */
47static void oled_wr_byte(uint8_t data, uint8_t cmd)
48{
49      oled_data_out(data);/* 向OLED输出一个8位字节 */
50      OLED_RS(cmd);   /* cmd为0,表示命令;cmd为1,表示数据 */
51      OLED_CS(0);       /* 片选引脚拉低,选中SSD1306 */
52      OLED_WR(0);       /* WR先拉低 */
53      OLED_WR(1);       /* WR再拉高,变成上升沿了,使字节写入到数据线D上 */
54      OLED_CS(1);       /* 片选引脚拉高,关闭片选 */
55      OLED_RS(1);       /* 即DC设置为1,DC电平恢复至初始态 */
56}
57#else   /* 使用SPI驱动OLED */
58/**
59   * @brief       向OLED写入一个字节
60   * @param       data: 要输出的数据
61   * @param       cmd: 数据/命令标志 0,表示命令;1,表示数据;
62   * @retval      无
63   */
64static void oled_wr_byte(uint8_t data, uint8_t cmd)
65{
66      uint8_t i;
67      OLED_RS(cmd);   /* cmd为0,表示命令;cmd为1,表示数据 */
68      OLED_CS(0);   /* 片选引脚拉低,选中SSD1306 */
69
70      for (i = 0; i < 8; i++)/* SPI是串行,一位一位地传输 */
71      {
72          OLED_SCLK(0);      /* SCLK为低电平,低电平开始采样数据 */
73          if (data & 0x80)   /* 高位在前 */
74          {
75            OLED_SDIN(1);   /* 写1 */
76          }
77          else
78          {
79            OLED_SDIN(0);   /* 写0 */
80          }
81          OLED_SCLK(1);       /* SCLK为高电平 */
82          data <<= 1;         /* 左移 */
83      }
84
85      OLED_CS(1);             /* 关闭片选 */
86      OLED_RS(1);             /* DC电平恢复至初始态 */
87}
88#endif
      oled_wr_byte函数的代码我们已经在上面列出了,通过此函数向OLED写入一个字节。8080并口方式和SPI方式接线不同,通信方式也会不同,所以这部分分为两种情况,如果使用SPI方式,只需要将宏OLED_MODE定义为0即可。此函数的代码是根据前文讲解的通信时序图编写的代码。我们来分析这段代码。
      对于8080并口方式:
      第49行,向OLED输出一个8位字节;
      第50行,OLED_RS(cmd)中的cmd可选1或0,选1表示对数据进行操作,选0表示对命令进行操作;
      第51行,片选引脚拉低,选中OLED芯片SSD1306;
      第52和53行,先将WR拉低再拉高,这样就变成一个上升沿了,前面我们分析过,将数据写入SSD1306时,WR引脚的信号状态处于是上升沿:
表26.4.2. 2功能引脚的状态
      第54行,将片选拉高,关闭片选引脚;
      第55行,设置DC引脚为高电平,回复为初始态。初始态下DC是高电平。
      对于4线SPI方式:
      第68行,将片选拉低,选中OLED芯片;
      第70~83行,SPI通信方式是一位一位地进行传输,所以分8次传输。SPI通信是在SCLK未低电平的时候采样数据,为高电平的时候停止采样;
      第85和86行,关闭片选,并将DC恢复至初始状态。
(3)oled_refresh_gram和oled_clear函数
oled_refresh_gram函数是把我们在程序中定义的二维数组g_oled_gram的值一次性刷新到OLED的显存GRAM中,该数组值与OLED显存GRAM值一一对应。在操作的时候我们只需要先修改该数组的值,然后再通过调用oled_refresh_gram函数把数组的值一次性刷新到OLED 的GRAM上即可。
      oled_clear函数是清屏函数,g_oled_gram的值为0x00,即屏幕的像素点是黑的,没有点亮。
1   /*
2    * OLED的显存
3    * 每个字节表示8个像素, 128,表示有128列, 8表示有64行, 高位表示第行数.
4    * 比如:g_oled_gram,包含了第一列,第1~8行的数据.
5    * g_oled_gram.7,即表示坐标(0,0)
6    * 类似的: g_oled_gram.6,表示坐标(1,1), g_oled_gram.5,
7    * 表示坐标(10,10),
8    *
9    * 存放格式如下(高位表示低行数).
10   * 0 1 2 3 ... 127
11   * 0 1 2 3 ... 127
12   * 0 1 2 3 ... 127
13   * 0 1 2 3 ... 127
14   * 0 1 2 3 ... 127
15   * 0 1 2 3 ... 127
16   * 0 1 2 3 ... 127
17   * 0 1 2 3 ... 127
18   */
19static uint8_t g_oled_gram;
20
21/**
22   * @brief       更新显存到OLED
23   * @param       无
24   * @retval      无
25   */
26void oled_refresh_gram(void)
27{
28      uint8_t i, n;
29
30      for (i = 0; i < 8; i++)
31      {
32          oled_wr_byte (0xb0 + i, OLED_CMD); /* 设置页地址(0~7) */
33          oled_wr_byte (0x00, OLED_CMD);   /* 设置显示位置—列低地址 */
34          oled_wr_byte (0x10, OLED_CMD);   /* 设置显示位置—列高地址 */
35
36          for (n = 0; n < 128; n++)
37          {
38            oled_wr_byte(g_oled_gram, OLED_DATA);
39          }
40      }
41}
42      /**
43 * @brief       清屏函数,清完屏,整个屏幕是黑色的!和没点亮一样!!!
44 * @param       无
45 * @retval      无
46 */
47      void oled_clear(void)
48      {
49    uint8_t i, n;
50
51 for (i = 0; i < 8; i++)for (n = 0; n < 128; n++)g_oled_gram = 0X00;
52
53    oled_refresh_gram();    /* 更新显示 */
54      }
      g_oled_gram 二维数组中的128代表列数(x坐标),而8代表的是页,每页又包含8行,总共64行(y坐标),从高到低对应行数从小到大,如下图所示:
图26.4.2. 2 OLED_GRAM和OLED屏坐标对应关系
      上表中G代表OLED_GRAM,G就表示OLED_GRAM。比如,我们要在x=3,y=9这个点写入1,则可以用这个句子实现:
                                                OLED_GRAM|=1<<6;
一个通用的在点(x,y)置1表达式为:
                                                OLED_GRAM|=1<<(7-y%8);
其中x的范围为:0~127;y的范围为:0~63。
(4)oled_draw_point画点函数
下面我们介绍重要的画点函数,函数代码如下:
1   /**
2    * @brief       OLED画点
3    * @param       x: 0~127
4    * @param       y: 0~63
5    * @param       dot: 1 填充 0,清空
6    * @retval      无
7    */
8   void oled_draw_point(uint8_t x, uint8_t y, uint8_t dot)
9   {
10      uint8_t pos, bx, temp = 0;
11
12      if (x > 127 || y > 63) return;/* 超出范围了. */
13
14    pos = 7 - y / 8;/* 计算GRAM里面的y坐标所在的字节, 每个字节可以存储8个行坐标 */
15
16   bx = y % 8; /* 取余数,方便计算y在对应字节里面的位置,及行(y)位置 */
17    temp = 1 << (7 - bx);   /* 高位表示低行号, 得到y对应的bit位置,将该bit先置1 */
18
19      if (dot)    /* 画实心点 */
20      {
21          g_oled_gram |= temp;
22      }
23      else      /* 画空点,即不显示 */
24      {
25          g_oled_gram &= ~temp;
26      }
27}
该函数有3个形参,前两个是横纵坐标,第三个dot为要写入1还是0。该函数实现了我们在OLED模块上任意位置画点的功能。
第12行,OLED屏幕的分辨率是128*64,x和y的坐标从0开始,x最大为127,y最大为63,如果超出范围,则屏幕上无法显示;
第14行,pos代表的是第几页,这行代码是为了确定页数。我们面对屏幕,由屏幕从上往下看,最上是第7页,最下是第0页;
第16行,bx = y % 8是为了确定移位的个数;
第17行,temp是表示该页的哪一列;
如下图是本实验程序中硬件的页排序和程序中的页分布关系:
表26.4.2. 3硬件的页排序示意图
      上图是屏幕硬件上GRAM里页的排布的关系,下图是程序中的页排布关系,正因为这种关系,所以以上的画点程序作了特殊处理。
图26.4.2. 3 程序中的页排布示意图
(5)OLED显示字符、数字函数
1   /**
2    * @brief       在指定位置显示一个字符,包括部分字符
3    * @param       x   : 0~127
4    * @param       y   : 0~63
5    * @param       size: 选择字体 12/16/24
6    * @param       mode: 0,反白显示;1,正常显示
7    * @retval      无
8    */
9   void oled_show_char(uint8_t x, uint8_t y, uint8_t chr, uint8_t size, uint8_t mode)
10{
11      uint8_t temp, t, t1;
12      uint8_t y0 = y;
13      uint8_t *pfont = 0;
14      /* 得到字体一个字符对应点阵集所占的字节数 */
15      uint8_t csize = (size / 8 + ((size % 8) ? 1 : 0)) * (size / 2);
16      chr = chr - ' ';/* 得到偏移后的值,因为字库是从空格开始存储的,第一个字符是空格 */
17
18      if (size == 12)         /* 调用1206字体 */
19      {
20          pfont = (uint8_t *)oled_asc2_1206;
21      }
22      else if (size == 16)    /* 调用1608字体 */
23      {
24          pfont = (uint8_t *)oled_asc2_1608;
25      }
26      else if (size == 24)    /* 调用2412字体 */
27      {
28          pfont = (uint8_t *)oled_asc2_2412;
29      }
30      else                  /* 没有的字库 */
31      {
32          return;
33      }
34
35      for (t = 0; t < csize; t++)
36      {
37          temp = pfont;
38          for (t1 = 0; t1 < 8; t1++)
39          {
40            if (temp & 0x80)oled_draw_point(x, y, mode);
41            else oled_draw_point(x, y, !mode);
42
43            temp <<= 1;
44            y++;
45
46            if ((y - y0) == size)
47            {
48                  y = y0;
49                  x++;
50                  break;
51            }
52          }
53      }
54}
oled_show_char函数为字符以及字符串显示的核心部分,函数中chr = chr - ' ';这句是要得到在字符点阵数据里面的实际地址,因为我们的取模是从空格键开始的,例如oled_asc2_1206 ,代表的是空格符开始的点阵码。在接下来的代码,我们也是按照从上到小(先y++),从左到右(再x++)的取模方式来编写的,先得到最高位,然后判断是写1还是0,画点;接着读第二位,如此循环,直到一个字符的点阵全部取完为止。这其中涉及到列地址和行地址的自增,根据取模方式来理解,就不难了。
oled.c的内容比较多,其他的函数请大家自行理解,这里就不一一介绍了。下面开始main.c文件的介绍。
5. 修改main.c文件
main.c文件代码如下:
int main(void)
{
    uint8_t t = 0;
   
    HAL_Init();                           /* 初始化HAL库 */
    /* 初始化M4内核时钟,209M */
    if(IS_ENGINEERING_BOOT_MODE())
    {
      sys_stm32_clock_init(34, 2, 2, 17, 6826);
    }
    delay_init(209);                        /* 延时初始化 */
    led_init();                     /* 初始化LED */
    oled_init();                  /* 初始化OLED */
    oled_show_string(0, 0, "ALIENTEK", 24);
    oled_show_string(0, 24, "0.96' OLED TEST", 16);
    oled_show_string(0, 40, "ATOM 2020/5/11", 12);
    oled_show_string(0, 52, "ASCII:", 12);
    oled_show_string(64, 52, "CODE:", 12);
    oled_refresh_gram();          /* 更新显示到OLED */

    t = ' ';
    while (1)
    {
      oled_show_char(36, 52, t, 12, 1); /* 显示ASCII字符 */
      oled_show_num(94, 52, t, 3, 12);/*显示ASCII字符的码值 */
      oled_refresh_gram();                /*更新显示到OLED */
      t++;
      if (t > '~')t = ' ';
      delay_ms(500);
      LED0_TOGGLE();                     /* LED0闪烁 */
    }
}
Main.c主要功能就是在OLED上显示一些实验信息字符,然后开始从空格键开始不停的循环显示ASCII字符集,并显示该字符的ASCII值。最后LED0闪烁提示程序正在运行。
26.4.3 编译和测试
      下载代码后,LED0不停的闪烁,提示程序已经在运行了。同时OLED模块显示ASCII字符集等信息,如下图所示(本实验使用的OLED模块是双色的,显示的颜色有两种):
图26.4.3. 1 OLED显示效果
OLED显示了三种尺寸的字符:24*12(ALIENTEK)、16*8(0.96’ OLED TEST)和12*6(剩下的内容)。说明我们的实验是成功的,实现了三种不同尺寸ASCII字符的显示,在最后一行不停的显示ASCII字符以及其码值。
通过这一章的学习,我们学会了ALIENTEK OLED模块的使用,在调试代码的时候,又多了一种显示信息的途径,在以后的程序编写中,大家可以好好利用。
26.5 OLED显示图片实验
      本实验是在上一章实验的基础上修改的,注意main.c文件修改的地方。配置好的实验工程已经放到了开发板光盘中,由于几个实验共用一个工程,所以要测试哪个实验,就在min.c文件中把其它实验的代码注释掉即可。路径为:开发板光盘A-基础资料\1、程序源码\3、M4裸机驱动例程\ MP157-M4 HAL库V1.2\实验15-2 OLED显示图片实验。
26.5.1 取模
1. 选择.bmp格式图片
      我们在前面实验的基础上,实现此实验,本节实验需要借助前面实验的画点函数。我们选择前面使用gif分离器分离出的.bmp图片中的某一张来操作本实验,例如使用第一张图片,此图片像素是120*60::
图26.5.1. 1选择一张bmp格式图片
2. 取模
      我们对上面选择的第一章图片取模如下,取模方式设置:阴码+逐列式+顺向+C51格式+十六进制,字宽和字高均为12:
图26.5.1. 2图片取模
26.5.2 添加用户代码
1. 添加logo.h文件代码
      在工程的Drivers\BSP\OLED目录下新建一个logo.h文件,将上面取模的数据拷贝到logo.h文件的一维数组Image[]中,此一维数组总共有960个十六进制的数据,也就是此.bmp的图片有最多960个像素点。
#ifndef _LOGO_H_
#define _LOGO_H_

uint8_t Image[]={//960
         0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
         0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
         0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
         0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
         /***************省略部分数据 *************/
         0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
         0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
         0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
         0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};
#endif
2. 修改oled.c
      在oled.h文件中添加如下代码:
void OLED_Show(uint8_t x,uint8_t y,uint16_t bytenumber, uint8_t height, uint8_t* Image, uint8_t mode);
      在前面实验的oled.c文件中添加如下代码:
/**
* @brief       显示图片
* @param       x,y : 起始坐标
*            number: 图片像素点的个数
*            height:图片像素点的高度,即每一列总共有几行
*             Image : 包含图像数据的一维数组首地址
*            mode:1点亮数据点,0不点亮数据点
* @retval      无
*/
void OLED_Show(uint8_t x,uint8_t y,uint16_t number, uint8_t height, uint8_t* Image, uint8_t mode)
{
    uint16_t i=0;
    uint8_t y0=y, temp=0, j=0;

    for(i=0; i<number; i++)               /* number个像素点就要循环number次 */
    {
      temp=Image;
      for(j=0; j<8; j++)               /* 对于某行中的每个点进行判断*/
      {
            if(temp&0x80)                         /* 如果像素点是1,则点亮数据点 */
                oled_draw_point(x,y,mode);
            else                                        /* 如果像素点是0,则不点亮数据点 */
                oled_draw_point(x,y,!mode);
            temp <<= 1;                         /* 将右边的数据往左移动*/
            y++;      
            if(y-y0 == height)
            {
                y=y0;
                x++;
                break;
            }
      }
    }
}
3. 修改main.c
      在main.c文件中添加如下代码:
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/BEEP/beep.h"
#include "./BSP/KEY/key.h"
#include "./BSP/OLED/oled.h"
#include "./BSP/OLED/logo.h"
/**
* @brief       主函数
* @param       无
* @retval      无
*/
int main(void)
{   
    HAL_Init();                     /* 初始化HAL库 */
    /* 初始化M4内核时钟,209M */
    if(IS_ENGINEERING_BOOT_MODE())
    {
      sys_stm32_clock_init(34, 2, 2, 17, 6826);
    }
    delay_init(209);      /* 延时初始化 */
    led_init();            /* 初始化LED */
    oled_init();             /* 初始化OLED */
    while (1)
    {
            OLED_Show(0,0,960, 60, Image, 1); /* 显示一张图片 */
            oled_refresh_gram();                                 /*更新显存到OLED */
            delay_ms(500);                                                   /* 延时500ms */
            LED0_TOGGLE();                  /* LED0闪烁 */
    }
}
      注意OLED_Show(0,0,960, 60, Image, 1)这一行,x和y坐标均是0,像素点个数是960(前面取模后我们计算出来.bmp的图片有最多960个像素点),像素点的高度是60,因为我们使用的图片像素是120*60的,高度则为60。
26.5.3. 编译运行
      编译无报错,运行后的效果如下图所示:
图26.5.3. 1编译运行结果
26.6 OLED显示动图实验
      本实验是在上一章实验的基础上修改的,注意main.c文件修改的地方。配置好的实验工程已经放到了开发板光盘中,由于几个实验共用一个工程,所以要测试哪个实验,就在min.c文件中把其它实验的代码注释掉即可。路径为:开发板光盘A-基础资料\1、程序源码\3、M4裸机驱动例程\ MP157-M4 HAL库V1.2\实验15-3 OLED显示动图实验。
26.6.1 取模
1. 选择.bmp格式文件
      每一张动图是由一帧帧的图片组合成的,前面我们使用gif图片分离器分离出了58张的.bmp格式的文件,我们可以考虑使用其中几张的图片来生成点阵数据,再将每张图片显示出来,显示的每张图片延时400ms的时间,这样看上去就像是一个动图了。如下,我们选择这58张图片中的10张(0/2/6/13/16/22/34/43/48/57):
图26.6.1. 1选取图片
2. 取模
      我们依次对这10张图片进行取模,保存每张图片取模后的数据,取模方式和前面的一样,即阴码+逐列式+顺向+C51格式+十六进制,字宽和字高均为12:
图26.6.1. 2取模配置
      观察取模后的数据,每张图片的数据的个数都是960个(即每张图片的像素点个数是960)。
26.6.2 添加用户代码
1. 修改logo.h
      我们可以直接在上一章节的logo.h文件中创建10个一维数组,每个数组存放一张图片的数据。由于代码太多,我们这里只截图一部分,具体可以参考本实验的工程代码:
图26.6.2. 1 logo.h中添加的代码
2. 修改main.c
      然后在main.c文件中直接添加如下代码:
OLED_Show(0,0,960, 60, Image1, 1);
oled_refresh_gram();
HAL_Delay(400);
OLED_Show(0,0,960, 60, Image2, 1);
oled_refresh_gram();
HAL_Delay(400);
OLED_Show(0,0,960, 60, Image3, 1);
oled_refresh_gram();
HAL_Delay(400);
OLED_Show(0,0,960, 60, Image4, 1);
oled_refresh_gram();
HAL_Delay(400);
OLED_Show(0,0,960, 60, Image5, 1);
oled_refresh_gram();
HAL_Delay(400);
OLED_Show(0,0,960, 60, Image6, 1);
oled_refresh_gram();
HAL_Delay(400);
OLED_Show(0,0,960, 60, Image7, 1);
oled_refresh_gram();
HAL_Delay(400);
OLED_Show(0,0,960, 60, Image8, 1);
oled_refresh_gram();
HAL_Delay(400);
OLED_Show(0,0,960, 60, Image9, 1);
oled_refresh_gram();
HAL_Delay(400);
OLED_Show(0,0,960, 60, Image10, 1);
oled_refresh_gram();
HAL_Delay(400);
      以上是创建了10个一维的数组,然后分10次进行显示,每次显示一张图片,每张图片显示400ms。编译后测试,就可以看到一个动图了。
      以上代码比较繁琐,下面我们将以上代码进行改进:
3. 修改image.h
      下面我们在工程的Drivers\BSP\OLED下创建一个image.h头文件,在此头文件中创建一个二维数组,数组中的常量表达式1是图片的索引值,共有10张图片,所以值为10,每张图片的像素点个数为960,所以数组中的常量表达式2的值为960:
uint8_t BMP={
    {//IMG00000.bmp
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
   /***********省略部分代码**********************************/
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    },
    {//IMG00002.bmp
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    /***********省略部分代码**********************************/
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    },
    .
    .
    .
   {//IMG00057.bmp
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
   /***********省略部分代码**********************************/
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
   0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
    },
};
4. 修改oled.c
      在oled.c文件中添加代码如下:
#include "./BSP/OLED/image.h"
/**
* @brief   显示图片
* @param   x,y : 起始坐标
*            px,py: 图片的宽度和高度,本实验所使用的图片
*            宽是120像素,高是60像素
*            index : 图片的索引,为0~9,共10张图
*            mode:1点亮数据点,0不点亮数据点
* @retval    无
*/
void OLED_ShowBMP(uint8_t x, uint8_t y, uint8_t px, uint8_t py, uint8_t index, uint8_t mode)
{
    uint8_t temp,t1;
    uint16_t j,i;
    uint8_t y0=y;

    i=960;
   // i = (px/2)*(py/4); /* 或者使用此函数 */

    for(j = 0; j < i;j++)
    {
      temp = BMP; /* 调用图片 */
      for(t1=0;t1<8;t1++)/* 对于某行中的每个点进行判断*/
      {
            if(temp&0x80)/* 如果像素点是1,则点亮数据点 */
                oled_draw_point(x,y,mode);
            else      /* 如果像素点是0,则不点亮数据点 */
                oled_draw_point(x,y,!mode);
            temp<<= 1;/* 将右边的数据往左移动*/
            y++;
            if((y-y0) == py)
            {
                y=y0;
                x++;
                break;
            }
      }
    }
}
      以上代码中,index是索引值,取值为0~9。
5. 修改main.c
      main.c文件代码如下:
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/BEEP/beep.h"
#include "./BSP/KEY/key.h"
#include "./BSP/OLED/oled.h"
#include "./BSP/OLED/logo.h"
/**
* @brief       主函数
* @param       无
* @retval      无
*/
int main(void)
{   
    HAL_Init();/* 初始化HAL库 */
    /* 初始化M4内核时钟,209M */
    if(IS_ENGINEERING_BOOT_MODE())
    {
      sys_stm32_clock_init(34, 2, 2, 17, 6826);
    }
    delay_init(209);      /* 延时初始化 */
    led_init();             /* 初始化LED */
    oled_init();            /* 初始化OLED */
    while (1)
    {
            int i;
            for(i = 0;i < 10;i ++)                              /* 共10张图片 */
            {
             OLED_ShowBMP(0,0,120, 60,i, 1);/* 显示一张图片 */
             oled_refresh_gram();                     /*更新显存到OLED */
             HAL_Delay(400);                            /* 延时500ms */
            }

            delay_ms(500);                              /* 延时500ms */
            LED0_TOGGLE();                              /* LED0闪烁 */
    }
}
      以上代码中,定义i,取i依次为0~9,每隔400ms显示一张图片,效果和前面的实验一样。
26.6.3 编译测试
      编译运行,可以看到一个动图效果:
图26.6.3. 1实验效果
26.7 OLED显示一个汉字
      本实验是在上一章实验的基础上修改的,注意main.c文件修改的地方。配置好的实验工程已经放到了开发板光盘中,由于几个实验共用一个工程,所以要测试哪个实验,就在min.c文件中把其它实验的代码注释掉即可。路径为:开发板光盘A-基础资料\1、程序源码\3、M4裸机驱动例程\ MP157-M4 HAL库V1.2\实验15-3 OLED显示一个汉字。      
      本小节实验我们按照26.2.3小节讲解中文字体显示的思路来显示一个汉字,本节的实验代码就不去深究GB2312这些编码了。下面以一个简单的例子来讲解,大家可以编写适合自己的代码显示一句话。
26.7.1 取模
      我们选择显示一个宋体“正”字。这里我们设置字体为宋体,字宽和字高可以随意,我们后面的程序会对字宽和字高做处理,使用阴码、顺向、逐列式、十六进制取模方式:
图26.7.1. 1取模设置
      取模后的点阵数据如下,共有128个十六进制数据(每个数据8位,相当于1个字节),如果将其分为两组,每组为64个:
图26.7.1. 2点阵数据
26.7.2 添加用户代码
1. 修改logo.h
      在logo.h后面添加一个一维数组Chinese[],数组的成员就是上面取模后的点阵数据:
图26.7.2. 1添加的一维数组
2. 修改oled.c
      在oled.c文件中添加显示中文函数OLED_Show_Chinese如下:
1   /**
2   * @brief   显示一个汉字
3   * @param   x,y : 起始坐标
4   *            px,py: 分别是中文的字宽和字高
5   *            ch : 包含汉字数据的一维数组首地址
6   *            mode:1点亮数据点,0不点亮数据点
7   * @retval    无
8   */
9   void OLED_Show_Chinese(uint8_t x,uint8_t y, uint8_t px, uint8_t                                                                                                         py,uint8_t* ch, uint8_t mode)
10{
11      uint8_t j,y0=y;
12      uint16_t i,number;
13      unsigned char f, s;
14      number=px*py/16;      /* number是字体所占用的字节数除以2 */
15      //number=64; /* 或者直接写等于64 */
16      for (i = 0; i <number ; ++i)
17      {
18          f = ch;
19          s = ch;
20          /* 第一个for循环 */
21          for (j = 0; j < 8; ++j)
22          {
23            if (0x80 & f)
24                  oled_draw_point(x,y,mode);
25            else
26                  oled_draw_point(x,y,!mode);
27            f <<= 1;
28            y++;
29            if(y-y0 == py)
30            {
31                  y=y0;
32                  x++;
33                  break;
34            }
35          }
36          /* 第二个for循环 */
37          for (j = 0; j < 8; ++j)
38          {
39            if (0x80 & s)
40                  oled_draw_point(x,y,mode);
41            else
42                  oled_draw_point(x,y,!mode);
43            s <<= 1;
44            y++;
45            if(y-y0 == py)
46            {
47                  y=y0;
48                  x++;
49                  break;
50            }
51          }
52
53      }
54}
      我们参考前面第26.2.3讲解的显示汉字的方法,显示一个英文字符使用的是一个for循环就可以了,因为汉字的字宽是英文字符的两倍,所以将一个汉字分为两个部分进行显示(相当于两个英文字符),所以用到两个for循环。
      程序中,x和y分别代表要在屏幕上显示的坐标点。px和py是要显示的汉字的字宽和字高,字宽和字高可以根据取模软件上设置的来,本实验我们取模软件上设置的字宽是33,字高是26,所以如果在main.c文件中调用此函数的话,这两个数字要写正确,否则可能会导致显示异常。
      第14行number是字体所占用的字节数除以2,即将字体所占用的字节数分为两组,所以后面会有两个for循环。本实验生成的字模共有128个十六进制数据,所以可以将number直接设置为64(即128/2)。
3. 修改main.c
      main.c文件代码如下,OLED_Show_Chinese(0,0, 33,26, Chinese, 1)这一行,x和y坐标是0,字宽和字高分别为33和26。
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/LED/led.h"
#include "./BSP/BEEP/beep.h"
#include "./BSP/KEY/key.h"
#include "./BSP/OLED/oled.h"
#include "./BSP/OLED/logo.h"
/**
* @brief       主函数
* @param       无
* @retval      无
*/
int main(void)
{   
    HAL_Init();    /* 初始化HAL库 */
    /* 初始化M4内核时钟,209M */
    if(IS_ENGINEERING_BOOT_MODE())
    {
      sys_stm32_clock_init(34, 2, 2, 17, 6826);
    }
    delay_init(209);      /* 延时初始化 */
    led_init();            /* 初始化LED */
    oled_init();             /* 初始化OLED */
    while (1)
    {
            OLED_Show_Chinese(0,0, 33,26, Chinese, 1);/* 显示一个汉字 */
            oled_refresh_gram();                           /* 更新显存到OLED*/                                          

            delay_ms(500);                                  /* 延时500ms */
            LED0_TOGGLE();                                  /* LED0闪烁 */
    }
}
26.7.3 编译运行
      编译运行,显示结果如下:
图26.7.3. 1实验效果
页: [1]
查看完整版本: 《STM32MP1 M4裸机HAL库开发指南》第二十六章 OLED实验