实战问题
实战问题
初见
//优先级
#define START_TASK_PRIO 1
#define LED0_TASK_PRIO 4
#define LED1_TASK_PRIO 5
#define FLOAT_TASK_PRIO 6
#define KEY_TASK_PRIO 3
//Q:最大可以设置多大少?被配置文件哪个宏决定的?由 configMAX_PRIORITIES 宏决定,优先级范围:0 到 configMAX_PRIORITIES - 1
//堆栈
#define START_STK_SIZE 128
#define LED0_STK_SIZE 128
#define LED1_STK_SIZE 128
#define FLOAT_STK_SIZE 128
#define KEY_STK_SIZE 128
//Q:分配的时候单位是4字节吧?实际要*4?// 在 task.c 文件中
BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
const char * const pcName,
const uint16_t usStackDepth, // 注意这个参数!
void * const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * const pxCreatedTask )
{
TCB_t *pxNewTCB;
BaseType_t xReturn;
// 为TCB和堆栈分配内存
pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) );
if( pxNewTCB != NULL ) {
// 为任务堆栈分配内存 - 关键行!
pxNewTCB->pxStack = ( StackType_t * ) pvPortMalloc(
( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ); // 这里乘以了sizeof(StackType_t)
if( pxNewTCB->pxStack != NULL ) {
// 初始化任务...
prvInitialiseNewTask( pxTaskCode, pcName, usStackDepth,
pvParameters, uxPriority, pxCreatedTask, pxNewTCB );
xReturn = pdPASS;
}
}
return xReturn;
}对的,V9的freeRTOS单位是字
typedef uint32_t StackType_t; // 堆栈单位是32位(4字节)
//声明
void start_task(void *pv);
void led0_task(void *pv);
void led1_task(void *pv);
void float_task(void *pv);
void key_task(void *pv);
//Q:为什么要参数,并且是万能指针?也就是说,如果LED控制逻辑一样,只是控制的参数不同的话(亮灭时间不同),就可以复用相同的代码逻辑。再创建多个任务。
// 没有参数 - 只能创建相同的LED任务
void led_task(void *pv) {
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // 固定控制PB0
}
// 有参数 - 可以控制不同的LED
void led_task(void *pv) {
LED_Config *led_cfg = (LED_Config *)pv; // 通过参数区分
HAL_GPIO_TogglePin(led_cfg->port, led_cfg->pin);
}
// 创建多个不同的LED任务
LED_Config led1_cfg = {GPIOB, GPIO_PIN_0, 500}; // PB0, 500ms
LED_Config led2_cfg = {GPIOB, GPIO_PIN_1, 1000}; // PB1, 1000ms
xTaskCreate(led_task, "LED1", 128, &led1_cfg, 4, NULL);
xTaskCreate(led_task, "LED2", 128, &led2_cfg, 4, NULL);xTaskCreate((TaskFunction_t)start_task,
(const char *)"start_task",
(uint16_t) START_STK_SIZE,
(void *)NULL,
(UBaseType_t)START_TASK_PRIO,
(TaskHandle_t*)&StartTask_Handler
);
//Q:能不能不在参数前面强制转换?很麻烦可以,本来就是隐式强转
void start_task(void *pv){
taskENTER_CRITICAL();
xTaskCreate(led0_task,
"led0_task",
LED0_STK_SIZE,
NULL,
LED0_TASK_PRIO,
&LED0Task_Handler);
xTaskCreate(led1_task,
"led1_task",
LED1_STK_SIZE,
NULL,
LED1_TASK_PRIO,
&LED1Task_Handler);
xTaskCreate(float_task,
"flaot_task",
FLOAT_STK_SIZE,
NULL,
FLOAT_TASK_PRIO,
&FLOATTask_Handler);
xTaskCreate(key_task,
"key_task",
KEY_STK_SIZE,
NULL,
KEY_TASK_PRIO,
&KEYTask_Handler);
vTaskDelete(StartTask_Handler);
//Q:删除自身在空闲任务里面才释放对吧?
taskEXIT_CRITICAL();
}删除过程:
- 立即发生:任务立即停止执行,从就绪/运行态移除
- 资源标记:TCB和堆栈标记为"待释放"
- 空闲任务清理:由空闲任务实际释放内存
void start_task(void *pv){
taskENTER_CRITICAL(); // 进入临界区
// 创建多个任务...
vTaskDelete(StartTask_Handler); // ❌ 在临界区内删除自身!
taskEXIT_CRITICAL(); // ❌ 这行代码永远不会执行c!
}
//AI说交换下上面两行的顺序,并且是用vTaskDelete(NULL) vTaskDelay(1000);//Q:这个能放在程序中间吗?毕竟回来之后还会接着往下运行当然没问题
消息队列
xLEDQueue=xQueueCreate(10,sizeof(LED_Command_t));//Q:为什么创建10个,明明只用了一个?来当缓冲?相当于一个数组,第一个参数是数组的大小,第二个参数是数组单位元素的大小
按键每次按下都会发送,然后LED任务去接收处理。我们设置保持时间是1s,也就是接收到了就vTaskDelay(1000)
xQueueSend(xLEDQueue, &cmd2, 0);//Q:队列满了,再向队列发消息会失败还是覆盖? 失败,连点试试如果队列的大小是1.那么很容易出现队列满,导致发送失败(队列满了再发送数据不会覆盖),也就是数据丢失。
队列会满是因为接收方没有及时接收
//队列句柄
QueueHandle_t xLEDQueue;//Q:指向的是队列控制块?队列结构?对,见下面
信号量
互斥访问资源
// 创建二值信号量
xUARTMutex = xSemaphoreCreateBinary();
if(xUARTMutex == NULL) {
printf("信号量创建失败!\r\n");
} else {
printf("串口互斥信号量创建成功!\r\n");
xSemaphoreGive(xUARTMutex); // 初始化为可用
}
//Q:返回的是信号量句柄?和任务句柄一样吗?都指向任务控制块?信号量的本质是长度是1的队列,信号量句柄实际上就是队列句柄,指向队列控制块
typedef struct QueueDefinition
{
int8_t *pcHead; // 队列缓冲区头
int8_t *pcTail; // 队列缓冲区尾
int8_t *pcWriteTo; // 当前写入位置
int8_t *pcReadFrom; // 当前读取位置
List_t xTasksWaitingToSend; // 等待发送的任务列表
List_t xTasksWaitingToReceive; // 等待接收的任务列表
volatile UBaseType_t uxMessagesWaiting; // 当前队列中消息数量
UBaseType_t uxLength; // 队列长度
UBaseType_t uxItemSize; // 每个消息大小
...
} Queue_t;
typedef struct tskTaskControlBlock
{
StackType_t *pxTopOfStack; // 当前任务栈顶指针
ListItem_t xStateListItem; // 就绪、阻塞等状态链表节点
ListItem_t xEventListItem; // 等待事件(如信号量)的链表节点
StackType_t *pxStack; // 任务栈起始地址
char pcTaskName[configMAX_TASK_NAME_LEN]; // 任务名
UBaseType_t uxPriority; // 任务优先级
...
} TCB_t;堆区内存(FreeRTOS自己管理的一大块RAM)
┌────────────────────────────────────┐
│ │
│ [TCB_t] ←── TaskHandle_t(task1) │ ← 任务句柄指向任务控制块
│ [Stack1] │ ← 任务自己的栈空间
│ │
│ [Queue_t] ←── SemaphoreHandle_t │ ← 信号量/队列句柄指向队列控制块
│ [Queue Buffer] │ ← 队列或信号量的数据存放处
│ │
└────────────────────────────────────┘注意:在等待信号量或者消息的过程中,任务都是被阻塞的,CPU去执行其他任务,只有当阻塞时间到了或者拿到了信号量才会切换到运行态
多个任务都要用串口,那么就轮流拿取这个信号。
纠正:时间片轮转时,可以看作多个任务几乎并行运行,但是如果一个任务拿到了信号,其他任务运行一次后就会被阻塞,时间片就失效了(因为只有它可以运行)
感觉和消息队列差不太多
事件标志组
#define TEMP_HIGH_BIT (1UL<<0)//Q:为什么要加括号并且加UL?防止数学运算的时候出错
// 不加括号的危险:
#define TEMP_HIGH_BIT 1UL << 0
// 使用时可能出问题:
if(events & TEMP_HIGH_BIT == 1) {
// 编译器理解为:events & (1UL << 0 == 1)
// 实际是:events & (1UL << (0 == 1))
// 这完全不是我们想要的意思!
}
// 加括号后安全:
#define TEMP_HIGH_BIT (1UL << 0)
if(events & TEMP_HIGH_BIT == 1) {
// 编译器理解为:(events & (1UL << 0)) == 1
// 这才是正确的!
}有符号整数的话,有一位是符号位
// 在32位系统中:
#define BIT_31 (1 << 31) // ❌ 有符号整数,结果是负数!
#define BIT_31 (1UL << 31) // ✅ 无符号整数,正确
// 使用:
uint32_t events = 0;
events |= BIT_31; // 如果是 (1 << 31),会警告符号问题软件定时器
xSoilCheckTimer=xTimerCreate("SoilCheck",pdMS_TO_TICKS(3000),pdTRUE,0,soil_check_callback);
//Q:回调函数void soil_check_callback(TimerHandle_t xTimer)里面也有参数,我传入了吗?在这个函数里面会自己操作一个控制块地址,这个地址一个用来返回,另一个用来传入当传入的参数
任务通知模拟信号量
2大API
API家族1
简化版API(专为eIncrement模式)
没有具体的数字,模拟信号量
// 发送:直接递增通知值
void vTaskNotifyGive(TaskHandle_t xTaskToNotify);
BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify);
// 接收:获取并清零通知值
uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit, TickType_t xTicksToWait);底层
#define xTaskNotifyGive( xTaskToNotify ) xTaskGenericNotify( ( xTaskToNotify ), ( 0 ), eIncrement, NULL )模拟二值信号量的话,就要把xClearCountOnExit参数改成pdTURE,这样的话拿到就会清零那32位的值;无论释放了几次通知(在eIncrement模式下,每通知一次就会使那32位的值++),最终只会执行一次
反之,就是计数信号,调用函数释放了几次,始终会接收几次
家族2
完整模式
// 发送:支持所有5种模式
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction);
// 接收:灵活的参数控制
BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,
TickType_t xTicksToWait);底层
BaseType_t xTaskGenericNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue, eNotifyAction eAction, uint32_t *pulPreviousNotificationValue ) PRIVILEGED_FUNCTION;
#define xTaskNotify( xTaskToNotify, ulValue, eAction ) xTaskGenericNotify( ( xTaskToNotify ), ( ulValue ), ( eAction ), NULL )任务通知模拟消息邮箱
err=xTaskNotifyWait(0x00,0xffffffffUL,&NotifyValue,portMAX_DELAY);
//Q:另一个api返回的是32位的任务通知值?对
err=xTaskNotify(KEY_Handler,key,eSetValueWithOverwrite);
//Q:覆写,覆盖的是32位的任务通知吗?那只能一次只能发一个?对,所以叫做邮箱,覆写模式下,发一次,下一次发送就覆盖
5种模式
| 模式 | 英文名 | 通知值的变化 | 若上次值未读取 | 典型用途 |
|---|---|---|---|---|
| ① eNoAction | 无动作 | 不改通知值 | 无影响 | 相当于“任务同步”,只唤醒任务 |
| ② eSetBits | 置位 | `NotifyValue | = bits` | 累积多个事件 |
| ③ eIncrement | 自增 | NotifyValue++ | 继续加 | 统计次数(如中断计数) |
| ④ eSetValueWithOverwrite | 覆写 | NotifyValue = newValue | 直接覆盖 | 只关心最后一个值的情况 |
| ⑤ eSetValueWithoutOverwrite | 不覆盖 | NotifyValue = newValue | 忽略(返回失败) | 确保不丢数据的场合 |
| 模式 | 比喻 |
|---|---|
| eNoAction | 按门铃,不放信(只唤醒) |
| eSetBits | 邮箱里放一封“可以累积的信” |
| eIncrement | 邮递次数 +1 |
| eSetValueWithOverwrite | 丢掉旧信,直接放新信 |
| eSetValueWithoutOverwrite | 邮箱有信就不放(防止覆盖) |
任务通知模拟事件标志组
xTaskNotify(LED_Handler,EVENTBIT_0,eSetBits);
err=xTaskNotifyWait(0x00,0xffffffffUL,&NotifyValue,portMAX_DELAY);总结:
每个任务自带一个32位的任务通知,还有32/16位的事件标志组。
其实模拟就是不同模式下的任务通知,信号量用eincrement,消息邮箱用eSetValueWithOverwrite,事件标志组用eSetBits

