1. 软件框架

手表OV-Watch的总体软件架构如下所示,具体代码详见仓库。

V2.4版本以后的手表的代码分为Bootloader和APP了,为的是方便用户戴在手上进行不用拆解的升级,BOOT区后面划分了一个Flag区,用于记录是否是完整的APP,这个位置是APP传输完成后才记录的,为的是保证程序完整性。

下面我将从自底向上来讲解手表的软件结构

1.1. MDK工程结构

MDK工程结构不是完全对应Software文件夹中的文件结构的。注意,本项目默认使用的MDK中编译器为compiler version 5

├─Application/MDK-ARM               # 用于存放.s文件
├─Application/User/Core             # 用于存放CubeMX生成的初始化文件
│  │  main.c
│  │  gpio.c
│  │  ...
│
├─Application/User/System           # 用于存放自定义的delay.c sys.h等
│  │  delay.c
│  │  ...
│
├─Application/User/Tasks            # 用于存放任务线程的函数
│  │  user_TaskInit.c
│  │  user_HardwareInitTask.c
│  │  user_RunModeTasks.c
│  │  ...
│
├─Application/User/MidFunc          # 用于存放管理函数
│  │  StrCalculate.c
│  │  HWDataAccess.c
│  │  PageManager.c
│
├─Application/User/GUI_APP          # 用于存放用户的ui app
│  │  ui.c
│  │  ...
│
├─Application/User/GUI_FONT_IMG     # 用于存放字体和图片
│  │  ...
│
├─Drivers/CMSIS
│  │  ...
│
├─Drivers/User/BSP                  # 用于存放板载设备驱动
│  │  ...
│
├─Middleware/FreeRTOS               # FreeRTOS的底层
│  │  ...
│
├─Middleware/LVGL/GUI               # LVGL的底层
│  │  ...
│
└─Middleware/LVGL/GUI_Port          # 用于存放LVGL驱动
    ├─lv_port_disp.c
    ├─lv_port_indev.c

1.2. CubeMX框架

工程是用CubeMX生成的MDK工程,这里默认使用的AC5编译,这里默认大家已经能够熟练使用CubeMX和HAL库了,HAL库淡化硬件层非常适合进行软件开发。

本次手表项目使用到的片上外设包括GPIO, IIC, SPI, USART, TIM, ADC, DMA, 具体的对PCB板上器件的驱动,例如LCD, EEPROM等,详见BSP。

[!TIP|style:flat|label:Tip 小贴士|iconVisibility:visible] 请下拉代码,自行看代码细节~

简述一下各个片上外设的用途:

  1. DMA这里主要是配合SPI,SPI通信不通过CPU而是通过DMA直接发送,如果使用多线程,那么视觉上来讲,刷屏应该就会快一些,因为CPU可以去执行其它任务;

  2. IIC主要用来跟Back各个传感器进行通信,传感器都挂在一个总线上的;

  3. TIM主要是提供时基,另外一个就是给LCD调节背光;

  4. ADC只接了一个电池的分压,进行电池电压采样,预估剩余电量;

  5. USART接了蓝牙,方便进行IAP和与手机和电脑的助手通信。

1.3. 板载驱动BSP

详见BSP文件夹中的代码,BSP很多很杂,不需要了解的同学可以先跳过,直接调用相关API函数即可。

[!Warning|style:flat|label:Warning 注意|iconVisibility:visible] 很多设备初始化函数是有低功耗设置的,不要轻易改!!

简述一下部分BSP:

  1. WDOG采用外置的原因是,想要做睡眠低功耗,那么使用MCU内部的看门狗关闭不了,只能一直唤醒喂狗,否则就要重启,那么这样就失去了睡眠的意义了;

  2. IIC使用的软件模拟的方式进行驱动,没有用硬件IIC,还是更推荐使用硬件IIC,IIC总线的定义如下,定义了GPIO的口和CLK使能函数,在各个设备中直接用iic_bus_t进行创建IIC即可,然后调用iic_hal.c中的API即可;

typedef struct
{

        GPIO_TypeDef * IIC_SDA_PORT;
        GPIO_TypeDef * IIC_SCL_PORT;
        uint16_t IIC_SDA_PIN;
        uint16_t IIC_SCL_PIN;
        void (*CLK_ENABLE)(void);

}iic_bus_t;
  1. key按键的驱动,GPIO设置有添加中断,这个是为了按键唤醒进入STOP模式的MCU;

  2. EM7028心率,驱动中只写了对寄存器的读取,具体的心率算法和血氧算法没有放在BSP中;

  3. DataSave数据保存,为了方便保存手表设置等数据,自定义了EEPROM中的存储帧,详见代码;

  4. IMU,dmp库中的init函数是改过的,还有MP6050.c,有比较多低功耗相关的,建议直接拿去用,不需要再改了。关于MPU6050记步的问题,难点主要在实现低功耗记步,它的内部是有一个寄存器存储步数的,应该MPU6050内部是有记步算法的,不需要用户在外部再自己设计记步算法了。

1.4. 硬件访问机制-HWDataAccess

1.4.1. LVGL仿真和MDK工程的互相移植

为什么加入HWDataAccess.c,而不直接调用BSP的API呢,主要是为了方便移植和管理。

上面图片所示这个OV_Watch/User文件夹中的Func文件夹和GUI_APP文件夹,全部复制到LVGL仿真文件夹中,如下所示,即完成了仿真的移植。

同理,你在LVGL仿真中,改完UI App后,想要移植回MDK工程,看下实物效果,也是直接将LVGL仿真中的user_test中的文件复制过去即可。

当然,MDK工程和LVGL仿真工程的移植过程需要改一个东西,就是HWDataAccess.h中的使能:


/***************************
 *  Hardware Define
 ***************************/
/**
 *  if not use, just set 0
 *
 *
 *  if just test ui, no hardware, just set HW_USE_HARDWARE 0
 *
 */

#define HW_USE_HARDWARE 1

#if HW_USE_HARDWARE
  #define HW_USE_RTC    1
  #define HW_USE_BLE    1
  #define HW_USE_BAT    1
  #define HW_USE_LCD    1
  #define HW_USE_IMU    1
  #define HW_USE_AHT21  1
  #define HW_USE_SPL06  1
  #define HW_USE_LSM303 1
  #define HW_USE_EM7028 1
#endif

如果是在仿真中,就把HW_USE_HARDWARE定义为0即可,MDK中自然就是定义为1。使用这个HWDataAccess就方便把硬件抽象出来了,具体的代码详见代码。

1.4.2. HWDataAccess具体使用方式

HWDataAccess.c中,使用结构体进行各个硬件管理,如下代码所示。各个typedef定义在HWDataAccess.h中可以看到。

/***************************
 *  External Variables
 ***************************/
HW_InterfaceTypeDef HWInterface = {
    .RealTimeClock = {
        .GetTimeDate = HW_RTC_Get_TimeDate,
        .SetDate = HW_RTC_Set_Date,
        .SetTime = HW_RTC_Set_Time,
        .CalculateWeekday = HW_weekday_calculate
    },
    .BLE = {
        .Enable = HW_BLE_Enable,
        .Disable = HW_BLE_Disable
    },
    .Power = {
        .power_remain = 0,
        .Init = HW_Power_Init,
        .Shutdown = HW_Power_Shutdown,
        .BatCalculate = HW_Power_BatCalculate
    },
    .LCD = {
        .SetLight = HW_LCD_Set_Light
    },

    .IMU = {
        .ConnectionError = 1,
        .Steps = 0,
        .wrist_is_enabled = 0,
        .wrist_state = WRIST_UP,
        .Init = HW_MPU_Init,
        .WristEnable = HW_MPU_Wrist_Enable,
        .WristDisable = HW_MPU_Wrist_Disable,
        .GetSteps = HW_MPU_Get_Steps,
        .SetSteps = HW_MPU_Set_Steps
    },
    .AHT21 = {
        .ConnectionError = 1,
        .humidity = 67,
        .temperature = 26,
        .Init = HW_AHT21_Init,
        .GetHumiTemp = HW_AHT21_Get_Humi_Temp
    },
    .Barometer = {
        .ConnectionError = 1,
        .altitude = 19,
        .Init = HW_Barometer_Init,
    },
    .Ecompass = {
        .ConnectionError = 1,
        .direction = 45,
        .Init = HW_Ecompass_Init,
        .Sleep = HW_Ecompass_Sleep
    },
    .HR_meter = {
        .ConnectionError = 1,
        .HrRate = 0,
        .SPO2 = 99,
        .Init = HW_HRmeter_Init,
        .Sleep = HW_HRmeter_Sleep
    }
};

