PID温控实验平台搭建(四)——PID温控系统实验代码讲解

2023-11-05

PID温控实验平台搭建

(一)PID基础知识介绍

(二)PID进阶知识介绍及源码分享

(三)从零开始搭建STM32温控实验平台

(四)PID温控系统代码讲解

(五)最终实验现象与总结


文章目录


前言

最近,我突发奇想去翻阅了一些我本科期间所做的一些小项目,发现都挺有意思的!当年做这些项目的时候可走了很多弯路,所以想着可以将它们上传到网络上,并通过我的讲解文章可以帮助你们少走一些弯路!

今天,我要分享的是一个PID温控实验平台的搭建,因为我想要讲的详细一点,所以打算做成一个系列,手把手地教你学习和认识PID算法,了解这种算法在温度控制中的应用。由于我知识有限,如果出现一些错误,希望大家可以帮助我指出来,我们一起学习进步!!!


一、主程序功能描述

主函数的运行过程:

在开始温控之前,会先进行一段时间的软件配置,软件配置成功后。当DS18B20温度传感器检测到温度低于起始温度(30­°C)时,将会开始加热到起始温度(30­°C);当高于起始温度(30­°C)时,将会冷却等待,直到温度达到起始温度(30­°C);而温度变化数据将会实时显示在OLED屏幕上,让我们可以实时观察到控温趋势,并且通过串口发送及VOFA++上位机显示,实验数据曲线可以实时显示在电脑屏幕上,最终便于我们总结规律,得出结论。

VOFA++上位机

这是一款最直观、灵活、强大的插件驱动高自由度的上位机,我们通过特定的数据格式,来获得实时的数据曲线,便于我们直观地总结规律,归纳结论,并且VOFA++自由度很高,可以定制化数据的可视化方式,从而让我们可以更便捷地整定PID参数!

09954d71294648f0a616f20193729527.png

图1 VOFA++上位机

流水灯含义解释:

1)红灯闪烁代表正在进行配置所有软件;

2)绿灯闪烁代表加热棒正在工作,PID控制正在进行;

3)黄灯闪烁代表正在加热到起始温度(30­°C);

4)蓝灯闪烁代表正在冷却到起始温度(30­°C);

5)青灯亮起代表马上进入PID温控;

 2bfe4913cbd74ec7996b6c6d2bf73583.gif413b416200144cd8b3732a9961cb0528.gif

(主程序代码)

/******************main.c***********************/
// 温度
float T=0.0;
// 媒介变量
int i = 0;
// 定义PID结构体并初始化
PID pid;

// 一次PID调节的时间
#define   WAIT_TIME     20
// 采样时间
#define   SAMPLE_TIME   200
// 最大输出
#define   MAX_OUT      10000

// 数组的元素的个数
#define   ARR_NUM       4
// PID参数
const float Kp = 90;
const float Ki = 0.15;
const float Kd[ARR_NUM] = {1000,2000,3000,4000};

// 声明
void All_Soft_Config(void);
void Wait_Temperature_Init(void);

// 可以循环测试PID参数(1、参数改宏改;2、PID初始化改)
int main()
{
	// 所有软件配置
	All_Soft_Config();

	for(uint8_t n=0;n<ARR_NUM;n++)
	{
		// i值清0
		i = 0;
		// 屏幕清空
		OLED_Fill(0x00);
		// 等待温度到达30度
		Wait_Temperature_Init();
		// 清空PID
		PID_Clear(&pid);
		// PID初始化
		PID_Init(&pid,Kp,Ki,Kd[n],10000,MAX_OUT);
		// 绿灯亮
		LED_GREEN;
		while(1)
		{
			// 绿灯闪烁
			LED2_TOGGLE;
			/* 测温 */
			T = DS18B20_Update_Temperature();      // DS18B20更新温度
			OLED_display_DS18B20_line(i,T);        // 实时显示温度线
			
			/* 根据PID计算值调整脉冲 */
			PID_SingleCalc(&pid, 70, T);           // 单级PID计算
			Pulse_Wave = pid.output;               // 改变其脉冲宽度

			
			i++;
			for(uint8_t tt = 0;tt<SAMPLE_TIME/200;tt++)
			{
				T = DS18B20_Update_Temperature();      // DS18B20更新温度
				OLED_display_DS18B20_line(i,T);        // 实时显示温度线
				// 打印出波形
				printf("Temperature-Pid: %.4f,%d,%d,%d,%.1f,%.3f,%.2f,%d\n",T,70,Pulse_Wave,i*SAMPLE_TIME/1000,pid.kp,pid.ki,pid.kd,SAMPLE_TIME);
				SysTick_Delay_ms(200);
			}
			
			
			// 若超出时间或者达到了特定温度直接换一个参数
			if(T>=110 || i >=(WAIT_TIME*60)*1000/SAMPLE_TIME)
			{
				for(uint8_t zero_time=0;zero_time<5;zero_time++)
				{
					// PWM的值必须清空
					Pulse_Wave = 0;
					SysTick_Delay_ms(200);
				}
				break;
			}
			// 等待两分钟若是没有变化则代表线掉了
			else if((i >= (2*60)*1000/SAMPLE_TIME)&&(T<40))
			{
				/* 线掉了 */
				LED_CYAN;
				printf("线掉了....");
				for(uint8_t zero_time=0;zero_time<5;zero_time++)
				{
					// PWM的值必须清空
					Pulse_Wave = 0;
					SysTick_Delay_ms(200);
				}
				while(1)
				{
					;;
				}	
			}
		}
	}

	
	while(1)
	{
		LED_RED;
		T = DS18B20_Update_Temperature();
		OLED_display_DS18B20(T);
//		printf("Temperature-Pid:%.4f\n",T);
		SysTick_Delay_ms(200);
	}
}

// 全部软件配置
void All_Soft_Config()
{
	// LED端口初始化
	LED_GPIO_Config();
	// 红灯亮
	LED_RED;
	//按键端口初始化
	Key_GPIO_Config();
	//定时器初始化,输出PWM波
	TEMP_PWM_TIM_Init();
	// 打开串口以输出调试信息
	USART_Config();
	// I2C GPIO引脚初始化
	I2C_GPIO_Init();
	// 配置DS18B20温度传感器
	DS18B20_Configure();
	// 配置OLED显示屏
	OLED_SSD1306_Configure();
	
}

/* 只有当温度恰好在30度附近的时候才会启动函数 */
void Wait_Temperature_Init()
{
	uint8_t i=0;
	// 测量一次温度
	T = DS18B20_Update_Temperature();
	OLED_display_DS18B20(T);
	printf("温度为%.4f℃\n",T);
	// 温度大于30,一直等待直到温度在30度附近
	if(T >= 30)
	{
		chill:
		Pulse_Wave = 0;
		LED_BLUE;
		while (T > 30)
		{
			// 蓝灯闪烁等待温度降下来
			LED3_TOGGLE
			// DS18B20更新温度
			T = DS18B20_Update_Temperature();
			OLED_display_DS18B20(T);
			printf("温度为%.4f℃\n",T);
			SysTick_Delay_ms(1000);
		}
		// 青灯亮起
		LED_CYAN;
		SysTick_Delay_ms(1000);
	}
	// 温度小于30度
	else
	{
		// 1000 加热
		Pulse_Wave = 0.1*MAX_OUT;
		LED_YELLOW;
		// 加热到33度附近
		while(T < 30)
		{
			i++;
			// 黄灯闪烁
			LED1_TOGGLE
			LED2_TOGGLE;
			// DS18B20更新温度
			T = DS18B20_Update_Temperature();
			OLED_display_DS18B20(T);
			printf("温度为%.4f℃\n",T);
			SysTick_Delay_ms(500);
			/* 线掉了 */
			if(i>2*60*2)
			{
				LED_CYAN;
				printf("线掉了....\n");
				Pulse_Wave = 0;
				while(1)
				{
					;;
				}
			}
			
		}
		goto chill;
	}

}

二、部分代码讲解

1、PID程序

PID程序已经在上一节介绍过了,不再赘述!

PID温控实验平台搭建(二)——PID进阶知识介绍及源码分享https://blog.csdn.net/qq_35953617/article/details/127849549https://blog.csdn.net/qq_35953617/article/details/127849549


2、PWM输出

/******************bsp_pwm.h***********************/

/************通用定时器TIM参数定义,只限TIM2、3、4、5************/
// 当使用不同的定时器的时候,对应的GPIO是不一样的,这点要注意
// 我们这里默认使用TIM3
/* ----------------   PWM信号 周期和占空比的计算--------------- */
// ARR :自动重装载寄存器的值
// CLK_cnt:计数器的时钟,等于 Fck_int / (psc+1) = 72M/(psc+1)
// PWM 信号的周期 T = ARR * (1/CLK_cnt) = ARR*(PSC+1) / 72M
// 占空比P=CCR/(ARR+1)

/* PWM波占空比(0 ~ 10000) */
extern uint16_t Pulse_Wave;

// 选择TIM3通用定时器
#define            TEMP_PWM_TIM                   TIM3
#define            TEMP_PWM_TIM_APBxClock_FUN     RCC_APB1PeriphClockCmd
#define            TEMP_PWM_TIM_CLK               RCC_APB1Periph_TIM3

// ARR的值为 10000,实际装载 Pulse_Wave 次,频率F = 100Hz
#define            TEMP_PWM_TIM_Period            (10000-1)

// 分频因子为 72-1
#define            TEMP_PWM_TIM_Prescaler         (72-1)
// PWM的脉冲宽度为 5000,占空比得出是50%
#define            TEMP_PWM_TIM_Pulse              Pulse_Wave
#define            TEMP_PWM_TIM_CCRx               CCR1

// TIM3 输出比较通道1
#define            TEMP_PWM_TIM_CH1_GPIO_CLK      RCC_APB2Periph_GPIOC
#define            TEMP_PWM_TIM_CH1_PORT          GPIOC
#define            TEMP_PWM_TIM_CH1_PIN           GPIO_Pin_6

// TIM3中断配置
#define   		   TEMP_PWM_TIMx_IRQn              TIM3_IRQn            //中断
#define            TEMP_PWM_TIMx_IRQHandler        TIM3_IRQHandler


/******************bsp_pwm.c***********************/

/* PWM波占空比(0~10000) */
uint16_t Pulse_Wave = 0;

/**
  * @brief  TIM定时器GPIO口配置
  * @param  无
  * @retval 无
  */
static void TEMP_PWM_TIM_GPIO_Config(void) 
{
	GPIO_InitTypeDef GPIO_InitStructure;

	// 开启重映射时钟(非常重要)
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
	// 输出比较通道1 GPIO 初始化
	RCC_APB2PeriphClockCmd(TEMP_PWM_TIM_CH1_GPIO_CLK, ENABLE);
	GPIO_InitStructure.GPIO_Pin =  TEMP_PWM_TIM_CH1_PIN;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(TEMP_PWM_TIM_CH1_PORT, &GPIO_InitStructure);

}


/**
  * @brief  配置嵌套向量中断控制器NVIC
  * @param  无
  * @retval 无
  */
static void NVIC_Config_PWM(void)
{
  NVIC_InitTypeDef NVIC_InitStructure;
  
  /* Configure one bit for preemption priority */
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
  
  /* 配置TIM3_IRQ中断为中断源 */
  NVIC_InitStructure.NVIC_IRQChannel = TEMP_PWM_TIMx_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
}



///*
// * 注意:TIM_TimeBaseInitTypeDef结构体里面有5个成员,TIM6和TIM7的寄存器里面只有
// * TIM_Prescaler和TIM_Period,所以使用TIM6和TIM7的时候只需初始化这两个成员即可,
// * 另外三个成员是通用定时器和高级定时器才有.
// *-----------------------------------------------------------------------------
// *typedef struct
// *{ TIM_Prescaler            都有
// *	TIM_CounterMode			     TIMx,x[6,7]没有,其他都有
// *  TIM_Period               都有
// *  TIM_ClockDivision        TIMx,x[6,7]没有,其他都有
// *  TIM_RepetitionCounter    TIMx,x[1,8,15,16,17]才有
// *}TIM_TimeBaseInitTypeDef; 
// *-----------------------------------------------------------------------------
// */

/* ----------------   PWM信号 周期和占空比的计算--------------- */
// ARR :自动重装载寄存器的值
// CLK_cnt:计数器的时钟,等于 Fck_int / (psc+1) = 72M/(psc+1)
// PWM 信号的周期 T = ARR * (1/CLK_cnt) = ARR*(PSC+1) / 72M
// 占空比P=CCR/(ARR+1)

static void TEMP_PWM_TIM_Mode_Config(void)
{
  // 开启定时器时钟,即内部时钟CK_INT=72M
	TEMP_PWM_TIM_APBxClock_FUN(TEMP_PWM_TIM_CLK,ENABLE);

/*--------------------时基结构体初始化-------------------------*/
	// 配置周期,这里配置为100K
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
	// 自动重装载寄存器的值,累计TIM_Period+1个频率后产生一个更新或者中断
	TIM_TimeBaseStructure.TIM_Period=TEMP_PWM_TIM_Period;	
	// 驱动CNT计数器的时钟 = Fck_int/(psc+1)
	TIM_TimeBaseStructure.TIM_Prescaler= TEMP_PWM_TIM_Prescaler;	
	// 时钟分频因子 ,配置死区时间时需要用到
	TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;		
	// 计数器计数模式,设置为向上计数
	TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;		
	// 重复计数器的值,没用到不用管
	TIM_TimeBaseStructure.TIM_RepetitionCounter=0;
	// 初始化定时器
	TIM_TimeBaseInit(TEMP_PWM_TIM, &TIM_TimeBaseStructure);
	
	// //改变指定管脚的映射 这里选择的是TIM3完全重映射(非常重要)
	GPIO_PinRemapConfig(GPIO_FullRemap_TIM3 ,ENABLE);

	/*--------------------输出比较结构体初始化-------------------*/	
	TIM_OCInitTypeDef  TIM_OCInitStructure;
	// 配置为PWM模式1
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
	// 输出使能
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;

	// 输出通道电平极性配置	
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
	
	// 空闲时的电平(低电平)
	TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Reset;
	
	// 输出比较通道 1
	TIM_OCInitStructure.TIM_Pulse = TEMP_PWM_TIM_Pulse;
	TIM_OC1Init(TEMP_PWM_TIM, &TIM_OCInitStructure);
	//使能预装载
	TIM_OC1PreloadConfig(TEMP_PWM_TIM, TIM_OCPreload_Enable);
	//使能TIM重载寄存器ARR
	TIM_ARRPreloadConfig(TEMP_PWM_TIM, ENABLE);
	
	// 使能计数器
	TIM_Cmd(TEMP_PWM_TIM, ENABLE);
	
	// 主输出使能(通用定时器不需要)
//	TIM_CtrlPWMOutputs(TEMP_PWM_TIM, ENABLE);

	//使能update中断
	TIM_ITConfig(TEMP_PWM_TIM, TIM_IT_Update, ENABLE);	
	
	// PWM 中断设置
	NVIC_Config_PWM();
}


// 定时器配置
void TEMP_PWM_TIM_Init(void)
{
	// GPIO设置
	TEMP_PWM_TIM_GPIO_Config();
	// 定时器模式设置
	TEMP_PWM_TIM_Mode_Config();	
	
	// 以往脉冲存储起来
	Pulse_Update_Temp = Pulse_Wave;
}

/******************stm32f10x_it.c***********************/

extern float T;
/* PWM波中断服务函数 */
void TEMP_PWM_TIMx_IRQHandler(void)
{		
	if (TIM_GetITStatus(TEMP_PWM_TIM , TIM_IT_Update) != RESET)	//TIM_IT_Update
 	{
		// 温度达到120警戒
		if(T >=120)
		{
			printf("DS18B20温度目前超过120度,必须停下来...");
			// 让PWM波的值为0
			TEMP_PWM_TIM -> TEMP_PWM_TIM_CCRx = 0;
			LED_PURPLE;
			// 程序在这卡死
			while(1)
			{
				;;
			}
		
		}
		if (Pulse_Update_Temp != Pulse_Wave)
		{
			// 修改CCR的值
			TEMP_PWM_TIM -> TEMP_PWM_TIM_CCRx = Pulse_Wave;	//根据PWM表修改定时器的比较寄存器值
			Pulse_Update_Temp = Pulse_Wave;
		}
		TIM_ClearITPendingBit(TEMP_PWM_TIM, TIM_IT_Update);	//必须要清除中断标志位
	}
}


3、DS18B20传感器代码

/******************bsp_one_wire.h***********************/
#include "stm32f10x.h"


/************(单总线)DS18B20温度传感器相关************/
#define   		   ONE_WIRE_GPIO_PORT              GPIOB
#define   		   ONE_WIRE_GPIO_PIN               GPIO_Pin_9
#define            RCC_ONE_WIRE_CLK 	           RCC_APB2Periph_GPIOB	


/******************bsp_one_wire.c***********************/
#include "bsp_one_wire.h" 
#include "bsp_systick.h"

/**
  * @brief  (单总线)DS18B20温度传感器DQ口配置(开漏输出)
  * @param  无
  * @retval 无
  */
void DS18B20_GPIO_Config(void) 
{
	GPIO_InitTypeDef GPIO_InitStructure;

	// 输出比较通道1 GPIO 初始化
	RCC_APB2PeriphClockCmd(RCC_ONE_WIRE_CLK, ENABLE);
	GPIO_InitStructure.GPIO_Pin =  ONE_WIRE_GPIO_PIN;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(ONE_WIRE_GPIO_PORT, &GPIO_InitStructure);

}

/**
  * @brief  DS18B20开始温度变换
  * @param  无
  * @retval 无
  */
void DS18B20_ConvertT(void)
{
	// GPIO配置
	DS18B20_GPIO_Config();
	OneWire_Init();
	OneWire_SendByte(DS18B20_SKIP_ROM);
	OneWire_SendByte(DS18B20_CONVERT_T);
}

/**
  * @brief  DS18B20读取温度
  * @param  无
  * @retval 温度数值
  */
float DS18B20_ReadT(void)
{
	unsigned char TLSB,TMSB;
	int Temp;
	float T;
	OneWire_Init();
	OneWire_SendByte(DS18B20_SKIP_ROM);
	OneWire_SendByte(DS18B20_READ_SCRATCHPAD);
	TLSB=OneWire_ReceiveByte();
	TMSB=OneWire_ReceiveByte();
	Temp=(TMSB<<8)|TLSB;
	T=Temp/16.0;

	return T;
}

// 配置DS18B20
void DS18B20_Configure()
{
	//上电先转换一次温度,防止第一次读数据错误
	DS18B20_ConvertT();	
	//等待转换完成
	SysTick_Delay_ms(1000);
}


// DS18B20更新温度
float DS18B20_Update_Temperature()
{
	float T;
	//转换温度
	DS18B20_ConvertT();
	//读取温度
	T = DS18B20_ReadT();
	// 返回温度值
	return T;
}


4、OLED显示

/******************bsp_spi_oled.h***********************/
/* 等待超时时间 */
#define SPIT_FLAG_TIMEOUT         ((uint32_t)0x1000)
#define SPIT_LONG_TIMEOUT         ((uint32_t)(10 * SPIT_FLAG_TIMEOUT))


/* 时钟 */
// SPI时钟
#define     OLED_SSD1306_SPIx     						   SPI1   
#define     OLED_SSD1306_SPI_APBxClock_FUN   		       RCC_APB2PeriphClockCmd
#define     OLED_SSD1306_SPI_CLK						   RCC_APB2Periph_SPI1
// OLED时钟 
#define     OLED_SSD1306_GPIO_CLK		                   (OLED_SSD1306_SPI_SCK_CLK|OLED_SSD1306_SPI_MOSI_CLK |OLED_SSD1306_RES_CLK|OLED_SSD1306_DC_CLK|OLED_SSD1306_SPI_CS_CLK|SPI_MISO_CLK )
#define     OLED_SSD1306_APBxClock_FUN                 		RCC_APB2PeriphClockCmd


/* OLED(SSD1306)*/
// 时钟线-D0
#define     OLED_SSD1306_SPI_SCK_GPIO_PORT             GPIOA
#define		OLED_SSD1306_SPI_SCK_GPIO_PIN              GPIO_Pin_5
#define     OLED_SSD1306_SPI_SCK_CLK                   RCC_APB2Periph_GPIOA  
// 主机输出-D1
#define     OLED_SSD1306_SPI_MOSI_GPIO_PORT            GPIOA
#define     OLED_SSD1306_SPI_MOSI_GPIO_PIN             GPIO_Pin_7
#define     OLED_SSD1306_SPI_MOSI_CLK                  RCC_APB2Periph_GPIOA
// 复位脚-RES
#define     OLED_SSD1306_RES_GPIO_PORT                 GPIOA
#define		OLED_SSD1306_RES_GPIO_PIN                  GPIO_Pin_4
#define     OLED_SSD1306_RES_CLK                       RCC_APB2Periph_GPIOA  
// 数据命令控制脚-DC 
#define     OLED_SSD1306_DC_GPIO_PORT                  GPIOE
#define		OLED_SSD1306_DC_GPIO_PIN                   GPIO_Pin_6
#define     OLED_SSD1306_DC_CLK                        RCC_APB2Periph_GPIOE  
// 软件片选-CS
#define     OLED_SSD1306_SPI_CS_GPIO_PORT              GPIOE
#define		OLED_SSD1306_SPI_CS_GPIO_PIN               GPIO_Pin_5
#define     OLED_SSD1306_SPI_CS_CLK                    RCC_APB2Periph_GPIOE  

/* 主机输入(OLED没用上) */
#define     SPI_MISO_GPIO_PORT                       GPIOA
#define	    SPI_MISO_GPIO_PIN                        GPIO_Pin_6
#define     SPI_MISO_CLK                             RCC_APB2Periph_GPIOA  
 

/* 片选拉高拉低 */
#define  		SPI_OLED_CS_LOW()     						GPIO_ResetBits(OLED_SSD1306_SPI_CS_GPIO_PORT, OLED_SSD1306_SPI_CS_GPIO_PIN)
#define  		SPI_OLED_CS_HIGH()    						GPIO_SetBits(OLED_SSD1306_SPI_CS_GPIO_PORT, OLED_SSD1306_SPI_CS_GPIO_PIN)


/* 数据命令控制脚拉高拉低 */
#define  		OLED_DC_LOW()     						    GPIO_ResetBits(OLED_SSD1306_DC_GPIO_PORT, OLED_SSD1306_DC_GPIO_PIN)
#define  		OLED_DC_HIGH()    						    GPIO_SetBits(OLED_SSD1306_DC_GPIO_PORT, OLED_SSD1306_DC_GPIO_PIN)

/* 复位引脚拉高拉低 */
#define  		OLED_RES_LOW()     						    GPIO_ResetBits(OLED_SSD1306_RES_GPIO_PORT, OLED_SSD1306_RES_GPIO_PIN)
#define  		OLED_RES_HIGH()    						    GPIO_SetBits(OLED_SSD1306_RES_GPIO_PORT, OLED_SSD1306_RES_GPIO_PIN)


/******************bsp_spi_oled.c***********************/


unsigned char OLED_GRAM[128][8] = {0};

/*
*********************************************************************************************************
*	函 数 名: OLED_SSD1306_Init
*	功能说明: OLED(SSD1306)初始化
*	形    参:无
*	返 回 值: 无
*********************************************************************************************************
*/
void OLED_SSD1306_Init(void)
{
	// GPIO初始化
	OLED_GPIO_Init();
	// SPI初始化
	OLED_SPI_Init();
	
	//延时1秒稳定端口状态
	SysTick_Delay_ms(1000);
	
	/* 复位 */
	OLED_RES_LOW();
	SysTick_Delay_ms(10);
	/* 复位正常信号 SSD1306: RES引脚高电平 */
	OLED_RES_HIGH();
	
	OLED_Write_Cmd(0xAE); //关闭显示
	
	OLED_Write_Cmd(0x20); //设置模式
	OLED_Write_Cmd(0x10); //设置为页显示模式

	OLED_Write_Cmd(0xb0);	// 设置起始页的地址模式 0-7
	OLED_Write_Cmd(0xc8); // 0xc9上下反置 0xc8正常

	OLED_Write_Cmd(0x00); // ---set low column address
	OLED_Write_Cmd(0x10); // ---set high column address
	
	OLED_Write_Cmd(0x40); //--set start line address
	OLED_Write_Cmd(0x81); //--set contrast control register
	OLED_Write_Cmd(0xff); //亮度调节 0x00~0xff
	
	OLED_Write_Cmd(0xa1); // 0xa0左右反置 0xa1正常
	OLED_Write_Cmd(0xa6); //设置显示方式;bit0:1,反相显示;0,正常显示

	OLED_Write_Cmd(0xa8); //--set multiplex ratio(1 to 64)
	OLED_Write_Cmd(0x3F); //

	OLED_Write_Cmd(0xa4); //全局显示开启;0xa4正常,0xa5无视命令点亮全屏

	OLED_Write_Cmd(0xd3); //-set display offset
	OLED_Write_Cmd(0x00); //-not offset

	OLED_Write_Cmd(0xd5); //设置时钟分频因子,震荡频率
	OLED_Write_Cmd(0xf0); //[3:0],分频因子;[7:4],震荡频率
	
	OLED_Write_Cmd(0xd9); //--set pre-charge period
	OLED_Write_Cmd(0x22); //

	OLED_Write_Cmd(0xda); //--set com pins hardware configuration
	OLED_Write_Cmd(0x12);
	
	OLED_Write_Cmd(0xdb); //--set vcomh
	OLED_Write_Cmd(0x20); //0x20,0.77xVcc

//	OLED_Write_Cmd(0x81); //设置对比度
//	OLED_Write_Cmd(0x7f); // 128

	OLED_Write_Cmd(0x8d); //设置电荷泵开关
	OLED_Write_Cmd(0x14); //开

	OLED_Write_Cmd(0xaf); //开启显示

}



 /**
	* @brief  OLED_SetPos,设置光标
	* @param  x,光标x位置
	*		  y,光标y位置
	* @retval 无
  */
void OLED_SetPos(unsigned char x, unsigned char y) //设置起始点坐标
{ 
	OLED_Write_Cmd(0xb0+y);
	OLED_Write_Cmd(((x&0xf0)>>4)|0x10);
	OLED_Write_Cmd((x&0x0f)|0x01);
}


 /**
  * @brief  OLED_Fill,填充整个屏幕
  * @param  fill_Data:要填充的数据
	* @retval 无
  */
void OLED_Fill(unsigned char fill_Data)//全屏填充
{
	unsigned char m,n;
	for(m=0;m<8;m++)
	{
		OLED_Write_Cmd(0xb0+m);		//page0-page1
		OLED_Write_Cmd(0x00);		//low column start address
		OLED_Write_Cmd(0x10);		//high column start address
		for(n=0;n<128;n++)
		{
			OLED_Write_Data(fill_Data);
			OLED_GRAM[n][m] = fill_Data;
		}
	}
}


 /**
  * @brief  OLED_CLS,清屏
  * @param  无
	* @retval 无
  */
void OLED_CLS(void)//清屏
{
	OLED_Fill(0x00);
}


 /**
	* @brief  OLED_Refresh_Gram,刷新整个屏幕数组并显示
	* @param  无
	* @retval 无
  */
void OLED_Refresh_Gram(void)
{
    unsigned char i,n;
    for(i=0;i<8;i++)
    {
		// 起始点开始全部刷新
        OLED_Write_Cmd(0xb0+i);  //设置页地址(0~7)
        OLED_Write_Cmd(0x00);    //设置显示位置—列低地址
        OLED_Write_Cmd(0x10);    //设置显示位置—列高地址   
        for(n=0;n<128;n++)  //写一PAGE的GDDRAM数据
        {
			// 设置起始点坐标
			OLED_Write_Data(OLED_GRAM[n][i]);
        }
    }
}


 /**
	* @brief  OLED_Part_Refresh_Gram,刷新部分屏幕数组并显示
	* @param  i : 第i页位置,(0~7);
	*		  n0,n1 : 从第n0列开始刷新,刷新到第n1列
	* @retval 无
  */
void OLED_Part_Refresh_Gram(unsigned char i,unsigned char n0,unsigned char n1)
{
	// 起始的点开始刷新
	OLED_SetPos(n0-1, i);
	// 写部分数组的数据
	for(;n0-1<n1+1;n0++)
	{
		// 设置起始点坐标
		OLED_Write_Data(OLED_GRAM[n0-1][i]);
	}

}



 /**
	* @brief  OLED_DrawDot,画点函数
	* @param  x,y : 绘画点的坐标(x:0~127, y:0~63);
	*		  t   : 0表示该像素不显示,1表示该像素显示 , -1表示像素点直接取反
	* @retval 无
  */
void OLED_DrawDot(unsigned char x,unsigned char y,unsigned char t)
{
	unsigned char pos,bx,temp=0;
		
	// 此OLED的分辨率为128*64,横坐标大于127,纵坐标大于63,则参数非法 
		
	if(x>127||y>63) return;
		
	// 因为此OLED是按页显示,每页8个像素,所以/8用于计算待显示的点在哪页中
	pos=(y)/8;
		
	// 一列中有8个像素,所以计算一下待显示的点,在当前列中的第几个点
	bx=y%8;
		
	// 移位,让temp的第bx位为1
	temp=1<<(bx);
		
	if(t==1)
	{
		OLED_GRAM[x][pos]|=temp;  //第bx位,置1,其他位值不变
	}
	else if(t==0)
	{
		OLED_GRAM[x][pos]&=~temp;  //第bx位,置0,其他位值不变
	}
	else
	{
		OLED_GRAM[x][pos]^=temp;  //第bx位,直接异或取反
	
	}
	

//	// 刷新整个液晶屏
	OLED_Refresh_Gram();
	
	// 部分刷新屏幕(这样会快)
//	OLED_Part_Refresh_Gram(pos,x,x);
	
}


 /**
	* @brief  OLED_DrawLine,画线函数
	* @param  x1,y1 : 起始点坐标(x1:0~127, y1:0~63);
	*		x2,y2 : 终点(结束点)的坐标(x2:0~128,y2:0~63)
	*		m : 1 为直接点亮,0 为直接点灭,-1为直接取反
	* @retval 无
  */
void OLED_DrawLine(unsigned int x1, unsigned int y1, unsigned int x2,unsigned int y2, unsigned int m)
{
	unsigned int t; 
	int offset_x,offset_y; 
	int incx,incy,uRow,uCol; 
	float K = 0.0f;
	offset_x=x2-x1;
	offset_y=y2-y1; 
	uRow=x1; 
	uCol=y1; 
	if(offset_x>0)
	{
		incx=1;
	}
	else if(offset_x==0)
	{
		incx=0;    //垂直线
	}
	else 
	{
		incx=-1;
		offset_x=-offset_x;
	}

	if(offset_y>0)
	{
		incy=1;
	}
	else if(offset_y==0)
	{
		incy=0;    //水平线
	}
	else
	{
		incy=-1;
		offset_y=-offset_y;
	}

	// 垂直线
	if(incx==0)
	{
		for(t=0;t<=offset_y+1;t++ )
		{ 
			OLED_DrawDot(uRow,uCol+t*incy,m);
		}
	}
	// 水平线
	else if(incy==0)
	{
		for(t=0;t<=offset_x+1;t++ )
		{ 
			OLED_DrawDot(uRow+t*incx,uCol,m);
		}
	}
	else
	{
		K = (float)(((float)y2-(float)y1)*1.000/((float)x2-(float)x1));
		printf("K=%.3f\r\n",K);
		for(t=0;t<=offset_x+1;t++ )
		{ 
			printf("X=%d,Y=%d\r\n",uRow+t,(uint8_t)(uCol+t*K));
			OLED_DrawDot(uRow+t,(uint8_t)(uCol+t*K),m);
		}
	}
	

}




 /**
	* @brief  OLED_Horizontal_Scroll,水平向左滚动函数(1个单位)
	* @param  page : 滚动的起始页码(page:0~7);
	* @retval 无
  */
void OLED_Horizontal_Scroll_One(uint8_t page)
{
	// 搬运部分数据
	for(uint8_t i=0;i<127;i++)
	{
		memcpy(&OLED_GRAM[i][page],&OLED_GRAM[i+1][page],sizeof(unsigned char)*(8-page));
	}
	// 向右滚动后最后一列数据清空(需要验证)
	for(uint8_t i=page;i<8;i++)  //写一PAGE的GDDRAM数据
	{
		// 最后一列清空
		OLED_GRAM[127][i] = 0x00;
	}
}



 /**
	* @brief  OLED_Coordinate_Display,显示坐标函数,以右下角为原点
	* @param  x,y : 绘坐标(x:0~127, y:0~63);
	* @retval 无
  */
// 测试一下,需要去有符号(是否出错)
void OLED_Coordinate_Display(uint8_t x,float T)
{
	uint8_t y;
	
#if 0
	if((T >= 46) && (T <= 77.5))
	{
		// 0.5°C为一个像素点,最高显示是77.5°C,最低显示是46°C
		// 坐标转换
		y = 63 - (T-46)/0.5;
		// 先描点
		OLED_DrawDot(x,y,-1);
		// 显示温度顶线 70°C
		OLED_Operation_Line(1, 7, 1);
		// 刷新显示函数
		OLED_Refresh_Gram();
	}
	else if(T < 46)
	{
		// 只有在达到46度时才会显示图像
		//i = 0;
		OLED_ShowStr(0,4,(uint8_t *)"Display temper",2);
		OLED_ShowStr(0,6,(uint8_t *)"not reached.",2);
		// 显示当前温度
		OLED_display_DS18B20(T);
	}

#else
	if((T >= 58) && (T <= 73.75))
	{
		// 0.2°C为一个像素点,最高显示是73.75°C,最低显示是58°C
		// 坐标转换
		y = 63 - (T-58)/0.25;
		// 先描点
		OLED_DrawDot(x,y,-1);
		// 显示温度顶线 70°C
		OLED_Operation_Line(1, 7, 1);
		// 刷新显示函数
		OLED_Refresh_Gram();
	}
	else if(T < 58)
	{
		// 只有在达到46度时才会显示图像
		//i = 0;
		OLED_ShowStr(0,4,(uint8_t *)"Display temper",2);
		OLED_ShowStr(0,6,(uint8_t *)"not reached.",2);
		
		// 显示当前温度
		OLED_display_DS18B20(T);
	}
	
#endif

}



// 配置OLED显示屏
void OLED_SSD1306_Configure()
{
	// OLED 初始化
	OLED_SSD1306_Init();
	//全屏点亮
	OLED_Fill(0xFF);
	//SysTick_Delay_ms(1000);
	//全屏灭
	OLED_Fill(0x00);
	//SysTick_Delay_ms(1000);
}



// OLED 显示温度
void OLED_display_DS18B20(float T)
{
	char str1[20];
	// 显示当前温度
	sprintf(str1, "Temp:%.4f",T);
	OLED_ShowStr(0,0,(uint8_t *)str1,2);
	//printf("温度为%.4f℃\n",T);
}

// OLED 显示温度线
void OLED_display_DS18B20_line(int i,float T)
{	
	
	// 当列数达到120时
	if(i<120)
	{
		// 在第i横排点显示温度(有可显示的范围值)
		OLED_Coordinate_Display(i,T);
	}
	else
	{

		// 滚动起来
		OLED_Horizontal_Scroll_One(0);
		// 始终在第120列显示
		OLED_Coordinate_Display(120,T);
	}

}

总结

本节重点讲述了部分实验代码,稍后我会将完整代码放在评论区!下一节,将为大家带来最终实验现象和总结!敬请期待!

这一节的代码源文件和VOFA++控件,我将稍后会放在评论区,需要的自取!!!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

PID温控实验平台搭建(四)——PID温控系统实验代码讲解 的相关文章

  • 浏览器背后的运行机制

    浏览器背后的运行机制 从本章开始 我们的性能优化探险也正式进入到了 深水区 浏览器端的性能优化 平时我们几乎每天都在和浏览器打交道 在一些兼容任务比较繁重的团队里 苦逼的前端攻城师们甚至为了兼容各个浏览器而不断地去测试和调试 还要在脑子中记
  • vant-ui 按需引入

    我们在开发移动端的时候 在布局的过程中大部分会用到vant ui 方便快捷 也能更好的帮助我们快速的完成项目布局 首先 第一步 安装vant 在现有项目中使用 Vant 时 可以通过 npm 或 yarn 进行安装 Vue 2 项目 安装
  • 【华为OD机试】跳房子1【2023 B卷

    华为OD机试 真题 点这里 华为OD机试 真题考点分类 点这里 题目描述 跳房子 也叫跳飞机 是一种世界性的儿童游戏 游戏参与者需要分多个回合按顺序跳到第1格直到房子的最后一格 跳房子的过程中 可以向前跳 也可以向后跳 假设房子的总格数是c
  • 无线地磁传感器更适合路边停车系统

    随着停车难问题日益严重 在有限的固定停车场外 路边停车就成为当下很常见的一种停车方式 早起国外采用电感线圈来作为车位的传感器 国内比较少见 而我国直接跨过了电感线圈 直接采用了无线地磁传感器作为车位的传感器 那么无线地磁传感器优势在哪 地磁
  • 程序员经常聚集的国内开发者社区总览表

    转载 http www iteye com topic 1135562 云盘 http yunpan 360 cn 不管是编程菜鸟还是程序员大牛 都需要有自己的交流圈和学习平台 根据我自己的经验总结分享一些开发者论坛 社区啊 有大牛聚集的地

随机推荐

  • Nginx完美解决前后端分离端口号不同导致的跨域问题

    笔者在做前后端分离系统时 出现了很多坑 比如前后端的url域名相同 但是端口号不同 例如前端页面为 http 127 0 0 1 后端api根路径为 http 127 0 0 1 8888 这样就导致跨域问题 前端设置的request he
  • 雷达辐射源调制信号仿真(代码)

    雷达辐射源调制信号仿真 说明 通过Matlab进行单载频 CW 线性调频 LFM 非线性调频 NLFM 二相编码 BPSK 四相编码 QPSK 二频编码 BFSK 四频编码 QFSK 七种雷达脉内调制信号的方仿真 环境 Matlab 直通
  • 【Web前端学习笔记】第一章 HTML常用标签

    Web前端学习笔记 第一章 HTML常用标签 文章目录 Web前端学习笔记 前言 一 HTML是什么 二 常见标签 1 文本标签 2 列表标签 3 图片标签img 4 超链接a 5 表格标签table 6 表单form 7 分区标签 总结
  • 开源软件收集

    http www 7 zip org 7 Zip 4 16 Beta 文件压缩工具 可与Windows资源管理器集成http a note sourceforge net A Note 4 2 1 可在Windows桌面放置便笺 并可提供闹
  • 大模型训练时,使用bitsandbytes报错的解决方法

    前言 在对大语言模型 LLaMa Chat GLM等 进行微调时 考虑到减少显存占用 会使用如下方式加载模型 from transformers import AutoModel model AutoModel from pretraine
  • 2021-07-31

    2周目总结 7 19 7 25 无事 打牌 7 26 8 1 河南加油 无事打牌 我似乎忘了什么 哦 还有作业没弄 作业qaq 正在补习ing
  • T027基于51单片机的智能窗帘窗户控制系统proteus仿真原理图PCB

    功能 0 本系统采用单片机STC89C52作为系统的主控芯片 1 系统采用LCD1602液晶实时显示当前时间 窗帘状态 光照强度 2 系统具有四个功能按键 支持手动按键 定时 遥控三种模式控制窗帘 3 系统采用一个轻触按键模拟限位开关 步进
  • 树的基本概念

    什么是树 一棵树是一些节点的集合 这个集合可以是空集 若非空 则一棵树由一个称作根 root 的节点r以及0个或n个非空的树T1 T2 Tn组成 我们把T1 T2 Tn称为根 root 的子树 这些子树中每一棵的根都被来自根r的一条边 ed
  • 基于sonar 的C#静态代码扫描使用总结

    1 原理简介 C 语言接入Sonar代码静态扫描相较于Java Python来说 相对麻烦一些 Sonar检测C 代码时需要预先编译 而且C 代码必须用MSbuid进行编译 如果需要使用SonarQube对C 进行代码质量分析 则需要Son
  • [Git & Jetbrains] - Jetbrains系列软件Git使用知识点(一)

    前言 基础使用技巧 正文 右下角白框处可查看项目所有分支 在分支前的星 代表提交将哪些分支更新 若要将远程分支下载到本地 选择远程分支 再点击Checkout 此处还有merge等操作选项 左下角的Git选项的第一个功能是查看当前项目改动
  • Kubectl logs 命令

    1 查看创建的状态 状态为Pending 准备中 Running状态 已经创建成功 kubectl get pods n test gt 2 查看POD详细信息 kubectl get pods o wide n test gt 3 创建p
  • angular 学习之组件component

    组件新建 ng g c name 如是想在哪个目录里建 就直接CD进入那目录里执行就可以了 系统自动生成文件 name componet less name compoent html name component spec ts name
  • 关于EXCLE 下拉框多选的设置

    关于EXCLE 下拉框多选的设置 本文转载于 https www cnblogs com boosasliulin p 5970120 html 本文转载于 https blog csdn net qq 33269520 article d
  • linux wayland体验速度,Wayland安装(转)

    Wayland 是一個極精簡的 display server 它是由 Kristian H gsberg 在工作之餘所進行的實驗性計畫 與 X server 不同 Wayland client 要負責所有的繪圖動作 server 只處理最後
  • 用openlayers在加载离线瓦片(里面附带下载瓦片的软件,请往下看)

    首先先来看看效果 这个是谷歌卫星图 然后我们说说怎么实现的吧 div style width 100 height 800px div
  • Vue + Spring Boot 项目实战项目简介

    参考https learner blog csdn net article details 88925013 githubhttps github com Antabot White Jotter
  • Java使用Spire.Pdf实现PDF添加图片水印

    通过本文你将学到 Spire Pdf是什么 如何在项目中引入Spire Pdf依赖 项目中基于Spire Pdf实现PDF添加图片水印 一 Spire Pdf是什么 1 Spire Pdf是成都冰蓝科技有限公司开发的一款简单易用 功能强大的
  • 雨课堂 文件和磁盘练习(1)

    若某文件系统索引结点 inode 中有直接地址项和间接地址项 与单个文件长度有关的因素是 间接地址索引的级数 地址项的个数 文件块大 与单个文件长度无关的因素是 索引结点的总数 相关解释 如果系统中有1000个 索引结点 说明有1000个物
  • 面试官:为什么Vue中的v-if和v-for不建议一起用?

    一 作用 v if 指令用于条件性地渲染一块内容 这块内容只会在指令的表达式返回 true值的时候被渲染 v for 指令基于一个数组来渲染一个列表 v for 指令需要使用 item in items 形式的特殊语法 其中 items 是
  • PID温控实验平台搭建(四)——PID温控系统实验代码讲解

    PID温控实验平台搭建 一 PID基础知识介绍 二 PID进阶知识介绍及源码分享 三 从零开始搭建STM32温控实验平台 四 PID温控系统代码讲解 五 最终实验现象与总结 文章目录 前言 一 主程序功能描述 二 部分代码讲解 1 PID程