文章目录
- 前言
- 一、实现效果
- 二、过程详解
- 1.串口帧中断
- 2.FATFS文件管理系统
- 3.Socket服务端和ESP8266配置
- 三、Keil工程链接
前言
本次实验基于正点原子的探索者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工程下载链接