您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   模型库  
会员   
   
人工智能、机器学习 TensorFlow
6月30日-7月1日 直播
基于 UML 和EA进行分析设计
7月30-31日 北京+线上
图数据库与知识图谱
8月21日-22日 北京+线上
   
 
 订阅
FreeRTOS实时系统

 
 
  66  次浏览      4 次
 2025-6-27
 
编辑推荐:
本文主要介绍了FreeRTOS实时系统 相关内容。 希望对您的学习有所帮助。
本文来自于博客园,由火龙果软件Linda编辑、推荐。

一.FreeRTOS介绍

1.什么是FreeRTOS?

free即免费,RTOS的全称是Real time operating system,即实时操作系统。

注意:RTOS不是指一个确定的系统,而是指一类的系统。比如:us/OS,FreeOS,RTX,RT-Thread等,这些都是RTOS类操作系统。

FreeRTOS是一个迷你的实时操作系统内核。作为一个轻量级的操作系统,功能包括:任务管理、时间 管理、信号量、消息队列、内存管理、记录功能、软件定时器、协程等,可基本满足较小系统的需要。

由于RTOS需占用一定的系统资源(尤其是RAM资源),只有μC/OS-II、embOS、salvo、FreeRTOS等少 数实时操作系统能在小RAM单片机上运行。相对μC/OS-II、embOS等商业操作系统,FreeRTOS操作系 统是完全免费的操作系统,具有源码公开、可移植、可裁减、调度策略灵活的特点,可以方便地移植 到各种单片机上运行,其最新版本为10.4.4版。 (以上来自百度百科)

2.为什么选择 FreeRTOS ?

FreeRTOS 是免费的;

很多半导体厂商产品的SDK(Software Development Kit)软件开发工具包,就使用FreeRTOS作为其 操作系统,尤其是WIFI、蓝牙这些带有协议栈的芯片或模块。

简单,因为FreeRTOS的文件数量很少。

二、FreeRTOS移植到STM32芯片上

使用Cubemx快速移植,

创建一个模板项目

打开mcu

选中芯片

选中system core

配置SYS

配置RCC

配置串口,用于打印调试

选中FreeRTOS系统

项目生成配置

选择generate code,生成代码

选择open project

在usart.c中加入

/* USER CODE BEGIN 0 */
#include "stdio.h"
int fputc(int ch, FILE *f)
{
unsigned char temp[1]={ch};
HAL_UART_Transmit(&huart1,temp,1,0xffff);
return ch;
}
/* USER CODE END 0 */

 

在main中测试是否打印到串口上

#include "stdio.h"
/* USER CODE BEGIN 2 */
printf("hello world!\r\n");
/* USER CODE END 2 */

 

一些常见问题:

1.Timebase source为什么不能设置为systick?

裸机版的时钟默认是SysTick,但是开启FreeRTOS后,FreeRTOS会占用SysTick(用来生成1ms的定时用于任务调度),所以需要为其他总线提供另外的时钟源。

2.FreeRTOS版本问题

V2的内核版本更高,更能更多,多数情况下V1版本内核完全够用

3.FreeRTOS 各配置选项卡的解释

Events:事件相关的创建

Task and Queues: 任务与队列的创建

Timers and Semaphores: 定时器和信号量的创建

Mutexes: 互斥量的创建

FreeRTOS Heap Usage: 用于查看堆使用情况

config parameters: 内核参数设置,用户根据自己的实际应用来裁剪定制FreeRTOS 内核

Include parameters: FreeRTOS 部分函数的使能

User Constants: 相关宏的定义,可以自建一些常量在工程中使用

Advanced settings:高级设置

4. 内核配置、函数使能的一些翻译

三.任务的创建与删除

1.什么是任务?

任务可以理解为进程或者是一个任务,会在内存开辟一个空间用于执行任务。

比如,在电脑打开记事本,微信等都是一个任务。

任务通常含有一个while(1)死循环

2.任务创建与删除相关函数

动态创建与静态创建的区别:

动态创建任务的堆栈由系统分配,而静态创建任务的堆栈由用户自己传递。

大多数情况使用动态创建任务。

xTaskCreate()函数原型

pvTaskCode:指向任务函数的指针,任务必须实现为永不返回(即连续循环);2. pcName:任务的名字,主要是用来调试,默认情况下最大长度是16;

pvParameters:指定的任务栈的大小;

uxPriority:任务优先级,数值越大,优先级越大;

pxCreatedTask:用于返回已创建任务的句柄可以被引用。

官方案例:

/* Task to be created. */
void vTaskCode( void * pvParameters )
{
/* The parameter value is expected to be 1 as 1 is passed in the
pvParameters value in the call to xTaskCreate() below.
configASSERT( ( ( uint32_t ) pvParameters ) == 1 );
for( ;; )
{
/* Task code goes here. */
}
}
/* Function that creates a task. */
void vOtherFunction( void )
{
BaseType_t xReturned;
TaskHandle_t xHandle = NULL;
/* Create the task, storing the handle. */
xReturned = xTaskCreate(
vTaskCode, /* Function that implements the task. */
"NAME", /* Text name for the task. */
STACK_SIZE, /* Stack size in words, not bytes. */
( void * ) 1, /* Parameter passed into the task. */
tskIDLE_PRIORITY,/* Priority at which the task is created. */
&xHandle ); /* Used to pass out the created task's handle.
*/
if( xReturned == pdPASS )
{
/* The task was created. Use the task's handle to delete the task. */
vTaskDelete( xHandle );
}
}

 

