【学习日记】【FreeRTOS】任务调度时如何考虑任务优先级——任务的自动切换
写在前面
本文开始为 RTOS 加入考虑任务优先级的自动调度算法,代码大部分参考野火。
本文主要是一篇学习笔记,加入了笔者自己对野火代码的梳理和理解。
一、基本思路
- 首先我们要知道,在 RTOS 中,优先级越高、越需要被先执行的的任务的优先级的数字越大。比如优先级数字为 5 的任务就要比 优先级数字为 1 的任务先执行。
- 在之前我们定义过就绪列表。所谓就绪列表,就是一个包含多条链表的数组,其中的每条链表又包含多个 TCB 作为列表的多个节点。现在,我们要把相同优先级的任务放入同一条链表中,而优先级越高的链表在就绪列表中下标越大。如图:
- 所以,我们需要把不在延时中的任务放进就绪列表中,然后按照优先级的大小进行切换执行;而如果任务进入了就绪状态,就将其从就绪列表中剔除。
二、代码实现
依据上面的思路,我们需要:
- 一个记录就绪任务的最高优先级的变量
- 一个设置就绪任务的最高优先级的变量的函数
- 一个清除记录就绪任务的最高优先级的变量的函数
- 一个让当前任务指针切换到最高优先级任务的函数
- TCB 中要增加一个优先级的参数
- 修改了 TCB 后,相关的任务创建函数需要修改
- 如果任务进入延时,把它的就绪状态清除
- 修改任务切换函数,使其找到优先级最高的任务执行
- 修改计时函数,当有任务的延时结束,使其变回就绪状态
1. 记录就绪任务的最高优先级的变量
初始化为空闲任务的优先级,也就是最低的优先级 0:
#define tskIDLE_PRIORITY ( ( UBaseType_t ) 0U )static volatile UBaseType_t uxTopReadyPriority = tskIDLE_PRIORITY;
2. 设置、清除、选择就绪任务的最高优先级的变量的函数
① 通用方法
一种很朴素的想法是,使用上面定义的变量 uxTopReadyPriority 直接记录当前可执行的最高优先级。
- 设置函数:
#define taskRECORD_READY_PRIORITY( uxPriority ) \{ \if( ( uxPriority ) > uxTopReadyPriority ) \{ \uxTopReadyPriority = ( uxPriority ); \} \} /* taskRECORD_READY_PRIORITY */
- 选择优先级最高任务的函数:
#define taskSELECT_HIGHEST_PRIORITY_TASK() \{ \UBaseType_t uxTopPriority = uxTopReadyPriority; \\/* 寻找包含就绪任务的最高优先级的队列 */ \while( listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ] ) ) ) \{ \--uxTopPriority; \} \\/* 获取优先级最高的就绪任务的TCB,然后更新到pxCurrentTCB */ \listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \/* 更新uxTopReadyPriority */ \uxTopReadyPriority = uxTopPriority; \} /* taskSELECT_HIGHEST_PRIORITY_TASK */
② 优化方法
下面这段话是野火教程的解释:
所谓优化方法,就是使用Cortex-M 内核一个计算前导零的指令
CLZ,所谓前导零就是计算一个变量(Cortex-M 内核单片机的变量为 32位)从高位开始第
一次出现 1 的位的前面的零的个数。比如:一个 32 位的变量 uxTopReadyPriority,其位 0、
位 24 和 位 25 均 置 1 , 其 余 位 为 0 , 具 体 见 。 那 么 使 用 前 导 零 指 令 __CLZ
(uxTopReadyPriority)可以很快的计算出 uxTopReadyPriority 的前导零的个数为 6。
如果 uxTopReadyPriority 的每个位号对应的是任务的优先级,任务就绪时,则将对应
的位置 1,反之则清零。那么图 10-2 就表示优先级 0、优先级 24 和优先级 25 这三个任务
就绪,其中优先级为 25的任务优先级最高。利用前导零计算指令可以很快计算出就绪任务
中的最高优先级为:( 31UL - ( uint32_t ) __clz( ( uxReadyPriorities ) ) ) = ( 31UL - ( uint32_t )
6 ) = 25。
- 设置函数
#define taskRECORD_READY_PRIORITY( uxPriority ) portRECORD_READY_PRIORITY( uxPriority, uxTopReadyPriority )#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities ) ( uxReadyPriorities ) |= ( 1UL << ( uxPriority ) )
- 清除函数
#define taskRESET_READY_PRIORITY( uxPriority ) \{ \portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) ); \}#define portRESET_READY_PRIORITY( uxPriority, uxReadyPriorities ) ( uxReadyPriorities ) &= ~( 1UL << ( uxPriority ) )
- 选择函数
#define taskSELECT_HIGHEST_PRIORITY_TASK() \{ \UBaseType_t uxTopPriority; \\/* 寻找最高优先级 */ \portGET_HIGHEST_PRIORITY( uxTopPriority, uxTopReadyPriority ); \/* 获取优先级最高的就绪任务的TCB,然后更新到pxCurrentTCB */ \listGET_OWNER_OF_NEXT_ENTRY( pxCurrentTCB, &( pxReadyTasksLists[ uxTopPriority ] ) ); \} /* taskSELECT_HIGHEST_PRIORITY_TASK() */
3. 修改 TCB 和相关的 TCB 创建函数
① 修改 TCB
增加 UBaseType_t uxPriority; /* 用于优先级 */ 参数:
typedef struct tskTaskControlBlock
{volatile StackType_t *pxTopOfStack; /* 栈顶 */ListItem_t xStateListItem; /* 任务节点 */StackType_t *pxStack; /* 任务栈起始地址 *//* 任务名称,字符串形式 */char pcTaskName[ configMAX_TASK_NAME_LEN ]; TickType_t xTicksToDelay; /* 用于延时 */UBaseType_t uxPriority; /* 用于优先级 */
} tskTCB;
typedef tskTCB TCB_t;
② 修改静态任务创建函数:
调用任务初始化函数并将任务添加到就绪列表:
TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, /* 任务入口 */const char * const pcName, /* 任务名称,字符串形式 */const uint32_t ulStackDepth, /* 任务栈大小,单位为字 */void * const pvParameters, /* 任务形参 */UBaseType_t uxPriority, /* 任务优先级,数值越大,优先级越高 */StackType_t * const puxStackBuffer, /* 任务栈起始地址 */TCB_t * const pxTaskBuffer ) /* 任务控制块 */
{TCB_t *pxNewTCB;TaskHandle_t xReturn;if( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) ){ pxNewTCB = ( TCB_t * ) pxTaskBuffer; pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer;/* 创建新的任务 */prvInitialiseNewTask( pxTaskCode, pcName, ulStackDepth, pvParameters,uxPriority, &xReturn, pxNewTCB);/* 将任务添加到就绪列表 */prvAddNewTaskToReadyList( pxNewTCB );}else{xReturn = NULL;}/* 返回任务句柄,如果任务创建成功,此时xReturn应该指向任务控制块 */return xReturn;
}
③ 修改任务初始化函数
主要是初始化了任务的优先级:
static void prvInitialiseNewTask( TaskFunction_t pxTaskCode, /* 任务入口 */const char * const pcName, /* 任务名称,字符串形式 */const uint32_t ulStackDepth, /* 任务栈大小,单位为字 */void * const pvParameters, /* 任务形参 */UBaseType_t uxPriority, /* 任务优先级,数值越大,优先级越高 */TaskHandle_t * const pxCreatedTask, /* 任务句柄 */TCB_t *pxNewTCB ) /* 任务控制块 */{StackType_t *pxTopOfStack;UBaseType_t x; /* 获取栈顶地址 */pxTopOfStack = pxNewTCB->pxStack + ( ulStackDepth - ( uint32_t ) 1 );//pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );/* 向下做8字节对齐 */pxTopOfStack = ( StackType_t * ) ( ( ( uint32_t ) pxTopOfStack ) & ( ~( ( uint32_t ) 0x0007 ) ) ); /* 将任务的名字存储在TCB中 */for( x = ( UBaseType_t ) 0; x < ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ ){pxNewTCB->pcTaskName[ x ] = pcName[ x ];if( pcName[ x ] == 0x00 ){break;}}/* 任务名字的长度不能超过configMAX_TASK_NAME_LEN */pxNewTCB->pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = '\0';/* 初始化TCB中的xStateListItem节点 */vListInitialiseItem( &( pxNewTCB->xStateListItem ) );/* 设置xStateListItem节点的拥有者 */listSET_LIST_ITEM_OWNER( &( pxNewTCB->xStateListItem ), pxNewTCB );/* 初始化优先级 */if( uxPriority >= ( UBaseType_t ) configMAX_PRIORITIES ){uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U;}pxNewTCB->uxPriority = uxPriority;/* 初始化任务栈 */pxNewTCB->pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters ); /* 让任务句柄指向任务控制块 */if( ( void * ) pxCreatedTask != NULL ){ *pxCreatedTask = ( TaskHandle_t ) pxNewTCB;}
}
④ 增加将新创建的任务添加到就绪列表的函数
- 修改任务个数计数器
- 调用任务插入就绪列表函数
static void prvAddNewTaskToReadyList( TCB_t *pxNewTCB )
{/* 进入临界段 */taskENTER_CRITICAL();{/* 全局任务计数器加一操作 */uxCurrentNumberOfTasks++;/* 如果pxCurrentTCB为空,则将pxCurrentTCB指向新创建的任务 */if( pxCurrentTCB == NULL ){pxCurrentTCB = pxNewTCB;/* 如果是第一次创建任务,则需要初始化任务相关的列表 */if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 ){/* 初始化任务相关的列表 */prvInitialiseTaskLists();}}else /* 如果pxCurrentTCB不为空,则根据任务的优先级将pxCurrentTCB指向最高优先级任务的TCB */{if( pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority ){pxCurrentTCB = pxNewTCB;}}uxTaskNumber++;/* 将任务添加到就绪列表 */prvAddTaskToReadyList( pxNewTCB );}/* 退出临界段 */taskEXIT_CRITICAL();
}
⑤ 修改将任务添加到就绪列表的函数
- 修改优先级就绪变量
- 将任务按照优先级大小插入对应的列表下标链表中:
/* 将任务添加到就绪列表 */
#define prvAddTaskToReadyList( pxTCB ) \taskRECORD_READY_PRIORITY( ( pxTCB )->uxPriority ); \vListInsertEnd( &( pxReadyTasksLists[ ( pxTCB )->uxPriority ] ), &( ( pxTCB )->xStateListItem ) ); \
3. 修改任务阻塞延时函数
- 把任务从就绪列表中移除(因为我们现在还没有延时链表,所以先不做这个)
- 将任务的优先级就绪变量清除
void vTaskDelay( const TickType_t xTicksToDelay )
{TCB_t *pxTCB = NULL;/* 获取当前任务的TCB */pxTCB = pxCurrentTCB;/* 设置延时时间 */pxTCB->xTicksToDelay = xTicksToDelay;/* 将任务从就绪列表移除 *///uxListRemove( &( pxTCB->xStateListItem ) );taskRESET_READY_PRIORITY( pxTCB->uxPriority );/* 任务切换 */taskYIELD();
}
4. 修改任务切换函数
- 寻找优先级最高的就绪任务执行即可:
/* 任务切换,即寻找优先级最高的就绪任务 */
void vTaskSwitchContext( void )
{/* 获取优先级最高的就绪任务的TCB,然后更新到pxCurrentTCB */taskSELECT_HIGHEST_PRIORITY_TASK();
}
- taskSELECT_HIGHEST_PRIORITY_TASK() 在上文已经定义
5. 修改计时函数
当有任务的延时结束,使其变回就绪状态:
void xTaskIncrementTick( void )
{TCB_t *pxTCB = NULL;BaseType_t i = 0;const TickType_t xConstTickCount = xTickCount + 1;xTickCount = xConstTickCount;/* 扫描就绪列表中所有线程的remaining_tick,如果不为0,则减1 */for(i=0; i<configMAX_PRIORITIES; i++){pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( ( &pxReadyTasksLists[i] ) );if(pxTCB->xTicksToDelay > 0){pxTCB->xTicksToDelay --;/* 延时时间到,将任务就绪 */if( pxTCB->xTicksToDelay ==0 ){taskRECORD_READY_PRIORITY( pxTCB->uxPriority );}}}/* 任务切换 */portYIELD();
}
三、结果展示
/* 创建任务 */Task1_Handle = xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */(char *)"Task1", /* 任务名称,字符串形式 */(uint32_t)TASK1_STACK_SIZE , /* 任务栈大小,单位为字 */(void *) NULL, /* 任务形参 */(UBaseType_t) 1, /* 任务优先级,数值越大,优先级越高 */(StackType_t *)Task1Stack, /* 任务栈起始地址 */(TCB_t *)&Task1TCB ); /* 任务控制块 *//* 将任务添加到就绪列表 */ //vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) );Task2_Handle = xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */(char *)"Task2", /* 任务名称,字符串形式 */(uint32_t)TASK2_STACK_SIZE , /* 任务栈大小,单位为字 */(void *) NULL, /* 任务形参 */(UBaseType_t) 2, /* 任务优先级,数值越大,优先级越高 */ (StackType_t *)Task2Stack, /* 任务栈起始地址 */(TCB_t *)&Task2TCB ); /* 任务控制块 */ /* 将任务添加到就绪列表 */ //vListInsertEnd( &( pxReadyTasksLists[2] ), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );/* 启动调度器,开始多任务调度,启动成功则不返回 */vTaskStartScheduler();
创建两个任务,在两个任务中分别对两个标志变量进行 电平变换-延时-电平变换 的循环操作,结果如下:
可以看到,两个标注变量几乎同时进行电平切换,CPU 没有被延时阻塞。
而且任务 2 由于设置的优先级比任务 1 高,所以电平先切换为高,优先级切换的功能添加成功。
后记
如果您觉得本文写得不错,可以点个赞激励一下作者!
如果您发现本文的问题,欢迎在评论区或者私信共同探讨!
共勉!
相关文章:

