FreeRTOS项目---WiFi模块(2)

一、Flash操作

在这里,我们用Flash保存读取到的ssid和pwd,在第二次如果flash中有数据就直接从flash中拿去。其次,如果想更改ssid和pwd,着时候就需要一个复位键,用来重置账户和密码

复位按键,我们选取一个GPIO管脚的OUTPUT模式,当按键按下去是,进行复位。

1.1 写flash

写flash是由固定的流程的,首先进行解锁,然后我们调用flash擦除函数,先擦除里面的内容我们才能进行有效的写入操作。在擦除函数的参数中有一个结构体类型的参数,所以在之前还需要创建一个相同类型的结构体,来决定我们是以什么方式进行擦除的

cpp 复制代码
// 写flash
int flash_write_data(uint32_t pageAddr, uint8_t* buf, uint16_t size)
{
    //创建擦除的结构体变量
	FLASH_EraseInitTypeDef flasObj;
	//配置擦除的方式
	flasObj.TypeErase = FLASH_TYPEERASE_PAGES;//页擦除
	flasObj.PageAddress = pageAddr;//擦除的页地址
	flasObj.NbPages = 1;//擦除的页数
	flasObj.Banks = FLASH_BANK_1;//擦除的Flash存储区域(Bank1)
	//一页的大小,一次擦除的大小不能超过1024
	if(size > 1024)
	{
		printf("size must be less than 1024.\r\n");
		return -1;
	}
	HAL_StatusTypeDef ret;
	//1、解锁Flash
	HAL_FLASH_Unlock();
	//检测PageError,擦除失败时会存储出错的Flash地址
	uint32_t PageError = 0; 
	//2、调用擦除函数擦除
	ret = HAL_FLASHEx_Erase(&flasObj, &PageError); 
	if(ret != HAL_OK)
	{
		printf("HAL_FLASH_Program failure\r\n");
		HAL_FLASH_Lock();
		return -1;
	}
	
	int writeTotalSize = size;
    //2字节2字节擦除,如果不是偶数要进行特殊处理
	if(size%2)
	{
		writeTotalSize -= 1;
	}
	int i = 0;
	for(; i < writeTotalSize; i+=2)
	{
		uint16_t Write_Flash_Data = (buf[i + 1] << 8) | buf[i];
		//3、对flash进行写入,半字写入
		ret = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, pageAddr, Write_Flash_Data);
		if(ret != HAL_OK)
		{
			printf("HAL_FLASH_Program failure\r\n");
			HAL_FLASH_Lock();
			return -1;
		}
		pageAddr += 2;
	}
	
	if(i < size)
	{
		uint16_t Last_byte = (buf[i] << 8) | buf[i]) ; 
		//3、对flash进行写入,半字写入
		ret = HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, pageAddr, Last_byte);
		if(ret != HAL_OK)
		{
			printf("HAL_FLASH_Program failure\r\n");
			HAL_FLASH_Lock();
			return -1;
		}
	}
	//4、等待Flash最后一次操作完成(防止操作未结束时进行其他操作导致错误)
	ret = FLASH_WaitForLastOperation(portMAX_DELAY); // 超时时间(单位:毫秒)
	if(ret != HAL_OK)
	{
		printf("FLASH_WaitForLastOperation failure\r\n");
		HAL_FLASH_Lock();
		return -1;
	}
	
	//5、锁定Flash
	HAL_FLASH_Lock();
	return 0;
}

2. 读flash

cpp 复制代码
// 从flash中读取数据
void flash_read_data(uint32_t readAddr, uint8_t* buf, uint16_t size)
{
	// 直接内存拷贝读取
    /*
        将从 readAddr 地址开始的、长度为 size 字节的内存数据,完整拷贝到以 buf 为起始地址的目标内存中
    */
	memcpy(buf, (uint8_t*)readAddr, size);
}

3. 擦除flash

