前言
忙了几天终于把项目做完了,内容没有很多,但是第一次接触TCP/IP并且实现labview和imx6ull网络通讯对我而言还是很有难度的,好在最后完成了;
第二点就是,之前在学习imx6ull包括驱动和应用部分,都是需要网线,必须有网线才能用网络挂载zImage、设备树还有根文件系统,这样调试是很方便的,只需要在Ubuntu上把编译好的模块或者可执行文件复制到根文件系统中,即可在板子上执行,问题就是:有时我觉得调试好了,把程序烧到板子里面之后,如果又有问题需要修改代码,这样就又需要网线;但是我具体做实验的时候实验室没有网线可用,这就大大限制了我对板子的使用;为了解决这个问题,正好买板子正点原子送了一块wifi模块,我就把wifi模块学起来、用起来,最后也是实现了用wifi的方式从ubuntu向板子发送文件,这样根本不需要网线了,如果你的终端也不需要在主机上显示的话,下位机和上位机就可以完全实现无线传输了,非常方便!
一、Labview与imx6ull实现TCP/IP网络通讯,以及imx6ull深度学习模型调用
首先说一下做这个项目的动机:
1.一方面是比较功利的,因为发现网络通讯以及网络协议部分是比较重要的内容,32裸机和带RTOS看的差不多了,然后前面Linux驱动有了一定的基础,就决定好好研究一下Linux应用,前面学习了一下一些I/O操作、信号、进程(还没看完)、线程(还没看!后面一定要看完!但是这个项目中已经用到了多线程)、TCP/IP、socket套接字网络编程
2.另一方面,学习了这么多内容,如果只是学习的话那也太没劲了,正好我的课题中有一部分内容可以结合,就是在python构建tflite模型(主要是用于定量分析)、将tflite模型部署到imx6ull、labview通过TCP向imx6ull发送待定量数据、imx6ull调用tflite模型进行定量、定量结果和上位机发送的数据以csv形式保存在板子上、imx6ull通过TCP将定量结果发送至上位机(这些是第一个线程的任务);第二个线程主要是针对有时labview会卡死,虽然我在labview上已经加了自动备份的功能,但是由于备份定时的设置,还是会丢数据,这是可以在ubuntu上开一个客户端,向板子发送请求数据,第二个线程就会将之前保存的两个csv发送给ubuntu,这样数据就都丢不了啦。
好了好了,说了这么多,直接进入代码部分,labview部分我就直接上图了
1.imx6ull与labview之间的发送与接收
Labview发送数据:
Labview接收数据:
起始发送与接收的labview程序我都是直接参照范例里面的最简单的那个例子,一个客户端一个服务器之间通讯,因为我这里要下发的也是一个一维数组,和范例是一致的,所以我直接采用了他的代码(我的很丑,懒得整理了,大家需要的直接去看范例的代码)
imx6ull接收数据与发送数据:
这部分我就直接上代码了,具体需要解释的部分我会详细解释(只放重要的线程和函数)
main函数
int main(int argc , char ** argv) {
/*1.参数校验*/
if(argc !=2)
{
printf("Wrong input!!!\r\n");
printf("Please enter : ./App .tflite file \r\n");
return -1;
}
//加载模型
filename_spec = argv[1];
//服务器变量
int server_fd1, server_fd2;
struct sockaddr_in address1, address2;
int opt = 1;
//创建多线程
pthread_t spectrum_thread, server_thread;
// 创建第一个套接字
if ((server_fd1 = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置第一个套接字选项
if (setsockopt(server_fd1, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
// 配置第一个套接字地址
address1.sin_family = AF_INET;
address1.sin_addr.s_addr = INADDR_ANY;
address1.sin_port = htons(PORT1);
// 绑定第一个套接字到端口
if (bind(server_fd1, (struct sockaddr *)&address1, sizeof(address1)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听第一个端口
if (listen(server_fd1, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
// 创建第二个套接字
if ((server_fd2 = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置第二个套接字选项
if (setsockopt(server_fd2, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
// 配置第二个套接字地址
address2.sin_family = AF_INET;
address2.sin_addr.s_addr = INADDR_ANY;
address2.sin_port = htons(PORT2);
// 绑定第二个套接字到端口
if (bind(server_fd2, (struct sockaddr *)&address2, sizeof(address2)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听第二个端口
if (listen(server_fd2, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
// 创建处理红外光谱数据的线程
if (pthread_create(&spectrum_thread, NULL, process_spectrum_data, (void *)(intptr_t)server_fd1) != 0) {
perror("Failed to create infrared thread");
exit(EXIT_FAILURE);
}
// 创建处理其他请求的线程
if (pthread_create(&server_thread, NULL, network_server, (void *)(intptr_t)server_fd2) != 0) {
perror("Failed to create request thread");
exit(EXIT_FAILURE);
}
pthread_join(spectrum_thread, NULL);
pthread_join(server_thread, NULL);
return 0;
}
首先是main函数,主要需要完成的任务是:
1.接收tflite文件名
2.创建两个套接字用于两个客户端
3.创建两个线程:一个处理数据,一个上位机需要备份数据时上s
数据处理线程
void* process_spectrum_data(void *arg) {
std::unique_ptr<tflite::FlatBufferModel> model =
tflite::FlatBufferModel::BuildFromFile(filename_spec);
TFLITE_MINIMAL_CHECK(model != nullptr);
//初始化服务器变量
int server_fd = (int)(intptr_t)arg;
int new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
spec prediction_result[3];//定量结果存储数组
while (1) {
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
printf("LabVIEW connect successful!\r\n");
while(1){
// 接受数据大小
if (read(new_socket, &count, sizeof(int)) <= 0) {
break; // 连接关闭或出错,跳出循环
}
//转换字序为大端序号
count = ntohl(count);
printf("the total number of data is %d\r\n",count);
// 确保数据长度正确
if (count != 1090 * 8) {
fprintf(stderr, "Unexpected data size: %d\n", count);
break;
}
char *data_buffer = (char *)malloc(count);
if (data_buffer == NULL) {
perror("Failed to allocate memory");
break;
}
// 接受红外光谱数据
int total_received = 0;
while (total_received < count) {
int bytes_received = recv(new_socket, data_buffer + total_received, count - total_received, 0);
if (bytes_received <= 0) {
perror("Failed to read data");
free(data_buffer);
break;
}
total_received += bytes_received;
}
if(total_received == count){
printf("Received %d bytes of data.\r\n", total_received);
for (int i = 0; i < SINGBUFF_LENTH; ++i) {
uint64_t temp;
memcpy(&temp, data_buffer + i * 8, 8);
temp = ntohll(temp); // 字节序转换
double value;
memcpy(&value, &temp, sizeof(double));
UL_Second_Buff_Data[i] = value;
}
//判断输入数据长度是否为定长1090 (2帧头+1数据长度+1084数据+1校验+2帧尾)
if(count == (1090*8)){
New_Meassage = 1;
printf("Received one frame of data!\r\n");
UL_Second_Buff_To_UL_Valid();
UL_Valid_Message_Def valid_data_temp = {0};
if(UL_Valid_Message_Para.Valid_Buff_New_Meassage == 1)
{
printf("Valid Message!\r\n");
/* 循环解析,直到读写指针相等 */
while(UL_Valid_Message_Para.Valid_W_Ptr != UL_Valid_Message_Para.Valid_R_Ptr)
{
//printf("1\r\n");
valid_data_temp = UL_Valid_Message[UL_Valid_Message_Para.Valid_R_Ptr];
// Build the interpreter with the InterpreterBuilder.
// Note: all Interpreters should be built with the InterpreterBuilder,
// which allocates memory for the Interpreter and does various set up
// tasks so that the Interpreter can read the provided model.
tflite::ops::builtin::BuiltinOpResolver resolver;
tflite::InterpreterBuilder builder(*model, resolver);
std::unique_ptr<tflite::Interpreter> interpreter;
builder(&interpreter);
TFLITE_MINIMAL_CHECK(interpreter != nullptr);
// Allocate tensor buffers.
TFLITE_MINIMAL_CHECK(interpreter->AllocateTensors() == kTfLiteOk);
//printf("2\r\n");
int input_index = interpreter->inputs()[0];
spec* input_data = interpreter->typed_tensor<spec>(input_index);
for(int j=0;j<SPECTRUM_LENGTH;j++){
input_data[j] = valid_data_temp.data[j];
}
save_data_to_csv(input_data, "data.csv");
// Run inference
TFLITE_MINIMAL_CHECK(interpreter->Invoke() == kTfLiteOk);
int output_index = interpreter->outputs()[0];
spec* output_data = interpreter->typed_tensor<spec>(output_index);
double quan_arr[QUAN_LENGTH] = {0};
for (int i = 0; i < QUAN_LENGTH; i++) {
quan_arr[i] = output_data[i];
}
//将定量数据保存到csv中
save_quantification_to_csv(quan_arr,"quantifications.csv");
UL_Valid_Message_Para.Valid_R_Ptr++;
if(UL_Valid_Message_Para.Valid_R_Ptr == VALID_MESSAGE_NUMBER)
{
UL_Valid_Message_Para.Valid_R_Ptr = 0;
}
free(data_buffer);
// 发送预测结果大小
int result_size = QUAN_LENGTH * sizeof(double);
// 将结果大小转换为小端字节顺序
int result_size_le = htonl(result_size);
if (write(new_socket, &result_size_le, sizeof(int)) <= 0) {
perror("Failed to send result size");
break;
}
//创建一个缓冲区,用于存储所有浮点数的小端字节序数据
uint64_t buffer[QUAN_LENGTH];
for (int i = 0; i < QUAN_LENGTH; ++i) {
buffer[i] = htond(quan_arr[i]);
}
if (write(new_socket, buffer, result_size) <= 0) {
perror("Failed to send prediction result");
break;
}
}
UL_Valid_Message_Para.Valid_Buff_New_Meassage = 0;
}
else printf("inValid Message!\r\n");
count = 0;
}
}
}
close(new_socket); // 关闭当前连接,等待新的连接
}
close(server_fd);
}
这个线程里我遇到了很多的坑,接下来详解一下
代码中的TCP/IP通讯框架大家可以直接看我的main函数,在线程中,只要调用accept函数等待客户端发起通讯即可。
第一个问题:上位机发送数据长度count接收错误
因为labview在发送数据时,会先发送有多少字节的数据,再发送具体数据,所以接收时,也是先接收数据大小,再接收具体数据,这里我上位机下发的数据字节数应该是8720个字节,对应1090个8字节数据,但是下位机接收时发现这个count值非常大,这里举个例子:上位机发送大小为16712,下位机接收到的数值是1212219392
这里的关键是(我也是查资料才发现),labview是小端字序,下位机是大端字序!!!!
所以这导致了上位机下发的数据长度,在下位机接收时出现异常
解决方法是:ntohl() 函数,直接用这个函数就可以转换字序了,很方便
转换后接收数据长度值正常
第二个问题:具体数据转换错误
这个问题和上一个问题类似,也是字序问题,只不过这里是浮点数出现问题,解决方法就是一个字节一个字节大小去处理,我把具体代码放在下面
// 字节序转换函数
uint64_t ntohll(uint64_t value) {
uint32_t high_part = ntohl((uint32_t)(value >> 32));
uint32_t low_part = ntohl((uint32_t)(value & 0xFFFFFFFFLL));
return ((uint64_t)low_part << 32) | high_part;
}
for (int i = 0; i < SINGBUFF_LENTH; ++i) {
uint64_t temp;
memcpy(&temp, data_buffer + i * 8, 8);
temp = ntohll(temp); // 字节序转换
double value;
memcpy(&value, &temp, sizeof(double));
UL_Second_Buff_Data[i] = value;}
第三个问题:tflite模型定量结果保持不变
这个问题我不太清楚具体导致的原因,这里就把正确定量的模型框架放在这里,大家需要的话可以直接用,根据自己的数据类型修改一下代码就可以
//加载模型
std::unique_ptr<tflite::FlatBufferModel> model =
tflite::FlatBufferModel::BuildFromFile(filename_spec);
TFLITE_MINIMAL_CHECK(model != nullptr);
//构建模型
tflite::ops::builtin::BuiltinOpResolver resolver;
tflite::InterpreterBuilder builder(*model, resolver);
std::unique_ptr<tflite::Interpreter> interpreter;
builder(&interpreter);
TFLITE_MINIMAL_CHECK(interpreter != nullptr);
TFLITE_MINIMAL_CHECK(interpreter->AllocateTensors() == kTfLiteOk);
int input_index = interpreter->inputs()[0];
spec* input_data = interpreter->typed_tensor<spec>(input_index);
for(int j=0;j<SPECTRUM_LENGTH;j++){
input_data[j] = valid_data_temp.data[j];
}
//运行模型
TFLITE_MINIMAL_CHECK(interpreter->Invoke() == kTfLiteOk);
//获得输出
int output_index = interpreter->outputs()[0];
pec* output_data = interpreter->typed_tensor<spec>(output_index);
第四个问题:回传的定量数据应该是一个一维数组,但是在labview中只获得了一个数值,且数值错误
这个问题应该是数据类型混乱不清导致回传时出现问题,大家在具体写代码的时候,一定要弄清要使用何种数据类型,因为涉及到字序的问题,如果数据类型错误的话,是会出问题的。
这里要注意的是,我的代码要回传的数据是double类型,这里注意要回传给labview,是要从大端字序变为小端字序,不然上位机接收到的数据是错误的
// 字节序转换函数
uint64_t ntohll(uint64_t value) {
uint32_t high_part = ntohl((uint32_t)(value >> 32));
uint32_t low_part = ntohl((uint32_t)(value & 0xFFFFFFFFLL));
return ((uint64_t)low_part << 32) | high_part;
}
//创建一个缓冲区,用于存储所有浮点数的小端字节序数据
uint64_t buffer[QUAN_LENGTH];
for (int i = 0; i < QUAN_LENGTH; ++i) {
buffer[i] = htond(quan_arr[i]);
}
if (write(new_socket, buffer, result_size) <= 0) {
perror("Failed to send prediction result");
break;
}
二、使用wifi实现ubuntu和imx6ull文件传输,极大便利调试
话不多说,直接上代码!大家直接拿去用,根据自己的需求稍作更改即可
接收端(unbuntu)
这里的端口号(PORT)以及IP地址要跟板子的端口号和IP地址对应,板子的IP地址的话,大家可以去学习一下wifi模块如何使用,我是直接看的正点原子的代码,分配好ip地址后,在板子上用ifconfig命令可以查看ip地址;端口号的话,这个比较随意,只要客户端和服务器端的端口号一致即可
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8082
#define BUFFER_SIZE 1024
int main(int argc, char const *argv[]) {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[BUFFER_SIZE] = {0};
FILE *file;
if (argc != 2) {
fprintf(stderr, "Usage: %s <file_path>\n", argv[0]);
return -1;
}
const char *file_path = argv[1];
const char *file_name = strrchr(file_path, '/'); // 提取文件名
if (file_name == NULL) {
file_name = file_path; // 文件路径中没有'/',整个路径即文件名
} else {
file_name++; // 跳过'/'
}
// 创建socket文件描述符
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Socket creation error");
return -1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 转换IP地址
if (inet_pton(AF_INET, "1xx.1xx.1xx.1xx", &serv_addr.sin_addr) <= 0) {
perror("Invalid address/ Address not supported");
return -1;
}
// 连接服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
perror("Connection Failed");
return -1;
}
// 发送文件名,包含文件名的长度
int file_name_length = strlen(file_name);
send(sock, &file_name_length, sizeof(file_name_length), 0); // 发送文件名长度
send(sock, file_name, file_name_length, 0); // 发送文件名
// 打开文件以读取
file = fopen(file_path, "rb");
if (file == NULL) {
perror("Failed to open file");
close(sock);
return -1;
}
// 发送文件数据
int bytes_read;
while ((bytes_read = fread(buffer, sizeof(char), BUFFER_SIZE, file)) > 0) {
send(sock, buffer, bytes_read, 0);
}
printf("File sent successfully: %s\n", file_name);
// 关闭文件和socket
fclose(file);
close(sock);
return 0;
}
服务器端(imx6ull)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8082
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
FILE *file;
char file_name[BUFFER_SIZE] = {0};
// 创建socket文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置socket选项
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
// 绑定socket到端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听端口
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
// 接受客户端连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
// 接收文件名长度
int file_name_length;
if (read(new_socket, &file_name_length, sizeof(file_name_length)) <= 0) {
perror("Failed to receive file name length");
close(new_socket);
close(server_fd);
return -1;
}
// 接收文件名
if (read(new_socket, file_name, file_name_length) <= 0) {
perror("Failed to receive file name");
close(new_socket);
close(server_fd);
return -1;
}
// 确保文件名以空字符结尾
file_name[file_name_length] = '\0';
// 打开文件以写入
file = fopen(file_name, "wb");
if (file == NULL) {
perror("Failed to open file");
close(new_socket);
close(server_fd);
return -1;
}
// 接收文件数据
int bytes_read;
while ((bytes_read = read(new_socket, buffer, BUFFER_SIZE)) > 0) {
fwrite(buffer, sizeof(char), bytes_read, file);
}
printf("File received successfully: %s\n", file_name);
// 关闭文件和socket
fclose(file);
close(new_socket);
close(server_fd);
return 0;
}
使用时先运行服务器端,再运行客户端
总结
还有一些内容没有涉及,例如另一个用于发送备份数据的线程,其实和wifi传输文件的代码是很相似的,如果大家有需要可以私聊,可能还有一些细节没有涉及到,暂时就想到了这些,把问题详细整理了一下,好累,休息去啦,大家加油啊!