vTaskDelete函数原型:

void vTaskDelete(TaskHandle_t xTaskToDelete);

 

只需将待删除的任务句柄传入该函数,即可将该任务删除。

当传入的参数为NULL,则代表删除任务自身(当前正在运行的任务)。

3.实现

四、任务调度

什么是任务调度器?

调度器就是使用相关的调度算法来决定当前执行到哪个任务了

FreeRTOS中任务调度器的函数是vTaskStartScheduler(),在FreeRTOS被封装为osKernelStart()

调度器规则:

FreeRTOS是一个实时操作系统,需要遵循一些调度规则:

抢占式调度:高优先级的抢占低优先级的,永远都是高优先级的优先执行

时间片调度:同等优先级的,谁先抢占到CPU资源,谁先执行。

抢占式调度运行过程:

时间片调度运行过程:

任务的状态

FreeRTOS中有4种状态:

Running运行态:当前任务正在使用CPU的资源,任务正在执行。

Ready就绪态:当前任务等待CPU的调度,任务等待运行。

Blocked阻塞态:当前任务因为一些延时、或等待信号量、消息队列、事件标记组而处于一个阻塞的状态。

Suspended挂起态:类似暂停,通过调用vTaskSuspend()对指定的任务进行挂起。需用通过调用vTaskResume()才能恢复。

总结:

只有就绪态才能变为运行态

其他状态只有调用指定的函数,先成功就绪态才能变为运行态。

任务小实验:

需求:

建 4 个任务:taskLED1,taskLED2,taskKEY1,taskKEY2,任务要求如下:

taskLED1:间隔 500ms 闪烁 LED1;

taskLED2:间隔 1000ms 闪烁 LED2;

taskKEY1:如果 taskLED1 存在,则按下 KEY1 后删除 taskLED1 ,否则创建 taskLED1 ;

taskKEY2:如果 taskLED2 正常运行,则按下 KEY2 后挂起 taskLED2 ,否则恢复 taskLED2

cubeMX配置:

代码实现:

/* USER CODE BEGIN Header_StartTaskLED1 */
/**
* @brief Function implementing the taskLED1 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskLED1 */
void StartTaskLED1(void const * argument)
{
/* USER CODE BEGIN StartTaskLED1 */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_8);
osDelay(500);
}
/* USER CODE END StartTaskLED1 */
}
/* USER CODE BEGIN Header_StartTaskLED2 */
/**
* @brief Function implementing the taskLED2 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskLED2 */
void StartTaskLED2(void const * argument)
{
/* USER CODE BEGIN StartTaskLED2 */
/* Infinite loop */
for(;;)
{
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_9);
osDelay(1000);
}
/* USER CODE END StartTaskLED2 */
}
/* USER CODE BEGIN Header_StartTaskKEY1 */
/**
* @brief Function implementing the taskKEY1 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskKEY1 */
void StartTaskKEY1(void const * argument)
{
/* USER CODE BEGIN StartTaskKEY1 */
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET)
{
printf("KEY1按下了!\r\n");
if(taskLED1Handle == NULL)
{
printf("任务1不存在,开始创建\r\n");
osThreadDef(taskLED1, StartTaskLED1, osPriorityNormal, 0, 128);
taskLED1Handle = osThreadCreate(osThread(taskLED1), NULL);
if(taskLED1Handle != NULL)
printf("任务1创建成功\r\n");
}
else
{
osThreadTerminate(taskLED1Handle);
taskLED1Handle = NULL;
printf("任务1已删除\r\n");
}
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET);
}
osDelay(10);
}
/* USER CODE END StartTaskKEY1 */
}
/* USER CODE BEGIN Header_StartTaskKEY2 */
/**
* @brief Function implementing the taskKEY2 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskKEY2 */
void StartTaskKEY2(void const * argument)
{
/* USER CODE BEGIN StartTaskKEY2 */
static int flag = 0;
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET)
{
if(flag == 0)
{
osThreadSuspend(taskLED2Handle);
printf("任务2已暂停\r\n");
flag = 1;
}
else
{
osThreadResume(taskLED2Handle);
printf("任务2已恢复\r\n");
flag = 0;
}
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET);
}
osDelay(10);
}
/* USER CODE END StartTaskKEY2 */
}

 

五、队列

什么队列?

队列又称消息队列,是任务通信的一种数据结构,队列可以在任务与任务之间中断和任务间的传递消息。

为啥不能使用全局变量呢?

全局的变量,在多任务修改的情况下,获取的不是最新的值,容易导致数据的错读。

队列的特点

1.入队出队方式

通常先进先出(FIFO),数据先进去的先被读取。

还有一种后进先出(LIFO),比较少用,一般有于栈。

2.数据传递方式

采用实际值传递,则将数据拷贝到队列进行传递,也可以使用指针,在数据比较大的时候通常采用指针传递。

3、多任务访问

队列不属于某个任务,任何任务都可以使用队列发送跟接收数据。

