当前位置:   article > 正文

基于FreeRTOS的ESP32环境监测系统:利用Arduino框架实现SD卡数据记录与FTP服务器集成(一)_arduino esp32 ftp服务器

arduino esp32 ftp服务器

本文档深入剖析一段C++代码,该代码运用FreeRTOS实时操作系统结合Arduino框架,构建了一个高效稳定的ESP32环境监测系统。系统不仅实时采集温度和湿度数据,还将这些数据记录到SD卡中,并通过FTP服务器实现远程访问。以下对代码中体现的FreeRTOS特性及与Arduino框架的融合进行详细解读。
本文所需要的硬件与我的另一篇文章完全一样,请参照ESP32环境下基于SD卡与FTP实现温湿度数据采集与存储
在这里插入图片描述

一、库导入与常量定义

代码导入了FreeRTOS相关库(如freertos/timers.hfreertos/queue.h, 这两个库是ESP32自带的,如果你已经在IDE配置好了ESP32,不用单独安装),与Arduino库(如Arduino.hSPI.hDHT.hWiFi.h等)一同支持ESP32的多任务并发执行及硬件设备交互。常量定义涵盖了Wi-Fi网络配置、FTP登录信息、传感器引脚映射、NTP服务器设置等,为系统运行提供必需的基础配置。

二、全局变量与对象

全局变量和对象中,除了与具体功能相关的DHTFTPServer对象外,还引入了FreeRTOS的互斥信号量sdCardMutexenvironmentDataMutex,以实现对共享资源(如SD卡访问和环境数据更新)的线程安全控制。此外,定义了用于存储环境数据的CSVRecordData结构体和临时存储CSV文件内容的LineBuffer结构体。

三、辅助函数声明

声明的辅助函数充分利用了FreeRTOS提供的线程同步机制,如:

  • printLocalTime():获取并格式化本地时间字符串。
  • handleWiFiConnection():采用线程安全的方式建立Wi-Fi连接,包括重试逻辑。
  • initializeSPIAndSD():初始化SPI接口与SD卡。
  • initializeSystem():初始化整个系统,包括DHT传感器、互斥信号量及SD卡,遵循FreeRTOS编程规范。
  • setupTasks():利用FreeRTOS任务调度功能创建并启动环境数据采集、CSV记录写入和文件读取任务。
  • handleFTP():设置FTP服务器参数并启动服务,与主任务并发运行。
  • syncTime():借助系统定时器同步系统时间至指定NTP服务器。

四、主函数setup()与循环函数loop()

setup() 函数作为系统初始化入口,遵循Arduino编程风格,执行以下操作:

  1. 初始化串口通信。
  2. 调用initializeSystem()进行系统整体初始化,整合FreeRTOS特性。
  3. 使用FreeRTOS兼容方式连接Wi-Fi网络,打印连接成功后的IP地址。
  4. 创建并启动各FreeRTOS任务,实现多任务并发处理。
  5. 设置并启动FTP服务器,作为独立任务运行。
  6. 使用定时器同步系统时间至NTP服务器,确保时间准确性。

loop() 函数遵循Arduino框架,仅负责处理FTP请求并监控Wi-Fi连接状态。若Wi-Fi连接断开,触发handleWiFiConnection()重新连接,保持网络连通性。

五、核心功能函数与FreeRTOS集成

1. initializeSPIAndSD()

初始化SPI总线(指定SCK、MISO、MOSI和CS引脚)并尝试连接SD卡。遵循标准Arduino流程,同时确保与FreeRTOS任务调度的兼容性。连接失败时输出错误信息,成功则输出连接成功的提示。

2. handleWiFiConnection()

在FreeRTOS环境下尝试连接指定Wi-Fi网络,实现线程安全的重试逻辑。每次尝试之间插入适当延时,并通过.字符输出连接进度。连接成功后,打印Wi-Fi连接状态和本地IP地址,遵循Arduino的简洁反馈原则。

3. setupTasks()

利用FreeRTOS的xTaskCreate()函数创建并启动三个任务:

  • EnvironmentTask:作为实时任务周期性采集环境数据(湿度和温度),利用FreeRTOS任务优先级确保数据采集的实时性。
  • WriteCSVRecordTask:定期检查是否有有效环境数据,若有则通过互斥信号量保护机制将其按照CSV格式写入指定文件(CSV_FILE_PATH)。任务调度确保数据写入与采集任务互不干扰。
  • ReadFileTask:同样作为FreeRTOS任务,定期读取CSV文件内容,通过互斥信号量保护暂存并打印最后MAX_LINES行数据,与其它任务并发执行,提高系统效率。

