电子说
本节内容介绍
1、HAL库GPIO在cubemx中的配置及注意事项;
2、HAL库GPIO操作详解与结构介绍;
3、rt-thread任务介绍与创建;
4、利用多任务点灯,实现rtos多任务创建于执行;
HAL库GPIO在cubemx中的配置
上节课程我们介绍了cubemx的界面、时钟配置以及如何新建工程等,本节咱们就继续进行程序员届的“hello world”-“点灯”。
GPIO选择与配置
嵌入式软件工程师拿到板子第一件事要做的一定是熟悉原理图,你可以不会设计,但一定要能看懂,软件工程师调试代码,一看原理图,二才是写代码、调试代码
先来看看开发板上的LED是哪个引脚,可以看到PB6、PE3、PD15都是LED控制引脚,采用的是灌电流的方式,低电平灯亮:
接下来,咱们在cubemx对这些IO进行配置,小飞哥只选择了两个LED灯,名字命名和原理图保持一致或者是按照实际功能命名
右击选择第一个选项,就可以修改label
单击选中,会有很多功能,MCU的一个引脚是可以复用为许多功能的,我们根据自己的需要配置对应的模式即可,此处我们控制LED灯,低电平-灯亮,高电平-灯灭,显然是要配置为输出模式的,选择output即可,其他输出引脚同理
接下来我们来看下GPIO的一些模式配置
对于开发板上的LED控制引脚,我们可以按照如下配置,初始化输出高电平,LED不开启,这样初始化就可以设置GPIO输出电平,设置为需要的状态:
GPIO配置比较简单,就不再啰嗦了
HAL库GPIO操作详解与结构介绍
打开生成的代码,看看上面配置的GPIO初始化内容,上面cubemx的配置项可以看到已经生成对应的代码了,GPIO的配置顺序:使能GPIO时钟->配置相关采参数:
void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; /* GPIO Ports Clock Enable */ __HAL_RCC_GPIOE_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_GPIOH_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); /*Configure GPIO pin Output Level */ HAL_GPIO_WritePin(Y_LED_GPIO_Port, Y_LED_Pin, GPIO_PIN_RESET); /*Configure GPIO pin Output Level */ HAL_GPIO_WritePin(CP_LED_GPIO_Port, CP_LED_Pin, GPIO_PIN_RESET); /*Configure GPIO pin : PtPin */ GPIO_InitStruct.Pin = Y_LED_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(Y_LED_GPIO_Port, &GPIO_InitStruct); /*Configure GPIO pin : PtPin */ GPIO_InitStruct.Pin = CP_LED_Pin; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(CP_LED_GPIO_Port, &GPIO_InitStruct); }
关于GPIO操作的API:
/* Initialization and de-initialization functions *****************************/ void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init); void HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin); /** * @} */ /** @addtogroup GPIO_Exported_Functions_Group2 IO operation functions * @{ */ /* IO operation functions *****************************************************/ GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState); void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin); void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin);
本节由于是LED操作,我们只需要操作:
void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init); void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState); void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
如何使用呢?
参数GPIO_TypeDef *GPIOx可以是GPIO组的地址: #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE) #define GPIOB ((GPIO_TypeDef *) GPIOB_BASE) #define GPIOC ((GPIO_TypeDef *) GPIOC_BASE) #define GPIOD ((GPIO_TypeDef *) GPIOD_BASE) #define GPIOE ((GPIO_TypeDef *) GPIOE_BASE) #define GPIOF ((GPIO_TypeDef *) GPIOF_BASE) #define GPIOG ((GPIO_TypeDef *) GPIOG_BASE) #define GPIOH ((GPIO_TypeDef *) GPIOH_BASE) #define GPIOI ((GPIO_TypeDef *) GPIOI_BASE)
参数GPIO_Pin可以是GPIO的引脚号: #define GPIO_PIN_0 ((uint16_t)0x0001) /* Pin 0 selected */ #define GPIO_PIN_1 ((uint16_t)0x0002) /* Pin 1 selected */ #define GPIO_PIN_2 ((uint16_t)0x0004) /* Pin 2 selected */ #define GPIO_PIN_3 ((uint16_t)0x0008) /* Pin 3 selected */ #define GPIO_PIN_4 ((uint16_t)0x0010) /* Pin 4 selected */ #define GPIO_PIN_5 ((uint16_t)0x0020) /* Pin 5 selected */ #define GPIO_PIN_6 ((uint16_t)0x0040) /* Pin 6 selected */ #define GPIO_PIN_7 ((uint16_t)0x0080) /* Pin 7 selected */ #define GPIO_PIN_8 ((uint16_t)0x0100) /* Pin 8 selected */ #define GPIO_PIN_9 ((uint16_t)0x0200) /* Pin 9 selected */ #define GPIO_PIN_10 ((uint16_t)0x0400) /* Pin 10 selected */ #define GPIO_PIN_11 ((uint16_t)0x0800) /* Pin 11 selected */ #define GPIO_PIN_12 ((uint16_t)0x1000) /* Pin 12 selected */ #define GPIO_PIN_13 ((uint16_t)0x2000) /* Pin 13 selected */ #define GPIO_PIN_14 ((uint16_t)0x4000) /* Pin 14 selected */ #define GPIO_PIN_15 ((uint16_t)0x8000) /* Pin 15 selected */ #define GPIO_PIN_All ((uint16_t)0xFFFF) /* All pins selected */
参数GPIO_PinState PinState可以是: /** * @brief GPIO Bit SET and Bit RESET enumeration */ typedef enum { GPIO_PIN_RESET = 0U, GPIO_PIN_SET } GPIO_PinState;
输出低电平:
/*Configure GPIO pin Output Level */ HAL_GPIO_WritePin(Y_LED_GPIO_Port, Y_LED_Pin, GPIO_PIN_RESET); /*Configure GPIO pin Output Level */ HAL_GPIO_WritePin(CP_LED_GPIO_Port, CP_LED_Pin, GPIO_PIN_RESET);
输出高电平:
/*Configure GPIO pin Output Level */ HAL_GPIO_WritePin(Y_LED_GPIO_Port, Y_LED_Pin, GPIO_PIN_SET); /*Configure GPIO pin Output Level */ HAL_GPIO_WritePin(CP_LED_GPIO_Port, CP_LED_Pin, GPIO_PIN_SET);
翻转电平:
HAL_GPIO_TogglePin(Y_LED_GPIO_Port,Y_LED_Pin); HAL_GPIO_TogglePin(CP_LED_GPIO_Port,CP_LED_Pin);
关于GPIO输出不外乎这几个API,掌握如何使用就可以了
rt-thread任务介绍与创建
主要摘取一些比较关键的信息,也可参见RT-Thread官网介绍
线程管理
嵌入式系统执行这样的任务,系统通过传感器采集数据,并通过显示屏将数据显示出来,在多线程实时系统中,可以将这个任务分解成两个子任务,如下图所示,一个子任务不间断地读取传感器数据,并将数据写到共享内存中,另外一个子任务周期性的从共享内存中读取数据,并将传感器数据输出到显示屏上。
在 RT-Thread 中,与上述子任务对应的程序实体就是线程,线程是实现任务的载体,它是 RT-Thread 中最基本的调度单位,它描述了一个任务执行的运行环境,也描述了这个任务所处的优先等级,重要的任务可设置相对较高的优先级,非重要的任务可以设置较低的优先级,不同的任务还可以设置相同的优先级,轮流运行。
线程状态
线程运行的过程中,同一时间内只允许一个线程在处理器中运行,从运行的过程上划分,线程有多种不同的运行状态,如初始状态、挂起状态、就绪状态等。在 RT-Thread 中,线程包含五种状态,操作系统会自动根据它运行的情况来动态调整它的状态。RT-Thread 中线程的五种状态,如下表所示:
状态 | 描述 |
---|---|
初始状态 | 当线程刚开始创建还没开始运行时就处于初始状态;在初始状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_INIT |
就绪状态 | 在就绪状态下,线程按照优先级排队,等待被执行;一旦当前线程运行完毕让出处理器,操作系统会马上寻找最高优先级的就绪态线程运行。此状态在 RT-Thread 中的宏定义为 RT_THREAD_READY |
运行状态 | 线程当前正在运行。在单核系统中,只有 rt_thread_self() 函数返回的线程处于运行状态;在多核系统中,可能就不止这一个线程处于运行状态。此状态在 RT-Thread 中的宏定义为 RT_THREAD_RUNNING |
挂起状态 | 也称阻塞态。它可能因为资源不可用而挂起等待,或线程主动延时一段时间而挂起。在挂起状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_SUSPEND |
关闭状态 | 当线程运行结束时将处于关闭状态。关闭状态的线程不参与线程的调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_CLOSE |
线程状态切换
RT-Thread 提供一系列的操作系统调用接口,使得线程的状态在这五个状态之间来回切换。几种状态间的转换关系如下图所示:
线程优先级
RT-Thread 线程的优先级是表示线程被调度的优先程度。每个线程都具有优先级,线程越重要,赋予的优先级就应越高,线程被调度的可能才会越大。
RT-Thread 最大支持 256 个线程优先级 (0~255),数值越小的优先级越高,0 为最高优先级。在一些资源比较紧张的系统中,可以根据实际情况选择只支持 8 个或 32 个优先级的系统配置;对于 ARM Cortex-M 系列,普遍采用 32 个优先级。最低优先级默认分配给空闲线程使用,用户一般不使用。在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。
时间片
每个线程都有时间片这个参数,但时间片仅对优先级相同的就绪态线程有效。系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行调度时,时间片起到约束线程单次运行时长的作用,其单位是一个系统节拍(OS Tick),详见《时钟管理》章节。假设有 2 个优先级相同的就绪态线程 A 与 B,A 线程的时间片设置为 10,B 线程的时间片设置为 5,那么当系统中不存在比 A 优先级高的就绪态线程时,系统会在 A、B 线程间来回切换执行,并且每次对 A 线程执行 10 个节拍的时长,对 B 线程执行 5 个节拍的时长,如下图。
线程通过调用函数 rt_thread_create/init() 进入到初始状态(RT_THREAD_INIT);
初始状态的线程通过调用函数 rt_thread_startup() 进入到就绪状态(RT_THREAD_READY);
就绪状态的线程被调度器调度后进入运行状态(RT_THREAD_RUNNING);当处于运行状态的线程调用 rt_thread_delay(),rt_sem_take(),rt_mutex_take(),rt_mb_recv() 等函数或者获取不到资源时,将进入到挂起状态(RT_THREAD_SUSPEND);
处于挂起状态的线程,如果等待超时依然未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态。挂起状态的线程,如果调用 rt_thread_delete/detach() 函数,将更改为关闭状态(RT_THREAD_CLOSE);
而运行状态的线程,如果运行结束,就会在线程的最后部分执行 rt_thread_exit() 函数,将状态更改为关闭状态。
线程相关的API
创建和删除线程
一个线程要成为可执行的对象,就必须由操作系统的内核来为它创建一个线程。可以通过如下的接口创建一个动态线程:
rt_thread_t rt_thread_create(const char* name, void (*entry)(void* parameter), void* parameter, rt_uint32_t stack_size, rt_uint8_t priority, rt_uint32_t tick);
调用这个函数时,系统会从动态堆内存中分配一个线程句柄以及按照参数中指定的栈大小从动态堆内存中分配相应的空间。分配出来的栈空间是按照 rtconfig.h 中配置的 RT_ALIGN_SIZE 方式对齐。线程创建 rt_thread_create() 的参数和返回值见下图:
对于一些使用 rt_thread_create() 创建出来的线程,当不需要使用,或者运行出错时,我们可以使用下面的函数接口来从系统中把线程完全删除掉:
rt_err_t rt_thread_delete(rt_thread_t thread);
初始化和脱离线程
线程的初始化可以使用下面的函数接口完成,来初始化静态线程对象:
rt_err_t rt_thread_init(struct rt_thread* thread, const char* name, void (*entry)(void* parameter), void* parameter, void* stack_start, rt_uint32_t stack_size, rt_uint8_t priority, rt_uint32_t tick);
静态线程的线程句柄(或者说线程控制块指针)、线程栈由用户提供。静态线程是指线程控制块、线程运行栈一般都设置为全局变量,在编译时就被确定、被分配处理,内核不负责动态分配内存空间。需要注意的是,用户提供的栈首地址需做系统对齐(例如 ARM 上需要做 4 字节对齐)。线程初始化接口 rt_thread_init() 的参数和返回值见下表:
对于用 rt_thread_init() 初始化的线程,使用 rt_thread_detach() 将使线程对象在线程队列和内核对象管理器中被脱离。线程脱离函数如下:
rt_err_t rt_thread_detach (rt_thread_t thread);
启动线程
创建(初始化)的线程状态处于初始状态,并未进入就绪线程的调度队列,我们可以在线程初始化 / 创建成功后调用下面的函数接口让该线程进入就绪态:
rt_err_t rt_thread_startup(rt_thread_t thread);
...还有其他一些线程API,就不再一一赘述了,可以在RT-Thread官网查看
创建任务
上面对线程的介绍,罗里吧嗦的说了一大堆,接下来一起实战来看看,如何创建并运行任务
创建任务实现多任务点灯
根据创建任务的API
rt_thread_t rt_thread_create(const char* name, void (*entry)(void* parameter), void* parameter, rt_uint32_t stack_size, rt_uint8_t priority, rt_uint32_t tick);
来创建我们的2个任务:
/** ****************************************************************************** * @file rt_user_task.c * @brief 用户任务文件 * ****************************************************************************** * @attention * * Copyright (c) 2022 公众号:小飞哥玩嵌入式. * All rights reserved. * Author:小飞哥 * *****************************************************************************/ /* Includes ------------------------------------------------------------------*/ #include "main.h" #include#include "rt_user_task.h" /* Private typedef -----------------------------------------------------------*/ /* Private define ------------------------------------------------------------*/ #define THREAD1_PRIORITY 27 //线程 #define THREAD_STACK_SIZE 512 //线程栈深度 #define THREAD_TIMESLICE 5 //线程的时间片 #define THREAD2_PRIORITY 26 //线程 /* Private macro -------------------------------------------------------------*/ /* Private variables ---------------------------------------------------------*/ /* Private function prototypes -----------------------------------------------*/ /* Private user code ---------------------------------------------------------*/ /** * @function rt_ledflash_entry * @author:小飞哥玩嵌入式-小飞哥 * @TODO: LED控制线程 * @param: * @return: NULL */ static void rt_led1_flash_entry(void *parameter) { for (;;) { HAL_GPIO_TogglePin(Y_LED_GPIO_Port, Y_LED_Pin); rt_kprintf("LED1 Task is Running! "); rt_thread_mdelay(500); } } /** * @function rt_led2flash_entry * @author:小飞哥玩嵌入式-小飞哥 * @TODO: LED控制线程 * @param: * @return: NULL */ static void rt_led2_flash_entry(void *parameter) { for (;;) { HAL_GPIO_TogglePin(CP_LED_GPIO_Port, CP_LED_Pin); rt_kprintf("LED2 Task is Running! "); rt_thread_mdelay(500); } } /** * @function rt_user_thread_entry * @author:小飞哥玩嵌入式-小飞哥 * @TODO: 创建线程 * @param: * @return: NULL */ int rt_user_thread_entry(void) { static rt_thread_t result = RT_NULL; /*创建一个线程,名称是rt_ledflash,入口是rt_ledflash_entry*/ result = rt_thread_create("rt_led1flash", rt_led1_flash_entry, NULL, THREAD_STACK_SIZE, THREAD1_PRIORITY, THREAD_TIMESLICE); if (result != RT_NULL) //线程创建成功 { rt_thread_startup(result); } else { rt_kprintf(" rt_led1flash thread create failed "); } /*创建一个线程,名称是rt_ledflash,入口是rt_ledflash_entry*/ result = rt_thread_create("rt_led2flash", rt_led2_flash_entry, NULL, THREAD_STACK_SIZE, THREAD2_PRIORITY, THREAD_TIMESLICE); if (result != RT_NULL) //线程创建成功 { rt_thread_startup(result); } else { rt_kprintf(" rt_led2flash thread create failed "); } return 0; }
上面创建了两个任务,rt-thread任务的数值越小优先级是越高的,我们设置任务1的优先级为27,任务2的优先级为26,按照优先级设置规则,任务2的优先级是比较高的,接下来我们通过打印的方式(LED就不展示啦)来看看是不是按照我们的优先级去执行,可以看到,任务2的优先级是比较任务1高的
然后我们测试下同一个优先级情况下会发生什么,不出意外的话,应该是先执行先创建的任务,下图也可以看到,确实如此
至此,本节教程就完了,讲的还是比较粗略的,LED就不展示啦,希望对小伙们有所帮助哈!
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !