嵌入式——FreeRTOS操作系统——学习笔记

嵌入式——FreeRTOS操作系统——学习笔记

H_Haozi Lv2

A引入

A.1移植FreeRTOS至Stm32F103C8T6(标准库)

因为暂时只学了标准库,而正点原子里的教程移植HAL库,所以参考别人https://blog.csdn.net/cairongshou/article/details/129090567来移植。
虽然最后发现移植过程都差不太多emm

B笔记

先附一个图

B.1临界段/区代码保护

什么是临界段:

临界段代码也称临界区,指那些必须完整运行而不能被中断打断的代码段。

适用场合:

1:外设的各种初始化函数,IIC,SPI等等
2:系统自身需求
3:用户需求

什么东西可以打断当前程序的运行

  1. 中断
  2. 任务调度 PendSV
    PendSV是可悬起异常,如果我们把它配置最低优先级,那么如果同时有多个异常被触发,它会在其他异常执行完毕后再执行,而且任何异常都可以中断它。由于PendSV的特点就是支持【缓期执行】,所以嵌入式OS可以利用它这个特点,进行任务调度过程的上下文切换。

怎么进入临界区

调用API函数:

1
2
3
4
5
6
7
taskENTER_CRITICAL();           //任务级进入临界区  
/*本质上就是关闭中断,但是FreeRTOS只能管理5~15优先级之间的中断,也就是说进入临界区之后优先级大的那几个中断(0~4)
还是可以打断程序执行 */
taskEXIT_CRITICAL(); //任务级退出临界区
//本质上就是打开中断
taskENTER_CRITICAL_FROM_ISR(); //中断级进入临界区
taskEXIT_CRITICAL_FROM_ISR(); //中断级退出临界区

任务级示例:

1
2
3
4
5
taskENTER_CRITICAL();  //进入
{
/*临界区代码*/
}
taskEXIT_CRITICAL(); //退出

中断级示例:

1
2
3
4
5
6
7
8
9
void ISR()  /*中断服务函数*/
{
uint32_t save_status; //用来记录中断屏蔽寄存器的值
save_status = taskENTER_CRITICAL_FROM_ISR(); //进入
{
/*临界区代码*/
}
taskEXIT_CRITICAL_FROM_ISR(save_status); //退出
}

任务调度器的挂起

挂起任务调度器,但是不关闭中断,仅仅是为了防止任务与任务之间的资源争夺。
适合用于临界区在任务与任务之间,不用区延时中断,又可以保证临时区的安全。

1
2
3
4
5
xTaskSuspendALL(); //挂起任务调度器
{
/*临界区代码*/
}
xTaskResumeALL(); //恢复任务调度器

B.2列表和列表项

列表是 FreeRTOS 中的一个数据结构,概念上和链表有点类似,列表被用来跟踪 FreeRTOS中的任务。
列表项就是存放在列表中的项目。
(长的就是个双向循环链表,列表项相当于节点)

优点同链表,成员数量好更改,增删改查
列表和列表项的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct xLIST
{
listFIRST_LIST_INTEGRITY_CHECK_VALUE /*校验值(调试中使用,一般默认不开启)*/
volatile UBaseType_t uxNumberOfItems; /*列表中的列表项数量(不包含末尾/迷你列表项)*/
ListItem_t * configLIST_VOLATILE pxIndex; /*用于遍历列表项的指针*/
MiniListItem_t xListEnd; /*末尾列表项/迷你列表项*/
listSECOND_LIST_INTEGRITY_CHECK_VALUE /*校验值*/
} List_t;

struct xLIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /*检测列表项完整性*/
configLIST_VOLATILE TickType_t xItemValue; /*列表项的值*/
struct xLIST_ITEM * configLIST_VOLATILE pxNext; /*下一个列表项*/
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /*上一个列表项*/
void * pvOwner; /*列表项的拥有者(比如Task1)*/
struct xLIST * configLIST_VOLATILE pxContainer; /*列表项所在列表(比如就绪态列表)*/
listSECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE /*检测列表项完整性*/
};
typedef struct xLIST_ITEM ListItem_t;

迷你列表项/末尾列表项
迷你列表项也是列表项,但迷你列表项仅用于标记列表的末尾和挂载其他插入列表中的列表项,类似于链表的头结点
迷你列表项中没有成员变量,以节省开销

1
2
3
4
5
6
7
8
struct xMINI_LIST_ITEM
{
listFIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE /*检测列表项完整性*/
configLIST_VOLATILE TickType_t xItemValue; /*列表项的值*/
struct xLIST_ITEM * configLIST_VOLATILE pxNext; /*上一个列表*/
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious; /*下一个列表*/
};
typedef struct xMINI_LIST_ITEM MiniListItem_t;

列表项的初始化和插入等链表的插入基本相似
列表相关的API函数代码详解查看手册《FreeRTOS开发指南》第七章 –“FreeRTos列表和列表项’

B.2任务调度器

API函数

1
2
vTaskStartScheduler();  //开启任务调度器
//....全是源码,后面再看吧

B.3时间片调度

简介

在FreeRTOS中,一个时间片等于 SysTick的中断周期,可以通过设置滴答定时器的中断频率来更改时间片

运行过程

在相同优先级的n个任务,第一个任务运行一个时间片后,切换到第二个任务,第二个任务运行一个时间片后,切换到第三个任务……直到到最后一个任务,碰到阻塞,再运行第一个任务,不断循环。

B.4各种任务相关API函数

在使用 uxTaskGetSystemState() 函数的时候出现了问题

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
//任务二:实现任务API函数使用
void Task2(void *pvParameters)
{

UBaseType_t priority_num = 0;
priority_num = uxTaskPriorityGet(task1_handler); //获取任务1的任务优先级
Serial_Printf("Task1任务优先级:%d\r\n",priority_num);

vTaskPrioritySet(task2_handler,23); //设置任务2的优先级为23
priority_num = uxTaskPriorityGet(NULL); //获取任务的2任务优先级
Serial_Printf("Task2任务优先级:%d\r\n",priority_num);

UBaseType_t task_num = 0;
task_num = uxTaskGetNumberOfTasks(); //获取任务总数
Serial_Printf("任务总数:%d\r\n",task_num);
//启动任务调度器函数中开启了两个任务: 空闲任务 和 软件定时器任务 加上这个文件创建的3个任务一共是5个
//虽然已经删除启动任务了,但是只有在执行空闲函数的时候,才会真正删除启动任务吧(应该)

UBaseType_t task_num2;
TaskStatus_t * status_array;
status_array = pvPortMalloc( task_num * sizeof( TaskStatus_t ) ); //申请内存空间:总任务数*结构体大小
//pvPortMalloc() 为FreeRTOS提供的函数
//status_array = (TaskStatus_t *)my_mem_malloc(SRAMIN,(sizeof(TaskStatus_t) * task_num)); //正点原子内存管理函数
//status_array = (TaskStatus_t *)malloc((sizeof(TaskStatus_t) * task_num)); //c语言库函数

task_num2 = uxTaskGetSystemState(status_array,task_num,NULL); //查找所有任务的信息 返回值是任务个数
/* TaskStatus_t * const pxTaskStatusArray */
/* const UBaseType_t uxArraySize */
/* configRUN_TIME_COUNTER_TYPE * const pulTotalRunTime */ //时间统计,这里先不使用
Serial_Printf("任务名\t\t任务优先级\t任务编号\r\n");
for(int i=0;i<task_num2;i++) //循环打印
{
Serial_Printf("%s\t\t%d\t%d\r\n",status_array[i].pcTaskName,
status_array[i].uxCurrentPriority,
status_array[i].xTaskNumber);
}
//这里我自己的打印结果 里面并没有 task1 任务的信息
// 或者 是 task1 任务的信息有误(任务编号无穷大/任务名乱码)

//下面为当没有task1任务的情况出现时我加的代码
Serial_Printf("1:%s\r\n",status_array[0].pcTaskName);
Serial_Printf("2:%s\r\n",status_array[1].pcTaskName); //发现这个数组里面直接为空
Serial_Printf("3:%s\r\n",status_array[2].pcTaskName);
Serial_Printf("4:%s\r\n",status_array[3].pcTaskName);
Serial_Printf("5:%s\r\n",status_array[4].pcTaskName);
Serial_Printf("6:%s\r\n",status_array[5].pcTaskName);

TaskStatus_t * status_array2;
status_array2 = pvPortMalloc( sizeof( TaskStatus_t ) ); //申请内存空间

vTaskGetInfo( task1_handler,status_array2,pdTRUE,eInvalid);
/* TaskHandle_t xTask,
TaskStatus_t * pxTaskStatus,
BaseType_t xGetFreeStackSpace,
eTaskState eState ) */

Serial_Printf("任务名:%s\r\n",status_array2->pcTaskName); //这里依旧有问题
Serial_Printf("任务优先级:%ld\r\n",status_array2->uxCurrentPriority);
Serial_Printf("任务编号:%ld\r\n",status_array2->xTaskNumber); //这里依旧有问题
Serial_Printf("任务状态:%d\r\n",status_array2->eCurrentState);

TaskHandle_t task1_handle = 0;
task1_handle = xTaskGetHandle("Task1"); //通过任务名获取任务句柄
Serial_Printf("任务1句柄:%#x\r\n",(int)task1_handle); //这里输出的是0,还是获取失败
Serial_Printf("任务1句柄:%#x\r\n",(int)task1_handler);

TaskHandle_t task2_handle = 0;
task2_handle = xTaskGetHandle("Task2"); //通过任务名获取任务句柄
Serial_Printf("任务2句柄:%#x\r\n",(int)task2_handle);
Serial_Printf("任务2句柄:%#x\r\n",(int)task2_handler);

UBaseType_t task_stack_min = 0;

eTaskState state = 0;
state = eTaskGetState(task2_handle); //根据句柄查询任务状态
Serial_Printf("任务2当前状态:%d\r\n",state);

vTaskList( task_buff );
Serial_Printf("%s\r\n",task_buff);
//这里我修改了 Serial_Printf()函数定义中的 char String[100]; --> char String[300];
//同时增加了task2的 堆栈大小
//按需要修改即可,如果过小,可能程序会卡死,即下面循环里的最小堆栈位不再打印或者led灯不再闪烁


while(1)
{
task_stack_min = uxTaskGetStackHighWaterMark(task2_handler); //当我写到这里的时候,前面的错误也找到了
Serial_Printf("task2剩余最小堆栈位%ld\r\n",task_stack_min); //因为任务2的堆栈爆辣!!!!把TASK2_SIZE的64改成128就行
vTaskDelay(1000);
}
}

B.5时间统计API函数

在使用时间统计API函数之前,有以下流程

  1. 将宏configGENERATE_RUN_TIME_STATS 置 1
  2. 将宏configUSE_STATS_FORMATTING_FUNCTIONS 置 1
  3. 然后还要实现两个宏定义:
  1. portCONFIGURE_TIMER_FOR_RUN_TIME_STATS(): 用于初始化用于配置任务运行时间统计的时基定时器;
    注意:这个时基定时器的计时精度需高于系统时钟节拍精度的10至100倍
  1. portGET_RUN_TIME_COUNTER_VALUE(): 用于获取该功能时基硬件定时器计数的计数值。
1
2
3
4
5
6
7
8
//这个是在FreeRTOS的配置文件中
#define configGENERATE_RUN_TIME_STATS 1 //为1时启用运行时间统计功能

#include "timer.h" //在这个文件中定义下面的函数
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() ConfigureTimeForRunTimeStats()
extern uint32_t FreeRTOSRunTimeTicks;
#define portGET_RUN_TIME_COUNTER_VALUE() FreeRTOSRunTimeTicks

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
//在timer.h文件中

uint32_t FreeRTOSRunTimeTicks = 0; //创建计数器

void ConfigureTimeForRunTimeStats(void)
{
/************************************ 开启时钟TIM3 *****************************************************************/

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //开启TIM3的时钟

/*配置时钟源*/
TIM_InternalClockConfig(TIM3); //选择TIM3为内部时钟,若不调用此函数,TIM默认也为内部时钟

/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
TIM_TimeBaseInitStructure.TIM_Period = 10 - 1; //计数周期,即ARR的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //预分频器,即PSC的值 T = (72Mhz/ARR+1)/(PSC+1)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器,高级定时器才会用到
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM3的时基单元

/*中断输出配置*/
TIM_ClearFlag(TIM3, TIM_FLAG_Update); //清除定时器更新标志位
//TIM_TimeBaseInit函数末尾,手动产生了更新事件
//若不清除此标志位,则开启中断后,会立刻进入一次中断
//如果不介意此问题,则不清除此标志位也可

TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); //开启TIM3的更新中断

/*NVIC中断分组*/
//NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //配置NVIC为分组4
//即抢占优先级范围:0~`15,响应优先级范围:0
//此分组配置在整个工程中仅需调用一次
//若有多个中断,可以把此代码放在main函数内,while循环之前
//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置

/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //选择配置NVIC的TIM3线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 6; //指定NVIC线路的抢占优先级为6
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //指定NVIC线路的响应优先级为0
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设

/*TIM使能*/
TIM_Cmd(TIM3, ENABLE); //使能TIM3,定时器开始运行
FreeRTOSRunTimeTicks = 0;
}

//计数器自加
void TIM3_IRQHandler(void)
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) == SET)
{

TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
FreeRTOSRunTimeTicks++;
}
}

然后就可以直接调用vTaskGetRunTimeStats();函数,使用方法和上面的vTaskList()使用方法相同。

1
2
3
4

vTaskGetRunTimeStats(task_buff); //获取运行时间统计函数的表格
Serial_Printf("%s\r\n",task_buff); //串口输出

B.6队列相关函数

这个就直接贴成功运行的代码了:

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

/***********************************************************************************************/
/*
*程序名称:队列操作实验
*/
/***********************************************************************************************/

uint8_t key_num;
QueueHandle_t key_queue; // 小数据句柄
QueueHandle_t big_data_queue; // 大数据句柄
char buff[100] = "难道说还是报错";

#define START_TASK_PRIO 1
#define START_TASK_SIZE 64
TaskHandle_t start_task_handler;
void Start_Task(void *pvParameters);

#define TASK1_PRIO 2
#define TASK1_SIZE 64
TaskHandle_t task1_handler;
void Task1(void *pvParameters);

#define TASK2_PRIO 4
#define TASK2_SIZE 256
TaskHandle_t task2_handler;
void Task2(void *pvParameters);

#define TASK3_PRIO 4
#define TASK3_SIZE 256
TaskHandle_t task3_handler;
void Task3(void *pvParameters);

void freertos_demo()
{
//小数据队列创建
key_queue = xQueueCreate( 2, sizeof(uint8_t)); //两个key 返回key的值大小为uint8_t
if(key_queue != NULL)
{
Serial_Printf("小创建成功\r\n");
}
else Serial_Printf("小创建失败\r\n");

//大数据队列创建
big_data_queue = xQueueCreate( 1, sizeof(char*)); //一个数组 返回地址大小为char*
if(big_data_queue != NULL)
{
Serial_Printf("大创建成功\r\n");
}
else Serial_Printf("大创建失败\r\n");

xTaskCreate(
(TaskFunction_t ) Start_Task,
(char *) "Start_Task",
(configSTACK_DEPTH_TYPE ) START_TASK_SIZE,
(void *) NULL,
(UBaseType_t ) START_TASK_PRIO,
(TaskHandle_t * ) &start_task_handler
);
vTaskStartScheduler(); //开启任务调度器
}