4. WriteCSVRecordTask()

此FreeRTOS任务在每个周期内检查环境数据的有效性。若数据有效,获取当前本地时间并格式化为字符串,然后锁定SD卡和环境数据互斥信号量,使用FreeRTOS兼容方式打开CSV文件进行追加写入。写入格式为“时间戳,湿度值,温度值”。完成写入后释放互斥信号量,关闭文件,确保操作的原子性。

5. ReadFileTask()

作为FreeRTOS任务,此函数每30秒读取一次CSV文件。锁定SD卡互斥信号量后,按照Arduino文件操作习惯打开文件并逐行读取,将最后MAX_LINES行数据暂存在LineBuffer结构体中。随后释放互斥信号量,打印暂存的行数据,与其它任务并发执行,避免阻塞系统运行。

6. syncTime()

通过配置FreeRTOS定时器,定时触发系统时间与指定NTP服务器同步,确保系统内部时间的准确性和实时性。

总结而言,本代码充分利用FreeRTOS实时操作系统的优势,结合Arduino编程框架的简洁易用性,构建了一套高效、稳定且功能完备的ESP32环境监测系统。系统不仅实现了温度和湿度数据的实时采集、SD卡数据记录,还通过FTP服务器实现了远程访问,所有功能均在FreeRTOS任务调度下并发执行,确保了系统的高响应速度与任务间互不干扰的并发处理能力。

六、源代码与接线方式如下

6.1 源代码

#include <Arduino.h>
/*
  sd Card Interface code for ESP32
  SPI Pins of ESP32 sd card as follows:
  CS    = 23;
  MOSI  = 17;
  MISO  = 2;
  SCK   = 16;
*/

#include <SPI.h>
#include <DHT.h>
#include <WiFi.h>
#include <time.h>
#include <ESP-FTP-Server-Lib.h>
#include <SD.h>
#include <FS.h>
#include <freertos/timers.h>
#include <freertos/queue.h>
#include <string>

// 常量定义
const char *const SSID = "xxx";
const char *const PASSWORD = "xxxxxxxx";
const int MAX_RETRY_COUNT = 5;
int retryCount = 0;
const char *const FTP_USER = "ftp";
const char *const FTP_PASSWORD = "ftp";
const int DHTPIN = 13;
const int DHTTYPE = DHT22;
const int CS = 23;
const int MOSI_PIN = 17;
const int MISO_PIN = 2;
const int SCK_PIN = 16;
const char *ntpServer = "ntp1.aliyun.com";
const long gmtOffset_sec = 8 * 3600;
const int daylightOffset_sec = 0;
const char *const CSV_FILE_PATH = "/record0001.csv";
const size_t MAX_LINES = 3;
const size_t MAX_LINE_LENGTH = 1024;

// 全局变量和对象
DHT dht(DHTPIN, DHTTYPE);
FTPServer ftp;
SPIClass CustomSPI;
SemaphoreHandle_t sdCardMutex;
SemaphoreHandle_t environmentDataMutex;

// 数据结构:CSV记录
typedef struct
{
  float humidity;
  float temperature;
} CSVRecordData;
// 初始化CSV记录
CSVRecordData record;

// 用于存储固定数量的字符串行
struct LineBuffer
{
  std::string lines[MAX_LINES]; // 用于存储字符串行的数组,MAX_LINES定义了最大行数。
  size_t currentLineIndex = 0; // 当前操作的行索引,默认为0,表示数组的起始位置。
};



// 辅助函数声明
String printLocalTime();
void handleWiFiConnection();
void initializeSPIAndSD();
void initializeSystem();
void setupTasks();
void EnvironmentTask(void *pvParameters);
void WriteCSVRecordTask(void *pvParameters);
void ReadFileTask(void *pvParameters);
void handleFTP();
void syncTime();

void setup()
{

  Serial.begin(9600);
  delay(500);
  initializeSystem();
  Serial.println("System initialization complete");
  handleWiFiConnection();
  Serial.println("WiFi connected successfully!");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  Serial.println("WiFi connection established");
  setupTasks();
  Serial.println("Task initialization complete");
  handleFTP();
  Serial.println("FTP server initialization complete");
  syncTime();
  delay(500);
  Serial.println("Time synchronization completed");
}