【学习日记】【FreeRTOS】任务调度时如何考虑任务优先级——任务的自动切换
写在前面 本文开始为 RTOS 加入考虑任务优先级的自动调度算法,代码大部分参考野火。 本文主要是一篇学习笔记,加入了笔者自己对野火代码的梳理和理解。 一、基本思路 首先我们要知道,在 RTOS 中,优先级越高、越需要被先执行的的…...

C语言暑假刷题冲刺篇——day3
目录 一、选择题 二、编程题 🎈个人主页:库库的里昂 🎐CSDN新晋作者 🎉欢迎 👍点赞✍评论⭐收藏✨收录专栏:C语言每日一练✨其他专栏:代码小游戏C语言初阶🤝希望作者的文章能对你有…...

Taro+vue3小程序开启分享他人和分享到朋友圈
import Taro, { useShareAppMessage, useShareTimeline } from tarojs/taro;onMounted(() > {Taro.showShareMenu({withShareTicket: true,menus: [shareAppMessage, shareTimeline]}); }); useShareAppMessage((res) > {console.log(页面转发的回调)return {title: 开票…...

JAVA-Spring中IOC容器是什么?
目录 JAVA-Spring中IOC容器是什么?什么是IOC?什么是IOC容器?IOC和IOC容器的对比Spring框架中的IOC容器是如何工作的?使用XML配置的ApplicationContext使用注解的AnnotationConfigApplicationContext总结 JAVA-Spring中IOC容器是什…...

QT多屏显示程序
多屏显示的原理其实很好理解,就拿横向扩展来说: 计算机把桌面的 宽度扩展成了 w1(屏幕1的宽度) w2(屏幕2的宽度) 。 当一个窗口的起始横坐标 > w1,则 他就被显示在第二个屏幕上了。 多屏虚拟成一个桌面࿰…...

python使用xlwt时,报ValueError: More than 4094 XFs (styles)
在写表格时,遇到如下报错 一、报错原因 xlwt最多只能有4094个样式,超出这个样式数量就报错了。 二、解决办法 (1)去掉样式的要求...

GitHub 打不开解决方案
GitHub 这几年国内普通用户越来越难以访问,github 作为全球最大的开源平台,里面有用的内容很多,不管是对专业用户还是普通用户,无法访问都是很严重的问题。 1.GitHub 加速镜像 kgithub 是一个公益加速项目,仅需在 gi…...

Java网络编程(一)网络基础
概述 计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统、网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递 网络分类 局域网(LAN) 局域网是一种在小区域内使用的,由多台计算机组成的网络,覆盖范围…...

matlab使用教程(17)—多项式的定义和运算
1.创建并计算多项式 此示例说明如何在 MATLAB 中将多项式表示为向量以及根据相关点计算多项式。 1.1 表示多项式 MATLAB 将多项式表示为行向量,其中包含按降幂排序的系数。例如,三元素向量 p [p2 p1 p0]; 表示多项式 创建一个向量以表示二次多项式…...

华为认证 | 这门HCIA认证正式发布!
华为认证云服务工程师HCIA-Cloud Service V3.5(中文版)自2023年8月11日起,正式在中国区发布。 01 发布概述 基于“平台生态”战略,围绕“云-管-端”协同的新ICT技术架构,华为公司打造了覆盖ICT领域的认证体系…...

【Docker】Docker安装 MySQL 8.0,简洁版-快速安装使用
今天,使用docker安装mysql数据库进行一个测试,结果网上找了一篇文章,然后。。。。坑死我… 特总结本篇安装教程,主打一个废话不多说! 坑:安装成功,客户端工具连接不上数据库》。。。 正文&…...

CSS自己实现一个步骤条
前言 步骤条是一种用于引导用户按照特定流程完成任务的导航条,在各种分步表单交互场景中广泛应用。例如:在HIS系统-门诊医生站中的接诊场景中,我们就可以使用步骤条来实现。她的执行步骤分别是:门诊病历>遗嘱录入>完成接诊…...

Visual Studio 2019 解决scanf函数报错问题
前言 Visual Studio 2019 解决scanf函数报错问题 博主博客链接:https://blog.csdn.net/m0_74014525 关注博主,后期持续更新系列文章 *****感谢观看,希望对你有所帮助***** 系列文章 第一篇:Visual Studio 2019 详细安装教程&…...

亚马逊无限买家号如何注册?
如果想要拥有大批量的亚马逊买家号,可以使用亚马逊鲲鹏系统进行自动注册操作。在注册之前我们需要先准备好账号所需要的资料; 1、Ip:软件系统支持11个亚马逊站点使用,因此注册哪一个国家的买家号时就需要购买相应国家的ip&#x…...

前端框架学习-ES6新特性(尚硅谷web笔记)
ECMASript是由 Ecma 国际通过 ECMA-262 标准化的脚本程序设计语言。javaScript也是该规范的一种实现。 新特性目录 笔记出处:b站ES6let 关键字const关键字变量的解构赋值模板字符串简化对象写法箭头函数rest参数spread扩展运算符Promise模块化 ES8async 和 await E…...

普陀发布新规服务元宇宙企业 和数软件发展元宇宙场景落地
近日,数智中国AIGC科技周2023全球元宇宙大会上海站活动现场“半马苏河”元宇宙企业科创政策包正式发布。政策包在普陀原有科创政策基础上进行了叠加升级,一共涵盖了十条专项服务元宇宙企业的专项政策,简称普陀“元十条”。 普陀“元十条”从…...

Kotlin差异化分析,let,run,with,apply及also
作用域函数是Kotlin比较重要的一个特性,共分为以下5种:let、run、with、apply 以及 also,这五个函数的工作方式可以说非常相似,但是我们需要了解的是这5种函数的差异,以便在不同的场景更好的利用它。 读完这篇文章您将…...

(stm32)低功耗模式
低功耗模式 执行哪个低功耗模式的程序判断流程 标志位设置操作一定要在WFI/WFE之前,调用此指令后立即进入睡眠判断流程 模式对比 睡眠模式 停止模式 待机模式...

【C++学习手札】一文带你认识C++虚函数(内层剖析)
食用指南:本文在有C基础的情况下食用更佳 🍀本文前置知识: C初识继承 ♈️今日夜电波:No title —REOL 1:02 ━━━━━━️💟──────── 4:03 …...

Python“牵手”1688商品评论数据采集方法,1688API申请指南
1688平台API接口是为开发电商类应用程序而设计的一套完整的、跨浏览器、跨平台的接口规范,1688API接口是指通过编程的方式,让开发者能够通过HTTP协议直接访问1688平台的数据,包括商品信息、店铺信息、物流信息等,从而实现1688平台…...

“深入解析JVM内部机制:探秘Java虚拟机的奥秘“
标题:深入解析JVM内部机制:探秘Java虚拟机的奥秘 摘要:本文将深入解析JVM(Java虚拟机)的内部机制,从字节码执行到垃圾回收,逐步揭示Java程序运行的奥秘。通过理论分析和示例代码,读…...

【系统工具】开源服务器监控工具WGCLOUD初体验
经常看到服务器上传下载流量一直在跑,也不知道是啥软件在偷偷联网~~~官网地址:www.wgstart.com,个人使用是免费的。 WGCLOUD官网介绍 "WGCLOUD支持主机各种指标监测(cpu使用率,cpu温度,内存使用率&am…...

powerBI应用技巧
power BI应用技巧 持续跟新 1.函数日期表 使用高级编辑器—M函数(把下面代码直接复制即可) 版本1:前单位写的M函数,日期是2021年开始的 letdate(optional 请输入开始年份 as number,optional 请输入结束年份 as number)>l…...

RK3568背光调试分享
一.PWM 驱动 驱动文件所在位置: drivers/pwm/pwm-rockchip.c 3.10 和 4.4 及以上版本内核下驱动文件名字是同一个, pwm-rockchip.c 只支持 Continuous mode , 但是里面的代码有些差别。 4.4 及以上内核版本将 pwm_config() , pwm_enable()...

Numpy入门(2)—随机数
随机数np.random 主要知识点: 创建ndarray随机数组随机打乱ndarray元素顺序随机选取元素 2.1 创建随机ndarray数组 创建随机ndarray数组主要包含设置随机种子、均匀分布和正态分布三部分内容,具体代码如下所示。 设置随机数种子 # 可以多次运行&am…...

ansible的playbook剧本
playbook剧本 PlayBook1.playbooks 本身由以下各部分组成2.示例:3.运行playbook补充参数: 4.定义、引用变量5.指定远程主机sudo切换用户6.when条件判断7.迭代8.Templates 模块1.先准备一个以 .j2 为后缀的 template 模板文件,设置引用的变量2…...

【03 英语语法:从句(名词从句、定语从句/形容词从句、状语从句/副词从句)】
从句 从句:名词从句、定语从句、状语从句(名定状名形副)1. 名词从句(名词):主语、宾语、表语、同位语、宾补▲名词从句的种类(按引导词): that、whether、疑问词 句子⑴…...

vue动态修改audio地址
问题:点击后替换url地址,实现了,但是播放器依旧没有反应。 解决:vue中动态替换只是替换了地址,并没有告诉audio标签是否要执行,执行什么操作。要load后才能让它知道,是在喊他,他需求…...

CF113A Grammar Lessons 题解
一道模拟题。 题目传送门 题目意思: 给你一个句子,让你检查这个句子的语法是否正确。(语法请自行在题目中查看) 思路: 就是模拟。依次判断这个句子是否符合每一条语法即可。但是细节很多就因为细节我错了好多次&…...

puzzle(0414)六边形拼图
目录 六边形拼图 简单 中等 困难 六边形拼图 taptap小游戏 简单 (3) (4) 中等 (3) (4) 困难 (2) (3) (4ÿ…...