//开始任务:创建任务一,任务二和任务三
void Start_Task(void *pvParameters)
{
taskENTER_CRITICAL(); //进入临界区,即关闭比该任务优先级大的“中断任务”
Serial_SendString("start_ing\r\n");
xTaskCreate(
(TaskFunction_t ) Task1,
(char *) "Task1",
(configSTACK_DEPTH_TYPE ) TASK1_SIZE,
(void *) NULL,
(UBaseType_t ) TASK1_PRIO,
(TaskHandle_t * ) &task1_handler
);

xTaskCreate(
(TaskFunction_t ) Task2,
(char *) "Task2",
(configSTACK_DEPTH_TYPE ) TASK2_SIZE,
(void *) NULL,
(UBaseType_t ) TASK2_PRIO,
(TaskHandle_t * ) &task2_handler
);
xTaskCreate(
(TaskFunction_t ) Task3,
(char *) "Task3",
(configSTACK_DEPTH_TYPE ) TASK3_SIZE,
(void *) NULL,
(UBaseType_t ) TASK3_PRIO,
(TaskHandle_t * ) &task3_handler
);
//删除自身任务
vTaskDelete(NULL);
taskEXIT_CRITICAL(); //退出临界区
}


//任务一: 实现入队
void Task1(void *pvParameters)
{
BaseType_t err =0; //判断接受失败或成功
char *data_to_send = buff; // 直接使用 buff 数据
while(1)
{
taskENTER_CRITICAL(); //进入临界区,即关闭比该任务优先级大的“中断任务”
key_num = Key_GetNum();
if(key_num == 1 || key_num == 2)
{
err = xQueueSend( key_queue, &key_num, portMAX_DELAY );//句柄,写入值的地址,延时(死等)
if(err != pdTRUE)
{
Serial_Printf("小发送失败\r\n");
}
}
else if(key_num == 3)
{
err = xQueueSend( big_data_queue, &data_to_send, portMAX_DELAY );//句柄,写入值的地址,延时(死等)
if(err != pdTRUE)
{
Serial_Printf("大发送失败\r\n");
}
else
{
Serial_Printf("大发送成功\r\n");
}
}
taskEXIT_CRITICAL(); //退出临界区
vTaskDelay(10);
}
}

//任务二:小数据出队
void Task2(void *pvParameters)
{
uint8_t key_r = 0; // 接受数据
BaseType_t err =0; //判断接受失败或成功
while(1)
{
/*注意这个任务可以不加延时函数,因为当队列为空时会系统会自动切换任务二到阻塞态*/
err = xQueueReceive( key_queue, //句柄
&key_r, //读取到->地址
portMAX_DELAY ); //死等
if(err != pdTRUE)
{
Serial_Printf("小接收失败\r\n");
}
else
{
Serial_Printf("小数据为:%d\r\n",key_r);
}
}
}

//任务三: 大数据出队
void Task3(void *pvParameters)
{
char *buf = NULL; // 接收数据时的指针
BaseType_t err = 0;
while(1)
{
err = xQueueReceive(big_data_queue, &buf, portMAX_DELAY);
if(err != pdTRUE)
{
Serial_Printf("大数据接收失败\r\n");
}
else
{
UBaseType_t task_stack_min = uxTaskGetStackHighWaterMark(task2_handler);
//Serial_Printf("任务2剩余最小堆栈空间:%ld\r\n", task_stack_min);
Serial_Printf("大数据接收成功\r\n");
Serial_Printf("%s\r\n", buf); // 直接打印接收到的字符串
}
}
}

暂时只在打印的时候遇到了问题(打印出现乱码),解决方法见C.4;

B.7信号量

信号量简介

信号量是一种解决同步问题的机制,可以实现对共享资源的有序访问

信号量(计数值)>1 代表有资源
当释放信号量,计数值就加1
当获取信号量,计数值就减1
信号量的作用就是用来传递状态

信号量的计数值会有限制:限定最大值
如果最大值为1,则它是二值信号量
如果最大值不是1,则它是计数型信号量

二值信号量

二值信号量的本质是一个队列长度为1的队列,队列只有空和满两种情况。
通常用于互斥访问或任务同步,与互斥信号量比较类型,但是二值信号量
有可能会导致优先级翻转的问题,所以更适合用于同步。