void loop()
{
  ftp.handle(); // 处理FTP请求
  // 检查WiFi连接状态,未连接则尝试重新连接
  if (WiFi.status() != WL_CONNECTED)
  {
    handleWiFiConnection();
  }
}

void initializeSPIAndSD()
{
  CustomSPI.begin(SCK_PIN, MISO_PIN, MOSI_PIN, CS);
  delay(500);
  while (!Serial)
  {
    ;
  }

  Serial.println("Initializing sd card...");

  if (!SD.begin(CS, CustomSPI, 1000000))
  {
    Serial.println("initialization failed!");
    return;
  }
  else
  {
    Serial.println("SD card initialization succeeded.");
  }
}
void initializeSystem()
{
  dht.begin();
  sdCardMutex = xSemaphoreCreateMutex(); // 创建互斥锁以用于SD卡访问控制
  environmentDataMutex = xSemaphoreCreateMutex(); // 创建互斥锁以用于环境数据访问控制
  initializeSPIAndSD(); // 初始化SPI和SD卡
}

void handleWiFiConnection()
{

  Serial.println("Connecting to Wi-Fi...");
  WiFi.begin(SSID, PASSWORD);
  delay(1000);
  while (WiFi.status() != WL_CONNECTED && retryCount < 5)
  {
    delay(500);
    Serial.print(".");
    WiFi.begin(SSID, PASSWORD);
    delay(1000);
    retryCount++;
  }
}

void setupTasks()
{
  // 创建名为"EnvironmentTask"的任务,堆栈大小为2048*4字节,优先级为1,不传递参数。
  xTaskCreate(&EnvironmentTask, "EnvironmentTask", 2048 * 4, NULL, 1, NULL);
  delay(500);
  // 创建名为"WriteCSVRecordTask"的任务,堆栈大小为2048*4字节,优先级为2,不传递参数。
  xTaskCreate(&WriteCSVRecordTask, "WriteCSVRecordTask", 2048 * 4, NULL, 2, NULL);
  delay(500);
  // 创建名为"ReadFileTask"的任务,堆栈大小为2048*4字节,优先级为3,不传递参数。
  xTaskCreate(&ReadFileTask, "ReadFileTask", 2048 * 4, NULL, 3, NULL);
}

void WriteCSVRecordTask(void *pvParameters)
{
  File currentFile;
  const char *currentFilePath = CSV_FILE_PATH;
  uint32_t fileSizeThreshold = 10 * 1024; // 1 kB threshold

  while (1)
  {
    // 判断温湿度是否有效
    if (record.humidity != NAN && record.temperature != NAN)
    {
      String localTimeStr = printLocalTime();
      char timeStr[30];
      strcpy(timeStr, localTimeStr.c_str());
      xSemaphoreTake(sdCardMutex, portMAX_DELAY);
      xSemaphoreTake(environmentDataMutex, portMAX_DELAY);

      currentFile = SD.open(currentFilePath, FILE_APPEND);

      if (!currentFile)
      {
        Serial.print("Error opening initial file: ");
        Serial.println(CSV_FILE_PATH);
      }
      else
      {
        Serial.printf("Writing to %s ", currentFilePath);
        currentFile.print(timeStr);
        currentFile.print(",");
        currentFile.print(record.humidity, 2);
        currentFile.print(",");
        currentFile.println(record.temperature, 2);
        currentFile.close();
        Serial.println("completed.");
      }

      xSemaphoreGive(sdCardMutex);
      xSemaphoreGive(environmentDataMutex);
    }
    vTaskDelay(pdMS_TO_TICKS(6000));
  }
}

void ReadFileTask(void *pvParameters)
{
  LineBuffer buffer;

  while (1)
  {
    xSemaphoreTake(sdCardMutex, portMAX_DELAY);
    File currentFile = SD.open(CSV_FILE_PATH);

    if (currentFile)
    {
      Serial.printf("Reading file from %s\n", CSV_FILE_PATH);

      std::string currentLine;
      size_t lineCount = 0;

      while (currentFile.available())
      {
        currentLine = std::string(currentFile.readStringUntil('\n').c_str());
        buffer.lines[buffer.currentLineIndex++] = currentLine;
        buffer.currentLineIndex %= MAX_LINES; // 确保索引在范围内
        lineCount++;
      }

      currentFile.close();
      xSemaphoreGive(sdCardMutex);

      if (lineCount >= MAX_LINES)
      {
        for (size_t i = 0; i < MAX_LINES; ++i)
        {
          Serial.println(buffer.lines[(buffer.currentLineIndex - i + MAX_LINES) % MAX_LINES].c_str()); // Cycle print last MAX_LINES rows
        }
      }
      else
      {
        for (size_t i = 0; i < lineCount; ++i)
        {
          Serial.println(buffer.lines[i % MAX_LINES].c_str()); // Print all content
        }
      }

      Serial.println("Read succeeded");
    }
    else
    {
      char error_msg[64];
      sprintf(error_msg, "error opening %s", CSV_FILE_PATH);
      Serial.println(error_msg);
    }
    vTaskDelay(pdMS_TO_TICKS(30000));
  }
}