4、出队、入队阻塞

当向一个队列发送消息是,可以指定一个阻塞时间,假设次数当队列已满无法入队时,可以设置一个阻塞时间:

0:直接返回,不阻塞。

0-portMAX_DELAY:设置指定的时间,超过该时间还没进入队列直接返回。

portMAX_DELAY:死等,等到队列有空闲位置为止。

队列相关API

1.创建队列

QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,
UBaseType_t uxItemSize );

 

参数:

uxQueueLength:队列的可容纳的长度

uxItemSize:一个队列容纳的大小

返回值:

如何一个队列创建成功,会分配到内存,反之失败则为NULL

2.写队列API

BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void * pvItemToQueue,
TickType_t xTicksToWait
);

 

参数:

xQueue:队列的句柄,数据项将发送到此队列。

pvItemToQueue:待写入数据

xTicksToWait:阻塞超时时间

返回值:

如果成功写入数据,返回 pdTRUE,否则返回 errQUEUE_FULL。

3、读队列API

BaseType_t xQueueReceive(
QueueHandle_t xQueue,
void *pvBuffer,
TickType_t xTicksToWait
);

 

参数:

xQueue:待读取的队列

pvItemToQueue:数据读取缓冲区

xTicksToWait:阻塞超时时间

返回值:

成功返回 pdTRUE,否则返回 pdFALSE。

实验:

创建一个队列,按下 KEY1 向队列发送数据,按下 KEY2 向队列读取数据。

cubeMX配置

代码实现

/* USER CODE BEGIN Header_StartTaskSend */
/**
* @brief Function implementing the taskSend thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskSend */
void StartTaskSend(void const * argument)
{
/* USER CODE BEGIN StartTaskSend */
uint16_t buf = 100;
BaseType_t status;
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == RESET)
{
status = xQueueSend(myQueue01Handle,&buf,0);
if(status == pdTRUE)
printf("数据发送成功:%d \r\n",buf);
else
printf("数据发送失败\r\n");
}
while(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == RESET);
}
osDelay(10);
}
/* USER CODE END StartTaskSend */
}
/* USER CODE BEGIN Header_StartTaskReceive */
/**
* @brief Function implementing the taskReceive thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskReceive */
void StartTaskReceive(void const * argument)
{
/* USER CODE BEGIN StartTaskReceive */
uint16_t buf = 100;
BaseType_t status;
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1) == RESET)
{
status = xQueueReceive(myQueue01Handle,&buf,0);
if(status == pdTRUE)
printf("数据接收成功:%d \r\n",buf);
else
printf("数据接收失败\r\n");
}
while(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1) == RESET);
}
osDelay(10);
}
/* USER CODE END StartTaskReceive */
}

 

六、二值信号量

什么是信号量?

信号量(semaphore)实在多任务环境下使用的一种机制,是可以用来保证两个或者多个关键代码段不被并发调用。

信号量可以拆分为信号和量,信号起到了通知的作用,而量表示资源的数量,当我们的量只有0和1的时候,我们称为二值信号量,只有两种状态,当我们那个量没有限制时,称为计数型信号量。

什么是二值信号量?

二值信号量其实是一个长度为1,大小为1的队列,只有0和1两种状态,通常用它进行互斥访问或任务同步。

二值信号量API

创建二值信号量

SemaphoreHandle_t xSemaphoreCreateBinary( void )

参数:

返回值:

成功,返回对应二值信号量的句柄;

失败,返回 NULL 。

释放二值信号量

BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore )

 

参数:

xSemaphore:要释放的信号量句柄

返回值:

成功,返回 pdPASS ;

失败,返回 errQUEUE_FULL 。

获取二值信号量

BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait );

BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore,

TickType_t xTicksToWait );

参数:

xSemaphore:要获取的信号量句柄

xTicksToWait:超时时间,0 表示不超时,portMAX_DELAY表示卡死等待;

返回值:

成功,返回 pdPASS ;

失败,返回 errQUEUE_FULL 。

实现:

创建一个二值信号量,按下 KEY1 则释放信号量,按下 KEY2 获取信号量。

cubeMX配置

代码实现

osSemaphoreDef(myBinarySem);
//myBinarySemHandle = osSemaphoreCreate(osSemaphore(myBinarySem), 1);//使用这个默认会创建一个二值信号量
myBinarySemHandle = xSemaphoreCreateBinary();//需要手动创建
/* USER CODE BEGIN Header_StartTaskSemaphoreGive */
/**
* @brief Function implementing the taskSemaphoreGi thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskSemaphoreGive */
void StartTaskSemaphoreGive(void const * argument)
{
/* USER CODE BEGIN StartTaskSemaphoreGive */
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET)
{
if(xSemaphoreGive(myBinarySemHandle) == pdTRUE)
printf("二极信号放入成功\r\n");
else
printf("二极信号放入失败\r\n");
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET);
}
osDelay(10);
}
/* USER CODE END StartTaskSemaphoreGive */
}
/* USER CODE BEGIN Header_StartTaskSemaphoreTake */
/**
* @brief Function implementing the taskSemaphoreTa thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskSemaphoreTake */
void StartTaskSemaphoreTake(void const * argument)
{
/* USER CODE BEGIN StartTaskSemaphoreTake */
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET)
{
if(xSemaphoreTake(myBinarySemHandle, 0) == pdTRUE)
printf("二极信号获取成功\r\n");
else
printf("二极信号获取失败\r\n");
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET);
}
osDelay(10);
}
/* USER CODE END StartTaskSemaphoreTake */
}

