基于STM32(F103ZGTX系列)项目:智能楼宇(第二部分)

七、LCD屏显示模块

液晶显示器,简称 LCD,具有功耗低、体积小、承载的信息量大及不伤眼的优点。而液晶拥有如果被施加电场,它的分子排列会改变,从而改变光线的传播方向的特性。配合偏振光片,它就具有控制光线透过率的作用,再配合彩色滤光片,改变加给液晶电压大小,就能改变某一颜色透光量的多少。利用这种原理,做出可控红、绿、蓝光输出强度的显示结构,把三种显示结构组成一个显示单位,通过控制红绿蓝的强度,可以使该单位混合输出不同的色彩,这样的一个显示单位被称为像素。
本项目的LCD屏使用ILI9341芯片进行驱动。该芯片最主核心部分是位于中间的 GRAM,也就是显存。 GRAM 中每个存储单元都对应着液晶面板的一个像素点。它右侧的各种模块共同作用把 GRAM 存储单元的数据转化成液晶面板的控制信号,使像素点呈现特定的颜色,而像素点组合起来则成为一幅完整的图像。该芯片可配置使用 SPI 接口、8080 接口以及 RGB 接口与 单片机 进行通讯。 单片机也能够通过 SPI、 8080 接口或 RGB 接口与ILI9341 进行通讯,从而访问它的控制寄存器、地址计数器以及 GRAM。由于LCD 为非发光性的显示装置,它需要借助背光源才能达到显示功能,因此在 GRAM 的左侧还有一个 LED 控制器,能够控制液晶屏中的 LED 背光源。
在本项目中,是通过FSMC模拟8080时序与LCD屏幕进行通信的。FSMC全称是可变静态存储控制器,这是STM32系列采用的一种新型的存储器扩展技术。FSMC 连接好外部的存储器并初始化后,就可以直接通过访问地址来读写数据,而使用 FSMC 外接存储器时,其存储单元是映射到 STM32 的内部寻址空间的;在程序里,定义一个指向这些地址的指针然后就可以通过指针直接修改该存储单元的内容, FSMC 外设会自动完成数据访问过程,读写命令之类的操作不需要程序控制。控制 FSMC 使用 SRAM 存储器时主要是配置时序寄存器以及控制寄存器,利用 ST 标准库的 SRAM 时序结构体以及初始化结构体即可对屏幕参数进行修改。
在这里插入图片描述

八、Flash存储

中控端的W25Q32 是一个拥有4M字节存储空间、引脚和功耗的存储器解决方案的串行 Flash 存储器,而节点板的FLASH则是使用W25Q16,拥有2M字节的存储空间。基于双倍/四倍的 SPI,它们能够可以立即完成提供数据给 RAM,包括存储声音、文本和数据。W25Q32内的内存空间划分是:一页 256 字节,十六页为一个扇区(4096个字节),16 个扇区为 1 块,一共4M字节的内存空间,64个块、1024个扇区。而W25Q16内存空间划分是:一页 256 字节,十六页为一个扇区(4096个字节),16 个扇区为 1 块,一共2M字节的内存空间,32个块、512个扇区,。在地址划分上具体表现为一个6位的十六进制数,其中第0和第1位表示的是字节,第2位表示的是页,第3位表示的是扇区,第4第5位表示的是块。在对W25Q32或W25Q16进行读写时只需要对相应地址进行操作即可。
在本项目中是使用五线制SPI与FLASH进行通信。SPI是一种串行同步全双工的通信方式,其拥有MOSI(主出从入)、MISO(主入从出)、时钟线、地线、CS(片选线)五个接口。在通信前需要先把相位、极性、数据宽度(数据帧格式)、SPI模式、帧格式(先发高位或低位)、CRC校验、SPI方向、软件控制配置好。在与W25Q32模块进行通信时,需要先进行写使能(将CS片选引脚的电平拉低,然后发送06H指令,然后将片选引脚拉高),然后根据需求发送指令与地址,待到将数据发送完毕后,发送读取指令并判断BUSY标志位。读取数据同理,拉低片选引脚电平、发送指令与地址,判断BUSY标志位。
代码如下:

#include "main.h"
#include "flash.h"
#include "spi.h"
#include "stdio.h"
#include "usart.h"

uint8_t Flash_SendData(uint8_t data)
{
	uint8_t revDat=0;
	HAL_SPI_TransmitReceive(&hspi2,&data,&revDat,1,100);
	return revDat;
}
/*****************************************************************
函 数 名 称:Read_ID
函 数 功 能:读取 SPI 设备的 ID
函 数 形 参:无
函 数 返 回:无
*******************************************************************/
void Read_FlashID(void)
{
	uint8_t ID = 0, ID1 = 0, ID2 = 0;
	FLASH_CS_L();
	Flash_SendData(0x9F); // 发送读取 ID 命令
	ID = Flash_SendData(0xAA);
	ID1 = Flash_SendData(0xAA);
	ID2 = Flash_SendData(0xAA);
	printf("厂商ID: %x%x%x\r\n", ID, ID1, ID2);
	FLASH_CS_H();
}
/*****************************************************************
函 数 名 称:sFLASH_WriteEnable
函 数 功 能:使能 Flash 写操作
函 数 形 参:无
函 数 返 回:无
*******************************************************************/
void sFLASH_WriteEnable(void)
{
	FLASH_CS_L(); // 拉低片选
	Flash_SendData(0x06); // 发送写使能命令
	FLASH_CS_H(); // 拉高片选
}

/*****************************************************************
函 数 名 称:sFLASH_WaitForWriteEnd
函 数 功 能:等待 Flash 写操作完成
函 数 形 参:无
函 数 返 回:无
*******************************************************************/
void sFLASH_WaitForWriteEnd(void)
{
	uint8_t flashstatus = 0;
	FLASH_CS_L(); // 拉低片选
	Flash_SendData(0x05); // 发送读取状态寄存器命令
	do
	{
		flashstatus = Flash_SendData(0xA5); // 读取状态寄存器
	}
	while ((flashstatus & 0x01) == SET); // 等待写操作完成
	FLASH_CS_H(); // 拉高片选
}

/*****************************************************************
函 数 名 称:sFLASH_EraseSector
函 数 功 能:擦除指定扇区(4KB)
函 数 形 参:uint32_t SectorAddr 扇区地址
函 数 返 回:无
*******************************************************************/
void sFLASH_EraseSector(uint32_t SectorAddr)
{
	sFLASH_WriteEnable(); // 写使能
	FLASH_CS_L(); // 拉低片选
	Flash_SendData(0x20); // 发送扇区擦除命令
	Flash_SendData((SectorAddr & 0xFF0000) >> 16); // 发送高字节地址
	Flash_SendData((SectorAddr & 0xFF00) >> 8); // 发送中字节地址
	Flash_SendData(SectorAddr & 0xFF); // 发送低字节地址
	FLASH_CS_H(); // 拉高片选
	sFLASH_WaitForWriteEnd(); // 等待擦除完成
}

/*****************************************************************
函 数 名 称:sFLASH_EraseChip
函 数 功 能:擦除整个芯片
函 数 形 参:无
函 数 返 回:无
*******************************************************************/
void sFLASH_EraseChip(void)
{
	sFLASH_WriteEnable(); // 写使能
	FLASH_CS_L(); // 拉低片选
	Flash_SendData(0x60); // 发送芯片擦除命令
	FLASH_CS_H(); // 拉高片选
	sFLASH_WaitForWriteEnd(); // 等待擦除完成
}

/*****************************************************************
函 数 名 称:sFLASH_WritePage
函 数 功 能:写入数据到指定页,不超过256字节
函 数 形 参:uint8_t* pBuffer 数据缓冲区
            uint32_t WriteAddr 写入地址
            uint16_t NumByteToWrite 写入的字节数
函 数 返 回:无
*******************************************************************/
void sFLASH_WritePage(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
	//1. 先写使能FLASH
	sFLASH_WriteEnable();
	FLASH_CS_L(); // 拉低片选
	Flash_SendData(0x02); // 发送页写指令
	// 分三次发地址
	Flash_SendData((WriteAddr & 0xFF0000) >> 16);
	Flash_SendData((WriteAddr & 0xFF00) >> 8);
	Flash_SendData(WriteAddr & 0xFF);
	while(NumByteToWrite--)
	{
		Flash_SendData(*pBuffer); // 发送数据
		pBuffer++;
	}
	FLASH_CS_H(); // 拉高片选
	// 等待写入完成
	sFLASH_WaitForWriteEnd();
}

