背景条件:
在准备电赛的时候,速成了一下STM32,我用的HAL库,而且之前有一些基础,入门比较快。想着找一些题目来做,来提升自己的开发的能力,考虑到最近一直在搞串口,顺便题目的一个附加功能实现一下。
题目的具体描述是这样的,把单片机的数据通过WIFI模块传给服务器,然后服务器再把数据发送给电脑并绘制成图像,X轴的数据是确定的,这里为了简单实现起见,我X轴的数据都设置的比较简单,只需要传送y轴的数据即可。
思路与硬件选择:
WIFI模块与服务器都是用的esp32,其中一个esp32作为客户端,用UART2硬串口接收STM32F103的串口数据,然后经过数据检验传给服务器的esp32,服务器的esp32把接收到的数据转成浮点数发给PC端,然后PC端直接绘制就很方便了,esp32都是使用Arduino进行开发。PC端这边我用的是python进行开发,因为python做起来比较简单。
各部分代码:
STM32F103部分:
由于我用的是HAL库进行开发,所以我一些配置直接在CubeMX勾勾勾就搞定了,我用的USART1来进行串口通信,而且我用的是HAL_UART_Transmit_IT()函数来异步串口通信,并不是阻塞传输。传输的是10个浮点数,
main.c代码片段:按键函数可以自己定义这里我用的原子哥的。
#include "uarttoesp32.h"#include "gpio.h"int main(void){ float floatarr[10] = {2.34, 45.6, 44.1, 6.7, 23.3, 3.3, 9.8, 74.2, 11.2, 34.2}; HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); while (1) { s = KEY_Scan(0); if (s == 1) { HAL_GPIO_TogglePin(LED0_GPIO_Port,LED0_Pin); FloatFrameSend(floatarr); } }}
这里的通信协议,比较简单因为我这边的通信距离比较短,信息失真率可以忽略,如果比较远的话可能要把数据封装更复杂一点
我的通信协议具体是这样的:
uarttoesp32.c代码片段(这里仅给出一些关键的代码,初始化的可以自行改一下):
#include "uarttoesp32.h"uint8_t TransmitCpl = 0;uint8_t frame_send_buff[SEND_LEN] = {0};uint8_t current_index = 1;UART_HandleTypeDef huart1; // USART句柄uint8_t SumCheck(uint8_t index) // 数据和{ uint8_t sum_check = 0; for(uint8_t i=0; i<4;i++) { sum_check += frame_send_buff[index + i]; } return sum_check;}void float_to_singleframe(float* data,uint8_t* buffer) // 把数据做成单帧{ uint8_t *byte_arr; // 取数据的首地址,数据以字节存贮,看成一个字节数组 byte_arr = (uint8_t*)data; for(uint8_t i=0; i<PRE_DATA_LEN; i++) { if (i<PRE_DATA_LEN - 1) buffer[current_index+i] = byte_arr[i]; // 前4位是数据,这种多字节存储是小端存储 else buffer[current_index+i] = SumCheck(current_index); // 最后一位是校验和 } current_index += PRE_DATA_LEN;}void FloatFrameSend(float* data) // float类型发送,中断发送{ frame_send_buff[0] = FRAME_HEAD; // 先加个帧头 for(uint8_t i=0;i<DATA_NUM;i++) // 循环遍历数据,将帧加入数据域 { float_to_singleframe(&data[i],frame_send_buff); } frame_send_buff[END_POS] = FRAME_END; // 加个帧尾 current_index = 1; HAL_UART_Transmit_IT(&huart1,frame_send_buff,SEND_LEN); // 异步发送}
uarttoesp32.h代码片段:
#ifndef __UARTTOESP32_H#define __UARTTOESP32_H#include "main.h"#define FRAME_HEAD 0xFF // 帧头#define FRAME_END 0xDD // 帧尾#define PRE_DATA_LEN 5 // 每个数据的长度(加上校验和)#define DATA_NUM 10 // 数据个数#define DATA_AREA_LEN (DATA_NUM*PRE_DATA_LEN) // 数据域长度#define END_POS (DATA_AREA_LEN + 1) // 帧尾位置#define SEND_LEN (DATA_AREA_LEN + 2) // 发送帧的帧长度void MX_USART1_UART_Init(void);extern uint8_t TransmitCpl;void USART1_IRQHandler(void);void FloatFrameSend(float* data);#endif
esp32客户端代码:
我用的是UART2串口,之前用的8266的软串口还有波特率限制,esp32就爽多了,用两根杜邦线把esp32的RX,TX跟STM32的PA9和PA10连接就好了,当客户端的esp32串口接收完数据之后就校验数据,数据是对的才会发送。用WIFI把数据传输给服务端的esp32,用的TCP连接,传输完就关闭,释放资源。
运行结果:
#include <WiFi.h>//#include <WiFiServer.h>#include <Arduino.h>/**************************定义***************************/#define FRAME_LEN 52#define DATA_NUM 10/**********************声明变量***********************/uint8_t frame[FRAME_LEN];uint8_t current_index = 0;uint8_t receive_done = 0;const char* ssid = "ESP32";const char* password = "12345678";const char* serverIP = "192.168.1.1"; // Replace with your ESP32 server IP addressconst int serverPort = 120; // Replace with your ESP32 server portbool head_flag = false; // 帧头是否正确bool end_flag = false; // 帧尾是否正确bool sum_error = false; // 校验和是否全正确WiFiClient espclient;void setup() { Serial.begin(115200); // 初始化串口用于调试输出 Serial2.begin(115200); // 初始化UART2,波特率9600 WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.println("WiFi connected"); Serial.print("IP address: "); Serial.println(WiFi.localIP());}void loop() { if (Serial2.available()) { // 检查是否有数据可读 while (Serial2.available()) { // 持续读取串口信息 if (current_index < FRAME_LEN){ uint8_t bytes = Serial2.read(); if (bytes == 0xFF){ // 帧头是否正确 head_flag = 1; } if (bytes == 0xDD){ // 帧尾是否正确 end_flag = 1; } if (head_flag){ frame[current_index] = bytes; } current_index++; } } CheckSum(); if (current_index == FRAME_LEN && end_flag && head_flag && !sum_error){ // 判断长度,帧头,帧尾是否正确,判断校验和是不是全对 receive_done = 1; // 可以发送标志位置1 } current_index = 0; // 清除标志位 end_flag = 0; head_flag = 0; sum_error = 0; } if (receive_done == 1){ SendFrame(); }}void CheckSum(){ // 校验和检验函数 uint8_t sum = 0; for(uint8_t i=1; i<FRAME_LEN-2; i++){ // 遍历数据域 if(i%5 == 0){ // 到达校验和位置 if(sum != frame[i]) sum_error = 1; // 只要有校验和有一个出错,置1 sum = 0; // 归零 } else{ sum += frame[i]; // 未到达校验和位置,累加 } }}void printdata(){ // 数据打印函数 if (receive_done==1){ for(uint8_t i=0;i<FRAME_LEN;i++){// Serial.print("第"+String(i)+"位"); Serial.print(frame[i],HEX); Serial.print(" "); } Serial.println(" "); receive_done = 0; }}
esp32服务端代码:
用TCP连接接收客户端esp32传输的数据,同样地,需要检验数据是否正确,正确才会发送给PC端,而对于PC端的传输,是采用HTTP请求,当PC端来一次请求就发起一次,用的WebServer库。由于用的是HTTP请求,只能发字符串,我采用JSON格式来封装数据,提高数据的稳定性,所以也导入了ArduinoJson库。注意的是,TCP传输和Web传输两个使用不同的类,而且也要用不同的端口。
运行结果:
#include <ArduinoJson.h>#include <WiFi.h>#include <WebServer.h>#include <WiFiServer.h>#include <Arduino.h>/************************定义****************************/#define FRAME_LEN 52#define DATA_NUM 10/*******************变量声明****************************/const char* ssid = "ESP32";const char* password = "12345678";//float Xarray[] = {10,20,30,40,50,60,70,80,90,100};int bytesRead = 0; // 用于保存已读取的字节数uint8_t recceive_frame[FRAME_LEN]; // 接收的数据float Yarray[DATA_NUM];bool head_flag = false; // 帧头是否正确bool end_flag = false; // 帧尾是否正确bool sum_error = false; // 校验和是否全正确bool data_correct = false; // 接收数据是否正确WiFiServer server32(120);WebServer server(80); // 配置端口号IPAddress local_ip(192,168,1,1); // 设置esp32网络参数,IP,以及子网掩码IPAddress gateway(192,168,1,1);IPAddress subnet(255,255,255,0);String jsondata() // 数据JSON格式化{ String jsonStr; StaticJsonDocument<512> doc; // 将浮点型数组添加到 JSON 对象中 JsonArray yArray = doc.createNestedArray("yData"); for (int i = 0; i < DATA_NUM; i++) { yArray.add(Yarray[i]); } // 将 JSON 对象序列化为字符串 serializeJson(doc, jsonStr); return jsonStr;}void setup() { Serial.begin(115200); // 配置串口波特率 WiFi.softAP(ssid, password); // 开启AP模式,启动热点 WiFi.softAPConfig(local_ip, gateway, subnet); // 写入网络配置参数 Serial.print("WiFi名称为::"); Serial.println(ssid); Serial.print("IP地址为:: "); Serial.println(WiFi.softAPIP()); //启动服务器 server.begin(); server32.begin(); server.on("/data",HTTP_GET,handleRX); server.onNotFound(handleFound);}void loop() { // 主函数 WiFiClient clientfrom32 = server32.available(); // esp32客户端请求 if(clientfrom32){ handleTransmit(clientfrom32); } server.handleClient(); // 处理URL请求}void bytes_to_float(){ // 字节数据转浮点型数据 for (int i = 0; i < DATA_NUM; i++) { float floatValue; memcpy(&floatValue, &recceive_frame[i*5 + 1], sizeof(float)); Yarray[i] = floatValue; }}void CheckSum(){ // 校验和检验函数 uint8_t sum = 0; for(uint8_t i=1; i<FRAME_LEN-2; i++){ // 遍历数据域 if(i%5 == 0){ // 到达校验和位置 if(sum != recceive_frame[i]) sum_error = 1; // 只要有校验和有一个出错,置1 sum = 0; // 归零 } else{ sum += recceive_frame[i]; // 未到达校验和位置,累加 } }}void handleRX() // http请求处理函数{ String jst = ""; jst = jsondata(); Serial.println("有客户访问,"); server.send(200,"text/plain",jst);}void handleTransmit(WiFiClient clientfrom32){ // 发送处理函数 Serial.println("New client connected!"); // Read data from the client and send it back while (clientfrom32.available()) { // 下标小于帧长 if (bytesRead < FRAME_LEN) { uint8_t bytes = clientfrom32.read(); if (bytes == 0xFF){ // 帧头是否正确 head_flag = 1; } if (bytes == 0xDD){ // 帧尾是否正确 end_flag = 1; } if (head_flag){ recceive_frame[bytesRead] = bytes; } bytesRead++; } } CheckSum(); if (bytesRead == FRAME_LEN && end_flag && head_flag && !sum_error){ // 判断长度帧头尾校验和是不是对的 data_correct = 1; } bytesRead = 0; // 清除标志位 end_flag = 0; head_flag = 0; sum_error = 0; if(data_correct){// printframe(); bytes_to_float();// printfloat(); } clientfrom32.stop(); // 关闭客户端连接}void printframe(){ // 打印接收到的字节数据 if(data_correct){ for(uint8_t i=0;i<FRAME_LEN;i++){ Serial.print(recceive_frame[i],HEX); Serial.print(" "); } Serial.println(" "); }}void printfloat(){ if(data_correct){ for(uint8_t i=0; i<DATA_NUM; i++){ Serial.print(Yarray[i]); Serial.print(" "); } Serial.println(" "); }}void handleFound() // 请求错误{ server.send(404,"text/plain","404:Not Found!");}
PC端代码:
电脑这边我用的是python来进行接收,用json库把数据解读然后用matplotlib来绘制数据图像,用PySimpleGUI来弄一个交互窗口,这个PySimpleGUI确实够simple的,具体怎么样你们可以参考网上其他的文章。这里我先把图画出来然后保存,然后把图插入GUI里面,有些文章是直接把窗口插进去然后渲染,这样太慢了。每次按下刷新按钮就向esp32请求一次,然后就把新的数据重新绘图并插入到GUI
运行结果:
import PySimpleGUI as sgimport matplotlib.pyplot as pltimport requestsimport jsonXarray = [10,20,30,40,50,60,70,80,90,100] # X轴数据correct = False # 未能正确请求,服务器返回非数据def client(): global correct try: url = 'http://192.168.1.1/data' # 服务器地址 r = requests.get(url) # 用JSON格式进行数据传输,方便数据传输和读取 Y = json.loads(r.text)['yData'] correct = True return Y except: correct = False return [0]*10def draw_graph(x, y): plt.clf() # 清除之前的图形 plt.plot(x, y, marker='o', linestyle='-') plt.xlabel('X轴') plt.ylabel('Y轴') plt.grid() # 用来正常显示中文标签 plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示负号 plt.rcParams['axes.unicode_minus'] = False plt.savefig('plot.png') # 保存生成的图片# 初始化数据和图形data_y = client()draw_graph(Xarray, data_y)layout = [ # 界面布局 [sg.Image(filename='plot.png', key='-IMAGE-')], # 图片显示 [sg.Button('刷新数据', key='-REFRESH-'), sg.Button('退出', key='-EXIT-')] # 按钮]if(correct): window = sg.Window('数据图形', layout) # 窗口else: window = sg.Window('错误请求', layout) # 窗口while True: # 循环等待事件发生 event, values = window.read() if event == sg.WIN_CLOSED or event == '-EXIT-': # 退出 break elif event == '-REFRESH-': # 刷新数据 data_y = client() draw_graph(Xarray, data_y) window['-IMAGE-'].update(filename='plot.png') # 刷新图片window.close()
结尾:
以上就是我本次STM32串口数据利用WIFI传输到电脑接收的开发过程,客户端先用了esp8266,不知道是他软串口的问题还是什么,反正有时候串口会犯病,单片机没发数据就给我读一些奇怪数据,后面就换了esp32来读串口。而且有个很怪的点,我这个单片机得把DAP下载器插到电脑上面才能按键按下然后发送,我不懂这是为什么。
新手上路,请多多指教,如果本篇博客有哪里是错误的请指正。