七、计数型信号量

计量型信号量相当于长度大于1的队列,能容纳更多资源,在创建时确定了长度大小。

应用场景:

一个停车场有固定的停车位,当一辆车开进来时,车位减一,一辆车开走时,车位加一,当车位满了,表示队列装满了,此时再进来只能等待或者是开走。

计数型相关API函数

计数型信号量的释放和获取与二值信号量完全相同 !

SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount);

参数:

uxMaxCount:可以达到的最大计数值

uxInitialCount:创建信号量时分配给信号量的计数值

返回值:

成功,返回对应计数型信号量的句柄;

失败,返回 NULL 。

实现案例:

创建一个计数型信号量,按下 KEY1 则释放信号量,按下 KEY2 获取信号量。

cubeMX配置

将USE_COUNTING_SEMAPHORES的值从Disabled改成Enabled

代码实现

/* USER CODE BEGIN Header_StartTaskSemaphoreGive */
/**
* @brief Function implementing the taskSemaphoreGi thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskSemaphoreGive */
void StartTaskSemaphoreGive(void const * argument)
{
/* USER CODE BEGIN StartTaskSemaphoreGive */
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET)
{
if(xSemaphoreGive(myCountingSemaHandle) == pdTRUE)
printf("计数信号量放入成功\r\n");
else
printf("计数信号量放入失败\r\n");
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET);
}
osDelay(10);
}
/* USER CODE END StartTaskSemaphoreGive */
}
/* USER CODE BEGIN Header_StartTaskSemaphoreTake */
/**
* @brief Function implementing the taskSemaphoreTa thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskSemaphoreTake */
void StartTaskSemaphoreTake(void const * argument)
{
/* USER CODE BEGIN StartTaskSemaphoreTake */
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET)
{
if(xSemaphoreTake(myCountingSemaHandle, 0) == pdTRUE)
printf("计数信号量获取成功\r\n");
else
printf("计数信号量获取失败\r\n");
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET);
}
osDelay(10);
}
/* USER CODE END StartTaskSemaphoreTake */
}

 

八、互斥量

什么是互斥量?

在多数情况下,互斥型信号量与二值型信号量非常相似,但功能上,二值型信号量用于同步,而互斥型信号量用于资源保护。

互斥型信号量和二值型信号量还有一个最大的区别,互斥型信号量可以有效解决优先级反转现象。

什么是优先级翻转?

如图上所示,当低优先级获得CPU资源时,还没执行完就会被高优先级的任务打断,如果执行重要的东西时,容易发送错误,所以互斥型信号量就起到作用了,低优先级 在获取到CPU资源时,不会被其他高优先级的任务打断,直到释放信号。

互斥量相关API

互斥信号量不能用于中断服务函数中!

SemaphoreHandle_t xSemaphoreCreateMutex( void )

参数:

返回值:

成功,返回对应互斥量的句柄;

失败,返回 NULL 。

实现

演示优先级翻转

使用互斥量优化优先级翻转问题

cubeMX配置

代码实现

创建了三个任务StartTaskH、StartTaskM、StartTaskL

/* USER CODE BEGIN Header_StartTaskH */
/**
* @brief Function implementing the TaskH thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskH */
void StartTaskH(void const * argument)
{
/* USER CODE BEGIN StartTaskH */
/* Infinite loop */
for(;;)
{
xSemaphoreTake(myMutexHandle,portMAX_DELAY);
printf("TaskH:我在上厕所...\r\n");
HAL_Delay(1000);
printf("TaskH:我出来了...\r\n");
xSemaphoreGive(myMutexHandle);
osDelay(1000);
}
/* USER CODE END StartTaskH */
}
/* USER CODE BEGIN Header_StartTaskM */
/**
* @brief Function implementing the TaskM thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskM */
void StartTaskM(void const * argument)
{
/* USER CODE BEGIN StartTaskM */
/* Infinite loop */
for(;;)
{
printf("TaskM:我占用CPU资源\r\n");
osDelay(1000);
}
/* USER CODE END StartTaskM */
}
/* USER CODE BEGIN Header_StartTaskL */
/**
* @brief Function implementing the TaskL thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskL */
void StartTaskL(void const * argument)
{
/* USER CODE BEGIN StartTaskL */
/* Infinite loop */
for(;;)
{
xSemaphoreTake(myMutexHandle,portMAX_DELAY);
printf("TaskL:我在上厕所...\r\n");
HAL_Delay(3000);
printf("TaskL:我出来了...\r\n");
xSemaphoreGive(myMutexHandle);
osDelay(1000);
}
/* USER CODE END StartTaskL */
}

九、事件标志组

什么是事件标志组?

事件标志位:表示某件事是否发生,联想:全局变量flag,通常每个位表示一个事件(高8位不算)

事件标志组:是一组事件标志位的集合,也就是一个整数

事件标志组本质是一个 16 位或 32 位无符号的数据类型 EventBits_t ,由 configUSE_16_BIT_TICKS 决定。

虽然使用了 32 位无符号的数据类型变量来存储事件标志, 但其中的高8位用作存储事件标志组的控制信息,

低 24 位用作存储事件标志 ,所以说一个事件组最多可以存储 24 个事件标志!

事件标志组API

1.创建事件标志组

EventGroupHandle_t xEventGroupCreate( void );

 

参数:

返回值:

成功,返回对应事件标志组的handle;

失败,返回 NULL 。

2.设置事件标志位

EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );

 

参数:

xEventGroup:对应事件组句柄。

uxBitsToSet:指定要在事件组中设置的一个或多个位的按位值。

返回值:

设置之后事件组中的事件标志位值。

3.清除事件标志位

EventBits_t xEventGroupClearBits(EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToClear );

 

参数:

xEventGroup:对应事件组句柄。

uxBitsToClear:指定要在事件组中清除的一个或多个位的按位值。

返回值:

清零之前事件组中事件标志位的值。

4.等待事件标志位

EventBits_t xEventGroupWaitBits(
const EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait );

 

参数:

xEventGroup:对应的事件标志组handle

uxBitsToWaitFor:指定事件组中要等待的一个或多个事件位的按位值

xClearOnExit:pdTRUE——清除对应事件位,pdFALSE——不清除

xWaitForAllBits:pdTRUE——所有等待事件位全为1(逻辑与),pdFALSE——等待的事件位有一个为1

(逻辑或)

xTicksToWait:超时

返回值:

等待的事件标志位值:等待事件标志位成功,返回等待到的事件标志位

其他值:等待事件标志位失败,返回事件组中的事件标志位

实现:

创建一个事件标志组和两个任务( task1 和 task2),task1 检测按键,如果检测到 KEY1 和 KEY2 都按过,

则执行 task2 。

/* USER CODE BEGIN Variables */
EventGroupHandle_t eventgroup_handle;
/* USER CODE END Variables */
/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
eventgroup_handle = xEventGroupCreate();
/* USER CODE END RTOS_THREADS */
/* USER CODE BEGIN Header_StartTask1 */
/**
* @brief Function implementing the Task1 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTask1 */
void StartTask1(void const * argument)
{
/* USER CODE BEGIN StartTask1 */
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET)
{
xEventGroupSetBits(eventgroup_handle, 0x01);
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET);
}
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET)
{
xEventGroupSetBits(eventgroup_handle, 0x02);
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET);
}
osDelay(10);
}
/* USER CODE END StartTask1 */
}
/* USER CODE BEGIN Header_StartTask2 */
/**
* @brief Function implementing the Task2 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTask2 */
void StartTask2(void const * argument)
{
/* USER CODE BEGIN StartTask2 */
EventBits_t result;
/* Infinite loop */
for(;;)
{
result = xEventGroupWaitBits(eventgroup_handle, 0x01 | 0x02 , pdTRUE, pdFALSE , portMAX_DELAY);
printf("同意请假了:%#x \r\n",result);
osDelay(1);
}
/* USER CODE END StartTask2 */
}

 

十、任务通知

什么是任务通知?

FreeRTOS 从版本 V8.2.0 开始提供任务通知这个功能,每个任务都有一个 32 位的通知值。按照 FreeRTOS 官方的说法,使用消息通知比通过二进制信号量方式解除阻塞任务快 45%, 并且更加省内存(无需创建队 列)。

在大多数情况下,任务通知可以替代二值信号量、计数信号量、事件标志组,可以替代长度为 1 的队列(可 以保存一个 32 位整数或指针值),并且任务通知速度更快、使用的RAM更少!

任务通知值的更新方式

FreeRTOS 提供以下几种方式发送通知给任务 :

发送消息给任务,如果有通知未读, 不覆盖通知值

发送消息给任务,直接覆盖通知值

发送消息给任务,设置通知值的一个或者多个位

发送消息给任务,递增通知值

通过对以上方式的合理使用,可以在一定场合下替代原本的队列、信号量、事件标志组等。

任务通知的优势和劣势

任务通知的优势

使用任务通知向任务发送事件或数据,比使用队列、事件标志组或信号量快得多。

使用其他方法时都要先创建对应的结构体,使用任务通知时无需额外创建结构体。

任务通知的劣势

只有任务可以等待通知,中断服务函数中不可以,因为中断没有 TCB 。

通知只能一对一,因为通知必须指定任务。

等待通知的任务可以被阻塞, 但是发送消息的任务,任何情况下都不会被阻塞等待。

任务通知是通过更新任务通知值来发送数据的,任务结构体中只有一个任务通知值,只能保持一个数 据。

任务通知相关API函数

1.发送通知

BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction );

 

参数:

xTaskToNotify:需要接收通知的任务handle;

ulValue:用于更新接收任务通知值, 具体如何更新由形参 eAction 决定;

eAction:一个枚举,代表如何使用任务通知的值;

返回值:

如果被通知任务还没取走上一个通知,又接收了一个通知,则这次通知值未能更新并返回 pdFALSE, 而其他

情况均返回pdPASS。

BaseType_t xTaskNotifyAndQuery( TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
uint32_t *pulPreviousNotifyValue );

 

参数:

xTaskToNotify:需要接收通知的任务handle;

ulValue:用于更新接收任务通知值, 具体如何更新由形参 eAction 决定;

eAction:一个枚举,代表如何使用任务通知的值;

pulPreviousNotifyValue:对象任务的上一个任务通知值,如果为 NULL, 则不需要回传, 这个时候就等价 于函数 xTaskNotify()。

返回值:

如果被通知任务还没取走上一个通知,又接收了一个通知,则这次通知值未能更新并返回 pdFALSE, 而其他 情况均返回pdPASS。

BaseType_t xTaskNotifyGive( TaskHandle_t xTaskToNotify );

 

参数:

xTaskToNotify:接收通知的任务句柄, 并让其自身的任务通知值加 1。

返回值:

总是返回 pdPASS。

2.等待通知

等待通知API函数只能用在任务,不可应用于中断中!

uint32_t ulTaskNotifyTake( BaseType_t xClearCountOnExit,
TickType_t xTicksToWait );

 

参数:

xClearCountOnExit:指定在成功接收通知后,将通知值清零或减 1,pdTRUE:把通知值清零(二值信号 量);pdFALSE:把通知值减一(计数型信号量);

xTicksToWait:阻塞等待任务通知值的最大时间;

返回值:

0:接收失败

非0:接收成功,返回任务通知的通知值

BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,
TickType_t xTicksToWait );

 

ulBitsToClearOnEntry:函数执行前清零任务通知值那些位 。

ulBitsToClearOnExit:表示在函数退出前,清零任务通知值那些位,在清 0 前,接收到的任务通知值会先被 保存到形参*pulNotificationValue 中。

pulNotificationValue:用于保存接收到的任务通知值。 如果 不需要使用,则设置为 NULL 即可 。

xTicksToWait:等待消息通知的最大等待时间。

实现:

模拟二值信号量

/* USER CODE BEGIN Header_StartTaskSend */
/**
* @brief Function implementing the TaskSend thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskSend */
void StartTaskSend(void const * argument)
{
/* USER CODE BEGIN StartTaskSend */
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET)
{
xTaskNotifyGive(TaskReceiveHandle);
printf("任务通知:二值信号量数据发送成功! \r\n");
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET);
}
osDelay(10);
}
/* USER CODE END StartTaskSend */
}
/* USER CODE BEGIN Header_StartTaskReceive */
/**
* @brief Function implementing the TaskReceive thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskReceive */
void StartTaskReceive(void const * argument)
{
/* USER CODE BEGIN StartTaskReceive */
uint32_t rev = 0;
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET)
{
rev = ulTaskNotifyTake(pdTRUE,portMAX_DELAY);
if(rev != 0)
printf("任务通知:二值信号量数据接收成功!rev=%d \r\n",rev);
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET);
}
osDelay(10);
}
/* USER CODE END StartTaskReceive */
}

模拟计数型信号量 

/* USER CODE BEGIN Header_StartTaskSend */
/**
* @brief Function implementing the TaskSend thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskSend */
void StartTaskSend(void const * argument)
{
/* USER CODE BEGIN StartTaskSend */
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET)
{
xTaskNotifyGive(TaskReceiveHandle);
printf("任务通知:计数信号量数据发送成功! \r\n");
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET);
}
osDelay(10);
}
/* USER CODE END StartTaskSend */
}
/* USER CODE BEGIN Header_StartTaskReceive */
/**
* @brief Function implementing the TaskReceive thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskReceive */
void StartTaskReceive(void const * argument)
{
/* USER CODE BEGIN StartTaskReceive */
uint32_t rev = 0;
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET)
{
rev = ulTaskNotifyTake(pdFALSE,portMAX_DELAY);
if(rev != 0)
printf("任务通知:计数信号量数据接收成功!rev=%d \r\n",rev);
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET);
}
osDelay(10);
}
/* USER CODE END StartTaskReceive */
}

模拟事件标志组

/* USER CODE BEGIN Header_StartTaskSend */
/**
* @brief Function implementing the TaskSend thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskSend */
void StartTaskSend(void const * argument)
{
/* USER CODE BEGIN StartTaskSend */
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET)
{
printf("按下了KEY1,发送0x01\r\n");
xTaskNotify(TaskReceiveHandle,0x01,eSetBits);
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET);
}
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET)
{
printf("按下了KEY2,发送0x02\r\n");
xTaskNotify(TaskReceiveHandle,0x02,eSetBits);
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET);
}
osDelay(10);
}
/* USER CODE END StartTaskSend */
}
/* USER CODE BEGIN Header_StartTaskReceive */
/**
* @brief Function implementing the TaskReceive thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskReceive */
void StartTaskReceive(void const * argument)
{
/* USER CODE BEGIN StartTaskReceive */
uint32_t notify_val = 0, event_bit = 0;
/* Infinite loop */
for(;;)
{
xTaskNotifyWait(0,0xFFFFFFFF,&notify_val, portMAX_DELAY);
if(notify_val & 0x01)
event_bit |= 0x01;
if(notify_val & 0x02)
event_bit |= 0x02;
if(event_bit == (0x01 | 0x02))
{
event_bit = 0;
printf("请假成功\r\n");
}
osDelay(10);
}
/* USER CODE END StartTaskReceive */
}