如何在UI层使用HWDataAccess呢,例如在HomePage中的调节LCD亮度的回调函数中,这么使用,可以看到直接调用HWInterface.LCD.SetLight(ui_LightSliderValue);即可。

void ui_event_LightSlider(lv_event_t * e)
{
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_VALUE_CHANGED)
    {
        ui_LightSliderValue = lv_slider_get_value(ui_LightSlider);
        HWInterface.LCD.SetLight(ui_LightSliderValue);
    }
}

那么他是如何在有硬件的MDK工程中也能用,LVGL无硬件的仿真也能用,我们看到HWInterface.LCD.SetLight对应的函数是什么:

HW_InterfaceTypeDef HWInterface = {
    // 省略前面
    .LCD = {
            .SetLight = HW_LCD_Set_Light
        },
    // 省略后面
}

首先看到HWInterface.LCD.SetLight定义的是函数HW_LCD_Set_Light,而这个函数的内容如下,即当HW_USE_LCD使能时,运行这个函数,能够正常调光,当LVGL仿真中不使能硬件HW_USE_HARDWARE时, HW_USE_LCD也不使能,则此函数执行空,工程也不会报错。

void HW_LCD_Set_Light(uint8_t dc)
{
    #if HW_USE_LCD
        LCD_Set_Light(dc);
    #endif
}

1.5. LVGL页面管理-PageManager

[!TIP|style:flat|label:Tip 小贴士|iconVisibility:visible] 这个PageManager可以直接拿过去用于LVGL相关项目,方便进行页面管理~

1.5.1. PageManager框架

OV-Watch手表项目的LVGL页面有很多,在GUI_App文件夹中,Screen文件夹中存放着所有的page。由于screen很多,所以有必要进行页面管理。这里开一个栈进行页面管理。

首先看到PageManager.h, Page_t结构体是用于描述一个LVGL页面的,里面的对象有初始化函数init,反初始化函数deinit以及一个用于存放lvgl对象的地址的lv_obj_t **page_obj

PageStack_t结构体描述一个界面栈,用于存放Page_t页面结构体,top表示栈顶。

// 页面栈深度
#define MAX_DEPTH 6

// 页面结构体
typedef struct {
    void (*init)(void);
    void (*deinit)(void);
    lv_obj_t **page_obj;
} Page_t;


// 页面堆栈结构体
typedef struct {
    Page_t* pages[MAX_DEPTH];
    uint8_t top;
} PageStack_t;

extern PageStack_t PageStack;

再看到PageManager.c,栈的初始化还有push和pop操作就不再赘述了,在pop函数中,除了将top减1,还调用了页面deinit函数,负责反初始化当前页面

stack->pages[--stack->top]->deinit();

Page_Back(), Page_Back_Bottom(), Page_Load()就是主要在代码中调用的函数了,分别的作用是Back到上一个界面,Back到最底部的Home界面,以及load新的界面。

1.5.2. 如何在ui app中使用PageManager

这里以代码比较少的ui_ChargPage.c为例,这个page也是我很久之前使用square line生成的,一般会生成ui_ChargPage_screen_init函数。

首先我们需要注册一个Page结构体存储当前的页面,填充好初始化init,反初始化函数deinit以及LVGL页面对象&ui_ChargPage,然后我的deinit是用于删除定时器timer的,这里的timer主要用于刷当前页面的数据,所以不在当前页面时需要删除掉。

