前言

想找一个综合了HAL库,LVGL,FreeRTOS的项目学习复刻练手,于是发现了嘉立创的OV-Watch项目,且这个项目有淘宝店,如果硬件复刻失败可以直接买;资料也比较丰富,在b站和这个博主的网站上都有一些教程,以及QQ群也可以相互交流问题,非常便于学习复刻。本人以这个项目为基础,从BSP底层驱动写起,到最后的IAP升级,自下而上实现了绝大部分内容,并且自行修改了很多,比如LVGL界面是完全自己做的。本来是复刻完没有写博客的心思的,打算上传自己的问题记录就行,但发现有个群友这个项目面试被问了很多问题,我一时也忘了该怎么回答,故写下此篇博客以进行回顾总结。

问题记录

  1. cubemx 选freertos版本需选择1.8.5,1.8.6有问题
  2. 官方LCD屏幕驱动设置背光(TIM3->CCR2)的值时,不能传1000(CCR值不能给满),得改小点,否则屏幕无法正常显示,会出现各种离奇问题。
  3. 官方给的驱动代码即使在宏定义那里改了引脚,但可能还有其他地方需要改(TIM通道等等);cubemx引脚的默认高低电平也要和官方配置一样。
  4. 可能别人的代码初始化没有写在一个模块,比如PWM初始化在其他地方,但lcd模块代码lcd_init里没写
  5. printf如果没有实现,即使不报错,也会影响现象。此外,得包含头文件stdio.h以及注意printf别漏参数了,否则虽然是警告,但会print未定义的数。
  6. 在keil中添加文件后,vscode没有显示。可以先保存,然后vscode会重新连一下文件夹。
  7. 若程序卡死,可以一步步调试,观察卡死的位置,可能是别人的delay函数你没有进行初始化。
  8. keil优化等级偏低可能会导致lvgl报错(关于断言的)
  9. 蓝牙串口插上电脑后,开启蓝牙即可在串口助手找到几个端口,进行串口打印。
  10. 有的芯片可能出厂时没有器件地址,读不到的。比如这个触摸芯片,读出来都是ff,但功能可用(要先复位才能正常使用)。
  11. 寄存器的位数bit[7]可能就是第八位(从0开始计数),想&某一位,要从二进制换算到十六进制(举例:&【第3位】【bit[2]】,不是& 0x03,而应该是0x04【0100】)。
  12. 输出型参数正确的声明和传参应该是 例如:float temp = 0;(声明),func(&temp);(传参)。不能是以下的:float *temp = 0; func(temp);后者传的是空指针。
  13. 用转编码工具转换后,需要全部rebuild一下,否则keil和cubemx错误。
  14. 如果显示屏显示出现重叠,断断续续等等问题,注意SPI位数和DMA位数是否一致,例如:SPI 8位,用到了DMA,但颜色是16位,
    那么DMA需要设置为16位的,并在DMA使用的时候设置SPI位数从8到16,之后再转回来。
  15. lvgl显示设备函数直接使用画矩形的函数会有问题,需要使用底层的SPI函数,逐点绘制太慢,用SPI + DMA 整块进行则很快。
  16. cmake在windows里用cmake 指令时需要加 -G”MinGW Makefiles”。如果是别人的项目,直接cmake会失败(需要删除原来的,重新生成build)。对lvgl模拟器来说,运行程序需要SDL2.dll
  17. squareline studio导入keil中字体报错,需加–local=english
  18. lvgl采用定时器,在回调函数里更新页面,在上面定义了页面的数值,在函数里是 如果页面数值与硬件当前测得数值不等,就刷新页面。 这个会出现一个问题,就是自己定义的数值和squareline studio绘制的
    页面数值不一样,所以当第一次进入回调函数改变的数值是我们自定义的数值,并不是页面本身的数值,但后续再进入这个函数时,因为我们比较的是自己定义的数值(已经改变为和硬件数值一样),
    所以if循环不进入,导致页面label数值不刷新,显示的是squareline studio绘制的数值,出现切换页面,但数值不变的现象。解决方法是去掉if语句即可。但这样每次到这个页面的时候就会出现先出现原来的数值,
    然后再刷新到现在的数值的问题。
  19. 看门狗在不焊接电池的情况下容易复位,可能是电压不稳导致认为卡死,在焊接电池后看门狗正常运作。

BSP部分

BSP部分我是对着手册和up代码自己对着写的,本想直接改改官方的驱动代码,不接触底层的,但是问淘宝客服我只获得了LCD屏幕的驱动代码,其他的均无。加上LCD的官方驱动代码涉及的头文件太多了,LCD初始化需要调用很多函数,而且很多内容(例如显示汉字等等)对于LVGL来说其实用不到,LVGL只需要一个矩形填充就行(但这个矩形填充也不能直接用官方代码,得自己写SPI+DMA)。故最后还是走向了写BSP这一步,不过也能学到很多东西,而且也是很有必要的,不会写底层驱动的不是好的嵌入式er。BSP中LSM303DLHC由于新的芯片容易烧坏,所以没有焊接,故我没有编写这部分。

屏幕

这个项目屏幕是P169H002-CTP,是块LCD触摸屏,所以有显示和触摸两部分需要编写。

显示

LCD屏幕的显示部分的芯片是ST7789V,这部分内容有官方代码,写的也很好,可以直接改引脚和PWM通道,照着cubemx工程驱动屏幕的,我LCD的驱动学习主要就是根据这个写的。

芯片引脚有:RST,MOSI,CLK,CS,DC,背光。
MOSI为SPI接收数据(屏幕只显示,不输出,所以在cubemx里配置也是”Transmit Only Master”)。
CS片选,置0表示芯片被选中,允许数据通讯,置1则禁止数据传输。
DC选择接收命令还是数据,1为数据,0为命令。
背光,用PWM控制可根据占空比设定屏幕的背光亮度。

一般先是由DC控制是发数据还是指令,然后CS置0允许传输数据,之后通过SPI写指令或者数据,如果是指令,可能后面还会跟上指令的参数(再发送数据)。

