在STM32实现Linux操作系统与RAM文件系统

架构与代码仓库

本项目使用了ARM架构下的Cortex-M7核心,用户只需要复制必要文件就可以完成相关操作

Git仓库:https://github.com/iMinloha/H7OS.git

文件系统DrT

目前实现了RAMFS、动态内存管理、动态线程管理以及指令体系。其中RAMFS使用了DrT树结构,结构图如下:

1

结构体FS_t实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct FS{
// 路径
char* path;

// 子设备链表
DrTNode_t node;
// 设备数量
int node_count;
// --------------------
// proc目录子项目
Task_t tasklist;

// 父级
FS_t parent;
// 层级
FS_t next;
// 子级
FS_t child;
};

// 根文件系统
static FS_t RAM_FS;

我们可以对一个FS_t节点添加线程节点、设备节点或者其他FS_t节点,其中在层级添加就是在当前目录同层新建文件夹,而在子级就是在文件夹下新建文件夹。也就是兄弟节点和子节点关系。

对于设备和线程需要有相关的属性,其中对设备的使用需要考虑多线程的互斥,所以每个设备节点必须自带一个互斥锁,在此之前我们为了确保RAM空间的充足就必须要实现SDRAM的内存管理,这里使用了tlsf算法:

1
2
3
4
5
6
7
8
9
10
11
/***
* @brief SDRAM内存管理初始化
* @note 该函数会初始化SDRAM内存管理算法, 请在使用SDRAM内存管理前调用该函数.
* */
void MemControl_Init(){
flashSDRAM();
// 内核空间(会自动保存在QSPI Flash中)
kernel_pool = tlsf_create_with_pool((void*)SDRAM_BANK_ADDR, 2 * 1024 * 1024);
// 程序运行空间
mem_pool = tlsf_create_with_pool((void*)SDRAM_BANK_ADDR + 2 * 1024 * 1024, 30 * 1024 * 1024);
}

首先对SDRAM空间进行刷新,也就是将内部莫名的数据清0,然后分别区分为内核空间和程序运行空间,保留出足够DrT使用的内核空间(2M足够了)后其他的空间作为用户应用程序空间即可。所以需要实现kernel_alloc和ram_alloc算法,两者极其相似:

1
2
3
4
5
6
7
8
9
10
11
12
13
/***
* @brief SDRAM内存管理分配
* @param size: 分配内存大小
* @return 分配内存的地址
* */
void* ram_alloc(uint32_t size){
using_mem += size;
return tlsf_malloc(mem_pool, size);
}

void* kernel_alloc(uint32_t size){
return tlsf_malloc(kernel_pool, size);
}

然后就可以声明设备结构体了:

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
/****************************************************
* DrT 设备树
* - 设备树是一个树形结构,用于描述系统中的硬件设备。
* - 设备树是一个平台无关的描述硬件的数据结构。
* - 文件包含:DrT的创建和声明以及指令处理
* */

enum DeviceType{
DEVICE_TIMER, // 定时器
DEVICE_BS, // 基设备
DEVICE_STORAGE, // 存储设备(qspi, sd, emmc)
DEVICE_DISPLAY, // 显示设备(RGB, LVDS, HDMI)
DEVICE_INPUT, // 输入设备(OTG)
DEVICE_SERIAL, // 总线设备(USART, UART, SPI, IIC, CAN, LIN, USB)
DEVICE_TRANSPORT, // 传输设备(USB, ETH, WIFI)
DEVICE_VOTAGE, // 电压设备(ADC, DAC)
DEVICE_TASK, // 任务设备(proc显示的内容)
// --------------------
FILE_SYSTEM, // 文件系统
FILE, // 文件
};

typedef enum DeviceType DeviceType_E;

// 设备状态
enum DeviceStatus{
DEVICE_OFF, // 关闭
DEVICE_ON, // 打开
DEVICE_SUSPEND, // 挂起
DEVICE_ERROR, // 错误
DEVICE_BUSY, // 占用
};

typedef enum DeviceStatus DeviceStatus_E;
typedef struct FS* FS_t;
typedef struct DrTNode* DrTNode_t;

struct DrTNode{
// 设备地址
void* device;
// 设备状态
DeviceStatus_E status;
// 设备类型
DeviceType_E type;
// 设备名称
char* name;
// 设备描述
char* description;
// 设备数据缓冲
void* data;
// --------------------
// 设备驱动
Func_t driver;
// --------------------
Mutex_t mutex; // 设备锁
// --------------------
DrTNode_t next;
FS_t parent;
};

为此我们需要确保线程数据安全,必须实现以下Mutex_t锁的相关内容:

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
#include <stdatomic.h>

typedef struct param* param_t;

/**
* @brief 函数指针
* @param param 参数结构体
* */
typedef void (*Func_t)(param_t param);

/**
* @brief 互斥锁
* @param lockflag 锁标志
* @param count 锁计数
* @param owner 锁拥有者
* @namespace lock 加锁方法
* @namespace unlock 解锁
* @namespace status 锁状态
* */
struct _mutex {
atomic_int lockflag;
atomic_int count;
atomic_int owner;

void (*lock)(struct _mutex *self);
void (*unlock)(struct _mutex *self);
int (*status)(struct _mutex *self);
};
/**
* @brief 互斥锁类型
* */
typedef struct _mutex* Mutex_t;

#define MUTEX_SIZE sizeof(struct _mutex)

/**
* @brief 互斥锁初始化
* @param self 互斥锁
*/
void mutex_init(Mutex_t self);

/**
* @brief 互斥锁加锁
* @param self 互斥锁
*/
void mutex_lock(Mutex_t self);

/**
* @brief 互斥锁解锁
* @param self 互斥锁
* */
void mutex_unlock(Mutex_t self);

/**
* @brief 互斥锁状态
* @param self 互斥锁
* */
int mutex_status(Mutex_t self);

/**
* @brief 互斥锁执行函数
* @param self 互斥锁
* @param func 函数
* */
void LockFunc(Mutex_t self, Func_t func, param_t param);

这里我使用了几个原子操作进行状态绑定,在同时保护设备的同时还可以确定设备拥有者和设备使用计数周期,这样就可以完成精确的设备保护了。

为了定位串口终端所处位置,我们需要新建两个结构体指向根文件系统和终端节点。

1
2
3
4
5
// 根文件系统
static FS_t RAM_FS;

// 初始化设备树(添加设备目录与分类)
void DrTInit();
1
2
// 串口设备指针(用于指向当前串口终端所在位置)
FS_t currentFS;

然后我们需要实现快捷的添加设备,添加文件夹,添加线程等等方法:

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
/**
* @brief 添加层文件节点
* @param parent 父节点
* @param path 子节点路径
*/
void addFSChild(FS_t parent, char *path){
// 创建子节点
FS_t child = (FS_t) kernel_alloc(sizeof(struct FS));
child->path = (char*) kernel_alloc(strlen(path) + 1);
strcopy(child->path, path);

child->node = NULL;
child->node_count = 0;
child->parent = parent;
child->next = NULL; // 子文件夹
child->child = NULL; // 同层文件夹

// 添加到父节点的next的child同层列表
FS_t p = parent->next;
if (p == NULL) {
parent->next = child;
return;
}else{
while(p->child != NULL) p = p->child;
p->child = child;
}
}
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
// 添加设备
/**
* @brief 添加设备
* @param node 设备节点
* @param devicePtr 设备指针(htim, huart, hspi)
* @param name 设备名称(如TIM1, USART1, SPI1)
* @param description 设备描述(如定时器1, 串口1, SPI1)
* @param type 设备类型(如通信设备,时钟设备)
* @param driver 设备驱动(一个函数指针)
*/
void addDevice(char *path, void* devicePtr, char *name, char *description, DeviceType_E type,
DeviceStatus_E status, Func_t driver){
FS_t node = getFSChild(RAM_FS, path);
if (node == NULL) return;
// 创建设备节点
DrTNode_t device = (DrTNode_t) kernel_alloc(sizeof(struct DrTNode));
device->name = (char*) kernel_alloc(strlen(name) + 1);
device->description = (char*) kernel_alloc(strlen(description) + 1);

strcopy(device->name, name);
strcopy(device->description, description);

device->device = devicePtr;
device->status = status;
device->type = type;
device->data = kernel_alloc(128);
device->driver = driver;
Mutex_t mutex = (Mutex_t) kernel_alloc(MUTEX_SIZE);
mutex_init(mutex);
device->mutex = mutex;
device->parent = node;
device->next = NULL;

// 添加到设备链表
DrTNode_t p = node->node;
if (p == NULL) {
node->node = device;
node->node_count++;
return;
}else{
while(p->next != NULL) p = p->next;
p->next = device;
node->node_count++;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 添加线程
void addThread(Task_t task){
FS_t node = getFSChild(RAM_FS, "proc");
if (node == NULL) return;
Task_t p = node->tasklist;
if (p == NULL) {
node->tasklist = task;
return;
}else{
while(p->next != NULL) p = p->next;
p->next = task;
}
}

根据FS_t的name可以组建文件路径,我们当然可以从路径获取FS_t的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @brief 获取子节点
* @param parent 父节点
* @param path 子节点文件名
* @return 子节点
*/
FS_t getFSChild(FS_t parent, char *path){
// path就是子目录名
FS_t p = parent->next;
while(p != NULL){
if(strcmp(p->path, path) == 0) return p;
p = p->child;
}
return NULL;
}

这里的实现方法极为简单。接下来完整组成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @brief 加载路径(获取节点)
* @param path
* @return
*/
FS_t loadPath(char* path) {
FS_t node = RAM_FS;
if (strcmp(path, "/") == 0) return node;
else {
// 如果想获取路径就是根路径,直接返回Root节点
if (path[0] == '/') path++;
// 不同的话就按斜杠分割,然后循环获取子节点
char *token = strtok(path, "/");
while (token != NULL) {
// 循环获取直到没有,一旦到头就证明获取完毕
node = getFSChild(node, token);
if (node == NULL) return NULL;
token = strtok(NULL, "/");
}
}

return node;
}

从此方法就可以获得FS节点信息,如果路径对应的FS_t对象不存在就会返回NULL。

除了加载文件夹路径的方法外,还需要加载设备:

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
/**
* @brief 加载设备
* @param path
* @return
*/
DrTNode_t loadDevice(char* path_aim){
char *path = (char*) kernel_alloc(strlen(path_aim) + 1);
strcopy(path, path_aim);

FS_t node;
if (path[0] == '/') node = RAM_FS;
else node = currentFS;
// 根节点不做任何设备添加
if (strcmp(path, "/") == 0) return NULL;

else {
if (path[0] == '/') path++;
char *token = strtok(path, "/");
while (token != NULL) {
FS_t tmp_node = getFSChild(node, token);
if (tmp_node == NULL) break;
token = strtok(NULL, "/");
node = tmp_node;
}
// 一直遍历路径直到获取不到,如果到底的路径不存在其他"/"符号,只需找设备路径即可
// 如果遍历到最后还有"/"符号,证明有不存在的FS_t节点,返回NULL即可。

// token字符大于0说明path中有多余的路径
if (strcmp(token, strtok(token, "/")) != 0){
return NULL;
} else {
DrTNode_t p = node->node;
while (p != NULL) {
if (strcmp(p->name, token) == 0) return p;
p = p->next;
}
}
}
return NULL;
}

加载任务也如此简单,和加载设备方法一样:

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
Task_t loadTask(char* path){
FS_t node;
if (path[0] == '/') node = RAM_FS;
else node = currentFS;

if (strcmp(path, "/") == 0) return NULL;

else {
if (path[0] == '/') path++;
char *token = strtok(path, "/");
while (token != NULL) {
FS_t tmp_node = getFSChild(node, token);
if (tmp_node == NULL) break;
token = strtok(NULL, "/");
node = tmp_node;
}

// token字符大于0说明path中有多余的路径
if (strcmp(token, strtok(token, "/")) != 0){
return NULL;
} else {
Task_t p = node->tasklist;
while (p != NULL) {
if (strcmp(p->name, token) == 0) return p;
p = p->next;
}
}
}

return NULL;
}

线程管理u_Thread

线程结构体实现如下:

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
enum TaskStatus{
TASK_READY,
TASK_RUNNING,
TASK_SUSPEND,
TASK_STOP
};

typedef enum TaskStatus TaskStatus_E;

// 任务权限枚举
enum TaskPriority{
TASK_PRIORITY_NORMAL,
TASK_PRIORITY_HIGH,
TASK_PRIORITY_ROOT,
TASK_PRIORITY_SYSTEM,
};

typedef enum TaskPriority TaskPriority_E;


typedef struct Task* Task_t;

// 任务管理器
struct Task{
// 任务ID
char *name;
// 任务状态
TaskStatus_E status;
// CPU占用率
float cpu;
// 任务权限
TaskPriority_E priority;
// 任务句柄
osThreadId handle;
// 计数
uint32_t lastWakeTime;
// 累计时间
uint32_t accumulatedTime;
// PID
uint8_t PID;
// 下一个任务
Task_t next;
};

我们只需要实现对应的方法:

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
Task_t xTaskManager;
Task_t xShell;
Task_t xTest;

uint8_t PID_Global = 0;

Task_t RAMFS_TASK_Create(char *name, TaskStatus_E status, TaskPriority_E priority, osThreadId handle){
Task_t task = (Task_t) kernel_alloc(sizeof(struct Task));

task->name = kernel_alloc(strlen(name) + 1);
strcopy(task->name, name);

task->status = status;
task->PID = PID_Global++;
task->cpu = 0;
task->priority = priority;
task->handle = handle;
task->next = NULL;
return task;
}


void ThreadInit(){
// 线程都在这里进行注册
osThreadDef(xShell, ShellTask, osPriorityNormal, 0, 1024);
xShellHandle = osThreadCreate(osThread(xShell), NULL);

osThreadDef(xTaskManager, TaskManager, osPriorityAboveNormal, 0, 1024);
xTaskManagerHandle = osThreadCreate(osThread(xTaskManager), NULL);

osThreadDef(xTaskInit, QueueInit, osPriorityIdle, 0, 2048);
xTaskInitHandle = osThreadCreate(osThread(xTaskInit), NULL);

osThreadDef(xTaskTest, testFunc, osPriorityRealtime, 0, 2048);
xTaskTestHandle = osThreadCreate(osThread(xTaskTest), NULL);

xTaskManager = RAMFS_TASK_Create("xTaskManager", TASK_READY, TASK_PRIORITY_SYSTEM, xTaskManagerHandle);
xShell = RAMFS_TASK_Create("xShell", TASK_READY, TASK_PRIORITY_SYSTEM, xShellHandle);
xTest = RAMFS_TASK_Create("xTaskTest", TASK_READY, TASK_PRIORITY_NORMAL, xTaskTestHandle);
}

这里我使用了FreeRTOS的线程调度方法,我们只需要在proc下面添加对应的结构体即可,每个线程需要实现一个专属PID,之后会在指令管理处完成利用。

然后利用任务管理器线程实现占用率统计:

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
// 任务管理线程
void TaskManager(void const * argument){
addThread(xShell);
addThread(xTaskManager);
addThread(xTest);

Task_t head = getTaskList();

uint32_t lastTotalTicks = xTaskGetTickCount();

while(1){
// 自统计
TaskTickStart(xTaskManager);
// 定义临时指针
Task_t currentTask = head;

uint32_t currentTotalTicks = xTaskGetTickCount(); // 当前系统总滴答数
uint32_t deltaTime = currentTotalTicks - lastTotalTicks; // 计算自上次统计以来的增量时间

while (currentTask != NULL){
// 计算占用公式
currentTask->cpu = (float)currentTask->accumulatedTime / deltaTime * 10.0f;
// 任务移动
currentTask = currentTask->next;
}
lastTotalTicks = currentTotalTicks;

// 1s测量一次,所以最后占用率是乘以10
osDelay(1000);
TaskTickEnd(xTaskManager);
}
}

在每个线程的死循环前后都需要实现运行时间统计:

1
2
3
4
TaskTickStart(xShell);
// TODO do somethings
osDelay(1000);
TaskTickEnd(xShell);

然后定义一个Task头,实现各个线程的Handle和时刻统计宏:

1
2
3
4
5
6
7
8
// 所有线程都需要放在这里进行注册
static osThreadId xTaskInitHandle;
static osThreadId xTaskManagerHandle;
static osThreadId xShellHandle;
static osThreadId xTaskTestHandle;

#define TaskTickStart(task) task->lastWakeTime = xTaskGetTickCount();
#define TaskTickEnd(task) task->accumulatedTime = xTaskGetTickCount() - task->lastWakeTime;

指令系统CMD

指令系统使用了单链表,具体结构体如下:

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
// ===============================[指令操作]===============================

typedef void (*Comand_t)(int argc, char **argv);

typedef struct CMD* CMD_t;

struct CMD{
char* name; // 指令名,如mkdir, rm, ls
char* description; // 指令描述, 用于help
char* usage; // 指令使用方法
/***
* @brief 指令函数要求
* @param argc 参数数量
* @param argv 参数列表(char**)
* */
Comand_t cmd; // 指令主函数
CMD_t next; // 下一个指令
};

static CMD_t CMDList;

// 添加指令
void addCMD(char* name, char* description, char* usage, Comand_t cmd);

// 执行指令
void execCMD(char* command);

void helpCMD(char *cmd);

#define CMD(name, description, usage, cmd) addCMD(name, description, usage, cmd)

这样在指令管理线程内就可以直接使用CMD宏添加内容。这里我新建了一个Command文件夹专门实现各种指令,目前实现了:

  • ls :显示特定目标下的子内容(包括文件夹、设备、线程)
  • info :显示除了FS_t对象以外的对象信息(可以是设备、线程、CPU)
  • echo :串口终端输出一些内容
  • cd :切换串口终端FS_t的指针
  • help : 指令帮助,遍历CMDList匹配name并输出usage

添加指令方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 添加指令
void addCMD(char* name, char* description, char* usage, Comand_t cmd){
CMD_t p = CMDList;
while(p->next != NULL) p = p->next;
CMD_t newCMD = (CMD_t) kernel_alloc(sizeof(struct CMD));

newCMD->name = (char*) kernel_alloc(strlen(name) + 1);
newCMD->description = (char*) kernel_alloc(strlen(description) + 1);
newCMD->usage = (char*) kernel_alloc(strlen(usage) + 1);
strcopy(newCMD->name, name);
strcopy(newCMD->description, description);
strcopy(newCMD->usage, usage);

newCMD->cmd = cmd;
newCMD->next = NULL;
p->next = newCMD;
}

添加指令就是插入节点,接下来是执行指令,这里需要对指令进行解析:

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
// 执行指令
void execCMD(char* command_rel){
// 复制command, 防止command被修改
// 这句话是必须的,一旦后期开启串口DMA可能会造成缓冲异常
char* command = (char*) kernel_alloc(strlen(command_rel) + 1);
strcopy(command, command_rel);

// command解析, command按空格分割保存到argv数组中
char *argv[128] = {0};
int argc = 0;
char* token = strtok(command, " ");
while(token != NULL){
// 按空格分割
argv[argc++] = token;
token = strtok(NULL, " ");
}
// -1是因为指令头也会算入
argc -= 1;


// 查找指令
CMD_t p = CMDList->next;
while(p != NULL){
if(strcmp(p->name, argv[0]) == 0){
p->cmd(argc, &argv[1]);
return;
}
p = p->next;
}

u_print("Command not found\n");
}

每个CMD节点都有一个函数指针,函数指针需要指令参数数目和指令参数列表,一旦匹配到直接使用函数指针运行就可以。下图就是指令列表结构

2

接下来实现指令集帮助,参数输入一个需要查询的指令,直接输出信息即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void helpCMD(char *cmd){
char buf[128];
memoryCopy(buf, cmd, strlen(cmd) + 1);
CMD_t p = CMDList->next;
u_print("Command\t\tDescription\t\tUsage\n");
if(buf[0] == '\0') {
while(p != NULL){
u_print("%s\t\t%s\t\t%s\n", p->name, p->description, p->usage);
p = p->next;
}
}else{
while(p != NULL){
if(strcmp(p->name, cmd) == 0){
u_print("%s\t\t%s\t\t%s\n", p->name, p->description, p->usage);
return;
}
p = p->next;
}

}
}

最后需要初始化DrT数,CMD链表等等,方法如下:

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
/**
* @brief 初始化设备树
*/
void DrTInit(){
// 创建根节点RootFS与命令列表
RAM_FS = (FS_t) kernel_alloc(sizeof(struct FS));
CMDList = (CMD_t) kernel_alloc(sizeof(struct CMD));
CMDList->next = NULL;
RAM_FS->path = "/";

RAM_FS->node = NULL;
RAM_FS->node_count = 0;

RAM_FS->parent = NULL;
RAM_FS->next = NULL;
RAM_FS->child = NULL;

// 串口终端路径
currentFS = RAM_FS;

// 添加子路径
addFSChild(RAM_FS, "dev");
addFSChild(RAM_FS, "tmp");
addFSChild(RAM_FS, "mnt");
addFSChild(RAM_FS, "bin");
addFSChild(RAM_FS, "usr");
addFSChild(RAM_FS, "root");
addFSChild(RAM_FS, "opt");
addFSChild(RAM_FS, "etc");
addFSChild(RAM_FS, "proc");

// 添加设备
addDevice("dev", &CortexM7, "Cortex-M7", "Central Processing Unit", DEVICE_BS, DEVICE_BUSY, NULL);
addDevice("dev", &huart1, "USART1", "Serial bus device", DEVICE_SERIAL, DEVICE_BUSY, NULL);

addDevice("mnt", &hsdram1, "SDMMC", "SD card", DEVICE_STORAGE, DEVICE_ON, NULL);
addDevice("mnt", &SDFatFS, "FATFS", "FAT file system", FILE_SYSTEM, DEVICE_ON, NULL);
addDevice("mnt", &hqspi, "QSPI", "Quad SPI", DEVICE_STORAGE, DEVICE_ON, NULL);


// 指令注册
register_main();
}

指令注册方法如下:

1
2
3
4
5
6
7
8
9
void register_main(){
// 注册指令集
CMD("ls", "List files", "ls /path or ls", ls_main);
CMD("cd", "Change Directory", "cd /path", cd_main);
CMD("info", "list something infomation", "info path/file", info_main);
CMD("echo", "echo something", "echo your want print things", echo_main);
CMD("help", "help using command", "help command", help_main);
// CMD("tree", "show tree", "tree path", tree_main);
}

tree指令没有实现,所以注释了

这里使用ls指令解释一下指令系统的书写:

1
2
3
4
5
6
7
8
9
10
11
12
13
extern FS_t currentFS;

void ls_main(int argc, char **argv){
if (argc > 1)
u_print("ls: too many arguments\r\n");
else if (argc == 1)
ram_ls(argv[0]);
else{
char *pwd = (char *)kernel_alloc(1024);
ram_pwd(currentFS, pwd);
ram_ls(pwd);
}
}

这个函数写起来非常简单,其中使用了ram_pwd和ram_ls方法,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @brief 显示当前目录
* @param fs 当前文件夹
* @param path 路径(已经ram_alloc)
*/
void ram_pwd(FS_t fs, char* path){
FS_t temp_node;
// 递归获取路径
if (fs == RAM_FS) {
strcopy(path, "/");
return;
}else{
temp_node = fs;
ram_pwd(temp_node->parent, path);
strconcat(path, temp_node->path);
strconcat(path, "/");
}

path[strlen(path) - 1] = '\0';
}
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
/**
* @brief 显示目录
* @param path
*/
void ram_ls(char* path){
FS_t node = loadPath(path);
if(node == NULL) return;

FS_t temp = node->next;

while(temp != NULL){
// 输出node的子文件夹
u_print("%s ", temp->path);
temp = temp->child;
}

if (node->node_count != 0) {
DrTNode_t p = node->node;
while(p != NULL){
u_print("%s ", p->name);
p = p->next;
}
}

if(node->tasklist != NULL){
Task_t p = node->tasklist;
while(p != NULL){
u_print("%s\t", p->name);
p = p->next;
}
}

u_print("\n");
}

其中pwd可以直接获取当前FS_t节点的路径,后期也可以实现pwd指令获取当前串口终端位置。

其他对象与U库

虽然线程管理也属于U库,但是因为与DrT交集过多,需要单独提出。

CPU对象是一个结构体:

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
typedef float (*CPU_Func_t)(void);

struct CPU{
// CPU名称
char* name;
// CPU描述
char* description;
// CPU频率
uint32_t frequency;
// CPU温度
double temperature;
// CPU负载
double load;
};

typedef struct CPU* CPU_t;

static CPU_t CortexM7;

/**
* 创建CPU对象
* @return CPU对象
* @note 该函数会自动初始化CPU对象,对象为CortexM7
*/
void createCPU();

void showCPUInfo();

H743iit6的adc3可以采样CPU温度,所以我们实现一下这个方法:

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
float updateCPU(){
HAL_ADC_Start(&hadc3);
uint16_t adc_v = HAL_ADC_GetValue(&hadc3);
double adc_x = (110.0-30.0)/(*(unsigned short*)(0x1FF1E840) - *(unsigned short*)(0x1FF1E820));
return adc_x * (adc_v - *(unsigned short*)(0x1FF1E820)) + 30;
}

/**
* 创建CPU对象
* @return CPU对象
* @note 该函数会自动初始化CPU对象,对象为CortexM7
*/
void createCPU(){
CortexM7 = (CPU_t) kernel_alloc(sizeof(struct CPU));
CortexM7->name = "Cortex-M7";
CortexM7->description = "Cortex-M7 CPU";
CortexM7->frequency = HAL_RCC_GetSysClockFreq();
CortexM7->temperature = 0;
}

void showCPUInfo(){
u_print("CPU name: %s\n", CortexM7->name);
u_print("CPU description: %s\n", CortexM7->description);
u_print("CPU frequency: %d HZ\n", CortexM7->frequency);
float temp = updateCPU();
u_print("CPU temperature:");
put_double(temp, 10, 1);
u_print(" C\n");
}

其中showCPUInfo是显示CPU信息,U库的u_print输出小数存在浮点精度问题,所以单独put_num就可以了。这个showCPUInfo是给info指令使用的,一旦info了CPU就可以显示温度了。

U库实现了u_stdio文件,主要是u_print与字符串方法:

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
#include <stdint.h>

// 布尔值
#define TRUE 1
#define FALSE 0

// 字符串结束符
#define EOL '\n'
#define EOLR '\r'
#define EOLN '\0'

#define ThreadLock __disable_irq()
#define ThreadUnlock __enable_irq()

// 格式字符
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
#define va_start(ap,v) (ap = (va_list)&v + _INTSIZEOF(v))
#define va_arg(ap,t) (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
#define va_end(ap) ( ap = (va_list)0 )

typedef char * va_list;

#define _OS_WEAK __attribute__((weak))
// 对象类型
#define type(x) typeof(x)

/**
* @brief 输出格式化字符串到串口
* @param fmt: 格式化字符串
* @param ...: 可变参数
* @retval 0
* */
int u_print(const char *fmt, ...);

/**
* @brief 输出字符到串口
* @param fmt: 格式化字符串
* @param ap: 可变参数列表
* */
static int v_printf(const char *fmt, va_list ap);

/**
* @brief 输出字符到串口
* @param c: 字符
* */
void put_char(char c);

/**
* @brief 输出数字到串口
* @param num: 数字
* */
void put_num(int num, int base, int sign);

/**
* @brief 输出双精度浮点数到串口
* @param num: 双精度浮点数
* */
void put_double(double num, int base, int sign);

/**
* @brief 输出大数字到串口
* @param num: 大数字
* */
void put_huge_num(uint32_t num, int base, int sign);

/**
* @brief 输出地址到串口
* @param num: 地址
* */
void put_address_num(uint32_t num, int base, int sign);

/**
* @brief 输出字符串到串口
* @param str: 字符串
* */
void put_s(char *str);

/**
* @brief get the length of a string
* @param str: string
* @retval length of the string
* */
int strlen(char *str);

/**
* @brief split a string by a delimiter
* @param str: string
* @param delim: delimiter
* @retval the first token
* */
char* strtok(char *str, char *delim);

/**
* @brief split a string by a delimiter
* @param str: string
* @param delim: delimiter
* @param outlist: output list
* @retval number of elements in the output list
* */
int strspilt(char *str, char *delim, char *outlist[]);

/**
* @brief output formatted string to a buffer
* @param out: output buffer
* @param fmt: format string
* @param ...: variable arguments
* */
void u_sprintf(char *out, const char *fmt, ...);

/**
* @brief copy a string
* @param dest: destination string
* @param src: source string
* */
void strcopy(char *dest, char *src);

/**
* @brief concatenate two strings
* @param dest: destination string
* @param src: source string
* */
void strconcat(char *dest, char *src);

/**
* @brief compare two strings
* @param str1: string 1
* @param str2: string 2
* @retval 0 if equal, 1 if not equal
* */
int strcmp(char *str1, char *str2);


#define LED_ON HAL_GPIO_WritePin(GPIOH, GPIO_PIN_7, GPIO_PIN_RESET)

#define LED_OFF HAL_GPIO_WritePin(GPIOH, GPIO_PIN_7, GPIO_PIN_SET)

内存管理不仅可以管理SDRAM还可以管理QSPI设备(flash),这里flash是ROM,可以掉电保存数据,之后会单开线程保存SDRAM的2M系统内容到flash内。不过目前还用不到,同时也把SD卡挂载了FatFS。

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
// 定义内存管理算法
/***
* @brief 内存拷贝
* @param dest: destination address
* @param src: source address
* @param size: size of the memory block
* */
void memoryCopy(void *dest, void *src, int size);

/***
* @brief 内存设置
* @param dest: destination address
* @param val: value to set
* @param size: size of the memory block
* */
void memorySet(void *dest, char val, int size);

/***
* @brief 内存比较
* @param dest: destination address
* @param src: source address
* @param size: size of the memory block
* @return 比较结果
* */
int memoryCompare(void *dest, void *src, int size);


// ========================= 以下是QSPI Flash内存管理算法 =========================
/***
* @brief QSPI flash写入数据
* @param addr: 写入地址(从0x0000开始到0x0400)
* @param data: 写入数据
* @param size: 写入数据大小
* */
void flashWrite(uint32_t addr, uint8_t *data, uint32_t size);

/***
* @brief QSPI flash读取数据
* @param addr: 读取地址
* @param data: 读取数据存放地址
* @param size: 读取数据大小
* */
void flashRead(uint32_t addr, uint8_t *data, uint32_t size);

// ========================= 以下是SDRAM内存管理算法 =========================

// 定义IO操作
#define IO_U8 *(__IO uint8_t*) // 定义IO_U8为一个指向uint8_t的指针
#define IO_U16 *(__IO uint16_t*) // 定义IO_U16为一个指向uint16_t的指针
#define IO_U32 *(__IO uint32_t*) // 定义IO_U32为一个指向uint32_t的指针
#define IO_U64 *(__IO uint64_t*) // 定义IO_U64为一个指向uint64_t的指针
#define IO_VOID *(__IO void*) // 定义IO_VOID为一个指向void的指针
#define IO_STRUCT(Pointer) *(__IO Pointer) // 定义IO_STRUCT为一个指向结构体或函数的指针

这里为了之后的自定义编程语言,在SDRAM内实现了结构体和函数数据指针,函数也是一组指令,也可以保存在内存,所以IO_STRUCT非常有必要。

为了获取随机数,获取系统时间,这里打开了RTC和RAM方法:

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
#include <stdint-gcc.h>

// 开机时间, 需要根据实际情况修改
#define Correct_Year 2024
#define Correct_Month 8 - 1
#define Correct_Day 14 - 1
#define Correct_Hour 22
#define Correct_Minute 9

/***
* 获取当前秒
* @return 当前秒
*/
uint8_t getSecond();

/***
* 获取当前分钟
* @return 当前分钟
*/
uint8_t getMinute();

/***
* 获取当前小时
* @return 当前小时
*/
uint8_t getHour();

/***
* 获取当前日期
* @return 当前日期
*/
uint8_t getDay();

/***
* 获取当前月份
* @return 当前月份
*/
uint8_t getMonth();

/***
* 获取当前年份
* @return 当前年份
*/
uint16_t getYear();

/***
* 更新时间
*/
void UpdateTime();

/***
* 获取随机数
* @return 随机数
*/
uint32_t Random();

而其中重要的就是updateTime:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/***
* 更新时间
*/
void UpdateTime(){
HAL_RTC_GetTime(&hrtc,&Time_Struct,RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc,&Date_Struct,RTC_FORMAT_BIN);
}

/***
* 获取随机数
* @return 随机数
*/
uint32_t Random(){
uint32_t *random = NULL;
HAL_RNG_GenerateRandomNumber(&hrng, random);
return *random;
}

u库都是硬件驱动,其他的内容都在线程内像开发操作系统一样开发即可。

最后就是一些杂七杂八的初始化:

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
// 初始化全局任务
void taskGlobalInit(){
// 输出SD卡信息, 确保SD卡正常工作
HAL_SD_CardInfoTypeDef SDCardInfo;
HAL_SD_CardCIDTypeDef SDCard_CID;

HAL_SD_GetCardCID(&hsd1,&SDCard_CID);
HAL_SD_GetCardInfo(&hsd1,&SDCardInfo);
uint64_t CardCap=(uint64_t)(SDCardInfo.LogBlockNbr)*(uint64_t)(SDCardInfo.LogBlockSize); //计算SD卡容量
u_print("SD card Drive Capacitor: %D MB\r\n", (uint32_t)(CardCap>>20));

// CPU采样初始化
HAL_ADCEx_Calibration_Start(&hadc3,ADC_CALIB_OFFSET,ADC_SINGLE_ENDED);
HAL_ADC_Start(&hadc3); /* 启动ADC3的转换 */

// QSPI Flash初始化,擦除所有数据
if(QSPI_W25Qxx_BlockErase_32K(0) != QSPI_W25Qxx_OK)
u_print("Erase Failed\n");
else u_print("QSPI Flash Succeed, ID: %d\n", QSPI_W25Qxx_ReadID());

// RAMFS初始化
DrTInit();
// CPU结构体初始化(用于标注CPU的信息)
createCPU();
}
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
/*** 声明 **
* @note: 该函数用于初始化SD卡,如果SD卡未挂载,则尝试格式化SD卡
* 本项目使用CubeMX生成的Fatfs,因使用了Freertos,Fatfs必须使用FreeRTOS的消息队列
* 所以需要在osKernelStart()之后初始化Fatfs,这个函数也必须放在线程内。
* 注意:需要开机MDMA才可以正常使用FATFS的f_mkfs函数
* */
void QueueInit(void const * argument){
// SD卡挂载FATFS
FRESULT FSRes = f_mount(&SDFatFS,SDPath,1);
BYTE work[_MAX_SS];
// 如果挂载失败,尝试格式化SD卡
if (FSRes != FR_OK){
// 创建FAT32文件系统
FSRes = f_mkfs(SDPath, FM_FAT32, 0, work, sizeof work);
// 判断是否初始化成果
if (FSRes == FR_OK) {
// 初始化成功,重新挂载
u_print("SD card init succeed\r\n");
f_mount(&SDFatFS,SDPath,1);
}
// 初始化失败,提示用户更换SD卡
else u_print("Init Faild, please replace SD card\r\n");
}
// 初始化成功,提示用户
else u_print("SD card Succeed\r\n");
// 一次性初始化完成,挂起初始化任务
vTaskSuspend(xTaskInitHandle);
}

项目规划

实现其他的指令等等内容,按照github内容即可。


在STM32实现Linux操作系统与RAM文件系统
https://blog.minloha.cn/posts/171115410851c42024091116.html
作者
Minloha
发布于
2024年9月11日
更新于
2024年9月15日
许可协议