此程序是旧版本,请下载最新的20230315版使用。


lwipopts.h里面的MEM_SIZE这个选项非常重要!一定要尽可能大(至少10240以上,最好是102400以上),这样lwip才有足够的内存可以分配。因为FatFs里面DIR结构体和FIL结构体都很大,都是从lwip里面mem_malloc出来的,很耗内存。否则FTP的连接会经常因为内存不足而断开!!!!!!!!!!!!!

本程序在LwIP 2.1.2协议栈上用raw API实现了一个FTP服务器。文件存储在Winbond的W25Q128 SPI Flash中,通过FatFs读写文件,建立了FAT文件系统,容量为16MB。程序只有1700多行代码,由头文件ftp.h和源文件ftpd.c组成。

主要特点:
1. 采用lwip的raw API实现,可以在裸机环境下运行,支持IPv6
2. 兼容Windows文件管理器和FileZilla FTP客户端
3. 实现了文件浏览、上传、下载、重命名、新建文件夹、删除文件夹和文件等基本FTP功能
4. 支持IPv4和IPv6的主动模式和被动模式。可以在FileZilla客户端中强制使用主动模式,主动模式下电脑必须要关闭防火墙才能访问FTP服务器
5. nettime.c实现了从互联网获取北京时间的功能,并保存到单片机的RTC实时时钟,不用担心通过FTP上传的文件没有日期和时间信息。时间服务器选择的是utcnist.colorado.edu,端口号为37

不足之处:
1. 没有用户权限配置功能,目前所有的用户(包括匿名用户)都能上传和下载文件
2. 暂不支持FileZilla中的断点续传功能
3. FileZilla无法自动检测编码,必须改为强制UTF-8,文件名才不会乱码

【STM32F107VC+DP83848+W25Q128+FTP程序】

程序下载地址:百度网盘 请输入提取码(提取码:gk5t)

DP83848.c中的三个重要配置项:
(1)ETH_REMAP:若ETH部分引脚被重映射到了PD口,则应配置为1,否则为0
(2)USE_MII:如果DP83848以太网收发芯片使用的是MII接口则应配置为1,使用RMII接口则应配置为0
(3)RESET_N引脚:DP83848芯片的复位引脚,本例程中接的是PB15,可以改成其他引脚
请根据开发板的情况,正确配置DP83848.c中的这三个项目。

DP83848芯片的RESET引脚最好接上外部下拉电阻,避免单片机PA8没有输出时钟时DP83848未处于复位状态,干扰电路正常运行。
启动文件startup_stm32f107xc.s中应该设置合适的栈大小Stack_Size,供FatFs使用。

HSE晶振的大小在项目属性的C/C++选项卡中的Preprocessor Symbols中的HSE_VALUE上设置。
本例程设置的是25000000,即25MHz。若修改了晶振大小,则还需修改common.c的clock_init时钟初始化函数。

【STM32F103RE+88W8801+W25Q128+FTP程序】

请参阅:【程序】在STM32F103RE裸机环境下用lwip 2.1.2驱动88W8801 WiFi模块并运行FTP服务器读写W25Q128上存储的文件(20200703版)_ZLK1214的专栏-CSDN博客

【LwIP 2.0.3版本兼容性】

若要在2.0.3版本的lwip中运行,需要将2.1.2版本的pbuf_free_header和pbuf_remove_header函数复制过来,然后在ftpd.c顶部包含头文件<ctype.h>。

【BUG勘误】

1. Windows文件管理器中往FTP里面复制某些中文文件名的文件会失败。
这是由于Windows本身的BUG导致的,微软一直没有修复该BUG:UTF8 Encoding Bug Report about using ftp with windows explorer
Win7和Win10均有此BUG。往电脑上的FTP服务器上传文件后会出现问号,往本文的ftpd服务器上上传文件会直接提示失败。
FTP程序本身没有问题,fatfs的配置也没有问题。
要想上传中文文件名的文件,最好选择专业的FTP软件,如FileZilla。

2. tcp_accept函数没有对err参数做判断。当内存不足时收到新的FTP连接,整个程序就会卡死。
可通过FileZilla连续上传多个文件来复现此bug。串口输出如下:

ftpd_data_sent_list: paused! sndbuf=24, slen=62
[Send] len=590
[Recv] len=60
ftpd_sent: 39 bytes of response sent
ftpd_sent: processed 6 bytes
ftpd_data_sent_list: paused! sndbuf=24, slen=62
[Recv] len=66
FTPD accepted [233.2.0.8]:755!
220 LwIP FTP Service
Assertion "tcp_write: invalid pcb" failed at line 414 in lwip-2.1.2\core\tcp_out.cSIGABRT: Abnormal termination
Exited! returncode=1

在lwip中,tcp_accept回调函数的newpcb参数有可能为NULL,此时err参数为ERR_MEM。所以回调函数中必须要先判断err是否为ERR_OK,才能执行后面的操作。

解决方案是在ftpd_accept和ftpd_data_accept函数的最开头,添加如下判断:

if (err != ERR_OK)return err;

修改后的文件的下载链接:百度网盘 请输入提取码(提取码:p1du)

【nettime.c移植到其他系列STM32】

以STM32H743ZI为例。
首先,只有STM32F1才需要把日期保存到backup domain中,因此删掉rtc_savedate(),还要把common.c里面跟hrtc.DateToUpdate有关的代码都删掉。
另外,设置日期的时候,还需要填写date.WeekDay(星期几)这个成员。当前日期是星期几不需要自己写代码计算,C库函数localtime_r已经帮我们计算好了,直接用就行了。ptm->tm_wday=0是星期天,=1~6是星期一到星期六。
RTC_TimeTypeDef新增了很多成员。设置时间前,RTC_TimeTypeDef结构体要用={0}语句清零。

int nettime_set(const struct tm *ptm, int check_before_update)
{
......RTC_TimeTypeDef time = {0}, curr_time, empty_time = {0};
......  if (!check_before_update || diff){HAL_RTC_SetTime(&hrtc, &empty_time, RTC_FORMAT_BIN); // 先清空时间if (ptm->tm_wday == 0)date.WeekDay = RTC_WEEKDAY_SUNDAY;elsedate.WeekDay = ptm->tm_wday;HAL_RTC_SetDate(&hrtc, &date, RTC_FORMAT_BIN); // 设置日期HAL_RTC_SetTime(&hrtc, &time, RTC_FORMAT_BIN); // 设置时间//rtc_savedate();//sys_restart_timeouts();printf("RTC is updated from ");printf("20%02u-%u-%u %02u:%02u:%02u [%d] to ", curr_date.Year, curr_date.Month, curr_date.Date, curr_time.Hours, curr_time.Minutes, curr_time.Seconds, curr_date.WeekDay);printf("20%02u-%u-%u %02u:%02u:%02u [%d] (UTC+8)!\n", date.Year, date.Month, date.Date, time.Hours, time.Minutes, time.Seconds, date.WeekDay);return 1;}else{printf("RTC is not updated!\n");return 0;}
}

【程序运行截图】

1. 使用Windows文件管理器通过计算机名访问FTP服务器(已登录admin用户,密码为123456)

2. FTP登录界面

3. FileZilla访问FTP服务器(必须要开启强制UTF-8编码才能防止中文文件名乱码)

4. FileZilla上传文件

6. 通过板子的IPv6地址访问FTP服务器

【建议】
建议使用时将lwipopts.h里面的如下选项调大:
#define MEMP_NUM_TCP_PCB 50 // TCP最大连接数
#define MEMP_NUM_TCP_PCB_LISTEN 50 // TCP最大监听数
#define MEMP_NUM_PBUF 50 // struct pbuf结构体最大数量
建议配置下面的选项:
#define TCP_MSS 1500 // TCP报文携带的最大数据量
#define LWIP_TCP_SACK_OUT 1 // 允许TCP选择性确认

【程序运行结果】

STM32F107VC ETH
SystemCoreClock=72000000
LSI is ready!
Failed to start ETH!
MAC address: 00:80:E1:CD:9E:1A
IPv6 link-local address: FE80::280:E1FF:FECD:9E1A
SPI Flash: M=0xef, ID=0x17
SPI Flash: M=0xef, ID=0x4018
Disk is mounted!
DP83848 interrupt occurred! status=0x2c20
Link is up!
ETH is restarted!
[Send] len=350
[Send] len=86
[Send] len=78
[Send] len=86
[Recv] len=60
[Recv] len=60
[Recv] len=60
[Send] len=350
[Recv] len=60
[Recv] len=60
[Send] len=86
[Recv] len=208
[Send] len=70
[Recv] len=142
[Send] len=86
[Recv] len=86
[Send] len=78
[Recv] len=590
[Send] len=350
[Recv] len=590
[Send] len=42
[Send] len=42
[Send] len=42
[Send] len=42
[Send] len=86
DHCP supplied address!
IP address: 192.168.1.2
Subnet mask: 255.255.255.0
Default gateway: 192.168.1.1
IPv6 address 1: 2409:8A62:362:1050:280:E1FF:FECD:9E1A
DNS Server: 192.168.1.1
[Send] len=42
Time server IP: not in cache!
[Recv] len=60
[Send] len=80
[Recv] len=116
Time server IP: 128.138.140.44
Connecting to 128.138.140.44...
[Send] len=58
[Recv] len=60
Connected!
[Send] len=54
[Recv] len=208
[Recv] len=60
[Send] len=54
[Recv] len=60
Time from network: 2020-07-03 21:17:57
RTC is not updated!
Connection to the time server is closed!
[Send] len=54
[Recv] len=60
[Send] len=42
[Send] len=42
[Recv] len=60
[Send] len=42
[Recv] len=208
[Recv] len=60
[Recv] len=208
[Recv] len=208
[Recv] len=60
[Recv] len=140
[Recv] len=208
[Recv] len=140
[Recv] len=60
[Recv] len=208
[Recv] len=140
[Recv] len=208
[Recv] len=60
[Recv] len=208
[Recv] len=140
[Recv] len=140
[Recv] len=60
[Recv] len=208
[Recv] len=86
[Send] len=86
[Recv] len=86
[Send] len=78
[Recv] len=74
FTPD accepted [2409:8A62:362:1050:883B:55B0:939C:EE8]:54466!
220 LwIP FTP Service
[Send] len=96
[Recv] len=208
[Send] len=96
[Recv] len=74
ftpd_sent: 22 bytes of response sent
[Recv] len=90
ftpd_recv: received 16 bytes
USER anonymous
331 Anonymous access allowed, send identity (e-mail name) as password.
[Send] len=146
[Recv] len=74
ftpd_sent: 72 bytes of response sent
ftpd_sent: processed 16 bytes
[Recv] len=88
ftpd_recv: received 14 bytes
PASS IEUser@
230 Login successful.
[Send] len=97
[Recv] len=88
[Send] len=74
[Recv] len=60
[Send] len=97
[Recv] len=74
ftpd_sent: 23 bytes of response sent
ftpd_sent: processed 14 bytes
[Recv] len=88
ftpd_recv: received 14 bytes
opts utf8 on
200 Always in UTF8 mode.
[Send] len=100
[Recv] len=74
ftpd_sent: 26 bytes of response sent
ftpd_sent: processed 14 bytes
[Recv] len=80
ftpd_recv: received 6 bytes
syst
500 Unknown command.
[Send] len=96
[Recv] len=80
[Send] len=74
[Recv] len=208
[Send] len=96
[Recv] len=74
ftpd_sent: 22 bytes of response sent
ftpd_sent: processed 6 bytes
[Recv] len=85
ftpd_recv: received 11 bytes
site help
500 Unknown command.
[Send] len=96
[Recv] len=85
[Send] len=74
[Recv] len=92
[Send] len=96
[Recv] len=74
ftpd_sent: 22 bytes of response sent
ftpd_sent: processed 11 bytes
[Recv] len=79
ftpd_recv: received 5 bytes
PWD
257 "/" is the current directory.
[Send] len=109
[Recv] len=74
ftpd_sent: 35 bytes of response sent
ftpd_sent: processed 5 bytes
[Recv] len=82
ftpd_recv: received 8 bytes
TYPE A
200 Switching to ASCII mode.
[Send] len=104
[Recv] len=82
[Send] len=74
[Recv] len=92
[Recv] len=60
[Recv] len=208
[Recv] len=92
[Send] len=104
[Recv] len=86
[Send] len=86
[Recv] len=208
[Recv] len=140
[Send] len=104
[Recv] len=74
ftpd_sent: 30 bytes of response sent
ftpd_sent: processed 8 bytes
[Recv] len=80
ftpd_recv: received 6 bytes
EPSV
229 Entering Extended Passive Mode (|||57805|).
[Send] len=123
[Recv] len=80
[Send] len=74
[Recv] len=140
[Recv] len=60
[Send] len=123
[Recv] len=74
ftpd_sent: 49 bytes of response sent
ftpd_sent: processed 6 bytes
[Recv] len=86
[Send] len=78
[Recv] len=74
FTPD data connection to [2409:8A62:362:1050:883B:55B0:939C:EE8]:54468 is established!
[Recv] len=80
ftpd_recv: received 6 bytes
LIST
150 Here comes the directory listing.
[Send] len=113
[Recv] len=80
[Send] len=74
[Send] len=113
[Recv] len=74
ftpd_sent: 39 bytes of response sent
ftpd_sent: processed 6 bytes
04-04-2020  08:18PM                 9471 111110.xlsx
04-01-2020  11:26PM               934844 HeartOfCat_20200401.zip
05-18-2020  09:13PM                 9236 pt32.xlsx
04-27-2020  09:57PM                  917 test.c
FTPD data connection [2409:8A62:362:1050:883B:55B0:939C:EE8]:54468 is shutdown by the server!
[Send] len=295
226 Directory send OK.
[Send] len=98
[Recv] len=74
ftpd_sent: 24 bytes of response sent
[Recv] len=208
[Send] len=295
[Recv] len=74
[Recv] len=74
FTPD data connection [2409:8A62:362:1050:883B:55B0:939C:EE8]:54468 is closed by the client!
[Send] len=74
[Recv] len=74
[Send] len=74
[Recv] len=60
[Recv] len=208

【主要代码】

ftpd.h:

#ifndef _FTPD_H
#define _FTPD_H#ifndef FTPD_DEBUG
#define FTPD_DEBUG LWIP_DBG_OFF
#endif#define FTPD_PORT 21 // FTP服务器端口号
#define FTPD_PASV 1 // 是否允许使用PASV命令// 命令处理过程中的异常情况
#define FTPD_CMDSTEP_CONNFAILED 0x1000 // 连接建立失败
#define FTPD_CMDSTEP_CONNABORTED 0x2000 // 连接建立成功但异常中止
#define FTPD_CMDSTEP_CONNSHUTDOWN 0x4000 // 连接建立成功后被客户端关闭// FTP客户端状态位
#if FTPD_PASV
#define FTPD_FLAG_PASSIVE 0x01 // 当前是否为被动模式
#endif
#define FTPD_FLAG_CLOSE 0x02 // 收到客户端的TCP第一次挥手后, 请求发送第三次挥手
#define FTPD_FLAG_SHUTDOWN 0x04 // 请求发送TCP第一次挥手, 然后接收客户端第三次挥手
#define FTPD_FLAG_RENAME 0x08 // 是否正在重命名文件
#define FTPD_FLAG_AGAIN 0x10 // 当前FTP命令还没有执行完毕, 控制连接上的数据发送完毕后应继续回来处理
#define FTPD_FLAG_NEWDATACONN 0x20 // 数据连接已创建但还未连接上
#define FTPD_FLAG_TCPERROR 0x40 // TCP发送数据出错// 数据连接关闭方式
#define FTPD_FREEDATA_ABORT 0 // 强行中止数据连接
#define FTPD_FREEDATA_CLOSE 1 // 关闭数据连接 (客户端已关闭)
#define FTPD_FREEDATA_SHUTDOWN 2 // 关闭数据连接 (客户端未关闭)#ifndef MAX_PATH
#define MAX_PATH 260
#endifstruct ftpd_user
{char *name;char *password;
};struct ftpd_account
{struct ftpd_user user;char *rootpath;
};#ifdef FF_DEFINED
struct ftpd_state
{struct tcp_pcb *ctrlconn;struct tcp_pcb *dataconn;int dataport;char cmd[MAX_PATH + 20];int cmdlen;char *cmdarg;int cmdstep;char last;char type;char path[MAX_PATH];char rename[MAX_PATH];struct ftpd_user user;int userid;int flags;int sent; // 未收到确认的已发送字节数struct pbuf *queue; // 数据接收队列void *dataout;int dataout_len;DIR *dp;FIL *fp;FILINFO *finfo;
};
#else
struct ftpd_state;
#endifint ftpd_concat_path(char *buffer, int bufsize, const char *filename);
int ftpd_file_exists(const char *path);
#ifdef FF_DEFINED
time_t ftpd_filetime(WORD fdate, WORD ftime, struct tm *ptm);
#endif
int ftpd_fullpath(const struct ftpd_state *state, char *buffer, int bufsize, const char *filename, char **puserpath);
int ftpd_init(void);
void *ftpd_memrchr(const void *s, int c, size_t n);
int ftpd_simplify_path(char *path, int basepos);
char *ftpd_strdup(const char *s);#endif

ftpd.c:(2021年7月24日版本)

/*************************** 基于LwIP raw API的FTP服务器*****************************
** 注意事项:
** 1. 使用FileZilla客户端连接FTP服务器时, 字符集应该选择"强制UTF-8"
**    不能选择"自动检测", 这样文件名才不会乱码
** 2. 若想要移动文件, 可在文件管理器中使用"xxx/", "../"这样的语法重命名文件
**    例如想要把"abc.txt"移动到当前目录的123目录下, 则可以将文件重命名为"123/abc.txt"
**    将"def.doc"移动到父目录的456文件夹下, 则应该重命名为"../456/def.doc"
** 3. 服务器使用FatFs读写磁盘文件
**    如果出现HardFault错误, 则可能是startup_stm32*.s启动文件里面的Stack_Size值太小
**    将其改大就可以解决问题
************************************************************************************/
#include <ff.h>
#include <lwip/tcp.h>
#include <string.h>
#include <time.h>
#include "ftpd.h"static err_t ftpd_accept(void *arg, struct tcp_pcb *newpcb, err_t err);
static void ftpd_change_user(struct ftpd_state *state, const char *newuser);
static int ftpd_copy_cmd(struct ftpd_state *state);
#if FTPD_PASV
static err_t ftpd_data_accept(void *arg, struct tcp_pcb *newpcb, err_t err);
#endif
static void ftpd_data_check(struct ftpd_state *state);
static err_t ftpd_data_connected(void *arg, struct tcp_pcb *tpcb, err_t err);
static void ftpd_data_err(void *arg, err_t err);
static err_t ftpd_data_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err);
static err_t ftpd_data_sent(void *arg, struct tcp_pcb *tpcb, u16_t len);
static err_t ftpd_data_sent_list(void *arg, struct tcp_pcb *tpcb, u16_t len);
static err_t ftpd_data_sent_retr(void *arg, struct tcp_pcb *tpcb, u16_t len);
static void ftpd_err(void *arg, err_t err);
static void ftpd_free(struct ftpd_state *state);
static err_t ftpd_free_data(struct ftpd_state *state, int option);
static int ftpd_is_valid_user(struct ftpd_user *user, int *pid);
static int ftpd_prepare_data(struct ftpd_state *state);
static void ftpd_process_cmd(struct ftpd_state *state);
static int ftpd_process_data_cmd(struct ftpd_state *state);
static int ftpd_process_directory_cmd(struct ftpd_state *state);
static int ftpd_process_file_cmd(struct ftpd_state *state);
static int ftpd_process_opt_cmd(struct ftpd_state *state);
static int ftpd_process_user_cmd(struct ftpd_state *state);
static err_t ftpd_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err);
static int ftpd_send_msg(struct ftpd_state *state, const char *s);
static err_t ftpd_sent(void *arg, struct tcp_pcb *tpcb, u16_t len);// 用户列表以及对应的根目录
static const struct ftpd_account ftpd_users[] = {{{"anonymous", NULL}, "C:/public"}, // 匿名用户{{"admin", "123456"}, "C:/"},{{"test", "789123"}, "C:/test"}
};
// 盘符可在ffconf.h中的FF_VOLUME_STRS处指定static struct tcp_pcb *ftpd_tpcb;/* 控制连接收到新请求 */
static err_t ftpd_accept(void *arg, struct tcp_pcb *newpcb, err_t err)
{struct ftpd_state *state;if (err != ERR_OK){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("FTPD failed to accept a client! err=%d\n", err));return err;}state = mem_malloc(sizeof(struct ftpd_state));if (state == NULL){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("FTPD failed to accept [%s]:%d!\n", ipaddr_ntoa(&newpcb->remote_ip), newpcb->remote_port));tcp_abort(newpcb);return ERR_ABRT;}memset(state, 0, sizeof(struct ftpd_state));state->ctrlconn = newpcb;state->dataport = -1;state->type = 'A';strcpy(state->path, "/");state->userid = -1;LWIP_DEBUGF(FTPD_DEBUG, ("FTPD accepted [%s]:%d!\n", ipaddr_ntoa(&newpcb->remote_ip), newpcb->remote_port));ftpd_send_msg(state, "220 LwIP FTP Service\r\n");tcp_arg(newpcb, state);tcp_err(newpcb, ftpd_err);tcp_recv(newpcb, ftpd_recv);tcp_sent(newpcb, ftpd_sent);return ERR_OK;
}/* 改变用户名并清空密码 */
static void ftpd_change_user(struct ftpd_state *state, const char *newuser)
{if (state->user.name != NULL){mem_free(state->user.name);state->user.name = NULL;}if (state->user.password != NULL){mem_free(state->user.password);state->user.password = NULL;}if (newuser != NULL)state->user.name = ftpd_strdup(newuser);
}/* 将文件夹和文件名连接在一起形成新路径 */
// 将buffer和filename连接起来, 保存到buffer中, 同时保证字符串末尾不带斜杠(根目录除外); buffer的最大容量为bufsize
// 成功时返回字符串的长度; 失败时返回-1且buffer中的内容不变
int ftpd_concat_path(char *buffer, int bufsize, const char *filename)
{char *p;int addslash, fileabs, folderlen, namelen, len;// 找出字符串的连接位置, 并去掉buffer的尾斜杠和filename的首斜杠if (filename != NULL && filename[0] == '/'){// 如果文件名是绝对路径, 则需要把文件夹路径改为根目录fileabs = 1;filename++; // 去掉首斜杠if (buffer[0] == '/')folderlen = 1; // 文件夹路径不带盘符时只保留根目录符号 (首斜杠)else{p = strchr(buffer, ':');if (p != NULL)folderlen = p + 1 - buffer; // 文件夹路径带盘符时只保留盘符elsefolderlen = 0; // 如果buffer是相对路径, 清空字符串}}else{// 如果文件名不是绝对路径, 则可以直接在文件夹路径末尾连接上文件名fileabs = 0;folderlen = strlen(buffer);if (folderlen > 1 && buffer[folderlen - 1] == '/')folderlen--;}// 去掉filename的尾斜杠if (filename != NULL)namelen = strlen(filename);elsenamelen = 0;if (namelen != 0 && filename[namelen - 1] == '/')namelen--;// 计算字符串连接在一起后需要的缓冲区大小if (folderlen == 0)addslash = fileabs; // 路径为空时加不加斜杠取决于文件名是不是绝对路径else if (folderlen == 1 && buffer[0] == '/')addslash = 0; // 路径为斜杠时不加斜杠else if (folderlen != 0 && buffer[folderlen - 1] == ':')addslash = 1; // 路径最后一个字符为冒号时要加斜杠else if (namelen == 0)addslash = 0; // 文件名为空时不加斜杠elseaddslash = 1; // 其他情况都要加斜杠len = folderlen + addslash + namelen; // 连接后的长度if (len >= bufsize)return -1; // 缓冲区不够// 连接字符串if (addslash)buffer[folderlen] = '/';if (namelen != 0)memcpy(buffer + folderlen + addslash, filename, namelen);buffer[len] = '\0';return len;
}/* 将数据接收队列queue中的FTP命令字符串提取到state->cmd中, 并释放占用的pbuf内存 */
// 返回值: 0表示还没有收到完整命令; 1表示收到了完整命令; 2表示收到了完整命令, 但超过了缓冲区最大长度
// state->cmdlen表示已收到了当前命令多少个字符 (包括\r\n)
static int ftpd_copy_cmd(struct ftpd_state *state)
{char *c;int complete = 0; // 是否收到完整命令int cnt = 0; // 本次复制的字符数int i;struct pbuf *p;for (p = state->queue; p != NULL && complete == 0; p = p->next){c = p->payload;for (i = 0; i < p->len && complete == 0; i++){if (state->last == '\r' && *c == '\n'){if (state->cmdlen <= sizeof(state->cmd)){state->cmd[state->cmdlen - 1] = '\0'; // 把\r替换成\0complete = 1;}elsecomplete = 2;}else{if (state->cmdlen < sizeof(state->cmd))state->cmd[state->cmdlen] = *c;}state->cmdlen++;state->last = *c;c++;cnt++;}}state->queue = pbuf_free_header(state->queue, cnt);return complete;
}#if FTPD_PASV
/* 数据连接被动模式连接建立成功 */
static err_t ftpd_data_accept(void *arg, struct tcp_pcb *newpcb, err_t err)
{struct ftpd_state *state = arg;if (err != ERR_OK){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("FTPD data failed to accept a client! err=%d\n", err));return err;}if (!ip_addr_cmp(&newpcb->remote_ip, &state->ctrlconn->remote_ip)){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_WARNING, ("%s: IP address mismatch!\n", __FUNCTION__));tcp_abort(newpcb);return ERR_ABRT;}tcp_close(state->dataconn); // 关闭端口监听state->dataconn = newpcb;tcp_err(newpcb, ftpd_data_err);return ftpd_data_connected(arg, newpcb, err);
}
#endif/* 检查数据连接是否未开始发送数据 */
// 这个函数应该在控制连接发送完开始信息后调用一次
static void ftpd_data_check(struct ftpd_state *state)
{
#if FTPD_PASVif (state->flags & FTPD_FLAG_PASSIVE){// 在PASV模式下, 连接可能会在PASV命令执行完毕的时候就建立成功// 但必须要等到数据传输命令(如LIST命令)的响应(如150响应)发送完毕后, 才能开始发送数据if ((state->flags & FTPD_FLAG_NEWDATACONN) == 0)ftpd_data_sent(state, state->dataconn, 0);// PORT模式下不存在这个问题, 因为连接建立后就可以立即开始发送数据}
#endif
}/* 数据连接主动模式建立成功 */
static err_t ftpd_data_connected(void *arg, struct tcp_pcb *tpcb, err_t err)
{struct ftpd_state *state = arg;LWIP_DEBUGF(FTPD_DEBUG, ("FTPD data connection to [%s]:%d is established!\n", ipaddr_ntoa(&tpcb->remote_ip), tpcb->remote_port));state->flags &= ~FTPD_FLAG_NEWDATACONN;tcp_recv(tpcb, ftpd_data_recv);tcp_sent(tpcb, ftpd_data_sent);return ftpd_data_sent(arg, tpcb, 0);
}/* 数据连接出错 */
static void ftpd_data_err(void *arg, err_t err)
{struct ftpd_state *state = arg;LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_WARNING, ("FTPD data error! err=%d\n", err));if (state != NULL){state->dataconn = NULL; // 调用err回调函数时, tpcb已经被LwIP释放了, 所以不需要再次释放ftpd_free_data(state, FTPD_FREEDATA_ABORT);if (state->flags & FTPD_FLAG_NEWDATACONN){state->flags &= ~FTPD_FLAG_NEWDATACONN;state->cmdstep |= FTPD_CMDSTEP_CONNFAILED;ftpd_send_msg(state, "425 Failed to establish connection.\r\n");}else{state->cmdstep |= FTPD_CMDSTEP_CONNABORTED;ftpd_process_cmd(state);}}
}/* 数据连接收到数据 */
static err_t ftpd_data_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{struct ftpd_state *state = arg;struct pbuf *q;FRESULT fr;UINT bw;if (p != NULL){if (state != NULL){if (strcasecmp(state->cmd, "STOR") == 0){LWIP_DEBUGF(FTPD_DEBUG, ("%s: %d bytes received\n", __FUNCTION__, p->tot_len));for (q = p; q != NULL; q = q->next){fr = f_write(state->fp, q->payload, q->len, &bw);if (bw != q->len){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: f_write() failed! fr=%d, q->len=%u, bw=%u\n", __FUNCTION__, fr, q->len, bw));pbuf_free(p);err = ftpd_free_data(state, FTPD_FREEDATA_ABORT);state->cmdstep = FTPD_CMDSTEP_CONNABORTED;ftpd_process_cmd(state);return err;}}}}tcp_recved(tpcb, p->tot_len);pbuf_free(p);}else{if (state != NULL){LWIP_DEBUGF(FTPD_DEBUG, ("FTPD data connection [%s]:%d is shutdown by the client!\n", ipaddr_ntoa(&tpcb->remote_ip), tpcb->remote_port));ftpd_free_data(state, FTPD_FREEDATA_CLOSE);// 通知命令处理函数, 数据连接已被客户端关闭state->cmdstep |= FTPD_CMDSTEP_CONNSHUTDOWN;ftpd_process_cmd(state);}elseLWIP_DEBUGF(FTPD_DEBUG, ("FTPD data connection [%s]:%d is closed by the client!\n", ipaddr_ntoa(&tpcb->remote_ip), tpcb->remote_port));}return ERR_OK;
}static err_t ftpd_data_sent(void *arg, struct tcp_pcb *tpcb, u16_t len)
{err_t err = ERR_OK;struct ftpd_state *state = arg;if (state != NULL){if (strcasecmp(state->cmd, "LIST") == 0)err = ftpd_data_sent_list(arg, tpcb, len);else if (strcasecmp(state->cmd, "RETR") == 0)err = ftpd_data_sent_retr(arg, tpcb, len);}return err;
}/* 发送文件列表 */
static err_t ftpd_data_sent_list(void *arg, struct tcp_pcb *tpcb, u16_t len)
{char buffer[MAX_PATH + 100];err_t err;int bufsize, loop, slen;struct ftpd_state *state = arg;struct tm tm;FRESULT fr;if (state->finfo == NULL){loop = 2;state->finfo = mem_malloc(sizeof(FILINFO));if (state->finfo == NULL){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: mem_malloc() failed!\n", __FUNCTION__));goto err;}}elseloop = 1;while (loop){if (loop == 2){// 读取下一个文件的信息fr = f_readdir(state->dp, state->finfo);if (fr != FR_OK || state->finfo->fname[0] == '\0'){if (fr != FR_OK)LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: f_readdir() failed! fr=%d\n", __FUNCTION__, fr)); // 读取文件信息失败// 列表发送完毕ftpd_free_data(state, FTPD_FREEDATA_SHUTDOWN);state->cmdstep = 2;ftpd_process_cmd(state);break;}}if (strcmp(state->finfo->fname, ".") == 0 || strcmp(state->finfo->fname, "..") == 0){LWIP_DEBUGF(FTPD_DEBUG, ("%s: jumping over \"%s\"\n", __FUNCTION__, state->finfo->fname));continue;}ftpd_filetime(state->finfo->fdate, state->finfo->ftime, &tm);slen = strftime(buffer, sizeof(buffer), "%m-%d-%Y  %I:%M%p       ", &tm);if (state->finfo->fattrib & AM_DIR)strcpy(buffer + slen, "<DIR>          ");elsesprintf(buffer + slen, "%14u ", state->finfo->fsize);slen += 15;slen += sprintf(buffer + slen, "%s\r\n", state->finfo->fname);LWIP_ASSERT("slen < sizeof(buffer)", slen < sizeof(buffer));bufsize = tcp_sndbuf(tpcb);if (bufsize >= slen){LWIP_DEBUGF(FTPD_DEBUG, ("%s", buffer));err = tcp_write(tpcb, buffer, slen, TCP_WRITE_FLAG_COPY);if (err != ERR_OK){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: tcp_write() failed! err=%d\n", __FUNCTION__, err));goto err;}loop = 2;}else{// TCP滑动窗口不够了, 暂时退出, 等待前面的数据发送完毕LWIP_DEBUGF(FTPD_DEBUG, ("%s: paused! sndbuf=%d, slen=%d\n", __FUNCTION__, bufsize, slen));loop = 0;}}return ERR_OK;err:err = ftpd_free_data(state, FTPD_FREEDATA_ABORT);state->cmdstep = FTPD_CMDSTEP_CONNABORTED;ftpd_process_cmd(state);return err;
}/* 发送文件内容 */
static err_t ftpd_data_sent_retr(void *arg, struct tcp_pcb *tpcb, u16_t len)
{char buffer[30];err_t err;struct ftpd_state *state = arg;unsigned int size;FRESULT fr;UINT br;// 等待上一段数据发送完毕state->dataout_len -= len;if (state->dataout_len != 0)return ERR_OK;// 释放上一段数据占用的内存if (state->dataout != NULL){mem_free(state->dataout);state->dataout = NULL;}size = tcp_sndbuf(tpcb);LWIP_ASSERT("sndbuf != 0", size != 0);state->dataout = mem_malloc(size);if (state->dataout == NULL){// 内存分配失败时改用buffer缓冲区LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: mem_malloc() failed!\n", __FUNCTION__));if (size > sizeof(buffer))size = sizeof(buffer);}if (state->dataout != NULL)fr = f_read(state->fp, state->dataout, size, &br);elsefr = f_read(state->fp, buffer, size, &br);if (fr != FR_OK){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: failed to read file! fr=%d\n", __FUNCTION__, fr));goto err;}if (br < size)size = br;if (size > 0){state->dataout_len = size;if (state->dataout != NULL)err = tcp_write(tpcb, state->dataout, size, 0);elseerr = tcp_write(tpcb, buffer, size, TCP_WRITE_FLAG_COPY);if (err != ERR_OK){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: tcp_write() failed! err=%d\n", __FUNCTION__, err));goto err;}}if (f_eof(state->fp)){// 文件发送完毕ftpd_free_data(state, FTPD_FREEDATA_SHUTDOWN);state->cmdstep = 2;ftpd_process_cmd(state);}return ERR_OK;err:err = ftpd_free_data(state, FTPD_FREEDATA_ABORT);state->cmdstep = FTPD_CMDSTEP_CONNABORTED;ftpd_process_cmd(state);return err;
}/* 控制连接出错 */
static void ftpd_err(void *arg, err_t err)
{struct ftpd_state *state = arg;LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_WARNING, ("FTPD error! err=%d\n", err));if (state != NULL){state->ctrlconn = NULL;ftpd_free(state);}
}/* 判断指定文件是否存在 */
// 如果是判断文件夹是否存在, 则字符串末尾不能有斜杠
int ftpd_file_exists(const char *path)
{FRESULT fr;if (strcmp(path + 1, ":") == 0 || strcmp(path + 1, ":/") == 0)return 1;fr = f_stat(path, NULL);return fr == FR_OK;
}/* 将文件时间转换为C标准格式 */
// 在STM32中, time_t是32位的无符号整数 (unsigned int)
// 因为没有符号位, 所以time_t支持超过2038年的年份, 可以放心使用
time_t ftpd_filetime(WORD fdate, WORD ftime, struct tm *ptm)
{memset(ptm, 0, sizeof(struct tm));ptm->tm_year = ((fdate >> 9) & 0x7f) + 80;ptm->tm_mon = ((fdate >> 5) & 0x0f) - 1;ptm->tm_mday = fdate & 0x1f;ptm->tm_hour = (ftime >> 11) & 0x1f;ptm->tm_min = (ftime >> 5) & 0x3f;ptm->tm_sec = (ftime & 0x1f) << 1;// 如果ptm中的日期有误, 则这个函数能自动修正// 否则如果输出了错误的日期, 那么Windows的文件管理器会错误地显示快捷方式图标return mktime(ptm);
}/* 关闭FTP控制连接和数据连接, 释放state结构体以及里面的成员占用的内存 */
static void ftpd_free(struct ftpd_state *state)
{if (state == NULL)return;ftpd_free_data(state, FTPD_FREEDATA_ABORT); // 如果数据连接尚未关闭, 则强行中止if (state->ctrlconn != NULL){if (state->flags & FTPD_FLAG_CLOSE)LWIP_DEBUGF(FTPD_DEBUG, ("FTPD connection [%s]:%d is closed by the server!\n", ipaddr_ntoa(&state->ctrlconn->remote_ip), state->ctrlconn->remote_port));elseLWIP_DEBUGF(FTPD_DEBUG, ("FTPD connection [%s]:%d is shutdown by the server!\n", ipaddr_ntoa(&state->ctrlconn->remote_ip), state->ctrlconn->remote_port));tcp_arg(state->ctrlconn, NULL);tcp_close(state->ctrlconn);state->ctrlconn = NULL;}ftpd_change_user(state, NULL);mem_free(state);
}/* 关闭FTP数据连接并释放相关内存 */
// 关闭连接时通常将option设为FTPD_FREEDATA_SHUTDOWN
// 只有在ftpd_data_recv(p=NULL)中才使用FTPD_FREEDATA_CLOSE
static err_t ftpd_free_data(struct ftpd_state *state, int option)
{err_t err = ERR_OK;if (state == NULL)return err;state->dataport = -1;if (state->dataconn != NULL){tcp_arg(state->dataconn, NULL); // 连接关闭后, 回调函数仍有可能触发, 所以必须和state彻底脱离关系
#if FTPD_PASVif ((state->flags & FTPD_FLAG_PASSIVE) && (state->flags & FTPD_FLAG_NEWDATACONN)){LWIP_DEBUGF(FTPD_DEBUG, ("FTPD data pcb is removed!\n"));tcp_close(state->dataconn);}else{
#endifif (option == FTPD_FREEDATA_ABORT){LWIP_DEBUGF(FTPD_DEBUG, ("FTPD data connection [%s]:%d is aborted!\n", ipaddr_ntoa(&state->dataconn->remote_ip), state->dataconn->remote_port));tcp_abort(state->dataconn);err = ERR_ABRT;}else{if (option == FTPD_FREEDATA_CLOSE)LWIP_DEBUGF(FTPD_DEBUG, ("FTPD data connection [%s]:%d is closed by the server!\n", ipaddr_ntoa(&state->dataconn->remote_ip), state->dataconn->remote_port));elseLWIP_DEBUGF(FTPD_DEBUG, ("FTPD data connection [%s]:%d is shutdown by the server!\n", ipaddr_ntoa(&state->dataconn->remote_ip), state->dataconn->remote_port));tcp_close(state->dataconn);}
#if FTPD_PASV}
#endifstate->dataconn = NULL;}if (state->dataout != NULL){mem_free(state->dataout);state->dataout = NULL;state->dataout_len = 0;}// 只有文件夹成功打开了之后, 才可以将指针赋给state->dpif (state->dp != NULL){f_closedir(state->dp);mem_free(state->dp);state->dp = NULL;}// 只有文件成功打开了之后, 才可以将指针赋给state->fpif (state->fp != NULL){f_close(state->fp);mem_free(state->fp);state->fp = NULL;}if (state->finfo != NULL){mem_free(state->finfo);state->finfo = NULL;}return err;
}/* 将用户根文件夹路径(rootpath)、当前文件夹路径(state->path)和文件名(filename)连接起来, 放入buffer缓冲区中 */
// buffer的原有内容会被忽略并清空, bufsize为缓冲区的大小
// puserpath为输出参数, 其内容是以用户文件夹为根目录的文件路径
int ftpd_fullpath(const struct ftpd_state *state, char *buffer, int bufsize, const char *filename, char **puserpath)
{int basepos, ret;// 在缓冲区中准备好用户文件夹的路径if (state->userid == -1)return -1; // 未登录, 连接失败basepos = strlen(ftpd_users[state->userid].rootpath);if (basepos + 1 > bufsize)return -1; // 缓冲区不够strcpy(buffer, ftpd_users[state->userid].rootpath);// 获取相对于用户文件夹的文件路径if (buffer[basepos - 1] == '/')basepos--; // 使userpath的第一个字符为斜杠// 如果没有斜杠, 则userpath指向\0, 下面连接路径后可能会变成斜杠if (puserpath != NULL)*puserpath = buffer + basepos;// 连接state->path字符串if (filename == NULL || filename[0] != '/'){// filename不是以用户文件夹为根目录的绝对路径, 而是相对于state->path的相对路径// 需要将rootpath, state->path和filename这三个字符串连在一起// filename == NULL的情况可视为空字符串, 是相对路径LWIP_ASSERT("state->path[0] == '/'", state->path[0] == '/'); // state->path的首字符始终为斜杠ret = ftpd_concat_path(buffer, bufsize, state->path + 1);if (ret == -1)return -1;}else{// filename是以用户文件夹为根目录的绝对路径// 跳过斜杠字符, 只将rootpath和不带首斜杠的filename连起来filename++;}// 连接filename字符串ret = ftpd_concat_path(buffer, bufsize, filename);if (ret == -1)return -1;ret = ftpd_simplify_path(buffer, basepos);if (puserpath != NULL && **puserpath == '\0')*puserpath = "/"; // 如果最终结果就是用户根目录, 那么应该用正斜杠表示, 而不是空字符串return ret;
}/* 判断输入的用户名和密码是否正确 */
static int ftpd_is_valid_user(struct ftpd_user *user, int *pid)
{int i;int n = LWIP_ARRAYSIZE(ftpd_users);if (user->name == NULL)return 0; // 未输入用户名for (i = 0; i < n; i++){if (strcasecmp(user->name, ftpd_users[i].user.name) == 0){if (pid != NULL)*pid = i;if (ftpd_users[i].user.password == NULL)return 1; // 任何密码都可以else if (user->password != NULL && strcmp(user->password, ftpd_users[i].user.password) == 0)return 1; // 密码正确elsereturn 0; // 密码错误}}return 0; // 用户名不存在
}/* 启动ftpd服务器 */
int ftpd_init(void)
{err_t err;struct tcp_pcb *temp;if (ftpd_tpcb != NULL){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_WARNING, ("%s: FTPD server is already started!\n", __FUNCTION__));return -1;}temp = tcp_new();if (temp == NULL){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: tcp_new() failed!\n", __FUNCTION__));return -1;}err = tcp_bind(temp, IP_ANY_TYPE, FTPD_PORT);if (err != ERR_OK){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: tcp_bind() failed! err=%d\n", __FUNCTION__, err));tcp_close(temp);return -1;}ftpd_tpcb = tcp_listen(temp);if (ftpd_tpcb == NULL){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: tcp_listen() failed!\n", __FUNCTION__));tcp_close(temp);return -1;}temp = NULL;tcp_accept(ftpd_tpcb, ftpd_accept);return 0;
}/* memchr的反向版本 */
void *ftpd_memrchr(const void *s, int c, size_t n)
{const char *p = s;int i;for (i = n - 1; i >= 0; i--){if (p[i] == c)return (void *)&p[i];}return NULL;
}/* 准备好数据连接 */
// 若函数返回-1, 则表示连接建立失败, 此时已发送了425消息, 不用再发送其他错误消息
static int ftpd_prepare_data(struct ftpd_state *state)
{err_t err;int ret = -1;if (state->dataport == -1){ftpd_send_msg(state, "425 Use PORT or PASV first.\r\n");return -1;}#if FTPD_PASVif (state->flags & FTPD_FLAG_PASSIVE){LWIP_ASSERT("state->dataconn != NULL", state->dataconn != NULL);ret = 0;}else{
#endifLWIP_ASSERT("state->dataconn == NULL", state->dataconn == NULL);state->dataconn = tcp_new();if (state->dataconn == NULL){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: tcp_new() failed!\n", __FUNCTION__));goto end;}tcp_arg(state->dataconn, state);err = tcp_connect(state->dataconn, &state->ctrlconn->remote_ip, state->dataport, ftpd_data_connected);if (err == ERR_OK){// 使用PORT模式时, 最好将电脑的防火墙关闭, 以免板子连不上电脑而出错LWIP_DEBUGF(FTPD_DEBUG, ("FTPD is connecting to [%s]:%d...\n", ipaddr_ntoa(&state->ctrlconn->remote_ip), state->dataport));tcp_err(state->dataconn, ftpd_data_err);state->flags |= FTPD_FLAG_NEWDATACONN;ret = 0;}elseLWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: tcp_connect() failed! err=%d\n", __FUNCTION__, err));
#if FTPD_PASV}
#endifend:if (ret == -1){ftpd_send_msg(state, "425 Failed to establish connection.\r\n");if (state->dataconn != NULL){tcp_arg(state->dataconn, NULL);tcp_close(state->dataconn);state->dataconn = NULL;state->dataport = -1;}}return ret;
}/* 处理命令 */
static void ftpd_process_cmd(struct ftpd_state *state)
{int ret;// 只有当上一个命令的所有回应都发送完毕时, 才开始处理下一条命令if (state->sent != 0)return;else if (state->flags & FTPD_FLAG_TCPERROR)goto end;if ((state->flags & FTPD_FLAG_AGAIN) == 0){// 上一条命令已执行完毕if (state->flags & (FTPD_FLAG_CLOSE | FTPD_FLAG_SHUTDOWN)){ftpd_free(state);return;}// 从接收队列中取出一条新命令ret = ftpd_copy_cmd(state);if (ret == 0)return; // 命令不完整LWIP_DEBUGF(FTPD_DEBUG, ("%s\n", state->cmd));state->cmdstep = 0; // 当前是命令的第一步操作if (ret == 2){ftpd_send_msg(state, "500 Syntax error, command unrecognized.\r\n");goto end;}// 提取出命令参数state->cmdarg = strchr(state->cmd, ' ');if (state->cmdarg != NULL)*state->cmdarg++ = '\0';elsestate->cmdarg = "";}else{// 上一条命令未执行完毕, 虽然state->sent==0, 但还需要继续发送更多数据// cmd和cmdarg不变, 命令可根据cmdstep的值决定当前是第几步操作state->flags &= ~FTPD_FLAG_AGAIN;}// 处理各种命令if (ftpd_process_user_cmd(state)) // 这个必须第一个处理;else if (ftpd_process_data_cmd(state));else if (ftpd_process_directory_cmd(state));else if (ftpd_process_file_cmd(state));else if (ftpd_process_opt_cmd(state));elseftpd_send_msg(state, "500 Unknown command.\r\n");end:// TCP无法发送数据时, 强制关闭连接if (state->sent == 0 && state->flags & FTPD_FLAG_TCPERROR){state->flags = (state->flags & ~FTPD_FLAG_AGAIN) | FTPD_FLAG_SHUTDOWN;ftpd_free(state);}
}/* 处理与数据连接有关的命令 */
static int ftpd_process_data_cmd(struct ftpd_state *state)
{char ip[IPADDR_STRLEN_MAX];int i, j, ret;int isport = 0;ip_addr_t ipaddr;
#if FTPD_PASVchar buffer[100];err_t err;int ispasv = 0;struct tcp_pcb *newpcb;
#endif#if LWIP_IPV6if (IP_IS_V4_VAL(state->ctrlconn->remote_ip)){
#endifif (strcasecmp(state->cmd, "PORT") == 0)isport = 4;
#if FTPD_PASVelse if (strcasecmp(state->cmd, "PASV") == 0)ispasv = 4;
#endif
#if LWIP_IPV6}else if (IP_IS_V6_VAL(state->ctrlconn->remote_ip)){if (strcasecmp(state->cmd, "EPRT") == 0)isport = 6;
#if FTPD_PASVelse if (strcasecmp(state->cmd, "EPSV") == 0)ispasv = 6;
#endif}
#endifif (isport){// 如果之前启动了PASV模式, 则关闭创建的监听连接state->dataport = -1;
#if FTPD_PASVif (state->flags & FTPD_FLAG_PASSIVE){state->flags &= ~FTPD_FLAG_PASSIVE;if (state->dataconn != NULL){tcp_close(state->dataconn);state->dataconn = NULL;}}
#endif// 提取出IP地址
#if LWIP_IPV6if (isport == 4){
#endiffor (i = j = 0; i < sizeof(ip) && j < 4; i++){if (isdigit(state->cmdarg[i]))ip[i] = state->cmdarg[i];else if (state->cmdarg[i] == ','){ip[i] = '.';j++;}elsebreak;}if (j != 4)goto porterr;ip[i - 1] = '\0';
#if LWIP_IPV6}else{if (memcmp(state->cmdarg, "|2|", 3) != 0)goto porterr;for (i = 0; i < sizeof(ip); i++){if (state->cmdarg[3 + i] == '|')break;ip[i] = state->cmdarg[3 + i];}if (i == sizeof(ip))goto porterr;ip[i] = '\0';}
#endifret = ipaddr_aton(ip, &ipaddr);if (ret == 0 || !ip_addr_cmp(&ipaddr, &state->ctrlconn->remote_ip))goto porterr;// 提取出端口号
#if LWIP_IPV6if (isport == 4){
#endifret = sscanf(state->cmdarg + i, "%d,%d", &i, &j);if (ret != 2)goto porterr;ret = i * 256 + j;
#if LWIP_IPV6}else{i = sscanf(state->cmdarg + 4 + i, "%d", &ret);if (i != 1)goto porterr;}
#endifif (ret != 0 && ret < 65536){state->dataport = ret;
#if LWIP_IPV6if (isport == 4){
#endif
#if FTPD_PASVftpd_send_msg(state, "200 PORT command successful. Consider using PASV.\r\n");
#elseftpd_send_msg(state, "200 PORT command successful.\r\n");
#endif
#if LWIP_IPV6}elseftpd_send_msg(state, "200 EPRT command successful.\r\n");
#endifreturn 1;}
porterr:
#if LWIP_IPV6if (isport == 4)
#endifftpd_send_msg(state, "500 Illegal PORT command.\r\n");
#if LWIP_IPV6elseftpd_send_msg(state, "500 Illegal EPRT command.\r\n");
#endif}
#if FTPD_PASVelse if (ispasv){if (state->dataconn == NULL){state->dataconn = tcp_new();if (state->dataconn == NULL)goto pasverr;#if LWIP_IPV6if (ispasv == 4)
#endiferr = tcp_bind(state->dataconn, IP_ADDR_ANY, 0);
#if LWIP_IPV6elseerr = tcp_bind(state->dataconn, IP6_ADDR_ANY, 0);
#endifif (err != ERR_OK)goto pasverr;newpcb = tcp_listen(state->dataconn);if (newpcb == NULL)goto pasverr;state->dataconn = newpcb;tcp_arg(state->dataconn, state);tcp_accept(state->dataconn, ftpd_data_accept);state->dataport = state->dataconn->local_port;state->flags |= FTPD_FLAG_NEWDATACONN | FTPD_FLAG_PASSIVE;}#if LWIP_IPV6if (ispasv == 4){
#endifipaddr_ntoa_r(&state->ctrlconn->local_ip, ip, sizeof(ip));for (i = 0; ip[i] != '\0'; i++){if (ip[i] == '.')ip[i] = ',';}sprintf(buffer, "227 Entering Passive Mode (%s,%d,%d).\r\n", ip, (state->dataport >> 8) & 0xff, state->dataport & 0xff);
#if LWIP_IPV6}elsesprintf(buffer, "229 Entering Extended Passive Mode (|||%d|).\r\n", state->dataport);
#endifftpd_send_msg(state, buffer);return 1;
pasverr:
#if LWIP_IPV6if (ispasv == 4)
#endifftpd_send_msg(state, "500 PASV command failed.\r\n");
#if LWIP_IPV6elseftpd_send_msg(state, "500 EPSV command failed.\r\n");
#endifif (state->dataconn != NULL){tcp_close(state->dataconn);state->dataconn = NULL;}}
#endifelsereturn 0;return 1;
}static int ftpd_process_directory_cmd(struct ftpd_state *state)
{char buffer[MAX_PATH];char *path;int ret;DIR *dp = NULL;FRESULT fr;if (strcasecmp(state->cmd, "PWD") == 0){ftpd_send_msg(state, "257 \"");ftpd_send_msg(state, state->path);ftpd_send_msg(state, "\" is the current directory.\r\n");}else if (strcasecmp(state->cmd, "CWD") == 0){ret = ftpd_fullpath(state, buffer, MAX_PATH, state->cmdarg, &path);if (ret == -1)goto cwderr;else if (!ftpd_file_exists(buffer))goto cwderr;strcpy(state->path, path);ftpd_send_msg(state, "250 Directory successfully changed.\r\n");return 1;
cwderr:ftpd_send_msg(state, "550 Failed to change directory.\r\n");}else if (strcasecmp(state->cmd, "LIST") == 0){if (state->cmdstep == 0){ret = ftpd_fullpath(state, buffer, MAX_PATH, NULL, NULL);if (ret == -1)goto listerr;LWIP_ASSERT("state->dp == NULL", state->dp == NULL);dp = mem_malloc(sizeof(DIR));if (dp == NULL){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: mem_malloc(sizeof(DIR)) failed!\n", __FUNCTION__));goto listerr;}fr = f_opendir(dp, buffer);if (fr != FR_OK)goto listerr;state->dp = dp; // 文件夹打开了之后才能赋给state->dpret = ftpd_prepare_data(state);if (ret == -1)goto listerr2;state->cmdstep = 1;state->flags |= FTPD_FLAG_AGAIN;ftpd_send_msg(state, "150 Here comes the directory listing.\r\n");return 1;
listerr:state->cmdstep = FTPD_CMDSTEP_CONNABORTED;
listerr2:// 这里涉及到两个不同的操作// 一个是关闭文件夹, 另一个是释放存储文件夹信息的内存if (state->dp != NULL){f_closedir(state->dp);state->dp = NULL;}if (dp != NULL){mem_free(dp);dp = NULL;}}else if (state->cmdstep == 1){state->flags |= FTPD_FLAG_AGAIN;ftpd_data_check(state);}else if (state->cmdstep == 2)ftpd_send_msg(state, "226 Directory send OK.\r\n");if (state->cmdstep & (FTPD_CMDSTEP_CONNABORTED | FTPD_CMDSTEP_CONNSHUTDOWN))ftpd_send_msg(state, "450 Failed to list the folder.\r\n");}else if (strcasecmp(state->cmd, "MKD") == 0){ret = ftpd_fullpath(state, buffer, MAX_PATH, state->cmdarg, &path);if (ret != -1){fr = f_mkdir(buffer);if (fr == FR_OK){ftpd_send_msg(state, "257 \"");ftpd_send_msg(state, path);ftpd_send_msg(state, "\" created.\r\n");return 1;}}ftpd_send_msg(state, "550 Create directory operation failed.\r\n");}else if (strcasecmp(state->cmd, "RMD") == 0){ret = ftpd_fullpath(state, buffer, MAX_PATH, state->cmdarg, NULL);if (ret != -1){fr = f_unlink(buffer);if (fr == FR_OK){ftpd_send_msg(state, "250 Remove directory operation successful.\r\n");return 1;}}ftpd_send_msg(state, "550 Remove directory operation failed.\r\n");}elsereturn 0;return 1;
}/* 处理与文件有关的命令 */
static int ftpd_process_file_cmd(struct ftpd_state *state)
{char buffer[MAX_PATH];int ret;long size;FIL *fp = NULL;FRESULT fr;if (strcasecmp(state->cmd, "SIZE") == 0){ret = ftpd_fullpath(state, buffer, MAX_PATH, state->cmdarg, NULL);if (ret == -1)goto sizeerr;fp = mem_malloc(sizeof(FIL));if (fp == NULL){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: mem_malloc(sizeof(FIL)) failed!\n", __FUNCTION__));goto sizeerr;}fr = f_open(fp, buffer, FA_READ);if (fr != FR_OK){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: f_open() failed! fr=%d\n", __FUNCTION__, fr));goto sizeerr;}size = f_size(fp);f_close(fp);mem_free(fp);sprintf(buffer, "213 %ld\r\n", size);ftpd_send_msg(state, buffer);return 1;
sizeerr:ftpd_send_msg(state, "550 Could not get file size.\r\n");if (fp != NULL)mem_free(fp);}else if (strcasecmp(state->cmd, "RETR") == 0){if (state->cmdstep == 0){ret = ftpd_fullpath(state, buffer, MAX_PATH, state->cmdarg, NULL);if (ret == -1)goto retrerr;LWIP_ASSERT("state->fp == NULL", state->fp == NULL);fp = mem_malloc(sizeof(FIL));if (fp == NULL){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: mem_malloc(sizeof(FIL)) failed!\n", __FUNCTION__));goto retrerr;}fr = f_open(fp, buffer, FA_READ);if (fr != FR_OK){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: f_open() failed! fr=%d\n", __FUNCTION__, fr));goto retrerr;}state->fp = fp; // 文件打开了之后才能赋给state->fpret = ftpd_prepare_data(state);if (ret == -1)goto retrerr2;state->cmdstep = 1;state->flags |= FTPD_FLAG_AGAIN;sprintf(buffer, "150 Opening %s mode data connection for ", (state->type == 'I') ? "BINARY" : "ASCII");ftpd_send_msg(state, buffer);ftpd_send_msg(state, state->cmdarg);size = f_size(state->fp);sprintf(buffer, " (%ld bytes).\r\n", size);ftpd_send_msg(state, buffer);return 1;
retrerr:ftpd_send_msg(state, "550 Failed to open file.\r\n");
retrerr2:if (state->fp != NULL){f_close(state->fp);state->fp = NULL;}if (fp != NULL){mem_free(fp);fp = NULL;}}else if (state->cmdstep == 1){state->flags |= FTPD_FLAG_AGAIN;ftpd_data_check(state);}else if (state->cmdstep == 2)ftpd_send_msg(state, "226 Transfer complete.\r\n");else if (state->cmdstep & (FTPD_CMDSTEP_CONNSHUTDOWN | FTPD_CMDSTEP_CONNABORTED))ftpd_send_msg(state, "451 Requested action aborted: local error in processing.\r\n");}else if (strcasecmp(state->cmd, "STOR") == 0){if (state->cmdstep == 0){ret = ftpd_fullpath(state, buffer, MAX_PATH, state->cmdarg, NULL);if (ret == -1)goto storerr;LWIP_ASSERT("state->fp == NULL", state->fp == NULL);fp = mem_malloc(sizeof(FIL));if (fp == NULL){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: mem_malloc(sizeof(FIL)) failed!\n", __FUNCTION__));goto storerr;}fr = f_open(fp, buffer, FA_CREATE_ALWAYS | FA_WRITE);if (fr != FR_OK){LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: f_open() failed! fr=%d, path=\"%s\"\n", __FUNCTION__, fr, buffer));goto storerr;}state->fp = fp; // 文件打开了之后才能赋给state->fpret = ftpd_prepare_data(state);if (ret == -1)goto storerr2;state->cmdstep = 1;state->flags |= FTPD_FLAG_AGAIN;ftpd_send_msg(state, "150 Ok to send data.\r\n");return 1;
storerr:ftpd_send_msg(state, "550 Failed to open file.\r\n");
storerr2:if (state->fp != NULL){f_close(state->fp);state->fp = NULL;}if (fp != NULL){mem_free(fp);fp = NULL;}}else if (state->cmdstep == 1){state->flags |= FTPD_FLAG_AGAIN;ftpd_data_check(state);}else if (state->cmdstep & FTPD_CMDSTEP_CONNSHUTDOWN)ftpd_send_msg(state, "226 Transfer complete.\r\n");else if (state->cmdstep & FTPD_CMDSTEP_CONNABORTED)ftpd_send_msg(state, "451 Requested action aborted: local error in processing.\r\n");}else if (strcasecmp(state->cmd, "DELE") == 0){ret = ftpd_fullpath(state, buffer, MAX_PATH, state->cmdarg, NULL);if (ret != -1){ret = f_unlink(buffer);if (ret == 0){ftpd_send_msg(state, "250 Delete operation successful.\r\n");return 1;}}ftpd_send_msg(state, "550 Delete operation failed.\r\n");}else if (strcasecmp(state->cmd, "RNFR") == 0){ret = ftpd_fullpath(state, state->rename, sizeof(state->rename), state->cmdarg, NULL);if (ret != -1){state->flags |= FTPD_FLAG_RENAME;ftpd_send_msg(state, "350 Ready for RNTO.\r\n");}else{state->flags &= ~FTPD_FLAG_RENAME;ftpd_send_msg(state, "550 RNFR command failed.\r\n");}}else if (strcasecmp(state->cmd, "RNTO") == 0){if ((state->flags & FTPD_FLAG_RENAME) == 0){ftpd_send_msg(state, "503 RNFR required first.\r\n");return 1;}state->flags &= ~FTPD_FLAG_RENAME;ret = ftpd_fullpath(state, buffer, MAX_PATH, state->cmdarg, NULL);if (ret != -1){ret = f_rename(state->rename, buffer);if (ret == 0){ftpd_send_msg(state, "250 Rename successful.\r\n");return 1;}}ftpd_send_msg(state, "550 Rename failed.\r\n");}elsereturn 0;return 1;
}/* 处理与服务器选项有关的命令 */
static int ftpd_process_opt_cmd(struct ftpd_state *state)
{if (strcasecmp(state->cmd, "opts") == 0){if (strcasecmp(state->cmdarg, "utf8 on") == 0)ftpd_send_msg(state, "200 Always in UTF8 mode.\r\n");elseftpd_send_msg(state, "501 Option not understood.\r\n");}else if (strcasecmp(state->cmd, "TYPE") == 0){if (strcasecmp(state->cmdarg, "A") == 0)ftpd_send_msg(state, "200 Switching to ASCII mode.\r\n");else if (strcasecmp(state->cmdarg, "I") == 0)ftpd_send_msg(state, "200 Switching to Binary mode.\r\n");else{ftpd_send_msg(state, "500 Unrecognised TYPE command.\r\n");return 1;}state->type = state->cmdarg[0];}else if (strcasecmp(state->cmd, "noop") == 0)ftpd_send_msg(state, "200 NOOP ok.\r\n");elsereturn 0;return 1;
}/* 处理与用户有关的命令 */
static int ftpd_process_user_cmd(struct ftpd_state *state)
{int userid;if (strcasecmp(state->cmd, "USER") == 0){if (state->userid != -1)ftpd_send_msg(state, "530 Can't change to another user.\r\n");else{ftpd_change_user(state, state->cmdarg);if (strcasecmp(state->cmdarg, "ANONYMOUS") == 0 && ftpd_is_valid_user(&state->user, NULL))ftpd_send_msg(state, "331 Anonymous access allowed, send identity (e-mail name) as password.\r\n");elseftpd_send_msg(state, "331 Please specify the password.\r\n");}}else if (strcasecmp(state->cmd, "PASS") == 0){if (state->userid != -1)ftpd_send_msg(state, "230 Already logged in.\r\n");else if (state->user.name == NULL)ftpd_send_msg(state, "503 Login with USER first.\r\n");else{state->user.password = ftpd_strdup(state->cmdarg);if (ftpd_is_valid_user(&state->user, &userid)){if (ftpd_file_exists(ftpd_users[userid].rootpath)){state->userid = userid;ftpd_send_msg(state, "230 Login successful.\r\n");}else{ftpd_change_user(state, NULL);ftpd_send_msg(state, "530 Please create the home directory \"");ftpd_send_msg(state, ftpd_users[userid].rootpath);ftpd_send_msg(state, "\" before logging in.\r\n");}}else{ftpd_change_user(state, NULL);ftpd_send_msg(state, "530 Login incorrect.\r\n");}}}else if (strcasecmp(state->cmd, "QUIT") == 0){ftpd_send_msg(state, "221 Goodbye.\r\n");state->flags |= FTPD_FLAG_SHUTDOWN;}else if (state->userid == -1)ftpd_send_msg(state, "530 Please login with USER and PASS.\r\n");elsereturn 0;return 1;
}static err_t ftpd_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{struct ftpd_state *state = arg;if (p != NULL){LWIP_DEBUGF(FTPD_DEBUG, ("%s: received %d bytes\n", __FUNCTION__, p->tot_len));if (state->queue == NULL)state->queue = p;elsepbuf_cat(state->queue, p);}else{if (state != NULL){LWIP_DEBUGF(FTPD_DEBUG, ("FTPD connection [%s]:%d is shutdown by the client!\n", ipaddr_ntoa(&tpcb->remote_ip), tpcb->remote_port));state->flags |= FTPD_FLAG_CLOSE;}else{LWIP_DEBUGF(FTPD_DEBUG, ("FTPD connection [%s]:%d is closed by the client!\n", ipaddr_ntoa(&tpcb->remote_ip), tpcb->remote_port));return ERR_OK;}}ftpd_process_cmd(state);return ERR_OK;
}/* 发送回应 */
static int ftpd_send_msg(struct ftpd_state *state, const char *s)
{err_t err;int len;if (state->flags & FTPD_FLAG_TCPERROR)return -1;len = strlen(s);LWIP_DEBUGF(FTPD_DEBUG, ("%s", s));LWIP_ASSERT("sndbuf >= len", tcp_sndbuf(state->ctrlconn) >= len);err = tcp_write(state->ctrlconn, s, len, TCP_WRITE_FLAG_COPY);if (err != ERR_OK){state->flags |= FTPD_FLAG_TCPERROR;LWIP_DEBUGF(FTPD_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("%s: tcp_write() failed! err=%d\n", __FUNCTION__, err));return -1;}state->sent += len;return len;
}static err_t ftpd_sent(void *arg, struct tcp_pcb *tpcb, u16_t len)
{struct ftpd_state *state = arg;LWIP_DEBUGF(FTPD_DEBUG, ("%s: %d bytes of response sent\n", __FUNCTION__, len));if (state != NULL){state->sent -= len;if (state->sent == 0){if (state->cmdlen != 0){LWIP_DEBUGF(FTPD_DEBUG, ("%s: processed %d bytes\n", __FUNCTION__, state->cmdlen));tcp_recved(state->ctrlconn, state->cmdlen);state->cmdlen = 0;}ftpd_process_cmd(state);}}return ERR_OK;
}/* 去除路径中的"./"和"../"以及"//" */
// path必须为绝对路径 (可以带盘符也可以不带盘符), 不允许为相对路径
// basepos是字符串中用户根目录末尾的斜杠的位置, 用于保证"../"在后退时不会退到用户根目录以外
// 比如"C:/foo/bar", 如果用户根目录是C:/foo, 那么basepos应该为6
int ftpd_simplify_path(char *path, int basepos)
{char *base, *p, *pp, *q;int len;// 检查path是否为绝对路径if (*path != '/'){p = strchr(path, '/');if (p == NULL || *(p - 1) != ':')return -1; // path不允许为相对路径}// 检查并修正basepos参数len = strlen(path);if (basepos < 0)basepos = 0;else if (basepos > len)basepos = len;base = path + basepos;if (*base != '/' && *base != '\0'){base = strchr(base, '/');if (base == NULL)base = path + len;basepos = base - path;}p = base; // 当前目录pp = base; // 父目录do{q = strchr(p + 1, '/');if (q != NULL){len = q - p;if (len == 1 || (len == 2 && memcmp(p, "/.", 2) == 0))memmove(p, q, strlen(q) + 1);else if (len == 3 && memcmp(p, "/..", 3) == 0){memmove(pp, q, strlen(q) + 1);p = pp;pp = ftpd_memrchr(base, '/', pp - base);if (pp == NULL)pp = p;}else{pp = p;p = q;}}else{len = strlen(p);if (len == 1 || (len == 2 && memcmp(p, "/.", 2) == 0)){if (p == path || *(p - 1) == ':')p++;*p = '\0';}else if (len == 3 && memcmp(p, "/..", 3) == 0){if (pp == path || *(pp - 1) == ':')pp++;*pp = '\0';}}} while (q != NULL);return 0;
}/* 开辟一块内存空间, 用于长期保存局部变量中的字符串, 避免函数退出时局部变量失效 */
char *ftpd_strdup(const char *s)
{char *p;int len;len = strlen(s) + 1;p = mem_malloc(len);if (p != NULL)memcpy(p, s, len);return p;
}

【程序】在STM32单片机上用1700行代码实现基于LwIP 2.1.2协议栈raw API和FatFs文件系统的FTP服务器(20200703版)相关推荐

  1. 【程序】在STM32单片机上用1700行代码实现基于LwIP 2.1.2协议栈raw API和FatFs文件系统的FTP服务器(20230315版)

    [更新记录] 本程序基于20200703版的程序,作出了如下更新: 1. 解决了当accept函数的参数err!=ERR_OK时,程序出现HardFault错误的bug. 2. 当lwip MEM_S ...

  2. 【程序】在STM32单片机上实现基于LwIP 2.1.3协议栈raw API的DHCP服务器,为其他设备分配IPv4地址(20220122版)

    本程序是参考了udhcp-0.9.8后编写的. 使用方法: ip4_addr_t ip4addr, netmask, gw; struct dhcpd_config dhcpd; struct net ...

  3. 【程序】STM32H743ZI单片机驱动DP83848以太网PHY芯片,移植lwip 2.1.3协议栈,并加入网线热插拔检测的功能

    STM32H7的ETH HAL库封装得比较好,真正做到了完全用HAL API函数操作,不需要再用寄存器操作. 而STM32F1的ETH HAL库就没有完全封装,有些操作必须要用寄存器完成,而且还需要单 ...

  4. 【python】一个目录里面多个python程序文件,统计一下里面有多少行代码。即分别列出:代码、空行、注释的行数。

    一个目录里面多个python程序文件,统计一下里面有多少行代码.即分别列出:代码.空行.注释的行数. 题目 代码 结果 题目 一个目录里面多个python程序文件,统计一下里面有多少行代码.即分别列出 ...

  5. 社区说 | Wechaty: 6 行代码构建基于个人微信和Whatsapp的对话式人机交互界面应用

    [活动时间]7月22日(本周四)20:00 pm-21:00 pm [活动日程] - 20:00-20:45 Wechaty: 6 行代码构建基于个人微信和 Whatsapp 的对话式人机交互界面应用 ...

  6. 我在STM32单片机上跑神经网络算法

    01  前言 为什么可以在STM32上面跑神经网络? 简而言之就是使用STM32CubeMX中的X-Cube-AI扩展包将当前比较热门的AI框架进行C代码的转化,以支持在嵌入式设备上使用. 目前使用X ...

  7. 在STM32单片机上跑神经网络算法

    摘要:为什么可以在STM32上面跑神经网络?简而言之就是使用STM32CubeMX中的X-Cube-AI扩展包将当前比较热门的AI框架进行C代码的转化,以支持在嵌入式设备上使用,目前使用X-Cube- ...

  8. POWERLINK协议源码(最新)在stm32单片机上的移植指南

    最近着了powerlink的道,连续几晚十二点前没睡过觉.不得不说兴趣这东西劲太大了,让人睡不着.喜欢上研究POWERLINK,最新版的源码结构挺清晰的,移植并测试了嵌入式linux作为从站和电脑主站 ...

  9. [MATLAB学习]:Matlab生成滑动平均滤波算法文件并移植到STM32单片机上运行——基于CubeMX

    前言 人生如逆旅,我亦是行人. 今天分享一个在 MATLAB 上生成C算法文件,并将其移植到 keil5上,运行至 STM32 单片机,一个很有用的方法. 准备工作: 已安装 MATLAB 的软件(注 ...

最新文章

  1. 在NVIDIA A100 GPU中使用DALI和新的硬件JPEG解码器快速加载数据
  2. 学python可以做什么知乎-学会python有哪些好处?python抓取知乎神回复
  3. 美国波特兰市禁用人脸识别,被违规监控可获赔1000美元
  4. c语言书籍elf文件,扒一扒ELF文件
  5. java之IO流(一)
  6. Pixhawk-串级pid介绍
  7. React开发(282):公共组件可以提升一下文件层级
  8. Spring框架----Spring的bean的作用范围
  9. Mysql存中文字符出错:Incorrect string value: '\xC2\xE9\xD7\xED\解决方法
  10. mysql安装手册(2)
  11. SpringCloud+Seata+nacos案例(包含源码 Seata及nacos安装教程)
  12. 【车标识别】基于SIFT算子的车标识别算法matlab仿真
  13. QQ影音播放器 for Mac
  14. 感应(异步)电机磁场定向控制速度环PI控制参数设计
  15. 云服务器怎么安装声音驱动_GPU云服务器如何安装NVIDIA Tesla驱动
  16. 【HTML 教程系列第 9 篇】什么是 HTML 中的换行标签 br
  17. 干货!量子技术入门、进阶、行业专家观点、最新资讯!1000篇好文帮你揭开量子技术神秘面纱!...
  18. 李宏毅2023春季机器学习笔记 - 01生成AI(ChatGPT)
  19. Android使用MediaCodec进行视频编码 视频的一些基础概念介绍
  20. 春节流量争夺战:互联网巨头跪求你收红包

热门文章

  1. [学习笔记] CDQ分治 从感性理解到彻底晕菜
  2. 通信端口感叹号_PCI简易通讯控制器有黄色感叹号怎么办?
  3. 综合指标评价及绩效评价方法
  4. 如何对多个if-else判断进行优化
  5. CPU 上下文切换是什么意思?(下)
  6. 可视化智能预警为生产运行保驾护航
  7. def python_multi(n)_Python:MultiPing.py
  8. Generative Adversarial Text to Image Synthesis 论文翻译精校版
  9. 2019前端工程师的自检清单
  10. 杰理之MIC 省电 容方案 微信语音 或通话 时前面 几秒钟有 哒哒声【篇】