STM32代码实现
开启本章节需要完成下方的前置任务:
点击跳转:
物联网实践教程:微信小程序结合OneNET平台MQTT实现STM32单片机远程智能控制 远程上报和接收数据——汇总
目标
1.连接OneNET:STM32使用串口与ESP8266/01s连接发送AT指令连接上OneNET MQTT设备
2.数据上报OneNET:将STM32采集的数据上传到OneNET
3.接收OneNET下发数据:STM32接收OneNET下发的指令并进行对应的操作
代码主要逻辑
无线模块状态结构体
typedef struct {
uint8_t receiveDataFlag; //接收数据标志位
uint8_t sendDataFlag; //发送数据标志位
uint8_t wirelessInitFlag; //无线模块初始化完成标志位
uint16_t wirelessInitErrorCode; //无线模块初始化错误代码
}Wireless_TypeDef;
extern Wireless_TypeDef WirelessStatus;
无线模块连接OneNET MQTT设备需要发送的AT指令
全部使用宏定义,方便其他的用户直接修改设备等相关信息即可直接使用
#include "wireless.h"
/*
只需更新这些信息
*/
#define JsonEnable 1
Wireless_TypeDef WirelessStatus = {0, 0, 0, 0};
#define WIFI_SSID "207" //WIFI用户名
#define WIFI_PASSWORD "12345678" //WIFI密码
#define ONENET_MQTT_PRODUCT_ID "YqRZ5hrM6p" //OneNET MQTT产品ID
#define ONENET_MQTT_DEVICE_NAME "CSDN" //OneNET MQTT设备名称
#define ONENET_MQTT_TOKEN "version=2018-10-31&res=products%2FYqRZ5hrM6p%2Fdevices%2FCSDN&et=2028715245&method=md5&sign=G4I0xqIYmYUtCdTTo2t%2FqQ%3D%3D" //token
#define ONENET_MQTT_CMD_IDENTIFIER "cmd" //OneNET MQTT命令标识符
const char *floatIdentifiers[] = {"temp", "bright"}; //需要上报数据的整形标识符
float floatValues[] = {23.7, 55.6}; //上报数据对应的整形大小
const char *stringIdentifiers[] = {0}; //需要上报数据的字符串标识符
const char *stringValues[] = {0}; //上报数据对应的字符串
/*
只需更新这些信息
*/
#define WIRELESS_WIFI_INFO "AT+CWJAP=\"" WIFI_SSID "\",\"" WIFI_PASSWORD "\"\r\n"
#define WIRELESS_RECEIVE_CMD "\"" ONENET_MQTT_CMD_IDENTIFIER "\":"
#define ONENET_MQTT_SERVER_INFO "AT+MQTTCONN=0,\"mqtts.heclouds.com\",1883,1\r\n"
#define ONENET_MQTT_USERCFG_INFO "AT+MQTTUSERCFG=0,1,\"" ONENET_MQTT_DEVICE_NAME "\",\"" ONENET_MQTT_PRODUCT_ID "\",\"" ONENET_MQTT_TOKEN "\",0,0,\"\"\r\n"
#define ONENET_MQTT_SET_TOPIC "AT+MQTTSUB=0,\"$sys/" ONENET_MQTT_PRODUCT_ID "/" ONENET_MQTT_DEVICE_NAME "/thing/property/set\",0\r\n"
#if JsonEnable
#define ONENET_MQTT_SET_MQTTPUBRAW "AT+MQTTPUBRAW=0,\"$sys/" ONENET_MQTT_PRODUCT_ID "/" ONENET_MQTT_DEVICE_NAME "/thing/property/set_reply\""
#define ONENET_MQTT_PUB_MQTTPUBRAW "AT+MQTTPUBRAW=0,\"$sys/" ONENET_MQTT_PRODUCT_ID "/" ONENET_MQTT_DEVICE_NAME "/thing/property/post\""
#else
#define ONENET_MQTT_PUB_SET "AT+MQTTPUB=0,\"$sys/" ONENET_MQTT_PRODUCT_ID "/" ONENET_MQTT_DEVICE_NAME "/thing/property/set_reply\""
#define ONENET_MQTT_PUBTOPIC "AT+MQTTPUB=0,\"$sys/" ONENET_MQTT_PRODUCT_ID "/" ONENET_MQTT_DEVICE_NAME "/thing/property/post\""
#endif
1.初始化
由前置任务可以知道,每次发送AT指令后,无线模块总是会回应我们,例如“OK”又或者其他的内容,这里初始化发送的每条AT指令都必须要得到无线模块的回应,这样防止无线模块未按照预期模式运行,如果初始化出现错误时,需要即使去处理对应的问题
这里如果不了解为什么要这样写代码的逻辑,放个传送门:
物联网实践教程:微信小程序结合OneNET平台MQTT实现STM32单片机远程智能控制 远程上报和接收数据——ESP8266/01s AT指令连接OneNET MQTT篇
/**
* @简要 无线模块初始化函数
* @参数 无
* @注意事项 如果函数初始化失败,会进入错误函数
* @返回值 无
*/
void Wireless_Init(void)
{
const uint8_t sendDataCount = 10;
printf("\r\nStart MQTT service\r\n");
printf("1. AT+RST\r\n"); //需要延迟最少500ms,否则下一句指令发送无效
if(Wireless_Send_Command("AT+RST\r\n\r\n", "", sendDataCount) == 1) WirelessStatus.wirelessInitErrorCode |= 1 << 0;
HAL_Delay(500);
printf("2. ATE0\r\n"); //关闭回显
if(Wireless_Send_Command("ATE0\r\n", "OK", sendDataCount) == 1) WirelessStatus.wirelessInitErrorCode |= 1 << 1;
HAL_Delay(100);
printf("3. AT+CWAUTOCONN=0\r\n"); //上电不自动连接 AP
if(Wireless_Send_Command("AT+CWAUTOCONN=0\r\n", "OK", sendDataCount) == 1) WirelessStatus.wirelessInitErrorCode |= 1 << 2;
HAL_Delay(100);
printf("4. AT+CWMODE=1\r\n"); //设置 Station 模式
if(Wireless_Send_Command("AT+CWMODE=1\r\n", "OK", sendDataCount) == 1) WirelessStatus.wirelessInitErrorCode |= 1 << 3;
HAL_Delay(100);
printf("5. AT+CWDHCP=1,0\r\n"); //启用DHCP
if(Wireless_Send_Command("AT+CWDHCP=1,0\r\n", "OK", sendDataCount) == 1) WirelessStatus.wirelessInitErrorCode |= 1 << 4;
HAL_Delay(100);
printf("6. AT+CWJAP=\"%s\",\"%s\"\r\n", WIFI_SSID, WIFI_PASSWORD); //连接AP
if(Wireless_Send_Command(WIRELESS_WIFI_INFO, "GOT IP", 8) == 1) WirelessStatus.wirelessInitErrorCode |= 1 << 5;
HAL_Delay(100);
printf("7. ESP8266_USERCFG_INFO=%s\r\n", ONENET_MQTT_USERCFG_INFO); //设置 MQTT 用户属性
if(Wireless_Send_Command(ONENET_MQTT_USERCFG_INFO, "OK", sendDataCount) == 1) WirelessStatus.wirelessInitErrorCode |= 1 << 6;
HAL_Delay(100);
printf("8. ESP8266_ONENET_INFO=%s\r\n",ONENET_MQTT_SERVER_INFO); //连接 MQTT Broker
if(Wireless_Send_Command(ONENET_MQTT_SERVER_INFO, "OK", sendDataCount) == 1) WirelessStatus.wirelessInitErrorCode |= 1 << 7;
HAL_Delay(100);
printf("9. SET_TOPIC=%s\r\n",ONENET_MQTT_SET_TOPIC); //设置 MQTT 用户属性
if(Wireless_Send_Command(ONENET_MQTT_SET_TOPIC, "OK", sendDataCount) == 1) WirelessStatus.wirelessInitErrorCode |= 1 << 8;
HAL_Delay(100);
if(WirelessStatus.wirelessInitErrorCode == 0)
{
WirelessStatus.wirelessInitFlag = 1;
printf("MQTT service started successfully\r\n");
}
else
{
WirelessStatus.wirelessInitFlag = 0;
printf("MQTT service failed to start,ERROR CODE:%X\r\n", WirelessStatus.wirelessInitErrorCode);
Wireless_Error_Handler(WirelessStatus.wirelessInitErrorCode); //错误处理函数
}
}
在上方代码中:
if(Wireless_Send_Command("AT+RST\r\n\r\n", "", sendDataCount) == 1)
这个函数原型:
/**
* @简要 无线模块发送指令并且等待回应数据
* @参数 cmd:需要发送的指令字符串地址
* @参数 res:需要回应的指令
* @参数 sendCount:最大发送指令次数
* @返回值 无
*/
uint8_t Wireless_Send_Command(char *cmd, char *res, uint8_t sendCount)
{
uint8_t status = 1;
while(sendCount--)
{
Wireless_Usart_Send(cmd);
//printf("cmd :%s",cmd);
if(WirelessStatus.receiveDataFlag == 1)
{
if(strstr((const char *)WirelessRx.RxBuffer, res) != NULL) //若找到关键字
{
status = 0;
WirelessStatus.receiveDataFlag = 0;
Wireless_Buffer_Clear();
break;
}
Wireless_Buffer_Clear();
WirelessStatus.receiveDataFlag = 0;
}
HAL_Delay(200);
}
return status;
}
这里还需要修改明白一个底层的发送函数:
需要将里面的串口发送修改为自己的
这里面的DMA配置也有文章可以参考:
STM32基于HAL库使用串口+DMA 不定长接收数据 学习记录
/**
* @简要 用来适配无线模块发送字符的函数
* @参数 cmd: 传入需要发送字符指针
* @返回值 无
*/
void Wireless_Usart_Send(char *cmd)
{
if (cmd == NULL) return; // 确保指针不为空
// 发送字符串
// 参数依次为:USART句柄指针、发送数据缓冲区指针、数据字节长度
HAL_UART_Transmit_DMA(&huart2, (uint8_t *)cmd, strlen(cmd));
}
串口接收采用了DMA空闲接收回调函数处理(可以参考上方连接配置):
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART1)
{
Uart1Rx.RxDataCnt = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
if(Uart1Rx.RxBuffer[Uart1Rx.RxDataCnt - 2] == '\r' && Uart1Rx.RxBuffer[Uart1Rx.RxDataCnt - 1] == '\n')
{
HAL_UART_Transmit_DMA(&huart1, Uart1Rx.RxBuffer, Uart1Rx.RxDataCnt);
}
}
if (huart->Instance == USART2)
{
WirelessRx.RxDataCnt = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);
if(WirelessRx.RxBuffer[WirelessRx.RxDataCnt - 2] == '\r' && WirelessRx.RxBuffer[WirelessRx.RxDataCnt - 1] == '\n')
{
WirelessStatus.receiveDataFlag = 1;
if(WirelessStatus.wirelessInitFlag == 1) printf("Receive\r\n");
//HAL_UART_Transmit_DMA(&huart1, WirelessRx.RxBuffer, WirelessRx.RxDataCnt);
}
}
}
2.上报数据
这里开启了一个定时器,用一个LED灯去显示当前wifi模块状态,同时也在后台计时,准备设置发送数据标志位
这个定时器的存在只是为了加强用户交互,显示当前WiFi模块的工作状态,例如连接OneNET初始化失败,正在连接,成功连接等等,如果不需要的话完全可以删除,然后发送计时可以在主函数用一个变量累加到一定数量时触发发送
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static uint16_t countTimer_SendData_OneNET = 0;
static uint16_t countTimer_LED_Toggle = 0;
if (htim == (&htim1))
{
if(countTimer_SendData_OneNET++ > 4000)
{
countTimer_SendData_OneNET = 0;
WirelessStatus.sendDataFlag = 1;
}
if(countTimer_LED_Toggle++ >= 1000)
{
countTimer_LED_Toggle = 0;
if(WirelessStatus.wirelessInitErrorCode == 0 && WirelessStatus.wirelessInitFlag == 0)
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin); //正在初始化
else if(WirelessStatus.wirelessInitErrorCode == 0 && WirelessStatus.wirelessInitFlag == 1)
HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_SET); //初始化成功
else if(WirelessStatus.wirelessInitErrorCode != 0)
HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET); //初始化失败
}
}
}
这里是整个无线模块的运行函数
/**
* @简要 无线模块运行函数
* @参数 无
* @注意事项 一直轮询,可以设置为定时触发,但是回应下发的数据会变慢
* @返回值 无
*/
void Wireless_Loop(void)
{
if(WirelessStatus.receiveDataFlag == 1)
{
WirelessStatus.receiveDataFlag = 0; //清除接收数据标志位
Wireless_Receive_Command_Respond((char *)WirelessRx.RxBuffer); //接收到指令后进行回应操作
Wireless_Buffer_Clear(); //执行完接收数据的所有操作后再清除缓存
}
else if(WirelessStatus.sendDataFlag == 1 && WirelessStatus.wirelessInitFlag == 1)
{
WirelessStatus.sendDataFlag = 0; //清除发送数据标志位
Wireless_Publish_Data(2, floatIdentifiers, floatValues, 0, stringIdentifiers, stringValues); //上报数据
}
}
这里代码是放在主函数的while循环中一直执行
在这个代码中可以看到,上报数据和接收数据是互斥的,接收数据处理优先级比发送大,这里这样互斥处理的原因是:如果在wifi模块正在接收OneNET下发的数据时,又正好发送数据,无线模块接收的数据通过串口发送到STM32中,数据中可能会连带两种状态的数据(1:OneNET下发的数据,2:STM32上报数据时发送的AT指令的应答)这样解析接收的数据时出错率会大大提升
上报数据的核心代码:
这里代码逻辑不清楚可以参考下方两个文章:
这两个文章中有说明需要发送的数据json格式和数据类型
物联网实践教程:微信小程序结合OneNET平台MQTT实现STM32单片机远程智能控制 远程上报和接收数据——ESP8266/01s AT指令连接OneNET MQTT篇
OneNET官方文档:MQTT协议接入 最佳实践
手动拼装json格式数据
/**
* @简要 无线模块上报数据
* @参数 floatCount: 需要发送的float数据个数
* @参数 *floatIdentifiers[]:float标识符数组
* @参数 floatValues[]:float数据数组
* @参数 stringCount:需要发送的string数据个数
* @参数 *stringIdentifiers[]:string标识符数组
* @参数 const char *stringValues[]:string数据数组
* @注意事项 这里需要二重转义:第一次转义字符是软件上字符串要求格式,第二次是因为AT指令发送字符串,
需要再转义一次,故不方便使用json组装数据
* @返回值 无
*/
void Wireless_Publish_Data(unsigned char floatCount, const char *floatIdentifiers[], float floatValues[], unsigned char stringCount, const char *stringIdentifiers[], const char *stringValues[])
{
// 初始化缓冲区位置
size_t bufferPos = 0;
const uint8_t BUFFER_SIZE = 255;
char globalBuffer[BUFFER_SIZE];
// 拼接主题和JSON开头
bufferPos += snprintf(globalBuffer + bufferPos, BUFFER_SIZE - bufferPos, "%s,\"{\\\"id\\\":\\\"123\\\"\\,\\\"params\\\":{", PUBTOPIC);
// 处理浮点数数据
for (unsigned char i = 0; i < floatCount; ++i) {
bufferPos += snprintf(globalBuffer + bufferPos, BUFFER_SIZE - bufferPos,
"\\\"%s\\\":{\\\"value\\\":%.2f\\}%s",
floatIdentifiers[i], floatValues[i],
(i < floatCount - 1 || stringCount > 0) ? "\\," : "");
}
// 处理字符串数据
for (unsigned char i = 0; i < stringCount; ++i) {
bufferPos += snprintf(globalBuffer + bufferPos, BUFFER_SIZE - bufferPos,
"\\\"%s\\\":{\\\"value\\\":\\\"%s\\\"\\}%s",
stringIdentifiers[i], stringValues[i],
(i < stringCount - 1) ? "\\," : "");
}
// 拼接JSON结尾
bufferPos += snprintf(globalBuffer + bufferPos, BUFFER_SIZE - bufferPos, "}}\",0,0\r\n");
// 确保我们没有超出缓冲区
if (bufferPos >= BUFFER_SIZE) {
// 处理错误,例如通过日志记录
return;
}
Wireless_Send_Command(globalBuffer,"OK",1);
}
使用keil.Jansson库组装
需要注意使用jansson需要调大启动文件中的堆空间大小(方法可参考下方链接中)
这里放个keil.Jansson使用方法:
STM32在Keil5中利用Jansson库处理和组装JSON数据【详细版】
/**
* @简要 无线模块上报数据
* @参数 floatCount: 需要发送的float数据个数
* @参数 *floatIdentifiers[]:float标识符数组
* @参数 floatValues[]:float数据数组
* @参数 stringCount:需要发送的string数据个数
* @参数 *stringIdentifiers[]:string标识符数组
* @参数 const char *stringValues[]:string数据数组
* @注意事项 这里使用json组装,需要动态分配大量的内容,需要将堆大小设置最小0x600
* @返回值 无
*/
void Wireless_Publish_Data(unsigned char floatCount, const char *floatIdentifiers[], float floatValues[], unsigned char stringCount, const char *stringIdentifiers[], const char *stringValues[]) {
json_t *root, *id_obj, *version_obj, *params_obj;
char *json_str;
size_t json_str_len;
// 创建JSON对象
root = json_object();
id_obj = json_string("123");
version_obj = json_string("1.0");
params_obj = json_object();
// 添加id和version到root对象
json_object_set_new(root, "id", id_obj);
json_object_set_new(root, "version", version_obj);
// 添加浮点数数据到params对象
for (unsigned char i = 0; i < floatCount; ++i) {
json_t *float_val_obj = json_object();
json_object_set_new(float_val_obj, "value", json_real(floatValues[i]));
json_object_set_new(params_obj, floatIdentifiers[i], float_val_obj);
}
// 添加字符串数据到params对象
for (unsigned char i = 0; i < stringCount; ++i) {
json_t *string_val_obj = json_object();
json_object_set_new(string_val_obj, "value", json_string(stringValues[i]));
json_object_set_new(params_obj, stringIdentifiers[i], string_val_obj);
}
// 将params对象添加到root对象
json_object_set_new(root, "params", params_obj);
// 将JSON对象转换为字符串
json_str = json_dumps(root, JSON_INDENT(0) | JSON_REAL_PRECISION(4));
json_str_len = strlen(json_str);
// printf("json_str = %s\r\n",json_str);
// 为MQTT发布准备最终的消息字符串
char globalBuffer[256]; // 确保缓冲区足够大
snprintf(globalBuffer, sizeof(globalBuffer), "%s,%d,0,0\r\n", ONENET_MQTT_PUB_MQTTPUBRAW, json_str_len + 2); //添加\r\n的长度
// 发送命令
Wireless_Send_Command(globalBuffer, "OK", 1);
strcat(json_str, "\r\n");
printf("%s%s",globalBuffer, json_str);
Wireless_Send_Command(json_str, "+MQTTPUB:OK", 1);
// 释放JSON对象
json_decref(root);
free(json_str); // 注意:json_dumps分配的内存需要手动释放
}
该函数使用示例:
Wireless_Publish_Data(2, floatIdentifiers, floatValues, 0, stringIdentifiers, stringValues);
//代表发送两个浮点数据,0个字符串数据
3.接收OneNET下发的指令数据
这里下发数据部分可以查看该文章:
物联网实践教程:微信小程序结合OneNET平台MQTT实现STM32单片机远程智能控制 远程上报和接收数据——ESP8266/01s AT指令连接OneNET MQTT篇
在OneNET的API调试:物模型设置:设备属性设置,填入对应的json格式数据和参数
如果上文宏定义中的命令标识符是什么,那么OneNET API调试中就应该填写对应的
这是串口接收函数
if (huart->Instance == USART2)
{
WirelessRx.RxDataCnt = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);
if(WirelessRx.RxBuffer[WirelessRx.RxDataCnt - 2] == '\r' && WirelessRx.RxBuffer[WirelessRx.RxDataCnt - 1] == '\n')
{
WirelessStatus.receiveDataFlag = 1;
if(WirelessStatus.wirelessInitFlag == 1) printf("Receive\r\n");
//HAL_UART_Transmit_DMA(&huart1, WirelessRx.RxBuffer, WirelessRx.RxDataCnt);
}
}
这里可以看出,如果wifi模块接收到OneNET下发的数据后,接收标志位会被置1,这里不影响无线模块函数初始化
这里是整个无线模块的运行函数
/**
* @简要 无线模块运行函数
* @参数 无
* @注意事项 一直轮询,可以设置为定时触发,但是回应下发的数据会变慢
* @返回值 无
*/
void Wireless_Loop(void)
{
if(WirelessStatus.receiveDataFlag == 1)
{
WirelessStatus.receiveDataFlag = 0; //清除接收数据标志位
Wireless_Receive_Command_Respond((char *)WirelessRx.RxBuffer); //接收到指令后进行回应操作
Wireless_Buffer_Clear(); //执行完接收数据的所有操作后再清除缓存
}
else if(WirelessStatus.sendDataFlag == 1 && WirelessStatus.wirelessInitFlag == 1)
{
WirelessStatus.sendDataFlag = 0; //清除发送数据标志位
Wireless_Publish_Data(2, floatIdentifiers, floatValues, 0, stringIdentifiers, stringValues); //上报数据
}
}
这里代码是放在主函数的while循环中一直执行
可以看出如果接受标志位被置1后,会执行这个函数
/**
* @简要 无线模块接收到下发的数据后进行回应
* @参数 str:无线模块接收到的数据
* @注意事项 这里判断下发的指令中是否包含关键字: "+MQTTSUBRECV"
里面的printf可以打印接收到的数据和转换后的数据
* @返回值 无
*/
void Wireless_Receive_Command_Respond(const char * str)
{
char *jsonData;
char id[5] = {0};
int command = 0;
if(strstr(str, "+MQTTSUBRECV") != NULL) //利用AT指令的特性去判断接受的内容是否包含关键字以验证数据来源
{
//printf("str = %s",str); //【调试】
jsonData = strchr((const char*)str, '{'); //提取json格式数据,通常是以 “{”开始 以“}”结尾
//printf("jsonData = %s",jsonData); //【调试】
Remove_Trailing_Crlf(jsonData); //这里需要去除末尾的换行回车符
Wireless_Extract_Receive_Command(jsonData, id, &command); //这里使用keil.Jansson去提取下发数据的id和command
Wireless_Receive_Ack_CloudPlatform((char *)str, id); //这里回应OneNET,表示接收到了数据
Wireless_Receive_Command_Control(command); //这里对接收到的command执行对应的操作
}
}
这个函数会先判断数据来源,再利用Keil.Jansson库解析数据,这个库的使用方法:
STM32在Keil5中利用Jansson库处理和组装JSON数据【详细版】
这个函数就是通过OneNET下发的数据去执行对应的操作
/**
* @简要 执行无线模块接收到下发指令对应的操作
* @参数 cmdValue:提取后的指令数据
* @注意事项 这里的指令可以更改为比int更大类型,需要在提取Wireless_Extract_Receive_Command函数中修改提取的数据大小
* @返回值 无
*/
void Wireless_Receive_Command_Control(int cmdValue)
{
//1000 1 0 :1000灯 2 代表2号灯 0代表灭
if(cmdValue == 10021) HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin,GPIO_PIN_SET);
else if(cmdValue == 10020) HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin,GPIO_PIN_RESET);
if(cmdValue == 10031) HAL_GPIO_WritePin(LED3_GPIO_Port, LED3_Pin,GPIO_PIN_SET);
else if(cmdValue == 10030) HAL_GPIO_WritePin(LED3_GPIO_Port, LED3_Pin,GPIO_PIN_RESET);
}
代码下载
地址:
补充
如果后续有什么问题,会在这里说明