STM32F407+ESP8266图片传输并保存至SD卡


前言

本次实验基于正点原子的探索者STM32F407开发板,
在这里插入图片描述

代码基于正点原子提供的例程:实验41图片显示实验。
在这里插入图片描述
使用的ESP8266是AT指令版本的,通过串口与MCU的UART2相连
在这里插入图片描述


提示:本次实验默认竖屏,如果想要实现横屏效果请在工程lcd.c文件中把屏幕显示函数改为LCD_Display_Dir(1); //竖屏赋值0,横屏为1

一、实现效果

Python/Java运行Socket服务端代码,ESP8266通过AT指令设置为上电自动连接到服务端并透传,服务端给ESP8266发送图片字节,STM32通过FATFS把数据流写入本地SD卡。
在这里插入图片描述
上电后ESP8266连到Socket服务端后透传数据,服务端检测有设备连上后就发送图片数据

二、过程详解

1.串口帧中断

因为串口接收的图片数据是不定长的,而且为了更快的传输,我设置串口每次(一帧数据)最多能接收高达10K的数据包,不用帧中断的话容易丢失数据。
帧中断初始化代码:

void uart2_init(u32 bound){
	//GPIO端口设置
	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;

	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE); //使能GPIOA时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);//使能USART2时钟

	//串口2对应引脚复用映射
	GPIO_PinAFConfig(GPIOA,GPIO_PinSource2,GPIO_AF_USART2); //GPIOA2复用为USART2
	GPIO_PinAFConfig(GPIOA,GPIO_PinSource3,GPIO_AF_USART2); //GPIOA3复用为USART2

	//USART2端口配置
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3; //GPIOA2与GPIOA3
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;//复用功能
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	//速度50MHz
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
	GPIO_Init(GPIOA,&GPIO_InitStructure); //初始化PA2,PA3

	//USART2 初始化设置
	USART_InitStructure.USART_BaudRate = bound;//波特率设置
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
	USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
	USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	//收发模式
	USART_Init(USART2, &USART_InitStructure); //初始化串口2

	USART_Cmd(USART2, ENABLE);  //使能串口2 

	//USART_ClearFlag(USART1, USART_FLAG_TC);

	/* 打开空闲中断 */
	USART_ITConfig(USART2, USART_IT_IDLE, ENABLE);	
	USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);//开启相关中断

	//Usart2 NVIC 配置
	NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;//串口1中断通道
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3;//抢占优先级3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority =3;		//子优先级3
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
	NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器、

}
extern u8 data_from_esp8266[10240];	//在main文件定义的全局变量,用于存放图片数据
extern unsigned int DataCount;		//在main文件定义的全局变量,用于记录接收的数据个数
extern uint8_t Uart2_State;			//在main文件定义的全局变量,用于判断是否接收到了一帧数据

//串口2中断服务程序
void USART2_IRQHandler(void)  
{
	uint8_t Clear=Clear;//这种定义方法,用来消除编译器的"没有用到"提醒
	
	if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) // 如果接收到1个字节
	{ 	
		data_from_esp8266[DataCount++] = USART2->DR;// 把接收到的字节保存,数组地址加1
	} 
	else if(USART_GetITStatus(USART2, USART_IT_IDLE) != RESET)// 如果接收到1帧数据
	{
		Clear=USART2->SR;// 读SR寄存器
		Clear=USART2->DR;// 读DR寄存器(先读SR再读DR,就是为了清除IDLE中断)
		Uart2_State=1;// 标记接收到了1帧数据
	}
}  

其中data_from_esp8266,DataCount,Uart2_State这三个数据是main文件的全局变量,用于在主函数通过Uart2_State的值判断是否一帧数据是否已经保存到了data_from_esp8266,然后把data_from_esp8266的8位二进制数据写入文件中,而DataCount是总共接收到的数据大小,在后面FATFS向文件写入数据我们会用到这个值。

2.FATFS文件管理系统

FATFS是用于嵌入式的文件管理系统,简单来说就是通过FATFS你可以很方便的操作SD卡的文件,这个在原子的实验39 FATFS实验中可以看到详情,FATFS官网给的例程包括的就有文件复制链接:FATFS
在这里插入图片描述
所以我们按照官网给的例程把二进制数据写入SD卡文件中,这里的思路与正点原子给的例程不一样,正点原子给的例程是创建一个TXT文本文件,并且是以文本的格式写入的,不是二进制写入的,如果我们要写入图片数据必须要按照官方给的例程那样用二进制写入。

从上图官方给的代码可以看出,写入文件用的函数:
f_write(&fdst, buffer, br, &bw);
fdst是目标文件地址,buffer是存放二进制数据的数组(对应本次工程的data_from_esp8266),这个br是要写入数据的二进制的个数,这个我们无法通过strlen(buffer)函数或者sizeof buffer得到,因为strlen得到是字符长度和二进制数据个数是不一样的,而sizeof buffer是个固定长的数据(官方例程的长度是4096),所以这个br的值我们只能通过上面说的 DataCount得到,所以在keil中主函数处理接收到的数据的代码是这样的:

while(1)
	{
		if(Uart2_State==1)
		{
			Uart2_State=0;
			LED0 = !LED0;

			memset(temp_data, '\0', sizeof(temp_data));
			strncpy((char *)temp_data,(char *)data_from_esp8266,strlen("pic_data_start"));
			//temp_data用于判断服务端是否即将发来图片数据
			if (strcmp("pic_data_start",(char *)temp_data)==0)  //服务端即将开始发送图像数据
			{
				write_flag = 1;
				memset((char *)temp_data, '\0', sizeof(temp_data));
				memset((char *)full_path, '\0', sizeof(full_path));
				strcpy((char *)temp_data, (char *)data_from_esp8266+strlen("pic_data_start"));
				strcpy((char *)full_path, "0:/PHOTO/");
				strcat((char *)full_path,(char *)temp_data);
				printf("开始发送图片数据,全路径是:%s\r\n",full_path);

				res=f_open (&des,(char *)full_path, FA_CREATE_ALWAYS|FA_WRITE);
			}
			else if (strcmp("pic_data_end",(char *)data_from_esp8266)==0)
			{
				write_flag = 0;
				//写完后一定要关闭文件
				f_close(&des);
				printf("数据发送结束,开始显示图片\r\n");

				LCD_Clear(BLACK);
				ai_load_picfile(full_path,0,0,lcddev.width,lcddev.height,1);//显示图片    
				Show_Str(2,2,240,16,full_path,16,1); 				//显示图片名字
			}
			else if (write_flag==1 && DataCount!=0)
			{
				printf("接收数据成功,开始写入文件\r\n");
				f_write(&des, (char *)data_from_esp8266, DataCount, &bww);
				DataCount = 0;
			}
			else	//发来的是未知数据
			{
				printf("传入的没有意义的数据,传入的数据是:%s\r\n",(char *)data_from_esp8266);
				printf("数据的长度是:%d\r\n",strlen((char *)data_from_esp8266));
				printf("----------------------------------------------------");
			}
			
			DataCount=0;
			memset((char *)data_from_esp8266, '\0', sizeof(data_from_esp8266));
		}

		t++; 
		//delay_ms(20);		 			   
		LED0=!LED0;
	} 
	

代码逻辑还是很清楚的,就是服务端先发来一个pic_data_start后面跟文件名(例如pic_data_start测试图片.jpg),说明服务端即将发来图像数据,MCU接收到该指令后就通过操作FATFS新建一个图像文件(名为pic_data_start后面跟的文件名),接着服务端循环不断的发来数据,MCU一直接收再写入SD卡的图片文件,直到服务端发来pic_data_end,说明此时图片已经发送完成了,然后MCU通过f_close(&des);关闭打开的文件,然后通过正点原子封装的函数显示SD卡里面的JPG图像

、在给STM32F407插上SD卡之前,先在SD卡的根目录下创建PHOTO文件夹,以存放从服务端传来的图片,同时把正点原子 探索者F4 资料盘(A盘)\5,SD卡根目录文件里面的SYSTEM放到SD卡中,用来加载字库
在这里插入图片描述

3.Socket服务端和ESP8266配置

ESP8266需要通过TCP与服务端连接,服务端选择用Java/Python编写(推荐Python)
Java版代码:

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class UploadPicSTM32 {

	public static void main(String[] args) throws Exception {
		// TODO Auto-generated method stub
		InetAddress addr = InetAddress.getLocalHost();
		  System.out.println("Local HostAddress: "+addr.getHostAddress());
		  String hostname = addr.getHostName();
		  System.out.println("Local host name: "+hostname);
		ServerSocket serverSocket = new ServerSocket(8080);
		
		int count = 0;//记录客户端数量
		while (true) {
			final Socket new_socket = serverSocket.accept();//侦听并接受到此套接字的连接
			new Thread(new MyServerThread(new_socket)).start();
			System.out.println("客户端数量:" + (++count));//打印客户端数量
		}
	}

}


class MyServerThread implements Runnable{
	private Socket socket;
	
	public MyServerThread(Socket socket) {
		this.socket = socket;
	}
	
	public void run(){
		String ip = socket.getInetAddress().getHostAddress();
		
		try {
			InputStream in = socket.getInputStream();
			OutputStream os = socket.getOutputStream(); //获取服务端输出流
			
			File file = new File("upload_pics/des.jpg");	//目标图片的地址

			System.out.println("文件"+file.getName()+"的大小是:"+file.length()/1024+"KB");
			
			InputStream is = new FileInputStream(file);
            int len = 0;
            byte[] bytes = new byte[1024 * 10];
            
            os.write(("pic_data_start"+"des.jpg").getBytes());
            while ((len = is.read(bytes)) != -1) {
            	//os.write(("pic_data_count"+len).getBytes());
            	Thread.sleep(1300);
            	os.write(bytes, 0, len);
                System.out.println("输出的长度是:" + len);
            }
            Thread.sleep(1300);
            os.write("pic_data_end".getBytes());
            
            is.close();
            
            System.out.println("---------------图片已经发生完成了!!!------------------------------------------");
            //os.flush();
            //socket.close();
            //socket.shutdownInput();
            //socket.shutdownOutput();
			
			
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}
}

不过我个人更推荐用Python的Socket服务端代码,因为Python自带的有TCP多线程socketserver类,可以在客户端断开后自动进入该类的finish函数,方便做写连接断开后的处理,Java的多线程Socket就需要自己判断连接是否断开了,比较麻烦

Python版代码:(还没有整理好,等回头整理好了再贴出来)

ESP8266在连上WiFi后可以设置为上电自动连接TCP并开启透传,免去要在STM32代码里面初始化,而且在STM32里面初始化ESP8266是相当不稳定:
AT+SAVETRANSLINK=1,“192.168.1.106”,8080,"TCP"
把上面的IP和端口换成你电脑的内网IP地址和端口这个我在Java代码中已打印到控制台了
如果想要退出上电自动连接,输入下面指令就行了:
AT+SAVETRANSLINK=0
(不过这些指令官方手册里面也没给,我还是从网上搜到的,大坑、、、、、、)

三、Keil工程链接

下载链接免积分,大家可以看一下,有什么问题可以在评论区一起交流
免积分keil工程下载链接

  • 18
    点赞
  • 119
    收藏
    觉得还不错? 一键收藏
  • 11
    评论
STM32是一种嵌入式开发平台,广泛用于控制和通信领域。在STM32中实现视频传输需要考虑多个因素,其中帧率是一个重要的指标。 帧率是指视频中每秒显示的帧数。在STM32中实现视频传输涉及到两个主要方面,即视频信号读取和传输。 首先,视频信号读取是指从外部设备(如摄像头)读取视频信号的过程。对于大多数STM32开发板,其GPIO接口和外设模块(例如DCMI 接口)可以用于接收视频信号。帧率的测量精度依赖于所选择的外设和控制算法,这些算法包括中断请求(DMA)和直接存储器访问(DMA)。 然后,视频信号传输是指将从摄像头读取到的视频信号传输到显示设备(如LCD屏幕)或存储设备(如SD卡)的过程。帧率受到传输速度和存储设备的处理能力的限制。如果传输速度不够快,可能会导致帧率下降。 为了实现更高的帧率,可以采取一些优化措施。首先,选择合适的外设模块和传输协议(如SPI或I2C)来提高传输速度。其次,使用高性能的图像处理算法,如硬件加速或并行处理,可以减少处理时间并提高帧率。最后,正确配置和优化STM32的时钟树和中断设置,以提高整个系统的性能和响应能力。 总之,STM32视频传输的帧率取决于多个因素,包括视频信号读取和传输的能力,以及外设和算法的选择和优化。通过仔细设计和优化,可以实现更高的帧率,并满足特定应用的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值