更新。点击上方"蓝字"关注我们
01、思路
>>>STM32F4OTA_ESP8266_Bootloader为引导程序
远程更新的代码(APP):远程更新的APP
Ymoden_server:为运行在Linux的TCP服务器
备注:STM32 OTA远程更新需要连接热点
电脑与ubuntuIP同一个网段
热点名称:WSDSB
热点密码:TLYU.4936
第一步:
设置远程更新的代码(APP)中的ROM为第五扇区,编译生成的.bin文件放置在ymodem_server中。
IROM1 0x08020000 0x20000
起始地址 大小
第二步:
设置FLASH为默认值,修STM32F4OTA_ESP8266_Bootloader中的WIFI热点名称与密码,修服务器IP地址。
IROM1 0x08000000 0x80000
起始地址 大小
第三步:烧录STM32F4OTA_ESP8266_Bootloader,并重启,按下S3按键,启动服务器代码传输。
02、ymodem_server
>>>需要源码的私聊,后续所有内容发布完,在统一整理资源分享。
// ymodem_server.c
/****************************************************************
*名 称:基于TCP服务器通过ymodem协议实现固件更新/文件传输
*作 者:Qt历险记
*创建日期:2021/05/28
*知 识 点:
1.TCP服务器
2.ymodem协议
3.本地文件访问与传输
*****************************************************************/
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
//ymodem用到的信号
#define SOH 0x01
#define STX 0x02
#define ACK 0x06
#define NACK 0x15
#define EOT 0x04
#define CCC 0x43
//状态机
#define YMODEM_STA_IDLE 0
#define YMODEM_STA_START 1
#define YMODEM_STA_DATA 2
#define YMODEM_STA_EOT1 3
#define YMODEM_STA_EOT2 4
#define YMODEM_STA_END 5
uint16_t crc16(uint8_t *addr, int32_t num, uint16_t crc);
unsigned long file_size_get(const char *pfile_path);
int main (int argc,char **argv)
{
int fd_socket;
int fd_client;
int rt;
int len;
int crc;
int m=0,n=0;
char buf_rx[133];
char buf_tx[133];
unsigned char tx_seq=0;
char file_name[128]={0};
int file_size=0;
int buf_tx_offset=0;
int ymodem_sta = YMODEM_STA_IDLE;
/* 检查输入的参数 */
if(argv[1] ==NULL)
{
printf("please input format:./ymodem filename\r\n");
return -1;
}
printf("This is ymodem server by Teacher.Wen\r\n");
/* 获取文件名 */
strcpy(file_name,argv[1]);
/* 创建套接字,协议为IPv4,类型为TCP */
fd_socket = socket(AF_INET,SOCK_STREAM,0);
if(fd_socket<0)
{
perror("create socket fail:");
return -1;
}
//30秒超时
struct timeval timeout={30,0};
//设置发送超时
rt = setsockopt(fd_socket,SOL_SOCKET,SO_SNDTIMEO,(const char *)&timeout,sizeof timeout);
if(rt < 0)
{
perror("setsockopt SO_RCVTIMEO:");
return -1;
}
//设置接收超时
rt = setsockopt(fd_socket,SOL_SOCKET,SO_RCVTIMEO,(const char *)&timeout,sizeof timeout);
if(rt < 0)
{
perror("setsockopt SO_RCVTIMEO:");
return -1;
}
int on =1;
/* 设置socket允许重复使用地址与端口 */
setsockopt(fd_socket,SOL_SOCKET,SO_REUSEADDR,&on,sizeof on);
struct sockaddr_in local_addr;
local_addr.sin_family = AF_INET; //IPv4
local_addr.sin_port = htons(8888); //本机端口为8888
local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
rt=bind(fd_socket,(struct sockaddr *)&local_addr,sizeof local_addr);
if(rt < 0)
{
perror("bind");
return -1;
}
/* 设置连接数目为1个 */
rt = listen(fd_socket,5);
if(rt < 0)
{
perror("listen fail:");
return -1;
}
struct sockaddr_in dest_addr;
socklen_t dest_addr_len =sizeof dest_addr;
printf("waiting tcp client...\r\n");
/* 等待客户端连接 */
fd_client = accept(fd_socket,(struct sockaddr *)&dest_addr, &dest_addr_len);
if(fd_client < 0)
{
perror("accept fail:");
return -1;
}
char *p = inet_ntoa(dest_addr.sin_addr);
printf("client ip:%s,port%d\n",p,ntohs(dest_addr.sin_port));
while(1)
{
switch(ymodem_sta)
{
case YMODEM_STA_IDLE:
{
printf("[YMODEM_STA_IDLE]\r\n");
bzero(buf_rx,sizeof buf_rx);
len = recv(fd_client, buf_rx, sizeof buf_rx, 0);
if(len <= 0)
{
perror("[YMODEM_STA_IDLE]recv");
printf("[YMODEM_STA_IDLE]please try again\r\n");
break;
}
if(buf_rx[0]=='C')
{
ymodem_sta = YMODEM_STA_START;
}
}break;
case YMODEM_STA_START:
{
printf("[YMODEM_STA_START]\r\n");
memset(buf_tx,0,sizeof buf_tx);
tx_seq=0;
buf_tx_offset=0;
file_size=0;
buf_tx[buf_tx_offset++]=(unsigned char )SOH; //符号
buf_tx[buf_tx_offset++]=(unsigned char )tx_seq; //序号
buf_tx[buf_tx_offset++]=(unsigned char )~tx_seq; //序号反码
/* 文件名 */
memcpy(&buf_tx[buf_tx_offset],file_name,strlen(file_name));
buf_tx_offset+=strlen(file_name);
/* 文件名与文件大小要相隔一个空字符 */
buf_tx_offset++;
/* 文件大小 */
file_size = file_size_get(file_name);
buf_tx[buf_tx_offset++]=(unsigned char)((file_size>>24)&0xFF);
buf_tx[buf_tx_offset++]=(unsigned char)((file_size>>16)&0xFF);
buf_tx[buf_tx_offset++]=(unsigned char)((file_size>>8)&0xFF);
buf_tx[buf_tx_offset++]=(unsigned char)((file_size>>0)&0xFF);
/* 计算crc16 */
crc=0;
crc=crc16(&buf_tx[3],128,0);
buf_tx[131]=(unsigned char)((crc>>8)&0xFF);
buf_tx[132]=(unsigned char)((crc>>0)&0xFF);
/* 发送 */
len = send(fd_client,buf_tx,133,0);
if(len <= 0)
{
perror("[YMODEM_STA_START]send");
break;
}
/* 等待应答 */
memset(buf_rx,0,sizeof buf_rx);
len = recv(fd_client, buf_rx, 1, 0);
if(len <= 0)
{
perror("[YMODEM_STA_START]recv ack");
break;
}
if(buf_rx[0] != ACK)
{
printf("[YMODEM_STA_START]please try again\r\n");
ymodem_sta = YMODEM_STA_IDLE;
break;
}
/* 等待大写字母C */
memset(buf_rx,0,sizeof buf_rx);
len = recv(fd_client, buf_rx, 1, 0);
if(len <= 0)
{
perror("[YMODEM_STA_START]recv ack");
break;
}
if(buf_rx[0] != CCC)
{
printf("[YMODEM_STA_START]please try again\r\n");
ymodem_sta = YMODEM_STA_IDLE;
break;
}
ymodem_sta = YMODEM_STA_DATA;
}break;
case YMODEM_STA_DATA:
{
printf("YMODEM_STA_DATA\r\n");
int fd = open(file_name,O_RDONLY);
if(fd < 0)
{
perror("[YMODEM_STA_DATA]open");
break;
}
while(1)
{
memset(buf_tx,0,sizeof buf_tx);
len = read(fd,&buf_tx[3],128);//读取128字节数据
if(len == 0)
{
ymodem_sta = YMODEM_STA_EOT1;
break;
}
buf_tx[0]=(unsigned char )SOH; //符号
tx_seq++;
buf_tx[1]=(unsigned char )tx_seq; //序号
buf_tx[2]=(unsigned char )~tx_seq;//序号反码
//printf("YMODEM_STA_DATA read() len is %d\r\n",len);
/* 计算crc16 */
crc=0;
crc=crc16(&buf_tx[3],128,0);
buf_tx[131]=(unsigned char)((crc>>8)&0xFF);
buf_tx[132]=(unsigned char)((crc>>0)&0xFF);
/* 发送 */
len = send(fd_client,buf_tx,133,0);
if(len <= 0)
{
perror("[YMODEM_STA_DATA]send");
break;
}
//printf("YMODEM_STA_DATA tx_seq=%d\r\n",tx_seq);
/* 等待应答 */
memset(buf_rx,0,sizeof buf_rx);
len = recv(fd_client, buf_rx, 1, 0);
if(len <= 0)
{
perror("[YMODEM_STA_DATA]recv ack");
break;
}
if(buf_rx[0] != ACK)
{
printf("[YMODEM_STA_DATA]please try again\r\n");
ymodem_sta = YMODEM_STA_IDLE;
break;
}
printf(".");
}
close(fd);
printf("\r\n");
}break;
case YMODEM_STA_EOT1:
{
printf("[YMODEM_STA_EOT1]\r\n");
buf_tx[0]=EOT;
/* 发送 */
len = send(fd_client,buf_tx,1,0);
if(len <= 0)
{
perror("[YMODEM_STA_EOT1]send");
break;
}
/* 等待应答 */
memset(buf_rx,0,sizeof buf_rx);
len = recv(fd_client, buf_rx, 1, 0);
if(len <= 0)
{
perror("[YMODEM_STA_EOT1]recv nak");
break;
}
/* 等待NAK */
if(buf_rx[0] != NACK)
{
printf("[YMODEM_STA_EOT1]please try again\r\n");
ymodem_sta = YMODEM_STA_IDLE;
break;
}
ymodem_sta = YMODEM_STA_EOT2;
}break;
case YMODEM_STA_EOT2:
{
printf("[YMODEM_STA_EOT2]\r\n");
buf_tx[0]=EOT;
/* 发送 */
len = send(fd_client,buf_tx,1,0);
if(len <= 0)
{
perror("[YMODEM_STA_EOT2]send");
break;
}
/* 等待应答 */
memset(buf_rx,0,sizeof buf_rx);
len = recv(fd_client, buf_rx, 1, 0);
if(len <= 0)
{
perror("[YMODEM_STA_EOT2]recv ack");
break;
}
if(buf_rx[0] != ACK)
{
printf("[YMODEM_STA_EOT2]please try again\r\n");
ymodem_sta = YMODEM_STA_IDLE;
break;
}
/* 等待大写字母C */
memset(buf_rx,0,sizeof buf_rx);
len = recv(fd_client, buf_rx, 1, 0);
if(len <= 0)
{
perror("[YMODEM_STA_EOT2]recv ack");
break;
}
if(buf_rx[0] != CCC)
{
printf("[YMODEM_STA_EOT2]please try again\r\n");
ymodem_sta = YMODEM_STA_IDLE;
break;
}
ymodem_sta = YMODEM_STA_END;
}break;
case YMODEM_STA_END:
{
printf("[YMODEM_STA_END]\r\n");
buf_tx[0]=SOH;
buf_tx[1]=0x00;
buf_tx[2]=0xFF;
memset(&buf_tx[3],0,128);
/* 计算crc16 */
crc=0;
crc=crc16(&buf_tx[3],128,0);
buf_tx[131]=(unsigned char)((crc>>8)&0xFF);
buf_tx[132]=(unsigned char)((crc>>0)&0xFF);
/* 发送 */
len = send(fd_client,buf_tx,133,0);
if(len <= 0)
{
perror("[YMODEM_STA_END]send");
break;
}
/* 等待应答 */
memset(buf_rx,0,sizeof buf_rx);
len = recv(fd_client, buf_rx, 1, 0);
if(len <= 0)
{
perror("[YMODEM_STA_END]recv ack");
break;
}
if(buf_rx[0] != ACK)
{
printf("[YMODEM_STA_END]please try again\r\n");
ymodem_sta = YMODEM_STA_IDLE;
break;
}
printf("ymodem download success\r\n");
close(fd_socket);
exit(0);
}break;
default:break;
}
}
return 0;
}
#define POLY 0x1021
uint16_t crc16(uint8_t *addr, int32_t num, uint16_t crc)
{
int32_t i;
for (; num > 0; num--) /* Step through bytes in memory */
{
crc = crc ^ (*addr++ << 8); /* Fetch byte from memory, XOR into CRC top byte*/
for (i = 0; i < 8; i++) /* Prepare to rotate 8 bits */
{
if (crc & 0x8000) /* b15 is set... */
crc = (crc << 1) ^ POLY; /* rotate and XOR with polynomic */
else /* b15 is clear... */
crc <<= 1; /* just rotate */
} /* Loop for 8 bits */
crc &= 0xFFFF; /* Ensure CRC remains 16-bit value */
} /* Loop until num=0 */
return(crc); /* Return updated CRC */
}
/****************************************************
*函数名称:file_size_get
*输入参数:pfile_path -文件路径
*返 回 值:-1 -失败
其他值 -文件大小
*说 明:获取文件大小
****************************************************/
unsigned long file_size_get(const char *pfile_path)
{
unsigned long filesize = -1;
struct stat statbuff;
if(stat(pfile_path, &statbuff) < 0)
{
return filesize;
}
else
{
filesize = statbuff.st_size;
}
return filesize;
}
03、STM32F4OTA_ESP8266_Bootloader
>>>知 识 点:
1.esp8266 wifi的AT指令的使用
2.ymodem协议
3.内部FLASH的读、写、擦除
4.系统复位、代码区跳转技巧
*说 明:
1.复位时检测到按键1持续按下,表示使用COM1接口,使用SecureCRT软件进行固件更新
2.复位时检测到按键3持续按下,表示使用COM3接口,使用ESP8266无线WiFi实现OTA固件更新
// mainc.c
/****************************************************************
*名 称:基于ymodem协议实现stm32f4的固件本地/OTA更新
*作 者:Qt历险记
*创建日期:2021/05/28
*知 识 点:
1.esp8266 wifi的AT指令的使用
2.ymodem协议
3.内部FLASH的读、写、擦除
4.系统复位、代码区跳转技巧
*说 明:
1.复位时检测到按键1持续按下,表示使用COM1接口,使用SecureCRT软件进行固件更新
2.复位时检测到按键3持续按下,表示使用COM3接口,使用ESP8266无线WiFi实现OTA固件更新
*****************************************************************/
#include "stm32f4xx.h"
#include "sys.h"
#include "usart.h"
#include "delay.h"
#include "led.h"
#include "beep.h"
#include "flash.h"
#include "key.h"
#include "esp8266.h"
#include "tim.h"
#include "ymodem.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/**
* @bieaf 进行BootLoader的启动
*
* @param none
* @return none
*/
void bootloader_start(void)
{
/*==========打印消息==========*/
switch(read_start_mode()) //读取是否启动应用程序
{
case STARTUP_NORMAL: //正常启动
{
printf("> normal start......\r\n");
break;
}
case STARTUP_UPDATE: //升级再启动
{
printf("> start update......\r\n");
move_code(APPLICATION_1_ADDR,APPLICATION_2_ADDR,APPLICATION_2_SIZE);
printf("> update success......\r\n");
break;
}
case STARTUP_RESET: //恢复出厂设置 目前没使用
{
printf("> restore to factory program......\r\n");
break;
}
default: //启动失败
{
printf("> error:%X!!!......\r\n", read_start_mode());
return;
}
}
/* 跳转到应用程序 */
printf("> start up......\r\n\r\n");
iap_execute_app(APPLICATION_1_ADDR);
}
//主函数
int main(void)
{
//led初始化
led_init();
//beep初始化
beep_init();
//按键检测
key_init();
//定时器3初始化
tim3_init();
//串口1初始化波特率为115200bps
usart1_init(115200);
//串口延迟一会,确保芯片内部完成全部初始化,printf无乱码输出
delay_ms(1000);
printf("This is bootloader test by teacher.chen\r\n");
// sector_erase(0x08020000);
// delay_ms(200);
// sector_erase(0x08040000);
// delay_ms(200);
//启动后,没有任何按键按下,则调用bootloader_start函数
if(key_sta_get() == 0)
bootloader_start();
if(key_sta_get()&0x01)
{
g_ymodem_com=1;
printf("now start ymodem download from com1\r\n");
}
if(key_sta_get()&0x04)
{
printf("now start ymodem download from com3(esp8266)\r\n");
//esp8266初始化
esp8266_init();
g_ymodem_com=3;
}
while(1)
{
if(g_ymodem_com == 1)
{
ymodem_download_from_com1();
}
if(g_ymodem_com == 3)
{
ymodem_download_from_com3();
}
}
return 0;
}
04、esp8266.h
#ifndef __ESP8266_H__
#define __ESP8266_H__
#define EN_DEBUG_ESP8266 0
//添加WIFI热点宏定义,此处根据自己的wifi作调整
#define WIFI_SSID "WSDSB"
#define WIFI_PASSWORD "TLYU.4936"
//#define WIFI_SSID "zc-room-10"
//#define WIFI_PASSWORD "88888888"
#define YMODEM_SERVER "192.168.3.8"
extern volatile uint8_t g_esp8266_rx_buf[512];
extern volatile uint32_t g_esp8266_rx_cnt;
extern volatile uint32_t g_esp8266_rx_end;
extern volatile uint32_t g_esp8266_transparent_transmission_sta;
extern int32_t esp8266_init(void);
extern int32_t esp8266_self_test(void);
extern int32_t esp8266_exit_transparent_transmission (void);
extern int32_t esp8266_entry_transparent_transmission(void);
extern int32_t esp8266_connect_ap(char* ssid,char* pswd);
extern int32_t esp8266_connect_server(char* mode,char* ip,uint16_t port);
extern int32_t esp8266_disconnect_server(void);
extern void esp8266_send_bytes(uint8_t *buf,uint32_t len);
extern void esp8266_send_str(char *buf);
extern void esp8266_send_at(char *str);
extern int32_t esp8266_enable_multiple_id(uint32_t b);
extern int32_t esp8266_create_server(uint16_t port);
extern int32_t esp8266_close_server(uint16_t port);
extern int32_t esp8266_enable_echo(uint32_t b);
extern int32_t esp8266_reset(void);
#endif
05、esp8266.c
#include "stm32f4xx.h"
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "esp8266.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
volatile uint8_t g_esp8266_rx_buf[512];
volatile uint32_t g_esp8266_rx_cnt=0;
volatile uint32_t g_esp8266_rx_end=0;
volatile uint32_t g_esp8266_transparent_transmission_sta=0;
int32_t esp8266_init(void)
{
int32_t rt;
usart3_init(115200);
//退出透传模式,才能输入AT指令
rt=esp8266_exit_transparent_transmission();
if(rt)
{
printf("esp8266_exit_transparent_transmission fail\r\n");
return -1;
}
printf("esp8266_exit_transparent_transmission success\r\n");
delay_ms(2000);
//复位模块
rt=esp8266_reset();
if(rt)
{
printf("esp8266_reset fail\r\n");
return -2;
}
printf("esp8266_reset success\r\n");
delay_ms(2000);
//关闭回显
rt=esp8266_enable_echo(0);
if(rt)
{
printf("esp8266_enable_echo(0) fail\r\n");
return -3;
}
printf("esp8266_enable_echo(0)success\r\n");
delay_ms(2000);
//连接热点
rt = esp8266_connect_ap(WIFI_SSID,WIFI_PASSWORD);
if(rt)
{
printf("esp8266_connect_ap fail\r\n");
return -4;
}
printf("esp8266_connect_ap success\r\n");
delay_ms(2000);
rt =esp8266_connect_server("TCP",YMODEM_SERVER,8888);
if(rt)
{
printf("esp8266_connect_server fail\r\n");
return -5;
}
printf("esp8266_connect_server success\r\n");
delay_ms(2000);
//进入透传模式
rt =esp8266_entry_transparent_transmission();
if(rt)
{
printf("esp8266_entry_transparent_transmission fail\r\n");
return -6;
}
printf("esp8266_entry_transparent_transmission success\r\n");
delay_ms(2000);
return 0;
}
void esp8266_send_at(char *str)
{
//清空接收缓冲区
memset((void *)g_esp8266_rx_buf,0, sizeof g_esp8266_rx_buf);
//清空接收计数值
g_esp8266_rx_cnt = 0;
//串口3发送数据
usart3_send_str(str);
}
void esp8266_send_bytes(uint8_t *buf,uint32_t len)
{
usart3_send_bytes(buf,len);
}
void esp8266_send_str(char *buf)
{
usart3_send_str(buf);
}
/* 查找接收数据包中的字符串 */
int32_t esp8266_find_str_in_rx_packet(char *str,uint32_t timeout)
{
char *dest = str;
char *src = (char *)&g_esp8266_rx_buf;
//等待串口接收完毕或超时退出
while((strstr(src,dest)==NULL) && timeout)
{
delay_ms(1);
timeout--;
}
if(timeout)
return 0;
return -1;
}
/* 自检程序 */
int32_t esp8266_self_test(void)
{
esp8266_send_at("AT\r\n");
return esp8266_find_str_in_rx_packet("OK",1000);
}
/**
* 功能:连接热点
* 参数:
* ssid:热点名
* pwd:热点密码
* 返回值:
* 连接结果,非0连接成功,0连接失败
* 说明:
* 失败的原因有以下几种(UART通信和ESP8266正常情况下)
* 1. WIFI名和密码不正确
* 2. 路由器连接设备太多,未能给ESP8266分配IP
*/
int32_t esp8266_connect_ap(char* ssid,char* pswd)
{
#if 0
//不建议使用以下sprintf,占用过多的栈
char buf[128]={0};
sprintf(buf,"AT+CWJAP_CUR=\"%s\",\"%s\"\r\n",ssid,pswd);
#endif
//设置为STATION模式
esp8266_send_at("AT+CWMODE_CUR=1\r\n");
if(esp8266_find_str_in_rx_packet("OK",1000))
return -1;
//连接目标AP
//sprintf(buf,"AT+CWJAP_CUR=\"%s\",\"%s\"\r\n",ssid,pswd);
esp8266_send_at("AT+CWJAP_CUR=");
esp8266_send_at("\"");esp8266_send_at(ssid);esp8266_send_at("\"");
esp8266_send_at(",");
esp8266_send_at("\"");esp8266_send_at(pswd);esp8266_send_at("\"");
esp8266_send_at("\r\n");
if(esp8266_find_str_in_rx_packet("OK",5000))
if(esp8266_find_str_in_rx_packet("CONNECT",5000))
return -2;
return 0;
}
/* 退出透传模式 */
int32_t esp8266_exit_transparent_transmission (void)
{
esp8266_send_at ("+++");
//退出透传模式,发送下一条AT指令要间隔1秒
delay_ms ( 1000 );
//记录当前esp8266工作在非透传模式
g_esp8266_transparent_transmission_sta = 0;
return 0;
}
/* 进入透传模式 */
int32_t esp8266_entry_transparent_transmission(void)
{
//进入透传模式
esp8266_send_at("AT+CIPMODE=1\r\n");
if(esp8266_find_str_in_rx_packet("OK",5000))
return -1;
delay_ms(2000);
//开启发送状态
esp8266_send_at("AT+CIPSEND\r\n");
if(esp8266_find_str_in_rx_packet("OK",5000))
return -2;
//记录当前esp8266工作在透传模式
g_esp8266_transparent_transmission_sta = 1;
return 0;
}
/**
* 功能:使用指定协议(TCP/UDP)连接到服务器
* 参数:
* mode:协议类型 "TCP","UDP"
* ip:目标服务器IP
* port:目标是服务器端口号
* 返回值:
* 连接结果,非0连接成功,0连接失败
* 说明:
* 失败的原因有以下几种(UART通信和ESP8266正常情况下)
* 1. 远程服务器IP和端口号有误
* 2. 未连接AP
* 3. 服务器端禁止添加(一般不会发生)
*/
int32_t esp8266_connect_server(char* mode,char* ip,uint16_t port)
{
#if 0
//使用MQTT传递的ip地址过长,不建议使用以下方法,否则导致栈溢出
//AT+CIPSTART="TCP","a10tC4OAAPc.iot-as-mqtt.cn-shanghai.aliyuncs.com",1883,该字符串占用内存过多了
char buf[128]={0};
//连接服务器
sprintf((char*)buf,"AT+CIPSTART=\"%s\",\"%s\",%d\r\n",mode,ip,port);
esp8266_send_at(buf);
#else
char buf[16]={0};
esp8266_send_at("AT+CIPSTART=");
esp8266_send_at("\""); esp8266_send_at(mode); esp8266_send_at("\"");
esp8266_send_at(",");
esp8266_send_at("\""); esp8266_send_at(ip); esp8266_send_at("\"");
esp8266_send_at(",");
sprintf(buf,"%d",port);
esp8266_send_at(buf);
esp8266_send_at("\r\n");
#endif
if(esp8266_find_str_in_rx_packet("CONNECT",5000))
if(esp8266_find_str_in_rx_packet("OK",5000))
return -1;
return 0;
}
/* 断开服务器 */
int32_t esp8266_disconnect_server(void)
{
esp8266_send_at("AT+CIPCLOSE\r\n");
if(esp8266_find_str_in_rx_packet("CLOSED",5000))
if(esp8266_find_str_in_rx_packet("OK",5000))
return -1;
return 0;
}
/* 使能多链接 */
int32_t esp8266_enable_multiple_id(uint32_t b)
{
char buf[32]={0};
sprintf(buf,"AT+CIPMUX=%d\r\n", b);
esp8266_send_at(buf);
if(esp8266_find_str_in_rx_packet("OK",5000))
return -1;
return 0;
}
/* 创建服务器 */
int32_t esp8266_create_server(uint16_t port)
{
char buf[32]={0};
sprintf(buf,"AT+CIPSERVER=1,%d\r\n", port);
esp8266_send_at(buf);
if(esp8266_find_str_in_rx_packet("OK",5000))
return -1;
return 0;
}
/* 关闭服务器 */
int32_t esp8266_close_server(uint16_t port)
{
char buf[32]={0};
sprintf(buf,"AT+CIPSERVER=0,%d\r\n", port);
esp8266_send_at(buf);
if(esp8266_find_str_in_rx_packet("OK",5000))
return -1;
return 0;
}
/* 回显打开或关闭 */
int32_t esp8266_enable_echo(uint32_t b)
{
if(b)
esp8266_send_at("ATE1\r\n");
else
esp8266_send_at("ATE0\r\n");
if(esp8266_find_str_in_rx_packet("OK",5000))
return -1;
return 0;
}
/* 复位 */
int32_t esp8266_reset(void)
{
esp8266_send_at("AT+RST\r\n");
if(esp8266_find_str_in_rx_packet("OK",10000))
return -1;
return 0;
}
06、ymodem.h
#ifndef __YMODEM_H__
#define __YMODEM_H__
#define SOH 0x01
#define STX 0x02
#define ACK 0x06
#define NACK 0x15
#define EOT 0x04
#define CCC 0x43
/* 升级的步骤 */
enum UPDATE_STATE
{
TO_START = 0x01,
TO_RECEIVE_DATA = 0x02,
TO_RECEIVE_EOT1 = 0x03,
TO_RECEIVE_EOT2 = 0x04,
TO_RECEIVE_END = 0x05
};
extern uint32_t g_ymodem_com;
extern void ymodem_download_from_com1(void);
extern void ymodem_download_from_com3(void);
#endif
07、ymodem.c
#include "stm32f4xx.h"
#include "sys.h"
#include "usart.h"
#include "delay.h"
#include "led.h"
#include "beep.h"
#include "flash.h"
#include "esp8266.h"
#include "key.h"
#include "ymodem.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
uint32_t g_ymodem_com = 3;
/**
* @bieaf CRC-16 校验
*
* @param addr 开始地址
* @param num 长度
* @param num CRC
* @return crc 返回CRC的值
*/
#define POLY 0x1021
uint16_t crc16(uint8_t *addr, int32_t num, uint16_t crc)
{
int32_t i;
for (; num > 0; num--) /* Step through bytes in memory */
{
crc = crc ^ (*addr++ << 8); /* Fetch byte from memory, XOR into CRC top byte*/
for (i = 0; i < 8; i++) /* Prepare to rotate 8 bits */
{
if (crc & 0x8000) /* b15 is set... */
crc = (crc << 1) ^ POLY; /* rotate and XOR with polynomic */
else /* b15 is clear... */
crc <<= 1; /* just rotate */
} /* Loop for 8 bits */
crc &= 0xFFFF; /* Ensure CRC remains 16-bit value */
} /* Loop until num=0 */
return(crc); /* Return updated CRC */
}
/* 设置升级的步骤 */
static enum UPDATE_STATE update_state = TO_START;
void ymodem_set_state(enum UPDATE_STATE state)
{
update_state = state;
}
/* 查询升级的步骤 */
uint8_t ymodem_get_state(void)
{
return update_state;
}
/* 发送指令 */
void ymodem_send_cmd(uint8_t command)
{
if(g_ymodem_com ==1)
{
USART_SendData(USART1,command);
//等待数据发送成功
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
USART_ClearFlag(USART1,USART_FLAG_TXE);
}
if(g_ymodem_com ==3)
{
USART_SendData(USART3,command);
//等待数据发送成功
while(USART_GetFlagStatus(USART3,USART_FLAG_TXE)==RESET);
USART_ClearFlag(USART3,USART_FLAG_TXE);
}
delay_ms(10);
}
/* 标记升级完成 */
void update_set_down(void)
{
uint32_t update_flag = 0xAAAAAAAA; // 对应bootloader的启动步骤
flash_program((APPLICATION_2_ADDR + APPLICATION_2_SIZE - 4), &update_flag,1 );
}
/**
* @bieaf ymodem下载
*
* @param none
* @return none
*/
void ymodem_download_from_com1(void)
{
uint16_t crc = 0;
static
uint8_t data_state = 0;
if(ymodem_get_state()==TO_START)
{
ymodem_send_cmd(CCC);
delay_ms(1000);
}
/* 串口1接收完一个数据包 */
if(g_usart1_rx_end)
{
/* 清空接收完成标志位、接收计数值 */
g_usart1_rx_end=0;
g_usart1_rx_cnt=0;
switch(g_usart1_rx_buf[0])
{
case SOH://数据包开始
{
crc = 0;
/* 计算crc16 */
crc = crc16((uint8_t *)&g_usart1_rx_buf[3], 128, crc);
if(crc != (g_usart1_rx_buf[131]<<8|g_usart1_rx_buf[132]))
return;
if((ymodem_get_state()==TO_START)&&(g_usart1_rx_buf[1] == 0x00)&&(g_usart1_rx_buf[2] == (uint8_t)(~g_usart1_rx_buf[1])))// 开始
{
ymodem_set_state(TO_RECEIVE_DATA);
/* 若ymodem_send_cmd执行在sector_erase之前,则导致串口数据丢包,因为擦除会关闭所有中断 */
/* 擦除应用程序2的扇区 */
sector_erase(APPLICATION_2_SECTOR);
data_state = 0x01;
ymodem_send_cmd(ACK);
ymodem_send_cmd(CCC);
}
else if((ymodem_get_state()==TO_RECEIVE_END)&&(g_usart1_rx_buf[1] == 0x00)&&(g_usart1_rx_buf[2] == (uint8_t)(~g_usart1_rx_buf[1])))// 结束
{
update_set_down();
ymodem_set_state(TO_START);
ymodem_send_cmd(ACK);
/* 嘀一声示,表示下载完成 */
PFout(9)=0;beep_on();
delay_ms(80);
PFout(9)=1;beep_off();
/* 复位 */
NVIC_SystemReset();
}
else if((ymodem_get_state()==TO_RECEIVE_DATA)&&(g_usart1_rx_buf[1] == data_state)&&(g_usart1_rx_buf[2] == (uint8_t)(~g_usart1_rx_buf[1])))// 接收数据
{
/* 烧录程序 */
flash_program((APPLICATION_2_ADDR + (data_state-1) * 128), (uint32_t *)(&g_usart1_rx_buf[3]), 32);
data_state++;
ymodem_send_cmd(ACK);
}
}break;
case EOT://数据包传输结束
{
if(ymodem_get_state()==TO_RECEIVE_DATA)
{
ymodem_set_state(TO_RECEIVE_EOT2);
ymodem_send_cmd(NACK);
}
else if(ymodem_get_state()==TO_RECEIVE_EOT2)
{
ymodem_set_state(TO_RECEIVE_END);
ymodem_send_cmd(ACK);
ymodem_send_cmd(CCC);
}
}break;
default:break;
}
}
}
void ymodem_download_from_com3(void)
{
uint16_t crc = 0;
static
uint8_t data_state = 0;
if(ymodem_get_state()==TO_START)
{
ymodem_send_cmd(CCC);
delay_ms(1000);
}
/* 串口3接收完一个数据包 */
if(g_esp8266_rx_end)
{
/* 清空接收完成标志位、接收计数值 */
g_esp8266_rx_end=0;
g_esp8266_rx_cnt=0;
switch(g_esp8266_rx_buf[0])
{
case SOH://数据包开始
{
crc = 0;
/* 计算crc16 */
crc = crc16((uint8_t *)&g_esp8266_rx_buf[3], 128, crc);
if(crc != (g_esp8266_rx_buf[131]<<8|g_esp8266_rx_buf[132]))
return;
if((ymodem_get_state()==TO_START)&&(g_esp8266_rx_buf[1] == 0x00)&&(g_esp8266_rx_buf[2] == (uint8_t)(~g_esp8266_rx_buf[1])))// 开始
{
ymodem_set_state(TO_RECEIVE_DATA);
/* 若ymodem_send_cmd执行在sector_erase之前,则导致串口数据丢包,因为擦除会关闭所有中断 */
/* 擦除应用程序2的扇区 */
sector_erase(APPLICATION_2_SECTOR);
data_state = 0x01;
ymodem_send_cmd(ACK);
ymodem_send_cmd(CCC);
}
else if((ymodem_get_state()==TO_RECEIVE_END)&&(g_esp8266_rx_buf[1] == 0x00)&&(g_esp8266_rx_buf[2] == (uint8_t)(~g_esp8266_rx_buf[1])))// 结束
{
update_set_down();
ymodem_set_state(TO_START);
ymodem_send_cmd(ACK);
/* 嘀一声示,表示下载完成 */
PFout(9)=0;beep_on();
delay_ms(80);
PFout(9)=1;beep_off();
/* 复位 */
NVIC_SystemReset();
}
else if((ymodem_get_state()==TO_RECEIVE_DATA)&&(g_esp8266_rx_buf[1] == data_state)&&(g_esp8266_rx_buf[2] == (uint8_t)(~g_esp8266_rx_buf[1])))// 接收数据
{
/* 烧录程序 */
flash_program((APPLICATION_2_ADDR + (data_state-1) * 128), (uint32_t *)(&g_esp8266_rx_buf[3]), 32);
data_state++;
ymodem_send_cmd(ACK);
}
}break;
case EOT://数据包传输结束
{
if(ymodem_get_state()==TO_RECEIVE_DATA)
{
ymodem_set_state(TO_RECEIVE_EOT2);
ymodem_send_cmd(NACK);
}
else if(ymodem_get_state()==TO_RECEIVE_EOT2)
{
ymodem_set_state(TO_RECEIVE_END);
ymodem_send_cmd(ACK);
ymodem_send_cmd(CCC);
}
}break;
default:break;
}
}
}
08、flash.c
#include "stm32f4xx.h"
#include "flash.h"
#include <stdio.h>
/**
* @bieaf 扇区擦除
*
* @param sector_num 扇区号
* @return 1
*/
int32_t sector_erase(uint32_t sector_num)
{
int32_t rt=-1;
/* 解锁FLASH*/
FLASH_Unlock();
if(FLASH_COMPLETE==FLASH_EraseSector(sector_num,VoltageRange_3))
rt=0;
/* 锁定FLASH*/
FLASH_Lock();
return rt;
}
/**
* @bieaf 写若干个数据
*
* @param addr 写入的地址
* @param buf 写入数据的起始地址
* @param word_size 长度
* @return
*/
void flash_program(uint32_t addr, uint32_t * buf, uint32_t word_size)
{
uint32_t i;
/* 解锁FLASH*/
FLASH_Unlock();
for(i = 0; i < word_size; i++)
{
/* 对FLASH烧写*/
FLASH_ProgramWord( addr + 4 * i, buf[i]);
}
/* 锁定FLASH*/
FLASH_Lock();
}
/**
* @bieaf 读若干个数据
*
* @param addr 读数据的地址
* @param buf 读出数据的数组指针
* @param word_size 长度
* @return
*/
void flash_read(uint32_t addr, uint32_t * buf, uint32_t word_size)
{
uint32_t i=0;
for(i =0; i < word_size; i++)
buf[i] = *(__IO uint32_t*)(addr + 4 * i);
}
/* 读取启动模式 */
uint32_t read_start_mode(void)
{
uint32_t mode = 0;
flash_read((APPLICATION_2_ADDR + APPLICATION_2_SIZE - 4), &mode, 1);
return mode;
}
/**
* @bieaf 进行程序的覆盖
* @detail 1.擦除目的地址
* 2.源地址的代码拷贝到目的地址
* 3.擦除源地址
*
* @param 搬运的源地址
* @param 搬运的目的地址
* @return 搬运的程序大小
*/
void move_code(uint32_t dest_addr, uint32_t src_addr,uint32_t word_size)
{
uint32_t temp[256];
uint32_t i;
/*1.擦除目的地址*/
printf("> start erase application 1 sector......\r\n");
//擦除
sector_erase(APPLICATION_1_SECTOR);
printf("> erase application 1 success......\r\n");
/*2.开始拷贝*/
printf("> start copy......\r\n");
for(i = 0; i <word_size/1024; i++)
{
flash_read((src_addr + i*1024), temp, 256);
flash_program((dest_addr + i*1024), temp, 256);
}
printf("> copy finish......\r\n");
/*3.擦除源地址*/
printf("> start erase application 2 sector......\r\n");
//擦除
sector_erase(APPLICATION_2_SECTOR);
printf("> erase application 2 success......\r\n");
}
/* 采用汇编设置栈的值 */
__asm void MSR_MSP (uint32_t ulAddr)
{
MSR MSP, r0 //set Main Stack value
BX r14
}
void iap_execute_app (uint32_t app_addr)
{
jump_func jump_to_app;
//printf("* ( __IO uint32_t * ) app_addr =%08X ,app_addr=%08X\r\n",* ( __IO uint32_t * ) app_addr,app_addr );
//if ( ( ( * ( __IO uint32_t * ) app_addr ) & 0x2FFE0000 ) == 0x200006B0 ) //检查栈顶地址是否合法.
//{
//printf("stack is legal\r\n");
jump_to_app = (jump_func) * ( __IO uint32_t *)(app_addr + 4); //用户代码区第二个字为程序开始地址(复位地址)
MSR_MSP( * ( __IO uint32_t * ) app_addr ); //初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址)
jump_to_app(); //跳转到APP.
//}
//printf("stack is illegal\r\n");
}
09、usart.c
#include "stm32f4xx.h"
#include "sys.h"
#include "usart.h"
#include "esp8266.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
static USART_InitTypeDef USART_InitStructure;
static GPIO_InitTypeDef GPIO_InitStructure;
static NVIC_InitTypeDef NVIC_InitStructure;
volatile uint8_t g_usart1_rx_buf[1280];
volatile uint32_t g_usart1_rx_cnt=0;
volatile uint32_t g_usart1_rx_end=0;
#pragma import(__use_no_semihosting_swi)
struct __FILE { int handle; /* Add whatever you need here */ };
FILE __stdout;
FILE __stdin;
int fputc(int ch, FILE *f)
{
USART_SendData(USART1,ch);
//等待数据发送成功
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
USART_ClearFlag(USART1,USART_FLAG_TXE);
return ch;
}
void _sys_exit(int return_code) {
}
void usart1_init(uint32_t baud)
{
//使能端口A硬件时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);
//使能串口1硬件时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
//配置PA9、PA10为复用功能引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9|GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Speed = GPIO_High_Speed;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//将PA9、PA10连接到USART1的硬件
GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);
//配置USART1的相关参数:波特率、数据位、校验位
USART_InitStructure.USART_BaudRate = baud;//波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//8位数据位
USART_InitStructure.USART_StopBits = USART_StopBits_1;//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(USART1, &USART_InitStructure);
//使能串口接收到数据触发中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
//使能串口1工作
USART_Cmd(USART1,ENABLE);
}
void usart3_init(uint32_t baud)
{
//使能端口B硬件时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);
//使能串口3硬件时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3,ENABLE);
//配置PB10、PB11为复用功能引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Speed = GPIO_High_Speed;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;
GPIO_Init(GPIOB,&GPIO_InitStructure);
//将PB10、PB11连接到USART3的硬件
GPIO_PinAFConfig(GPIOB, GPIO_PinSource10, GPIO_AF_USART3);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource11, GPIO_AF_USART3);
//配置USART1的相关参数:波特率、数据位、校验位
USART_InitStructure.USART_BaudRate = baud;//波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//8位数据位
USART_InitStructure.USART_StopBits = USART_StopBits_1;//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(USART3, &USART_InitStructure);
//使能串口接收到数据触发中断
USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
//使能串口3工作
USART_Cmd(USART3,ENABLE);
}
void usart3_send_str(char *str)
{
char *p = str;
while(*p!='\0')
{
USART_SendData(USART3,*p);
p++;
//等待数据发送成功
while(USART_GetFlagStatus(USART3,USART_FLAG_TXE)==RESET);
USART_ClearFlag(USART3,USART_FLAG_TXE);
}
}
void usart1_send_bytes(uint8_t *buf,uint32_t len)
{
uint8_t *p = buf;
while(len--)
{
USART_SendData(USART1,*p);
p++;
//等待数据发送成功
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
USART_ClearFlag(USART1,USART_FLAG_TXE);
}
}
void usart3_send_bytes(uint8_t *buf,uint32_t len)
{
uint8_t *p = buf;
while(len--)
{
USART_SendData(USART3,*p);
p++;
//等待数据发送成功
while(USART_GetFlagStatus(USART3,USART_FLAG_TXE)==RESET);
USART_ClearFlag(USART3,USART_FLAG_TXE);
}
}
void USART1_IRQHandler(void)
{
uint8_t d=0;
//检测是否接收到数据
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
{
d=USART_ReceiveData(USART1);
g_usart1_rx_buf[g_usart1_rx_cnt++]=d;
if(g_usart1_rx_cnt >= sizeof g_usart1_rx_buf)
{
g_usart1_rx_end=1;
}
//清空标志位,可以响应新的中断请求
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
void USART3_IRQHandler(void)
{
uint8_t d=0;
//检测是否接收到数据
if (USART_GetITStatus(USART3, USART_IT_RXNE) == SET)
{
d=USART_ReceiveData(USART3);
g_esp8266_rx_buf[g_esp8266_rx_cnt++]=d;
if(g_esp8266_rx_cnt >= sizeof g_esp8266_rx_buf)
{
g_esp8266_rx_end=1;
}
//清空标志位,可以响应新的中断请求
USART_ClearITPendingBit(USART3, USART_IT_RXNE);
}
}
10、远程更新的代码(APP)
>>>main.c
#include "stm32f4xx.h"
#include "led.h"
#include "key.h"
#include "exti.h"
#include "delay.h"
#include "tim.h"
#include "pwm.h"
#include "usart.h"
#include "string.h"
#include "sys.h"
#include "dht11.h"
u8 buffer[32] = {0};
u8 rx_buffer[32] = {0};
u8 count = 0, rx_i;
u8 rx_flag = 0; //rx_flag = 1说明接受到数据
//USART中断接收
void USART1_IRQHandler(void)
{
//若是非空,则返回值为1,与RESET(0)判断,不相等则判断为真
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
//判断为真后,为下次中断做准备,则需要对中断的标志清零
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
/* DR读取接受到的数据*/
buffer[count++] = USART_ReceiveData(USART1);
if(buffer[count-1] == ':')
{
//过滤帧尾
for(rx_i=0; rx_i < (count-1); rx_i++)
{
rx_buffer[rx_i] = buffer[rx_i];
}
count = 0;
//清空数组
memset(buffer, 0, sizeof(buffer));
rx_flag = 1;//标志一帧数据接受完毕
}
}
}
int main(void)
{
int ret;
u8 data[5] = {0};
//NVIC中断分组:第二组, 抢占优先级取值范围:0x00~0x03; 响应优先级取值范围:0x00~0x03;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
Delay_Init();
Led_Init();
Usart1_Init();
Dht11_Init();
while(1)
{
ret = Dht11_Start();
if(ret == 0)
{
ret = Dht11_Read(data);
if(ret == 0)
{
printf("温度:%d.%d, 湿度:%d,%d\n",data[2], data[3], data[0], data[1]);
}
}
GPIO_ToggleBits(GPIOF, GPIO_Pin_9);
delay_s(2);
}
return 0;
}
总结
>>>所有内容会在今年更新。
故我在
点击下方卡片 关注我
↓↓