前言:
函数指针:本质意义上是一个指针,指向某一个函数。
#include "stdio.h" //包含输入输出头文件
void (*funcp)( ); //声明函数指针
void FileFunc( ),EditFunc( ); //声明函数
void main(void)
{
funcp=FileFunc; //FileFunc函数的地址赋给funcp
(*funcp)( );
funcp=EditFunc; //EditFunc函数的地址赋给funcp
(*funcp)( );
}
void FileFunc( ) //函数
{
printf("File\n");
}
void EditFunc( ) //函数
{
printf("Edit\n");
}
指针函数:本质上是一个函数,返回值是一个指针。
类型标识符 *函数名(参数表)
int *f(x,y);
首先它是一个函数,只不过这个函数的返回值是一个地址值。函数返回值必须用同类型的指针变量来接受,也就是说,指针函数一定有函数返回值,而且,在主调函数中,函数返回值必须赋给同类型的指针变量。
表示:
float *fun( );
float *p;
p = fun(a);
一、堆
一块内存空间,可以从中分配出一个小buffer,用完后再把它放回去。
char heap_buf[1024];
int pos = 0;
void *my_malloc(int size)
{
int old_pos = pos;
pos += size;
return &heap_buf[old_pos];
}
void my_free(void *buf)
{
/* err */
}
int main(void)
{
char ch = 65; // char ch = 'A';
int i;
char *buf = my_malloc(100);
unsigned char uch = 200;
for (i = 0; i < 26; i++)
buf[i] = 'A' + i;
return 0;
}
我现在要实现my_free()函数,我并不知道前面malloc分配了多少内存。但是malloc分配的时候有个头部里面保存了字节大小,指针减去读取里面的值就行。
二、栈
栈:也是一块内存空间,CPU的SP寄存器指向它,它可以用于函数调用、局部变量、多任务系统里保存现场
五、任务
任务的三要素:函数、栈、优先级
5.1创建任务
TaskHandle_t xTaskCreateStatic (
TaskFunction_t pxTaskCode, // 函数指针, 任务函数
const char * const pcName, // 任务的名字
const uint32_t ulStackDepth, // 栈大小,单位为word,10表示40字节
void * const pvParameters, // 调用任务函数时传入的参数
UBaseType_t uxPriority, // 优先级
StackType_t * const puxStackBuffer, // 静态分配的栈,就是一个buffer
StaticTask_t * const pxTaskBuffer // 静态分配的任务结构体的指针,用它来操作这个任务
);
这里使用静态创建任务,参数意思如下:
pvTaskCode | 函数指针,可以简单地认为任务就是一个C函数。 它稍微特殊一点:永远不退出,或者退出时要调用"vTaskDelete(NULL)" |
pcName | 任务的名字,FreeRTOS内部不使用它,仅仅起调试作用。 长度为:configMAX_TASK_NAME_LEN |
usStackDepth | 每个任务都有自己的栈,这里指定栈大小。 单位是word,比如传入100,表示栈大小为100 word,也就是400字节。 最大值为uint16_t的最大值。 怎么确定栈的大小,并不容易,很多时候是估计。 精确的办法是看反汇编码。 |
pvParameters | 调用pvTaskCode函数指针时用到:pvTaskCode(pvParameters) |
uxPriority | 优先级范围:0~(configMAX_PRIORITIES – 1) 数值越小优先级越低, 如果传入过大的值,xTaskCreate会把它调整为(configMAX_PRIORITIES – 1) |
puxStackBuffer | 静态分配的栈内存,比如可以传入一个数组, 它的大小是usStackDepth*4。 |
pxTaskBuffer | 静态分配的StaticTask_t结构体的指针 |
返回值 | 成功:返回任务句柄; 失败:NULL |
第一个是默认任务,
后面两个分别是光和色的静态创建任务。
5.1.1 估算栈的大小
栈中有:返回地址,其他寄存器,局部变量,可以保存现场(16个Reg,为64Byte)
粗略估计栈的大小(根据代码):
每一级调用估计会使用9个寄存器,每个寄存器4个字节,
以上述代码为例,4层调用,第二层调用时有2个局部变量(4Byte)。
5.1.2 创建任务 --使用任务参数(不同的任务里面使用同一个函数)
例子:
没有加延时函数时,只能够在屏幕上看到task3内容,其他2个内容看不到,因为task3执行完后很有可能在LCDprintf那里被task1或者task2切换,此时g_LCDCanUse=0,然后这2个任务不能够打印,加了延时函数后就可能在延时函数里面被切换就可以打印了。
5.1.3 删除任务
while (1)
{
/* 读取红外遥控器 */
if (0 == IRReceiver_Read(&dev, &data))
{
if (data == 0xa8) /* play */
{
/* 创建播放音乐的任务 */
extern void PlayMusic(void *params);
if (xSoundTaskHandle == NULL)
{
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Create Task");
ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, osPriorityNormal, &xSoundTaskHandle);
}
}
else if (data == 0xa2) /* power */
{
/* 删除播放音乐的任务 */
if (xSoundTaskHandle != NULL)
{
LCD_ClearLine(0, 0);
LCD_PrintString(0, 0, "Delete Task");
vTaskDelete(xSoundTaskHandle);
PassiveBuzzer_Control(0); /* 停止蜂鸣器 */
xSoundTaskHandle = NULL;
}
}
}
}
缺陷:
频繁动态创建任务、删除任务很容易造成内存碎片,多次操作后可能会分配不到内存了。
5.5.1任务的挂起与恢复(任务状态----改进播放的控制)
任务状态转换图如下分为4个状态(就绪、执行、等待、暂停):
其中暂停状态可以由自己调用函数或者其他任务调用函数。
任务刚创建出来是就绪状态(ready),然后马上就能运行,变成运行状态,当调用vTaskDelay()时,变成阻塞状态。按下按键变成暂停状态。
5.5.2任务的管理与调度
A、高优先级的任务未执行完,低优先级的任务无法运行;
B、一旦高优先级的任务就绪,会立刻马上运行;
C、最高优先级的任务有多个,会轮流运行。
5.5.3 空闲任务
(创建的任务必须是死循环,不然会进入一个错误的函数,会关闭所有中断,并且死循环,所有的任务都无法使用)
一个任务想要退出的话必须清掉,要么自杀要么他杀。
收尸:释放栈和TCB。
以上图可以看到空闲任务的优先级最低,如果某些任务自杀的话,空闲任务需要释放其栈和TCB,其他任务中delay全都改成vTaskDelay进入等待状态。
5.6 两个delay函数
编写任务函数建议使用事件来驱动。(比如说按键按下就触发)
- 使用vTaskDelay(n)时,进入、退出vTaskDelay的时间间隔至少是n个Tick中断
- 使用xTaskDelayUntil(&Pre, n)时,前后两次退出xTaskDelayUntil的时间至少是n个Tick中断
- 退出xTaskDelayUntil时任务就进入的就绪状态,一般都能得到执行机会
- 所以可以使用xTaskDelayUntil来让任务周期性地运行
执行某一个过程时间相同的话就调用第二个函数
6 同步与互斥
同步实例:
现在任务A计算一个比较大的数值,任务B来打印数值和消耗的时间。最后屏幕上面显示时间为2.5s,实际上B任务一直在死等,消耗CPU的资源。现在在B任务添加vTaskDelay(3000),让B直接进入阻塞状态,等A执行完后直接唤醒B任务。
8-1-1 数据传输方法-----全局变量、环形缓冲区、队列
使用全局变量的时候可能会导致传递错误的数据,比如说A任务修改某一结构体数据时,在A任务还没有修改完,就被B任务切换出去了,此时结构体里面的数据肯定是错误的数据;并且没有阻塞和唤醒机制,效率会很低下。
环形缓存区具体实现:
int w=0;
int r=0;
int buf[8]={0};
//空:r==w
//满:next_w==r
为了区分空和满,设定最后一个位置不能写数据。
写数据:
//写数据
while(1)
{
int next_w=r+1;
if(next_w==8)
next_w=0;
if(next_w!=r)//如果未满
{
buf[w]=val;
w++;
w=w%8;
}
}
读数据:
//读数据
while(1)
{
if(w!=r)
{
val=buf[r];
r++;
r=r%8;
}
}
8-1-2 队列的本质
例子:A放产品,B拿出产品
A相对于写队列,B相当于读队列;
对于B来说:
当流水线中没有产品时,B就眯一会(会设定闹钟,即超时时间)
同事A放入产品后会叫醒B(任务A写队列,并唤醒任务B)
闹钟时间到后,会唤醒B
当流水线上面没有产品时,B会阻塞,任务B就会从就绪链表里面删除,放到接收队列链表和超时时间链表里面。当这两个条件(A放物品到流水线或者超时时间达到)任意一个条件到达时B任务会被唤醒,会转到就绪链表里面。
对于挡球板游戏:
将环形缓存区改为队列传输,