引言
在前四篇文章中自己介绍了如何配置freeRTOS以及如何配置LWIP,并使用lwip实现一个httpd服务器,使浏览器可以访问,并利用CGI功能,实现通过网页来控制单片机的一个LED灯的电平翻转。
在这篇文章中,本人使用CubeMX软件配置STM32驱动FLASH实现文件系统,为以后使用FTP远程登录做准备。
自己写的另外三篇文章
从零开始Cubemx配置STM32搭载freeRTOS实现多路ADC(一)
从零开始Cubemx配置STM32搭载freeRTOS以及lwip实现tcp网络通信(二)
从零开始使用CubeMX配置STM32使用lwip实现httpd服务器以及使用vscode编辑阅读keil代码(三)
CubeMX配置STM32实现httpd服务器CGI功能并使用网页控制STM32单片机(四)
CubeMX配置FatFS功能
配置过程主要看了这位大佬的文章 STM32CUBEIDE之SPI读写FLASH进阶串行FLASH文件系统FatFs以及野火的课程https://www.bilibili.com/video/BV18X4y1M763?p=64&spm_id_from=pageDriver的P57讲的部分,在这里表示感谢,我这篇文章主要自己做了一遍,然后根据自己的理解总结一下。
移植基本不需要怎么改动,需要改动的是将编码改成简体中文,打开长文件名选项。由于我的程序带有操作系统,因此我就放在HEAP也就是堆中了。
放在HEAP中的话记得把HEAP的大小改大。
W25Q128是16M spi flash,一共有256个block ,每个Block 64KB。
一个Block可以分割为16个扇区(small sector),每个扇区4096字节
由于我用的是野火的板级支持包,因此SPI通信的FLASH我就不配置了,直接移植就可以了。其他的型号按照如何驱动存储介质的操作进行驱动就行了,这一部分一般是驱动工程师的工作。
驱动移植
上面的东西配置完成后,生成代码。
代码部分需要我们做的主要是驱动的移植。我们打开user_diskio.c文件。我们的驱动文件就移植到这个文件里面。
#ifdef USE_OBSOLETE_USER_CODE_SECTION_0
/*
* Warning: the user section 0 is no more in use (starting from CubeMx version 4.16.0)
* To be suppressed in the future.
* Kept to ensure backward compatibility with previous CubeMx versions when
* migrating projects.
* User code previously added there should be copied in the new user sections before
* the section contents can be deleted.
*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
#endif
/* USER CODE BEGIN DECL */
/* Includes ------------------------------------------------------------------*/
#include <string.h>
#include "ff_gen_drv.h"
#include "bsp_spi_flash.h"
/* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/
#define SPI_FLASH 0 // 外部SPI Flash
/* Private variables ---------------------------------------------------------*/
/* Disk status */
static volatile DSTATUS Stat = STA_NOINIT;
/* USER CODE END DECL */
/* Private function prototypes -----------------------------------------------*/
DSTATUS USER_initialize (BYTE pdrv);
DSTATUS USER_status (BYTE pdrv);
DRESULT USER_read (BYTE pdrv, BYTE *buff, DWORD sector, UINT count);
#if _USE_WRITE == 1
DRESULT USER_write (BYTE pdrv, const BYTE *buff, DWORD sector, UINT count);
#endif /* _USE_WRITE == 1 */
#if _USE_IOCTL == 1
DRESULT USER_ioctl (BYTE pdrv, BYTE cmd, void *buff);
#endif /* _USE_IOCTL == 1 */
Diskio_drvTypeDef USER_Driver =
{
USER_initialize,
USER_status,
USER_read,
#if _USE_WRITE
USER_write,
#endif /* _USE_WRITE == 1 */
#if _USE_IOCTL == 1
USER_ioctl,
#endif /* _USE_IOCTL == 1 */
};
/* Private functions ---------------------------------------------------------*/
/**
* @brief Initializes a Drive
* @param pdrv: Physical drive number (0..)
* @retval DSTATUS: Operation status
*/
DSTATUS USER_initialize (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
/* USER CODE BEGIN INIT */
uint16_t i;
DSTATUS status = STA_NOINIT;
switch (pdrv) {
case SPI_FLASH: /* SPI Flash */
/* 初始化SPI Flash */
SPI_FLASH_Init();
/* 延时一小段时间 */
i=500;
while(--i);
/* 唤醒SPI Flash */
SPI_Flash_WAKEUP();
/* 获取SPI Flash芯片状态 */
status=USER_status(SPI_FLASH);
break;
default:
status = STA_NOINIT;
}
return status;
/* USER CODE END INIT */
}
/**
* @brief Gets Disk Status
* @param pdrv: Physical drive number (0..)
* @retval DSTATUS: Operation status
*/
DSTATUS USER_status (
BYTE pdrv /* Physical drive number to identify the drive */
)
{
/* USER CODE BEGIN STATUS */
DSTATUS status = STA_NOINIT;
switch (pdrv) {
case SPI_FLASH:
/* SPI Flash状态检测:读取SPI Flash 设备ID */
if(sFLASH_ID == SPI_FLASH_ReadID())
{
/* 设备ID读取结果正确 */
status &= ~STA_NOINIT;
}
else
{
/* 设备ID读取结果错误 */
status = STA_NOINIT;;
}
break;
default:
status = STA_NOINIT;
}
return status;
/* USER CODE END STATUS */
}
/**
* @brief Reads Sector(s)
* @param pdrv: Physical drive number (0..)
* @param *buff: Data buffer to store read data
* @param sector: Sector address (LBA)
* @param count: Number of sectors to read (1..128)
* @retval DRESULT: Operation result
*/
DRESULT USER_read (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
BYTE *buff, /* Data buffer to store read data */
DWORD sector, /* Sector address in LBA */
UINT count /* Number of sectors to read */
)
{
/* USER CODE BEGIN READ */
DRESULT status = RES_PARERR;
switch (pdrv) {
case SPI_FLASH:
/* 扇区偏移6MB,外部Flash文件系统空间放在SPI Flash后面10MB空间 */
sector+=1536;
SPI_FLASH_BufferRead(buff, sector <<12, count<<12);
status = RES_OK;
break;
default:
status = RES_PARERR;
}
return status;
/* USER CODE END READ */
}
/**
* @brief Writes Sector(s)
* @param pdrv: Physical drive number (0..)
* @param *buff: Data to be written
* @param sector: Sector address (LBA)
* @param count: Number of sectors to write (1..128)
* @retval DRESULT: Operation result
*/
#if _USE_WRITE == 1
DRESULT USER_write (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
const BYTE *buff, /* Data to be written */
DWORD sector, /* Sector address in LBA */
UINT count /* Number of sectors to write */
)
{
/* USER CODE BEGIN WRITE */
/* USER CODE HERE */
uint32_t write_addr;
DRESULT status = RES_PARERR;
if (!count) {
return RES_PARERR; /* Check parameter */
}
switch (pdrv) {
case SPI_FLASH:
/* 扇区偏移6MB,外部Flash文件系统空间放在SPI Flash后面10MB空间 */
sector+=1536;
write_addr = sector<<12;
SPI_FLASH_SectorErase(write_addr);
SPI_FLASH_BufferWrite((uint8_t *)buff,write_addr,count<<12);
status = RES_OK;
break;
default:
status = RES_PARERR;
}
return status;
/* USER CODE END WRITE */
}
#endif /* _USE_WRITE == 1 */
/**
* @brief I/O control operation
* @param pdrv: Physical drive number (0..)
* @param cmd: Control code
* @param *buff: Buffer to send/receive control data
* @retval DRESULT: Operation result
*/
#if _USE_IOCTL == 1
DRESULT USER_ioctl (
BYTE pdrv, /* Physical drive nmuber (0..) */
BYTE cmd, /* Control code */
void *buff /* Buffer to send/receive control data */
)
{
/* USER CODE BEGIN IOCTL */
DRESULT status = RES_PARERR;
switch (pdrv) {
case SPI_FLASH:
switch (cmd) {
/* 扇区数量:2560*4096/1024/1024=10(MB) */
case GET_SECTOR_COUNT:
*(DWORD * )buff = 2560;
break;
/* 扇区大小 */
case GET_SECTOR_SIZE :
*(WORD * )buff = 4096;
break;
/* 同时擦除扇区个数 */
case GET_BLOCK_SIZE :
*(DWORD * )buff = 1;
break;
}
status = RES_OK;
break;
default:
status = RES_PARERR;
}
return status;
/* USER CODE END IOCTL */
}
#endif /* _USE_IOCTL == 1 */
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
读这个代码可以发现,我们需要根据实际使用的硬件来填写这几个函数。
USER_initialize,
USER_status,
USER_read,
USER_write,
USER_ioctl,
比如写操作,我们需要写入SPI_FLASH的实际操作过程,这也就是驱动。SPI对FLASH的操作一般由驱动工程师或者硬件厂家完成。
DRESULT USER_write (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
const BYTE *buff, /* Data to be written */
DWORD sector, /* Sector address in LBA */
UINT count /* Number of sectors to write */
)
{
/* USER CODE BEGIN WRITE */
/* USER CODE HERE */
uint32_t write_addr;
DRESULT status = RES_PARERR;
if (!count) {
return RES_PARERR; /* Check parameter */
}
switch (pdrv) {
case SPI_FLASH:
/* 扇区偏移6MB,外部Flash文件系统空间放在SPI Flash后面10MB空间 */
sector+=1536;
write_addr = sector<<12;
SPI_FLASH_SectorErase(write_addr);
SPI_FLASH_BufferWrite((uint8_t *)buff,write_addr,count<<12);
status = RES_OK;
break;
default:
status = RES_PARERR;
}
return status;
/* USER CODE END WRITE */
}
#endif /* _USE_WRITE == 1 */
SPI_FLASH的驱动,我直接用的野火的FLASH板级支持包bsp_spi_flash.c,这里吗有FLASH的驱动实现,以及SPI_FLASH_BufferWrite()等函数的实现,到这里我们以及移植好了,可以进行代码编写了。
案例演示
实际使用主要参考http://elm-chan.org/fsw/ff/00index_e.html。全部API都有相应的解释。
自己的代码写在fatfs.c文件里,在这里贴出一个我用的测试案例。
/**
******************************************************************************
* @file fatfs.c
* @brief Code for fatfs applications
******************************************************************************
* @attention
*
* <h2><center>© Copyright (c) 2022 STMicroelectronics.
* All rights reserved.</center></h2>
*
* This software component is licensed by ST under Ultimate Liberty license
* SLA0044, the "License"; You may not use this file except in compliance with
* the License. You may obtain a copy of the License at:
* www.st.com/SLA0044
*
******************************************************************************
*/
#include "fatfs.h"
uint8_t retUSER; /* Return value for USER */
char USERPath[4]; /* USER logical drive path */
FATFS USERFatFS; /* File system object for USER logical drive */
FIL USERFile; /* File object for USER */
/* USER CODE BEGIN Variables */
BYTE work[_MAX_SS];
UINT fnum;
BYTE WriteBuffer[] = "hello world 你好!!";
/* USER CODE END Variables */
void MX_FATFS_Init(void)
{
/*## FatFS: Link the USER driver ###########################*/
retUSER = FATFS_LinkDriver(&USER_Driver, USERPath);
/* USER CODE BEGIN Init */
retUSER = f_mount(&USERFatFS,"",1);
if(retUSER == FR_NO_FILESYSTEM)
{
printf("FLASH is not formatted\r\n");
retUSER=f_mkfs("", FM_ANY, 0, work, sizeof work);
if(retUSER == FR_OK)
{
printf("The FLASH formats the file system successfully\r\n");
retUSER = f_mount(NULL,"",1);
retUSER = f_mount(&USERFatFS,"",1);
}
else
{
printf("Formatting failure\r\n");
while(1);
}
}
else if(retUSER!=FR_OK)
{
printf("The external Flash failed to mount the file system. Procedure (%d)\r\n",retUSER);
printf("Possible cause: The SPI Flash initialization fails\r\n");
while(1);
}
else
{
printf("The file system is mounted successfully, and you can perform read/write tests\r\n");
}
/*----------------------- 文件系统测试:写测试 -----------------------------*/
/* 打开文件,如果文件不存在则创建它 */
printf("\r\n****** 即将进行文件写入测试... ******\r\n");
retUSER = f_open(&USERFile, "0:FatUSERFatFS读写测试文件.txt",FA_CREATE_ALWAYS | FA_WRITE );
if (retUSER == FR_OK)
{
printf("》打开/创建FatUSERFatFS读写测试文件.txt文件成功,向文件写入数据。\r\n");
/* 将指定存储区内容写入到文件内 */
retUSER=f_write(&USERFile,WriteBuffer,sizeof(WriteBuffer),&fnum);
if(retUSER==FR_OK)
{
printf("》文件写入成功,写入字节数据:%d\n",fnum);
printf("》向文件写入的数据为:\r\n%s\r\n",WriteBuffer);
}
else
{
printf("!!文件写入失败:(%d)\n",retUSER);
}
/* 不再读写,关闭文件 */
f_close(&USERFile);
}
else
{
printf("!!打开/创建文件失败。\r\n");
}
printf("****** 即将进行文件读取测试... ******\r\n");
retUSER = f_open(&USERFile, "FatUSERFatFS读写测试文件.txt", FA_OPEN_EXISTING | FA_READ);
if(retUSER == FR_OK)
{
printf("》打开文件成功。\r\n");
retUSER = f_read(&USERFile, work, sizeof(work), &fnum);
if(retUSER==FR_OK)
{
printf("》文件读取成功,读到字节数据:%d\r\n",fnum);
printf("》读取得的文件数据为:\r\n%s \r\n", work);
}
else
{
printf("!!文件读取失败:(%d)\n",retUSER);
}
}
else
{
printf("!!打开文件失败。\r\n");
}
/* 不再读写,关闭文件 */
f_close(&USERFile);
/* additional user code for init */
/* USER CODE END Init */
}
/**
* @brief Gets Time from RTC
* @param None
* @retval Time in DWORD
*/
DWORD get_fattime(void)
{
/* USER CODE BEGIN get_fattime */
return 0;
/* USER CODE END get_fattime */
}
/* USER CODE BEGIN Application */
/* USER CODE END Application */
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
案例其实没有什么讲的,移植FatFs后的文件,操作就与win下的一样了,都是f_open打开,f_write写一类的操作,不同的地方就是使用前需要挂载一下。实验结果如下所示:
到此FatFs移植成功,并能正常使用。
代码解析
代码野火课程P63讲的比较详细,在这里我简要叙述一下。
Diskio_drvTypeDef USER_Driver =
{
USER_initialize,
USER_status,
USER_read,
#if _USE_WRITE
USER_write,
#endif /* _USE_WRITE == 1 */
#if _USE_IOCTL == 1
USER_ioctl,
#endif /* _USE_IOCTL == 1 */
};
这几个函数,也就是我们需要写的实现函数,在这个结构体中被赋值给了函数指针(也可以认为是回调函数),声明是这个样子的。
typedef struct
{
DSTATUS (*disk_initialize) (BYTE); /*!< Initialize Disk Drive */
DSTATUS (*disk_status) (BYTE); /*!< Get Disk Status */
DRESULT (*disk_read) (BYTE, BYTE*, DWORD, UINT); /*!< Read Sector(s) */
#if _USE_WRITE == 1
DRESULT (*disk_write) (BYTE, const BYTE*, DWORD, UINT); /*!< Write Sector(s) when _USE_WRITE = 0 */
#endif /* _USE_WRITE == 1 */
#if _USE_IOCTL == 1
DRESULT (*disk_ioctl) (BYTE, BYTE, void*); /*!< I/O control operation when _USE_IOCTL = 1 */
#endif /* _USE_IOCTL == 1 */
}Diskio_drvTypeDef;
定义好后,通过这个函数,将驱动进行链接。
retUSER = FATFS_LinkDriver(&USER_Driver, USERPath);
结构体指针在这个函数中被赋值
uint8_t FATFS_LinkDriverEx(const Diskio_drvTypeDef *drv, char *path, uint8_t lun)
{
uint8_t ret = 1;
uint8_t DiskNum = 0;
if(disk.nbr < _VOLUMES)
{
disk.is_initialized[disk.nbr] = 0;
disk.drv[disk.nbr] = drv; //这个就是自己的驱动函数
disk.lun[disk.nbr] = lun;
DiskNum = disk.nbr++;
path[0] = DiskNum + '0';
path[1] = ':';
path[2] = '/';
path[3] = 0;
ret = 0;
}
return ret;
}
到这里已经链接上了。
DRESULT disk_read (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
BYTE *buff, /* Data buffer to store read data */
DWORD sector, /* Sector address in LBA */
UINT count /* Number of sectors to read */
)
{
DRESULT res;
res = disk.drv[pdrv]->disk_read(disk.lun[pdrv], buff, sector, count);
return res;
}
这里看一个上层的读函数,f_read调用了disk_read函数,自己写的驱动函数,也就是控制SPI_FLASH的读操作已经赋值给了disk.drv。这样一切就耦合起来了。
总结
这次遇到的问题是开始不知道驱动怎么写,因为自己主要做的是网络通信方面,虽然知道驱动,但是一直不知道如何将软件以及硬件耦合起来,经过这次的项目,终于知道软件层以及硬件层是如何耦合到一起的,也将之前学的IIC,SPI等应用了起来。
下一步我继续网络方面,使用FTP远程连接,向文件系统中写入文件。