// 环境数据采集定时器回调函数
void EnvironmentTask(void *pvParameters)
{
  while (1)
  {
    xSemaphoreTake(environmentDataMutex, portMAX_DELAY);
    if (!isnan(record.humidity) || !isnan(record.temperature))
    {
      record.humidity = dht.readHumidity();
      record.temperature = dht.readTemperature();
    }
    xSemaphoreGive(environmentDataMutex);
    vTaskDelay(pdMS_TO_TICKS(3000));
  }
}

void syncTime()
{
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
}

String printLocalTime()
{
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo))
  {
    Serial.println("Failed to obtain time");
    return "";
  }

  char formattedTimeBuffer[30];                                                                // 为格式化的时间字符串分配足够的缓冲区
  strftime(formattedTimeBuffer, sizeof(formattedTimeBuffer), "%Y-%B-%d  %H:%M:%S", &timeinfo); // 使用 strftime 将 timeinfo 转换为格式化的字符串

  String formattedTime = formattedTimeBuffer; // 将格式化后的字符串复制到 String 对象中
  return formattedTime;
}

void handleFTP()
{
  ftp.addUser(FTP_USER, FTP_PASSWORD); // 添加FTP用户

  ftp.addFilesystem("SD", &SD); // 注册SD卡文件系统

  ftp.begin(); // 启动FTP服务
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308

6.2 如果你是使用的PlatfromIO IDE可以把以下内容复制到你的platformio.ini里

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 9600
lib_deps = 
	adafruit/DHT sensor library@^1.4.6
	adafruit/Adafruit Unified Sensor@^1.1.14
	peterus/ESP-FTP-Server-Lib@^0.14.0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

七、FTP命令行操作

7.1 步骤说明

  1. 打开命令提示符(Windows)或终端(macOS/Linux)。
  2. 输入以下命令连接到 FTP 服务器(请将 IP 地址替换为实际 FTP 服务器的 IP 地址)
  3. 输入用户名(默认为 “ftp”)和密码(默认为 “ftp”)。
  4. 输入 “ls” 或 “dir” 查看文件列表。
  5. 输入 “cd” 命令切换目录,例如 “cd /SD” 进入 “/SD” 目录。
  6. 输入 “lcd” 命令切换本地目录,例如 “lcd E:\temp” 进入 “E:\temp” 目录。
  7. 使用 “get” 或 “recv” 命令从 FTP 服务器下载文件,例如 “get record.csv” 下载 “record.csv” 文件。
  8. 使用 “put” 或 “send” 命令将文件上传到 FTP 服务器,例如 “put local_file.txt” 上传 “local_file.txt” 文件。
  9. 输入 “bye” 或 “quit” 命令退出 FTP 客户端。
  10. 更多命令直接输入"help"来了解

7.2我用如下步骤下载了SD卡中的record.csv到电脑上:

C:\Users\Firmin>ftp 192.168.3.39
连接到 192.168.3.39。
220--- Welcome to FTP Server for ESP32 ---
220---        By Peter Buchegger       ---
220 --           Version 0.1           ---
200 Ok
用户(192.168.3.39:(none)): ftp
331 OK. Password required
密码:
230 OK.
ftp> cd SD
250 Ok. Current directory is /SD
ftp> dir /B
200 PORT command successful
150 Accepted data connection
drwxr-xr-x 1 owner group             0 Jan 01  1970 System Volume Information
-rw-r--r-- 1 owner group          4543 Jan 01  1970 record2.csv
-rw-r--r-- 1 owner group         14349 Jan 01  1970 record.csv
-rw-r--r-- 1 owner group         72469 Jan 01  1970 record0001.csv
226 4 matches total
ftp: 收到 279 字节,用时 0.0146.50千字节/秒。
ftp> lcd D:\temp
目前的本地目录 D:\temp。
ftp> recv record0001.csv
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/天景科技苑/article/detail/839647
推荐阅读
相关标签
  

闽ICP备14008679号