ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FreeRTOS事件组 基于STM32

FreeRTOS事件组 基于STM32 概述文章对事件组的应用场景运作机制以及事件的创建删除等待置位同步等操作文章目录概述一、事件标志组简介1、事件位(事件标志)2、事件组3、事件标志组和事件位的数据类型二、事件的应用场景三、事件运作机制四、事件控制块五、事件组函数1.事件创建函数 xEventGroupCreate()2.事件删除函数 vEventGroupDelete()3.事件组置位函数 xEventGroupSetBits()任务4.等待事件函数 xEventGroupWaitBits()5. xEventGroupClearBits()与 xEventGroupClearBitsFromISR()六、事件实验七、实验现象一、事件标志组简介1、事件位(事件标志)事件位用来表明某个事件是否发生事件位通常用作事件标志比如下面的几个例子● 当收到一条消息并且把这条消息处理掉以后就可以将某个位(标志)置 1当队列中没有 消息需要处理的时候就可以将这个位(标志)置 0。● 当把队列中的消息通过网络发送输出以后就可以将某个位(标志)置 1当没有数据需要 从网络发送出去的话就将这个位(标志)置 0。● 现在需要向网络中发送一个心跳信息将某个位(标志)置 1。现在不需要向网络中发送 心跳信息这个位(标志)置 0。2、事件组一个事件组就是一组的事件位事件组中的事件位通过位编号来访问同样以上面列出 的三个例子为例● 事件标志组的 bit0 表示队列中的消息是否处理掉。● 事件标志组的 bit1 表示是否有消息需要从网络中发送出去。● 事件标志组的 bit2 表示现在是否需要向网络发送心跳信息。3、事件标志组和事件位的数据类型事件标志组的数据类型为 EventGroupHandle_t当 configUSE_16_BIT_TICKS 为 1 的时候 事件标志组可以存储 8 个事件位当 configUSE_16_BIT_TICKS 为 0 的时候事件标志组存储 24 个事件位。事件标志组中的所有事件位都存储在一个无符号的 EventBits_t 类型的变量中EventBits_t 在 event_groups.h 中有如下定义typedef TickType_t EventBits_t;数据类型 TickType_t 在文件 portmacro.h 中有如下定义#if( configUSE_16_BIT_TICKS 1 ) typedef uint16_t TickType_t; #define portMAX_DELAY ( TickType_t ) 0xffff #else typedef uint32_t TickType_t; #define portMAX_DELAY ( TickType_t ) 0xffffffffUL #define portTICK_TYPE_IS_ATOMIC 1 #endif可以看出当 configUSE_16_BIT_TICKS 为 0 的时候 TickType_t 是个 32 位的数据类型因 此 EventBits_t 也是个 32 位的数据类型。EventBits_t 类型的变量可以存储 24 个事件位另外的 那高 8 位有其他用。事件位 0 存放在这个变量的 bit0 上变量的 bit1 就是事件位 1以此类推。 对于 STM32 来说一个事件标志组最多可以存储 24 个事件位如图1所示图1 事件标志组和事件位二、事件的应用场景FreeRTOS 的事件用于事件类型的通讯无数据传输也就是说我们可以用事件来做 标志位判断某些事件是否发生了然后根据结果做处理那很多人又会问了为什么我 不直接用变量做标志呢岂不是更好更有效率非也非也若是在裸机编程中用全局变 量是最为有效的方法这点我不否认但是在操作系统中使用全局变量就要考虑以下问 题了如何对全局变量进行保护呢如何处理多任务同时对它进行访问如何让内核对事件进行有效管理呢使用全局变量的话就需要在任务中轮询查 看事件是否发送这简直就是在浪费 CPU 资源啊还有等待超时机制使用全局 变量的话需要用户自己去实现。所以在操作系统中还是使用操作系统给我们提供的通信机制就好了简单方便还 实用。在某些场合可能需要多个时间发生了才能进行下一步操作比如一些危险机器的启 动需要检查各项指标当指标不达标的时候无法启动但是检查各个指标的时候不 能一下子检测完毕啊所以需要事件来做统一的等待当所有的事件都完成了那么机 器才允许启动这只是事件的其中一个应用。事件可使用于多种场合它能够在一定程度上替代信号量用于任务与任务间中断 与任务间的同步。一个任务或中断服务例程发送一个事件给事件对象而后等待的任务被 唤醒并对相应的事件进行处理。但是它与信号量不同的是事件的发送操作是不可累计的 而信号量的释放动作是可累计的。事件另外一个特性是接收任务可等待多种事件即多 个事件对应一个任务或多个任务。同时按照任务等待的参数可选择是“逻辑或”触发还 是“逻辑与”触发。这个特性也是信号量等所不具备的信号量只能识别单一同步动作 而不能同时等待多个事件的同步。各个事件可分别发送或一起发送给事件对象而任务可以等待多个事件任务仅对感 兴趣的事件进行关注。当有它们感兴趣的事件发生时并且符合感兴趣的条件任务将被唤 醒并进行后续的处理动作。三、事件运作机制接收事件时可以根据感兴趣的参事件类型接收事件的单个或者多个事件类型。事件 接收成功后必须使用 xClearOnExit 选项来清除已接收到的事件类型否则不会清除已接收 到的 事件 这样就需要用户显式清除事 位。用户可 以自 定义 通过 传入参 数 xWaitForAllBits 选择读取模式是等待所有感兴趣的事件还是等待感兴趣的任意一个事件。设置事件时对指定事件写入指定的事件类型设置事件集合的对应事件位为 1可 以一次同时写多个事件类型设置事件成功可能会触发任务调度。清除事件时根据入参数事件句柄和待清除的事件类型对事件对应位进行清 0 操作。 事件不与任务相关联事件相互独立一个 32位的变量事件集合实际用于表示事 件的只有 24 位用于标识该任务发生的事件类型其中每一位表示一种事件类型0 表 示该事件类型未发生、1表示该事件类型已经发生一共 24种事件类型具体见图2图2事件集合 set一个 32 位的变量事件唤醒机制当任务因为等待某个或者多个事件发生而进入阻塞态当事件发生的 时候会被唤醒其过程具体见图3图3 事件唤醒任务示意图任务 1 对事件 3 或事件 5 感兴趣逻辑或当发生其中的某一个事件都会被唤醒 并且执行相应操作。而任务 2 对事件 3 与事件 5 感兴趣逻辑与当且仅当事件 3 与事 件 5 都发生的时候任务 2 才会被唤醒如果只有一个其中一个事件发生那么任务还是 会继续等待事件发生。如果接在收事件函数中设置了清除事件位 xClearOnExit那么当任 务唤醒后将把事件 3 和事件 5 的事件标志清零否则事件标志将依然存在。四、事件控制块事件标志组存储在一个 EventBits_t 类型的变量中该变量在事件组结构体中定义具 体见代码清单 20-1 加粗部分。如果宏 configUSE_16_BIT_TICKS 定义为 1那么变量 uxEventBits 就 是 16 位 的 其 中 有 8 个 位 用来存储 事 件 组 如 果 宏 configUSE_16_BIT_TICKS 定义为 0那么变量 uxEventBits 就是 32 位的其中有 24 个位 用来存储事件组每一位代表一个事件的发生与否利用逻辑或、逻辑与等实现不同事件 的不同唤醒处理。在 STM32 中uxEventBits 是 32 位的所以我们有 24 个位用来实现事件 组。除了事件标志组变量之外FreeRTOS 还使用了一个链表来记录等待事件的任务所有 在等待此事件的任务均会被挂载在等待事件列表 xTasksWaitingForBits。typedef struct xEventGroupDefinition { EventBits_t uxEventBits; List_t xTasksWaitingForBits; #if( configUSE_TRACE_FACILITY 1 ) UBaseType_t uxEventGroupNumber; #endif #if( ( configSUPPORT_STATIC_ALLOCATION 1 ) \ ( configSUPPORT_DYNAMIC_ALLOCATION 1 ) ) uint8_t ucStaticallyAllocated; #endif } EventGroup_t;五、事件组函数1.事件创建函数 xEventGroupCreate()事件创建函数顾名思义就是创建一个事件与其他内核对象一样都是需要先创 建才能使用的资源FreeRTOS 给我们提供了一个创建事件的函数 xEventGroupCreate()当 创建一个事件时系统会首先给我们分配事件控制块的内存空间然后对该事件控制块进 行基本的初始化创建成功返回事件句柄创建失败返回 NULL。所以在使用创建函数 之前我们需要先定义有个事件的句柄。/* 创建一个事件组返回它的句柄。 * 此函数内部会分配事件组结构体 * 返回值: 返回句柄非NULL表示成功 */ EventGroupHandle_t xEventGroupCreate( void ); /* 创建一个事件组返回它的句柄。 * 此函数无需动态分配内存所以需要先有一个StaticEventGroup_t结构体并传入它的指针 * 返回值: 返回句柄非NULL表示成功 */ EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t * pxEventGroupBuffer );2.事件删除函数 vEventGroupDelete()vEventGroupDelete()用于删除由函数 xEventGroupCreate()创建的事件组只有被创建 成功的事件才能被删除但是需要注意的是该函数不允许在中断里面使用。当事件组被删 除之后阻塞在该事件组上的任务都会被解锁并向等待事件的任务返回事件组的值为 0 其使用是非常简单的。/* * xEventGroup: 事件组句柄你要删除哪个事件组 */ void vEventGroupDelete( EventGroupHandle_t xEventGroup )3.事件组置位函数 xEventGroupSetBits()任务xEventGroupSetBits()用于置位事件组中指定的位当位被置位之后阻塞在该位上的 任务将会被解锁。使用该函数接口时通过参数指定的事件标志来设定事件的标志位然 后遍历等待在事件对象上的事件等待列表判断是否有任务的事件激活要求与当前事件对 象标志值匹配如果有则唤醒该任务。简单来说就是设置我们自己定义的事件标志位 为 1并且看看有没有任务在等待这个事件有的话就唤醒它。xEventGroupSetBits()函数说明xEventGroupSetBits()函数使用实例#define KEY1_EVENT (0x01 0)//设置事件掩码的位0 #define KEY2_EVENT (0x01 1)//设置事件掩码的位1 static EventGroupHandle_t Event_Handle NULL; Event_Handle xEventGroupCreate(); if(NULL ! Event_Handle) printf(Event_Handle 事件创建成功!\r\n); static void KEY_Task(void* parameter) { /* 任务都是一个无限循环不能返回 */ while (1) { if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) KEY_ON ) //如果KEY2被单击 { printf ( KEY1被按下\n ); /* 触发一个事件1 */ xEventGroupSetBits(Event_Handle,KEY1_EVENT); } if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) KEY_ON ) //如果KEY2被单击 { printf ( KEY2被按下\n ); /* 触发一个事件2 */ xEventGroupSetBits(Event_Handle,KEY2_EVENT); } vTaskDelay(20); //每20ms扫描一次 } }4.等待事件函数 xEventGroupWaitBits()既然标记了事件的发生那么我怎么知道他到底有没有发生这也是需要一个函数来 获 取 事 件 是 否 已 经 发 生 FreeRTOS 提 供 了 一 个 等 待 指 定 事 件 的 函 数 — — xEventGroupWaitBits()通过这个函数任务可以知道事件标志组中的哪些位有什么事 件发生了然后通过 “逻辑与”、“逻辑或”等操作对感兴趣的事件进行获取并且这个 函数实现了等待超时机制当且仅当任务等待的事件发生时任务才能获取到事件信息。 在这段时间中如果事件一直没发生该任务将保持阻塞状态以等待事件发生。当其它任 务或中断服务程序往其等待的事件设置对应的标志位该任务将自动由阻塞态转为就绪态。 当任务等待的时间超过了指定的阻塞时间即使事件还未发生任务也会自动从阻塞态转 移为就绪态。这样子很有效的体现了操作系统的实时性如果事件正确获取等待到则 返回对应的事件标志位由用户判断再做处理因为在事件超时的时候也会返回一个不能 确定的事件值所以需要判断任务所等待的事件是否真的发生。xEventGroupWaitBits()函数说明xEventGroupWaitBits()使用实例static void LED_Task(void* parameter) { EventBits_t r_event; /* 定义一个事件接收变量 */ /* 任务都是一个无限循环不能返回 */ while (1) { /******************************************************************* * 等待接收事件标志 * * 如果xClearOnExit设置为pdTRUE那么在xEventGroupWaitBits()返回之前 * 如果满足等待条件如果函数返回的原因不是超时那么在事件组中设置 * 的uxBitsToWaitFor中的任何位都将被清除。 * 如果xClearOnExit设置为pdFALSE * 则在调用xEventGroupWaitBits()时不会更改事件组中设置的位。 * * xWaitForAllBits如果xWaitForAllBits设置为pdTRUE则当uxBitsToWaitFor中 * 的所有位都设置或指定的块时间到期时xEventGroupWaitBits()才返回。 * 如果xWaitForAllBits设置为pdFALSE则当设置uxBitsToWaitFor中设置的任何 * 一个位置1 或指定的块时间到期时xEventGroupWaitBits()都会返回。 * 阻塞时间由xTicksToWait参数指定。 *********************************************************/ r_event xEventGroupWaitBits(Event_Handle, /* 事件对象句柄 */ KEY1_EVENT|KEY2_EVENT,/* 接收线程感兴趣的事件 */ pdTRUE, /* 退出时清除事件位 */ pdTRUE, /* 满足感兴趣的所有事件 */ portMAX_DELAY);/* 指定超时事件,一直等 */ if((r_event (KEY1_EVENT|KEY2_EVENT)) (KEY1_EVENT|KEY2_EVENT)) { /* 如果接收完成并且正确 */ printf ( KEY1与KEY2都按下\n); LED1_TOGGLE; //LED1 反转 } else printf ( 事件错误\n); } }5. xEventGroupClearBits()与 xEventGroupClearBitsFromISR()xEventGroupClearBits()与 xEventGroupClearBitsFromISR()都是用于清除事件组指定的 位如果在获取事件的时候没有将对应的标志位清除那么就需要用这个函数来进行显式 清除xEventGroupClearBits()函数不能在中断中使用而是由具有中断保护功能 的 xEventGroupClearBitsFromISR() 来代替中断清除事件标志位的操作在守护任务也叫定 时 器 服 务 任 务 里 面 完 成 。 守 护 进 程 的 优 先 级 由 FreeRTOSConfig.h 中 的 宏 configTIMER_TASK_PRIORITY 来定义 。 要 想 使 用 该 函 数 必 须 把 FreeRTOS/source/event_groups.c 这个 C 文件添加到工程中。xEventGroupClearBits()与 xEventGroupClearBitsFromISR()函数说明xEventGroupClearBits()函数使用实例#define BIT_0 ( 1 0 ) #define BIT_4 ( 1 4 ) void aFunction( EventGroupHandle_t xEventGroup ) { EventBits_t uxBits; /* 清楚事件组的 bit 0 and bit 4 */ uxBits xEventGroupClearBits( xEventGroup, BIT_0 | BIT_4 ); if ( ( uxBits ( BIT_0 | BIT_4 ) ) ( BIT_0 | BIT_4 ) ) { /* 在调用 xEventGroupClearBits()之前 bit0 和 bit4 都置位 但是现在是被清除了*/ } else if ( ( uxBits BIT_0 ) ! 0 ) { /* 在调用 xEventGroupClearBits()之前 bit0 已经置位 但是现在是被清除了*/ } else if ( ( uxBits BIT_4 ) ! 0 ) { /* 在调用 xEventGroupClearBits()之前 bit4 已经置位 但是现在是被清除了*/ } else { /* 在调用 xEventGroupClearBits()之前 bit0 和 bit4 都没被置位 */ } }六、事件实验事件标志组实验是在 FreeRTOS 中创建了两个任务一个是设置事件任务一个是等 待事件任务两个任务独立运行设置事件任务通过检测按键的按下情况设置不同的事件 标志位等待事件任务则获取这两个事件标志位并且判断两个事件是否都发生如果是 则输出相应信息LED 进行翻转。等待事件任务的等待时间是 portMAX_DELAY一直在等待事件的发生等待到事件之后清除对应的事件标记位具体见代码清单 20-13 加粗部 分。/* FreeRTOS头文件 */ #include FreeRTOS.h #include task.h #include event_groups.h /* 开发板硬件bsp头文件 */ #include bsp_led.h #include bsp_usart.h #include bsp_key.h /**************************** 任务句柄 ********************************/ /* * 任务句柄是一个指针用于指向一个任务当任务创建好之后它就具有了一个任务句柄 * 以后我们要想操作这个任务都需要通过这个任务句柄如果是自身的任务操作自己那么 * 这个句柄可以为NULL。 */ static TaskHandle_t AppTaskCreate_Handle NULL;/* 创建任务句柄 */ static TaskHandle_t LED_Task_Handle NULL;/* LED_Task任务句柄 */ static TaskHandle_t KEY_Task_Handle NULL;/* KEY_Task任务句柄 */ /********************************** 内核对象句柄 *********************************/ /* * 信号量消息队列事件标志组软件定时器这些都属于内核的对象要想使用这些内核 * 对象必须先创建创建成功之后会返回一个相应的句柄。实际上就是一个指针后续我 * 们就可以通过这个句柄操作这些内核对象。 * * 内核对象说白了就是一种全局的数据结构通过这些数据结构我们可以实现任务间的通信 * 任务间的事件同步等各种功能。至于这些功能的实现我们是通过调用这些内核对象的函数 * 来完成的 * */ static EventGroupHandle_t Event_Handle NULL; /******************************* 全局变量声明 ************************************/ /* * 当我们在写应用程序的时候可能需要用到一些全局变量。 */ /******************************* 宏定义 ************************************/ /* * 当我们在写应用程序的时候可能需要用到一些宏定义。 */ #define KEY1_EVENT (0x01 0)//设置事件掩码的位0 #define KEY2_EVENT (0x01 1)//设置事件掩码的位1 /* ************************************************************************* * 函数声明 ************************************************************************* */ static void AppTaskCreate(void);/* 用于创建任务 */ static void LED_Task(void* pvParameters);/* LED_Task 任务实现 */ static void KEY_Task(void* pvParameters);/* KEY_Task 任务实现 */ static void BSP_Init(void);/* 用于初始化板载相关资源 */ /***************************************************************** * brief 主函数 * param 无 * retval 无 * note 第一步开发板硬件初始化 第二步创建APP应用任务 第三步启动FreeRTOS开始多任务调度 ****************************************************************/ int main(void) { BaseType_t xReturn pdPASS;/* 定义一个创建信息返回值默认为pdPASS */ /* 开发板硬件初始化 */ BSP_Init(); printf(这是一个FreeRTOS事件标志组实验\n); /* 创建AppTaskCreate任务 */ xReturn xTaskCreate((TaskFunction_t )AppTaskCreate, /* 任务入口函数 */ (const char* )AppTaskCreate,/* 任务名字 */ (uint16_t )512, /* 任务栈大小 */ (void* )NULL,/* 任务入口函数参数 */ (UBaseType_t )1, /* 任务的优先级 */ (TaskHandle_t* )AppTaskCreate_Handle);/* 任务控制块指针 */ /* 启动任务调度 */ if(pdPASS xReturn) vTaskStartScheduler(); /* 启动任务开启调度 */ else return -1; while(1); /* 正常不会执行到这里 */ } /*********************************************************************** * 函数名 AppTaskCreate * 功能说明 为了方便管理所有的任务创建函数都放在这个函数里面 * 参数 无 * 返回值 无 **********************************************************************/ static void AppTaskCreate(void) { BaseType_t xReturn pdPASS;/* 定义一个创建信息返回值默认为pdPASS */ taskENTER_CRITICAL(); //进入临界区 /* 创建 Event_Handle */ Event_Handle xEventGroupCreate(); if(NULL ! Event_Handle) printf(Event_Handle 事件创建成功!\r\n); /* 创建LED_Task任务 */ xReturn xTaskCreate((TaskFunction_t )LED_Task, /* 任务入口函数 */ (const char* )LED_Task,/* 任务名字 */ (uint16_t )512, /* 任务栈大小 */ (void* )NULL, /* 任务入口函数参数 */ (UBaseType_t )2, /* 任务的优先级 */ (TaskHandle_t* )LED_Task_Handle);/* 任务控制块指针 */ if(pdPASS xReturn) printf(创建LED_Task任务成功!\r\n); /* 创建KEY_Task任务 */ xReturn xTaskCreate((TaskFunction_t )KEY_Task, /* 任务入口函数 */ (const char* )KEY_Task,/* 任务名字 */ (uint16_t )512, /* 任务栈大小 */ (void* )NULL,/* 任务入口函数参数 */ (UBaseType_t )3, /* 任务的优先级 */ (TaskHandle_t* )KEY_Task_Handle);/* 任务控制块指针 */ if(pdPASS xReturn) printf(创建KEY_Task任务成功!\n); vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务 taskEXIT_CRITICAL(); //退出临界区 } /********************************************************************** * 函数名 LED_Task * 功能说明 LED_Task任务主体 * 参数 * 返回值 无 ********************************************************************/ static void LED_Task(void* parameter) { EventBits_t r_event; /* 定义一个事件接收变量 */ /* 任务都是一个无限循环不能返回 */ while (1) { /******************************************************************* * 等待接收事件标志 * * 如果xClearOnExit设置为pdTRUE那么在xEventGroupWaitBits()返回之前 * 如果满足等待条件如果函数返回的原因不是超时那么在事件组中设置 * 的uxBitsToWaitFor中的任何位都将被清除。 * 如果xClearOnExit设置为pdFALSE * 则在调用xEventGroupWaitBits()时不会更改事件组中设置的位。 * * xWaitForAllBits如果xWaitForAllBits设置为pdTRUE则当uxBitsToWaitFor中 * 的所有位都设置或指定的块时间到期时xEventGroupWaitBits()才返回。 * 如果xWaitForAllBits设置为pdFALSE则当设置uxBitsToWaitFor中设置的任何 * 一个位置1 或指定的块时间到期时xEventGroupWaitBits()都会返回。 * 阻塞时间由xTicksToWait参数指定。 *********************************************************/ r_event xEventGroupWaitBits(Event_Handle, /* 事件对象句柄 */ KEY1_EVENT|KEY2_EVENT,/* 接收线程感兴趣的事件 */ pdTRUE, /* 退出时清除事件位 */ pdTRUE, /* 满足感兴趣的所有事件 */ portMAX_DELAY);/* 指定超时事件,一直等 */ if((r_event (KEY1_EVENT|KEY2_EVENT)) (KEY1_EVENT|KEY2_EVENT)) { /* 如果接收完成并且正确 */ printf ( KEY1与KEY2都按下\n); LED1_TOGGLE; //LED1 反转 } else printf ( 事件错误\n); } } /********************************************************************** * 函数名 KEY_Task * 功能说明 KEY_Task任务主体 * 参数 * 返回值 无 ********************************************************************/ static void KEY_Task(void* parameter) { /* 任务都是一个无限循环不能返回 */ while (1) { if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) KEY_ON ) //如果KEY2被单击 { printf ( KEY1被按下\n ); /* 触发一个事件1 */ xEventGroupSetBits(Event_Handle,KEY1_EVENT); } if( Key_Scan(KEY2_GPIO_PORT,KEY2_GPIO_PIN) KEY_ON ) //如果KEY2被单击 { printf ( KEY2被按下\n ); /* 触发一个事件2 */ xEventGroupSetBits(Event_Handle,KEY2_EVENT); } vTaskDelay(20); //每20ms扫描一次 } } /*********************************************************************** * 函数名 BSP_Init * 功能说明 板级外设初始化所有板子上的初始化均可放在这个函数里面 * 参数 * 返回值 无 *********************************************************************/ static void BSP_Init(void) { /* * STM32中断优先级分组为4即4bit都用来表示抢占优先级范围为0~15 * 优先级分组只需要分组一次即可以后如果有其他的任务需要用到中断 * 都统一用这个优先级分组千万不要再分组切忌。 */ NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 ); /* LED 初始化 */ LED_GPIO_Config(); /* 串口初始化 */ USART_Config(); /* 按键初始化 */ Key_GPIO_Config(); } /********************************END OF FILE****************************/七、实验现象程序编译好用 USB 线连接电脑和开发板的 USB 接口对应丝印为 USB 转串口 用 仿真器把配套程序下载到 STM32 开发板在电脑上打开串口调试助手然后复位开发板就可 以在调试助手中看到串口的打印信息按下开发版的 KEY1 按键发送事件 1按下 KEY2 按键发送事件 2我们按下 KEY1 与 KEY2 试试在串口调试助手中可以看到运行结果 并且当事件 1 与事件 2都发生的时候开发板的 LED 会进行翻转。
返回列表