创建二值信号量:
动态:xSemaphoreCreateBinary(void)
静态:xSemaphoreCreateBinaryStatic(void)
返回值:NULL 表示创建失败 其它表示二值信号量的句柄

释放二值信号量:相当于把该标志置”满”
任务级:xSemaphoreGive()
中断级:xSemaphoreGiveFromISR()
形参:要释放的信号量句柄,不支持设置阻塞时间
返回值:pdPASS表示释放成功,errQUEUE_FULL表示释放失败

获取二值信号量:相当于把该标志置”空”
任务级:xSemaphoreTake()
中断级:xSemaphoreTakeFromISR()
形参:要释放的信号量句柄,阻塞时间
返回值:pdTRUE表示获取成功,pdFALSE表示获取失败

二值信号量API函数实验

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 "semphr.h" //相关头文件
//创建二值信号量
semphore_handle = xSemaphoreCreateBinary();
if(semphore_handle != NULL)
{
Serial_Printf("创建成功\r\n");
}

//任务一: 按键扫描,检查到按钮1按下,释放二值信号量
void Task1(void *pvParameters)
{
BaseType_t err = 0;
while(1)
{
taskENTER_CRITICAL(); //进入临界区,即关闭比该任务优先级大的“中断任务”
key_num = Key_GetNum();
if(key_num == 1)
{
if(semphore_handle != NULL)
{
err = xSemaphoreGive(semphore_handle);//释放信号量
if(err == pdPASS)
{
Serial_Printf("释放成功\r\n");
}
}
else
{
Serial_Printf("释放失败\r\n");
}
}
taskEXIT_CRITICAL(); //退出临界区
vTaskDelay(10);
}
}
//任务二:获取二值信号量,获取成功后打印提示信息
void Task2(void *pvParameters)
{
uint32_t i = 0;
BaseType_t err = 0;
while(1)
{
//如果获取不到信号量,就死等,进入阻塞态
err = xSemaphoreTake(semphore_handle,portMAX_DELAY);//获取信号量
if(err == pdTRUE)
{
Serial_Printf("获取成功:%d\r\n",i++);
}
else if(err == pdFALSE) Serial_Printf("获取失败");
}
}

计数型信号量

计数型信号量相当于队列大于1的队列。因此计数型信号量可以容纳多个资源,队列长度是在创建它时确定的

使用过程:创建计数型信号量->释放信号量->获取信号量

创建二值信号量:
动态:xSemaphoreCreateCounting()
静态:xSemaphoreCreateCountingStatic()
形参:uxMaxCount计数值的最大限定值,uxInitialCount计数值的初始值
返回值:NULL 表示创建失败 其它表示二值信号量的句柄

获取信号量的计数值:
xSemaphoreGetCount()

释放获取信号量的API函数与二值信号量相同

优先级翻转

简介:高优先级的任务反而慢执行,低优先级的任务反而先执行
在抢占式内核中很常见,但是在实时操作系统中是不允许出现的,会破坏任务的预期顺序可能会导致位置的严重后果

在使用二值信号量的时候,可能会导致优先级翻转

*start_task: 创建task1和taks2和task3
*task1: 低优先级任务,先获取信号量,然后执行一段较长时间的任务,然后释放信号量
*task2: 中等优先级任务,简单的应用任务
*task3: 高优先级任务,先获取信号量,然后执行一段拍普通时间的任务,然后释放信号量
比如在运行这个程序的时候,当task1获得信号量时,执行任务的时候,被高优先级task3抢占,但是因为task1还没有释放,所以task3进入阻塞,一直到task1释放,task3才可以运行,优先级发生了翻转,具体现象见实验13

互斥信号量

互斥信号量是一个拥有优先级继承的二值信号量,在同步的应用中使用二值信号量最适合,而互斥信号量适合用于就那些需要互斥访问的应用中。可以用来解决二值信号量的优先级翻转的问题

优先级继承