模拟邮箱 

/* USER CODE BEGIN Header_StartTaskSend */
/**
* @brief Function implementing the TaskSend thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskSend */
void StartTaskSend(void const * argument)
{
/* USER CODE BEGIN StartTaskSend */
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET)
{
printf("按下了KEY1\r\n");
xTaskNotify(TaskReceiveHandle,1,eSetValueWithOverwrite);
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == RESET);
}
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET)
{
osDelay(20);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET)
{
printf("按下了KEY2\r\n");
xTaskNotify(TaskReceiveHandle,0x02,eSetValueWithOverwrite);
}
while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == RESET);
}
osDelay(10);
}
/* USER CODE END StartTaskSend */
}
/* USER CODE BEGIN Header_StartTaskReceive */
/**
* @brief Function implementing the TaskReceive thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTaskReceive */
void StartTaskReceive(void const * argument)
{
/* USER CODE BEGIN StartTaskReceive */
uint32_t notify_val = 0;
/* Infinite loop */
for(;;)
{
xTaskNotifyWait(0,0xFFFFFFFF,&notify_val, portMAX_DELAY);
printf("接收到的通知值为:%d \r\n",notify_val);
osDelay(10);
}
/* USER CODE END StartTaskReceive */
}

十一、延时函数

什么是延时函数?

延时函数是延迟执行

延时函数分类

相对延时:vtaskDelay

不能在延迟时间中发生中断

绝对延时:vTaskDelayUntil

延时时间能在中断中,直到延时结束

vtaskDelay与HAL_DELAY区别

vTaskDelay 作用是让任务阻塞,任务阻塞后,RTOS系统调用其它处于就绪状态的优先级最高的任务来执 行。

HAL_Delay 一直不停的调用获取系统时间的函数,直到指定的时间流逝然后退出,故其占用了全部CPU时 间。

十二、软件定时器

什么是定时器?

简单可以理解为闹钟,到达指定一段时间后,就会响铃。

STM32 芯片自带硬件定时器,精度较高,达到定时时间后会触发中断,也可以生成 PWM 、输入捕获、输出 比较,等等,功能强大,但是由于硬件的限制,个数有限。

软件定时器也可以实现定时功能,达到定时时间后可调用回调函数,可以在回调函数里处理信息。

软件定时器优缺点

优点:

简单、成本低;

只要内存足够,可创建多个;

缺点:

精度较低,容易受中断影响。在大多数情况下够用,但对于精度要求比较高的场合不建议使用。

软件定时器原理

定时器是一个可选的、不属于 FreeRTOS 内核的功能,它是由定时器服务任务来提供的。

在调用函数 vTaskStartScheduler() 开启任务调度器的时候,会创建一个用于管理软件定时器的任务,这个 任务就叫做软件定时器服务任务。

负责软件定时器超时的逻辑判断

调用超时软件定时器的超时回调函数

处理软件定时器命令队列

FreeRTOS提供了很多定时器有关的API函数,这些API函数大多都使用FreeRTOS的队列发送命令给定时器服 务任务。这个队列叫做定时器命令队列。定时器命令队列是提供给FreeRTOS的软件定时器使用的,用户不能直接访问!

软件定时器相关配置

软件定时器有一个定时器服务任务和定时器命令队列,这两个东西肯定是要配置的,相关的配置也是放到文 件FreeRTOSConfig.h中的,涉及到的配置如下:

1、configUSE_TIMERS

如果要使用软件定时器的话宏configUSE_TIMERS一定要设置为1,当设置为1的话定时器服务任务就会在启 动FreeRTOS调度器的时候自动创建。

2、configTIMER_TASK_PRIORITY

设置软件定时器服务任务的任务优先级,可以为0~(configMAX_PRIORITIES-1)。优先级一定要根据实际的应 用要求来设置。如果定时器服务任务的优先级设置的高的话,定时器命令队列中的命令和定时器回调函数就 会及时的得到处理。

3、configTIMER_QUEUE_LENGTH

此宏用来设置定时器命令队列的队列长度。

4、configTIMER_TASK_STACK_DEPTH

此宏用来设置定时器服务任务的任务堆栈大小。

单次定时器和周期定时器

单次定时器: 只超时一次,调用一次回调函数。可手动再开启定时器;

周期定时器: 多次超时,多次调用回调函数。

软件定时器相关API函数

1. 创建软件定时器

TimerHandle_t xTimerCreate
( const char * const pcTimerName,
const TickType_t xTimerPeriod,
const UBaseType_t uxAutoReload,
void * const pvTimerID,
TimerCallbackFunction_t pxCallbackFunction );

 

参数:

pcTimerName:软件定时器名称

xTimerPeriodInTicks:定时超时时间,单位:系统时钟节拍。宏 pdMS_TO_TICKS() 可用于将以毫秒为单位 指定的时间转换为以 tick 为单位指定的时间。

uxAutoReload:定时器模式, pdTRUE:周期定时器, pdFALSE:单次定时器 pvTimerID:软件定时器 ID,用于多个软件定时器公用一个超时回调函数 返回值:

成功:定时器handle

失败:NULL

2. 开启软件定时器