亮点

  1. 编写了Debug.h文件,里面通过宏定义预编译指令实现Debug信息是否打印的控制(#if DEV_DEBUG…)和显示是哪个文件(__FILE__),哪行(__LINE__),以及自己输出的信息(可变参数 ##__VA_ARGS__)。

  2. 巧用宏定义,定义一个宏定义实现引脚的GPIO和PIN的定义,避免重复写电平状态强制转换:

    1
    2
    3
    4
    #define LCD_RST_PORT_PIN GPIOB, GPIO_PIN_7  // RST

    #define LCD_Pin_Write(_pin, _value) \
    HAL_GPIO_WritePin(_pin, _value == 0 ? GPIO_PIN_RESET : GPIO_PIN_SET)
  3. typedef定义一个屏幕实例,方便屏幕不同方向显示管理:

    1
    2
    3
    4
    5
    6
    // 保存屏幕特征
    typedef struct {
    uint16_t WIDTH;
    uint16_t HEIGHT;
    uint8_t SCAN_DIR; // 显示方向
    } LCD_ATTRIBUTES;

初始化首先是将DC,CS,RST等置1,PWM开启,进行引脚的初始化默认电平设置。
然后执行复位,设置屏幕方向,初始化寄存器,最后设置背光,清屏显示白色完成初始化操作。
复位实现是将RST的引脚电平从1变化到0,最后置1回到默认。
设置屏幕方向是在指定地址写命令,并把屏幕实例的高和宽进行是否颠倒操作。

之后是初始化寄存器操作,用的官方的操作,只不过需要把前两行屏幕方向设置给注释掉,因为我们已经设置过一次方向,如果再设置方向可能导致默认是竖屏,但我们设置是横屏,此处再设置方向导致屏幕出现撕裂。
寄存器操作的流程如下:设置界面像素格式(16位) -> 空白区设置(porch setting)(显示有效像素数据之前和之后,需要在屏幕上插入的无数据时间间隔。扫描时间长度) -> 门控设置(gate control)(调整像素电压的开关时间,以控制液晶的亮度和对比度) -> VCOMS Setting(液晶分子偏转的参考电压) -> LCM控制(液晶显示模组控制) -> VDV和VRH命令启用(VDV指的是VCOM未选中时的电压,而VRH则涉及到VCOM电压的高端范围) -> VRH Set -> VDV Set -> 正常模式下的帧速率控制 -> 电源控制 -> 正电压伽马控制 -> 负电压伽马控制 -> 门控 -> 退出反色模式 -> 退出睡眠模式 -> 开启显示

设置背光,通过PWM控制背光引脚,做限幅,控制在0到100
最后进行清屏操作完成初始化,通过全屏写入白色实现。如果不清屏,会导致花屏。

反初始化是将DC,CS,RST置0,然后关闭PWM。

绘制图形我只实现了画点,线和矩形,因为LVGL只需要绘制矩形区域的函数就行,此处的矩形绘制是通过遍历高的方式(y)来画线实现的,但这种方式无法用于LVGL,LVGL的填充是先设定绘制的窗口区域,然后改变SPI的数据传输位数从8位改为16位(颜色数据是16位),需要通过改寄存器实现。之后用DMA(cubemx设定为16位)来实现DMA传输,等待传输完成后再恢复SPI位数。

LVGL也可以用自带的两层循环然后画点实现,但是效率太低,很卡很卡,根本用不了。
画点函数也是先设定绘制区域,然后直接传输颜色数据就行。画线是用Bresenham直线算法,用画点函数就行。设定绘制区域窗口大小也是通过发送指令,设置列,行的范围实现,最后开启内存写入供颜色数据传输。官方的这个设定窗口区域的范围需要把里面的”-1”给删了,否则导致不同区间绘制图形出现间隙,LVGL绘制也是出现裂缝。

其他睡眠等函数也是发送指令等等就能完成。

触摸

触摸芯片是CST816T,官方驱动是用硬件I2C实现的,但这个项目用的软件I2C,加上官方代码很简陋,不如up写的好。后续的BSP基本上都是参考up和手册了。

CST816T的寄存器写的很清楚,比屏幕显示要容易,但是初始化必须要先复位,还有就是这个芯片没有ID号的,读出来是ff,但是功能是能用的,这点比较误导人。

引脚为RST,SCL,SDA,还有一个INT没有用到。

亮点
这里主要是I2C的函数写的很好,利用typedef来构建一个I2C_Bus_t的结构体,用于存放各I2C设备的引脚。用这个结构体在使用I2C的时候传入,做到一套I2C代码实现多设备复用的效果,有点C++类的感觉。

初始化函数是复位后设定自动睡眠(x时间无触摸睡眠)。所有操作都是读写寄存器就能得到,比如获取坐标等等。LVGL只需要用到检测是否触摸和获取坐标就行。

蓝牙串口

蓝牙串口是用的KT6328,运费非常贵!两块钱的芯片,十二的运费。。吐血了。像这种蓝牙都是协议方面不需要我们考虑,当作串口用就行,配置也是,cubemx里正常配串口,加上DMA传输和串口中断。默认是115200的波特率。

涉及编程的引脚只有三个,TX,RX,还有一个使能引脚,置1或0使能或失能。还有一个外接LED表示串口数据的接受与否,两个外接一个晶振(24MHz),一个接出去与板子所画的天线连接。

printf重定向配好后打开电脑蓝牙,上位机就能找到端口了,连接就可以当作串口使用。

传感器

这里的传感器很多都用I2C协议进行通讯,我们采用软件I2C将他们挂在同一引脚上,节省引脚资源。使用也简单,I2C通讯读写寄存器就行。

温湿度传感器

芯片是AHT20,读取流程手册里写的很明白,首先要查看状态字的校准位,如果某位不为1就发送初始化指令,复位指令也是通过发送指令实现。读取数据则是发送测量指令后接收六个字节数据,这里需要把读的数据做移位拼接等的操作,最后用拼接的数据按手册公式计算即可得到温度和湿度。

海拔大气压传感器

这个传感器是SPL006_001,可以计算海拔,大气压,温度,但是海拔计算需要大气压和温度数据,所以寄存器偏多。手册里给了三个例子场景:天气站,室内导航,运动。我们只需要精度不是很高的天气站用例就行,按他给的配置参数进行寄存器读写操作初始化就行(压力和温度传感器测量速率和过采样率配置,模式配置,中断和FIFO配置),需要读很多寄存器获取参数,最后用手册公式进行计算。期间根据选择的采样精度和频率需要对照手册选择KT和KP的值。

心率血氧测量

使用EM7028,资料非常少,手册也不写明白怎么计算心率和血氧,官方代码只在CSDN上看到个付费的,淘宝客服说没有。。up的代码是自己写了一个局部寻峰算法寻找心率,但也不是特别准确。

初始化配置了测量模式是否开启,数据偏移,接近传感器增益控制,心率测量频率等的配置,中断控制。

局部寻峰算法是自己写了一个队列queue,然后每次获取数据就放到队列里,将第3个数据(下标从零开始,这个即为中间数据)和其余6个数据进行比较,若第三个为最大,则判断第三个数据的时间和上次最大数据的时间间隔大小,将大于425的时间进行保存,用于下次计算时间间隔,然后用60000(60 * 1000ms)除以这两次时间间隔,得到心率数据,得到7个后进行均值滤波返回最后的数值。

抬腕检测与步数检测

采用MPU6050,步数检测用的是官方的库函数,移植DMP库。抬腕检测是软解pitch和roll角度(没有滤波等处理),因为只是抬腕检测,不需要太高精度。判断抬腕是检测是否水平,如果roll和pitch不在-0.5和0.5间就判断为水平。具体方法见FreeRTOS部分。

MPU6050的初始化emmmm其实没必要自己写,DMP库自带了初始化函数,我们只需要实现I2C读和写函数,延时等等函数接口就行。这部分代码太多了,up说里面改了低功耗,不要随便改,直接用就行,我也没咋看了,感觉挺麻烦的。这里我遇到了DMP库的欧拉角解算问题,会在dmp_read_fifo()那里失败,网上也有很多人遇到了这个问题,我折腾了半天放弃了,正好这个项目没有用到这里的欧拉角解算,之前做平衡车的时候这里就耗费了很长时间,不想再被折磨了。

我这里用up的MPU6050初始化代码会导致温度读取失败,但是DMP库的初始化则可以,不过MPU6050的温度读取也没用到,不管了。

EEPROM

EEPROM用来保存用户设置,步数等数据,芯片采用BL24C02,虽然也是I2C协议,但是另外的引脚,没有和传感器挂在一起。2K大小,每页16字节。

BL24C02不需要初始化配置,把I2C引脚配好就行,读写数据按I2C协议读,用数组存储数据。

为保证数据读入正确,写了一个检查函数,读取EEPROM的0x00处的前两个字节,若第一个字节不为0x55,第二个不为0xAA,则再次写入值,若再次读值仍不等,则返回1;反之返回0。其中0x55为01010101,0xAA为10101010,刚好可以验证所有位数。

电源管理

电源管理分为电池的供电和充电问题,电池供电采用TPS6302x高效率单电感器降压/升压转换器,充电采用TP4056锂电池充电器。

TPS6302x引脚部分需要外部接一个电感(决定输出电流),此处是1.5uH,存在两个电池输入(VIN),两个电池输出引脚(VOUT),一个电压反馈引脚(接在VOUT),一个使能引脚,其他不是很重要。

电池的功能代码里电源启用是通过使能引脚的电平高低来控制。电压采集则是通过在电池那里引出一条线到单片机的ADC1的通道1(12位)进行,经过电压分压,在代码部分开启ADC转换,8次结果取平均后,转换完后计算电压为 data * 2 * 3.3 / 4096,其中4096是2^12次,3.3为参考电压,2则是由于电压分压,需要乘回去还原到原来的电压值。计算电池的电量百分比则是需要首先检查是否充电状态,如果是还要减去电流引起的电压偏差,最后根据电压和电量对应返回剩余电量百分比就行。

充电采用TP4056充电器芯片,TP4056引脚里其中STDBY充电完成指示端和CHRG充电状态指示端接LED,反馈充电状态。PROG恒流充电电流设置端则是通过外接电阻来控制充电电流大小,然后就是BAT充电引脚接电池BAT+和VCC输入电压。

充电功能代码只有一个充电检查,充电检查是从CHRG的LED指示灯引出一条线接到单片机上,然后单片机读引脚电压,为1就是充电。

看门狗

看门狗就一个作用,当程序卡死的时候复位,我一开始还认为不是很重要,直到我的程序卡死了。。
由于要做低功耗,使用主控内部的看门狗需要不断去喂狗,无法做到休眠模式,所以采用了外部模拟开关和外部看门狗。模拟开关是BL1551B,外部看门狗则是TPS382x。

看门狗的引脚主要是WDI喂狗,还有一个引脚输出复位信号,低电平有效,狗叫,喂狗逻辑是在200ms内翻转WDI电平。

模拟开关则是控制使能/失能看门狗,以及复位信号的传达。引脚有EN使能,A1,A2输入,B输出。单片机引出一条线输入开关的EN引脚控制看门狗,当为低电平时A2接通,A2另一头是看门狗的输出引脚;当为高电平时A1接通,接的是3v3电源。所以,看门狗使能低电平有效。当看门狗复位时低电平信号拉低单片机的复位引脚,发生复位。

侧部按键

有两个按键,Key1和Key2,这两个按键的引脚配置也不一样,两个都配置为外部中断模式(好发出中断信号供后期唤醒等等),Key1是上拉模式,Key2是不上拉不下拉。Key1另一头是GND,当按下时会拉低电平,说明按下Key1;Key2则是另一头接的电池的BAT+,这导致Key2需要接电池才能检测到是否被按下,Key2被按下是高电平输入,读电平为1。

代码里主要是检测哪一个按键被按下,输入一个mode参数用于是否重置按键检测的状态和值,通过设置keyUp和keyDown表示按键的状态,和读取引脚电平来判断哪个被按下,最后返回按下的按键值。

LVGL

图形化界面采用LVGL,ui的绘制是用Squareline Studio进行,将生成的ui代码放到keil工程中进行,我Squareline Studio只用来绘制screen界面,具体里面的事件逻辑我用另一个文件ui_addition里写,我把这个文件单独与生成的ui代码分开,为的是方便多次生成Squareline Studio代码,直接覆盖原先代码就行,而不需要另外在生成代码里写东西,也为了保持结构的独立性。

LVGL移植主要是需要提供屏幕初始化和绘制区域接口函数,还有触摸的检测是否按下和返回坐标值。这里绘制区域函数必须要底层SPI+DMA实现,否则会失败或很慢。值得注意的是lvgl的颜色是指针形式,还有可能存在的颜色反色问题,若反色需要在配置文件里LV_COLOR_16_SWAP进行修改。

界面绘制Squareline Studio我的是个人版,限制10个screen,150个widget,1个panel等等。由于屏幕达到限制了,我放弃了计算器和游戏的实现。其他功能均已完成。我的screen一共有10个,HomePage,MenuPage,HRPage,PressurePage,AboutPage,SetPage,ChargePage,PasswordPage,TimerPage,CalendarPage。

HomePage为开机初始界面,显示时间,电量,年月日,温湿度,上次测得心率,每日步数。文本用label显示,图标用图标字体,步数进度有个进度条显示,用bar实现。还配有我老婆kuro的图片
MenuPage是菜单界面,里面有其他的功能选择,比如日历,秒表等等。全是button里嵌套label实现。
CalendarPage是日历界面,用Calendar填充整个屏幕就行。
HRPage是心率测量界面,在心率显示外面加一个spinner转动,显示出动态的样子。
PressurePage是海拔测量界面,显示温度,大气压,海拔。懒的绘制界面了,这里全是label文字修饰。
AboutPage为关于界面,显示作者等信息,全label。
SetPage是设置界面,用tabview管理时间,密码,息屏,其他设置。时间设置用roller显示每个时间选择,加上一个按钮用作确定。密码设置则是用一个textarea表示输入框,keyboard为数字键盘。息屏设置采用了dropdown下拉显示各种时间设置,一个按钮确认。其他设置里是抬腕开启和蓝牙开启用switch开关,屏幕亮度则是用slider滑动设置。
ChargePage是充电界面,一个bar显示电量,其他是label。
PasswordPage是输入密码界面,textarea加keyboard。
TimerPage是秒表界面,两个button表示开始和重置,其他为label。

页面设置拖拽就行,在右面可以配置各flag和属性,flag可设置是否滚动,是否有按下状态等等,字体图片Squareline Studio自带了取模功能,动画我没有配置,怕消耗大。具体事件部分见中间层部分。

中间层

我学习项目的顺序是BSP,移植LVGL,移植FreeRTOS+LVGL,编写中间层(硬件抽象层和页面管理),LVGL页面绘制和事件处理与rtos任务编写共同推进。本来想直接上LVGL和FreeRTOS学习的,但正所谓“一口气吃不成胖子”,最后还是转为BSP开始慢慢写了,最后在上操作系统。值得注意的是,不要把FreeRTOS和LVGL文件夹放一起在Hal生成的Middleware下,否则取消FreeRTOS后LVGL文件也会一起没的,此外对hal库来说,hal库没让写或改的地方千万不能写,否则下次生成代码就没了。

硬件访问层

HWDataAccess为硬件访问层,这里up也写的很好,用typedef+结构体定义了各硬件的类型,结构体里写了函数指针和值用于后面绑定对应的函数等,用宏定义管理是否有硬件支持,便于LVGL仿真的实现(这样没有硬件也不会代码报错)。例如:

1
2
3
4
5
6
7
8
9
10
#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_AHT20 1
#define HW_USE_SPL06 1
#define HW_USE_EM7028 1
#endif
1
2
3
4
5
6
typedef struct {
uint8_t PowerRemain;
void (*Init)(void);
void (*Shutdown)(void);
uint8_t (*BatCalculate)(void);
} HW_POWER_InterfaceTypeDef;
1
2
3
4
5
void HW_Power_Init(void) {
#if HW_USE_BAT
POWER_Init();
#endif
}
1
2
3
4
5
6
7
8
9
//...
.Power =
{
.PowerRemain = 0,
.Init = HW_Power_Init,
.Shutdown = HW_Power_Shutdown,
.BatCalculate = HW_Power_BatCalculate,
},
//...

页面管理层

由于screen页面众多,加上还涉及到按键切换页面,进行页面管理比直接添加事件更高效。页面管理采用栈来管理,typedef定义一个页面类型和页面栈类型,然后实现栈的入栈出栈等操作,最后再封装页面管理的操作。

返回页面是先判断栈是否为空,为空直接返回。出栈后如果栈为空就入栈Home和Menu界面,否则就加载现在栈顶的页面,页面加载函数需要指定最后参数为true自动删除旧页面,防止资源无法释放。

UI事件管理

这个是我自己加的一个中间层,方便统一管理页面的事件和变量等等。头文件extern出了在c文件或其他文件用到的变量,函数倒只有一个ui_myInit(),用于替代Squareline Studio里的ui_init(),其他函数在c文件按顺序编写,后面函数会用到前面的函数,所以如果不按顺序写会找不到定义,全部堆在h文件又太丑和没有必要。

c文件一开始是各个用到的变量,比如是否开启蓝牙,密码,时间等等变量,还有定时器的声明。接下来是页面定时器回调函数,自定义事件函数,自定义界面初始化/反初始化函数,页面结构管理,最后是其他函数。

定时器部分里用到定时器的有HomePage,HRPage,PressurePage,ChargePage,TimerPage,因此回调函数也有这几个,再外加一个main_timer用于刷新屏幕。
HomePage回调函数是用于刷新主页面温湿度,步数等数据的,从硬件访问层实例获取数据,用sprintf控制格式输入到一个数组中,最后用LVGL的函数设置。其他也都差不多。

自定义界面初始化/反初始化函数是用于在ui代码的原始初始化后面增加事件的,界面初始化直接绕过原始的init函数,调用我们自定义的函数,实现在后面添加事件,定时器等操作。

页面结构管理用到了页面管理定义的页面类型,里面有自定义初始化函数,反初始化函数,还有屏幕对象。定义了页面对象,使得和页面栈相互使用。

自定义事件函数实现了页面的滑动切换,按钮的功能实现等等。逻辑一般都是获取事件代码,判断代码是什么事件,如果是某个事件,就执行xxx。在自定义初始化函数里需要添加事件的作用对象和函数等等。

其他函数就一个,自定义的ui_init函数,里面把原先的init函数里的第一个页面初始化改为页面栈初始化,增加main定时器用于刷新。

这里页面里在MenuPage里点设置时,导入的是密码页面,当输入密码正确后才load设置SetPage页面,在设置页面里的更改,除了时间外的设置都被保存在EEPROM中永久存储,为了显示是否开启的状态变化,每次都需要先根据EEPROM的数据来设置状态,使之在ui层面也表现出被永久改变了。

充电界面需要用到FreeRTOS,发生充电事件会load此页面。
按键切换页面逻辑也在FreeRTOS。

FreeRTOS

本项目采用FreeRTOS管理,其中用到的有Tasks和Queues,timer,其他的均没用到。利用FreeRTOS运行多个任务,当发生一些事件时向消息队列发送消息,相应任务获取到合适的信号后执行。HAL库的FreeRTOS移植很简单,cubemx里配置就行,不过需要把vApplicationIdleHook和vApplicationTickHook启用,前者用于处理空闲任务,后者则是写LVGL的心跳以及秒表的计时。

FreeRTOS的各个任务等都是自己写,用一个单独的文件夹存放,然后写一个入口函数,在MX_FREERTOS_Init()里写入,这样初始化可以直接进入我们自定义的任务初始化。为什么不用cubemx配置,因为cubemx里配置也只能配置名称,任务大小等等,里面的逻辑还是自己写,用处不大;况且生成的所有代码都会在freertos.c里存放,不利于管理;如果突然不想用FreeRTOS了,也会导致所有的代码消失。

FreeRTOS里自己写的代码存放在Tasks文件中:

user_TasksInit 所有任务的初始化
user_HardwareInitTask 硬件初始化
user_SensUpdateTask 传感器更新
user_RunModeTask 运行模式
user_KeyTask 按键
user_ScrRenewTask 按键切换页面
user_MessageSendTask 串口发送消息
user_DataSaveTask 数据存储
user_ChargCheckTask 充电检查

user_TasksInit

初始化所有任务,每个任务首先需要定义自己的属性(名字,栈大小,优先级),栈大小以128字节为单位乘以数量,均采用动态分配内存方式。在前面也声明一些timer,queue,任务句柄等的定义。

自定义的初始化函数
timer用了一个IdleTimer,实现一定时间不操作逐渐灭屏的操作。

创造的queue有key,idle,stop,idleBreak,homeUpdate,dataSave。
key是存放key的按键消息,idle是否进入idle模式,stop是否进入stop模式,idleBreak从idle退出,homeUpdate更新HomePage的信息,dataSave保存数据。

之后是创建各任务的thread,硬件初始化任务,LVGL定时器handler任务,看门狗任务,idle进入任务,stop进入任务,按键任务,关闭心率的任务,传感器更新任务,心率数据更新任务,充电界面进入任务,串口数据发送任务,MPU抬腕检查任务,数据存储任务。

最后给homeUpdate的messageQueue发消息,更新HomePage的信息用于第一次显示。

LVGL handler任务函数是获取屏幕的当前未活动时间,超过一定时间则发送idleBreak消息。在这里也进行lv_task_handler()的填写。

HardwareInitTask

初始化各种硬件,在临界区内,执行完后任务被删除,只执行一次。初始化RTC,USART,PWM,delay,Power,key,Sensor等,并读取EEPROM的数据,初始化设置,步数,密码等信息。之后进行蓝牙,lvgl的初始化,最后加载自己的ui初始化函数。

传感器的初始化在while循环中完成,由于每个传感器类型都定义了是否初始化错误的变量,while循环三次还失败就跳过,例如:

1
2
3
4
5
num = 3;
while (num && HWInterface.Barometer.ConnectionError) {
num--;
HWInterface.Barometer.ConnectionError = HWInterface.Barometer.Init();
}

EEPROM读取则是首先判断0x10的前两个字节是否和存储在里面的数据一致,验证是否读取错误,之后读取是否抬腕和蓝牙设置,亮度设置,最后根据RTC的日期判断是否和EEPROM的一致,一致则读取步数,否则就写入。

SensUpdateTask

含多个任务,但大多数都是数据更新任务。MPU检查抬腕是首先判断抬腕是否启用,启用则判断是否水平,水平就将当前状态定义为抬腕状态(1),否则判断当前状态是否为抬腕,是则先将状态提前变为手下垂状态(0),如果当前页面在HomePage,MenuPage,SetPage就向stop的queue发送进入stop的信息。如果不水平就默认为下垂状态。

心率更新是当前页面是心率测量页面时就向idleBreak发消息,防止测量心率时屏幕休眠。之后启用心率,在临界区进行心率的计算,把一定阈值内的数值(过滤错误数据)赋给心率测量的结果。

传感器数据更新任务是当接收到homeUpdate的queue消息时才进行更新,更新HomePage的电量,步数,温湿度,之后在向dataSave的queue发送消息保存步数结果。海拔大气压的数据更新则是先判断当前页面是不是PressurePage,是的话发送idleBreak防止休眠,计算数据。

RunModeTask

运行模式任务,手表有三个模式,正常的运行模式,idle模式,stop模式,睡眠程度依次增加。有两个任务,一个进入idle,一个进入stop。一个idle的定时器的回调函数。

回调函数idle是实现不操作逐渐灭屏,外部定义了一个计数变量,若计数变量等于暗屏时间就发送idle消息,若等于灭屏时间就发送stop信息。

进入idle模式的任务是先获取idle消息,若为osOK,则将LCD亮度设置为5,如果获得idleBreak消息,就将亮度重新设回原先的值。

进入stop模式也和idle差不多,获得消息后串口,LCD背光,触摸停止。之后在临界区失能看门狗,禁用systick中断,芯片进入stop模式,自此手表再也没有亮过。自此程序停在这里,功耗最低,当发生外部中断,比如按键,抬腕,充电等后继续向下执行代码,重新将systick中断打开,进行systick中断配置,系统时钟配置,喂狗。之后是抬腕,充电和按键检测代码,最后重新初始化串口,LCD显示,触摸,发送homeUpdate消息,页面加载HomePage。

KeyTask

这个任务是实现按键的部分逻辑,扫描按键的值,当为1(按键1)则发送key消息和idleBreak消息(退出暗屏);当为2则先判断当前页面是不是HomePage,是就发送stop消息(实现关屏),否则就发送key消息和idleBreak消息(亮屏)。所以,按键1不论关屏还是亮屏都会唤醒手表,按键2则是关屏和亮屏切换。

ScrRenewTask

这个任务我觉得其实可以写到KeyTask里,流程是获取key消息,得到按键后若为1,则直接返回上一个页面(出栈),若返回到MenuPage,就失能心率测量(在测完心率后关闭);若为2则直接回到页面栈最底层(一般为HomePage),同时关闭心率测量(类似手机home键)。

MessageSendTask

主要功能是通过串口打印传感器信息,串口设置时间。任务里面是先判断自己写的变量,串口中断标志位有没有置1,若发送中断,说明有数据发过来,之后首先发送idleBreak消息,防止息屏,并打印(回传)发的信息以证明正常使用。之后可以通过发送APPSyEN来启用允许串口设置时间,或APPSyDIS禁止;通过发送AI+SEND来获取传感器时间;如果收到的字符串长度刚好20(AI+ST=20230629125555),判断APPSyEN是否开启,若开启则进行字符串的解析,将解析得到的时间设置到RTC。

DataSaveTask

EEPROM保存数据的任务,主要保存设置信息,比如是否抬腕,蓝牙,亮度调节,每日步数,密码。当获取dataSave消息时,就在指定位置进行数据保存,通过数组设置数据,然后调用EEPROM的函数完成。

ChargCheckTask

充电检查,当充电中断标志位置1时,检查是否充电,如果当前页面不是充电页面,就加载充电页面;如果不再充电且当前为充电页面,就返回先前页面。

其它

还有一点功能是零零散散的在各种生成的hal代码上,比如中断it.c里面定义了串口接收数组,串口/MPU抬腕/充电中断标志位变量;在充电外部引脚中断函数中将中断标志位置1;TIM1更新中断函数中判断key1的按下时间是否超过3000ms,超过就失能电池(关机);串口中断函数定义接收数据等等。rtc.c里也加了防止重复初始化,若初始化后就不再初始化的代码。

如果关机了,电池停止供电了,那怎么开机呢,我认为关键是key2的按键设置,当有电池连接时,按key2会导通电路到引脚,引脚就上电了,然后全部内容重新启动。

至此,APP部分结束。

IAP

通过蓝牙发送数据来进行远程烧录升级,改自官方例程,发送协议是Ymodem,需要SecureCRT将整个bin文件发送,bin文件由keil生成,需要在Options里的User下的After build里填keil自带的bin转换工具,我这里写的是C:\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin -o .\myTryF411\myTryF411.bin .\myTryF411\myTryF411.axf。IAP升级需要划分出一小段空间下载bootloader程序,再由bootloader转到app部分运行代码,up还再两者之间加入了flag,用于验证程序的完整性。在bootloader程序里,需要将keil的编译内存在option里起始设置为0x8000000,大小为0x8000(IAP大小为0x8000),然后由于flag大小(0x4000),故最后的APP起始位置就到了0x800C000(0x8000000+0x8000+0x4000),大小则是0x74000。此外,在APP程序还需要在main函数里面开头加上SCB->VTOR = FLASH_BASE | 0xC000;来设置中断向量表的偏移。

Bootloader程序由于也是一个单独的程序,所以需要BSP等,用到了按键,蓝牙,LCD和串口。不需要FreeRTOS。升级逻辑是当开机启动后,默认是bootloader程序,里面首先检测key1是否按下,若按下则LCD显示等待升级的图案,之后仿照官方历程调用他的函数就行;没有按下key1的话,就读取flag位置的数据,判断和代码里的是否相同,相同则禁用systick后直接跳转APP,若不同则说明程序不完整,显示标识图案。bootloader里while循环没有啥逻辑,若跑到这里了,说明出错了,失能电源。

由于bootloader程序是上电后检测key1,故进入方法是如果接电池的话,需要按下key1不放,然后按key2上电;插stlink也是先按key1,再插stlink。

最后

终于结束了!!!写完文档了,这个项目正式完结!若有时间再去学学建模吧,完结散花~