基于32单片机的RS485综合土壤传感器检测土壤PH、氮磷钾的使用(超详细)

1-3为RS485综合土壤传感器的基本内容

4-5为基于STM32F103C8T6单片机使用RS485传感器检测土壤PH、氮磷钾并显示在OLED显示屏的相关配置内容

注意:本篇文件讲解使用的是PH、氮磷钾四合一RS485综合土壤传感器,但里面的讲解内容适配市面上的所有多合一的RS485综合土壤传感器

一、RS485综合土壤传感器概述

1.1 产品概述

本产品性能稳定灵敏度高,响应快,输出稳定,适用于各种土质。是观测和研究盐渍土的发生、演变、改良以及水盐动态的重要工具。通过测量土壤的介电常数,能直接稳定地反映各种土壤的真实水分含量。可测量土壤水分的体积百分比,是符合目前国际标准的土壤水分测量方法。可长期埋入土壤中,耐长期电解,耐腐蚀,抽真空灌封,完全防水。

适用于土壤墒情监测、科学试验、节水灌溉、温室大棚、花卉蔬菜、草地牧场、土壤速测、植物培养、污水处理、精细农业等场合的温湿度、电导率、PH值测试。
1.2 功能特点

■ 门槛低,步骤少,测量快速,无需试剂,不限检测次数。

■ 电极采用特殊处理的合金材料,可承受较强的外力冲击,不易损坏。

■ 完全密封,耐酸碱腐蚀,可埋入土壤或直接投入水中进行长期动态检测。

■ 精度高,响应快,互换性好,探针插入式设计保证测量精确,性能可靠。

■ 也可用于水肥一体溶液、以及其他营养液与基质的电导率。

二、硬件连接

综合土壤传感器使用的RS485通信方式,而RS-485 是一种用于长距离通信的串行通信标准,常用于工业和商业应用。它在噪声干扰的环境中提供了良好的信号完整性,支持多点通信。

在与单片机(如 Arduino、8051、PIC、STM32 等)进行通信时,通常需要将 RS-485 信号转换为 TTL信号。

将 RS-485 信号转换为 TTL 信号是确保传感器和单片机之间能够顺利通信的关键步骤,这样可以解决电平匹配、通信协议、设计简化和电气保护等问题。通常使用专门的 **RS-485 到 TTL 的转换器(例如 MAX485、SN75176 等)**来实现这一转换。

单片机TTL转RS485模块


土壤综合传感器接线讲解

|----|-------|-------------|
| 线色 | 说明 | 备注 |
| 棕色 | 电源正 | 4.5~30V DC |
| 黑色 | 电源地 | GND |
| 黄色 | 485-A | 485-A |
| 蓝色 | 485-B | 485-B |

传感器和转换器接线讲解

|----------------|-------------|
| 单片机TTL转RS485模块 | 单片机和土壤综合传感器 |
| VCC | 单片机的5V(建议) |
| TXD | A2 |
| RXD | A3 |
| GND | GND |
| GND | GND |
| B | 485-B |
| A | 485-A |

三、单片机和RS485综合传感器的通信配置

综合土壤传感器通过硬件连接后,32单片机和其的通信方式则为串口通信,单片机通过串口发送****问询帧给综合土壤传感器,其接收到单片机的发送信息后,会反馈应答帧给单片机,单片机从应答帧接收到对的地址码后,则开始接收对应的信息,后通过转换数据得到我们想要的信息。

要想知道具体的通信细节,我们还得查看商家给我们对应传感器的文档,里面有我们需要的对应通信协议。

四、通信协议 (重要)

4.1通讯基本参数

|-------|-------------------------------------------------|
| 编 码 | 8位二进制 |
| 数据位 | 8位 |
| 奇偶校验位 | 无 |
| 停止位 | 1位 |
| 错误校验 | CRC(冗余循环码) |
| 波特率 | 2400bit/s、4800bit/s、9600 bit/s可设,出厂默认为4800bit/s |

注意: 这里的波特率指的是串口通信中使用的波特率,而不同厂家生产的综合土壤传感器出厂默认的波特率也有可能不相同,有的可能为9600bit/s,具体到哪个需要找商家拿到对应的资料文档从而进行查看,本次使用的传感器出厂默认为4800bit/s。

4 .2 数据帧格式定义

采用ModBus-RTU 通讯规约,格式如下:

初始结构 ≥4 字节的时间

地址码 = 1 字节

功能码 = 1 字节

数据区 = N 字节

错误校验 = 16 位CRC 码

结束结构 ≥4 字节的时间

地址码:为变送器的地址,在通讯网络中是唯一的(出厂默认0x01)。

功能码:本产品用到功能码0x03、0x06、0x10等。

数据区:数据区是具体通讯数据,注意16bits数据高字节在前!

CRC码:二字节的校验码。

其中,主机问询帧结构就是单片机通过串口发送给传感器的信息,而从机应答帧结构就是土壤传感器反馈给单片机的信息

主机问询帧结构:

|-----|-----|---------|-------|-------|-------|
| 地址码 | 功能码 | 寄存器起始地址 | 寄存器长度 | 校验码低位 | 校验码高位 |
| 1字节 | 1字节 | 2字节 | 2字节 | 1字节 | 1字节 |

从机应答帧结构:

|-----|-----|-------|------|-------|-------|-----|
| 地址码 | 功能码 | 有效字节数 | 数据一区 | 第二数据区 | 第N数据区 | 校验码 |
| 1字节 | 1字节 | 1字节 | 2字节 | 2字节 | 2字节 | 2字节 |

4.3 寄存器地址

不同厂家的土壤综合传感器的寄存器地址也可能会各不相同,无需理会,我们只需要读懂其使用方法就行。

4.4 通讯协议示例以及解释(重要)

以官方的文档为例

官方举例是电导率温度水分PH四合一设备的例子,那是不是所有的多合一的综合土壤综合传感器都一样呢?明显不是的,我们不同的综合土壤传感器所发的问询帧和返回的应答帧是不一样的,那具体该怎么发送,怎么修改呢?
问询帧:

地址码、功能码:这两个出厂就有个默认值,而且资料文档也会给出,在4、2的截图有说明,而且其中一般都是默认地址码0x01,功能码0x03

起始地址:两个字节,具体字节是你检测的数据最前的第一个地址,例如上面举例的电导率温度水分PH四合一检测的数据地址最前面的则是000H,所有它的起始地址为0x00 0x00,而本次讲解使用的是PH、氮磷钾四合一,那根据官方给的文档查阅,地址最前的则是PH的地址0003H,则起始地址为0x00 0x3H。

数据长度:就是你发送地址数据的长度。检测多少个数据,看需要检测的数据是多少个以及它的地址字节数,但文档的地址的检测数据都是低字节,所以可以简单的总结为,四合一的数据长度为0x00 0x04,七合一的数据长度:0x00 0x07,只检测一个数据的长度为:0x00 0x01

效验码低字节、效验码高字节:这里的二字节的校验码指的是CRC码。那CRC码又是什么呢?CRC码是一种广泛使用的错误检测码,用于检测数据传输或存储过程中发生的错误。它通过在数据中添加冗余信息,使接收方能够验证数据的完整性。

根据前面的发送数据来计算CRC码,那怎样计算CRC码呢?计算CRC码比较复杂,而且难度高,那我们可以直接通过查阅数据来获取我们的CRC码,地址如下:16进制(CRC16)(MODBUS RTU通讯)校验码在线计算器

举例:

文档电导率温度水分PH四合一发送的地址码、功能码、起始地址、数据长度分为01 03 00 00 00 04,那它的CRC码(MSB-LSB)为:0944,也就是说 效验码低字节:0x44,效验码高字节:0x09,这就和我上面列举的官方文档一致了。

那我们本次使用的PH、氮磷钾四合一,那其地址码、功能码、起始地址、数据长度分为01 03 00 03 00 04,那它的CRC码(MSB-LSB)为:09B4,也就是说 效验码低字节:0xB4,效验码高字节:0x09。

那假如我四合一传感器只想读取其中一个数据呢**,比如我只想读取****磷的数据,那我的问询帧前面的格式则为,地址码:0x01 功能码:0x03,起始地址:0x00 0x05 ,数据长度:0x00 0x01,效验码低字节0x94,效验码高字节:0x0B。其中起始地址中的0x00 0x05是磷的寄存器地址。**

当然你也可以全部读取所有数据,然后根据你数组存储的数据位置来获取单独的数据也是可以的。

应答帧:

以官方给出的电导率温度水分PH四合一例子讲解:

地址码、功能码:和上面问询帧讲解一样,出厂一般默认为0x01 0x03,具体看文档。

返回有效字节数:具体看从机应答帧结构。例如:四合一土壤综合传感器,那其有效字节数为为你检测的数据之和,也就是水分值+温度值+电导率+PH = 8,有效字节数则是0x08

后面的数据区就是根据你的地址数据从低到高排列的,我们用数组接受到返回的数据后,根据其位置分别读取就行

五、代码例子 (PH、氮磷钾四合一)

单片机通过串口发送****问询帧给综合土壤传感器,其接收到单片机的发送信息后,会反馈应答帧给单片机,单片机从应答帧接收到对的地址码后,则开始接收对应的信息,后通过转换数据得到我们想要的信息。

5.1 串口2定时发送问询帧给RS485综合土壤传感器

通过配置串口2通信,并且波特率选择为4800(根据厂家的默认出厂波特率为准),通过1S定时给综合土壤传感器发送问询帧,等待发送完毕。(注意其中串口使用的串口2,定时器使用的定时器3,同学们可以自由选择自己的配置串口和定时器)
串口2配置和发送一字节函数:

cpp 复制代码
void Usart2_Init(u32 bp)
{
	NVIC_InitTypeDef NVIC_InitStructure;
	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	// GPIOA时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE); //串口2时钟使能

 	USART_DeInit(USART2);  //复位串口2
		 //USART2_TX   
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; 
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出
    GPIO_Init(GPIOA, &GPIO_InitStructure); 
   
    //USART2_RX	  
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
    GPIO_Init(GPIOA, &GPIO_InitStructure);  
	
	USART_InitStructure.USART_BaudRate = bp;
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
	USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
	USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式
  
	USART_Init(USART2, &USART_InitStructure); //初始化串口2
  
	USART_Cmd(USART2, ENABLE);                    //使能串口 
	
	//使能接收中断
	USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);//开启中断   
	
	//设置中断优先级
	NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2 ;//抢占优先级3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
	NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器

}

//串口1发送一字节
void Send_Byte(uint8_t data)
{
	USART_SendData(USART2,data);
	while(!USART_GetFlagStatus(USART2,USART_FLAG_TXE));//等待发送数据完毕
	
}

定时器3配置:

通过定时一字节一字节的发送询问帧,其中 PH、氮磷钾四合一的询问帧为

01 03 00 03 00 04 B4 09(为什么是这个,上面内容有解释,同学们可查看4.4内容)

cpp 复制代码
void Tim3_Init(void)
{
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);//打开TIM3时钟
	
	//配置TIM3时基单元
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct={0};
	TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;//向上计数
	TIM_TimeBaseInitStruct.TIM_Period = 10000;//重装载值
	TIM_TimeBaseInitStruct.TIM_Prescaler = 7200 - 1;//预分频数
	TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStruct);

	TIM_ITConfig(TIM3,TIM_IT_Update, ENABLE);//使能更新中断
	
	//TIM3中优先限配置
	NVIC_InitTypeDef NVIC_InitStruct;
	NVIC_InitStruct.NVIC_IRQChannel=TIM3_IRQn;//配置TIM3中断源
	NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;//中断使能
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=0;//抢断优先级
	NVIC_InitStruct.NVIC_IRQChannelSubPriority=2;	//子优先级
	NVIC_Init(&NVIC_InitStruct);//初始化配置NVIC(中断向量控制寄存器)
	
	TIM_Cmd(TIM3, ENABLE);//使能定时器
}

u16 time[5]={0};
void TIM3_IRQHandler(void)
{
	if(TIM_GetITStatus(TIM3,TIM_IT_Update) != RESET)
	{
		TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
		time[0]++;
		if(time[0] == 1){
			time[0] = 0;
			delay_ms (1);//延时1ms
			Send_Byte( 0x01);
			Send_Byte( 0x03);
			Send_Byte( 0x00);
			Send_Byte( 0x03);
			Send_Byte( 0x00);
			Send_Byte( 0x04);
			Send_Byte( 0xB4);
			Send_Byte( 0x09);
		}

	}
}

5.2 发送完询问帧后,通过串口2中断,接收返回的信息(应答帧)

其中我们根据应答帧第一个地址码为包头,从而接收信息。其中代码的13个字节数据计算方法是根据应答帧结构来的,例如四合一=地址码(1)+功能码(1)+有效字节数(1)+数据区(4x2)+效验码(2) = 13

5.3 完整代码

main.c

cpp 复制代码
#include "main.h"
extern char USART_data[20];//串口数据存储数组
float ph;
int N;
int P;
int K;
u8 bufss1[20];
u8 bufss2[20];
u8 bufss3[20];
u8 bufss4[20];
int main(void)
{

	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//中断组的选择
	Usart2_Init(4800);//串口2初始化
	OLED_Init();//OLED初始化
	Tim3_Init();//定时器初始化
while(1)
{
    ph=(USART_data[3]*16+USART_data[4])/10;
    N=(USART_data[5]*16+USART_data[6]);
    P=(USART_data[7]*16+USART_data[8]);
    K=(USART_data[9]*16+USART_data[10]);
	sprintf((char *)bufss1,"PH:%.1f",ph);
	sprintf((char *)bufss2,"N:%d",N);
	sprintf((char *)bufss3,"P:%d",P);
	sprintf((char *)bufss4,"K:%d",K);
	Oled_ShowAll(0,0,bufss1);//显示中英字符串
	Oled_ShowAll(2,0,bufss2);//显示中英字符串
	Oled_ShowAll(4,0,bufss3);//显示中英字符串
	Oled_ShowAll(6,0,bufss4);//显示中英字符串

    }

}

串口2配置

cpp 复制代码
#include "usart.h"



void Usart2_Init(u32 bp)
{
	NVIC_InitTypeDef NVIC_InitStructure;
	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	// GPIOA时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE); //串口2时钟使能

 	USART_DeInit(USART2);  //复位串口2
		 //USART2_TX   
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; 
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	//复用推挽输出
    GPIO_Init(GPIOA, &GPIO_InitStructure); 
   
    //USART2_RX	  
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
    GPIO_Init(GPIOA, &GPIO_InitStructure);  
	
	USART_InitStructure.USART_BaudRate = bp;
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
	USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
	USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式
  
	USART_Init(USART2, &USART_InitStructure); //初始化串口2
  
	USART_Cmd(USART2, ENABLE);                    //使能串口 
	
	//使能接收中断
	USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);//开启中断   
	
	//设置中断优先级
	NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2 ;//抢占优先级3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;		//子优先级3
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
	NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器

}

	char USART_flag;
	char USART_data[20];
	int i=0;
	int USART_Ready=0;//数据接收完成标志
//串口中断
void USART2_IRQHandler(void)
{
	if(USART_GetITStatus(USART2,USART_IT_RXNE))//接收中断标志位
	{
		USART_flag = USART_ReceiveData(USART2);
		if(USART_flag==0x01)//检测包头
		{
		USART_Ready=1;
		}
		if(USART_Ready==1)
		{
		USART_data[i]=USART_flag;
		i++;
		if(i==12)//捕捉完成18个字节数据
		{
		USART_Ready=0;
		i=0;	
			}
		}
  USART_ClearITPendingBit(USART2,USART_IT_RXNE);//清除中断标志位
	}
}
//串口2发送一字节
void Send_Byte(uint8_t data)
{
	USART_SendData(USART2,data);
	while(!USART_GetFlagStatus(USART2,USART_FLAG_TXE));//等待发送数据完毕
	
}

//串口2接收一节字

u16 Rece_Byte(void)
{
	while(!USART_GetFlagStatus(USART2,USART_FLAG_RXNE))//等待接收数据完毕
	{
	}
	return USART_ReceiveData(USART2);
}

//回显函数
void Data_Echo(void)
{
	uint16_t date=0;
	date=Rece_Byte();
	Send_Byte(date);
}

//函数功能:printf重定向
int fputc(int c, FILE * stream)
{
	Send_Byte(c);
	return c;
}

定时器3配置:

cpp 复制代码
#include "time.h"
/***********************
函数名:
函数功能:定时器3初始化
形参:
返回值:
函数说明:


************************/
void Tim3_Init(void)
{
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);//打开TIM时钟
	
	//配置TIM3时基单元
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct={0};
	TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;//向上计数
	TIM_TimeBaseInitStruct.TIM_Period = 10000;//重装载值
	TIM_TimeBaseInitStruct.TIM_Prescaler = 7200 - 1;//预分频数
	TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStruct);

	TIM_ITConfig(TIM3,TIM_IT_Update, ENABLE);//使能更新中断
	
	//TIM3中优先限配置
	NVIC_InitTypeDef NVIC_InitStruct;
	NVIC_InitStruct.NVIC_IRQChannel=TIM3_IRQn;//配置TIM3中断源
	NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;//中断使能
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=0;//抢断优先级
	NVIC_InitStruct.NVIC_IRQChannelSubPriority=2;	//子优先级
	NVIC_Init(&NVIC_InitStruct);//初始化配置NVIC(中断向量控制寄存器)
	
	TIM_Cmd(TIM3, ENABLE);//使能定时器
}

u16 time[5]={0};
void TIM3_IRQHandler(void)
{
	if(TIM_GetITStatus(TIM3,TIM_IT_Update) != RESET)
	{
		TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
		time[0]++;
		if(time[0] == 1){
			time[0] = 0;
			delay_ms (1);//延时1ms
			Send_Byte( 0x01);
			Send_Byte( 0x03);
			Send_Byte( 0x00);
			Send_Byte( 0x03);
			Send_Byte( 0x00);
			Send_Byte( 0x04);
			Send_Byte( 0xB4);
			Send_Byte( 0x09);
		}

	}
}

oled.c

cpp 复制代码
#include "oled.h"
#include "oledfont.h" 
#include "pic.h"

extern const unsigned char Aciss_8X16[];//字符库
extern const unsigned char indexs[][3];//寻找汉字位置
const unsigned char GB2312[];//汉字库

/*
	@brief			初始化OLED与单片机的IO接口
	@param			无
	@retval			无
 */
static void OLED_GPIO_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;	//定义一个GPIO_InitTypeDef类型的结构体
	
	RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE);	//打开GPIOC的外设时钟
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13;	//选择控制的引脚
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;	//设置为通用开漏输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	//设置输出速率为50MHz
	GPIO_Init(GPIOB,&GPIO_InitStructure);	//调用库函数初始化GPIOA
	
	OLED_SCLK_Set();	//设PA5(SCL)为高电平
	OLED_SDIN_Set();	//设PA6(SDA)为高电平
}



/*
	@brief			模拟IIC起始信号
	@param			无
	@retval			无
 */
static void OLED_IIC_Start(void)
{

	OLED_SCLK_Set();	//时钟线置高
	OLED_SDIN_Set();	//信号线置高
	delay_us(1);	//延迟1us
	OLED_SDIN_Clr();	//信号线置低
	delay_us(1);	//延迟1us
	OLED_SCLK_Clr();	//时钟线置低
	delay_us(1);	//延迟1us
}


/*
	@brief			模拟IIC停止信号
	@param			无
	@retval			无
 */
static void OLED_IIC_Stop(void)
{
	OLED_SDIN_Clr();	//信号线置低
	delay_us(1);	//延迟1us
	OLED_SCLK_Set();	//时钟线置高
	delay_us(1);	//延迟1us
	OLED_SDIN_Set();	//信号线置高
	delay_us(1);	//延迟1us
}


/*
	@brief			模拟IIC读取从机应答信号
	@param			无
	@retval			无
 */
static unsigned char IIC_Wait_Ack(void)
{
	unsigned char ack;

	OLED_SCLK_Clr();	//时钟线置低
	delay_us(1);	//延迟1us
	OLED_SDIN_Set();	//信号线置高
	delay_us(1);	//延迟1us
	OLED_SCLK_Set();	//时钟线置高
	delay_us(1);	//延迟1us

	if(OLED_READ_SDIN())	//读取SDA的电平
		ack = IIC_NO_ACK;	//如果为1,则从机没有应答
	else
		ack = IIC_ACK;		//如果为0,则从机应答

	OLED_SCLK_Clr();//时钟线置低
	delay_us(1);	//延迟1us

	return ack;	//返回读取到的应答信息
}

