如果使用了freertos,那么要把systick留给freertos使用。 这里,需要修改sys->timebase为其他的timer,例如TIM6,或者TIM7。 所以,这里,如果之前使用的delay_us是用systick来做的,如果修改了time base 的硬件定时器,就需要做相应的修改。 在cubemx中,要注意,将time base的优先级设置为最高,即0. 以TIM7为例。
我们首先找到TIM7的定义。
#define TIM7 ((TIM_TypeDef *) TIM7_BASE)
发现它被定义成一个指针,指向一个结构体对象的指针,所以,它是一个句柄。 来看看这个结构体。
typedef struct
{
__IO uint32_t CR1; /*!< TIM control register 1, Address offset: 0x00 */
__IO uint32_t CR2; /*!< TIM control register 2, Address offset: 0x04 */
__IO uint32_t SMCR; /*!< TIM slave mode control register, Address offset: 0x08 */
__IO uint32_t DIER; /*!< TIM DMA/interrupt enable register, Address offset: 0x0C */
__IO uint32_t SR; /*!< TIM status register, Address offset: 0x10 */
__IO uint32_t EGR; /*!< TIM event generation register, Address offset: 0x14 */
__IO uint32_t CCMR1; /*!< TIM capture/compare mode register 1, Address offset: 0x18 */
__IO uint32_t CCMR2; /*!< TIM capture/compare mode register 2, Address offset: 0x1C */
__IO uint32_t CCER; /*!< TIM capture/compare enable register, Address offset: 0x20 */
__IO uint32_t CNT; /*!< TIM counter register, Address offset: 0x24 */
__IO uint32_t PSC; /*!< TIM prescaler, Address offset: 0x28 */
__IO uint32_t ARR; /*!< TIM auto-reload register, Address offset: 0x2C */
__IO uint32_t RCR; /*!< TIM repetition counter register, Address offset: 0x30 */
__IO uint32_t CCR1; /*!< TIM capture/compare register 1, Address offset: 0x34 */
__IO uint32_t CCR2; /*!< TIM capture/compare register 2, Address offset: 0x38 */
__IO uint32_t CCR3; /*!< TIM capture/compare register 3, Address offset: 0x3C */
__IO uint32_t CCR4; /*!< TIM capture/compare register 4, Address offset: 0x40 */
__IO uint32_t BDTR; /*!< TIM break and dead-time register, Address offset: 0x44 */
__IO uint32_t DCR; /*!< TIM DMA control register, Address offset: 0x48 */
__IO uint32_t DMAR; /*!< TIM DMA address for full transfer, Address offset: 0x4C */
__IO uint32_t OR; /*!< TIM option register, Address offset: 0x50 */
} TIM_TypeDef;
这个结构体,用来描述一个MMR,(Memory Mapped Region,简称为MMR。) 这个结构体对象,是一个资源描述块RDB(resource description block)。所描述的资源,是一个MMR,而这个MMR,不是RAM中的MMR,而是IO上的MMR,是一个寄存器组。所以,我们称之为RGDB。(Register Group Description Block) (补充, MMR具有如下特征, 第一,成块状地址区域,所有成员变量连续分布,每个成员变量均为uint32_t类型, 第二,每个成员均为IOMM,所以,所有成员均用volatile作为前导修饰,这里,用宏__IO表示volatile。) 从以上定义,可以看出,将一个base addr,强制转换成了一个RGDB的指针,作为RGDB的句柄。 这样,后续,当我们需要寻址访问MMR的某个offset时,只需要寻址RGDB中的某个成员变量即可。
来看看base addr的定义
#define TIM7_BASE (APB1PERIPH_BASE + 0x1400UL)
这个base addr,是在另一个base addr的基础上,加上offset得到的。 对于TIM7的base addr,我们称之为device base addr , 另一个更基础的base addr,我们称之为bus base addr, 来看看bus base addr,
#define APB1PERIPH_BASE PERIPH_BASE
#define PERIPH_BASE 0x40000000UL /*!< Peripheral base address in the alias region
所以,我们修改delay_us,只需要用RGDB的句柄来选择对应的寄存器即可。 对应于SysTick->LOAD,我们这里需要的是TIM7->ARR, 对应于SysTick->VAL,我们这里需要的是TIM7->CNT,
在HAL_InitTick中,可以找到,所以TIM7的计数值跳变时钟是1MHZ,即1us跳变一次。
htim7.Init.Period = (1000000U / 1000U) - 1U;
对应的,我们需要修改fac_us的算法。
//fac_us=1000000/1000/1000;
fac_us=1;
最终,修改后的代码如下:
static void delay_us_freertos_tim7(uint32_t nus)
{
uint32_t ticks;
uint32_t told,tnow,tcnt=0;
uint32_t reload=TIM7->ARR; //LOAD的值
ticks=nus;
//需要的节拍数
tnow=TIM7->CNT; //刚进入时的计数器值
while(1)
{
told=tnow;
tnow=TIM7->CNT;
if(tnow>=told)
tcnt+=tnow-told; //这里注意一下SYSTICK是一个递减的计数器就可以了.
else
tcnt+=reload-told+tnow;
if(tcnt>=ticks) break; //时间超过/等于要延迟的时间,则退出.
}
}
void delay_us(uint32_t nus)
{
delay_us_freertos_tim7(nus);
}
++++++++++++++++++++++++++++++++++++ freertos中的priority的概念和ucos是相反的。 在Freertos中,priority的值越大,优先级越高,所以,0是优先级最低的, 但是在ucos中,priority的值越小,优先级越高,所以,0是优先级最高的。
UCOS中的优先级的概念,和STM32的硬件中断优先级的概念是相同的,即,值越小,优先级越高。
freertos中,pensv和systick的库中断优先级,都是最低的。所谓的库中断优先级,其实就是cubemx中设置的NVIC优先级。 freertos中,还有一个max syscall priority,高于这个库中断优先级的硬件中断,freertos不管理。一般设置为5。 在freertos中,低于5的中断,在ISR中,可以安全的调用freertos的API,即那些fromISR后缀的API。 但是高于5的中断,在ISR中,禁止调用freertos的API,而且,freertos也不能屏蔽和禁止这些中断。 所以,这些超高优先级的中断,只能使用裸机函数。
+++++++++++++++++++++++++++++++++++++++ 对于裸机程序,进入main后,首先是一系列的init函数,然后进入一个while(1)总任务体中。 可以理解为,裸机程序是一个单任务系统,除了前台的ISR,就只有后台一个单任务。在这个单任务里,又串行执行各个子任务。进入一个子任务时,相当于创建了这个子任务,而子任务调用返回时,又相当于删除了这个子任务。 整个while(1)总任务体执行完成后,马上又进入下一次迭代,再次开始反复创建子任务,删除子任务的过程。
在freertos中,进入main后 ,也是一样,首先是一系列的init函数,然后创建第一个start起始任务,以及其他一些需要的任务,然后,开始schedule,main之后进入自旋,永不退出。 调度器会调度已经创建的task,使用CPU的时间片,如果一个任务都没有了,那么就是main的自旋来消耗掉CPU的时间片。 一般建议是,在start起始任务中,创建其他的任务,创建需要的内核对象,内核对象是一些系统管理起来的全局对象。例如sem,mutex,queue等。
如果从前后台的角度来看,freertos中的所有任务,都处于后台,所有的ISR,仍然是处于前台。 不同的是,在裸机中,当系统从前台返回时,是有明确的唯一断点的,CPU从唯一的断点继续执行,而在freertos中,当系统从前台返回时,并不是明确的断点继续,因为后台有多个进程,都有自己的断点,CPU选择哪个断点继续,取决于调度。
++++++++++++++++++++++++++++++++++++ freertos对于malloc的管理,如果没有外部SRAM,那么推荐使用heap4,如果有外部SRAM连接在FSMC上,那么推荐使用heap5,因为heap5在heap4的基础上,进一步扩展了功能,能够支持多个HeapRegion。
一个块连续的内存用一个 HeapRegion_t 表示,多个连续内存通过 HeapRegion_t 数组的形式组织成为了所有的内存; heap 5 要求,在调用正式的内存分配函数之前,需要定义 HeapRegion,并调用 vPortDefineHeapRegions 来初始化它; 官方给了一个 demo:
#define RAM1_START_ADDRESS ( ( uint8_t * ) 0x00010000 )
#define RAM1_SIZE ( 65 * 1024 )
#define RAM2_START_ADDRESS ( ( uint8_t * ) 0x00020000 )
#define RAM2_SIZE ( 32 * 1024 )
#define RAM3_START_ADDRESS ( ( uint8_t * ) 0x00030000 )
#define RAM3_SIZE ( 32 * 1024 )
const HeapRegion_t xHeapRegions[] =
{
{ RAM1_START_ADDRESS, RAM1_SIZE },
{ RAM2_START_ADDRESS, RAM2_SIZE },
{ RAM3_START_ADDRESS, RAM3_SIZE },
{ NULL, 0 } /* Marks the end of the array. */
};
int main( void ) {
//init other module
...
/* Initialize heap_5. */
vPortDefineHeapRegions( xHeapRegions );
// after init heap5 , can create start task
/* Add application code here. */
}
定义 HeapRegion_t 数组的时候,最后一定要定义成为 NULL 和 0,这样接口才知道这是终点; 被定义的 RAM 区域,都会去参与内存管理;
在真实使用的时候,有可能你很难去定义部分 RAM 的 Start Address! 在编译阶段,有可能你相关的代码和数据等等(.text,.data,.bss,等)都会占用一部分的 RAM,但凡是定义到这个 HeapRegion_t 数组表格的,都会参与内存管理的行为,这显然是我们不愿意的; 比如你芯片的 RAM 起始地址 0x2000_0000,你编译你的 Source Code 后,相关的代码和数据要占用 20KB,也就是 0x5000;那么你定义的 RAM1_START_ADDRESS 起始地址,就必须要大于 0x2000_0000+0x5000,这样才不会踩到你的其他数据; 考虑到实际的使用,官方给出参考 demo 的方法是:
#define RAM2_START_ADDRESS ( ( uint8_t * ) 0x00020000 )
#define RAM2_SIZE ( 32 * 1024 )
#define RAM3_START_ADDRESS ( ( uint8_t * ) 0x00030000 )
#define RAM3_SIZE ( 32 * 1024 )
#define RAM1_HEAP_SIZE ( 30 * 1024 )
static uint8_t ucHeap[ RAM1_HEAP_SIZE ];
const HeapRegion_t xHeapRegions[] =
{
{ ucHeap, RAM1_HEAP_SIZE },
{ RAM2_START_ADDRESS, RAM2_SIZE },
{ RAM3_START_ADDRESS, RAM3_SIZE },
{ NULL, 0 } /* Marks the end of the array. */
};
将链接的代码数据,根据链接器(Linker)配置后,这些都放置在第一段的区域,而ucHeap 也放在一样的地方,
+++++++++++++++++++++++++++++++++++ 勾选cmsis v1,
++++++++++++++++++++++++++++++++++++++ config param:
enable FPU----默认disable use preemption----默认enable tick rate ----默认1000,即1ms一次tick中断。 max priority----默认7,即从0到6,一共7级。 mini stack size ----默认是128word,即128*4=512BYTE。 max task name len ----默认是16, idle should yield ----默认是enable,修改为disable, use mutex ----默认是enable, use recursive mutex----默认是disable, use counting semaphore----默认是disable, queue registory size ----默认是8, use application task tag----默认是disable, use backward compatibility----默认是enable, use tickless idle----默认是disable, use task notifiy ----默认是enable, record stack high addr ---- 默认是disable,
mem alloc ----默认是dynamic / static, total heap size ----默认是15360, mm scheme----默认是heap4,
use ilde hook ---- 默认是disable, use tick hook ----默认是disable, use malloc fail hook ---- 默认是disable, use daemon task startup hook ---- 默认是disable, check for stack overflow ----默认是disable,
generate run time stat ----默认是disable, use trace facility ----默认是disable, use stat formating func ----默认是disable,
use co routing ----默认是disable, max co routing priority ----默认是2,
use software timer ----默认是disable,
library lowest interrupt priority ----默认是15, library max syscall interruput priority ----默认是5,
use posix error number ----默认是disable,
++++++++++++++++++++++++++++++++++++ include param
task priority set get ----默认是enable, task delete ----默认是enable, task cleanup resource ----默认是disable, task suspend ----默认是enable,
task delay until ----默认是disable, task delay ---- 默认是enable,
task resume from ISR ----默认是enable,
queue get mutex holder ----默认是disable, semaphore get mutex holder ----默认是disable,
task get task name ----默认是disable, task get stack high water mark ----默认是disable, task get current task handle ---- 默认是disable, task get state ----默认是disable,
event group set bit from ISR ---- 默认是disable, task abort delay ----默认是disable, task get handle ----默认是disable, task get stack high water mark2 ----默认是disable,
++++++++++++++++++++++++++++++++++ advance setting
use newlib reentrant ----默认是disable,
use fw pack heap file ----默认是enable,
+++++++++++++++++++++++++++++++++++ task and queue
default task -----作为最基本的一个task,
+++++++++++++++++++++++++++++++++++ timer and semaphore
如果使能了software timer ,那么可以在这里add software timer, 如果使能了counting semaphore,那么可以在这里add counting semaphore, 这些都是全局对象。 binary semaphore是必须使能的,同样,在这里add binary semaphore,
++++++++++++++++++++++++++++++++++ mutex mutex是必须使能的,在这里add, 如果使能了recursive mutex,那么也在这里add,
+++++++++++++++++++++++++++++++++++ event
如果使能了event,那么在这里add,
++++++++++++++++++++++++++++++++++++ freertos heap usage 这里报告heap 的使用情况
+++++++++++++++++++++++++++++++++++++ 在裸机系统中,前台ISR和后台任务之间,通信只能靠共享全局变量, 前台ISR打标,后台任务检查标志,清标。 这里后台对标志的使用,有两种方式,一种是让出,一种是自旋。 在让出方式时,子任务检查标志,如果不满足条件,则break,从而让后续的子任务得以执行。 在自旋方式时,子任务检查标志,如果不满足条件,则使用 while(!conditon); 自旋,直到满足条件后escape。继续执行本任务中的后续指令。
在freertos中,也是类似的。 前台ISR和后台task之间,通信有了更丰富的资源,通常使用共享内核对象。 前台ISR填充或者修改内核对象,后台任务检查内核对象的状态,满足条件则调度执行,不满足条件则继续阻塞。 所以,freertos中,从物理CPU的角度来看,任务对内核对象的使用,是让出方式的。在不满足通信条件的时候,任务task让出CPU,给其他的task使用CPU的时间片。 但是,从虚拟CPU的角度来看,任务对内核对象的时候,又是自旋方式的。 虚拟CPU是任务独享的,在不满足通信条件的时候,任务自旋,消耗掉虚拟CPU的时间片。
基于freertos进行系统设计时,最关键的设计思想,就是任务划分和任务通信。
+++++++++++++++++++++++++++++++++++++ 来看看加入了freertos后的代码,有哪些改变。
osThreadId startTaskHandle;
osThreadId myTask02Handle;
osThreadId myTask03Handle;
定义了三个全局变量。这是三个TCB句柄。我们在cubemx中定义的taskname。
typedef TaskHandle_t osThreadId;
void StartTask(void const * argument);
void StartTask02(void const * argument);
void StartTask03(void const * argument);
声明了三个taskfunc,这是我们需要实现的任务函数。我们在cubemx中定义的taskfunc。
/* Create the thread(s) */
/* definition and creation of startTask */
osThreadDef(startTask, StartTask, osPriorityLow, 0, 128);
startTaskHandle = osThreadCreate(osThread(startTask), NULL);
/* definition and creation of myTask02 */
osThreadDef(myTask02, StartTask02, osPriorityBelowNormal, 0, 128);
myTask02Handle = osThreadCreate(osThread(myTask02), NULL);
/* definition and creation of myTask03 */
osThreadDef(myTask03, StartTask03, osPriorityNormal, 0, 128);
myTask03Handle = osThreadCreate(osThread(myTask03), NULL);
用宏拟函数osThreadDef,填充了一个TDB实体,即一个TDB的结构体对象。
const osThreadDef_t os_thread_def_##name = \
{ #name, (thread), (priority), (instances), (stacksz), NULL, NULL }
然后调用osThreadCreate函数,该函数利用TDB和argument,填充TCB实体,返回一个TCB句柄。 其中用到了宏拟函数osThread,用它来索引到对应名称的TDB。
#define osThread(name) &os_thread_def_##name
如果是没有参数,那么可以用NULL作为参数, 如果我们需要一个参数名,那么在cubemx中定义的参数名,会被用来填充TCB。 例如:
/* definition and creation of startTask */
osThreadDef(startTask, StartTask, osPriorityLow, 0, 128);
startTaskHandle = osThreadCreate(osThread(startTask), (void*) &start_param);
/* definition and creation of myTask02 */
osThreadDef(myTask02, StartTask02, osPriorityBelowNormal, 0, 128);
myTask02Handle = osThreadCreate(osThread(myTask02), (void*) &task2_param);
/* definition and creation of myTask03 */
osThreadDef(myTask03, StartTask03, osPriorityNormal, 0, 128);
myTask03Handle = osThreadCreate(osThread(myTask03), (void*) &task3_param);
此时,keil会报错,因为定义的参数名被强转成(void*)类型的指针。它们是Parameter Description Block(PDB)的句柄。 所以,我们需要手工定义这些PDB,通常是定义成全局变量。然后将PDB的指针传递给TCB。 如果我们需要传递PDB的指针,在cubemx中输入时,前面要有"&"符号作为前导。 上述的NULL,是一个空指针,将NULL指针传递给TCB,是为了让参数传递合法,但是传递到任务函数中的NULL指针是不会被使用的。
这也给我们自己手工定义TDB和TCB,提供了样板。
接下来,就是main中调用调度器,进入永久循环。
/* Start scheduler */
osKernelStart();
这个函数,实际上就是对调度器的一次封装。
osStatus osKernelStart (void)
{
vTaskStartScheduler();
return osOK;
}
++++++++++++++++++++++++++++++++++++++++++++ cubemx为我们生成了代码框架, 接下来,我们需要做的,就是编写taskfunc的代码,以及ISR的代码。
通常,我们会为每一个taskfunc建立一个单独的C文件,并辅以对应的H文件。 ISR代码,则仍旧在xx_it.c文件中编写。 我们需要覆盖定义弱函数,即那些我们使用到的ISR的callback函数。
如果任务代码比较简单,也可以直接在main中定义。 cubemx在main中,已经生成了taskfunc的框架代码。我们在其中填写代码即可。 例如:
void StartTask(void const * argument)
{
/* init code for USB_DEVICE */
MX_USB_DEVICE_Init();
/* USER CODE BEGIN 5 */
/* Infinite loop */
for(;;)
{
taskENTER_CRITICAL(); //进入临界区
vTaskDelete(startTaskHandle); //删除开始任务
taskEXIT_CRITICAL(); //退出临界区
osDelay(1);
}
/* USER CODE END 5 */
}
任务代码中,首先是一些init,然后是一些one-off ,每个任务中,只在开始任务时,执行一次的代码,然后进入任务主体。 任务主体,是一个永久循环。首句,是一个osDelay函数的调用。
osStatus osDelay (uint32_t millisec)
{
TickType_t ticks = millisec / portTICK_PERIOD_MS;
vTaskDelay(ticks ? ticks : 1); /* Minimum delay = 1 tick */
return osOK;
}
可以看到,osDelay函数实际上就是封装了vTaskDelay函数。
调用osDelay的目的,是为了调用一次SYSAPI,从而发起一次SWI。 SWI的作用,第一,为任务主体的执行,设置一个断点,第二,引起一次任务调度。 从物理CPU的角度看,当任务发起了SWI的时候,设置了一个断点,让出CPU,当任务被再次调用执行时,会从SWI之前设置的断点位置, 继续向下执行。 从虚拟CPU的角度看,任务在此自旋一个time slice,之后再次进入任务主体,开始下一次迭代。
这在逻辑上,也是很清晰的,明确指出,本任务的本次迭代结束,让出CPU给其他任务,本任务进入休息时间,当休息时间结束时,任务再次开始下一次迭代的业务操作。
必须谨慎使用vTaskDelete函数, 如果一个任务,调用了这种函数,则称为killer task,杀手任务的角色,必须谨慎部署。 必须能够逻辑自洽,否则任由系统中存在多个杀手任务,甚至有一些潜在的杀手任务,那么系统将是不安全的。 杀手任务,必须是一个系统中的管理者角色的任务。
+++++++++++++++++++++++++++++++++++++ 来看看一个普通任务。
void StartTask03(void const * argument)
{
/* USER CODE BEGIN StartTask03 */
u8 task2_num=0;
POINT_COLOR = BLACK;
LCD_DrawRectangle(125,110,234,314); //画一个矩形
LCD_DrawLine(125,130,234,130); //画线
POINT_COLOR = BLUE;
LCD_ShowString(126,111,110,16,16,1,(u8*)"Task2 Run:000");
/* Infinite loop */
for(;;)
{
task2_num++; //任务2执行次数加1 注意task1_num2加到255的时候会清零!!
LED1=!LED1;
LCD_ShowxNum(206,111,task2_num,3,16,0x80); //显示任务执行次数
LCD_Fill(126,131,233,313,lcd_discolor[13-task2_num%14]); //填充区域
vTaskDelay(1000); //延时1s,也就是1000个时钟节拍
osDelay(1);
}
/* USER CODE END StartTask03 */
}
首先是一些one-off工作, 然后进入任务主体,在一次迭代完成后,断点设置好,让出CPU,进入休息时间。等待休息时间结束后,进入下一次迭代。 ++++++++++++++++++++++++++++++++++++ 来看看一个潜在杀手任务。
void StartTask02(void const * argument)
{
/* USER CODE BEGIN StartTask02 */
u8 task1_num=0;
POINT_COLOR = BLACK;
LCD_DrawRectangle(5,110,115,314); //画一个矩形
LCD_DrawLine(5,130,115,130); //画线
POINT_COLOR = BLUE;
LCD_ShowString(6,111,110,16,16,1,(u8*)"Task1 Run:000");
/* Infinite loop */
for(;;)
{
task1_num++; //任务执1行次数加1 注意task1_num1加到255的时候会清零!!
LED0=!LED0;
if(task1_num==5)
{
vTaskDelete(myTask02Handle);//任务1执行5次删除任务2
}
LCD_Fill(6,131,114,313,lcd_discolor[task1_num%14]); //填充区域
LCD_ShowxNum(86,111,task1_num,3,16,0x80); //显示任务执行次数
LCD_ShowxNum(0,140,SysTick->LOAD,8,16,0x80); //显示任务执行次数
LCD_ShowxNum(0,160,SystemCoreClock/1000/1000*120,8,16,0x80); //显示任务执行次数
LCD_ShowxNum(0,180,SysTick->VAL,8,16,0x80); //显示任务执行次数
osDelay(1000);
}
/* USER CODE END StartTask02 */
}
在条件不满足的时候,这个任务就是一个普通任务,但是当条件满足时,这个任务执行了一个操作,就是把另外一个任务删除了。
这种代码风格,是不推荐使用,更好的方式是,专门设置一个管理者任务,由这个管理者任务担当杀手任务的角色。
+++++++++++++++++++++++++++++++++++
|