BaseType_t xTimerStart( TimerHandle_t xTimer,
TickType_t xBlockTime );

 

参数:

xTimer:待开启的软件定时器的句柄

xTickToWait:发送命令到软件定时器命令队列的最大等待时间

返回值:

pdPASS:开启成功

pdFAIL:开启失败

3. 停止软件定时器

BaseType_t xTimerStop( TimerHandle_t xTimer,
TickType_t xBlockTime );

 

参数与返回值同上。

4. 复位软件定时器

BaseType_t xTimerReset( TimerHandle_t xTimer,
TickType_t xBlockTime );

 

参数与返回值同上。

该功能将使软件定时器的重新开启定时,复位后的软件定时器以复位时的时刻作为开启时刻重新定时。

5. 更改软件定时器定时时间

xNewPeriod:新的定时超时时间,单位:系统时钟节拍。

其余参数与返回值同上。

实现:

创建两个定时器:

定时器1,周期定时器,每1秒打印一次 liangxu shuai

定时器2,单次定时器,启动后 2 秒打印一次 laochen shuai

cubeMX配置

software timer ope修改USE_TIMERS的值为Enable

代码实现:

/* USER CODE BEGIN Header_StartDefaultTask */
/**
* @brief Function implementing the defaultTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void const * argument)
{
/* USER CODE BEGIN StartDefaultTask */
osTimerStart(myTimer01Handle, 1000);
//xTimerChangePeriod(myTimer01Handle,pdMS_TO_TICKS(1000),0); //使用原始的时钟节拍 pdMS_TO_TICKS ,单位为毫秒
osTimerStart(myTimer02Handle, 2000);
/* Infinite loop */
for(;;)
{
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
/* Callback01 function */
void Callback01(void const * argument)
{
/* USER CODE BEGIN Callback01 */
printf("callback01\r\n");
/* USER CODE END Callback01 */
}
/* Callback02 function */
void Callback02(void const * argument)
{
/* USER CODE BEGIN Callback02 */
printf("callback02\r\n");
/* USER CODE END Callback02 */
}

十三、中断管理

中断定义

中断(Interrupt)是一种由硬件或软件触发的信号,用于通知处理器需要立即处理某个事件。中断机制允许处理器暂停当前任务,转而去执行高优先级的任务(中断服务程序,ISR),处理完成后恢复原任务。

中断优先级

任何中断的优先级都大于任务!

在我们的操作系统,中断同样是具有优先级的,并且我们也可以设置它的优先级,但是他的优先级并不是从 0~15 ,默认情况下它是从 5~15 ,0~4 这 5 个中断优先级不是 FreeRTOS 控制的(5是取决于 configMAX_SYSCALL_INTERRUPT_PRIORITY)。

相关注意

在中断中必需使用中断相关的函数;

中断服务函数运行时间越短越好。

实现

创建一个队列及一个任务,按下按键 KEY1 触发中断,在中断服务函数里向队列里发送数据,任务则阻塞接

收队列数据。

cubeMX配置

代码实现

/* USER CODE BEGIN Includes */
#include "cmsis_os.h"
/* USER CODE END Includes */
/* USER CODE BEGIN 1 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
//往队列里面放入数据
uint32_t send_data = 1;
xQueueSendFromISR(myQueue01Handle,&send_data,NULL);
}
/* USER CODE END 1 */

freetos.c

/* USER CODE BEGIN Header_StartDefaultTask */
/**
* @brief Function implementing the defaultTask thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartDefaultTask */
void StartDefaultTask(void const * argument)
{
/* USER CODE BEGIN StartDefaultTask */
uint32_t rev;
/* Infinite loop */
for(;;)
{
if(xQueueReceive(myQueue01Handle, &rev, portMAX_DELAY) == pdTRUE)
printf("rev = %d \r\n",rev);
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}

 

   
66 次浏览       4
 
相关文章

CMM之后对CMMI的思考
对软件研发项目管理的深入探讨
软件过程改进
软件过程改进的实现
 
相关文档

软件过程改进框架
软件过程改进的CMM-TSP-PSP模型
过程塑造(小型软件团队过程改进)
软件过程改进:经验和教训
 
相关课程

以"我"为中心的过程改进(iProcess )
iProcess过程改进实践
CMMI体系与实践
基于CMMI标准的软件质量保证

最新活动计划
人工智能.机器学习TensorFlow 6-30[直播]
基于 UML 和EA进行分析设计 7-30[北京]
软件架构设计方法、案例与实践 7-24[北京]
用户体验、易用性测试与评估 7-25[西安]
图数据库与知识图谱 8-23[北京]
需求分析师能力培养 8-28[北京]
 
 
最新文章
iPerson的过程观:要 过程 or 结果
基于模型的需求管理方法与工具
敏捷产品管理之 Story
敏捷开发需求管理(产品backlog)
Kanban看板管理实践精要
最新课程
基于iProcess的敏捷过程
软件开发过程中的项目管理
持续集成与敏捷开发
敏捷过程实践
敏捷测试-简单而可行
更多...   
成功案例
英特尔 SCRUM-敏捷开发实战
某著名汽车 敏捷开发过程与管理实践
北京 敏捷开发过程与项目管理
东方证券 基于看板的敏捷方法实践
亚信 工作量估算
更多...