[APM32F4] 【技术分享】APM32F4实现用U盘记录LOG信息

[复制链接]
 楼主| susutata 发表于 2023-11-23 14:53 | 显示全部楼层 |阅读模式
本帖最后由 susutata 于 2023-11-23 14:55 编辑

#申请原创# @21小跑堂

APM32F4实现用U盘记录LOG信息

# 01 前言
MCU 组成的系统在实际应用中,经常需要记录系统 LOG 信息,可以是系统不同任务执行情况的 LOG 信息,也可以是内核寄存器等便于维护调试的信息,或者是传感器的信息等。
上述的这些 LOG 信息,如果在能联网的系统中,那么直接传输回服务器即可,但如果是离线的系统,那么就需要一个存储设备来记录这些 LOG 信息。一般有以下几种方式:
- 记录到 Nor  Flash 等板载存储器
- 记录到可移动的存储器,比如 U 盘等存储设备

本文提供后一种方式,即数据记录到 U 盘,这种方式方便进行现场维护,但实际应用中,可以结合 Nor Flash 加 U 盘的方式,避免读写 U 盘频率过快导致损坏。以下内容利用 APM32F407xx 的 OTG Host 和 Fatfs 文件系统,加上 RTC 功能来实现数据记录,包括以下功能。

- 按日期自动创建文件夹存放 `LOG.xls` 文件
- 按 1 秒的记录频率往 `LOG.xls` 文件写入带时间戳的数据
- 在 U 盘存储空间使用超过 90%时,删除日期最早的文件夹及 LOG 文件

# 02 外设和组件配置
下面简单介绍一下所用到的外设和组件的配置。

## OTG Host
OTG Host 的配置比较简单,配置为普通 FS 速度和 MSC Class 类即可。
  1. void USB_HostInitalize(void)
  2. {
  3.     /* USB host and class init */
  4.     USBH_Init(&gUsbHostFS, USBH_SPEED_FS, &USBH_MSC_CLASS, USB_HostUserHandler);
  5. }
复制代码
U 盘在枚举完成后,需要获取 `LUN` 的信息,以便后续对特定 `LUN` 进行命令和数据读写等操作。
Pasted image 20231122174402.png

有关 MSC Class 的处理在 `usbh_msc.c/h` 文件中的 `USBH_MSC_CLASS` 结构体句柄,其包含以下处理函数:
- `USBH_MSC_ClassInitHandler`:Class 初始化函数。
- `USBH_MSC_ClassDeInitHandler`: Class 解初始化函数。
- `USBH_MSC_ClassReqHandler`: Class 特定请求处理函数,用于处理 `MASS_STORAGE_RESET`、`GET_MAX_LUN` 等特定请求。
- `USBH_MSC_CoreHandler`: Class 内核处理函数,包括 BOT 命令、SCSI 命令的处理。
- `USBH_MSC_SOFHandler`:SOF 事件处理函数。
  1. /* MSC class handler */
  2. USBH_CLASS_T USBH_MSC_CLASS =
  3. {
  4.     "Class MSC",
  5.     USBH_CLASS_MSC,
  6.     NULL,
  7.     USBH_MSC_ClassInitHandler,
  8.     USBH_MSC_ClassDeInitHandler,
  9.     USBH_MSC_ClassReqHandler,
  10.     USBH_MSC_CoreHandler,
  11.     USBH_MSC_SOFHandler,
  12. };
复制代码
下面的 `USBH_MSC_Handler` 状态机由 `USBH_MSC_CoreHandler` 函数调用,包含常见的 BOT 命令、SCSI 命令的处理。
  1. /* USB host MSC state handler function */
  2. USBH_MscStateHandler_T USBH_MSC_Handler[] =
  3. {
  4.     USBH_MSC_InitHandler,
  5.     USBH_MSC_IdleHandler,
  6.     USBH_MSC_InquiryHandler,
  7.     USBH_MSC_TestUnitReadyHandler,
  8.     USBH_MSC_RequestSenseHandler,
  9.     USBH_MSC_ReadCapacityHandler,
  10.     USBH_MSC_ErrorUnrecoveredHandler,
  11.     USBH_MSC_ReadHandler,
  12.     USBH_MSC_WriteHandler,
  13.     USBH_MSC_RWRequestSenseHandler,
  14. };
复制代码
BOT SCSI 命令处理。
Pasted image 20231122174742.png

OTG Host 部分还需要了解以下 API 函数,因为后续需要应用到 FatFs 文件系统中。
- `USBH_MSC_ReadDevInfo()`:用于获取所枚举成功的 `MSC` 设备信息,比如 `LUN` 逻辑单元是否准备完毕、多媒体设备是否存在、扇区数、block 数等。
- `USBH_MSC_DevStatus()`:获取 `LUN` 是否准备就绪的状态。
- `USBH_MSC_ReadDevWP()`:获取 `LUN` 写保护状态。
- `USBH_MSC_DevRead()`:读取 `LUN` 数据。
- `USBH_MSC_DevWrite()`:往 `LUN` 写入数据。
  1. USBH_STA_T USBH_MSC_ReadDevInfo(USBH_INFO_T* usbInfo, uint8_t lun, USBH_MSC_STORAGE_INFO_T* device);
  2. uint8_t USBH_MSC_DevStatus(USBH_INFO_T* usbInfo, uint8_t lun);
  3. uint8_t USBH_MSC_ReadDevWP(USBH_INFO_T* usbInfo, uint8_t lun);
  4. USBH_STA_T USBH_MSC_DevRead(USBH_INFO_T* usbInfo, uint8_t lun, uint32_t address, \
  5.                             uint8_t* buffer, uint16_t cnt);
  6. USBH_STA_T USBH_MSC_DevWrite(USBH_INFO_T* usbInfo, uint8_t lun, uint32_t address, \
  7.                              uint8_t* buffer, uint16_t cnt);
复制代码

## RTC
APM32F4xx 的 RTC 是一个独立的 BCD 定时计数器,提供完整的日历时钟功能。不像 F1 系列的 RTC 只是个计数器,需要自行设定元年,然后换算日期和时间。RTC 的配置比较简单,配置时钟源为 LSE,24 小时制即可,然后利用 RTC 的备份寄存器来判断是否需要配置日历和时间。
  1. void RTC_CalendarConfig(void)
  2. {
  3.     RTC_DateTypeDef Date_Structure;
  4.     RTC_TimeTypeDef Time_Structure;

  5.     /* Configure the Date */
  6.     Date_Structure.Year = 0x23;
  7.     Date_Structure.Month = RTC_MONTH_NOVEMBER;
  8.     Date_Structure.Date = 0x22;
  9.     Date_Structure.WeekDay = RTC_WEEKDAY_MONDAY;

  10.     if(DAL_RTC_SetDate(&hrtc,&Date_Structure,RTC_FORMAT_BCD) != DAL_OK)
  11.     {
  12.         DAL_ErrorHandler();
  13.     }

  14.     /* Configure the Time */
  15.     Time_Structure.Hours = 0x00;
  16.     Time_Structure.Minutes = 0x00;
  17.     Time_Structure.Seconds = 0x00;
  18.     Time_Structure.TimeFormat = RTC_HOURFORMAT12_AM;
  19.     Time_Structure.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
  20.     Time_Structure.StoreOperation = RTC_STOREOPERATION_RESET;

  21.     if(DAL_RTC_SetTime(&hrtc,&Time_Structure,RTC_FORMAT_BCD) != DAL_OK)
  22.     {
  23.         DAL_ErrorHandler();
  24.     }

  25.     DAL_RTCEx_BKUPWrite(&hrtc,RTC_BKP_DR0, RTC_BKP_VALUE);
  26. }
复制代码

## TMR
APM32F4xx 系列的 RTC 没有秒中断,这里我们实现一秒记录一次数据到 U 盘,则开启一个定时为 1 秒的定时器中断来实现。
  1. void DAL_TMR3_Config(void)
  2. {
  3.     htmr3.Instance = TMR3;

  4.     htmr3.Init.Period = 10000 - 1;
  5.     htmr3.Init.Prescaler = 8400 - 1;
  6.     htmr3.Init.ClockDivision = 0;
  7.     htmr3.Init.CounterMode = TMR_COUNTERMODE_UP;
  8.     htmr3.Init.AutoReloadPreload = TMR_AUTORELOAD_PRELOAD_DISABLE;
  9.     if(DAL_TMR_Base_Init(&htmr3) != DAL_OK)
  10.     {
  11.        DAL_ErrorHandler();
  12.     }
  13. }
复制代码

## FatFs
使用 FatFs 文件系统前,要重新实现 `diskio.c` 及配置 `ffconf.h` 文件。

其中在 `diskio.c` 文件中,要利用 OTG Host 章节最后提到的 `MSC Class` 的 API ,重新实现以下函数。
`DSTATUS disk_status()`
`DSTATUS disk_initialize()`
`DRESULT disk_read()`
`DRESULT disk_write()
`DRESULT disk_ioctl()`

另外,简单介绍下后面应用代码中用到的几个 API,详细的使用方法,大家可以参考 FatFs 官方文档。

### f_opendir
该 API 用于打开一个已存在的目录文件夹。
  1. FRESULT f_opendir (
  2.   DIR* dp,           /* [OUT] Pointer to the directory object structure */
  3.   const TCHAR* path  /* [IN] Directory name */
  4. );
复制代码

### f_readdir
该 API 用于读取目录项,后续用于筛选所有的有效目录。
  1. FRESULT f_readdir (
  2.   DIR* dp,      /* [IN] Directory object */
  3.   FILINFO* fno  /* [OUT] File information structure */
  4. );
复制代码

### f_mkdir
该 API 用于创建一个新的目录文件夹。
  1. FRESULT f_mkdir (
  2.   const TCHAR* path /* [IN] Directory name */
  3. );
复制代码

### f_open
该 API 用于打开一个文件。要注意的是参数 `mode flags` 需要使用 `FA_OPEN_ALWAYS`,避免文件内容被覆盖。
  1. FRESULT f_open (
  2.   FIL* fp,           /* [OUT] Pointer to the file object structure */
  3.   const TCHAR* path, /* [IN] File name */
  4.   BYTE mode          /* [IN] Mode flags */
  5. );
复制代码

### f_unlink
该 API 用于删除文件或子目录。
  1. FRESULT f_unlink (
  2.   const TCHAR* path  /* [IN] Object name */
  3. );
复制代码

### f_close
该 API 用于关闭一个文件。
  1. FRESULT f_close (
  2.   FIL* fp     /* [IN] Pointer to the file object */
  3. );
复制代码

### f_lseek
该 API 用于移动被打开的文件对象的文件读或写指针,后面应用时,需要用来将写指针移动到文件末,以添加新的 LOG 信息。
  1. FRESULT f_lseek (
  2.   FIL*    fp,  /* [IN] File object */
  3.   FSIZE_t ofs  /* [IN] Offset of file read/write pointer to be set */
  4. );
复制代码

### f_printf
该 API 用于将格式化的字符串写入文件。使用该 API,我们可以方便的将各种数据类型,转换为字符串,再写入文件中。
  1. int f_printf (
  2.   FIL* fp,          /* [IN] File object */
  3.   const TCHAR* fmt, /* [IN] Format stirng */
  4.   ...
  5. );
复制代码

### f_getfree
该 API 用于获取 volume 中的剩余空间。使用该 API,我们可以知道 U 盘剩余空间是多少,从而对老旧的 LOG 数据进行处理。
  1. FRESULT f_getfree (
  2.   const TCHAR* path,  /* [IN] Logical drive number */
  3.   DWORD* nclst,       /* [OUT] Number of free clusters */
  4.   FATFS** fatfs       /* [OUT] Corresponding filesystem object */
  5. );
复制代码

# 03 实现数据记录
## 定义参数结构体
定义 `rtcInfo` 和 `diskInfo` 来记录应用过程中的信息。
  1. /* 记录 RTC 时间信息 */
  2. typedef struct
  3. {
  4.     uint16_t year;
  5.     uint8_t month;
  6.     uint8_t day;
  7.     uint8_t hour;
  8.     uint8_t minute;
  9.     uint8_t second;
  10. } RTC_TIME_INFO_T;

  11. /* 记录 U 盘和文件系统信息 */
  12. typedef struct
  13. {
  14.     char dirPath[512];
  15.     uint16_t dirNum;
  16.     uint32_t totSect;
  17.     uint32_t freSect;
  18.     double usedPercent;
  19. } DISK_INFO_T;

  20. RTC_TIME_INFO_T rtcInfo;
  21. DISK_INFO_T diskInfo;
复制代码

## 获取时间参数
在 TMR3 回调函数中获取 RTC 当前时间,并使能数据更新标志。
  1. void RTC_Application(void)
  2. {
  3.     char strBuf[50];

  4.     RTC_GetCalendar(&rtcInfo.year, &rtcInfo.month, &rtcInfo.day, &rtcInfo.hour, &rtcInfo.minute, &rtcInfo.second);

  5.     /* Display time */
  6.     sprintf(strBuf, "20%02d-%02d-%02d %02d:%02d:%02d", rtcInfo.year, rtcInfo.month, rtcInfo.day, rtcInfo.hour, rtcInfo.minute, rtcInfo.second);
  7.     DAL_LOGI(tag, "%s", strBuf);
  8. }

  9. void DAL_TMR_PeriodElapsedCallback(TMR_HandleTypeDef *htmr)
  10. {
  11.     if(htmr->Instance == TMR3)
  12.     {
  13.         RTC_Application();
  14.         dataUpdate = 1;
  15.     }
  16. }
复制代码

## 记录数据
### 提供日期和时间给 FatFs
FatFs 系统的文件和目录的日期、时间信息由 `get_fattime` 函数提供。该函数为 `__weak` 定义,我们在应用文件中重新实现即可,这里要注意 APM32F4xx 的 RTC 日历的起始年份和 FatFs 的不一致,做好偏移即可。
  1. DWORD get_fattime(void)
  2. {
  3.     return (DWORD)(rtcInfo.year + 20) << 25 |
  4.            (DWORD)rtcInfo.month << 21 |
  5.            (DWORD)rtcInfo.day << 16 |
  6.            (DWORD)rtcInfo.hour << 11 |
  7.            (DWORD)rtcInfo.minute << 5 |
  8.            (DWORD)rtcInfo.second >> 1;
  9. }
复制代码

### 创建和写数据到 Excel 文件
下面这个函数中,首先创建一个目录,然后创建或打开一个 `.xls` 文件,接着打开文件并用 `f_lseek` 函数移动写指针到文件末,最后用 ` f_printf ` 函数写入格式化后的字符串数据。
  1. void FATFS_WriteXlsFile(FIL* file, const TCHAR *path, char *dir, char *fileName, char* logInfo, char* timeStamp)
  2. {
  3.     FRESULT status;
  4.     char filePath[32];
  5.     char fileDir[20];
  6.    
  7.     /* Write file */
  8.     sprintf(fileDir, "%s", dir);
  9.     status = f_mkdir(fileDir);
  10.     if (status == FR_OK)
  11.     {
  12.         DAL_LOGI(tag, ">>> Create direction success");
  13.     }
  14.    
  15.     /* Open or create file */
  16.     sprintf(filePath, "%s%s%c%s.xls", path, fileDir, '/', fileName);

  17.     status = f_open(file, filePath, FA_OPEN_ALWAYS | FA_WRITE);
  18.     if (status == FR_OK)
  19.     {
  20.         DAL_LOGI(tag, ">>> Open or create %s %s file success", fileDir, fileName);
  21.         
  22.         /* Move the file pointer to the end */
  23.         f_lseek(file, f_size(file));
  24.         
  25.         f_printf(file, "%s + %s + %s\n", fileName, logInfo, timeStamp);
  26.     }
  27.     else
  28.     {
  29.         DAL_LOGE(tag, ">>> Open or create file fail, status is %d", status);
  30.     }

  31.     /* Close file */
  32.     f_close(file);
  33. }
复制代码

### 获取 U 盘剩余空间信息
Volume 的剩余空间,从之前 FatFs 章节中知道,可以用 `f_getfree` 函数来获取 U 盘剩余空间信息,并转换为使用率。
  1. void MSC_Application(void)
  2. {
  3.         ...
  4.         
  5.         res = f_getfree(USBHPath, &freClust, &fs);
  6.         diskInfo.totSect = (fs->n_fatent - 2) * fs->csize;  
  7.         diskInfo.totSect /= 2;
  8.         
  9.         diskInfo.freSect = freClust * fs->csize;
  10.         diskInfo.freSect /= 2;
  11.         
  12.         diskInfo.usedPercent = (1.0 - (double)diskInfo.freSect / diskInfo.totSect) * 100.0;
  13.         
  14.         ...
  15. }
复制代码

### 删除旧文件夹
以下代码,使用 `FATFS_ViewRootDir()` 函数获取目录文件夹信息,并按日期进行排序输出。最后由 `sscanf()` 来找到第一个目录,然后删除目录和文件。
  1. void MSC_Application(void)
  2. {
  3.         ...
  4.         
  5.         /* Get dir information */
  6.         DAL_LOGI(tag, "------> Get dir information");
  7.         FATFS_ViewRootDir(&USBHFatFS, USBHPath, diskInfo.dirPath, &diskInfo.dirNum);
  8.         
  9.         DAL_LOGI(tag, ">>> dir path :%s", diskInfo.dirPath);
  10.         DAL_LOGI(tag, ">>> dir number :%d", diskInfo.dirNum);
  11.         
  12.         /* Used percent > 90% and dir number > 1 */
  13.         if((diskInfo.usedPercent - DISK_SPACE_MAX_PERCENT) > EPS && (diskInfo.dirNum > 1))
  14.         {
  15.                 DAL_LOGW(tag, "<<< Disk space is not enough");
  16.         
  17.                 /* Delete first dir */
  18.                 sscanf(diskInfo.dirPath, "/%8[^/]", delectDirName);
  19.         
  20.                 FATFS_DelectDir(USBHPath, delectDirName);
  21.         }
  22.         ...
  23. }
复制代码

# 04 下载验证
经过之前几节的组件的配置,实现数据记录的 API 函数后,最后下载到 APM32F4xx 开发板看下应用的过程。

从下面 LOG 信息可以看到,插入 U 盘后,从 UART1 输出了 `MSC device is ready` 的 LOG 信息,这表明开发板已完成 U 盘的枚举识别。

同时可以看到 LOG 中带有时间戳信息,证明 RTC 已经开发工作,并按每秒打印一次的频率输出时间戳 LOG 信息。
  1. ...
  2. [main] 2023-11-22 00:00:37
  3. [main] 2023-11-22 00:00:38
  4. [main] 2023-11-22 00:00:39
  5. [main] 2023-11-22 00:00:40
  6. USB Device Reset Completed
  7. USB device speed is FS
  8. PID: 0x1234
  9. VID: 0x048D
  10. Endpoint 0 max packet size if 64
  11. USB device address: 1
  12. Manufacturer: USB
  13. Product: Disk 2.0k
  14. SerialNumber: 9207156342331724518
  15. USB device enumration ok
  16. This is a Mass Storage device
  17. Use 2 endpoint:
  18. Endpoint 0x01: max packet size is 64 bytes
  19. Endpoint 0x82: max packet size is 64 bytes
  20. USB device has only one configuration
  21. Set to default configuration
  22. Inquiry Revision :2.00
  23. Inquiry Product  :ProductCode
  24. Inquiry Vendor   :VendorCo
  25. MSC device is ready
  26. MSC device capacity     : 1006390784 bytes
  27. MSC device block number : 1965607
  28. MSC device block size   : 512
  29. Class is ready
复制代码
紧接着,就会进行 LOG 信息的记录,从下面 LOG 可以看到,FatFs 的操作过程。首先在 U 盘加载文件系统,然后创建或打开以 `20231122` 日期命名的文件夹并写入数据。接着获取 U 盘剩余空间,获取目录信息,最后卸载文件系统。
  1. [main] 2023-11-22 00:01:12
  2. [main] ------> Update information
  3. [main] ------> Mount U disk file system
  4. [main] ------> Write xls file
  5. [fatfs] >>> Open or create 20231122 LOG file success
  6. [main] ------> Get volume information
  7. [main] >>> (964657 / 966404) KiB Used 0.18 %
  8. [main] ------> Get dir information
  9. [fatfs] >>> View Directory
  10. [fatfs] >>> 20231122, (0x10)
  11. [fatfs] >>> 20231031, (0x10)
  12. [fatfs] >>> 20231030, (0x10)
  13. [fatfs] >>> 20231101, (0x10)
  14. [fatfs] >>> Sort Directory
  15. [main] >>> dir path :/20231030/20231031/20231101/20231122
  16. [main] >>> dir number :4
  17. [main] ------> Unmount U disk file system
  18. [main] >>> U disk file system unmounted ok
复制代码
我们打开 U 盘可以看到已经在设定的文件夹中创建了一个命名为 `LOG.xls` 的 Excel 文件。
Pasted image 20231123141930.png

以 LOG + RTC 时间戳为内容写入到 Excel 文件中。实际应用时记录的信息可以是系统不同任务执行情况,也可以是内核寄存器等便于维护调试的信息,或者是传感器的信息等。
Pasted image 20231123142127.png

以上,就是用 U 盘记录系统 LOG 信息的一些简单步骤和方法,实际应用中还要考虑更多情况,附件是源码,供大家参考。

# 参考资料

http://elm-chan.org/fsw/ff/00index_e.html

用U盘记录LOG信息(APM32F4xx).zip

9.66 MB, 下载次数: 21

评论

以U盘的方式储存LOG信息,大大缓解MCU的内存压力,便于对设备的信息维护,实现过程完整清晰,实现效果较佳。(满三篇原创文章可申请蓝v认证,获得蓝v标识可获得更多打赏,欢迎了解升级https://bbs.21ic.com/icview-3279072-1-1.html)  发表于 2023-11-28 15:25
kai迪皮 发表于 2023-11-23 21:31 | 显示全部楼层
学习了,顶
trucyw 发表于 2023-11-24 08:39 | 显示全部楼层
学习了
cmyldd 发表于 2023-11-24 09:09 | 显示全部楼层
学习了
caizhiwei 发表于 2023-11-28 18:44 | 显示全部楼层
学习了,excel文件是怎么生成的呢?
 楼主| susutata 发表于 2023-12-1 11:19 | 显示全部楼层
caizhiwei 发表于 2023-11-28 18:44
学习了,excel文件是怎么生成的呢?

用fatfs文件系统创建和写入内容即可。
excel是.xls文件,直接用.xls后缀创建相关文件。
excel文件内容实际上也是文本,的f_printf函数写入对应excel文件格式的数据即可,比如excel里换行用'\n',切换单元格用'\t'等
地球十强666 发表于 2023-12-2 23:15 | 显示全部楼层
帖子质量好高啊 学习学习
您需要登录后才可以回帖 登录 | 注册

本版积分规则

19

主题

32

帖子

5

粉丝
快速回复 在线客服 返回列表 返回顶部