cpp 复制代码
/*
此处代码和写操作类似,不多赘述
*/
void erase_flash_data()
{
	FLASH_EraseInitTypeDef flasObj;
	
	flasObj.TypeErase = FLASH_TYPEERASE_PAGES;//页擦除
	flasObj.PageAddress = 0x0800F800;//擦除的页地址
	flasObj.NbPages = 1;//擦除的页数
	flasObj.Banks = FLASH_BANK_1;//擦除的Flash存储区域(Bank1)
	
	HAL_StatusTypeDef ret;
	//1、解锁Flash
	HAL_FLASH_Unlock();
	//检测PageError,擦除失败时会存储出错的Flash地址
	uint32_t PageError = 0; 
	//2、调用擦除函数擦除
	ret = HAL_FLASHEx_Erase(&flasObj, &PageError); 
	if(ret != HAL_OK)
	{
		printf("HAL_FLASH_Program failure\r\n");
		HAL_FLASH_Lock();
		return;
	}
	
	//3、等待Flash最后一次操作完成(防止操作未结束时进行其他操作导致错误)
	ret = FLASH_WaitForLastOperation(portMAX_DELAY); // 超时时间(单位:毫秒)
	if(ret != HAL_OK)
	{
		printf("FLASH_WaitForLastOperation failure\r\n");
		HAL_FLASH_Lock();
		return;
	}
	
	//4、锁定Flash
	HAL_FLASH_Lock();
	return;
}
cpp 复制代码
sscanf是C语言标准库 <stdio.h> 中的一个输入函数,用于从字符串中按指定格式提取数据。
返回值:成功匹配的字段数
sscanf的格式化字符串可以通过以下通配符实现灵活匹配:
<1>%*[^:]:跳过所有非:的字符,*表示只读取不存储。
<2>%*c:跳过单个字符。
<3>ssid=%99[^&]:匹配ssid=后面直到&为止的内容(%99限制长度防止溢出,[^&] 表示匹配非&的所有
字符)。
<4>&pwd=%99s:匹配&pwd=后面的所有字符(直到字符串结束,%99s 限制长度)

4. 从flash中读取ssid和pwd

cpp 复制代码
// 从flash中读取ssid和pwd
int read_ssid_and_pwd_from_flash(uint32_t readAddr, char* ssid, char* pwd)
{
	char buf[64] = {0};
    //我们存储在0x800F800为起始地址的区域
	uint32_t pageAddr = 0x0800F800;
	flash_read_data(pageAddr, (uint8_t*)buf, sizeof(buf));
	//sscanf的返回值count是"成功解析并赋值的变量个数"
    //用 %s 能匹配所有非空白字符,刚好覆盖完整密码,所以pwd之后跟上s
	int count = sscanf((char*)buf, "ssid=%64[^&]&pwd=%64s", ssid, pwd);
	printf("count=%d\r\n", count);
		
	if(count != 2)
	{
		printf("parse read_ssid_and_pwd_from_flash failure.\r\n");
		return -1;
	}
	return 0;
}

二、连接网络

2.1 等待用户配置自己的网络

cpp 复制代码
//等待用户配置自己的网络
int waiting_config_user_network()
{
    //存储ssid和pwd
	char ssid[64] = {0};
	char pwd[64] = {0};//从flash中读取WiFi配置,切换station模式,从flash中读取WiFi配置
	int ret = read_ssid_and_pwd_from_flash(0X0800F800,ssid,pwd);
    //如果从flash中没有读取到数据
	if(ret < 0)
	{
        /****
        这里重点声明,进入AP模式后,不能直接发送数据,因为这时候缺少核心的通信链路,我们要指定要通过tcp还是udp的方式发送数据
        ***/
        //先进入ap模式,作为服务器接收用户输入的账户和密码
		ret = esp8266_enter_ap_mode();
		if(ret < 0)
		{
			return -1;
		}
        //再建立TCP协议传输数据
		ret = esp8266_tcp_server();
		if(ret < 0)
		{
			return -1;
		}

		// 接收用户的网络数据,到这里才能有效的接收到用户输入的数据
		uint8_t data[256] = {0};
        //循环的接收,接收到了才退出
		while(1)
		{
			memset(data, 0 ,sizeof(data));
            //从队列中读取数据
			ret = dma_receive_ack_data_from_queue(data, sizeof(data), portMAX_DELAY);
			
			if(ret < 0)
			{
				return -1;
			}

			printf("receive user network:%s\r\n", data);
			
			//提取数据 ssid=WiFi名称&pwd=WiFi密码
			
			memset(ssid, 0 ,sizeof(ssid));
			memset(pwd, 0 ,sizeof(pwd));
            //提取有效的数据
//%*[^:]:跳过所有不是:的字符,*表示只读取不存储
//到:字符了,匹配分隔符:消耗掉 : 字符,然后开始接收并存储数据
//接下来和flash提取数据的方式相同
			int count = sscanf((char*)data,"%*[^:]:ssid=%64[^&]&pwd=%64s",ssid,pwd);
		
			printf("count=%d\r\n",count);
			//如果读取到的有效数据不是2,重新读
			if(count != 2)
			{
				printf("sscanf parse userconfig failure.\r\n");
				continue;
			}
			break;
		}
		//保存数据到flash中【ssid=WiFi名称&pwd=WiFi密码】
		ret = saveWiFIConfigToFlash(ssid,pwd);
		if(ret < 0)
		{
			printf("flash_write_data_failure.\r\n");
			return -1;
		}
		printf("flash flash to page-63 successful.\r\n");

	}
//如果有数据打印出来看看对不对
	printf("ssid=%s,pwd=%s\r\n",ssid,pwd);
//接着进入工作站模式传输数据,连接其他网络进行数据传输
	ret = esp8266_enter_station_mode(ssid,pwd);
	printf("ret = %d\r\n",ret);
	if(ret < 0)
	{
		printf("网络连接失败.\r\n");
		return -1;
	}
	else
	{
		printf("网络连接成功.\r\n");
		return 0;
	}
}

2.2 进行Flash复位

cpp 复制代码
//使用GPIO的的外部中断回调函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_15)
    {
        //如果按键按下则复位
		printf("外部中断\r\n");
        erase_flash_data();
		HAL_NVIC_SystemReset();
    }
}

三、MQTT协议

3.1 搭建通信通道

我么要为为 MQTT 通信搭建 "串口 - Wi-Fi 透明传输通道" ------MQTT 协议需要稳定、双向的 TCP 连接,且要求数据 "原封不动" 传输(不能夹杂 AT 指令冗余信息)

流程:

  1. 单连接模式(AT+CIPMUX=0):透传模式仅支持 "一对一 TCP 连接",不能多连接;
  2. 无其他网络服务占用(关闭 TCP 服务器 AT+CIPSERVER=0):避免端口冲突、资源占用;
  3. 先开启透传功能(AT+CIPMODE=1),再建立 TCP 连接,最后启动透传(AT+CIPSEND):固件硬性规则,顺序不能乱。
cpp 复制代码
//让esp8266进入透传模式,进行mqtt通信搭建
int mqtt_transport_open(char* ip, char* port)
{
	uint8_t ackData[128] = {0};
	char pData[128] = {0};
	int rbytes;
	//1、设置单连接,透传模式仅仅支持一对一,发送"AT+CIPMUX=0"设置单连接
    //MQTT 通信的核心是 "设备与云服务器的一对一连接
    memset(pData, 0, sizeof(pData));
	strcpy(pData,"AT+CIPMUX=0\r\n");
	int8_t ret = esp8266_dma_send_AT_command(pData, strlen(pData));
	vTaskDelay(pdMS_TO_TICKS(3000));
	
	if(ret != HAL_OK)
	{
		printf("设置单连接失败\r\n");
		return -1;	
	}

	rbytes = dma_receive_ack_data_from_queue(ackData,sizeof(ackData),500);
	
	
	if(rbytes > 0)
	{
		ackData[rbytes] = 0;
		printf("ack Data=%s\r\n",ackData);
	}
	else
	{
		return -1;
	}
	
	//2、关闭tcp服务器,除此之外还要进行足够延时,确保esp8266能够正确的响应
	vTaskDelay(pdMS_TO_TICKS(3000));
	memset(pData, 0, sizeof(pData));
	strcpy(pData,"AT+CIPSERVER=0\r\n");
    ret = esp8266_dma_send_AT_command(pData, strlen(pData));
	
	vTaskDelay(pdMS_TO_TICKS(1000));
    if(ret != HAL_OK) 
	{
		printf("关闭tcp服务器\r\n");
        return -1;
    }
	
	rbytes = dma_receive_ack_data_from_queue(ackData,sizeof(ackData),500);
	
	
	if(rbytes > 0)
	{
		ackData[rbytes] = 0;
		printf("ack Data=%s\r\n",ackData);
	}
	else
	{
		return -1;
	}
	//3、进入Wi-Fi透传接收模式,仅支持TCP单连接、UDP固定通信对端
    strcpy(pData,"AT+CIPMODE=1\r\n");
    ret = esp8266_dma_send_AT_command(pData, strlen(pData));
	
	vTaskDelay(pdMS_TO_TICKS(1000));
    if(ret != HAL_OK) 
	{
		printf("设置透传失败\r\n");
        return -1;
    }
	
	rbytes = dma_receive_ack_data_from_queue(ackData,sizeof(ackData),500);
	
	
	if(rbytes > 0)
	{
		ackData[rbytes] = 0;
		printf("ack Data=%s\r\n",ackData);
	}
	else
	{
		return -1;
	}
	
	
	//4、成为客户端,建立TCP的连接,按格式将数据写入字符串缓冲区pData,进行TCP连接
	
    memset(pData, 0, sizeof(pData));
	sprintf(pData, "AT+CIPSTART=\"TCP\",\"%s\",%s\r\n", ip, port);
	
	ret = esp8266_dma_send_AT_command(pData, strlen(pData));
	vTaskDelay(pdMS_TO_TICKS(5000));
	printf("ret = %d",ret);
	
	if(ret != HAL_OK)
	{
		printf("建立TCP的连接.\r\n");
		return -1;	
	}

	rbytes = dma_receive_ack_data_from_queue(ackData,sizeof(ackData),5000);
	
	
	if(rbytes > 0)
	{
		ackData[rbytes] = 0;
		printf("ack Data=%s\r\n",ackData);
	}
	
	//5、进入WiFi透传
	//AT+CIPMODE=1是启动透传模式的开关,AT+CIPSEND启动透传状态,二着缺一不可
    memset(pData, 0, sizeof(pData));
	strcpy(pData,"AT+CIPSEND\r\n");
	ret = esp8266_dma_send_AT_command(pData, strlen(pData));
	vTaskDelay(pdMS_TO_TICKS(2000));
	
	if(ret != HAL_OK)
	{
		printf("进入WiFi透传失败.\r\n");
		return -1;	
	}

	rbytes = dma_receive_ack_data_from_queue(ackData,sizeof(ackData),2000);
	
	printf("rbytes:%d\r\n",rbytes);
	if(rbytes > 0)
	{
		ackData[rbytes] = 0;
		printf("ack Data=%s\r\n",ackData);
	}

	
	printf("esp8266进入透传模式成功.\r\n");
	return 0;
}

3.2 进行MQTT连接

cpp 复制代码
//这段代码是专门用于mqtt的,可以再网上自己找找

int mqtt_connect(char* brokerIp,char* borkerPort)
{
    //先打开mqtt传输
	int ret = mqtt_transport_open(brokerIp, borkerPort);
	
	if(ret < 0)
	{
		printf("mqtt_transport_open failure\r\n");
		return -1;
	}
	
	MQTTPacket_connectData data = MQTTPacket_connectData_initializer;
	unsigned char buf[200];
	int buflen = sizeof(buf);
    //客户端mqtt通信的名字
	data.clientID.cstring = "XCX_esp8266";
	data.keepAliveInterval = 200; //20s,超时时间,时间过了mqtt自动断开连接
	data.cleansession = 1;
	data.username.cstring = "";
	data.password.cstring = "";
	// Version of MQTT to be used. 3 = 3.1 4 = 3.1.1
	data.MQTTVersion = 3;
	// 将data按照mqtt协议进行序列化
	int len = MQTTSerialize_connect(buf, buflen, &data);
	// 发送mqtt协议
	ret = send_mqtt_protocol(buf, len);

	if(ret < 0)
	{
		return -1;
	}

	// 等待连接应答wait for connack */
	if(MQTTPacket_read(buf, buflen, transport_getdata) != CONNACK)
	{
		printf("等待连接应答失败\r\n");
		return -1;
	}
	return 0;
}