/*****************************************************************
函 数 名 称:sFLASH_ReadBuffer
函 数 功 能:读取指定地址的数据
函 数 形 参:uint8_t* pBuffer 读数据缓冲区
            uint32_t ReadAddr 读地址
            uint16_t NumByteToRead 读字节数
函 数 返 回:无
*******************************************************************/
void sFLASH_ReadBuffer(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{
	FLASH_CS_L(); // 拉低片选
	Flash_SendData(0x03); // 发送读数据指令
	// 分三次发地址
	Flash_SendData((ReadAddr & 0xFF0000) >> 16);
	Flash_SendData((ReadAddr & 0xFF00) >> 8);
	Flash_SendData(ReadAddr & 0xFF);
	while(NumByteToRead--)
	{
		*pBuffer = Flash_SendData(0xA5); // 读取数据
		pBuffer++;
	}
	FLASH_CS_H(); // 拉高片选
}

/*****************************************************************
函 数 名 称:sFLASH_WriteBuffer
函 数 功 能:写入指定缓冲区的数据
函 数 形 参:uint8_t* pBuffer 数据缓冲区
            uint32_t WriteAddr 写入地址
            uint16_t NumByteToWrite 写入字节数
函 数 返 回:无
*******************************************************************/
void sFLASH_WriteBuffer(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
	uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;

	Addr = WriteAddr % sFLASH_SPI_PAGESIZE; // 判断是否从头开始写
	count = sFLASH_SPI_PAGESIZE - Addr; // 开始一页还剩多少空间
	NumOfPage = NumByteToWrite / sFLASH_SPI_PAGESIZE; // 要写多少页
	NumOfSingle = NumByteToWrite % sFLASH_SPI_PAGESIZE; // 没页写满还与多少个字节

	if (Addr == 0) // 地址是页开头
	{
		if (NumOfPage == 0) // 一页能写完
		{
			sFLASH_WritePage(pBuffer, WriteAddr, NumByteToWrite);
		}
		else // 一页写不完
		{
			while (NumOfPage--)
			{
				sFLASH_WritePage(pBuffer, WriteAddr, 256);
				WriteAddr += sFLASH_SPI_PAGESIZE;
				pBuffer += sFLASH_SPI_PAGESIZE;
			}
			sFLASH_WritePage(pBuffer, WriteAddr, NumOfSingle);
		}
	}
	else // 不从头开始写
	{
		if (NumOfPage == 0) // NumByteToWrite < sFLASH_PAGESIZE
		{
			if (NumOfSingle > count) // (NumByteToWrite + WriteAddr) > sFLASH_PAGESIZE
			{
				temp = NumOfSingle - count;

				sFLASH_WritePage(pBuffer, WriteAddr, count);
				WriteAddr += count;
				pBuffer += count;

				sFLASH_WritePage(pBuffer, WriteAddr, temp);
			}
			else
			{
				sFLASH_WritePage(pBuffer, WriteAddr, NumByteToWrite);
			}
		}
		else // NumByteToWrite > sFLASH_PAGESIZE
		{
			NumByteToWrite -= count;
			NumOfPage = NumByteToWrite / sFLASH_SPI_PAGESIZE;
			NumOfSingle = NumByteToWrite % sFLASH_SPI_PAGESIZE;
			// 先把开头半页写完
			sFLASH_WritePage(pBuffer, WriteAddr, count);
			WriteAddr += count;
			pBuffer += count;
			// 循环写入剩下的数据 256
			while (NumOfPage--)
			{
				sFLASH_WritePage(pBuffer, WriteAddr, sFLASH_SPI_PAGESIZE);
				WriteAddr += sFLASH_SPI_PAGESIZE;
				pBuffer += sFLASH_SPI_PAGESIZE;
			}

			if (NumOfSingle != 0)
			{
				sFLASH_WritePage(pBuffer, WriteAddr, NumOfSingle);
			}
		}
	}
}

九、项目其他开发

9.1 LVGL界面设计

在使用LVGL之前需要将LVGL移植至工程当中方可使用,具体的界面设计使用GUIguider软件进行开发。根据空气质量检测仪的功能分析,项目的界面应当设计成信息简洁明了且数据详实,因此本项目一共拥有四个数据展示界面,分别对室内温湿度与光照强度、空气质量(包括甲醛、二氧化碳、挥发性有机气体以及烟雾)、室内温湿度走向、以及当日气温湿度与天气进行显示。
根据项目的实际情况(开机后的数据加载以及联网等待),还需要使用一个开机启动界面令系统在启动界面加载的过程中完成大部分的数据加载以及联网工作,优化项目的用户体验,提升系统整体的流畅度。
在进入数据展示页面后,需要有两个虚拟按键对现实的节点进行轮换,以及对两个节点端的继电器进行控制。在使用GUIguider设计界面时根据上述分析添加两个屏幕,同时在两个屏幕内放置两个容器,将所有组件都放置在容器当中,这样可以使屏幕加载的控制操作更为方便。在设计完成后只需要将生成的文件添加到工程当中,并根据版本将不适用于本项目的部分删去,同时在文件中启动并加载屏幕,即可将设计好的屏幕内容通过LCD屏展示出来。
效果如下图:
在这里插入图片描述
在这里插入图片描述

9.2Wifi设置功能

在本项目中,WIFI模块是使用官方固件通过串口5向ESP8266 发送AT指令来驱动的。只需要将串口6的波特率设置为115200,数据位设置为8位,停止位设置为1位,不设置校验位即可实现向WIFI模块发送信息,WIFI模块会自动分析接收到的指令并根据预设做出动作。官方固件提供的指令可以通过查阅手册获取信息,而发送的指令可以通过sprintf函数来自行拼接。
阿里云数据变化
在这里插入图片描述
手机云智能现象
在这里插入图片描述

9.3 freeRTOS实时操作系统

FREErtos系统通过时间片的方式对任务进行调度,任务有四种不同的状态:就绪态、运行态、挂起态、阻塞态,不同的任务可以设置不同的任务优先级。创建完任务后,需要开启任务调度器,对于相同优先级的任务,先创建的任务会后执行,而后创建的任务会先执行,一般来说是按照每个任务分别执行1毫秒的方式依次执行,在这个时间尺度下,用户几乎感觉不到任务之间切换执行的卡顿,从而实现单核系统的多任务伪并行。而对于不同优先级的任务,只要最高优先级任务不被阻塞,那么它就会一直抢占CPU资源从而令系统只执行该任务,其它较低优先级的任务完全无法执行。而当该最高优先级的任务被阻塞时它会进入阻塞链表中等待阻塞结束,次一级优先级的任务会立刻抢占CPU资源并执行任务,当高优先级任务阻塞结束时它会立刻夺回CPU资源并开始运行,次一级优先级的任务会再次回到就绪链表中等待。
而让任务阻塞的机制有:挂起、临界区、互斥锁、二值信号量、计数信号量、延时、队列这几种。任务除了有优先级这一属性外,还有任务栈这一属性,它代表的是该任务在RAM中所占据的空间大小。任务间虽然独立运行,但是可以通过全局变量、二值信号量、计数信号量、事件组、互斥锁、队列甚至是无线网络的方式进行通信。有时候我们想要实现一个任务在执行完某个步骤后调动起另一个任务,这个时候可以通过使用互斥锁以及二值信号量来实现任务的同步。
如下为 freeRTOS创建的任务
在这里插入图片描述

项目总结

1、该项目能让自己有什么收获

(1)制作该项目,更加理解了MCU的应用和相关知识点的使用。
(2)学习使用了485通信和Modbus协议
(3)更加深入学习了各个传感器模块接受数据和解析。
(4)学习并掌握了传感器的工作原理和技术规格。获得了数据采集、处理和分析的技术经验。掌握了物联网(IoT)设备的设计与开发。了解了数据可视化工具和技术。
(5)从需求分析到产品设计的完整流程体验。产品迭代和优化的经验。对市场调研和用户反馈的理解
(6)自信心和领导力的提升。解决问题和创新思维的培养。在专业领域内的知识深化和个人品牌的建立。

2、总结项目中遇到的问题,及解决方法

1)问题1:中控端与节点端通过485通信时出现末位数据丢失的情况
解决办法1:原因是串口在发送数据时并没有等待最后一位数据传输完毕就将 SP3485模块的控制引脚的电平拉低,导致模块在传输未完成的情况就切换为了接 收模式。只需要在发送最后一位数据之后加上一个判断串口数据是否发送完成的死 循环用于等待数据全部发送完毕即可。
2)问题2:解析云端发送回来的数据出错
解决办法2:原因是用于接收云端数据的数组空间过小导致数据接收出错,将数组 空间调大即可。
3)问题3:触摸屏获取坐标位置信息出错
解决办法3:原因是移植屏幕驱动的时候将部分重要程序段进行了不必要的屏蔽, 导致程序无法读取Flash中存储的坐标校准信息,致使坐标计算出错。
4)问题4:节点端向中控端发送地址时存在时间差导致原定程序运行顺序出错
解决办法4:原因是由于中控端数据通过串口发送到节点端需要一定时间,同时节 点端向中控端发送数据也需要一点时间,而这个时间还会受到硬件方面的影响,所 以中控端在判断是否有节点响应之前需要进行一定时间的等待。在判断是否有节点 响应之前加上一个小延时即可。由于此时中控端已经创建了任务,所以可以使用 FreeRTOS系统提供的不阻塞延时进行等待,这就可以令其它任务继续执行。

  • 21
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值