目录

简介

 硬件、外设初始化

任务创建、调度 

lvgl_task 

Sensor_Data_Update_task 

WDG_task 

总结 


简介

        本篇我们将除了LVGL之外的全部软件流程思路讲解给大家,包括部分STM32外设,包括窗口看门狗、dwt延时等等也给大家进行讲解,我们重点会讲解FreeRTOS部分,看看FreeRTOS是如何进行调度我们各个任务来实现我们项目的功能。

 硬件、外设初始化

static void Hard_Init(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
    SystemInit();
    dwt_delay_init();
    BEEP_PWM_Init();
    Blue_Hc05_Init();
    EXTI_Key_Config();
    MPU_Init();
    mpu_dmp_init();
    RTC_Config();
    MAX30102_Init();
    XGZP_Init();
    Lcd_Init();
    Lcd_Clear(0xffff);
    lv_init();          
    lv_port_disp_init();        
    lv_port_indev_init();
    IWDG_Config(IWDG_Prescaler_64, 3125); //5s
}

int main(void)
{
    Hard_Init();
    FreeRTOS_Task_Init();
    
    while (1)
    {}
}

进入main函数之后,我们首先要进行硬件以及外设的初始化,这里的硬件初始化部分,我们在前面有针对每个不同的传感器和模块进行讲解,这里就不去一个个的进行讲解了,这里我放下跳转链接,点击即可跳转查看每个模块和传感器的软件讲解。

LVGL+FreeRTOS实战项目:智能健康助手(无源蜂鸣器​​篇)

LVGL+FreeRTOS实战项目:智能健康助手(蓝牙模块篇)

LVGL+FreeRTOS实战项目:智能健康助手(按键篇)

LVGL+FreeRTOS实战项目:智能健康助手(mpu6050篇)

LVGL+FreeRTOS实战项目:智能健康助手(xgzp6847a篇)

LVGL+FreeRTOS实战项目:智能健康助手(Max30102篇)

LVGL+FreeRTOS实战项目:智能健康助手(lcd篇)LVGL+FreeRTOS实战项目:智能健康助手(dht11篇)

 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);

这个是设置中断优先级的,NVIC_PriorityGroup_4 表示将 4 位用于抢占优先级,0 位用于响应优先级。

SystemInit();

SystemInit() 函数执行了 STM32 启动时的一些初始化步骤,包括配置浮点单元权限、重置时钟控制器、初始化外部存储器、设置系统时钟和配置中断向量表位置。 

dwt_delay_init(); 

我们使用dwt来进行延时,在Cortex-M里面有一个外设叫DWT(Data Watchpoint and Trace),是用于系统调试及跟踪,它有一个32位的寄存器叫CYCCNT, 它是一个向上的计数器,记录的是内核时钟运行的个数,内核时钟跳动一次,该计数器就加1,精度非常高。 

要实现延时的功能,总共涉及到三个寄存器:DEMCR、DWT_CTRL、DWT_CYCCNT,分别用于开启DWT功能、开启CYCCNT及获得系统时钟计数值。

DEMCR

想要使能DWT外设,需要由另外的内核调试寄存器DEMCR的位24控制,写1使能。DEMCR的地址是:0xE000 EDFC。 

DWT_CYCCNT

使能DWT_CYCCNT寄存器之前,先清0。其基地址是0xE0001004,复位默认值是0,可读写类型。所以往0xE0001004这个地址写就将DWT_CYCCNT清0了。

CYCCNTENA

它是DWT控制寄存器的第一位,写1使能,则启用CYCCNT计数器,否则CYCCNT计数器将不会工作。

综上所述,想要使用DWT的CYCCNT需要以下3个步骤:

1.先使能DWT外设,这个由另外内核调试寄存器DEMCR的位24控制,写1使能。

2.使能CYCCNT寄存器之前,先清0。

3.使能CYCCNT寄存器,这个由DWT的CYCCNTENA 控制,也就是DWT控制寄存器的位0控制,写1使能。

正好对应我们的代码:

之后我们的延时都是基于dwt外设的了,可谓是非常的精准。

void RTC_Config(void) 

STM32的RTC外设(Real Time Clock),实质是一个掉电后还继续运行的定时器。从定时器的角度来说,相对于通用定时器TIM外设,它十分简单, 只有很纯粹的计时和触发中断的功能;但从掉电还继续运行的角度来说,它却是STM32中唯一一个具有如此强大功能的外设。 所以RTC外设的复杂之处并不在于它的定时功能,而在于它掉电还继续运行的特性。

框图中浅灰色的部分都是属于备份域的,在VDD掉电时可在VBAT的驱动下继续运行。 这部分仅包括RTC的分频器,计数器,和闹钟控制器。若VDD电源有效,RTC可以触发RTC_Second(秒中断)、 RTC_Overflow(溢出事件)和RTC_Alarm(闹钟中断)。从结构图可以分析到,其中的定时器溢出事件无法被配置为中断。 若STM32原本处于待机状态,可由闹钟事件或WKUP事件(外部唤醒事件,属于EXTI模块,不属于RTC)使它退出待机模式。 闹钟事件是在计数器RTC_CNT的值等于闹钟寄存器RTC_ALR的值时触发的。

在备份域中所有寄存器都是16位的, RTC控制相关的寄存器也不例外。它的计数器RTC_CNT的32位由RTC_CNTL和RTC_CNTH两个寄存器组成,分别保存定时计数值的低16位和高16位。 在配置RTC模块的时钟时,通常把输入的32768Hz的RTCCLK进行32768分频得到实际驱动计数器的时钟 TR_CLK =RTCCLK/32768= 1 Hz, 计时周期为1秒,计时器在TR_CLK的驱动下计数,即每秒计数器RTC_CNT的值加1。

由于备份域的存在,使得RTC核具有了完全独立于APB1接口的特性, 也因此对RTC寄存器的访问要遵守一定的规则。

系统复位后,默认禁止访问后备寄存器和RTC,防止对后备区域(BKP)的意外写操作。 执行以下操作使能对后备寄存器和RTC的访问:

(1) 设置RCC_APB1ENR寄存器的PWREN和BKPEN位来使能电源和后备接口时钟。

(2) 设置PWR_CR寄存器的DBP位使能对后备寄存器和RTC的访问。

设置后备寄存器为可访问后,在第一次通过APB1接口访问RTC时,因为时钟频率的差异,所以必须等待APB1与RTC外设同步, 确保被读取出来的RTC寄存器值是正确的。若在同步之后,一直没有关闭APB1的RTC外设接口,就不需要再次同步了。

如果内核要对RTC寄存器进行任何的写操作,在内核发出写指令后,RTC模块在3个RTCCLK时钟之后,才开始正式的写RTC寄存器操作。 由于RTCCLK的频率比内核主频低得多,所以每次操作后必须要检查RTC关闭操作标志位RTOFF,当这个标志被置1时,写操作才正式完成。

当然,以上的操作都具有库函数,读者不必具体地查阅寄存器。

void RTC_Config(void)
{
    /* RTC配置:选择时钟源,设置RTC_CLK的分频系数 */
  RTC_CLK_Config();

  if (RTC_ReadBackupRegister(RTC_BKP_DRX) != RTC_BKP_DATA)
  {
    /* 设置时间和日期 */
    RTC_TimeAndDate_Set();
  }
  else
  {
    /* 检查是否电源复位 */
    if (RCC_GetFlagStatus(RCC_FLAG_PORRST) != RESET)
    {
      //printf("\r\n 发生电源复位....\r\n");
    }
    /* 检查是否外部复位 */
    else if (RCC_GetFlagStatus(RCC_FLAG_PINRST) != RESET)
    {
      //printf("\r\n 发生外部复位....\r\n");
    }

    //printf("\r\n 不需要重新配置RTC....\r\n");
    
    /* 使能 PWR 时钟 */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
    /* PWR_CR:DBF置1,使能RTC、RTC备份寄存器和备份SRAM的访问 */
    PWR_BackupAccessCmd(ENABLE);
    /* 等待 RTC APB 寄存器同步 */
    RTC_WaitForSynchro();   
  } 

 void RTC_TimeAndDate_Set(void)
{
    RTC_TimeTypeDef RTC_TimeStructure;
    RTC_DateTypeDef RTC_DateStructure;
    
    // 初始化时间
    RTC_TimeStructure.RTC_H12 = RTC_H12_AMorPM;
    RTC_TimeStructure.RTC_Hours = HOURS;        
    RTC_TimeStructure.RTC_Minutes = MINUTES;      
    RTC_TimeStructure.RTC_Seconds = SECONDS;      
    RTC_SetTime(RTC_Format_BINorBCD, &RTC_TimeStructure);
    RTC_WriteBackupRegister(RTC_BKP_DRX, RTC_BKP_DATA);
    
  // 初始化日期    
    RTC_DateStructure.RTC_WeekDay = WEEKDAY;       
    RTC_DateStructure.RTC_Date = DATE;         
    RTC_DateStructure.RTC_Month = MONTH;         
    RTC_DateStructure.RTC_Year = YEAR;        
    RTC_SetDate(RTC_Format_BINorBCD, &RTC_DateStructure);
    RTC_WriteBackupRegister(RTC_BKP_DRX, RTC_BKP_DATA);
}

void IWDG_Config(uint8_t prv ,uint16_t rlv) 

STM32有两个看门狗,一个是独立看门狗,一个是窗口看门狗。我们知道独立看门狗的工作原理就是一个递减计数器不断的往下递减计数, 当减到0之前如果没有喂狗的话,产生复位。窗口看门狗跟独立看门狗一样,也是一个递减计数器不断的往下递减计数, 当减到一个固定值0X40时还不喂狗的话,产生复位,这个值叫窗口的下限,是固定的值,不能改变。这个是跟独立看门狗类似的地方, 不同的地方是窗口看门狗的计数器的值在减到某一个数之前喂狗的话也会产生复位,这个值叫窗口的上限,上限值由用户独立设置。 窗口看门狗计数器的值必须在上窗口和下窗口之间才可以喂狗,这就是窗口看门狗中窗口两个字的含义

RLR是重装载寄存器,用来设置独立看门狗的计数器的值。TR是窗口看门狗的计数器的值,由用户独立设置,WR是窗口看门狗的上窗口值,由用户独立设置。 

窗口看门狗时钟

窗口看门狗时钟来自PCLK1,PCLK1最大是36M,由RCC时钟控制器开启。

计数器时钟

计数器时钟由CK计时器时钟经过预分频器分频得到,分频系数由配置寄存器CFR的位8:7  WDGTB[1:0]配置,可以是[0,1,2,3], 其中CK计时器时钟=PCLK1/4096,除以4096是手册规定的,没有为什么。所以计数器的时钟CNT_CK=PCLK1/4096/(2^WDGTB), 这就可以算出计数器减一个数的时间T= 1/CNT_CK = Tpclk1 * 4096 * (2^WDGTB)。

计数器

窗口看门狗的计数器是一个递减计数器,共有7位,其值存在控制寄存器CR的位6:0,即T[6:0],当7个位全部为1时是0X7F, 这个是最大值,当递减到T6位变成0时,即从0X40变为0X3F时候,会产生看门狗复位。这个值0X40是看门狗能够递减到的最小值, 所以计数器的值只能是:0X40~0X7F之间,实际上真正用来计数的是T[5:0]。当递减计数器递减到0X40的时候,还不会马上产生复位, 如果使能了提前唤醒中断:CFR位9EWI置1,则产生提前唤醒中断,如果真进入了这个中断的话,就说明程序肯定是出问题了, 那么在中断服务程序里面我们就需要做最重要的工作,比如保存重要数据,或者报警等,这个中断我们也叫它死前中断。

窗口值

我们知道窗口看门狗必须在计数器的值在一个范围内才可以喂狗,其中下窗口的值是固定的0X40,上窗口的值可以改变, 具体的由配置寄存器CFR的位6:0 W[6:0]设置。其值必须大于0X40,如果小于或者等于0X40就是失去了窗口的价值,而且也不能大于计数器的值, 所以必须得小于0X7F。那窗口值具体要设置成多大?这个得根据我们需要监控的程序的运行时间来决定。如果我们要监控的程序段A运行的时间为Ta, 当执行完这段程序之后就要进行喂狗,如果在窗口时间内没有喂狗的话,那程序就肯定是出问题了。一般计数器的值TR设置成最大0X7F,窗口值为WR, 计数器减一个数的时间为T,那么时间:(TR-WR)*T应该稍微小于Ta即可,这样就能做到刚执行完程序段A之后喂狗,起到监控的作用,这样也就可以算出WR的值是多少。

计算看门狗超时时间 

 这个图来自数据手册,从图我们知道看门狗超时时间:Twwdg = Tpclk1 x 4096 x 2^wdgtb x (T[5:0] + 1) ms, 当PCLK1 = 36MHZ时,WDGTB取不同的值时有最小和最大的超时时间,那这个最小和最大的超时时间该怎么理解,又是怎么算出来的? 讲起来有点绕,这里我稍微讲解下WDGTB=0时是怎么算的。递减计数器有7位T[6:0] ,当位6变为0的时候就会产生复位,实际上有效的计数位是T[5:0], 而且T6必须先设置为1。如果T[5:0]=0时,递减计数器再减一次,就产生复位了, 那这减一的时间就等于计数器的周期=1/CNT_CK = Tpclk1 * 4096 * (2^WDGTB) = 1/36 * 4096 *2^0 =113.7us, 这个就是最短的超时时间。如果T[5:0]全部装满为1,即63,当他减到0X40变成0X3F时,所需的时间就是最大的超时时间=113.7*2^5=113.7*64=7.2768ms。 同理,当WDGTB等于1/2/3时,代入公式即可。

怎么用WWDG 

WWDG一般被用来监测,由外部干扰或不可预见的逻辑条件造成的应用程序背离正常的运行序列而产生的软件故障。我们这里的看门狗需要5s就去喂一次,如果在规定的时间窗口内还没有喂狗,那就说明我们监控的程序出故障了,跑飞了,那么就会产生系统复位,让程序重新运行。

我们的bsp_iwdg.c里面写的非常的清晰,我们只需要根据我们喂狗时间,用公式算出prv和rlv,然后调用void IWDG_Config(uint8_t prv ,uint16_t rlv);即可。之后我们喂狗就直接调用喂狗函数void IWDG_Feed(void);即可。
 

任务创建、调度 

void FreeRTOS_Task_Init(void);

void FreeRTOS_Task_Init(void)
{
    BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
    /* 创建AppTaskCreate任务 */
    xReturn = xTaskCreate((TaskFunction_t )AppTaskCreate,  /* 任务入口函数 */
                        (const char*    )"AppTaskCreate",/* 任务名字 */
                        (uint16_t       )1024,  /* 任务栈大小 */
                        (void*          )NULL,/* 任务入口函数参数 */
                        (UBaseType_t    )1, /* 任务的优先级 */
                        (TaskHandle_t*  )&AppTaskCreate_Handle);/* 任务控制块指针 */ 
    /* 启动任务调度 */           
    if(pdPASS == xReturn)
    vTaskStartScheduler();   /* 启动任务,开启调度 */

}

我们创建了一个 AppTaskCreate任务,我们进去这个任务看一下:

/***********************************************************************
  * @ 函数名  : AppTaskCreate
  * @ 功能说明: 为了方便管理,所有的任务创建函数都放在这个函数里面
  * @ 参数    : 无  
  * @ 返回值  : 无
  **********************************************************************/
static void AppTaskCreate(void)
{
    BaseType_t xReturn = pdPASS;/* 定义一个创建信息返回值,默认为pdPASS */
    char buf[64];
    
    taskENTER_CRITICAL();           //进入临界区

    Senser_Queue = xQueueCreate((UBaseType_t ) 20,/* 消息队列的长度 */ 
                                (UBaseType_t ) sizeof(SensorData_t));/* 消息的大小 */ 

    WDG_Event_Handle = xEventGroupCreate();
    
    xTaskCreate((TaskFunction_t )lvgl_task, /* 任务入口函数 */
                    (const char*    )"lvgl_task",/* 任务名字 */
                    (uint16_t       )512,   /* 任务栈大小 */
                    (void*          )NULL,    /* 任务入口函数参数 */
                    (UBaseType_t    )4,        /* 任务的优先级 */
                    (TaskHandle_t*  )&LVGL_Task_Handle);/* 任务控制块指针 */
                    
    xTaskCreate((TaskFunction_t )WDG_task, /* 任务入口函数 */
                    (const char*    )"WDG_task",/* 任务名字 */
                    (uint16_t       )128,   /* 任务栈大小 */
                    (void*          )NULL,    /* 任务入口函数参数 */
                    (UBaseType_t    )3,        /* 任务的优先级 */
                    (TaskHandle_t*  )&WDG_task_Handle);/* 任务控制块指针 */
                    
    xTaskCreate((TaskFunction_t )Sensor_Data_Update_task, /* 任务入口函数 */
                    (const char*    )"Sensor_Data_Update_Task",/* 任务名字 */
                    (uint16_t       )256,   /* 任务栈大小 */
                    (void*          )NULL,    /* 任务入口函数参数 */
                    (UBaseType_t    )3,        /* 任务的优先级 */
                    (TaskHandle_t*  )&Sensor_Data_Update_Task_Handle);/* 任务控制块指针 */
                                  
    vTaskDelete(AppTaskCreate_Handle); //删除AppTaskCreate任务

    taskEXIT_CRITICAL();            //退出临界区
}

这是个常见套路了, 为了方便管理,所有的任务创建函数都放在这个函数里面,之后又自杀,释放掉内存,可谓非常伟大了。

Senser_Queue = xQueueCreate((UBaseType_t ) 20,/* 消息队列的长度 */ 
                                (UBaseType_t ) sizeof(SensorData_t));/* 消息的大小 */ 
WDG_Event_Handle = xEventGroupCreate();

这里创建了一个 Senser_Queue 队列,这个队列用于接收传感器数值,我们可以去看一下SensorData_t这个结构体,代码如下:

typedef struct {
    RTC_TimeTypeDef rtc_time;
    RTC_DateTypeDef rtc_date;
    unsigned int dht11_data[4];
    MPU_ANGLE mpu6050_data;
    char P_disbuff[5];
    uint8_t Hr_Data;
    uint8_t Spo2_Data;
    uint32_t Step_Data;
} SensorData_t;

可以看到又时间、日期、温湿度、mpu6050、压力、心率、血氧、步数的成员,不难猜出,我们每次通过获取传感器任务之后,都会把最后所有传感器的值写入这个队列当中,可见这个队列非常的重要。 

这里也创建了一个WDG_Event_Handle事件组,这个我们可以通过事件以触发我们喂狗任务进行喂狗,我们看一下函数调用关系:

lvgl_task以及Sensor_Data_Update_task这两个任务在执行完一次之后,都会设置一次事件,之后我们喂狗任务通过事件组等待事件,就可以执行喂狗操作。

lvgl_task 

我们 lvgl_task这个任务其实执行的就是LVGL的 lv_task_handler(); 我们之前学习LVGL的时候,我们对这个肯定不陌生,如果是没加操作系统的话,我们就会在while(1)里面不断的执行,现在我们FreeRTOS中,就在任务中进行执行。

互斥量保证安全,由于 FreeRTOS 是一个实时操作系统,多个任务可能会并发执行。在没有互斥量的保护下,多个任务可能会同时调用 LVGL 的函数,导致数据不一致或者图形显示错误。例如,多个任务可能会同时更新图形库的状态,这会引发并发访问的问题,虽然我们这里其实不使用也不会出现这个问题,但是我们还是养成规范,方便以后的开发和扩展。

我们需要给LVGL提供一个时基,如图所示:

在 FreeRTOS 中,vApplicationTickHook() 是一个 应用程序钩子函数(hook function),它允许用户在 FreeRTOS 的 定时器中断(Tick Interrupt)发生时执行自定义代码。vApplicationTickHook() 会在 FreeRTOS 的系统时钟“滴答”中断处理程序中被调用。系统时钟是 FreeRTOS 内部用于任务调度的时基,它通常每隔一定时间(例如 1 毫秒)中断一次,以便于 FreeRTOS 进行任务切换和管理,他在更新图形界面库的时钟中非常常用。
至此,我们LVGL的任务就讲解完了,关于LVGL控件部分以及界面切换我们就留到LVGL篇中进行讲解。

Sensor_Data_Update_task 

其实就是去获取RTC、温湿度、MPU6050、Max30102、XGZP6847A传感器值的一个任务,如下所示:

void Sensor_Data_Update_task(void const * argument)
{
    static uint8_t MAX30102_Init_Flag = 0;
    
    for(;;)
    {
        if(Page_Get_NowPage()->page_obj  == &ui_MainPage)
        {
            // 获取日历
            RTC_GetTime(RTC_Format_BIN, &sens_data.rtc_time);
            RTC_GetDate(RTC_Format_BIN, &sens_data.rtc_date);
            
            taskENTER_CRITICAL();            //退出临界区
            DHT11_REC_Data(sens_data.dht11_data);
            dmp_get_pedometer_step_count(&sens_data.Step_Data);
            mpu_dmp_get_data(&sens_data.mpu6050_data.pitch,&sens_data.mpu6050_data.roll,&sens_data.mpu6050_data.yaw);
            taskEXIT_CRITICAL();            //退出临界区
            
        }
        else if(Page_Get_NowPage()->page_obj  == &ui_HeartPage || Page_Get_NowPage()->page_obj  == &ui_Blood_Oxy_Page)
        {
            if(MAX30102_Init_Flag == 0)
            {
                MAX30102_Data_Init();
                MAX30102_Init_Flag = 1;
            }
            Get_MAX30102_Data(&sens_data.Hr_Data, &sens_data.Spo2_Data);
        }
        else if(Page_Get_NowPage()->page_obj   == &ui_Blood_Pressure_Page)
        {
            Get_XGZP_Data(sens_data.P_disbuff);
        }
        
        xEventGroupSetBits(WDG_Event_Handle, SENSOR_EVENT);
        
        xQueueSend(Senser_Queue, &sens_data, 0);     
        
        vTaskDelay(200);
    }
}

获取完之后,把传感器数值发送到 Senser_Queue 队列上,并且设置WDG_Event_Handle事件,实际我们最后是LVGL任务来对我们的 Senser_Queue 进行接收显示,之后我们LVGL篇会讲解。

这里Page_Get_NowPage()->page_obj来判断当前处在哪个界面,从而去获取哪个传感器的值,具体我们也留到LVGL篇在进行讲解。

WDG_task 

这个任务主要就是进行喂狗操作,如下所示:

每次我们成功执行完一次 lvgl_task 以及 Sensor_Data_Update_task 都会触发一个事件组事件,当我们检测到这个事件之后,任务得以继续执行,执行喂狗操作,如果出现异常,喂狗失败,那么就会进行复位。

总结 

关于项目的FreeRTOS篇就讲解到这里了,我们讲解了部分STM32外设以及介绍了各个任务,读者仔细观看肯定可以看懂,之后在对着源码进行看,就可以彻底理解了,接下来我会去介绍LVGL篇,主要讲解界面如何进行切换、LVGL界面如何生成、按键如何控制LVGL菜单切换。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