3.3 订阅主题

cpp 复制代码
// 订阅主题
int mqtt_subscribe(char* subscribe)
{
    MQTTString topicString = MQTTString_initializer;
    int req_qos = 0;
    int msgid = 1;
    topicString.cstring = subscribe;
    unsigned char buf[200];
    int buflen = sizeof(buf);
    int len = MQTTSerialize_subscribe(buf, buflen, 0, msgid, 1, &topicString, &req_qos);

    if(len <= 0)
    {
        printf("订阅包序列化失败\r\n");
        return -1;
    }

    int ret = send_mqtt_protocol(buf, len);
    if(ret < 0)
    {
        return -1;
    }

    /* 等待订阅应答wait for suback */
    unsigned char buffer[256];
	if(MQTTPacket_read(buffer, sizeof(buffer), transport_getdata) != SUBACK)
	{
		printf("等待订阅应答失败\r\n");
		return -1;
	}

	printf("订阅主题[%s]成功\r\n", subscribe);
	return 0;
}

3.4 接收主题

cpp 复制代码
//接收订阅主题的消息  
int mqtt_receive_subscribe_data(unsigned char* buf)
{
    unsigned char buffer[512];

    if(MQTTPacket_read(buffer, sizeof(buffer), transport_getdata_block_mode) == PUBLISH)
    {
        unsigned char dup;
        int qos;
        unsigned char retained;
        unsigned short msgid;
        int payloadlen_in;
        unsigned char* payload_in;
        MQTTString receivedTopic;

        int rc = MQTTDeserialize_publish(&dup, &qos, &retained, &msgid, &receivedTopic,
                                        &payload_in, &payloadlen_in, buffer, sizeof(buffer));
        memcpy(buf, payload_in, payloadlen_in);
        return payloadlen_in; // 返回实际接收字节的个数
    }
    else
    {
        return -1;
    }
}

3.5 应答主题

cpp 复制代码
//应答函数
void mqtt_response_protocol_packet(uint8_t * ackProtocol,uint16_t size)
{
	// mqtt应答数据包
    unsigned char buf[200];
    int buflen = sizeof(buf);
    MQTTString topicString = MQTTString_initializer;
    topicString.cstring = "/iot/smarthome/doorlock/acktopic";//应答的主题名字
    int len = MQTTSerialize_publish(buf, buflen, 0, 0, 0, 0, topicString, (unsigned char*)ackProtocol, size);
    
	int ret = send_mqtt_protocol(buf, len);

    if(ret < 0) {
        printf("mqtt transmit failure\r\n");
    }
}
相关推荐
DIY机器人工房2 小时前
简单理解:什么是运放?
单片机·嵌入式硬件·运放·diy机器人工房
DIY机器人工房2 小时前
简单理解:反相比例放大器、同相比例放大器、比较器
单片机·嵌入式硬件·比较器·diy机器人工房·嵌入式面试题·同相比例放大器·反相比例放大器
DIY机器人工房2 小时前
简单理解:什么是施密特触发器?
stm32·单片机·嵌入式硬件·diy机器人工房·施密特触发器
非凡自我_成功3 小时前
寄存器开发控制LED
单片机·嵌入式硬件
曾哥嵌入式4 小时前
嵌入式项目:STM32刷卡RFID指纹识别考勤系统
stm32·单片机·嵌入式硬件
不败公爵4 小时前
Git的工作机制
笔记·git·stm32
up向上up4 小时前
基于STM32F103C8T6的充电桩计费系统
stm32·单片机·嵌入式硬件
lanhuazui105 小时前
I2C---读写EEPROM (AT24C02)
stm32
DIY机器人工房5 小时前
(十一)嵌入式面试题收集:18道
stm32·单片机·嵌入式硬件·diy机器人工房