当一个互斥信号量正在被一个低优先级的任务持有时,如果此时有个高优先级的任务也尝试获取这个互斥信号量,那么这个高优先级的任务就会被阻塞。但是这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级。
互斥信号量不能用于中断服务函数,整你用于任务中

使用流程:创建互斥信号量->(task)获取信号量->(give)释放信号量
注意互斥信号量创建后会主动释放一次信号量

创建互斥信号量:
动态:xSemaphoreCreateMutex()
静态:xSemaphoreCreateMutexStatic()
形参:uxMaxCount计数值的最大限定值,uxInitialCount计数值的初始值
返回值:NULL 表示创建失败 其它表示二值信号量的句柄

释放获取信号量的API函数与二值信号量相同,但是不支持中断中调用

B.8队列集

用来解决在一个任务中同时要使用多个队列的情况。如果按照之前的用法,在第一个队列没有获得消息时会阻塞,这样下一个队列即使有信息也无法执行。

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

//定义句柄
QueueSetHandle_t queueset_handle;
QueueHandle_t queue_handle;
QueueHandle_t semphr_handle;

//任务一:创建队列,信号量和队列集
queueset_handle = xQueueCreateSet( 2 ); //创建队列集
if(queueset_handle != NULL)
{
Serial_Printf("队列集创建成功\r\n");
}
else Serial_Printf("队列集创建失败\r\n");

queue_handle = xQueueCreate( 1, sizeof(uint8_t) ) ;//创建队列
semphr_handle = xSemaphoreCreateBinary(); //创建信号量

xQueueAddToSet(queue_handle,queueset_handle); //添加到队列集
xQueueAddToSet(semphr_handle,queueset_handle); //添加到队列集


//任务二:获取队列信息
void Task2(void *pvParameters)
{
while(1)
{
uint8_t key_r = 0; // 接受数据
QueueSetMemberHandle_t member_handle;
member_handle = xQueueSelectFromSet( queueset_handle,portMAX_DELAY); //返回队列句柄
if(member_handle == queue_handle)
{
xQueueReceive(member_handle,&key_r,portMAX_DELAY);
Serial_Printf("获取到的队列数据为%d\r\n",key_r);
}
else if(member_handle == semphr_handle)
{
xSemaphoreTake(semphr_handle,portMAX_DELAY);
Serial_Printf("获取信号量成功!\r\n");
}
}

}

B.9事件标志组

用一个32位字符的前24位的(0或1)表示事件是否发生,然后执行相关操作

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

//宏定义标志位,后续使用方便
#define EVENTBIT_0 (1 << 0)
#define EVENTBIT_1 (1 << 1)
#define EVENTBIT_2 (1 << 2)

//创建标志位组
eventgroup_handle = xEventGroupCreate();

//任务一: 按键扫描,根据不同的键值将时事件标志组相应的事件置一,模拟事件发生
void Task1(void *pvParameters)
{
while(1)
{
key_num = Key_GetNum();
if(key_num == 1)
{
xEventGroupSetBits( eventgroup_handle,EVENTBIT_0); //将指定位置1,表示事件已经发生
Serial_Printf("0\r\n");
}
if(key_num == 2)
{
xEventGroupSetBits( eventgroup_handle,EVENTBIT_1);
Serial_Printf("1\r\n");
}
if(key_num == 3)
{
xEventGroupSetBits( eventgroup_handle,EVENTBIT_2);
Serial_Printf("2\r\n");
}
vTaskDelay(10);
}
}

//任务二:同时等待事件标志组中的多个事件位,当这些事件全部为1时就执行相应的处理
void Task2(void *pvParameters)
{
EventBits_t event_bit;
while(1)
{
event_bit= xEventGroupWaitBits( eventgroup_handle, /*事件标志组句柄*/
EVENTBIT_0 | EVENTBIT_1 | EVENTBIT_2 , /*等待事件标志组的bit0,bit1,bit2*/
pdTRUE, /*成功等待到后清楚事件bit0,bit1,bit2标志位*/
pdTRUE, /*等待事件标志组bit0,bit1,bit2都发生*/
portMAX_DELAY); /*死等*/
Serial_Printf("等待的事件标志位值为:%#x\r\n",event_bit); /*bit0,bit1,bit2或起来应该是0x7*/
}
}

