本文主要介绍OLED屏显和SPI原理,使用STM32F103的SPI或IIC接口实现自己的学号和姓名的显示,AHT20的温度和湿度的显示,以及滚屏显示。
文章目录
一、题目要求
(1) 显示自己的学号和姓名;
(2) 显示AHT20的温度和湿度;
(3) 上下或左右的滑动显示长字符(最好使用硬件刷屏模式)。
一、SPI介绍
1、先说串口
因为UART没有时钟信号,无法控制何时发送数据,也无法保证双发按照完全相同的速度接收数据。因此,双方以不同的速度进行数据接收和发送,就会出现问题。
如果要解决这个问题,UART为每个字节添加额外的起始位和停止位,以帮助接收器在数据到达时进行同步;
双方还必须事先就传输速度达成共识(设置相同的波特率,例如每秒9600位)。
传输速率如果有微小差异不是问题,因为接收器会在每个字节的开头重新同步。相应的协议如下图所示;
如果注意到上图中的
11001010
不等于0x53,这是一个细节。串口协议通常会首先发送最低有效位,因此最小位在最左边LSB
。低四位字节实际上是0011 = 0x3
,高四位字节是0101 = 0x5
。
异步串行工作得很好,但是在每个字节发送的时候都需要额外的起始位和停止位以及在发送和接收数据所需的复杂硬件方面都有很多开销。
不难发现,如果接收端和发送端设置的速度都不一致,那么接收到的数据将是垃圾(乱码)。
下面开始讲一下SPI协议,会有哪些优点。
2、SPI通讯协议
于是我们想有没有更好一点的串行通讯方式;相比较于UART
,SPI
的工作方式略有不同。
SPI
是一个同步的数据总线,也就是说它是用单独的数据线和一个单独的时钟信号来保证发送端和接收端的完美同步。
时钟是一个振荡信号,它告诉接收端在确切的时机对数据线上的信号进行采样。
产生时钟的一侧称为主机,另一侧称为从机。总是只有一个主机(一般来说可以是微控制器/MCU),但是可以有多个从机(后面详细介绍);
数据的采集时机可能是时钟信号的上升沿(从低到高)或下降沿(从高到低)。
具体要看对SPI的配置;
整体的传输大概可以分为以下几个过程:
- 主机先将
NSS
信号拉低,这样保证开始接收数据; - 当接收端检测到时钟的边沿信号时,它将立即读取数据线上的信号,这样就得到了一位数据(
1bit
);
由于时钟是随数据一起发送的,因此指定数据的传输速度并不重要,尽管设备将具有可以运行的最高速度(稍后我们将讨论选择合适的时钟边沿和速度)。
- 主机发送到从机时:主机产生相应的时钟信号,然后数据一位一位地将从MOSI信号线上进行发送到从机;
- 主机接收从机数据:如果从机需要将数据发送回主机,则主机将继续生成预定数量的时钟信号,并且从机会将数据通过
MISO
信号线发送;
具体如下图所示:
注意,SPI是“全双工”(具有单独的发送和接收线路),因此可以在同一时间发送和接收数据,另外SPI的接收硬件可以是一个简单的移位寄存器。这比异步串行通信所需的完整UART要简单得多,并且更加便宜;
数据在传输中,高位在先还是低位在先,SPI协议并无明确规定,但是数据要在主从机中正确传输,自然双方要先约定好,一般会采用高位在先(MSB)方式传输。
3、SPI特性
SPI总线包括4条逻辑线,定义如下:
- MISO:
Master input slave output
主机输入,从机输出(数据来自从机); - MOSI:
Master output slave input
主机输出,从机输入(数据来自主机); - SCLK :
Serial Clock
串行时钟信号,由主机产生发送给从机; - SS:
Slave Select
片选信号,由主机发送,以控制与哪个从机通信,通常是低电平有效信号。
其他制造商可能会遵循其他命名规则,但是最终他们指的相同的含义。以下是一些常用术语;
- MISO也可以是
SIMO,DOUT,DO,SDO或SO
(在主机端); - MOSI也可以是
SOMI,DIN,DI,SDI或SI
(在主机端); - NSS也可以是
CE,CS或SSEL
; - SCLK也可以是
SCK
;
本文将按照以下命名进行讲解[MISO, MOSI, SCK,NSS]
下图显示了单个主机和单个从机之间的典型SPI连接。
4、时钟频率
SPI总线上的主机必须在通信开始时候配置并生成相应的时钟信号。在每个SPI时钟周期内,都会发生全双工数据传输。
主机在MOSI
线上发送一位数据,从机读取它,而从机在MISO
线上发送一位数据,主机读取它。
就算只进行单向的数据传输,也要保持这样的顺序。这就意味着无论接收任何数据,必须实际发送一些东西!在这种情况下,我们称其为虚拟数据;
从理论上讲,只要实际可行,时钟速率就可以是您想要的任何速率,当然这个速率受限于每个系统能提供多大的系统时钟频率,以及最大的SPI传输速率。
5、时钟极性 CKP/Clock Polarity
除了配置串行时钟速率(频率)外,SPI主设备还需要配置时钟极性。
根据硬件制造商的命名规则不同,时钟极性通常写为CKP或CPOL。时钟极性和相位共同决定读取数据的方式,比如信号上升沿读取数据还是信号下降沿读取数据;
CKP可以配置为1或0。这意味着您可以根据需要将时钟的默认状态(IDLE)设置为高或低。极性反转可以通过简单的逻辑逆变器实现。您必须参考设备的数据手册才能正确设置CKP和CKE。
CKP = 0
:时钟空闲IDLE
为低电平0
;CKP = 1
:时钟空闲IDLE
为高电平1
;
6、多从机模式
前面说到SPI总线必须有一个主机,可以有多个从机,那么具体连接到SPI总线的方法有以下两种:
第一种方法:多NSS
- 通常,每个从机都需要一条单独的SS线。
- 如果要和特定的从机进行通讯,可以将相应的
NSS
信号线拉低,并保持其他NSS
信号线的状态为高电平;如果同时将两个NSS信号线拉低,则可能会出现乱码,因为从机可能都试图在同一条MISO
线上传输数据,最终导致接收数据乱码。
具体连接方式如下图所示;
第二种方法:菊花链
在数字通信世界中,在设备信号(总线信号或中断信号)以串行的方式从一 个设备依次传到下一个设备,不断循环直到数据到达目标设备的方式被称为菊花链。
- 菊花链的最大缺点是因为是信号串行传输,所以一旦数据链路中的某设备发生故障的时候,它下面优先级较低的设备就不可能得到服务了;
- 另一方面,距离主机越远的从机,获得服务的优先级越低,所以需要安排好从机的优先级,并且设置总线检测器,如果某个从机超时,则对该从机进行短路,防止单个从机损坏造成整个链路崩溃的情况;
具体的连接如下图所示;
其中红线加粗为数据的流向;
所以最终的数据流向图可以表示为:
SCK为时钟信号,8clks表示8个边沿信号;
其中D为数据,X为无效数据;
7、优缺点
SPI通讯的优势
使SPI作为串行通信接口脱颖而出的原因很多;
- 全双工串行通信;
- 高速数据传输速率。
- 简单的软件配置;
- 极其灵活的数据传输,不限于8位,它可以是任意大小的字;
- 非常简单的硬件结构。从站不需要唯一地址(与I2C不同)。从机使用主机时钟,不需要精密时钟振荡器/晶振(与UART不同)。不需要收发器(与CAN不同)。
SPI的缺点
- 没有硬件从机应答信号(主机可能在不知情的情况下无处发送);
- 通常仅支持一个主设备;
- 需要更多的引脚(与I2C不同);
- 没有定义硬件级别的错误检查协议;
- 与RS-232和CAN总线相比,只能支持非常短的距离;
二、OLED介绍
OLED即有机发光二极管(Organic Light-Emitting Diode),又称为有机电激光显示(Organic Electroluminesence Display, OELD)。OLED 由于同时具备自发光,不需背光源、对比度高、厚度薄、视角广、反应速度快、可用于挠曲性面板、使用温度范围广、构造及制程较简单等优异之特性,被认为是下一代的平面显示器新兴应用技术。
LCD 都需要背光,而 OLED 不需要,因为它是自发光的。这样同样的显示,OLED 效果要来得好一些。以目前的技术,OLED 的尺寸还难以大型化,但是分辨率确可以做到很高。
我们使用的是 ALINETEK 的 OLED 显示模块,该模块有以下特点:
1)模块有单色和双色两种可选,单色为纯蓝色,而双色则为黄蓝双色。
2)尺寸小,显示尺寸为 0.96 寸,而模块的尺寸仅为 27mmx26mm 大小。
3)高分辨率,该模块的分辨率为128x64。
4)多种接口方式,该模块提供了总共 5 种接口包括:6800、8080 两种并行接口方式、3线或 4 线的穿行 SPI 接口方式、IIC 接口方式(只需要 2 根线就可以控制 OLED 了)。
5)不需要高压,直接接 3.3V 就可以工作了。
注意该模块不和 5.0V 接口兼容,所以在使用的时候一定要小心,勿直接接到 5V 的系统上去,否则可能烧坏模块。
该模块采用 8*2 的 2.54 排针与外部连接,总共有 16 个管脚,在 16 条线中,我们只用了 15条,有一个是悬空的。15 条线中,电源和地线占了 2 条,还剩下 13 条信号线。在不同模式下,我们需要的信号线数量是不同的,在 8080 模式下,需要全部 13 条,而在 IIC 模式下,仅需要2条线就够了!这其中有一条是共同的,那就是复位线 RST(RES),RST 上的低电平,将导致 OLED 复位,在每次初始化之前,都应该复位一下 OLED 模块。
三、STM32+OLED显示个人学号姓名
硬件条件:STM32F103C8T6,0.96寸四引脚OLED显示屏,ST-LINK,USB转TTL模块
OLED接线图:
1、文字取模方法
工具下载
PCtoLCD工具链接下载:
链接:https://pan.baidu.com/s/1KxyWcgbkp3vRlvovru3gGw
提取码:bl3e
取模汉字
(1)选择字符模式
(2)字符设置
(3)字体设置
这里的字宽和字高一般和显示字符里面的大小一致
(4)输入汉字并保存字模数据
2、显示汉字
把数据添加到一个二维数组里面
char Hzk[][32]={
{0x80,0x84,0x44,0x44,0x24,0x14,0x0C,0xFF,0x0C,0x14,0x24,0x44,0x44,0x84,0x80,0x00},
{0x08,0x08,0x08,0x08,0x09,0x49,0x89,0x79,0x0D,0x0B,0x09,0x08,0x08,0x08,0x08,0x00},/*"李",0*/
{0x02,0xFA,0x82,0x82,0xFE,0x80,0x10,0x98,0x54,0x93,0x10,0x10,0x54,0x98,0x30,0x00},
{0x08,0x18,0x48,0x84,0x44,0xBF,0x89,0x44,0x46,0x2B,0x12,0x2A,0x46,0x80,0x81,0x00},/*"骏",1*/
{0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0xFE,0x40,0xA0,0x10,0x08,0x00,0x00},
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x03,0x0C,0x10,0x21,0x42,0xF0,0x00},/*"飞",2*/
{0x00,0x00,0x00,0x7F,0x49,0x49,0x49,0x49,0x49,0x49,0x49,0x7F,0x00,0x00,0x00,0x00},
{0x81,0x41,0x21,0x1D,0x21,0x41,0x81,0xFF,0x89,0x89,0x89,0x89,0x89,0x81,0x81,0x00},/*"是",3*/
{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*/
{0x00,0x00,0x3E,0x22,0x2A,0x32,0x22,0xFE,0x22,0x32,0x2A,0x22,0x3E,0x00,0x00,0x00},
{0x88,0x68,0x09,0x09,0x29,0xC9,0x09,0x0F,0x29,0xC9,0x09,0x09,0x29,0xC8,0x08,0x00},/*"黑",1*/
{0x80,0x82,0x82,0x82,0x82,0x82,0x82,0xE2,0xA2,0x92,0x8A,0x86,0x82,0x80,0x80,0x00},
{0x00,0x00,0x00,0x00,0x00,0x40,0x80,0x7F,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"子",2*/
};
3、编写显示函数
OLED_ShowCHinese函数
//显示汉字
void OLED_ShowCHinese(u8 x,u8 y,u8 no)
{
u8 t,adder=0;
OLED_Set_Pos(x,y);
for(t=0;t<16;t++)
{
OLED_WR_Byte(Hzk[2*no][t],OLED_DATA);
adder+=1;
}
OLED_Set_Pos(x,y+1);
for(t=0;t<16;t++)
{
OLED_WR_Byte(Hzk[2*no+1][t],OLED_DATA);
adder+=1;
}
}
main.c文件
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "bmp.h"
int main(void)
{ u8 t;
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
// delay_ms(8000);
OLED_Init(); //初始化OLED
OLED_Clear();
// OLED_ShowChar(1,1,'A',16);
OLED_ShowCHinese(0,0,0);//李
OLED_ShowCHinese(18,0,1);//骏
OLED_ShowCHinese(36,0,2);//飞
OLED_ShowCHinese(54,0,3);//是
OLED_ShowString(72,8,"ikun",4);
OLED_ShowCHinese(0,11,4);//小
OLED_ShowCHinese(18,11,5);//黑
OLED_ShowCHinese(36,11,6);//子
OLED_ShowString(4,15,"632109160602",12);
while(1)
{
}
}
4、运行结果
四、STM32+OLED显示AHT20的温度和湿度
1、设备安装
我使用的是四引脚的OLED,其中SDA引脚接GPIOB_9,SCL引脚接GPIOB_8,AHT20模块的SDA引脚接GPIOB_7,SCL引脚接GPIOB_6
2、部分代码
只需要将上一节的AHT20的代码移植到OLED文字显示模块即可。
点阵显示文字
{0x10,0x60,0x02,0x8C,0x00,0x00,0xFE,0x92,0x92,0x92,0x92,0x92,0xFE,0x00,0x00,0x00},
{0x04,0x04,0x7E,0x01,0x40,0x7E,0x42,0x42,0x7E,0x42,0x7E,0x42,0x42,0x7E,0x40,0x00},/*"温",0*/
{0x10,0x60,0x02,0x8C,0x00,0xFE,0x92,0x92,0x92,0x92,0x92,0x92,0xFE,0x00,0x00,0x00},
{0x04,0x04,0x7E,0x01,0x44,0x48,0x50,0x7F,0x40,0x40,0x7F,0x50,0x48,0x44,0x40,0x00},/*"湿",1*/
{0x00,0x00,0xFC,0x24,0x24,0x24,0xFC,0x25,0x26,0x24,0xFC,0x24,0x24,0x24,0x04,0x00},
{0x40,0x30,0x8F,0x80,0x84,0x4C,0x55,0x25,0x25,0x25,0x55,0x4C,0x80,0x80,0x80,0x00},/*"度",2*/
{0x00,0x00,0x00,0xFE,0x92,0x92,0x92,0x92,0x92,0x92,0x92,0xFE,0x00,0x00,0x00,0x00},
{0x40,0x42,0x44,0x58,0x40,0x7F,0x40,0x40,0x40,0x7F,0x40,0x50,0x48,0x46,0x40,0x00},/*"显",3*/
{0x40,0x40,0x42,0x42,0x42,0x42,0x42,0xC2,0x42,0x42,0x42,0x42,0x42,0x40,0x40,0x00},
{0x20,0x10,0x08,0x06,0x00,0x40,0x80,0x7F,0x00,0x00,0x00,0x02,0x04,0x08,0x30,0x00},/*"示",4*/
main.c代码:
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "bmp.h"
#include "AHT10.h"
int main(void)
{ u8 t;
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
// delay_ms(8000);
OLED_Init(); //初始化OLED
OLED_Clear();
AHT10_Init(); //初始化AHT10
OLED_ShowCHinese(0,0,0);//温
OLED_ShowCHinese(18,0,1);//湿
OLED_ShowCHinese(36,0,2);//度
OLED_ShowCHinese(54,0,3);//显
OLED_ShowCHinese(72,0,4);//示
OLED_ShowCHinese(100,0,6);//骏
OLED_ShowCHinese(0,11,0);//温
OLED_ShowCHinese(18,11,2);//度
OLED_ShowChar(33,11,':',16);//:
// OLED_ShowChar(44,11,'°',16);//°
OLED_ShowCHinese(0,13,1);//湿
OLED_ShowCHinese(18,13,2);//度
OLED_ShowChar(33,13,':',16);//:
// OLED_ShowChar(44,13,'%',16);//%
uint32_t a=0;
uint16_t err_count=0;
while(1)
{
//a++;
// OLED_ShowNum(1, 1, a, 9,4);//计数显示,方便观察程序是否正常运行
// if(a==999999999)a=0;
float Temp,Hum; //声明变量存放温湿度数据
if(ReadAHT10(&Hum,&Temp)) //读取温湿度数据
{
if(Temp>=0)
{
char String[10];
sprintf(String, "+%.2fC", Temp);//格式化字符串输出到字符串变量
OLED_ShowString(40, 3, String,6); //显示温度
sprintf(String, " %.2f%%", Hum);//格式化字符串输出到字符串变量
OLED_ShowString(40,5, String,6); //显示湿度
}else
{
char String[10];
sprintf(String, "-%.2fC", Temp);//格式化字符串输出到字符串变量
OLED_ShowString(40, 3, String,6); //显示温度
sprintf(String, " %.2f%%", Hum);//格式化字符串输出到字符串变量
OLED_ShowString(40, 5, String,6); //显示湿度
}
}
else
{
err_count++;
OLED_ShowNum(4,1, err_count, 5,1); //显示错误次数计数
}
delay_ms(100); //延时100毫秒
}
}
3、运行结果
五、STM32+OLED上下或左右的滑动显示长字符
1、设备安装
OLED的SCL引脚接GPIOB_8,SDA引脚接GPIOB_9
2、部分代码
点阵显示文字
main.c文件:
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "bmp.h"
#include "AHT10.h"
void TEST_MainPage(void)
{
OLED_ShowCHinese(0,9,0);//守
OLED_ShowCHinese(18,9,1);//护
OLED_ShowCHinese(36,9,2);//全
OLED_ShowCHinese(54,9,3);//世
OLED_ShowCHinese(72,9,4);//界
OLED_ShowCHinese(54,12,5);//最
OLED_ShowCHinese(72,12,6);//好
OLED_ShowCHinese(90,12,7);//的
OLED_ShowCHinese(72,14,8);//坤
OLED_ShowCHinese(90,14,8);//坤
delay_ms(1500);
delay_ms(1500);
}
int main(void)
{
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2:2位抢占优先级,2位响应优先级
OLED_Init(); //初始化OLED
OLED_Clear(); //清屏(全黑)
OLED_WR_Byte(0x2E,OLED_CMD); //关闭滚动
OLED_WR_Byte(0x27,OLED_CMD); //水平向左或者右滚动 26/27
OLED_WR_Byte(0x00,OLED_CMD); //虚拟字节
OLED_WR_Byte(0x00,OLED_CMD); //起始页 0
OLED_WR_Byte(0x07,OLED_CMD); //滚动时间间隔
OLED_WR_Byte(0x07,OLED_CMD); //终止页 7
OLED_WR_Byte(0x00,OLED_CMD); //虚拟字节
OLED_WR_Byte(0xFF,OLED_CMD); //虚拟字节
TEST_MainPage();
OLED_WR_Byte(0x2F,OLED_CMD); //开启滚动
while(1)
{
}
}
4、运行结果
总结
本次实验主要为完成基于STM32及ISP协议的OLED显屏。本次实验学习了一遍SPI协议,和之前的I2C协议对比发现SPI的速度要比I2C的传输速度更快,更适合用作高速传输通信。还学习了OLED显屏,它的学习资料比较全,但也有很多不太了解的地方,虽然此次实验的代码是借鉴的学长学姐的,但也认真分析了代码。在实验过程中我也遇到了一些问题有很多代码都看不懂需要在网上搜索。总而言之,学习是个漫长的过程,还得加油。如有错误,请指正。