续上一篇博客《FreeRTOS学习笔记-第一个项目工程》:
https://blog.youkuaiyun.com/shuang_sz/article/details/145697273
补充模拟IIC、TF卡文件系统的部分
1 模拟IIC
大多数程序猿都喜欢用模拟IIC来操作IIC设备,这在裸机系统中完全没有问题,但在实时操作系统中时,情况有所不同,因为EEPROM器件的时钟频率一般最高400KHz,而单片机模拟的时钟可能不会那么准确,也许达不到400KHz,就按400KHz算,假设读写50字节的数据,那么一次读写操作大约需要消耗13ms,而OS的调度频率一般10ms,因此在读写IIC的过程中,调度器可能随时打断读写过程,导致本次通讯失败,而为了避免这个问题,我采用的方法时,操作IIC时挂起任务调度器,操作完成后再恢复调度器。
1.1 规划EEPROM的存储内容
项目开始前一定要先规划好EEPROM中数据的存放规则,否则项目进行中需要修改的话工作量会比较大,本项目采用了以下方法:
- 用存储器的第一个单元标志该存储器有没有被初始化过(有没有写入过该项目的信息),若没有则擦除所有使用到的存储单元,并存入系统默认值,然后将该标志单元写入一个特殊字节的数据,表示该存储器已被初始化过,后面存放的数据都是可信的。
- 存储器后面存放的数据都封装到结构体中,避免读写零散的数据,一是要记住每个数据的地址,二是可能造成越界,误改了相邻单元的正常数据
- 本项目设计了一个存放系统信息的结构体,和一个系统配置信息的结构体,其实用一个也没问题,另外再创建更多的项目相关的结构体也没问题
1.2 创建EEPROM的数据结构体
在 work.h 文件中添加下面一些宏和两个结构体的定义:
/********************* EEPROM存储空间分配 每页128字节****************************************/
#define EEPROM_FLAG 0xAA //有效标志
#define EEPROM_FLAG_ADDR 0 //存储器有效标志的存放地址
#define EEPROM_SYS_ADDR 10 //系统信息的起始地址
#define EEPROM_ID_SIZE 16 //定义设备ID的最大长度
#define EEPROM_ZONE_SIZE 16 //定义时区的最大长度
typedef struct
{
char ID[EEPROM_ID_SIZE + 1]; //设备ID
char Time_Zone[EEPROM_ZONE_SIZE + 1]; //时区
char Path_Spec[8]; //SD卡中规格文件夹的路径
char Path_Log[8]; //SD卡中Log文件夹的路径
}SystemInfo_t;
typedef struct
{
unsigned char Debug_EN;
unsigned char Log_EN;
//...
}SystemConfig_t;
extern SystemInfo_t DevInfo; //声明系统信息变量
extern SystemConfig_t SysConfig; //声明系统配置信息变量
1.3 初始化IIC
初始化IIC即创建EEPROM对象,打开init.c文件,首先定义相关的变量,然后调用MyIIC.lib中的函数创建EEPROM对象。
打开 init.c 文件,添加下面代码,定义EEPROM指针和两个结构体:
IIC_Device_t *EEPROM; //定义EEPROM设备
SystemInfo_t DevInfo; //定义系统信息变量
SystemConfig_t SysConfig; //定义系统配置信息变量
还是在该文件中,添加如下代码,创建EEPROM指针并初始化:
//EEPROM初始化
void EEPROM_Init(void)
{
uint8_t status;
EEPROM = NewMyIIC(IIC_MCU_Type_F4, 168, 0xA0, 256, "PE0", "PE1"); //创建EEPROM设备
if (EEPROM == NULL)
{
Error_Handle(1, "Creat EEPROM failed!");
}
if (IIC_ReadBytes(EEPROM, &status, EEPROM_FLAG_ADDR, 1) != OK) //读出1个字节,存储器有效标志
{
Error_Handle(1, "Failed to read the FLAG!");
}
if (status != EEPROM_FLAG) //存储器未初始化过,重新初始化
{
Clear(DevInfo.ID);
Clear(DevInfo.Time_Zone);
Append(DevInfo.Time_Zone, "CST"); //设置时区默认值
Clear(DevInfo.Path_Spec);
Append(DevInfo.Path_Spec, "/Spec");
Clear(DevInfo.Path_Log);
Append(DevInfo.Path_Log, "/Log");
if (IIC_WriteBytes(EEPROM, (uint8_t *)&DevInfo, EEPROM_SYS_ADDR, sizeof(DevInfo)) != OK)//格式化系统信息,未成功写入
{
Error_Handle(1, "Failed to init the DevInfo!");
}
HAL_Delay(5);
if (IIC_SetBytes(EEPROM, EEPROM_FLAG, EEPROM_FLAG_ADDR, 1) != OK) //写入有效标志,未成功写入
{
Error_Handle(1, "Failed to write the FLAG!");
}
HAL_Delay(5);
}
if (IIC_ReadBytes(EEPROM, (uint8_t *)&DevInfo, EEPROM_SYS_ADDR, sizeof(DevInfo)) != OK) //读出系统信息
{
Error_Handle(1, "Failed to read the DevInfo!");
}
}
MSH_INIT_EXPORT(2, EEPROM_Init, Creat EEPROM);
函数 NewMyIIC(IIC_MCU_Type_F4, 168, 0xA0, 256, “PE0”, “PE1”) 用于创建EEPROM对象,传入参数说明如下:
- IIC_MCU_Type_F4,单片机类型,枚举,在MyIIC.h中有定义
- 168,主频,IIC库会根据该设置的主频将时钟频率调整到400KHz
- 0xA0,IIC器件地址
- 256,器件容量,单位Kbits,比如24C02,就是2Kbits,24M512就是512Kbits,IIC库会根据该容量值自动判断地址字节宽度(单字节或两字节)页大小(EEPROM不允许跨页写)。
- “PE0",SCL的引脚名,IIC库会根据该字符串计算SCL引脚地址
- “PE1",SDA的引脚名,IIC库会根据该字符串计算SDA引脚地址
后面的流程是,先读一次标志字节,若不是设置的有效数据,如0xAA,则写入默认值(出厂初始参数),最后读出所有数据存入结构体中。
1.4 编写EEPROM读写命令
下面以设备ID为例,展示IIC库的用法,打开 work.c 文件,添加下面两条命令:
//命令,从EEPROM中读治具ID
void Get_ID(char * Payload)
{
if (DevInfo.ID[0] != 0) //检查是否已设置了ID
{
Printf_If_PC("OK,%s,%s\r\n@_@", __func__, DevInfo.ID);
Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_Value, DevInfo.ID, HMI_End);
}
else
{
Printf_If_PC("NG,%s,No ID\r\n@_@", __func__);
Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_Value, "No ID", HMI_End);
}
}
MSH_CMD_EXPORT(Get_ID, Get fixture ID);
//命令,将治具ID写入EEPROM
void Set_ID(char * Payload)
{
uint8_t ret;
uint16_t len = StrLen(Payload);
if ((len < 8) || (len > EEPROM_ID_SIZE))
{
Printf_If_PC("NG,%s,Wrong ID,[8 to %dbytes]\r\n@_@", __func__, EEPROM_ID_SIZE);
Printf_If_HMI("%s=\"Wrong ID,[8 to %dbytes]\"%s", HMI_Ctr_UartRcv, EEPROM_ID_SIZE, HMI_End);
return;
}
if (len < EEPROM_ID_SIZE) len++; //如果字符串长度小于空间大小时,要多存一个结束符
vTaskSuspendAll(); //下面的操作将用到模拟IIC,需要挂起调度器
ret = EEPROM->Write((uint8_t *)Payload, EEPROM_SYS_ADDR + offsetof(SystemInfo_t, ID), len);
xTaskResumeAll(); //操作完成,恢复调度器
if (ret == OK)
{
Printf_If_PC("OK,%s\r\n@_@", __func__);
Printf_If_HMI("%s=\"Set OK!\"%s", HMI_Ctr_UartRcv, HMI_End);
for (uint8_t i = 0; i < len; i++) DevInfo.ID[i] = Payload[i]; //写入成功后更新全局变量
}
else
{
Printf_If_PC("NG,%s,Save failure!\r\n@_@", __func__);
Printf_If_HMI("%s=\"Save failure!\"%s", HMI_Ctr_UartRcv, HMI_End);
}
}
MSH_CMD_EXPORT(Set_ID, Set fixture ID);
读ID其实是从全局变量的系统信息结构体中直接读取的,并没有操作EEPROM。
设置ID的过程是将新的ID写入EEPROM,若写入成功,则同步更新到全局变量系统信息结构体中。
说明下EEPROM的读写方法:
- EEPROM->Write(数据指针,写入地址,写入长度)
- EEPROM->Read(数据指针,读取地址,读取长度)
特别说明: - offsetof,用于计算结构体中的指定的成员的偏移地址
- vTaskSuspendAll 用于挂起调度器,使IIC读写不会被打断
- xTaskResumeAll 用于恢复调度器,一定要与挂起函数成对使用
其它的EEPROM操作命令不一一展示。
2 FATFS
2.1 定义一些相关全局变量
打开 work.h 文件,添加下列一些变量声明:
/********************* 文件系统 ****************************************************/
extern FATFS Fatfs; //声明文件系统全局变量
extern const char *Root; //声明根目录路径
extern DIR Dir; //声明文件夹全局变量
extern FIL File; //声明文件全局变量
extern const char *File_Err[]; //声明文件系统的描述
并不是一定要都定义为全局变量,但这些变量一般都只在一个任务中使用,定义为全局可节省内存,不必在每个使用文件系统的函数中都定义一遍。
新建一个 file.c 文件,添加如下代码:
const char *File_Err[] = {
"E00: Succeeded!",
"E01: A hard error occurred in the low level disk I/O layer",
"E02: Assertion failed",
"E03: The physical drive cannot work",
"E04: Could not find the file",
"E05: Could not find the path",
"E06: The path name format is invalid",
"E07: Access denied due to prohibited access or directory full",
"E08: Access denied due to prohibited access",
"E09: The file/directory object is invalid",
"E10: The physical drive is write protected",
"E11: The logical drive number is invalid",
"E12: The volume has no work area",
"E13: There is no valid FAT volume",
"E14: The f_mkfs() aborted due to any problem",
"E15: Could not get a grant to access the volume within defined period",
"E16: The operation is rejected according to the file sharing policy",
"E17: LFN working buffer could not be allocated",
"E18: Number of open files > _FS_LOCK",
"E19: Given parameter is invalid"
};
const char *Root = "0:"; //定义根目录
FATFS Fatfs; //定义文件系统全局变量
DIR Dir; //定义文件夹全局变量
FIL File; //定义文件全局变量
FRESULT ret; //定义文件系统操作函数的返回值
const char Size_Unit[] = "BKMGTP"; //定义容量单位
2.1 挂载文件系统
打开 init.c 文件,添加文件系统挂载命令:
//文件系统初始化
void File_Init(void)
{
if (SD_CD != 0) Error_Handle(2, "No SD card!"); //检查是否插入了SD卡
// SD_PWR_OFF;
// HAL_Delay(100);
// SD_PWR_ON; //打开SD卡电源
// HAL_Delay(200);
FRESULT ret = f_mount(&Fatfs, Root, 1); //尝试挂载文件系统
if (ret == FR_NO_FILESYSTEM) //如果没有文件系统就报错
{
Error_Handle(3, "No File system!");
}
if (ret != FR_OK) //挂载中遇到其它异常
{
printf("Mount error:%s\r\n", File_Err[ret]);
Error_Handle(4, "Mount error!");
}
}
MSH_INIT_EXPORT(3, File_Init, File system initialization);
2.2 编写文件操作命令
打开 file.c 文件,添加下列命令:
命令,格式化SD卡,参数是密码,密码正确才会执行,密码:123456
//void SD_Format(char * Payload)
//{
// if (StrCompare(Payload, "123456") != 0)
// {
// Printf_If_PC("NG,%s,Password error!\r\n@_@", __func__);
// Printf_If_HMI("%s=\"Password error!\"%s", HMI_Ctr_UartRcv, HMI_End);
// return;
// }
// unsigned char *buf = (unsigned char *)malloc(512); //为格式化操作申请内存
// if (buf == NULL)
// {
// Printf_If_PC("NG,%s,Malloc fail!\r\n@_@", __func__);
// Printf_If_HMI("%s=\"Malloc fail\"%s", HMI_Ctr_UartRcv, HMI_End);
// return;
// }
// ret = f_mkfs(Root, FM_FAT32, 8192, buf, sizeof(buf)); //格式化SD卡
// free(buf); //释放内存
// if(ret != FR_OK) //格式化失败
// {
// Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
// Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
// }
// else
// {
// Printf_If_PC("OK,%s\r\n@_@", __func__);
// Printf_If_HMI("%s=\"Format OK\"%s", HMI_Ctr_UartRcv, HMI_End);
// }
//}
//MSH_CMD_EXPORT(SD_Format, Formatting SD Card);
//命令,查询剩余空间
void SD_Size (char * Payload)
{
FATFS *fs;
DWORD fre_clust = 0, fre_sect = 0, tot_sect = 0;
ret = f_getfree(Root, &fre_clust, &fs); //得到磁盘信息及空闲簇数量
if (ret == FR_OK)
{
tot_sect = (fs->n_fatent - 2) * fs->csize; //计算总扇区数,簇数量 * 簇大小
fre_sect = fre_clust * fs->csize; //得到空闲扇区数
Printf_If_PC("OK,%s,", __func__);
char unit = 1;
float size = (float)(tot_sect >> 1); //计算多少K,扇区大小为512字节,扇区数除以2就是K字节数
while (size > 1024)
{
size /= 1024;
unit++;
}
Printf_If_PC("Total: %.2f%c ", size, Size_Unit[unit]);
Printf_If_HMI("%s=\"Total: %.2f%c,", HMI_Ctr_UartRcv, size, Size_Unit[unit]);
unit = 1;
size = (float)(fre_sect >> 1); //计算多少K,扇区大小为512字节,扇区数除以2就是K字节数
while (size > 1024)
{
size /= 1024;
unit++;
}
Printf_If_PC("Free: %.2f%c\r\n@_@", size, Size_Unit[unit]);
Printf_If_HMI("Free: %.2f%c\"%s", size, Size_Unit[unit], HMI_End);
}
else
{
Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
}
}
MSH_CMD_EXPORT(SD_Size, Example Query the remaining disk space);
//命令,挂载文件系统
void File_Mount(char * Payload)
{
f_mount(NULL, "", 1); //先卸载文件系统
ret = f_mount(&Fatfs, Root, 1); //尝试挂载文件系统
if (ret == FR_OK)
{
Printf_If_PC("OK,%s\r\n@_@", __func__);
Printf_If_HMI("%s=\"Mount OK\"%s", HMI_Ctr_UartRcv, HMI_End);
}
else
{
Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
}
}
MSH_CMD_EXPORT(File_Mount, Mount the file system);
//命令,获取当前目录地址
void Get_Path(char * Payload)
{
char *path = pvPortMalloc(128); //申请存放目录的内存
if (path == NULL)
{
Printf_If_PC("NG,%s,malloc\r\n@_@", __func__);
Printf_If_HMI("%s=\"malloc\"%s", HMI_Ctr_UartRcv, HMI_End);
return;
}
ret = f_getcwd(path, 128); //获取当前目录
if (ret == FR_OK)
{
Printf_If_PC("OK,%s,%s\r\n@_@", __func__, path);
Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_UartRcv, path, HMI_End);
}
else
{
Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
}
vPortFree(path);
}
MSH_CMD_EXPORT(Get_Path, Get current directory);
//命令,进入指定目录
void Set_Path(char * Payload)
{
ret = f_chdir(Payload); //改变当前目录
if (ret == FR_OK)
{
Printf_If_PC("OK,%s\r\n@_@", __func__);
Printf_If_HMI("%s=\"OK\"%s", HMI_Ctr_UartRcv, HMI_End);
}
else
{
Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
}
}
MSH_CMD_EXPORT(Set_Path, Enter the specified directory);
//命令,创建一个新文件夹
void New_Dir(char * Payload)
{
ret = f_mkdir(Payload); //创建文件夹
if (ret == FR_OK)
{
Printf_If_PC("OK,%s\r\n@_@", __func__);
Printf_If_HMI("%s=\"OK\"%s", HMI_Ctr_UartRcv, HMI_End);
}
else
{
Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
}
}
MSH_CMD_EXPORT(New_Dir, Create a new folder);
//命令,创建一个新文件,如果文件已存在,则报错
void New_File(char * Payload)
{
FRESULT ret;
ret = f_open(&File, Payload, FA_CREATE_ALWAYS); //创建文件
if (ret != FR_OK)
{
Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
Printf_If_HMI("%s=\"%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
return;
}
f_close(&File);
Printf_If_PC("OK,%s\r\n@_@", __func__);
Printf_If_HMI("%s=\"OK\"%s", HMI_Ctr_UartRcv, HMI_End);
}
MSH_CMD_EXPORT(New_File, Create a new file);
//命令,显示指定路径下的文件和文件夹信息
void File_List(char * Payload)
{
FILINFO fno;
FRESULT ret = f_opendir(&Dir, Payload); //以文件夹的形式打开指定的路径
if (ret != FR_OK)
{
Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
return;
}
ret = f_chdir(Payload); //切换当前目录到该文件夹
if (ret != FR_OK)
{
Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
return;
}
while (1)
{
ret = f_readdir(&Dir, &fno); /* Read a directory item */
if (ret != FR_OK || fno.fname[0] == 0) break; /* Break on error or end of dir */
if (fno.fattrib & AM_DIR) /* It is a directory */
{
Printf_If_PC("Folder,%s,0,", fno.fname);
}
else /* It is a file. */
{
Printf_If_PC("File,%s,%u,", fno.fname, fno.fsize);
}
Printf_If_PC("%d/%02d/%02d %02d:%02d:%02d\r\n",
((fno.fdate >> 9) & 0x007F) + 1980,
(fno.fdate >> 5) & 0x00F,
fno.fdate & 0x001F,
(fno.ftime >> 11) & 0x001F,
(fno.ftime >> 5) & 0x003F,
(fno.ftime & 0x001F) << 1);
}
f_closedir(&Dir);
Printf_If_PC("OK,%s\r\n@_@", __func__);
}
MSH_CMD_EXPORT(File_List, Displays information about files and directories);
//命令,读取文件内容
void File_Read (char * Payload)
{
unsigned int len, ReadLen, read_size;
if (SENDER_IS_HMI) return; //HMI不支持该命令
ret = f_open(&File, Payload, FA_OPEN_EXISTING | FA_READ); //以读取形式打开已存在的文件
if (ret != FR_OK)
{
Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
return;
}
len = f_size(&File);
unsigned int buf_size = len > 256 ? 256 : len;
unsigned char *buf = pvPortMalloc(buf_size); //申请一块存放读取到的文件内容的内存
if (buf == NULL)
{
Printf_If_PC("NG,%s,Malloc fail\r\n@_@", __func__);
goto End;
}
while (len > 0)
{
read_size = len > buf_size ? buf_size : len;
ret = f_read(&File, buf, read_size, &ReadLen);
if ((ret != FR_OK) || (read_size != ReadLen))
{
Printf_If_PC("NG,%s,%s\r\n@_@", __func__, File_Err[ret]);
goto End;
}
HAL_UART_Transmit(&UART_PC, buf, ReadLen, 50);
len -= ReadLen;
}
HAL_Delay(20);
Printf_If_PC("OK,%s\r\n@_@", __func__);
End:
vPortFree(buf);
f_close(&File);
}
MSH_CMD_EXPORT(File_Read, Read contents of the file);
//命令,以覆盖写的形式写一行文本到文件中,格式:path,content
void File_Write(char * Payload)
{
unsigned int len1, len2;
char *path, *content;
path = StrSplit(Payload, ','); //分割字符串,得到文件路径
ret = f_open(&File, path, FA_OPEN_ALWAYS | FA_WRITE | FA_CREATE_ALWAYS); //以覆盖写的方式打开文件
if (ret != FR_OK)
{
Printf_If_PC("NG,%s,Open error:%s\r\n@_@", __func__, File_Err[ret]);
Printf_If_HMI("%s=\"Open error:%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
return;
}
content = path; //计算文本内容指针
while (*content++);
f_write(&File, content, StrLen(content), &len1); //将指定存储区内容写入到文件内
ret = f_write(&File, "\r\n", 2, &len2); //追加写换行符
if(ret == FR_OK)
{
Printf_If_PC("OK,%s\r\n@_@", __func__);
Printf_If_HMI("%s=\"OK\"%s", HMI_Ctr_UartRcv, HMI_End);
}
else
{
Printf_If_PC("NG,%s,Write error:%s\r\n@_@", __func__, File_Err[ret]);
Printf_If_HMI("%s=\"Write error:%s\"%s", HMI_Ctr_UartRcv, File_Err[ret], HMI_End);
}
f_close(&File);
}
MSH_CMD_EXPORT(File_Write, Write the contents to file);
这里展示了文件系统的大部分操作命令,还有一些如删除文件、删除文件夹及子项、文件追加写、重命名等命令不作展示。
然后用串口助手测试命令,比如发送”File_List",单片机将列出当前目录下的所有文件夹和文件。