static void Write_IIC_Byte(unsigned char IIC_Byte)
{
	unsigned char i;  //定义变量
	for(i=0;i<8;i++) //for循环8次
	{
		OLED_SCLK_Clr();	//时钟线置低,为传输数据做准备
		delay_us(1);	//延迟1us

		if(IIC_Byte & 0x80)	//读取最高位
		  	OLED_SDIN_Set();//最高位为1
		else
			OLED_SDIN_Clr();	//最高位为0

		IIC_Byte <<= 1;  //数据左移1位
		delay_us(1);	//延迟1us
		OLED_SCLK_Set(); //时钟线置高,产生上升沿,把数据发送出去
		delay_us(1);	//延迟1us
	}
	OLED_SCLK_Clr();	//时钟线置低
	delay_us(1);	//延迟1us

	while(IIC_Wait_Ack());	//从机应答
}

/*
	@brief			IIC写入命令
	@param			IIC_Command:写入的命令
	@retval			无
 */
static void Oled_Send_Cmd(unsigned char IIC_Command)
{
   OLED_IIC_Start();
   Write_IIC_Byte(0x78);//写入从机地址,SD0 = 0
   Write_IIC_Byte(0x00);//写入命令
   Write_IIC_Byte(IIC_Command);//数据
   OLED_IIC_Stop();  //发送停止信号
}


/*
	@brief			IIC写入数据
	@param			IIC_Data:数据
	@retval			无
 */
static void Oled_Send_Data(unsigned char IIC_Data)
{
   OLED_IIC_Start();
   Write_IIC_Byte(0x78);	//写入从机地址,SD0 = 0
   Write_IIC_Byte(0x40);	//写入数据
   Write_IIC_Byte(IIC_Data);//数据
   OLED_IIC_Stop();		//发送停止信号
}



/*
	@brief			开显示
	@param			无
	@retval			无
 */ 
void OLED_Display_On(void)
{
	Oled_Send_Cmd(0X8D);  //设置OLED电荷泵
	Oled_Send_Cmd(0X14);  //使能,开
	Oled_Send_Cmd(0XAF);  //开显示
}


/*
	@brief			关显示
	@param			无
	@retval			无
 */  
void OLED_Display_Off(void)
{
	Oled_Send_Cmd(0XAE);  //关显示
	Oled_Send_Cmd(0X8D);  //设置OLED电荷泵
	Oled_Send_Cmd(0X10);  //失能,关
}		   			 


/*
	@brief			清屏
	@param			无
	@retval			无
 */	  
void OLED_Clear(u8 data)  
{  
	u8 i,j;
	for(i=0;i<8;i++)
	{
		Oled_Send_Cmd(0xB0+i);//发送页起始地址
		Oled_Send_Cmd(0x00);//发送列地址低四位
		Oled_Send_Cmd(0x10);//发送列地址高四位
		for(j=0;j<128;j++)
		{
		 Oled_Send_Data(data);
		}
	}
}



void OLED_Init(void)
{
	OLED_GPIO_Init();	//GPIO口初始化
 
	delay_ms(200);	//延迟,由于单片机上电初始化比OLED快,所以必须加上延迟,等待OLED上复位完成

	Oled_Send_Cmd(0xAE);	//关闭显示

	Oled_Send_Cmd(0x00);	//设置低列地址
	Oled_Send_Cmd(0x10);	//设置高列地址
	Oled_Send_Cmd(0x40);	//设置起始行地址
	Oled_Send_Cmd(0xB0);	//设置页地址

	Oled_Send_Cmd(0x81); 	// 对比度设置,可设置亮度
	Oled_Send_Cmd(0xFF);	//  265  

	Oled_Send_Cmd(0xA1);	//设置段(SEG)的起始映射地址;column的127地址是SEG0的地址
	Oled_Send_Cmd(0xA6);	//正常显示;0xa7逆显示

	Oled_Send_Cmd(0xA8);	//设置驱动路数(16~64)
	Oled_Send_Cmd(0x3F);	//64duty
	
	Oled_Send_Cmd(0xC8);	//重映射模式,COM[N-1]~COM0扫描

	Oled_Send_Cmd(0xD3);	//设置显示偏移
	Oled_Send_Cmd(0x00);	//无偏移
	
	Oled_Send_Cmd(0xD5);	//设置震荡器分频
	Oled_Send_Cmd(0x80);	//使用默认值
	
	Oled_Send_Cmd(0xD9);	//设置 Pre-Charge Period
	Oled_Send_Cmd(0xF1);	//使用官方推荐值
	
	Oled_Send_Cmd(0xDA);	//设置 com pin configuartion
	Oled_Send_Cmd(0x12);	//使用默认值
	
	Oled_Send_Cmd(0xDB);	//设置 Vcomh,可调节亮度(默认)
	Oled_Send_Cmd(0x40);	使用官方推荐值
	
	Oled_Send_Cmd(0x8D);	//设置OLED电荷泵
	Oled_Send_Cmd(0x14);	//开显示
	
	Oled_Send_Cmd(0xAF);	//开启OLED面板显示
	OLED_Clear(0);        //清屏
//	Set_Postion(0,0); 	 //设置数据写入的起始行、列
//	OLED_Clear(0);
}  
 /***********************
函数名:Set_Postion
函数功能:
形参:
u8 page 页 
u8 col 列
返回值:void
函数说明:
页地址:0xb0
列低位地址:0x00
列高位地址:0x10
************************/
void Set_Postion(u8 page,u8 col)
{
	Oled_Send_Cmd(0xB0+page);//发送页起始地址
	Oled_Send_Cmd(0x00+(col&0x0f));//发送列地址低四位
	Oled_Send_Cmd(0x10+((col&0xf0)>>4));//发送列地址高四位
}  	     	  

/***********************
函数名:Oled_ShowPic
函数功能:OLED显示图片
形参:
u8 page 页     
u8 col 列     
u8 height  图片高度
u8 width   图片宽度
u8 *pic    图片数据
返回值:void
64*64
************************/

void Oled_ShowPic(u8 page,u8 col,u8 height,u8 width,u8 *pic)
{
	u8 i,j;
	for(i=0;i<height/8;i++)//活得页范围
	{
		Set_Postion(page+i,col);//定义框架
		for(j=0;j<width;j++)
		{
			Oled_Send_Data(pic[j+i*width]);//往框架输入数据,以列方式
		}
	}

}
//清空某字符
void Oled_clear(u8 page,u8 col,u8 height,u8 width)
{
	u8 i,j;
	for(i=0;i<height/8;i++)//活得页范围
	{
		Set_Postion(page+i,col);//定义框架
		for(j=0;j<width;j++)
		{
			Oled_Send_Data(0x00);//往框架输入数据,以列方式
		}
	}

}


/***********************
函数名:Oled_ShowChar
函数功能:OLED显示字符
形参:
u8 page 页     
u8 col 列     

u8 pic    字符数据
返回值:void
8*16
************************/


void Oled_ShowChar(u8 page,u8 col,u8 eng)
{
	u8 i,j,Char;
	Char=eng-' ';//获取字符所在位置
	for(i=0;i<2;i++)
	{
		Set_Postion(page+i,col);
		for(j=0;j<8;j++)
		{
			Oled_Send_Data(Aciss_8X16[j+Char*16+i*8]);
		}
	}
}
/***********************
函数名:Oled_Showstring
函数功能:OLED显示字符串
形参:
u8 page 页  2
u8 col 列    8 

u8 *str    字符串
返回值:void
8*16
************************/
void Oled_Showstring(u8 page,u8 col,u8*str)
{

	while(*str !='\0')
	{
		if(col>=128)
		{
			col=0;
			page +=2;

		}
		if(page>=8)
		{
			page=0;
		}
		Oled_ShowChar(page,col,*str);
		str++;//字符递加
		col +=8;//每显示完一个字符,列+8				
}
		
}

/***********************
函数名:Oled_ShowChi
函数功能:OLED显示汉字
形参:
u8 page 页     
u8 col 列     
u8 *chi    显示汉字
返回值:void
    16*16  = 32 byte
************************/

void Oled_ShowChi(u8 page,u8 col,u8*chi)
{
	u8 i,j,z;
	for(i=0;i<sizeof(indexs)/sizeof(indexs[0]);i++)
	{
		if(*chi == indexs[i][0]&&*(chi+1)==indexs[i][1])//比较汉字在第几个位置
		{
			break;
		}
	}
	for(j=0;j<2;j++)
	{
		Set_Postion(page+j,col);
		for(z=0;z<16;z++)
		{
			Oled_Send_Data(GB2312[z+i*32+j*16]);
		}

	}

}

/***********************
函数名:Oled_ShowAll
函数功能:OLED显示汉英字符串
形参:
u8 page 页     
u8 col 列     
u8 *str    显示汉字
返回值:void
		8*16;
    16*16  = 32 byte
************************/


void Oled_ShowAll(u8 page ,u8 col ,u8*str)
{

	while(*str != '\0')
{
	
	if(*str>127)
	{
		if(col>=120)//放不下字
		{
			page +=2;//
			col=0;

		}
	if(page>=8)
		{
				page=0;
		}
		Oled_ShowChi(page,col,str);
		col +=16;
		str +=2;
	}
	else
	{
		if(col>=128)//放不下字
		{
			page +=2;//
			col=0;

		}
			if(page>=8)
		{
				page=0;
		}
	 Oled_ShowChar(page,col,*str);
		str++;
		col +=8;
	}
	
	
	
	
	}


}

/*
	@brief			OLED滚屏函数,范围0~1页,水平向左
	@param			无
	@retval			无
 */	
void OLED_Scroll(void)
{
	Oled_Send_Cmd(0x2E);	//关闭滚动
	Oled_Send_Cmd(0x27);	//水平向左滚动
	Oled_Send_Cmd(0x00);	//虚拟字节
	Oled_Send_Cmd(0x00);	//起始页 0
	Oled_Send_Cmd(0x00);	//滚动时间间隔
	Oled_Send_Cmd(0x01);	//终止页 1
	Oled_Send_Cmd(0x00);	//虚拟字节
	Oled_Send_Cmd(0xFF);	//虚拟字节
	Oled_Send_Cmd(0x2F);	//开启滚动
}

六、结语

到这一步,基本已经配置成功了,是不是很简单,只需要按照程序来做,大家都能顺利成功读取到RS485的综合土壤传感器的数据。需要代码的可以去我的主页获取下载,只需5积分,最后附上代码获取网址:

https://download.csdn.net/download/weixin_52680858/90121983

看到这的同学,如果觉得对你有用,就麻烦大家点点赞和收藏,谢谢!!!!

---------以下内容为打广告-----------

本人承接各种单片机设计,价格实惠,有需要的同学可联系本人QQ1972218606

---------------------------------------------
------------------------------------------------

相关推荐
Hello_Embed4 小时前
STM32HAL 快速入门(二十):UART 中断改进 —— 环形缓冲区解决数据丢失
笔记·stm32·单片机·学习·嵌入式软件
亿坊电商6 小时前
物联网领域中PHP框架的最佳选择有哪些?
物联网·struts·php
一起搞IT吧6 小时前
嵌入式ARM SOC开发中文专题分享一:ARM SOC外围资源介绍
arm开发·嵌入式硬件
研华嵌入式6 小时前
如何在高通跃龙QCS6490 Arm架构上使用Windows 11 IoT企业版?
arm开发·windows·嵌入式硬件
御控工业物联网6 小时前
灌区泵站远程监控物联网网关解决方案
物联网·远程监控·物联网网关·泵站·灌区泵站
矢志不移7927 小时前
裸机开发 时钟配置,EPIT
单片机·嵌入式硬件
清风6666667 小时前
基于STM32的APP遥控视频水泵小车设计
stm32·单片机·mongodb·毕业设计·音视频·课程设计
物随心转10 小时前
RTC驱动原理
嵌入式硬件
BAGAE10 小时前
MODBUS 通信协议详细介绍
linux·嵌入式硬件·物联网·硬件架构·iot·嵌入式实时数据库·rtdbs
风_峰11 小时前
Petalinux相关配置——ZYNQ通过eMMC启动
嵌入式硬件·ubuntu·fpga开发