IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 嵌入式 -> STM32应用开发实践教程:具备交互功能的人机界面应用开发 -> 正文阅读

[嵌入式]STM32应用开发实践教程:具备交互功能的人机界面应用开发

4.4.1 任务分析
本任务要求开发一个具备交互功能的人机界面应用,该应用集环境温湿度显示、光照强度显
示与日历功能于一身。系统与 DHT11 温湿度传感器、BH1750 环境光照强度传感器和 OLED 显
示模块连接,通电后默认每隔一段时间采集一次环境参数,并将其显示在 OLED 显示模块上。用
户操作按键并保持长按,系统切换为日历功能,OLED 显示模块上显示当前的日期、星期以及时
间。显示界面样式可参考图 4-4-1。

图 4-4-1(a)显示了环境参数,从图中可知当前环境温度为 15℃,环境湿度为 51%,光
照强度为 2135lx。图 4-4-1(b)为日历功能显示,样式中显示的当前日期为 2019 年 1 月 15
日、星期一,时间为早上 6 点 55 分 32 秒。
根据任务要求,OLED 显示模块的驱动与操作是本任务的核心内容。常见的 OLED 显示模块
通过 SPI 与 STM32F4 系列微控制器相连。因此,本任务涉及的知识点有以下 3 点:
? SPI 规范;
? OLED 显示模块的工作原理;
? 使用 STM32F4 系列微控制器进行 OLED 编程配置并显示指定内容的方法。
4.4.2 知识链接
1.SPI 规范
(1)SPI 概述
串行外围设备接口(Serial Peripheral Interface,SPI)是一种高速的、全双工、同步通信总
线。SPI 最早由摩托罗拉(Motorola)公司在其 MC68HCXX 系列处理器上定义,经过多年的发
展,已被广泛地应用于 EEPROM、Flash、实时时钟芯片、网络通信控制器、A/D 转换器、OLED
显示模块等器件上。
(2)典型的 SPI 通信系统的连接方式
图 4-4-2 展示了典型的 SPI 通信系统的连接方式。
从图 4-4-2 中可以看到,基于 SPI 的通信系统使用 4 条信号引脚:SCK、MOSI、MISO 和
NSS,下面分别对这 4 个信号引脚进行介绍。
① SCK
SCK(Serial Clock)引脚为同步时钟信号线,用于数据通信的同步。同步时钟由主机产生,
具体频率大小取决于数据接收方。不同的设备支持的最高时钟频率不同,当两个设备之间进行
SPI 通信时,通信速率取决于低速设备。
② MOSI
MOSI(Master Output/Slave Input)引脚为“主机输出/从机输入”信号线,用于数据收发。
当设备被配置为主机时,通过该信号线发送数据;当设备被配置为从机时,通过该信号线接收
数据。

?③ MISO
MISO(Master Input/Slave Output)引脚为“主机输入/从机输出”信号线,用于数据收发。当设
备被配置为从机时,通过该信号线发送数据;当设备被配置为主机时,通过该信号线接收数据。
④ NSS
NSS(Negative Slave Select, Negative 代表取反)引脚为“从机选择”信号线,也被称为片选
端,在不同的设备上有时也表示为 SS 或 CS。SPI 总线上同一时刻连接了多个从机,当主机需要与
某一台从机通信时,就通过“从机选择”信号线来确定要通信的从机。该信号线为低电平有效,因
此主机将某台从机的NSS 信号线置低后选中该从机,然后主机就可以开始与该从机进行通信。
(3)SPI 通信系统的通信时序
图 4-4-3 展示了 SPI 通信系统的通信时序。

从图 4-4-3 中可以看到,NSS、SCK 和 MOSI 这 3 条信号线为输出方向,由主机控制输出。
而 MISO 信号线为输入方向,主机通过该信号线接收从机数据。MOSI 与 MISO 信号线上的数据
收发仅当 NSS 信号线为低电平时有效,每个同步时钟(SCK)的信号周期均采样一位数据。
(4)SPI 通信的起始信号和停止信号
SPI 通信的起始信号如图 4-4-3 中标号①处所示。主机控制 NSS 信号线由高电平转为低电
平,选中总线上某从机之后,主从机之间开始通信。

SPI 通信的停止信号如图 4-4-3 中标号⑤处所示。主机控制 NSS 信号线由低电平转为高电
平,取消从机的选中状态并结束 SPI 通信。
(5)SPI 通信的数据有效性
从图 4-4-3 中可以看到,NSS 信号线电平的高低状态决定了 SPI 通信数据有效与否。仅当
NSS 信号线为低电平时(图 4-4-3 中标号④位置),MOSI 和 MISO 信号线上的数据收发有效。
两条数据线在 SCK 信号线的每个时钟周期传输一位数据,而且数据的输入与输出是同时进
行的。图 4-4-3 中的数据传输模式为最高有效位(Most Significant Bit,MSB)先行,但 SPI
规范并未规定数据传输应该 MSB 先行或最低有效位(Least Significant Bit,LSB)先行,只要
SPI 通信的双方约定好即可。
从图 4-4-3 中标号②和③处可以看到,MOSI 和 MISO 信号线上的数据在 SCK 的上升沿期
间触发电平,在 SCK 的下降沿被采样。我们称数据被采样的时刻为“数据有效”时刻,此时数
据线上的高电平表示数据“1”,低电平表示数据“0”。而在非数据采样时刻,两条数据线上的数
据均无效。
根据 SPI 规范,数据线上每次传输的数据可以是 8 位或 16 位。
(6)SPI 通信的模式
根据 SPI 规范,SPI 通信共有 4 种模式,分别是模式 0~模式 3。这几种模式之间的主要不
同有两点:一是总线空闲时,SCK 的电平状态不同;二是 MOSI 和 MISO 信号线上数据的采样
时刻不同。
在学习 SPI 通信的模式之前,我们需要掌握两个概念,分别是“时钟极性”与“时钟相位”。
① 时钟极性
“时钟极性(CPOL)”用于指示当 SPI 通信设备处于空闲状态(即 NSS 线为高电平)时 SCK
信号线的电平状态。当 CPOL=0 时,空闲状态的 SCK 为低电平;当 CPOL=1 时,空闲状态的
SCK 为高电平。
② 时钟相位
“时钟相位(CPHA)”用于配置数据采样的时刻。当 CPHA=0 时,MOSI 和 MISO 信号线上
的信号将会在 SCK 线的“奇数边沿”被采样,如图 4-4-4 所示 。当 CPHA=1 时,MOSI 和 MISO
信号线上的信号将会在 SCK 线的“偶数边沿”被采样,如图 4-4-5 所示。

图 4-4-4 展示了 CPHA=0 时的 SPI 通信时序。从图中可以看到,MOSI 和 MISO 线上的数
据采样时刻位于 SCK 线上的奇数(第 1、3、5、7、9 等)边沿。注意:当 CPOL=0 时,时钟的
奇数边沿为上升沿;而 CPOL=1 时,时钟的奇数边沿为下降沿,但这并不影响数据的采样。
图 4-4-5 展示了 CPHA=1 时的 SPI 通信时序。从图中可以看到,MOSI 和 MISO 线上的数
据采样时刻位于 SCK 线上的偶数(第 2、4、6、8、10 等)边沿。同样地,数据采样时刻不受
CPOL 参数的影响。

综上所述,CPOL 和 CPHA 各有两种不同的取值,不同取值的组合形成了 SPI 通信的 4 种模
式,如表 4-4-1 所示。需要注意的是,在 SPI 通信系统中,主机和从机必须工作在相同的模式
下才能正常通信。?

2.STM32F4 系列微控制器 SPI 的功能特性
STM32F4 系列微控制器内部集成了 SPI 外设,该外设具有以下主要特性:
? 可作为主机也可作为从机;
? 支持双线全双工、双线单工以及单线传输;
? 支持 SPI 通信的 4 种模式;
? 可选择 8 位或 16 位传输帧格式,并可设置数据传输 MSB 先行或 LSB 先行;
? 支持的 SCK 频率最高为 f PCLK /2(对于 STM32F407ZGT6 型号 MCU 而言, f PCLK2 =84 MHz,
f PCLK1 =42 MHz);
? 主机和从机模式都可通过硬件或软件方式进行 NSS 片选管理。
图 4-4-6 展示了 STM32F4 系列微控制器 SPI 外设的硬件框图。?

从图 4-4-6 中可以看到,SPI 外设的硬件框图被分为 4 个部分:外部引脚、数据收发控制、
通信波特率控制和 SPI 通信整体控制,下面分别对它们进行介绍。
(1)外部引脚
根据 SPI 规范,SPI 通信需要使用 4 根信号线:MOSI、MISO、SCK 和 NSS(图 4-4-6 中
标号①位置)。根据 ST 公司的产品规格书,STM32F4 系列微控制器配备了 6 个 SPI 外设,各 SPI
外设的引脚映射如表 4-4-2 所示。?

由表 4-4-2 可知,在 6 个 SPI 外设中,SPI1、SPI4、SPI5 和 SPI6 挂载于 APB2 总线上,
支持的最高通信速率为 84 MHz / 2 = 42 MHz,而 SPI2 和 SPI3 挂载于 APB1 总线上,支持的最
高通信速率为 42 MHz / 2 = 21 MHz。需要注意的是,只有 176 引脚的 MCU 才有 PH 和 PI 端口。
(2)数据收发控制
从图 4-4-6 中标号②处对应的阴影部分可知,“地址和数据总线”“接收缓冲区”“移位寄存
项目 4 环境参数监测与显示系统的设计与实现 ?233
Chapter 4
器”“发送缓冲区”“MOSI 与 MISO 数据引脚”等部分构成了 SPI 通信的数据收发控制逻辑。其
中,MOSI 与 MISO 数据引脚与移位寄存器直接相连,是 SPI 通信数据的主要出入口。下面对数
据收发控制流程进行介绍。
① 数据发送流程
? 微控制器通过“地址和数据总线”,将需要发送的数据填充到“发送缓冲区”中。
? “移位寄存器”以“发送缓冲区”为数据源,将数据一位一位地通过 MOSI 信号线发送
出去。
② 数据接收流程
? MISO 信号线将采样到的数据一位一位地接收进来。
? 通过“移位寄存器”将数据一位一位地存储到“接收缓冲区”中。
? 读取“数据寄存器”,获得“接收缓冲区”中的内容。
另外,配置“CR1”中的“DFF”位段,可将数据帧长度配置为 8bit 或 16bit。配置“CR1”
中的“LSBFIRST”位段,可控制数据传输是 MSB 先行还是 LSB 先行。
(3)通信波特率控制
在 SPI 通信中,主机须提供 SCK 信号,该信号决定了 SPI 通信的波特率。SCK 信号的时钟
源是 f PCLK1 或 f PCLK2 ,经分频后由波特率发生器输出。从图 4-4-6 中标号③处对应的阴影部分可知,
分频系数由“CR1”中的 BR[2:0]位段控制,具体配置方式如表 4-4-3 所示。?

(4)SPI 通信整体控制
从图 4-4-6 中标号④处对应的阴影部分可知,跟 SPI 通信配置相关的寄存器主要有 3 个:
SPI_CR1、SPI_CR2 和 SPI_SR。
SPI_CR1 和 SPI_CR2 可以对 SPI 的通信模式、波特率、是否 MSB 先行、主从模式、单双
向模式等工作参数进行配置。用户读取 SPI_SR 中相应位的值,可获取 SPI 通信的工作状态,为
程序的后续走向提供参考。另外,用户还应根据应用的需求,配置是否产生 SPI 中断、是否使用
DMA 进行数据的传输等参数。
接下来通过一个示例对 SPI 通信的过程进行解析。图 4-4-7 展示了 SPI 通信主机的数据收
发时序。
读者在分析该示例时,应关注数据收发的流程,并理解 SR 中各状态标志位(TXE、BSY
和 RXNE 等)对程序流程的控制作用。在该示例中主机需要发送 3 个字节的数据(0xF1、
0xF2 和 0xF3),接收 3 个字节的数据(0xA1、0xA2 和 0xA3),数据收发流程及主要事件
如下。

① 设置“SPI_CR1”中的“SPE”位为 1,使能 SPI 模块,并控制 NSS 信号线为低电平,
产生起始信号。
② 向“DR”中写入第一个要发送的数据 0xF1,该数据将会存储到“发送缓冲区”中。
③ “移位寄存器”通过 MOSI 数据线将“发送缓冲区”中的数据一位一位地发送出去。
④ 一帧数据发送完毕后,“SR”中的“TXE 标志位”置 1。此时可向“DR”中写入第二个
要发送的数据 0xF2,重复第②步和第③步的过程。0xF3 数据的发送流程亦是如此。
⑤ 等待“RXNE 标志位”变为 1,这表明接收到了一帧数据。此时可读取“DR”获取“接
收缓冲区”中的数据内容。读取“DR”的同时会清除“RXNE 标志位”。重复本步骤操作即可接
收所有的数据。
⑥ 等待“BSY 标志位”变为 0 后关闭 SPI 模块。
在这个示例中,数据收发的控制是通过“软件查询标志位”的方式进行的。这种控制方式效
率较低,且浪费 CPU 资源。我们可采用以下两种控制方式提高程序执行的效率。
一是配置使能 SPI 通信的“TXE 中断”与“RXNE 中断”。事件发生时会进入相应的中断服
务函数,用户可在中断服务函数中编写事件处理方法的程序。
二是配置使能 SPI 通信的直接内存访问(DMA)功能。使用 DMA 方式收发“DR”中的数
据可极大地提升程序执行的效率。

3.使用 STM32F4 标准外设库配置 SPI 外设的步骤
STM32F4 标准外设库提供了初始化与数据收发等函数,用于 SPI 外设的初始化配置与操作
控制,相关函数定义在库文件“stm32f4xx_spi.c”和“stm32f4xx_spi.h”中。若要将 STM32F4
系列微控制器的 SPI 外设配置为主机模式并能进行数据的收发,则具体的步骤如下。
(1)使能 SPI 时钟,配置相关的 GPIO 引脚功能
在使用某个 SPI 外设之前,需要先使能相应的 SPI 时钟。调用 RCC_APBxPeriphClockCmd()
函数可实现该功能,下列代码片段表示可使能 SPI2 的时钟。
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
除此之外,我们还需配置 SPI 外设对应 GPIO 引脚的工作模式为复用功能(复用为 SPI 外设
引脚)。根据表 4-4-2,PB15、PB14 和 PB10 这 3 个 GPIO 引脚可分别复用为 SPI2 的 MOSI、
MISO 和 SCK,配置的关键代码片段如下:?

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //GPIO 引脚配置为复用功能
GPIO_PinAFConfig(GPIOB,GPIO_PinSource15,GPIO_AF_SPI2); //PB15 复用为 SPI2_MOSI
GPIO_PinAFConfig(GPIOB,GPIO_PinSource14,GPIO_AF_SPI2); //PB14 复用为 SPI2_MISO
GPIO_PinAFConfig(GPIOB,GPIO_PinSource10,GPIO_AF_SPI2); //PB10 复用为 SPI2_SCK
(2)配置 SPI 的工作参数并初始化 SPI
STM32F4 标准外设库提供了 SPI 初始化结构体和相应的初始化函数来完成 SPI 工作参数的
配置。SPI 初始化结构体的原型定义如下:

typedef struct
{
uint16_t SPI_Direction; // 配置 SPI 的通信方向
uint16_t SPI_Mode; // 配置 SPI 的主 / 从机模式
uint16_t SPI_DataSize; // 配置 SPI 的数据帧长度,可选 8bit/16bit
uint16_t SPI_CPOL; // 配置时钟极性,可选高 / 低电平
uint16_t SPI_CPHA; // 配置时钟相位,可选奇 / 偶数边沿采样
uint16_t SPI_NSS;  // 配置 NSS 引脚由 SPI  硬件控制还是软件控制
uint16_t SPI_BaudRatePrescaler; // 配置时钟分频系数, f PCLK / 分频系数 = f SCK
uint16_t SPI_FirstBit; // 配置 MSB 或 LSB 先行
int16_t SPI_CRCPolynomial; // 配置 CRC 校验的多项式
} SPI_InitTypeDef;

接下来对 SPI 初始化结构体各成员变量的功能进行介绍。
① SPI_Direction
该成员被用于配置 SPI 的通信方向,可供配置的选项如下:
? 双线全双工(SPI_Direction_2Lines_FullDuplex);
? 双线只接收(SPI_Direction_2Lines_RxOnly);
? 单线只接收(SPI_Direction_1Line_Rx);
? 单线只发送(SPI_Direction_1Line_Tx)。
② SPI_Mode
该成员被用于配置 SPI 的模式,可供配置的选项如下:
? 主机模式(SPI_Mode_Master);
? 从机模式(SPI_Mode_Slave)。
在 SPI 通信系统中,微控制器一般被配置为主机模式。
③ SPI_DataSize
该成员被用于配置 SPI 通信的数据帧长度,可供配置的选项如下:
? 数据帧长度为 8 bit(SPI_DataSize_8b);
? 数据帧长度为 16 bit(SPI_DataSize_16b)。

④ SPI_CPOL
该成员被用于配置 SPI 外设的“时钟极性”参数,它与 SPI_CPHA 的不同取值组合形成了 4
种不同的 SPI 通信模式,具体如表 4-4-1 所示。可供配置的选项如下:
? CPOL 高电平(SPI_CPOL_High);
? CPOL 低电平(SPI_CPOL_Low)。
⑤ SPI_CPHA
该成员被用于配置 SPI 外设的“时钟相位”参数,它与 SPI_CPOL 的不同取值组合形成了 4
种不同的 SPI 通信模式,具体如表 4-4-1 所示。可供配置的选项如下:
? 在 SCK 的奇数边沿采集数据(SPI_CPHA_1Edge);
? 在 SCK 的偶数边沿采集数据(SPI_CPHA_2Edge)。
⑥ SPI_NSS
该成员被用于配置 NSS 引脚(俗称片选端)的控制模式,可供配置的选项如下:
? 硬件控制模式(SPI_NSS_Hard);
? 软件控制模式(SPI_NSS_Soft)。
在实际应用中,使用软件控制模式居多。本任务的示例程序就使用了软件控制模式对 NSS
引脚进行控制。
⑦ SPI_BaudRatePrescaler
该成员被用于配置 SPI 波特率的分频系数,时钟源为 f PCLK ,分频后的时钟信号作为 SCK 信
号线的时钟信号,可供配置的参数如表 4-4-3 所示。
例如:要对 f PCLK 时钟进行 2 分频的宏定义为 SPI_BaudRatePrescaler_2,其他分频系数的配
置选项与此类似。
⑧ SPI_FirstBit
该成员被用于配置 SPI 通信的数据传输是“高位数据先行”还是“低位数据先行”,可供配
置的选项如下:
? 高位数据(MSB)先行(SPI_FirstBit_MSB);
? 低位数据(LSB)先行(SPI_FirstBit_LSB)。
⑨ SPI_CRCPolynomial
该成员被用于配置 SPI 通信的 CRC 校验多项式,一般配置其值大于 1 即可。
SPI 初始化结构体各成员变量配置完毕后,调用 SPI_Init()函数将变量值写入 SPI_CR1 中,
完成 SPI 外设的初始化,关键代码片段如下:
SPI_Init(SPI2, &SPI_InitStructure);
(3)使能 SPI 外设
配置好 SPI 的工作参数后,我们可使能某 SPI 外设。标准外设库提供了使能 SPI 的函数
SPI_Cmd(),该函数使能 SPI2 外设的代码片段如下:
SPI_Cmd(SPI2, ENABLE);
(4)编写 SPI 通信的数据传输函数
STM32F4 标准外设库提供了 SPI 发送数据函数和接收数据函数,它们的原型定义如下:
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);

前者为 SPI 发送数据函数,它需要两个参数:一是 SPI 编号;二是需要发送的数据,数据类
型为无符号 16 位整型数。
后者为 SPI 接收数据函数,它只需一个参数,即 SPI 编号。该函数返回接收到的数据,数据
类型为无符号 16 位整型数。
用户可对 STM32F4 标准外设库提供的基本数据收发函数进行封装,编写相应的数据传输函
数,以满足具体应用场景的需求。
(5)获取 SPI 传输的状态
我们在解析SPI通信数据收发示例时,提到了若干个在SPI通信中用于指示传输状态的标志,
如 TXE、RXNE、BSY 等。在 SPI 通信系统的程序设计过程中,我们经常需要获取 SPI 传输的状
态(如数据是否发送完成、是否收到了新数据等),并使其作为后续程序走向的判断依据。
STM32F4 标准外设库提供了 SPI_I2S_GetFlagStatus()函数来获取相应的状态标志。例如,判断
SPI2 是否收到了新数据的代码片段如下:
SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE);
4.OLED 显示模块的编程控制
(1)OLED 显示模块概述
有机发光二极管(Organic Light-Emitting Diode,OLED),又称有机激光显示。OLED 同时
具备自发光、无需背光源、对比度高、厚度薄、视角广、反应速度快、可用于挠曲性面板、使用温
度范围广、构造及制程较简单等优良的特性,因此被认为是下一代的平面显示器的新兴应用技术。
OLED 分为被动矩阵 OLED 和主动矩阵 OLED 两种类型,两者在驱动方式上有所不同。
被动矩阵 OLED(PassiveMatrixOLED,PMOLED)由阴极带、有机层和阳极带构成,阳极
带与阴极带相互垂直,它们的交叉点形成像素,也就是发光的部位。外部电路向选取的阴极带和
阳极带施加电流,从而决定哪些像素发光,哪些像素不发光。另外,每个像素的亮度与施加电流
的大小成正比。PMOLED 的优点是结构简单,制造成本低。缺点是耗电量高,因此 PMOLED 不
适合在大尺寸、高分辨率的面板上应用。
主动矩阵 OLED(Active Matrix OLED,AMOLED)采用独立的薄膜晶体管(Thin Film
Transistor,TFT)控制每个像素,每个像素都可以连续且独立地发光。AMOLED 的优点是耗电
量低,发光元件寿命长。
接下来介绍一种在智能电子产品中常用的 OLED 显示模块,其实物如图 4-4-8 所示。

该显示模块具有以下特点。

① 集成了显示驱动芯片(如 SSD1306),该驱动芯片体积很小,一般被封装在显示屏背面
的玻璃基板上。
② 尺寸可选 0.96 寸、1.3 寸、1.54 寸(1 寸≈3.33cm)等,显示模块整体体积小。
③ 分辨率较高,一般为 128 像素×64 像素。
④ 所需供电电压低,一般为 3.3 V。
⑤ 支持多种接口,如 8 位 6800/8080 并行接口、3 线/4 线 SPI 和 I
2 C 接口等,用户可根据
应用需求选择合适的接口方式。
(2)显示驱动芯片的控制原理
在实际应用中,我们一般使用 MCU 作为主控,通过编程驱动 OLED 显示模块显示指定的字
符和数字。在编写 OLED 驱动程序之前,我们须了解显示驱动芯片的控制原理。接下来对显示驱
动芯片 SSD1306 的控制原理进行介绍。
SSD1306 是一款内置了“有机/高分子”发光二极管点阵显示系统控制器的单片
CMOSOLED/PLED 驱动芯片,它专门为共阴极 OLED 显示面板而设计。SSD1306 内置了对比
度调节电路、显示存储器和振荡器,可支持 256 级亮度调节。
SSD1306 具有以下主要特性。
? 支持 128 像素×64 像素的点阵显示面板。
? 输入工作电压低:1.65~3.3 V。
? 内部集成容量为(128×64)bit 的 SRAM 显示缓存。
? 内部集成振荡器,可减少周边电路元器件。
? 支持多种接口:8 位 6800/8080 并行接口、3 线/4 线 SPI 和 I
2 C 接口。
? 行列均可重映射。
? 支持水平方向和垂直方向的持续滚动,以实现屏幕保护的功能。
① SSD1306 的 GDDRAM 介绍
图形显示数据存储器(Graphic Display Data Random Access Memory,GDDRAM)主要
用于存储显示数据。用户通过 MCU 把需要显示的数据写入 GDDRAM,然后向 SSD1306 发送相
应的显示命令,显示驱动芯片则会按照用户的命令要求逐帧扫描显示。
SSD1306 的 GDDRAM 存储空间大小为 1024B,该存储空间里的每个 bit 与 OLED 显示屏
上 128 像素×64 像素的像素点阵是一一对应的。这些空间被分成了 8 页(编 号 Page0~Page7),
每页含 128 个位段(编号 Seg0~Seg127),如 图 4-4-9 所示。

图 4-4-10 展示了 GDDRAM 某分页(Page2)的存储结构,从图中可以看到一个页包含 8
行、128 列(即每页的存储空间为 128B)。每个字节按竖向排列,低位在上、高位在下。

② SSD1306 的指令系统解析
按照功能来分,SSD1306 的指令可分为 5 大类,即基础指令、显示滚动指令、地址设置指
令、硬件配置指令和时序设置指令。这些指令长短不一,最短的是单字节指令,最长的是 6B 指
令。各指令的详细说明如表 4-4-4 所示。

?③ 存储器寻址模式介绍
要在 OLED 显示屏的指定位置显示字符,我们需要先指定字符的显示位置(又称为“地址”
或“指针”)。因此我们有必要了解 SSD1306 的存储器寻址模式。SSD1306 有 3 种不同的存储器
寻址模式:页寻址模式、水平寻址模式和垂直寻址模式。表 4-4-4 中的第 12 条指令即可设置
SSD1306 的存储器寻址模式。
页寻址模式支持在本页内连续写入数据。MCU 往 SSD1306 依次发送“确定寻址模式”“确
定页指针”“确定列起始指针”指令后,就可以逐个字节连续写入数据。这种模式既可以写入整
行数据,也可以在该页的任意一列起写入一列或者多列数据,是最灵活的一种写入方式。页寻址
模式下的地址指针移动示意如图 4-4-11 所示。

④ SSD1306 初始化流程
掌握了 SSD1306 显示驱动芯片的控制原理和指令系统后,接下来我们对其初始化流程进行
学习。SSD1306 的典型初始化流程如图 4-4-14 所示,该初始化流程是芯片手册推荐的流程,
我们只需对其进行小幅度修改即可适应项目需求。?

4.4.3 任务实施
1.硬件连接
按照表 4-4-5 所示的人机界面应用系统硬件接线表将温湿度传感器 DHT11、环境光照强度
传感器 BH1750、OLED 显示模块与 STM32F4 系列微控制器相连。?

2.编写 SPI 外设的初始化程序和数据收发程序
复制一份任务 4.3 的工程,并将其重命名为“task4.4_OLED”。在“ HARDWARE”文件夹
下新建名为“SPI”的子文件夹,新建“bsp_spi.c”和“bsp_spi.h”两个文件,将它们加入工
程中,并配置头文件包含路径。在“bsp_spi.c”文件中编写 SPI 外设的初始化程序和数据收发
程序,在“bsp_spi.h”文件中编写 SPI 外设主要引脚的宏定义和函数声明。
首先在“bsp_spi.h”文件中输入以下代码:

#ifndef __BSP_SPI_H
#define __BSP_SPI_H
#include "sys.h"
/* SPI2_MOSI PB15/PC3 */
#define SPI2_MOSI_CLK RCC_AHB1Periph_GPIOB
#define SPI2_MOSI_PORT GPIOB
#define SPI2_MOSI_PIN GPIO_Pin_15
#define SPI2_MOSI_PINSOURCE GPIO_PinSource15
#define SPI2_MOSI_AF  GPIO_AF_SPI2
/* SPI2_SCK PB10/PB13 */
#define SPI2_SCK_CLK  RCC_AHB1Periph_GPIOB
#define SPI2_SCK_PORT GPIOB
#define SPI2_SCK_PIN  GPIO_Pin_10
#define SPI2_SCK_PINSOURCE GPIO_PinSource10
#define SPI2_SCK_AF GPIO_AF_SPI2
void SPI2_Init(void);
uint8_t SPI2_ReadWriteByte(uint8_t TxData);
#endif

?然后在“bsp_spi.c”文件中输入以下代码:

#include "bsp_spi.h"
/**
* @brief SPI2 外设初始化
* @param None
* @retval None
*/
void SPI2_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
RCC_AHB1PeriphClockCmd(SPI2_MOSI_CLK|SPI2_SCK_CLK,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2,ENABLE);
GPIO_PinAFConfig(SPI2_MOSI_PORT,SPI2_MOSI_PINSOURCE,SPI2_MOSI_AF);
GPIO_PinAFConfig(SPI2_SCK_PORT,SPI2_SCK_PINSOURCE,SPI2_SCK_AF);
/* MOSI-PB15 SCK-PB10 */
GPIO_InitStructure.GPIO_Pin = SPI2_MOSI_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 速度 50MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // 推挽复用输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(SPI2_MOSI_PORT,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = SPI2_SCK_PIN;
GPIO_Init(SPI2_SCK_PORT,&GPIO_InitStructure);
RCC_APB1PeriphResetCmd(RCC_APB1Periph_SPI2,ENABLE);  // 复位 SPI2
RCC_APB1PeriphResetCmd(RCC_APB1Periph_SPI2,DISABLE); // 停止复位 SPI2
/* SPI 工作参数配置 */
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;// 双线全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主机模式
SPI_InitStructure.SPI_DataSize =SPI_DataSize_8b; // 数据帧长度 8bit
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; // 空闲时 SCK 高电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;// 在 SCK 的偶数边沿采集数据
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 片选线由软件管理
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;//2 分频
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 高位在前
SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC 多项式
SPI_Init(SPI2,&SPI_InitStructure);
SPI_Cmd(SPI2,ENABLE);
}
/**
* @brief SPI2 发送与接收数据
* @param TxData:  要发送的数据
* @retval  通过 SPI2 接收到的数据
*/
uint8_t SPI2_ReadWriteByte(uint8_t TxData)
{
uint8_t retry = 0;
/*  检查发送缓存空标志位 TXE */
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET)
{
retry++;
if(retry>200)  return 0;
}
SPI_I2S_SendData(SPI2, TxData); // 通过外设 SPI2 发送数据
retry=0;
/*  检查接收缓存非空标志位 RXNE */
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET)
{
retry++;
if(retry>200) return 0;
}
return SPI_I2S_ReceiveData(SPI2);  // 返回 SPI2 接收的数据
}

3.编写 OLED 显示模块初始化程序和字符显示程序
在“HARDWARE”文件夹下新建名为“OLED”的子文件夹,新建“oled.c”和“oled.h”
两个文件,将它们加入工程中,并配置头文件包含路径。在“oled.c”文件中编写 OLED 显示模
块初始化程序和字符显示程序,具体函数清单如表 4-4-6 所示。在“oled.h”文件中编写 OLED
显示模块主要引脚的宏定义和函数声明。

首先在“oled.h”文件中输入以下代码:?

#ifndef __OLED_H
#define __OLED_H
#include "sys.h"
/* OLED CS(NSS) PA3 */
#define OLED_CS_CLK RCC_AHB1Periph_GPIOA
#define OLED_CS_PORT  GPIOA
#define OLED_CS_PIN GPIO_Pin_3
#define OLED_CS_LOW GPIO_ResetBits(OLED_CS_PORT,OLED_CS_PIN)
#define OLED_CS_HIGH  GPIO_SetBits(OLED_CS_PORT,OLED_CS_PIN)
/* OLED DC(Command or Data) PA12 */
#define OLED_DC_CLK RCC_AHB1Periph_GPIOA
#define OLED_DC_PORT  GPIOA
#define OLED_DC_PIN GPIO_Pin_12
#define OLED_DC_LOW GPIO_ResetBits(OLED_DC_PORT,OLED_DC_PIN)
#define OLED_DC_HIGH  GPIO_SetBits(OLED_DC_PORT,OLED_DC_PIN)
/* OLED RST PA11 */
#define OLED_RST_CLK  RCC_AHB1Periph_GPIOA
#define OLED_RST_PORT GPIOA
#define OLED_RST_PIN  GPIO_Pin_11
#define OLED_RST_LOW  GPIO_ResetBits(OLED_RST_PORT,OLED_RST_PIN)
#define OLED_RST_HIGH GPIO_SetBits(OLED_RST_PORT,OLED_RST_PIN)
void OLED_Write_Data(u8 data);
void OLED_Write_Cmd(u8 cmd);
void OLED_GPIO_Config(void);
void OLED_Init(void);
void OLED_Set_Pos(u8 x,u8 y);
void OLED_DrawPoint(u8 x,u8 y,u8 mode);
void OLED_Display_Clear(void);
void OLED_Display_Onechar(u8 x,u8 y,u8 chr,u8 size,u8 mode);
void OLED_Display_String(u8 x,u8 y,char *p, u8 size);
#endif

然后在“oled.c”文件中输入以下代码:

#include "delay.h"
#include "oled.h"
#include "bsp_spi.h"
#include "oledfont.h" // 该文件为字库文件
/*
定义 OLED 的显存 , 存放格式如下
[Page0]0 1 2 3 ... 127 // 第 0 页
[Page1]0 1 2 3 ... 127 // 第 1 页
[Page2]0 1 2 3 ... 127 // 第 2 页
[Page3]0 1 2 3 ... 127 // 第 3 页
[Page4]0 1 2 3 ... 127 // 第 4 页
[Page5]0 1 2 3 ... 127 // 第 5 页
[Page6]0 1 2 3 ... 127 // 第 6 页
[Page7]0 1 2 3 ... 127 // 第 7 页
*/
uint8_t OLED_GRAM[128][8];
/**
* @brief 向显示驱动芯片写入数据
* @param data :要写入的数据
* @retval None
*/
void OLED_Write_Data(u8 data)
{
OLED_CS_LOW;
OLED_DC_HIGH;  //DC 引脚高电平,写入数据
SPI2_ReadWriteByte(data);  // 调用硬件 SPI 写入 1 个字节
}
/**
* @brief 向显示驱动芯片写入命令
* @param Cmd :要写入的命令
* @retval None
*/
void OLED_Write_Cmd(u8 cmd)
{
OLED_CS_LOW;
OLED_DC_LOW; //DC 引脚低电平,写入命令
SPI2_ReadWriteByte(cmd); // 调用硬件 SPI 写入 1 个字节
}
/**
* @brief 将要显示的数据写入 SSD1306 的 GDDRAM
* @param None
* @retval None
*/
void OLED_Refresh_Gram(void)
{
uint8_t page,column;
for(page=0; page<8; page++)
{
OLED_Write_Cmd(0xB0+page); // 设置页地址 (0~7)
OLED_Write_Cmd(0x00);  // 设置列低 4 位地址
OLED_Write_Cmd(0x10);  // 设置列高 4 位地址
for(column=0; column<128; column++)
OLED_Write_Data(OLED_GRAM[column][page]);
}
}
/**
* @brief 初始化 OLED 控制相关 GPIO 引脚
* @param None
* @retval None
*/
void OLED_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(OLED_CS_CLK|OLED_DC_CLK|OLED_RST_CLK,ENABLE);
/* CS(NSS)-PA3 | DC(Data or Command)-PA12 | RST-PA11 */
GPIO_InitStructure.GPIO_Pin = OLED_CS_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(OLED_CS_PORT,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = OLED_DC_PIN;
GPIO_Init(OLED_DC_PORT,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = OLED_RST_PIN;
GPIO_Init(OLED_RST_PORT,&GPIO_InitStructure);
}
/**
* @brief OLED 显示模块初始化
* @param None
* @retval None
*/
void OLED_Init(void)
{
OLED_GPIO_Config(); // 初始化 OLED 控制相关引脚
OLED_RST_HIGH; // 硬件复位
delay_ms(100);
OLED_RST_LOW;
delay_ms(100);
OLED_RST_HIGH;
OLED_Write_Cmd(0xAE); // 关闭显示
OLED_Write_Cmd(0x20); // 设置存储器寻址模式
OLED_Write_Cmd(0x02); // 页寻址模式
OLED_Write_Cmd(0xA8); // 设置行扫多路系数
OLED_Write_Cmd(0x3F);
OLED_Write_Cmd(0xD3);  // 设置行扫偏移 (0x00~0x3F)
OLED_Write_Cmd(0x00);  //not offset
OLED_Write_Cmd(0x40|0x00); // 设置显示起始行 (0x00~0x3F)
OLED_Write_Cmd(0xA1);  // 设置段重映射 0xa0 左右反置 0xa1 正常
OLED_Write_Cmd(0xC8);  // 设置行扫方向 0xc0 上下反置 0xc8 正常
OLED_Write_Cmd(0xD9);  // 设置充放电周期
OLED_Write_Cmd(0xF1);  // 充电周期 15Clocks &  放电周期 1Clock
OLED_Write_Cmd(0xDA);  // 设置行扫方式
OLED_Write_Cmd(0x12);
OLED_Write_Cmd(0x81);  // 设置背光对比度
OLED_Write_Cmd(0x7F);  // 默认是 0x7F(0x00~0xFF)
OLED_Write_Cmd(0xA4);  // 设置非全屏显示 (0xa4/0xa5)
OLED_Write_Cmd(0xA6);  // 设置正常显示 (0xa6/a7)
OLED_Write_Cmd(0xD5);  // 设置时钟分频
OLED_Write_Cmd(0x80);
OLED_Write_Cmd(0x8D);  // 打开内置升压泵
OLED_Write_Cmd(0x14);  //0x14  打开, 0x10  关闭
OLED_Write_Cmd(0xDB);  // 反向截止电压设置
OLED_Write_Cmd(0x40);
OLED_Write_Cmd(0xAF);  // 打开 OLED 面板显示
OLED_Display_Clear();  // 清屏
}
/**
* @brief OLED 清屏函数
* @param None
* @retval None
*/
void OLED_Display_Clear(void)
{
u8 i,n;
for(i=0; i<8; i++)
for(n=0; n<128; n++)
OLED_GRAM[n][i]=0x00;
OLED_Refresh_Gram();  //OLED 显存写入全 0
}
/**
* @brief OLED 定位函数
* @param x :列地址 (0~127) , y :行地址 (0~63)
* @retval None
*/
void OLED_Set_Pos(u8 x,u8 y)
{
OLED_Write_Cmd(0xB0 + y/8); // 设置页指针
OLED_Write_Cmd(x&0x0F); // 列低 4 位地址
OLED_Write_Cmd(((x&0xF0)>>4)|0x10); // 列高 4 位地址
}
/**
* @brief 在指定位置画点函数
* @param x: 列地址 (0~127) , y: 行地址 (0~63)
* @param mode:1 填充, 0 清空
* @retval None
*/
void OLED_DrawPoint(u8 x,u8 y,u8 mode)
{
u8 page,bx,temp=0;
OLED_Set_Pos(x,y);
if(x>127||y>63) return; // 超出范围
page=y/8;
bx=y%8;
temp=1<<bx;
if(mode) OLED_GRAM[x][page]|=temp;
else OLED_GRAM[x][page]&=~temp;
/*  写入显示缓存 */
OLED_Write_Data(OLED_GRAM[x][page]);
}
/**
* @brief 在指定位置显示字符
* @param x :列地址 (0~127) , y :行地址 (0~63)
* @param chr :要显示的字符 | size :字体大小 12/16/24
* @param mode : 1 填充, 0 清空
* @retval None
*/
void OLED_Display_Onechar(u8 x,u8 y,u8 chr,u8 size,u8 mode)
{
u8 temp,t,t1;
u8 y0=y;
u8 csize=(size/8+((size%8)?1:0))*(size/2);
chr=chr-' '; // 得到偏移后的值
for(t=0; t<csize; t++)
{
if(size==12)
temp = oled_asc2_1206[chr][t];  // 调用 1206 字体
else if(size==16)
temp = oled_asc2_1608[chr][t];  // 调用 1608 字体
else if(size==24)
temp = oled_asc2_2412[chr][t];  // 调用 2412 字体
else return; // 字库中不存在相关字体    
for(t1=0; t1<8; t1++)
{
if(temp & 0x80)
OLED_DrawPoint(x, y, mode);
else
OLED_DrawPoint(x, y, !mode);
temp <<= 1;
y++;
if((y-y0) == size)
{
y = y0; x++; break;
}
}
}
}
/**
* @brief 在指定位置显示字符串
* @param x :列地址 (0~127) , y :行地址 (0~63)
* @param *p :要显示的字符串起始地址
* @param size :字体大小 12/16/24
* @retval None
*/
void OLED_Display_String(u8 x,u8 y,char *p, u8 size)
{
while((*p <= '~') && (*p >= ' '))// 判断是否为非法字符
{
if(x > (128-(size/2)))
{
x = 0;
y += size;
}
if(y > (64-size))
{
y = x = 0;
OLED_Display_Clear();
}
OLED_Display_Onechar(x,y,*p,size,1);
x += size/2;
p++;
}
}

4.编写 OLED 显示模块显示日历功能的函数
在任务 4.3 中,我们已完成了 STM32F4 系列微控制器的 RTC(实时时钟)的应用开发。我
们可继续沿用已编写好的 RTC 初始化程序,同时根据本任务的“日历显示”功能要求,增加相
应的显示函数与其他代码。在“bsp_rtc.c”文件中增加以下代码:

#include "bsp_rtc.h"
#include "oled.h"
#include "usart.h"
char DateShow[50],TimeShow[50];
void OLED_Show_DateTime(void);
/**
* @brief 显示日期和时间
* @param None
* @retval None
*/
void RTC_Show_DateTime(void)
{
char WeekDay[4]; // 用于存放星期的缩写,如 Mon
RTC_TimeTypeDef RTC_TimeStructure;
RTC_DateTypeDef RTC_DateStructure;
/*  获取日历 */
RTC_GetTime(RTC_Format_BIN, &RTC_TimeStructure);
RTC_GetDate(RTC_Format_BIN, &RTC_DateStructure);
/*  将 RTC 获取到的“星期”参数转化为相应的英文缩写 */
switch(RTC_DateStructure.RTC_WeekDay)
{
case 1: // 星期一
WeekDay[0]='M';WeekDay[1]='O';WeekDay[2]='N';
break;
case 2:  // 星期二
WeekDay[0]='T';WeekDay[1]='U';WeekDay[2]='E';
break;
case 3:  // 星期三
WeekDay[0]='W';WeekDay[1]='E';WeekDay[2]='D';
break;
case 4: // 星期四
WeekDay[0]='T';WeekDay[1]='H';WeekDay[2]='U';
break;
case 5: // 星期五
WeekDay[0]='F';WeekDay[1]='R';WeekDay[2]='I';
break;
case 6: // 星期六
WeekDay[0]='S';WeekDay[1]='A';WeekDay[2]='T';
break;
case 7: // 星期日
WeekDay[0]='S';WeekDay[1]='U';WeekDay[2]='N';
break;
}
/*  串口显示日期 */
printf("The Date : Y:20%0.2d - M:%0.2d - D:%0.2d - W:%0.2d\r\n",
RTC_DateStructure.RTC_Year,
RTC_DateStructure.RTC_Month,
RTC_DateStructure.RTC_Date,
RTC_DateStructure.RTC_WeekDay);
/*  使用 OLED 显示日期 */
sprintf(DateShow,"20%0.2d-%0.2d-%0.2d %s",
RTC_DateStructure.RTC_Year,
RTC_DateStructure.RTC_Month,
RTC_DateStructure.RTC_Date,
WeekDay);
/*  串口显示时间 */
printf("The Time : %0.2d:%0.2d:%0.2d \r\n\r\n",
RTC_TimeStructure.RTC_Hours,
RTC_TimeStructure.RTC_Minutes,
RTC_TimeStructure.RTC_Seconds);
/*  使用 OLED 显示时间 */
sprintf(TimeShow,"%0.2d:%0.2d:%0.2d",
RTC_TimeStructure.RTC_Hours,
RTC_TimeStructure.RTC_Minutes,
RTC_TimeStructure.RTC_Seconds);
OLED_Show_DateTime();  // 使用 OLED 显示日期时间
}
/**
* @brief OLED 显示日期和时间
* @param None
* @retval None
*/
void OLED_Show_DateTime(void)
{
OLED_Display_String(32,0,"Calendar",16);
OLED_Display_String(4,32,DateShow,16);
OLED_Display_String(32,48,TimeShow,16);
}

在“bsp_rtc.h”文件中可增加函数声明。
5.编写 main()函数
在“main.c”文件中输入以下代码:

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "key.h"
#include "exti.h"
#include "dht11.h"
#include "bsp_iic.h"
#include "bh1750.h"
#include "bsp_spi.h"
#include "bsp_rtc.h"
#include "oled.h"
uint16_t bh1750Light = 0; // 采集的光照值,单位 lx
uint8_t temperature = 0; // 采集的温度值,单位℃
uint8_t humidity = 0; // 采集的湿度值
char tempString[50], humiString[50], lightString[50];
uint8_t refresh_flag = 2, keyValue = 0;
void Show_TempHumiLight(void);
int main(void)
{
delay_init(168); // 延时函数初始化
LED_Init(); //LED 端口初始化
Key_Init(); // 按键端口初始化
EXTIx_Init();  // 外部中断初始化
USART1_Init(115200);  //USART1 初始化
IIC_Init(); //IIC 总线初始化
BH1750_Init(); //BH1750 初始化
DHT11_Init();  //DHT11 初始化
SPI2_Init(); //SPI2 外设初始化
OLED_Init(); //OLED 显示模块初始化
RTC_CLK_Config(1); //RTC 配置,时钟源选择 LSE
while(DHT11_Init()) //DHT11 初始化
{
printf("DHT11 Init Error!\r\n");
delay_ms(500);
} 
printf("DHT11 Init Success!\r\n");
/*  若备份区域读取的值不对 */
if (RTC_ReadBackupRegister(RTC_BKP_DR0) != 0x5F5F)
{
RTC_Set_DateTime(); // 设置时间和日期
}
else
{
printf("\r\n  不需要重新配置 RTC …… \r\n");
/*  使能 PWR  时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
/* PWR_CR:DBF 置 1 ,使能 RTC 、 RTC 备份寄存器和备份 SRAM 的访问 */
PWR_BackupAccessCmd(ENABLE);
/*  等待 RTC APB  寄存器同步 */
RTC_WaitForSynchro();
}
while(1)
{
if(keyValue == KEY_D_PRESS) // 如果“下键”按下
{
if(refresh_flag == 1)
{
refresh_flag = 2;
OLED_Display_Clear(); // 刷一次屏
}
RTC_Show_DateTime(); // 显示时间和日期
}
else // 如果“下键”没有按下
{
bh1750Light = BH1750_Measure(); // 读取 BH1750 的光照强度值
DHT11_Read_Data(&temperature,&humidity);// 读取 DHT11 的温湿度值
/*  组合需要显示的信息 */
sprintf(lightString,"Light:%0.5d",bh1750Light);
printf("%s",lightString); 
sprintf(tempString,"Temp:%d",temperature);
printf("%s",tempString);
sprintf(humiString,"Humi:%d",humidity);
printf("%s",humiString);
if(refresh_flag == 2)
{
refresh_flag = 1;
OLED_Display_Clear(); // 刷一次屏
}
Show_TempHumiLight(); // 显示环境参数
}
LED1 = ~LED1;delay_ms(500);
}
}
/**
* @brief OLED 显示环境参数 ( 温度 / 湿度 / 光照强度 )
* @param None
* @retval None
*/
void Show_TempHumiLight(void)
{
OLED_Display_String(20,0,"Environment",16);
OLED_Display_String(20,16,tempString,16);
OLED_Display_String(20,32,humiString,16);
OLED_Display_String(20,48,lightString,16);
}

  嵌入式 最新文章
基于高精度单片机开发红外测温仪方案
89C51单片机与DAC0832
基于51单片机宠物自动投料喂食器控制系统仿
《痞子衡嵌入式半月刊》 第 68 期
多思计组实验实验七 简单模型机实验
CSC7720
启明智显分享| ESP32学习笔记参考--PWM(脉冲
STM32初探
STM32 总结
【STM32】CubeMX例程四---定时器中断(附工
上一篇文章      下一篇文章      查看所有文章
加:2022-07-17 16:39:38  更:2022-07-17 16:41:14 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/18 14:08:07-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码