一、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 指令冗余信息)
流程:
- 单连接模式(
AT+CIPMUX=0):透传模式仅支持 "一对一 TCP 连接",不能多连接;- 无其他网络服务占用(关闭 TCP 服务器
AT+CIPSERVER=0):避免端口冲突、资源占用;- 先开启透传功能(
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");
}
}