// 省略前面...

///////////////////// Page Manager //////////////////
Page_t Page_Charg = {ui_ChargPage_screen_init, ui_ChargPage_screen_deinit, &ui_ChargPage};

/////////////////////// Timer //////////////////////
// need to be destroyed when the page is destroyed
static void ChargPage_timer_cb(lv_timer_t * timer)
{
    if(Page_Get_NowPage()->page_obj == &ui_ChargPage)
    {
        // 刷新数据等操作
    }
}

///////////////////// SCREEN init ////////////////////
void ui_ChargPage_screen_init(void)
{
    // 省略中间...
    // private timer
    ui_ChargPageTimer = lv_timer_create(ChargPage_timer_cb, 2000,  NULL);
}

/////////////////// SCREEN deinit ////////////////////
void ui_ChargPage_screen_deinit(void)
{
  lv_timer_del(ui_ChargPageTimer);
}

// 省略后面...

1.6. 多线程任务

这里默认大家已经会用FreeRTOS了,此项目都用的CMSIS_OS_V2的API。Tasks文件以及其作用如下所示。

├─Application/User/Tasks            # 用于存放任务线程的函数
│  ├─user_TaskInit.c                # 初始化任务
│  ├─user_HardwareInitTask.c        # 硬件初始化任务
│  ├─user_RunModeTasks.c            # 运行模式任务
│  ├─user_KeyTask.c                 # 按键任务
│  ├─user_DataSaveTask.c            # 数据保存任务
│  ├─user_MessageSendTask.c         # 消息发送任务
│  ├─user_ChargeCheckTask.c         # 充电检查任务
│  ├─user_SensUpdateTask.c          # 传感器更新任务
│  ├─user_ScrRenewTask.c            # 屏幕刷新任务
  1. 任务初始化TaskInit.c,注册各个任务,分配空间,注册一些信号量,任务的汇总可以看这个文件。同时也创建了一个软件定时器,用于记录空闲时间,即用户没有操作过长就会发出idle信号,idle过长,就会发出STOP信号,进入睡眠。LVGL的时钟提供也放在这个文件夹,为LvHandlerTask。看门狗得喂狗Task也放在这个文件中。

  2. 硬件初始化user_HardwareInitTask.c,其实这里应该叫BootTask,不仅仅有硬件的初始化,最后还有LVGL的初始化。这个任务运行完后,会把本任务删除,即调用vTaskDelete(NULL);

  3. 按键任务keytask,按键发生即发出信号量,调osMessageQueuePut(Key_MessageQueue, &keystr, 0, 1);osMessageQueuePut(IdleBreak_MessageQueue, &IdleBreakstr, 0, 1);,一个是按键信号量,一个是空闲打断信号量;

  4. 屏幕切换任务user_ScrRenewTask.c,接受按键信号量,然后调用PageManager中的函数;

  5. 运行模式切换任务user_RunModeTasks.c ,主要用于进入STOP模式和退出STOP模式,接收到Idle超时发出的Stop_MessageQueue信号量就进入STOP模式。这里跟硬件和UI的睡眠时间设置强相关,比较耦合,代码还有goto,有点小小不规范,请见谅~

  6. 其它详见代码。

2. BootLoader与无线升级

Bootloader程序在Software/IAP_F411中,这个是我拿官方的IAP程序稍微改了一下。这个工程有需要的就自行研究一下,大家可以直接拿去用的,不需要改什么。

我唯一改的地方就是加了一个FLAG验证,即在FLASH中留了一个位置放FLAG,用于判断APP程序是否完好。进入升级模式时,FLAG位置自动清空,FLAG位是在传输完成验证后才写入内容的,可以保证APP传输的完整。

Copyright © 油炸鸡开源硬件 | 渝ICP备2024035140号 | all right reserved,powered by Gitbook更新时间: 2024-07-30 23:36:23

results matching ""

    No results matching ""