目录
IIC协议简介
IIC总线系统结构
IIC总线物理层特点
IIC总线协议层
空闲状态
应答信号
数据的有效性
数据传输
STM32的IIC特性及架构
STM32的IIC结构体
0.96寸OLED显示屏
SSD1306框图及引脚定义
4针脚I2C接口模块原理图
字节传输-I2C
执行逻辑框图
命令表
硬件I2C的配置
软件I2C的配置
温湿度传感器
OELD显示温湿度项目
IIC协议简介
IIC协议协议简介
IIC通讯协议(Inter----Integrted Circuit)是由Phiips飞利浦公司开发的,
由于他引脚少,硬件实现简单,可拓展性强,不需要UASRT,CAN通讯协议的外部收发设备,现在被广泛使用在系统内多个集成电路IC(芯片)间的通讯。
半双工的通讯方式
引脚很少 SDA SCL一个数据一个时钟,和串口相比要一个芯片转换
IIC总线系统结构
他是一个支持多设备的总线。”总线”指多个设备共用的信号线,在一个IIC通讯总线中,可连接多个IIC通讯设备,支持多个通讯主机及多个通讯从机。主机就是MCU从机就是外部设备
一个IIC总线只使用两条总线线路,一条双向串行数据线(SDA),一条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。
每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备直接的访问。
IIC总线物理层特点
总线通过上拉电阻接到电源。当IIC设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态,由上拉电阻把总线拉成高电平。
多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定哪个设备占用总线。仲裁相当于中断优先级
具有三种传输模式:标准模式传输速率为100kbit/s,快速模式为400kbit/s,高速模式下可达3.4M/s,但目前大多IIC设备尚不支持高速模式。
IIC总线
硬件IIC:对应芯片上的IIC外设,有相对应的IIC驱动电路,其所使用的IIC管教也是专用的;
软件IIC:一般是用GPIO管教,用软件控制管脚状态以及模拟IIC通信波形;
区别:硬件IIC的效率要远高于软件的,而软件IIC不受引脚限制,接口比较灵活。
软件IIC是通过GPIO,软件模拟寄存器的工作方式,而硬件IIC是直接调用内部寄存器进行配置。如果要从具体硬件上来看,可以去看下芯片手册。因为固件IIC的端口是固定的,所以会有所区别。
如何区分?
1.硬件IIC用法复杂,模拟IIC流程更加清楚
2.硬件IIC速度比模拟快,并且可以用DMA
3.模拟IIC可以在任何管脚上,硬件IIC在固定管脚上
IIC总线协议层
IIC协议层
IIC的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。
1,IIC基本读写过程
主机写数据到从机
S:数据由主机传输至从机 SLAVE ADDRESS : 从机地址 P:数据传输结束
起始信号产生后,所有从机就开始紧接下来广播的从机地址信号。IIC总线,每个设备的地址都是唯一的,当主机广播的地址与某个设备的地址相同时,这个设备就被选中了,没被选中的设备讲会忽略之后的数据信号。根据IIC协议,这个从机地址可以是7位或10位。
地址位之后,传输方向RW选择位,为0:表示数据传输方向是由主机传输至从机,即主机向从机写数据。为1:则相反。 从机接收传输方向选择位后,主机或从机会返回一个应答A(ACK)或非应答A/A(NACK)信号,只有接收到应答信号后,主机才能继续发送或接收数据。
主机读数据到从机
读数据:
配置方向传输位为”读数据”方向。广播完地址后,接收到应答信号后,从机开始向主机返回数据(DATA),数据包大小也为8位,从机每发送完一个数,都会等待主机的应答信号(ACK),重复这个过程,可以返回N个数据,N没有限制大小。当主机希望停止接收数据时,就向从机返回一个非应答信号(NCAK),则从机自动停止数据传输。
通讯复合格式
复合格式,该传输过程有两次起始信号(S),在第一次传输过程中,主机通过SLAVE_ADDRESS寻找到从设备后,发送一段”数据”,这段数据通常用于表示从设备内部的寄存器或存储器地址;第二次传输中,对该地址的内容进行读或写。也就是说,第一次通讯是告诉从机读写地址,第二次则是读写的实际内容。
空闲状态
IIC总线的SDA和SCL两条信号线同时处于高电平时,规定位总线的空闲状态。
开始信号停止信号
起始信号:当SCL为高电平期间,SDA有高到低的跳变;启动信号是一种电平跳变时序信号,而不是一个电平信号。
停止信号:当SCL为高电平期间,SDA由低到高的跳变;停止信号也是一种高电平跳变时序信号,而不是一个电平信号
起始信号和停止信号一般由主机产生
应答信号
发送器每发送一个字节,就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ACK简称应答位)
表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。
对于反馈有效应答位ACK的要求是,接收器在第九个时钟脉冲之前的低电平期间将SDA线拉低,并且确保在该时钟的高电平期间为稳定的低电平。
数据的有效性
IIC总线进行数据传输时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。SDA数据线在SCL的每个时钟周期传输一位数据。
如果数据总线正在发送1然后想变为发送0只能在时钟总线的低电平的时候发生改变
即:数据在SCL的上升沿到来之前就需准备好。并在下降沿到来之前必须稳定
数据传输
在IIC总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据。数据位的传输是边沿触发。
STM32的IIC特性及架构
软件模拟协议:使用CPU直接控制通讯引脚的电平,产生出符合通讯协议标准的逻辑。
硬件实现协议:由STM32的IIC片上外设专门负责实现IIC通讯协议,只要配置好该外设,它就会自动根据协议要求产生通讯信号,收发数据并缓存起来,CPU只要检测该外设的状态和访问数据寄存器,就能完成数据收发。这种由硬件外设处理IIC协议的方式减轻了CPU的工作,且使软件设计更加简单。
STM32的IIC外设可用作通讯的主机及从机,支持100Kbit/s和400Kbits/s的速率,支持7位、10位设备地址,支持DMA数据传输,并具有数据校验功能。
通讯引脚、时钟控制逻辑、数据控制逻辑、整体控制逻辑
1.通讯引脚
STM32芯片有多个IIC外设,它们的IIC通讯信号引出到不同的GPIO引脚上,使用时必须配置这些指定的引脚。
2.时钟控制逻辑
SCL线的时钟信号,由IIC接口根据时钟控制寄存器(CCR)控制,控制的参数主要位时钟频率。
可选择IIC通讯的“标准/快速”模式,这两个模式分别对应100/400Kbits/s的通讯速率。
在快速模式下可选择SCL时钟的占空比,可选T(low)/T(high) = 2或T(low)/T(high)=16/9模式。
CCR寄存器中12位的配置因子CCR,它与IIC外设的输入时钟源共用作用,产生SCL时钟。STM32的IIC外设输入时钟源位PCKL1.
如果是标志模式下T(high)=T(low)的,一个周期等于T(high)+T(low),所以说占空比T(high)/T(high)+T(low)等于百分之50
计算时钟频率
标准模式
T high = CCR *T pckl1 T low= CCR*Tpclk1
快速模式中 Tlow/Thigh =2时:
Thigh = CCR*Tpckl1 T low = 2*low*Tpckl1
快速模式中 Tlow/Thigh =16/9时:
Thigh = 9*CCR*Tpckl1 T low = 16*low*Tpckl1
PCLK1 = 36MHz,想要配置400Kbits/s 方法:
PCLK时钟周期: TPCLK1 = 1/36 000 000(周期等于频率的分之一)
目标SCL线时钟周期: TSCL = 1/400 000
因为在快速模式下Tlow/Thigh =2,所以Tlow=2*Thigh
SCL时钟周期内的高电平时间: Thigh = TSCL/3
SCL时钟周期内的低电平时间: Tlow = 2*TSCL/3
计算CCR的值 : CCR = THIGH/TPCLK1 = 30
计算出来的值写入到寄存器即可
3.数据控制逻辑
IIC的SDA信号主要连接到数据移位寄存器上,数据移位寄存器的数据来源及目标是数据寄存器(DR)、地址寄存器(OAR)、PEC寄存器以及SDA数据线。
·当向外发送数据的时候,数据移位寄存器以“数据寄存器”为数据源,把数据一位一位地通过SDA信号线发送出去。(是从低位发送还是从高位发送呢由SMBA控制,一般是高位先行)
·当从外部接收数据的时候,数据移位寄存器把SDA信号线采样到的数据一位一位地存储到”数据寄存器”中。
使用IIC外设通讯时,在通讯的不同阶段它会对”状态寄存器(SR1和SR2)”的不同数据位写入参数,通过读取这些寄存器标志来了解通讯状态。
1.主发送器
控制产生起始信号(S),当发生起始信号后,它产生事件”EV5”,并会对SR1寄存器的 SB 位置1(通过配置我们的寄存器),表示起始信号已经发生。 发生设备地址并等待应答信号,若有从机应答,则产生时间 EV6 及 EV8 ,这时SR1寄存器的 ADDR位及 TXE位被置1,ADDR位1 表示地址已经发送,TEX表示数据寄存器为空。
·往IIC的数据寄存器DR写入要发送的数据,这时TXE位会被充值0,表示数据寄存器非空,IIC外设通过SDA信号线一位位把数据发送出去后,又会产生EV8事件,即TXE被置1,重复这个过程,可发送多个字节。
·发送数据完成后,控制IIC设备产生一个停止信号P,这个时候产生EV2事件,SR1的TEX位及BTF位被置1,表示通讯结束。
STM32的IIC结构体
typedef struct{uint32_t I2C_ClockSpeed; //设置SCL时钟频率,此值要低400 000uint16_t I2C_Mode; //指定工作模式,可选IIC模式及SMBUS模式uint16_t I2C_DutyCycle; //时钟占空比,可选low/high = 2:0或16:9uint16_t I2C_OwnAddress1; //自身的IIC设备地址 uint16_t I2C_Ack; //使能或者关闭响应,一般是使能uint16_t I2C_AcknowledgedAddress; //指定地址长度,可为7或10}I2C_InitTypeDef;
uint32_t I2C_ClockSpeed; //设置SCL时钟频率,此值要低于400 000
I2C_ClockSpeed
设置IIC的传输速率,在调用初始化函数时,函数会根据我们输入的数值经过运算后把时钟因子写入到IIC的时钟控制寄存器CCR。而我们写入的这个参数值不得高于400Khz.
实际上由于CCR寄存器不能写入小数类型的时钟因子,影响到SCL的实际频率可能会低于本成员设置的参数值,这时除了通讯会稍微慢点以外,不会对IIC的标准通讯造成其他影响。
根据频率比较会把结构体的值写入相对于的寄存器里相对于的位
uint16_t I2C_Mode; //指定工作模式,可选IIC模式SMBUS模式
选择IIC的使用方式,有IIC模式(IIC_Mode_IIC)和SMBus主、从模式(IIC_Mode_SMBusHost、IIC_Mode_SMBusDevice)
IIC不需要在此处区分主从模式,直接设置IIC_Mode_IICj即可
uint16_t I2C_DutyCycle; //时钟占空比,可选low/high = 2:0或16:9
·I2C_DutyCycle
设置IIC的SCL线时钟的占空比。该配置有两个选择,分别为低电平时间比高电平时间为2:1(IIC_DutyCycle_2)和16:9(IIC_DutyCycle_16_9).
其实这两个模式的比例差别并不大,一般要求都不会如此严格,这里随便选就可以了。
uint16_t I2C_OwnAddress1; //自身的IIC设备地址
·I2C_OwnAddress1
配置STM32的IIC设备自己的地址,每个连接到IIC总线上的设备都有一个自己的地址,作为主机也不例外。地址可以设置为7位或10位(受下面IIC_AcknowledgeAddress成员决定),只要该地址是IIC总线上唯一的即可。
STM32的IIC外设可同时使用两个地址,即同时对两个地址作出响应,这个结构体成员IIC_OwnAddress1配置的是默认的,OAR1寄存器存储的地址,若需要设置第二个地址寄存器OAR2,可使用IIC_OwmAddress2Conig函数来配置,OAR2不支持10位地址。主从设备在通信的时候另外也可以进行其他的活动操作,所以使用起来非常灵活
uint16_t I2C_Ack; //使能或者关闭响应,一般是使能
·I2C_Ack
配置IIC应答是否使能,设置位使能则可以发送响应信号,一般配置位允许应答(IIC_Ack_Enable),这是绝大多数遵循IIC标准的设备的通讯要求,改为禁止应答(IIC_Ack_Disable)往往会导致通讯错误。
uint16_t I2C_AcknowledgedAddress; //指定地址长度,可为7或10
·I2C_AcknowledgedAddress
选择IIC的寻址模式是7位或者是10位地址,这需要根据实际连接到IIC总线上设备的地址进行选择,这个成员的配置也影响到IIC_OwnAddress成员,只有这里设置成10位模式时,IIC_OwnAddress1才支持10位地址
配置完这些结构体成员的值,调用库函数IIC_Init就可以把结构体的配置写入到对应的寄存器中了。
0.96寸OLED显示屏
适用器件:
0.96寸OLED显示屏(驱动芯片:SSD1306 / SSD1315)
1.3寸OLED显示屏(驱动芯片:SH1106)
4针脚I2C接口
7针脚SPI接口
128*64像素
像素颜色不限
•SSD1306是一款OLED(有机发光二极管)/PLED(高分子发光二极管)点阵显示屏的控制器,可以嵌入在屏幕中,用于执行接收数据、显示存储、扫描刷新等任务
•驱动接口:128个SEG引脚和64个COM引脚,对应128*64像素点阵显示屏
•内置显示存储器(GDDRAM):128*64 bit (128*8 Byte)SRAM
•供电:VDD=1.65~3.3V(IC 逻辑),VCC=7~15V(面板驱动)
•通信接口:8位6800/8080并行接口,3/4线SPI接口,I2C接口
第一步我们通过引出来的通信引脚把我们想显示的内容发给驱动芯片,第二步驱动芯片收到数据然后把它存起来也就是芯片内部,自带的有RAM显示存储器,存储的是屏幕显示的内容,第三步驱动芯片内部有时钟和扫描电路,根据显示存储器的数据,它会自动对应刷新到屏幕,这样我们指定的内容就呈现在屏幕上了,这些步骤,我们只需要关心,如何通过通信协议把数据写入到屏幕里的显示存储器即可
SSD1306框图及引脚定义
收到一个字节之后首先要在MCU进行分流,发一个字节会指定,这个字节是作为数据还是作为命令,其中命令会进入到下面,命令译码器用于内部电路的执行,比如开启显示,关闭显示,设置对比度,设置显示起始位置,芯片内有一个命令表,参考命令表就知道这个命令是什么功能了
数据会进入到右边GDDRAM,定义为数据的字节决定的是屏幕上显示什么内容了,只有我们把想显示的内容写入到GDDRAM里的对应位置,在屏幕上就会显示对应的内容,所以我们操作显示屏只需要关心如何将数据写入到GDDRAM的正确位置就行了
显示控制器Display Counter的任务就是读取GDDRAM把里面的内容扫描刷新到屏幕上,所以控制器的输出就是显示屏点阵的驱动器了,
中间是短驱动器输出引脚是。。。上下两端是公共驱动器阶段点阵的64行
振荡器Oscillator用于提供刷新屏幕的时钟,CL是时钟输入脚,CLS是时钟源选择脚,这个芯片内部自带时钟,但也可以通过CLS选择,CLS接高电平,选择内容时钟,CL引脚不用,CLS接低电平,选择外部时钟,CL加一个外部时钟源,一般用内部时钟
电流控制和电压控制,VCOMEH一般是公共端的电压输出,一般在外面接一个电源滤波电容就行了,IREF是驱动电流控制,在外面接个电阻,可以控制驱动屏幕的电流
4针脚I2C接口模块原理图
字节传输-I2C
首先S起始条件,起始之后主机需要发送一个字节,并且必须是7位从机地址+1位读写位的格式,串行模式只能写入,所以RW位只给0,用于I2C寻址,用于在多从机的情况下,指定操作哪个从机,发送一个字节后需要接受应答,判断一下,从机是不是收到了,寻址完后就可以进入命令或数据的发送了,I2C没有单独的DC引脚,如何判断是命令还是数据呢,这里再发送Data byte之前要先发送一个Control byte用于指定后面的有效字节是命令还是数据,DC指定1或0表示是命令还是数据的选择
Co置1表示在每个Data byte都加一个Control byte, 这样可以对每一个Data byte 指定是命令还是数据,命令和数据可以来回切换,Co置0表示先来一个Control byte之后的全部都是Data byte,要么是命令要么是数据,不能来回切换,最后p停止,而且每发送一个字节都有应答位(高位先行模式)
执行逻辑框图
1.左上角为原点,向右为X轴坐标是0-127,向下为Y轴坐标是0-63,中间是所有的像素点,目前是单色屏幕,所以每个像素点只有亮灭两种状态,用一个bit,1或0就可以控制一个像素点
下图是驱动芯片内部的显示存储器128*64位即128*8字节的GDDRAM,这个GDDRAM和屏幕像素点是一一映射的关系,其X轴坐标也是0-127,但是Y轴有所不同,虽然Y轴总共也是64个坐标,但是Y轴会8个坐标为一组进行分页,坐标范围是PAGE0-7总共分为8页,为什么要进行分页?因为屏幕上只需要一个bit就可以控制一个像素点了,但是存储器的组织一般都是以字节为单位的,而且我们使用写数据的时序,一次性也是发过来一个字节,一个字节有8bit,如果8位只显示一个像素点太浪费了,所以这里会把字节的8位展开竖向排列,一个位控制一个像素点,是LSB低位在上MSB高位在上,所以说我们每一次写入一个数据都会控制纵向的8个像素点,既然是一起控制的,那么Y轴就必然会8位为一组,进行分页,这样做的好处就是充分利用一个字节8位,一次性写入8个像素点,效率也比较高,坏处是Y轴坐标只能8个为一组进行指定,不能在0-63任意指定(有解决方法)
2.怎么写入数据?比如我们先设定页地址为PAGE0,列地址设定为8,就指定了起始坐标是这个字节的位置,然后我们写入数据0X55(01010101)写入完成后驱动芯片会自动根据GDDRAM的数据,扫描点阵屏,所以上面在点阵屏的对应位置会写入一个小竖条,1的地方点亮,0的地方熄灭,接着写一个字节后内部的地址指针会向右移动一个单位,所以我们再写入一个数据比如0XAA(10101010)
3.比如我们想在屏幕的右上角,显示字符A,操作流程是使用写命令的函数第一步设置页地址为PAGE0,第二步设置页地址为122,这时当前的地址指针指向这个字节,然后使用写数据的函数写入0x00当前字节就变成了0x00写完之后地址指针自动右移继续写,连续写完6个字节而且都竖向排列后,存储器内部的数据就是这样的,现在写入到了最后一个字节的位置了,如果继续写的话默认会回到此页的最开头,当然也可以配置自动换页,换到下一页的开头,这个可以通过配置寻址模式来实现,这样对应的屏幕就显示一个字符A,这串数据可以提前存到数组里,这是6x8点阵,字符A的字模数据,当我们想显示A时就写入这一串数据就行了,如果我们想把A 往左移动一个像素,设置列地址时为121即可,往下移很难,如果指定页地址为PAGE1那么其实是往下移动了8个像素,这时得把A的字模切成两半,一半写入PAGE0的下部分,一半写入PAGE1的上部分,如果想实现Y轴任意指定或者更高级的绘图功能,那就得有读取GDDRAM的能力,并行模式可以读取,串行模式下读不了(但是可以在程序中定义缓存数组实现,先读写缓存数组,最后在一起更新到屏幕的GDDRAM里)
4.运行流程:如何指定坐标如何指定显示数据呢,首先是通信接口,它的作用是接受一个字节,一个字节进来后通过DC引脚指定这个字节是命令还是数据
命令表
第一个是双字节命令先写一个命令0X81,表示设置对比度紧跟着再写一个字节,这个字节就是设定对比度的值
下一个是单字节命令可以写入A4或者A5即这个字节的前七位是命令码,最低位1/0表示开关功能是GDDRAM是否开启,发送A4即最低位为0表示输出遵循RAM的内容,发送A5即最低位为1,表示输出忽略RAM的内容
下一个功能是反色显示,发送A6表示RAM里的1像素点亮,发送A7表示像素里的0像素点亮
AE表示显示关闭 AF表示显示开启
设置列地址为00~0F,其中高四位0000是命令码,低4位是列地址的低位,配合下面的命令10~1F,高4位0001命令码,低4位是列地址的高位,这两个四位拼在一起构成列地址的8位,因为命令的高位需要有命令码,防止各个命令冲突,所以指定列地址,需要拆分为高4位和低4位
设置页地址B0·别,高5位为10110为命令码,低3位指定页地址PAGE0-7
比如发送命令B0 表示设置页地址位PAGE0,发送命令00再发10表示设置列地址为0
命令0X81: 设置对比度。包含两个字节,第一个0X81为命令,随后方法是的一个字节要设置这个对比度,值越大屏幕越亮。
·命令0XAE/0XAF: 0XAE为关闭显示命令,0XAF为开启显示命令
·0X8D: 包含两个字节,第一个为命令字,第二个为设置值,第二个字节的BIT2表示电荷泵的开关状态,该位为1开启电荷泵,为0则关闭。模块初始化的时候,这个必须要开启,否则看不到屏幕显示。
·命令0XB0~B7:用于设置页地址,其低三位的值对应GRAM页地址。
·命令0X00~0X0F:用于设置显示时的起始列地址低四位。
·命令0X10~0X1F: 用于设置显示时的起始列地址高四位。
硬件I2C的配置
void I2C_Configuration()
{I2C_InitTypeDef I2C_InitStructure;GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);//PB6---SCL,PB7---SDAGPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;//因为I2C空闲的时候是高阻态,而且复用I2CGPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);I2C_DeInit(I2C1);I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;I2C_InitStructure.I2C_ClockSpeed = 400000;I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;I2C_InitStructure.I2C_OwnAddress1 = 0X30;I2C_Init(I2C1, &I2C_InitStructure);I2C_Cmd(I2C1, ENABLE);}
I2C怎么去写一个字节,需要再写一个函数,因为作为OLED屏幕的驱动肯定要发送过去
先发送地址,再发送数据
在发送之前要检查一下总线是否繁忙,然后开启I2C
开启之后要有EV5的标志,应答完,要发送地址了(先去.h定义一个OLED的地址)
再发送应答位,现在可以发送数据了
void I2C_WriteByte(uint8_t addr,uint8_t dat)
{while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));//检查I2C总线是否繁忙//这行代码的作用是在 I2C 总线处于繁忙状态时,函数会一直循环等待,直到总线空闲。I2C_GenerateSTART(I2C1, ENABLE);//打开I2Cwhile(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));//EV5,主模式//这行代码会使函数进入循环,直到 I2C 主机成功进入主模式。I2C_Send7bitAddress(I2C1,0x78, I2C_Direction_Transmitter);//发送器件地址(点名)while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));//EV6//函数会循环等待,直到主机成功进入主发送模式。I2C_SendData(I2C1, addr); //发送寄存器地址//用于向 I2C 数据寄存器写入数据。这里将之前传入的 addr(寄存器地址)发送给从设备。while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));//EV8//函数会循环等待,直到寄存器地址成功发送。I2C_SendData(I2C1,dat); //发送数据//将之前传入的 dat(要写入的数据)发送到从设备的指定寄存器地址while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));//EV8I2C_GenerateSTOP(I2C1, ENABLE);//关闭I2C总线}
//写命令
void WriteCmd(unsigned char I2C_Command)
{I2C_WriteByte(0x00,I2C_Command);该函数的作用是向 I²C 设备写入一个命令。它调用了I2C_WriteByte函数,将地址0x00和命令I2C_Command作为参数传递给I2C_WriteByte函数。这里的0x00代表Control Byte 为0000 0000 也就是DC位置0 表示之后发送的是命令}
//写数据
void WriteDate(unsigned char I2C_Date)
{I2C_WriteByte(0x40,I2C_Date);该函数的作用是向 I²C 设备写入一个数据。它调用了I2C_WriteByte函数,将地址0x40和数据I2C_Date作为参数传递给I2C_WriteByte函数。这里的0x40代表Control Byte 为0100 0000 也就是DC位置1 表示之后发送的是数据}
OLED屏幕初始化
void OLED_Init(void)
{delay(100);WriteCmd(0xAE); //display offWriteCmd(0x20); //Set Memory Addressing Mode WriteCmd(0x10); //00,Horizontal Addressing Mode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,InvalidWriteCmd(0xb0); //Set Page Start Address for Page Addressing Mode,0-7WriteCmd(0xc8); //Set COM Output Scan DirectionWriteCmd(0x00); //---set low column addressWriteCmd(0x10); //---set high column addressWriteCmd(0x40); //--set start line addressWriteCmd(0x81); //--set contrast control registerWriteCmd(0xff); //ÁÁ¶Èµ÷½Ú 0x00~0xffWriteCmd(0xa1); //--set segment re-map 0 to 127WriteCmd(0xa6); //--set normal displayWriteCmd(0xa8); //--set multiplex ratio(1 to 64)WriteCmd(0x3F); //WriteCmd(0xa4); //0xa4,Output follows RAM content;0xa5,Output ignores RAM contentWriteCmd(0xd3); //-set display offsetWriteCmd(0x00); //-not offsetWriteCmd(0xd5); //--set display clock divide ratio/oscillator frequencyWriteCmd(0xf0); //--set divide ratioWriteCmd(0xd9); //--set pre-charge periodWriteCmd(0x22); //WriteCmd(0xda); //--set com pins hardware configurationWriteCmd(0x12);WriteCmd(0xdb); //--set vcomhWriteCmd(0x20); //0x20,0.77xVccWriteCmd(0x8d); //--set DC-DC enableWriteCmd(0x14); //WriteCmd(0xaf); //--turn on oled panel}
设置起点坐标
void OLED_SetPos(unsigned char x,unsigned char y)
{WriteCmd(0xb0 +y);WriteCmd((x&0xf0)>>4|0x10);WriteCmd((x&0x0f)|0x01);}
OLED_SetPos
是函数名,用于设置 OLED 屏幕上的位置。x
和y
是两个无符号字符型(unsigned char
)的参数,分别代表屏幕上的列坐标和页坐标。在 OLED 显示屏中,通常采用页地址和列地址的方式来定位像素点,y
代表页地址,x
代表列地址。WriteCmd
函数在之前的内容中提到过,它用于向 I²C 设备(这里是 OLED 显示屏)写入命令。0xb0
是 OLED 显示屏设置页地址命令的基础值。在 OLED 显示屏中,通常将屏幕按页划分,每一页包含若干行像素。不同的页地址由0xb0
加上页号来表示,这里的y
就是页号(范围通常是 0 - 7,因为很多 OLED 显示屏有 8 页)。例如,当y = 0
时,发送的命令是0xb0
,表示选择第 0 页;当y = 1
时,发送的命令是0xb1
,表示选择第 1 页,以此类推。(x & 0xf0)
:使用按位与运算符&
将x
与0xf0
(二进制为11110000
)进行运算,目的是提取x
的高 4 位。例如,如果x = 0x25
(二进制00100101
),那么x & 0xf0
的结果是0x20
(二进制00100000
)。(x & 0xf0) >> 4
:将提取的高 4 位右移 4 位,使其成为低 4 位。继续上面的例子,0x20
右移 4 位后得到0x02
(二进制00000010
)。(x & 0xf0) >> 4 | 0x10
:使用按位或运算符|
将右移后的结果与0x10
(二进制00010000
)进行运算。0x10
是设置列地址高 4 位命令的基础值,通过按位或运算将提取的高 4 位组合到命令中。例如,0x02 | 0x10
的结果是0x12
,表示列地址的高 4 位为0010
。(x & 0x0f)
:使用按位与运算符&
将x
与0x0f
(二进制为00001111
)进行运算,目的是提取x
的低 4 位。例如,如果x = 0x25
,那么x & 0x0f
的结果是0x05
(二进制00000101
)。(x & 0x0f) | 0x01
:使用按位或运算符|
将提取的低 4 位与0x01
(二进制00000001
)进行运算。0x01
是设置列地址低 4 位命令的基础值,通过按位或运算将提取的低 4 位组合到命令中。例如,0x05 | 0x01
的结果是0x05
,表示列地址的低 4 位为0101
。
(x&0xf0)>>4|0x10
:x&0xf0
会把x
的低四位清零,保留高四位。接着>>4
把高四位右移到低四位的位置,最后|0x10
是按位或操作,可能是为了组合成符合 OLED 指令规范的高四位地址设置指令部分。(x&0x0f)|0x01
:x&0x0f
会把x
的高四位清零,保留低四位,然后|0x01
按位或操作,是为了组合成符合 OLED 指令规范的低四位地址设置指令部分。
//全屏填充
void OLED_Fill(unsigned char Fill_Data)
{//共八页unsigned char m,n;for(m=0;m<8;m++){WriteCmd(0xb0+m);WriteCmd(0x00);WriteCmd(0x10);for(n=0;n<128;n++){WriteDate(Fill_Data);} }}//清屏
void OLED_CLS(void)
{OLED_Fill(0x00);
}//OLED打开
void OLED_ON(void)
{WriteCmd(0X8D); //设置电荷泵WriteCmd(0X14); //开启电荷泵WriteCmd(0XAF); //OLED唤醒}
//OLED关闭
void OLED_OFF(void)
{WriteCmd(0X8D); //设置电荷泵WriteCmd(0X10); //关闭电荷泵WriteCmd(0XAE); //OLED关闭}
OLED显示字符串
//OLED存放格式
//一行128个 一列7个 有8页
//´æ·Å¸ñʽÈçÏ£º
//[0]0 1 2 3 ... 127
//[1]0 1 2 3 ... 127
//[2]0 1 2 3 ... 127
//[3]0 1 2 3 ... 127
//[4]0 1 2 3 ... 127
//[5]0 1 2 3 ... 127
//[6]0 1 2 3 ... 127
//[7]0 1 2 3 ... 127
//显示字符串函数
//TextSize表示两种显示方式 一种是6X8 一种是8X16
void OLED_ShowStr(unsigned char x,unsigned y,unsigned char ch[],unsigned TextSize)
{ unsigned char c = 0,i = 0,j = 0;//现在选择用那种表达方式switch(TextSize) { case 1:{while(ch[j] != '\0'} //先判断字符串是否输出到'\0'{ c = ch[j] - 32;//将字符的ASCII码减去32用于索引F6x8 (字库假设字库从空格字符开始)if(x>126){x = 0;y++;}//上面是已经找到该输出什么了//现在需要设置一下从什么地址开始输出OLED_SetPos(x,y);for(i=0;i<6;i++){WriteDate( F6x8[c][i]);x+=6; }j++;}}break;case 2;{while(ch[j] !='\0'){c = ch[j] - 32;if(x > 120){x = 0;y++;}OLED_SetPos(x,y);for(i = 0;i<8;i++)WriteDate( F8X16[c*16+i]);OLED_SetPos(x,y);for(i = 0;i<8;i++)WriteDate( F8X16[c*16+i+8]);x+=8;}break;}}
int main()
{ I2C_Configuration();OLED_Init();delay(100);OLED_Fill(0xFF); //屏幕全亮delay(100);OLED_Fill(0x00); //屏幕全灭delay(100);OLED_ShowStr(0,3,"Hello World",1);OLED_ShowStr(0,4,"Hello World",2);}
OLED显示文字
先生成字模
void OLED_ShowCN(unsigned char x,unsigned char y,unsigned char N)
{unsigned char wm=0;unsigned int addr = 32*N;OLED_SetPos(x,y);for(wm=0;wm<16;wm++){WriteDat(F16X16[addr]);addr +=1;}OLED_SetPos(x,y+1);for(wm=0;wm<16;wm++){WriteDat(F16X16[addr]);addr +=1;}
软件I2C的配置
先配置GPIO引脚
宏定义输出高电平和低电平
#define OLEDR_SCLK_Set() GPIO_SetBits(GPIOB, GPIO_Pin_0)//PB0(SCL)输出高电平
#define OLEDR_SCLK_Clr() GPIO_ResetBits(GPIOB, GPIO_Pin_0)//PB1(SCL)输出低电平
#define OLEDR_SDIN_Set() GPIO_SetBits(GPIOB, GPIO_Pin_1)//PB1(SDA)输出高电平
#define OLEDR_SDIN_Clr() GPIO_ResetBits(GPIOB, GPIO_Pin_1)//PB1(SDA)输出低电平
根据时序图,最开始时把SDA和SCL拉高 (开漏换成推挽)
模拟I2C起始信号和停止信号
根据时序图 模拟I2C起始信号和停止信号
static void OLEDR_IIC_Start(void)
{OLEDR_SCLK_Set();//时钟总线高电平OLEDR_SDIN_Set();//数据总线高电平delay_u(1);OLEDR_SDIN_Clr();//数据总线先低电平delay_u(1);OLEDR_SCLK_Clr();/时钟总线再低电平}
static void OLEDR_ICC_Stop(void)
{OLEDR_SDIN_Clr();//数据总线低电平delay_u(1);OLEDR_SCLK_Set();//时钟总线高电平delay_u(1);OLEDR_SDIN_Set();//数据总线在高电平delay_u(1);
}
模拟I2C的应答信号
根据时序图 模拟I2C的应答信号
先宏定义
static unsigned char IIC_Wait_Ack(void)
{unsigned char ack;OLEDR_SCLK_Clr();//时钟总线置低delay_u(1);OLEDR_SDIN_Set();//数据总线高电平delay_u(1);OLEDR_SCLK_Set();//时钟总线高电平delay_u(1);if(OLED_READ_SDIN())//如果读取到电平{ack = IIC_NO_ACK; }else{ack = IIC_ACK; }OLEDR_SCLK_Clr();//时钟总线置低delay_u(1);return ack; //返回取得应答信息}
模拟IIC写字节
根据数据的有效性 ,时钟和数据都高电平的时候才有效
static void Write_IIC_Byte(unsigned char IIC_Byte)
{unsigned char i ;//定义变量for(i = 0;i<8;i++){OLED_SCLK_Clr();delay_u(1);//因为是高位传输,所以先读取高位,如果高位为1,说明有数据进入if(IIC_Byte & 0x80) //循环8次 读取最高位,与0x80(1000 0000)进行与运算(逢0截止)OLEDR_SDIN_Set(); //最高位为1 数据总线置高电平elseOLEDR_SDIN_Clr(); //最高位位0 数据总线置低电平IIC_Byte <<=1; //数据向左移1位delay_u(1);OLEDR_SCLK_Set();//时钟线置高,产生上升沿,把数据发出去delay_u(1);}OLEDR_SCLK_Clr();//不循环的时候时钟线置低电平delay_u(1);IIC_Wait_Ack();//从机等待}
模拟I2C对OLED写入一个字节
//IIC写命令
static void Write_IIC_Command(unsigned char IIC_Command)
{void OLEDR_IIC_Start();//发送起始信号Write_IIC_Byte(0x78);//写入从机(oled屏)地址 SD0 = 0Write_IIC_Byte(0x00);//0x00 用于告诉 OLED 屏幕接下来接收的是命令Write_IIC_Byte(IIC_Command);//命令OLEDR_ICC_Stop();//发送停止信号}
//IIC写数据
static void Write_IIC_Command(unsigned char IIC_Command)
{void OLEDR_IIC_Start();//发送起始信号Write_IIC_Byte(0x78);//写入从机(oled屏)地址 SD0 = 0Write_IIC_Byte(0x40);//0x40 用于告诉 OLED 屏幕接下来接收的是数据Write_IIC_Byte(IIC_Command);//数据OLEDR_ICC_Stop();//发送停止信号}
//对OLED写入一个字节
void OLED_WR_Byte(unsigned char dat,unsigned char cmd)
{if(cmd)//如果cmd为真(1)则是写数据,否则是写命令{Write_IIC_Data(dat);//写入数据}else{Write_IIC_Command(dat);//写入命令}}
OLED起始坐标
先宏定义用于这个函数
//设置数据写入的起始行和列
//x:列的起始低地址与高地址
//y:页的起始地址 0-7
void OLED_Set_Pos(unsigned char x,unsigned char y)
{OLED_WR_Byte(0xb0+y,OLED_CMD); //写入页地址(命令)OLED_WR_Byte((x&0x0f),OLED_CMD);//写入列的低地址(命令)OLED_WR_Byte(((x&0xf0)>>4)|0x10,OLED_CMD);//写入列的高地址 (命令)}
//开显示
void OLED_Display_On(void)
{OLED_WR_Byte(0x8D,OLED_CMD);//设置OLED电荷泵OLED_WR_Byte(0x14,OLED_CMD);//使能 开OLED_WR_Byte(0xAF,OLED_CMD);//开显示}//关显示
void OLED_Display_Off(void)
{OLED_WR_Byte(0xAE,OLED_CMD);//关显示OLED_WR_Byte(0x8D,OLED_CMD);//设置OLED电荷泵OLED_WR_Byte(0x10,OLED_CMD);//失能}
//清屏幕
void OLED_Clear(void)
{unsigned char i,n;for(i=0;i<8;i++)//i代表页数 总共有8页 所以循环8次{OLED_WR_Byte(0xb0+i,OLED_CMD);//从0-7页写入OLED_WR_Byte(0x00,OLED_CMD);//列的低地址OLED_WR_Byte(0x10,OLED_CMD);//列的高地址for(n=0;n<128;n++){OLED_WR_Byte(0,OLED_DATA);//写入 0 清屏 }}}
显示字符 字符串 数字 汉字
宏定义一下
void OLED_ShowChar(unsigned char x,unsigned char y,unsigned char chr)
{unsigned char c = 0,i = 0;c = chr -' ';//为获取字符的偏移量//判断大小if(x>Max_Coulumn){x = 0;//列超出范围了 得从下两页开始y = y+2;}if(SIZE == 16)//字符大小如果为16 那就是16x8 需要占两页 每一页有8列{OLED_Set_Pos(x,y);//从x列 y页开始for(i=0;i<8;i++)//先显示前8位OLED_WR_Byte(F8X16[c*16+i],OLED_DATA);//找出字符C的数组,在第一页上依次把每一列写出//因为一列只能显8位,所以需要再写一页OLED_Set_Pos(x,y+1);//从x列 y+1页开始for(i=0;i<8;i++)//再显示剩下8位OLED_WR_Byte(F8X16[c*16+i+8],OLED_DATA);//i+8写出数组的后8位 }else //6x8的 6位 只需占一页 每一页六列{OLED_Set_Pos(x,y);//从x列 y页开始for(i=0;i<6;i++)OLED_WR_Byte(F6x8[c][i],OLED_DATA);//找出字符C的数组,依次把每列写出} }
//显示字符串
void OLED_ShowString(unsigned char x,unsigned char y,unsigned char *chr)
{unsigned char j = 0;while(chr[j] != '\0')//判断是不是最后一个字符{OLED_ShowChar(x,y,chr[j]);//显示字符x+=8; //每显示一个字符,重新从后八列开始,因为一个字符得用八个字节显示if(x>=128){x = 0;y =+2;}j++; }
//计算m^n次方
unsigned int oled_pow(unsigned char m,unsigned n)
{unsigned int result = 1;while(n--)result *=m;return result;}
//显示数字
void OLED_ShowNum(unsigned char x,unsigned char y,unsigned int num,unsigned char len,unsigned char size)
{ unsigned char t,temp; //t用于循环计数 temp用于存储当前要显示的数字位unsigned char enshow=0; //用来判断是否开始显示非零数字for(t=0;t<len;t++){temp=(num/oled_pow(10,len-t-1))%10;//取出输入数的每一位,由高到低if(enshow==0&&t<(len-1)) //如果enshow为0而且不是最后一位数字{if(temp==0) //如果该数字位为0{OLED_ShowChar(x+(size/2)*t,y,' ');//显示0 x+(size/2)*t根据字体大小偏移的列数continue; //跳出剩下语句,继续重复循环}else enshow=1; }OLED_ShowChar(x+(size/2)*t,y,temp+'0'); //显示一个位 x+(size/2)*t根据字体大小偏移的列数}
}
void OLED_ShowCHinese(unsigned char x,unsigned char y,unsigned char num)
{unsigned char t,adder = 0;OLED_Set_Pos(x,y);for(t=0;t<16;t++){ OLED_WR_Byte( Hzk[2*no][t],OLED_DATA);//画 number在数组位置的第一页16个列的点adder+=1; //数组地址+1}OLED_Set_Pos(x,y+1);for(t=0;t<16;t++){OLED_WR_Byte( Hzk[2*no+1][t],OLED_DATA);//画 number在数组位置的第二页16个列的点adder+=1; //数组地址+1}}
int main()
{ initSysTick();delay_ms(1500);OLED_Init(); OLED_Clear();OLED_ShowChar(30,2,'O');OLED_ShowChar(38,2,'K');OLED_ShowString(30,4,"123");
}
温湿度传感器
DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器,应用于专用数字模块采集技术和温湿度传感技术,确保产品具有极高的可靠性和卓越的长期稳定性。
传感器包括一个电阻式感湿元件和一个NTC测温元件,并与一个高性能八位单片机相连接。采用单线制串口行接口,信号传输距离可达20M以上。
应用于通暖空调,汽车,自动控制设备,气象站家电
湿度调节器,医疗,除湿器等等。
硬件接线图:
供电电压:3.3 - 5.5V直流电
输出为单总线数字信号
温度测量范围0-50度(精度正负2度,分辨率1度)
湿度测量范围为20-90%RH(精度为正负5%,分辨率1%)
模块的+ 接单片机的5V
模块的- 接单片机的GND
模块的OUT 接单片机定义的引脚
vcc和gnd之间可以加一个电容,用于去耦滤波
采用单总线双向串行通信协议,每次采集都要由单片机发起开始信号,然后DHT11会向单片机发送响应并开始传输40位数据帧(5x8),高位在前。
数据格式为:
第一二个字节: 8bit湿度整数数据+8bit湿度小数数据
第三四个字节: 8bit温度整数数据+8bit温度小数数据
第五个字节 : 8bit校验位(它是前四个数据相加后八位的数值)
温湿度小数部分默认为0,即单片机采集的数据都是整数,校验位为4个字节的数据相加取结果的低8位数据作为校验和;
示例一:
0011 0101 0000 0000 0011 1000 0000 0000 0100 1101
湿度高八位 湿度低八位 温度高八位 温度低八位 检验位
计算 : 0011 0101 (相加)
0011 1000
结果: 01001101 湿度为0011 0101 = 35H = 53%RH
温度为0011 1000 = 18H = 24°
示例二:
0011 0101 0000 0000 0001 1000 0000 0000 0100 1001
湿度高八位 湿度低八位 温度高八位 温度低八位 检验位
计算 : 0011 0101 (相加)
0001 1000
结果: 0100 1101 不等于 01001001 本次接收数据不正确,重新接收数据
温湿度传感器时序介绍
总线空闲状态为高电平,主机把总线拉低等待DHT11响应,主机把总线拉低必须大于18毫秒,保证DHT11能检测起始信号。主机立马拉高等待,DHT11接收到主机的开始信号后,等待主机开始信号结束,然后发送80us低电平响应信号,主机发送开始信号结束后,延时等待20-40us后,读取DHT11的响应信号,主机发送开始信号后,可以切换到输入模式,或者输出高电平均可,总线由上拉电阻拉高。
DHT11输出0时时序
DHT11输出1时时序
因为是采用单总线双向串行通信协议所以要配两次GPIO
#include "stm32f10x.h"
#include "DHT11.h"
#include "delay.h"
#include "stdio.h"uint16_t Rxbuff[5];
//先输出 发送起始信号等待响应后
void DHT11_GPIO_Init(void)
{GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE);GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);GPIO_SetBits(GPIOB, GPIO_Pin_11);}
//再转换为输入
void DHT11_GPIO_Init1(void)
{GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE);GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);GPIO_SetBits(GPIOB, GPIO_Pin_11);}static uint8_t DHT11_Back()
{uint8_t i =200;while(read_data && i--); //等待低电平的到来i= 200;while(!read_data && i--); //等待高点平的到来return 0;
}void DHT11_Start(void)
{data0;//将总线拉低delay_ms(20);//主机将总线拉低必须大于18毫秒data1;//再把总线拉高等待延时delay_us(10);DHT11_GPIO_Init1();//这时单片机是由输出切换成输入while(DHT11_Back());//同时等待响应信号(响应信号是低电平)
//如果是低电平代表这个值DHT11_Back()是0就会继续运行下面代码 如果是高电平就是1就会一直在while里循环
}
//总共接收5个字节 每个字节8位
void DHT11_ReceptionBuff()
{uint8_t y=1;uint16_t i;uint8_t x;for(x=0;x<5;x++)//接收5个字节{i=0;//i代表位for(y=1;y<9;y++){while(read_data){__nop();}delay_us(40);while(!read_data){__nop();}i= i<<1;delay_us(30);//每接收一位中间得隔50微秒if(read_data){i |=1;}while(read_data);}Rxbuff[x] =i;}}void DHT11_UpdateData()
{DHT11_GPIO_Init();//GPIO初始化 这时还是输出模式DHT11_Start();//开始信号DHT11_ReceptionBuff();//切换成接收信号}
OELD显示温湿度项目
int main()
{ initSysTick();delay_ms(1500);OLED_Init(); OLED_Clear();OLED_ShowCHinese(0,2,6); //当OLED_ShowCHinese(16,2,7); //前OLED_ShowCHinese(32,2,8); //温OLED_ShowCHinese(48,2,9); //度OLED_ShowCHinese(66,2,10); //:OLED_ShowCHinese(90,2,15); //.OLED_ShowCHinese(112,2,11); //COLED_ShowCHinese(0,5,6); //当OLED_ShowCHinese(16,5,7); //前OLED_ShowCHinese(32,5,12); //湿OLED_ShowCHinese(48,5,9); //度OLED_ShowCHinese(66,5,10); //:OLED_ShowCHinese(112,5,13); //% while(1){uint16_t i;uint8_t k;uint8_t kk;uint8_t kkk;DHT11_UpdateData();i = Rxbuff[0]+ Rxbuff[1]+Rxbuff[2]+ Rxbuff[3];if(Rxbuff[4] ==i){k=Rxbuff[2];kk=Rxbuff[0];kkk=Rxbuff[3];OLED_ShowNum(74,2,k/10,3,3);OLED_ShowNum(82,2,k%10,3,3);OLED_ShowNum(98,2,kkk,3,3);OLED_ShowNum(88,5,kk/10,3,3);OLED_ShowNum(98,5,kkk%10,3,3);}delay_ms(2000);}
}
- 温度显示
OLED_ShowNum(74,2,k/10,3,3);
:
k/10
:这是一个整数除法运算。例如,若k
的值为 25,那么k/10
的结果就是 2,它代表温度整数部分的十位数字。OLED_ShowNum(74,2,k/10,3,3);
表示在 OLED 屏幕的x
坐标为 74、y
坐标为 2 的位置,以长度为 3、字体大小为 3 显示温度整数部分的十位数字。OLED_ShowNum(82,2,k%10,3,3);
:
k%10
:这是取模运算,用于获取k
除以 10 的余数。若k
的值为 25,那么k%10
的结果就是 5,它代表温度整数部分的个位数字。OLED_ShowNum(82,2,k%10,3,3);
表示在 OLED 屏幕的x
坐标为 82、y
坐标为 2 的位置,以长度为 3、字体大小为 3 显示温度整数部分的个位数字。OLED_ShowNum(98,2,kkk,3,3);
:
kkk
存储的是温度的小数部分。OLED_ShowNum(98,2,kkk,3,3);
表示在 OLED 屏幕的x
坐标为 98、y
坐标为 2 的位置,以长度为 3、字体大小为 3 显示温度的小数部分。
湿度显示
OLED_ShowNum(88,5,kk/10,3,3);
:
kk/10
:同样是整数除法运算,用于获取湿度整数部分的十位数字。OLED_ShowNum(88,5,kk/10,3,3);
表示在 OLED 屏幕的x
坐标为 88、y
坐标为 5 的位置,以长度为 3、字体大小为 3 显示湿度整数部分的十位数字。OLED_ShowNum(98,55,kkk%10,3,3);
:
- 这里存在一个问题,
kkk
存储的是温度的小数部分,而此处本意可能是要显示湿度的小数部分(按照 DHT11 数据格式,湿度小数部分应从Rxbuff[1]
获取)。并且y
坐标为 55 可能超出了 OLED 屏幕的有效范围,会导致显示异常。kkk%10
获取kkk
除以 10 的余数。OLED_ShowNum(98,55,kkk%10,3,3);
表示在 OLED 屏幕的x
坐标为 98、y
坐标为 55 的位置,以长度为 3、字体大小为 3 显示数据,但这里数据和坐标可能都有误。