B.10样例实验

  1. 任务的创建和删除(动态)
  2. 任务的创建和删除(静态)
  3. 任务挂起和恢复
  4. 中断管理
  5. 列表项的插入和删除实现
  6. 时间片调度
  7. 任务相关API函数
  8. 任务时间统计API函数
  9. 延时函数延时
  10. 队列相关API函数
  11. 二值信号量
  12. 计数型信号量
  13. 模拟优先级翻转
  14. 互斥信号量
  15. 队列集
  16. 事件标志组

C遇到的问题

C.1在实验1,2中oled屏幕显示出现部分错位花屏等现象

问题原因:

oled 屏幕显示函数执行的时间不够,因切换任务而被打断。

解决方法:

  1. 添加函数taskENTER_CRITICAL()taskEXIT_CRITICAL() 在显示的时候进入临界区,防止被打断。
  2. 在比oled高优先级任务中**增加vTaskDelay()**函数的时长,这么显示函数的执行时间便会增长。

C.2在实验7中,出现报错:..\OBJ\LED.axf: Error: L6915E: Library reports error: __use_no_semihosting was requested, but _ttywrch was referenced

解决方法

1:在魔术棒中Target选项中 将Use MicroLIB勾选上,然后编译没有报错

问题原因

因为使用了malloc函数,反之如果想使用malloc函数就需要勾选上面这个选项并include “stdlib.h”

C.3在实验7中,调用uxTaskGetSystemState()函数出现问题

问题描述

函数执行完毕后 打印结果里面并没有 task1 任务的信息 或者 是 task1 任务的信息有误(任务编号无穷大/任务名乱码)

尝试直接不创建任务1,其它代码不变

使用vTaskGetInfo() 函数 查询Task1 依旧有错误

问题原因

执行这些查询操作函数的任务2的堆栈爆了,所以报错
使用 uxTaskGetStackHighWaterMark() 函数查询task2的时候发现的
当时定义的64,但实际需使用至少78,所以爆了。

解决方法

使用 uxTaskGetStackHighWaterMark() 函数查询各个任务,将堆栈大小设置在合理范围即可

C.4在实验10中大数据打印出现乱码

问题描述

在打印的时候遇到了问题(打印出现乱码)
Serial_Printf("%s\r\n", buf); // 直接打印接收到的字符串

问题原因

原因是我在创建大数据数组的时候
char buff[100] = "难道说还是报错";
err = xQueueSend(big_data_queue, &buff, portMAX_DELAY);
在 Task1 中将数据 “难道说还是报错” 作为 char* 类型的指针传入队列,
这样会导致队列存储的是一个指向字符串的指针,而不是字符串本身。队列
中的数据是一个指针,接收端 Task3 正确地接收了这个指针,但没有正确地
获取到实际的字符串内容,而导致打印出来可能会出现乱码。

解决方法

在任务1中发送字符串内容(通过指针传递)而不是发送指针本身。
添加一行代码:
char *data_to_send = buff; // 直接使用 buff 数据
修改一行代码:
err = xQueueSend(big_data_queue, &data_to_send, portMAX_DELAY);

D多线程项目

E结尾

参考文献

  1. 【正点原子】手把手教你学FreeRTOS实时系统
  2. FreeRTOS官方文档
  3. 基于STM32F103标准库移植FreeRTOS教程

文件地址

  1. github仓库
  2. 网盘地址 提取码: 92jt

留言

有问题请指出,你可以选择以下方式:

  1. 在下方评论区留言
  2. 邮箱留言
  • Title: 嵌入式——FreeRTOS操作系统——学习笔记
  • Author: H_Haozi
  • Created at : 2024-10-29 08:07:49
  • Updated at : 2025-03-14 17:42:55
  • Link: https://redefine.ohevan.com/2024/10/29/embedded_freertos/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments