# m5core **Repository Path**: ckunkun/m5core ## Basic Information - **Project Name**: m5core - **Description**: 二期培训 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-06-22 - **Last Updated**: 2024-07-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # **OpenHarmony M5core2开发教程(WSL V1.5)** # 版本 V1.0-1.2 内部迭代 V1.3 一文通搭建环境、下载代码、编译、工程详解。 V1.4 一文通搭建环境、下载源码、编译、倒计时案例、灌溉案例 **V1.5 改为WSL方式搭建开发环境,免去安装虚拟机、文件与Windows直接共享;只下载轻量系统代码,减少内存占用。** # 前言 本教程将会详细说明基于OpenHarmony完成倒计时系统、自动灌溉系统的开发。主要内容包括OpenHarmony简要介绍、开发环境搭建、获取源码、编写程序。实操教程从[1.3节开始](#1.3搭建开发环境)。案例基于命令行的方式开发(流程如下图右侧)。 ![前言](./images/01.png) ## 学习目标: A. 了解什么是OpenHarmony(本文简称OH) B. 熟悉OpenHarmony开发流程 C. 掌握基于OH+M5Core2的倒计时系统、灌溉系统开发案例 D. 基于此教程,可尝试开发自己的OpenHarmony项目 # 1 OpenHarmony及环境搭建 ## 1.1 认识OpenHarmony **项目介绍** OpenHarmony是由开放原子开源基金会(OpenAtom Foundation)孵化及运营的开源项目([OpenAtom OpenHarmony](https://www.openharmony.cn/mainPlay)),目标是面向全场景、全连接、全智能时代,基于开源的方式,搭建一个智能终端设备操作系统的框架和平台,促进万物互联产业的繁荣发展。 **技术架构** OpenHarmony整体遵从分层设计,从下向上依次为:内核层、系统服务层、框架层和应用层。系统功能按照“系统 > 子系统 > 组件”逐级展开,在多设备部署场景下,支持根据实际需求裁剪某些非必要的组件。OpenHarmony技术架构如下所示: ![架构](./images/02.png) **技术特性** **(一)** **硬件互助,资源共享** 主要通过下列模块达成 l **分布式软总线** 分布式软总线是多设备终端的统一基座,为设备间的无缝互联提供了统一的分布式通信能力,能够快速发现并连接设备,高效地传输任务和数据。 l **分布式数据管理** 分布式数据管理位于基于分布式软总线之上的能力,实现了应用程序数据和用户数据的分布式管理。用户数据不再与单一物理设备绑定,业务逻辑与数据存储分离,应用跨设备运行时数据无缝衔接,为打造一致、流畅的用户体验创造了基础条件 l **分布式任务调度** 分布式任务调度基于分布式软总线、分布式数据管理、分布式Profile等技术特性,构建统一的分布式服务管理(发现、同步、注册、调用)机制,支持对跨设备的应用进行远程启动、远程调用、绑定/解绑、以及迁移等操作,能够根据不同设备的能力、位置、业务运行状态、资源使用情况并结合用户的习惯和意图,选择最合适的设备运行分布式任务 l **设备虚拟化** 分布式设备虚拟化平台可以实现不同设备的资源融合、设备管理、数据处理,将周边设备作为手机能力的延伸,共同形成一个超级虚拟终端。 **(二)** **一次开发,多端部署** OpenHarmony提供用户程序框架、Ability框架以及UI框架,能够保证开发的应用在多终端运行时保证一致性。一次开发、多端部署。 多终端软件平台API具备一致性,确保用户程序的运行兼容性。 支持在开发过程中预览终端的能力适配情况(CPU/内存/外设/软件资源等)。 支持根据用户程序与软件平台的兼容性来调度用户呈现。 **(三)** **统一OS,弹性部署** OpenHarmony通过组件化和组件弹性化等设计方法,做到硬件资源的可大可小,在多种终端设备间,按需弹性部署,全面覆盖了ARM、RISC-V、x86等各种CPU。 ## 1.2 OpenHarmony能做什么?——示范案例 **开鸿智谷** **做懂行的操作系统提供商** 湖南开鸿智谷数字产业发展有限公司(以下简称“开鸿智谷”)成立于2021年12月,由拓维信息全资成立,是一家专注**OpenHarmony国产行业操作系统研发的高科技企业**,公司致力于联合政府、华为和伙伴制定行业标准,共建OpenHarmony生态,快速推进OpenHarmony商业化落地进程,基于1+1在鸿平台,打造万物互联的数字底座,赋能千行百业转型升级。 开鸿智谷是开放原子开源基金会白银捐赠人、OpenHarmony项目A类捐赠人、华为OpenHarmony生态使能合作伙伴、华为鸿蒙智联ISV合作伙伴、华为公路水运口岸智慧化军团战略合作伙伴,聚焦教育、交通、智慧城市等领域,做懂行的操作系统提供商。 **自主品牌:在鸿(ZaiOHOS**),品牌寓意“鸿蒙破晓、无处不在” **核心产品:在鸿行业发行版** 、在鸿设备管理平台、在鸿教学系列实验箱。 ![0](./images/03.png) ![04](./images/04.png) ## 1.3 搭建开发环境 这里首先推荐使用WSL,如果不能使用WSL,请参考V1.4教程搭建虚拟机环境。如果想要使用WSL,电脑必须运行 Windows 10或 Windows 11 才能使用WSL 。检查系统是否满足需求,键盘选择Win + R,输入winver,点击回车,便可查看当前系统的详细版本。如果不满足要求,则需要对系统进行升级。 - 对于x64系统:版本1903或更高版本,内部版本为18362.1049或更高版本。 - 对于ARM64 系统:版本2004或更高版本,内部版本为19041或更高版本。 ![image-20231129173454158](./images/51.png) ### 1.3.1 开启WSL功能 - **第一步**:打开**控制面板,**找到**程序和功能**,开启**Linux、虚拟机平台、Hyper-V**,如下图(开启后会提示重启,重启即可。) ![image-20231129173515535](./images/52.png) - **第二步**:重启之后,升级Linux内核,下载[适用于 x64 计算机的 WSL2 Linux 内核更新包](https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi)(安装包在仓库也有:“培训资料V1.5/wsl_update_x64.msi”),下载后双击安装。 搜索打开Windows PowerShell,用管理员身份运行,在打开的终端输入: ```c wsl --set-default-version 2 ``` ![image-20231129151753307](./images/09.png) - **第三步**:打开电脑的 [Microsoft Store 应用商店](http://),搜索Ubuntu20.04,然后下载安装,会默认安装到C盘,如果C盘不够大,建议扩大内存,至少有120G空余内存,如果你的应用默认安装到其他盘,需要将安装位置改到C盘,方法是打开**设置—>系统—>存储更改新内容的保存位置--->新的应用保存到C盘(系统盘)** ![image-20231129172517924](./images/45.png) ![image-20231129172813916](./images/46.png) - **第四步**:在启动界面搜索,Ubuntu20.04,直接点击即可打开 开启后会自动更新和配置,等待一段时间后,可看到如下图的提示,需要按如下说明输入用户名等信息: ```ABAP #01 提示Enter new UNIX username,输入 hispark (输入后都需要按回车entrer键,后续输入指令操作都如此) #02 提示 New password,输入提示输入密码 (注:输入密码不会显示是正常的,输入密码的场景都如此),输入: 2023 #03 提示Retype new password,再次输入: 2023 ``` 成功配置后,进入如下界面说明WSL安装成功: ![image-20231129172916869](./images/47.png) - 如果启动遇到: ```js Installing, this may take a few minutes... WslRegisterDistribution failed with error: 0x800701bc Error: 0x800701bc WSL 2 ?????????????????? https://aka.ms/wsl2kernel Press any key to continue... ``` 方法1:如果没有下载安装适用于 x64 计算机的最新 WSL2 Linux 内核更新包,下载并安装,下载地址:https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi 下载后直接双击安装,之后再重新启动Ubuntu20.04,可以正常启动。 方法2: 在Windows PoweShell 输入 netsh winsock reset 方法3:方法1-2不起效,则尝试**关闭网络代理,关闭梯子软件** ### 1.3.2 查看WSL共享文件 WSL文件本身与Windows是共享的,查看Linux系统文件方法如下: - 打开文件管理器,在【文件管理器窗口顶部的文件路径栏】输入:\\\wsl$,回车就可以看到wsl的共享文件: ![image-20240112103854680](./images/64.png) - 选中Ubuntu20.04文件夹,点击右键选择`映射网络驱动器`,随意配置一个驱动器号,然后点击完成: ![image-20231129173224124](./images/48.png) ![image-20231129173239770](./images/49.png) ![image-20231129173257205](./images/50.png) ### 1.3.3 安装VScode - 下载VScode([Visual Studio Code - Code Editing. Redefined](https://code.visualstudio.com/))安装包,windows下安装,点击下一步进行安装。 ![image-20231129173955611](./images/53.png) - 点击浏览按钮,选择VScode的安装路径,然后点击下一步。 ![image-20231129174015505](./images/54.png) - 一直点击下一步。 出现下方界面,点击安装,等待安装后,点击完成。 ![image-20231129174055653](./images/55.png) - 打开VScode,然后点击拓展,搜索安装remote SSH、WSL, (vscode右下角 出现弹窗,全部关闭) 安装remote SSH:![image-20231129174155888](./images/56.png) 安装WSL![image-20231129174243039](./images/wsl.png) - 打开WSL文件目录,点击Remote图标,按照如图选择WSL Targets: ![image-20231129174750960](./images/58.png) 点击弹出的ubuntu20.04选项,点击右边的"新建窗口",弹出来一个新窗口: ![imagevs_wsl](./images/vs_wsl.png) 在新的窗口打开文件夹, ![image-20240112095251458](./images/62.png) 你是hispark用户,则选择/home/hispark/,点击OK打开目录,(如果是root等用户,打开默认路径,点击ok) ![image-20240112095635625](./images/63.png) 打开后默认界面如下图,窗口保持最大化,方便操作,vscode终端(terminal)在菜单栏有。 ![image-20240112100719634](./images/60.png) # 2 OH源码与说明 ## 2.1 源码获取 **步骤1:安装基础软件** ```sh 在VSCode内打开终端(Terminal),下载安装需要的软件,依次执行以下指令: ##0.1 更新源,提示输入密码,输入2023(注:输入密码不会显示是正常的,输入密码的场景都如此),按回车键 sudo apt update ##0.2 下载所需要的软件,指令是一行。若下载中途出现(y/n) 输入y即可 sudo apt-get install openssh-server net-tools ccache vim git curl python3 python3-pip git-lfs -y ##0.3 更改python软链接 sudo ln -s /usr/bin/python3 /usr/bin/python #0.4 安装交叉编译工具链 mkdir -p ~/download && cd ~/download #0.5 下载交叉编译工具链压缩包,指令是一行 wget https://dl.espressif.com/dl/xtensa-esp32-elf-gcc8_4_0-esp-2021r2-linux-amd64.tar.gz #0.6 解压工具链到/opt 指令是一行 sudo tar axvf xtensa-esp32-elf-gcc8_4_0-esp-2021r2-linux-amd64.tar.gz -C /opt/ #0.7 安装hb 工具,指令是一行 pip3 install --trusted-host mirrors.aliyun.com -i http://mirrors.aliyun.com/pypi/simple jinja2 pyserial ohos-build==0.4.6 esptool ``` **步骤2:环境变量**,【如下图】所示,用windows记事本【或者】vscode等都可以打开配置文件, 将交叉编译工具、hb工具添加至环境变量: ![image-20231129153148135](./images/10.png) ```sh #0.8 在文件编辑器中,找到文件末尾,添加以下内容(添加后记得保存文件,然后关掉即可。): export PATH=~/bin:$PATH export PATH=$PATH:/opt/xtensa-esp32-elf/bin export PATH=$PATH:~/.local/bin ``` ![image-20231129153318973](./images/11.png) 使配置环境变量生效: ```sh #0.9 在VSCode终端执行指令,使配置生效: source ~/.bashrc ``` **步骤3:获取OH源码** * 克隆准备好的仓库,大小<800M ```sh #0.10 打开VSCode终端,进入你的用户名文件夹下载代码: # 如果你是hispark用户登录: cd /home/hispark git clone https://gitee.com/ckunkun/m5core.git 【#如果你是root用户,在你的root下创建一个hispark文件夹(注意:hispark用户跳过下面指令操作) cd /root mkdir hispark cd /root/hispark git clone https://gitee.com/ckunkun/m5core.git 】 ``` - 下载完成后,即可得到m5core的源码。注意: **将下载得到的m5core文件夹名改为m5core2**(便于教程统一)。 **将下载得到的m5core文件夹名改为m5core2**(便于教程统一)。 **将下载得到的m5core文件夹名改为m5core2**(便于教程统一)。 ## 2.2 查看源码【可跳过】 ### 2.2.1 WSL共享的源码目录 - **在windows下查看** 源码在home/hispark/m5core2/下。**hispark**是自己的用户名。进入WSL的共享目录即可看到源码,后面的操作基本都在这个路径下。 ![image-20231129153516980](./images/12.png) - **在vscode下查看** ![image-20231129175245377](./images/61.png) ### 2.2.2 OH源码说明 l 下载完成后,在文件管理器中可看到如下源码文件: ![image-20231129153720893](./images/13.png) - 每个文件夹下的源码文件都很重要,切记不可随意删除改动。下面是部分文件夹的说明: applications: 各系统APP demo build:编译构建相关文件 device:各类芯片、模组相关描述、支持文件 drivers:驱动相关文件 kernel:OH内核 out:编译二进制文件输出目录 third_party:第三方库,如LVGL vendor:各设备厂商提供的OH项目demo - 倒计时系统、灌溉系统等案例文件在**m5core2/vendor/m5stack/m5core2/app**下,我们主要是在该文件夹下开发。 # 3 倒计时系统开发教程 ## 案例说明: 倒计时系统需要M5core2、TypeC数据线。实现功能:能滑动数字选择分钟数、秒钟数,点击开始按钮开始倒计时,点击暂停按钮倒计时暂停,点击取消按钮时间初始化为10s,效果如下图。 ![image-20231129154540313](./images/14.png) (详情链接:**[**https://docs.m5stack.com/zh_CN/core/core2_for_aws**](https://docs.m5stack.com/zh_CN/core/core2_for_aws) **) ## 学习目标: A. 动手实操OpenHarmony开发 B. 学会M5Core2+OH的开发流程,熟悉事件、定时器的使用 C. 学习编程技能,开拓学生思维能力 ## 3.1 添加倒计时系统工程 - **步骤1**:在VScode最下面选择TERMINAL,终端要进入我们的工程目录,执行如下指令: ```sh #进入工程目录,hispark用户: cd /home/hispark/m5core2 hb set 【root用户: (hispark用户不需要执行下面这2条指令) cd /root/hispark/m5core2 hb set 】 ``` ![image-20231129154716393](./images/15.png) 弹出的框选择m5core2: ![image-20231129154854755](./images/16.png) - **步骤2:**在VScode最下面选择TERMINAL,终端里输入: ```sh cd /home/hispark/m5core2/kernel/liteos_m/ 【root用户: (hispark用户不需要执行下面这条指令) cd /root/hispark/m5core2/kernel/liteos_m/ 】 ``` **然后在进入的liteos_m**目录下的终端输入: ```sh make menuconfig ``` 示例: ![image-20231129155112286](./images/17.png) **在弹出的框里依次选择(键盘↓** **选择不同选项,Enter键是确认)**: ``` Platform->Board Selection->select board m5core2->use diemit m5core2 applition->m5core2 application choose->count_down_app ``` **看下面不会就看下面步骤:** **Platform** ![image-20231129155214366](./images/18.png) **Board Selection** ![image-20231129155342337](./images/19.png) **Select board m5core2** ![image-20231129155434329](./images/20.png) **按Enter**把每个都选择,前面会有*号。如果不选择第一个,开机屏幕不会显示内容。最后选择m5core2 application choose ![image-20231129155555845](./images/21.png) **count_down_app** ![image-20231129155658581](./images/22.png) **按S键保存,按两次enter确认,按Q键退出。** ## 3.2 编译倒计时系统工程 在终端中,执行如下指令编译工程: ```sh cd /home/hispark/m5core2 hb build -f 【root用户: (hispark用户不需要执行下面2条指令) cd /root/hispark/m5core2 hb build -f 】 ``` ![image-20231129165249337](./images/23.png) 如果没有操作错误,一段时间后提示编译成功: ![image-20231129165323792](./images/24.png) ## 3.3 烧录验证倒计时系统 **步骤1**:安装驱动。**目前存在两种驱动芯片版本(CP210X/CH9102),**同时安装两种驱动。 - CH9102驱动:找到【m5core2\培训资料V1.5\CH9102_VCP_SER_Windows.zip文件】, 拷贝到window桌面,右键解压后,进入解压缩的文件夹找到CH9102_VCP_SER_Windows.exe文件,右键以管理员身份运行 ![image-20231129165944739](./images/25.png) - CP210X驱动:找到【m5core2\培训资料V1.5\CP210x_VCP_Windows.zip文件】,拷贝到window桌面,右键解压后,进入解压缩的文件夹找到CP210xVCPInstaller_x64_v6.7.0.0.exe,右键以管理员身份运行。 ![image-20231129170008191](./images/26.png) **步骤2**:用Type C数据线将m5core2开发板连接到电脑USB接口上,打开Windows电脑搜索->设备管理器,查看自己设备的COM口,并记住它,后面需要使用这个COM口下载程序。操作如下图: ![image-20231129170057414](./images/27.png) **步骤3**:打开培训资料文件夹,找到flash_download_tool_3.9.2.zip文件夹,拷贝到windos桌面,进入解压后的文件夹,右键点击flash_download_tool_3.9.2.exe,选择管理员身份运行。 ![image-20231129170117726](./images/28.png) 启动程序后会弹出一个黑框,然后出现一个小框,选择ESP32,按ok进入下载界面: ![image-20231129170151705](./images/29.png) 下载界面选择要下载的文件(注意文件需要和后面的数字一一对应,不能错了),如果软件卡死,关掉再来: ![image-20231129170230626](./images/30.png) 给每个选择的文件设置分区,在后面填入对应数字(注意文件需要和后面的数字一一对应,不能错了)如下图: ![image-20231129170257054](./images/31.png) **步骤4**:左上角勾选三个文件;右下角选择步骤2 **查看到的COM口,波特率921600**。点击Start按钮下载 ![image-20231129170342148](./images/32.png) **步骤5**:按下复位键启动程序,可以看到倒计时APP运行起来了。 ![image-20231129170411068](./images/33.png) - **M5Core2**的复位键**同SD卡槽一侧** - **M5Core**的复位键**同TypeC电源线一侧** ## 3.4 拓展——修改倒计时系统 ### 3.4.1 倒计时系统源码讲解 【一、源码目录】 count_down_app源码架构说明如下,倒计时系统ui和逻辑主要是蓝色部分实现。 **count_down_app** **├──** **App** **│** **├── Common** **│** **│ └── HAL** **屏幕电源相关头文件** **├──** **HAL** **屏幕电源相关源码文件** **└──** **timercount** **倒计时程序**UI、触摸逻辑相关程序 **│** **└── screens ui**布局 **└──** **main.c** **倒计时系统程序入口** ![image-20231129170534702](./images/34.png) 【二、屏幕显示于UI交互】 /count_down_app/timercount/screens/ui_Screen1.c文件描述屏幕ui布局,ui有两个按钮、两个可滑动的时间显示组件和时间单位**。** ![image-20231129170622133](./images/35.png) 想要丰富ui设计可以修改该文件,这里不做详细讲解,可参考squareLine 设计,最导出该文件即可。参考[Tutorial 1 - Try out an example | SquareLine Studio](https://docs.squareline.io/docs/tutorials/example) - 为按钮绑定ui_event_Button1、ui_event_Button2交互事件,为滑动时间组件绑定ui_event_MinTime、ui_event_SecTime事件。当触摸屏幕上的按钮时,触发对应的回调函数。 ```C lv_obj_add_event_cb(ui_MinTime, ui_event_MinTime, LV_EVENT_ALL, NULL); lv_obj_add_event_cb(ui_SecTime, ui_event_SecTime, LV_EVENT_ALL, NULL); lv_obj_add_event_cb(ui_Button2, ui_event_Button2, LV_EVENT_ALL, NULL); lv_obj_add_event_cb(ui_Button1, ui_event_Button1, LV_EVENT_ALL, NULL); ``` - 回调函数在/count_down_app/timercount/ui.c中响应,如下代码是Start按钮的回调函数ui_event_Button1()的实现。其他ui组件类似。 ```C void ui_event_Button1(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_CLICKED) { start_ok(e); } } ``` - 当点击开始Start按钮(LV_EVENT_CLICKED事件)时,该函数会跳转到 start_ok(e)函数中处理 【三、倒计时实现】 - start_ok(e)函数在文件/count_down_app/timercount/ui_control.c中实现,主要任务是获取选择的倒计时秒数,开启定时器倒计时,并且要同步修改ui显示分钟数和秒数。程序框图如下图: ![image-20231129170653388](./images/36.png) start_ok(e)代码如下: ```c // 点击开始按钮,发送开始计时事件 void start_ok(lv_event_t *e) { // 获取到当前选择的时间 // 获取roller 选项ID button_status++; switch (button_status) { case BT_START: { ui_MinTimeID = lv_roller_get_selected(ui_MinTime); ui_SecTimeID = lv_roller_get_selected(ui_SecTime); countTime = ui_MinTimeID * 60 + ui_SecTimeID; // 获取总秒数 printf("Total count:%d\n", countTime); // 改变开始按钮内容为 Pause暂停,颜色为橙色 lv_label_set_text(ui_Label1, "Pause"); lv_obj_set_style_bg_color(ui_Button1, lv_color_hex(0xf88379), LV_STATE_DEFAULT); lv_obj_set_style_bg_color(ui_Panel1, lv_color_hex(0x2095F6), LV_PART_MAIN | LV_STATE_DEFAULT); // 发送开始计时事件 OS_Thread_EventSender(FLAGS_MSK1); break; } case BT_PAUSE: { LOS_SwtmrStop(g_timerId1); // 改变开始按钮内容为 Pause暂停,颜色为绿色 lv_label_set_text(ui_Label1, "GoOn"); lv_obj_set_style_bg_color(ui_Button1, lv_color_hex(0xbce672), LV_STATE_DEFAULT); break; } case BT_CONTINUE: { LOS_SwtmrStart(g_timerId1); // 改变开始按钮内容为 Pause暂停,颜色为橙色 lv_label_set_text(ui_Label1, "Pause"); lv_obj_set_style_bg_color(ui_Button1, lv_color_hex(0xf88379), LV_STATE_DEFAULT); button_status=BT_START; break; } defalut: { break; } } } ``` 发送开始计时事件 OS_Thread_EventSender(FLAGS_MSK1)后,在OS_Thread_EventReceiverOR(UINT32 argument)函数中接收到对应的开始计时事件,然后开启定时器计时。 ![image-20231129170739756](./images/37.png) 定时器的初始化在函数void OS_Event_example(void)中。这是一个独立线程,负责软件定时器的启停,保证UI操作的实时性与计时的准确性。创建定时器的方式如下: ```html LOS_SwtmrCreate(TICKS_100, LOS_SWTMR_MODE_PERIOD, Timer1_Callback, &g_timerId1, 1, OS_SWTMR_ROUSES_ALLOW, OS_SWTMR_ALIGN_INSENSITIVE); ``` 开启定时器后,每1s调用依次定时器回调函数。倒计时案例的回调函数中改变显示的时间,并刷新到UI界面。每倒计时60s,分钟数要减去1,具体实现如下: ``` void Timer1_Callback(UINT32 arg) // 回调函数1 { UINT32 tick_last1; g_timerCount1++; if (g_timerCount1 == countTime) // 到计时结束 { g_timerCount1 = 0; LOS_SwtmrStop(g_timerId1); printf("countTime OUT----stop Timer1 success\n"); ui_MinTimeID = 0; ui_SecTimeID = 0; lv_roller_set_selected(ui_MinTime, ui_MinTimeID, LV_ANIM_ON); lv_roller_set_selected(ui_SecTime, ui_SecTimeID, LV_ANIM_ON); lv_obj_set_style_bg_color(ui_Panel1, lv_color_hex(0xf88379), LV_PART_MAIN | LV_STATE_DEFAULT); return; } else // 刷新倒计时的 分- 秒 { ui_MinTimeID = (countTime - g_timerCount1) / 60; ui_SecTimeID = (countTime - g_timerCount1) % 60; lv_roller_set_selected(ui_MinTime, ui_MinTimeID, LV_ANIM_ON); lv_roller_set_selected(ui_SecTime, ui_SecTimeID, LV_ANIM_ON); } tick_last1 = (UINT32)LOS_TickCountGet(); // 获取当前Tick数 printf("g_timerCount1=%d, tick_last1=%d\n", g_timerCount1, tick_last1); } ``` 【四、程序编译的解释】 **BUILD**文件和**.application_config**文件配置后,让倒计时系统参与编译。打开 m5core2/vendor/m5stack/m5core2/app/count_down_app/BUILD.gn文件。查看如下内容: ```html import("//kernel/liteos_m/liteos.gni") assert(defined(LOSCFG_DRIVERS_HDF_CONFIG_MACRO), "Must Config LOSCFG_DRIVERS_HDF_CONFIG_MACRO in kernel/liteos_m menuconfig!") assert(defined(LOSCFG_DRIVERS_HDF_PLATFORM_SPI), "Must Config LOSCFG_DRIVERS_HDF_PLATFORM_SPI in kernel/liteos_m menuconfig!") module_name = get_path_info(rebase_path("."), "name") kernel_module(module_name) { sources = [ "main.c", "timercount/screens/ui_Screen1.c", "timercount/ui.c", "timercount/ui_helpers.c", "timercount/ui_control.c", #参与编译的源文件 ] lv_conf = rebase_path("${device_path}/lvgl/config/lv_conf.h") cflags = [ "-Wno-unused-variable" ] cflags += [ "-Wno-unused-but-set-variable" ] cflags += [ "-Wno-unused-parameter" ] cflags += [ "-DLV_CONF_PATH=${lv_conf}" ] cflags += ["-mlongcalls", "-std=gnu99", "-Os"] include_dirs = [ "//drivers/hdf_core/framework/include/platform/", "//drivers/hdf_core/framework/include/utils/", "//drivers/hdf_core/framework/support/platform/include/spi", "//drivers/hdf_core/adapter/khdf/liteos_m/osal/include/", "//drivers/hdf_core/framework/include/core/", "//drivers/hdf_core/framework/include/osal/", "${device_path}/drivers/power", "${device_path}/drivers/display", "${device_path}/drivers/adc_test", "//foundation/communication/wifi_lite/interfaces/wifiservice", "${device_path}/hals/driver/wifi_lite", "App", "HAL", "timercount", "//third_party/lvgl", #参与编译的源文件依赖路径 ] deps = [ "App", "HAL", ] } ``` - .application_config文件中,配置了倒计时程序的依赖,如I2C ```c config M5CORE2_APPLICATION_088 bool "count_down_app" select DRIVERS select DRIVERS_HDF select DRIVERS_HDF_PLATFORM select DRIVERS_HDF_CONFIG_MACRO select DRIVERS_HDF_PLATFORM_SPI select DRIVERS_HDF_PLATFORM_GPIO select DRIVERS_HDF_PLATFORM_I2C ``` ### 3.4.2 倒计时系统拓展实验 - **实现60min**倒计时 修改ui_Screen1.c文件中的lv_roller_set_options(ui_MinTime, "00\n01\n02\n03\n04\n05\n06\n07\n08\n09\n10", LV_ROLLER_MODE_INFINITE); ![image-20231129170913980](./images/38.png) OpenHarmony进阶学习学习:https://docs.openharmony.cn/pages/v3.1/zh-cn/device-dev/device-dev-guide.md/ 。 # 4 灌溉系统开发教程 ## 案例说明: 灌溉系统需要M5core2、TypeC数据线、LIGHT模块、ENV.Ⅲ模块、WATERING模块。实现功能:能自动获取湿度,若湿度低于设定阈值,水泵抽水灌溉;同时能自动获取温度、光照,并在屏幕上显示;提供了按钮,点击后可获取所有传感器数据,也可直接开启气泵。 ![](./images/40.png) **(详情链接:**[**https://docs.m5stack.com/zh_CN/core/core2_for_aws**](https://docs.m5stack.com/zh_CN/core/core2_for_aws) **)** ![image-20231129171052623](./images/39.png) ## 学习目标: A. 熟悉OpenHarmony开发,新建工程 B. 学会M5Core2+OH的ADC、I2C使用 ## 4.1 添加灌溉系统工程 **步骤1**:在VScode最下面选择TERMINAL,进入我们的工程目录下,例如: 在该目录下执行如下指令选择编译工程: ```sh #进入工程目录,hispark用户: cd /home/hispark/m5core2 hb set 【root用户: (hispark用户不需要执行下面这条指令) cd /root/hispark/m5core2 hb set 】 ``` ![image-20231129154716393](./images/15.png) 弹出的框选择m5core2: ![image-20231129154854755](./images/16.png) - **步骤2:**在VScode最下面选择TERMINAL,终端里输入: ```sh cd /home/hispark/m5core2/kernel/liteos_m/ 【root用户: (hispark用户不需要执行下面这条指令) cd /root/hispark/m5core2/kernel/liteos_m/ 】 ``` **然后在进入的liteos_m**目录下的终端输入: ```sh make menuconfig ``` 示例: ![image-20231129155112286](./images/17.png) **在弹出的框里依次选择(键盘↓** **选择不同选项,Enter键是确认)**: ``` Platform->Board Selection->select board m5core2->use diemit m5core2 applition->m5core2 application choose->irrigation_system_app ``` **看下面不会就看下面步骤:** **Platform** ![image-20231129155214366](./images/18.png) **Board Selection** ![image-20231129155342337](./images/19.png) **Select board m5core2** ![image-20231129155434329](./images/20.png) **按Enter**把每个都选择,前面会有*号。如果不选择第一个,开机屏幕不会显示内容。最后选择m5core2 application choose ![image-20231129155555845](./images/21.png) **irrigation_system_app** ![image-20231129171240185](./images/41.png) **按S键保存,按两次enter确认,按Q键退出。** ## 4.2 编译灌溉系统工程 在工程路径下执行指令编译工程: ```sh cd /home/hispark/m5core2 hb build -f 【root用户: (hispark用户不需要执行下面2条指令) cd /root/hispark/m5core2 hb build -f 】 ``` ![image-20231129165249337](./images/23.png) 如果没有操作错误,一段时间后提示编译成功: ![image-20231129165323792](./images/24.png) ## 4.3 烧录验证灌溉系统 **步骤1:** **按照倒计时系统案例说明,烧录,方法一致。** **步骤2:**连接各个模块。M5Core2/M5Core均有三个防反接Port口,ENV.Ⅲ接PortA, WATERING 接PortB,LIGHT接PortC,如图所示: ![image-20231129171958060](./images/42.png) - **灌溉系统连接图** 按下复位键启动程序,可以看到灌溉系统Irrigation System运行起来了。实验现象: l 湿度(Humidity)进度条显示Watering采集的当前湿度,湿度低于55%水泵自动启动;(Watering模块可插入盆摘中使用) l 光照(Light)显示LIGHT模块采集到的强度(拔掉灌溉,Light**接到portB**); l 水泵手动控制Manual Pump,开关为ON气泵一直开启,OFF关; l 温度(Temp,Temperature的缩写)单位℃,曲线为历史温度。 l 也可以点击对应按钮,手动获取数据。 ![image-20231129172031888](./images/43.png) ## 4.4 灌溉系统程序解析 **l知识点1:系统框架** ![image-20231129172116398](./images/44.png) 定义了一个MyIrrigationSys结构体描述对应的系统部件: ```c struct irrigation { float humidity; float light; float temperature; uint16_t pump_status; } MyIrrigationSys; ``` - **知识点1**:传感器数据获取与**UI**动态刷新的程序在文件m5core2/vendor/m5stack/m5core2/app/irrigation_system_app/display_sensor_data/ui_control.c中。其中Irrigation_system()函数不断地循环,adc_humidity()获取湿度、SetHumiData()刷新湿度UI,adc_light()获取光照,SetLightData()刷新光照UI; 函数i2c_temperature()获取温度, SetTepmData()熟悉温度UI; **水泵**的控制较为简单,GPIO26输出1,水泵启动,GPIO输出为0,水泵停止。函数GpioSetDir(PUMP_GPIO_26, GPIO_DIR_OUT)设置GPIO26为输出,函数**GpioWrite**(PUMP_GPIO_26, PUMP_OFF)为GPIO写入状态。 ```c static void Irrigation_system(void) { GpioSetDir(PUMP_GPIO_26, GPIO_DIR_OUT); // PUMP GPIO26 配置为输出 GpioWrite(PUMP_GPIO_26, PUMP_OFF); g_adcHandle = AdcOpen(ADC_DEVICE_NUM); if (g_adcHandle == NULL) { HDF_LOGE("AdcOpen1 fail!\r\n"); return; } for (;;) { // 湿度 adc_humidity(); SetHumiData(); // 灌溉 if ((MyIrrigationSys.humidity < HUMIDITY_REF) | (MyIrrigationSys.pump_status == PUMP_ON)) { GpioWrite(PUMP_GPIO_26, PUMP_ON); } else { GpioWrite(PUMP_GPIO_26, PUMP_OFF); } HDF_LOGE("MyIrrigationSys.humidity: %.lf\r\n", MyIrrigationSys.humidity); // 光照 需要接到portB 和 灌溉共用一个接口 adc_light(); SetLightData(); // 温度 i2c_temperature(); SetTepmData(); LOS_TaskDelay(50); /* 0.05 Seconds */ } } ``` - **知识点3**:光照和湿度都使用ADC,配合传感器将模拟量(光照、湿度)转换为数字量(具体的数值),涉及函数: ①在Irrigation_system中开启ADC,开启一次即可。函数AdcOpen(ADC_DEVICE_NUM); ADC_DEVICE_NUM是ADC通道,m5core2有两,现在使用通道1 ,示例如下: ```c g_adcHandle = AdcOpen(ADC_DEVICE_NUM); if (g_adcHandle == NULL) { HDF_LOGE("AdcOpen1 fail!\r\n"); return; } ``` ② AdcRead(g_adcHandle, ADC_CHANNEL_NUM, &g_readVal)读取ADC数据,g_adcHandle是AdcOpen得到的句柄,描述打开的adc通道,ADC_CHANNEL_NUM代表ADC1对应的通道口,即是port B的GPIO36口。应用案例湿度代码如下:(光照同理): ```c static void adc_humidity(void) { if (AdcRead(g_adcHandle, ADC_CHANNEL_NUM, &g_readVal) != HDF_SUCCESS) { HDF_LOGE("%s: read adc1 humidity fail!\r\n", __func__); AdcClose(g_adcHandle); return; } MyIrrigationSys.humidity = (float)(100 * (MAX_HUMIDITY - g_readVal) / MAX_HUMIDITY); // 百分化处理 } ``` - **知识点4**:温度使用I2C获取。流程如下: ①打开i2c:i2c_handle = I2cOpen(I2C_NUM); ②写入指令(在读取温度传感器数据之前写入单次获取数据指令0x2C06): I2cWrite(i2c_handle, 0x2C, 0x06); ③获取数据:I2cRead(i2c_handle, HIGH_ENABLED_CMD, data, BUFF_LEN),data是存储数据的字符数组,BUFF_LEN是获取数据的长度,温度传感器是6字节。 获取温度的代码如下: ```c static void i2c_temperature(void) { char data[BUFF_LEN] = {0}; i2c_handle = I2cOpen(I2C_NUM); // 打开 I2C if (i2c_handle == NULL) { printf("I2c %d Open fail!!\n", I2C_NUM); return NULL; } printf("i2c_handle %d,i2c_open succeseful----01\n", i2c_handle); float cTemp = 0; float fTemp = 0; float humidity = 0; I2cWrite(i2c_handle, 0x2C, 0x06); LOS_TaskDelay(50); // 要加延时,给传感器准备数据 if (I2cRead(i2c_handle, HIGH_ENABLED_CMD, data, BUFF_LEN) == I2C_SUCCESE) // 从寄存器地址读取6个字节 { cTemp = ((((data[0] * 256.0) + data[1]) * 175) / 65535.0) - 45; fTemp = (cTemp * 1.8) + 32; humidity = ((((data[3] * 256.0) + data[4]) * 100) / 65535.0); printf("cTemp---=%.2f\r\n", cTemp); printf("Temp---=%.2f\r\n", fTemp); printf("humidity---=%.2f\r\n", humidity); MyIrrigationSys.temperature = cTemp; } I2cClose(i2c_handle); } ``` - **知识点**5**:在ui_control**中注册灌溉系统任务 ```C void Irrigation_Task(void) { HDF_LOGE("into Irrigation system example!\n"); osThreadAttr_t attr; osThreadId_t g_taskID = NULL; attr.name = "Irrigation_system+"; attr.attr_bits = 0U; attr.cb_mem = NULL; attr.cb_size = 0U; attr.stack_mem = NULL; attr.stack_size = TASK_STACK_SIZE; attr.priority = TASK_PRIO; g_taskID = osThreadNew((osThreadFunc_t)Irrigation_system, NULL, &attr); if (g_taskID == NULL) { HDF_LOGE("Falied to create Irrigation_task thread!\n"); } } ``` - **在main.c**中启动整个灌溉系统程序。 ```c void display_test(void) { //初始化设备 DeviceInit(); lv_init(); lv_port_disp_init(); lv_port_indev_init(); printf("base_lvgl_init\n"); ui_init(); //灌溉系统 Irrigation_Task(); while (1) { LOS_TaskDelay(1); /* 1 Seconds */ lv_task_handler(); } } OHOS_APP_RUN(display_test); ``` 扩展实验一:ENV.Ⅲ模块还有一个气压传感器模块,也是i2C方式获取,可以尝试自行获取该数据。 扩展实验二:获取最近测量的10次温度数据,并绘制到UI的图表上。【目前已实现】 # 5 人体红外开发教程 ## 案例说明: 人体红外需要M5core2或M5Core、TypeC数据线、PIR Motion Unit模块。实现功能:能自动检测周围环境的红外强调,检测到人体靠近时会在屏幕上显示具体数值。 ![image-20240619031836312](./images/pirunit.png) ![image-20240619032005939](./images\motion_pit.png) ## 学习目标: A. 熟悉OpenHarmony开发,新建工程 B. 学会M5Core2+OH的ADC使用 ## 5.1 添加人体红外工程 **步骤1**:在VScode最下面选择TERMINAL,进入我们的工程目录下,例如: 在该目录下执行如下指令选择编译工程,选择m5core2。 ```sh #进入工程目录,hispark用户: cd /home/hispark/m5core2 hb set 【root用户: (hispark用户不需要执行下面这条指令) cd /root/hispark/m5core2 hb set 】 ``` **步骤2:**在VScode最下面选择TERMINAL,终端里输入: ```sh cd /home/hispark/m5core2/kernel/liteos_m/ 【root用户: (hispark用户不需要执行下面这条指令) cd /root/hispark/m5core2/kernel/liteos_m/ 】 ``` **然后在进入的liteos_m**目录下的终端输入: ```sh make menuconfig ``` 选择**pir_motion_unit** ![image-20240619032521532](./images/pir_motion_menuconfig.png) ## 5.2 编译人体红外工程 在工程路径下执行指令编译工程: ```sh cd /home/hispark/m5core2 hb build -f 【root用户: (hispark用户不需要执行下面2条指令) cd /root/hispark/m5core2 hb build -f 】 ``` ## 5.3 烧录验证人体红外工程 将PIR Motion Unit接到M5Core2或M5Core的PortB,烧录后按下复位键,即可看到如下图所示效果。可尝试遮挡/避开传感器,查看环境周围有人的概率是多少。 ![image-20240619033004684](./images/PirportB.png) ## 5.4 人体红外程序解析 主要的开发流程: 开启ADC通道---> 读取红外传感器数值--->判断是否有人接近--->刷新UI。 主要核心代码在vendor/m5stack/m5core/app/pir_motion_unit/display_pir_data/ui_control.c,解释如下: ```C #define ADC_DEVICE_NUM 1 #define ADC_CHANNEL_NUM 0 // port B GPIO36 PIR GPIO36 #define MAX_PIR 4096 // MAX_PIR sensor max adc value DevHandle i2c_handle = NULL; DevHandle g_adcHandle; uint32_t g_readVal = 0; struct PIR_Obj { float pir_value; uint16_t pir_status; } MyPIR_Obj; // 读取红外传感器数值 static void adc_pir(void) { if (AdcRead(g_adcHandle, ADC_CHANNEL_NUM, &g_readVal) != HDF_SUCCESS) { HDF_LOGE("%s: read adc1 PIR Motion fail!\r\n", __func__); AdcClose(g_adcHandle); return; } if(g_readVal>(MAX_PIR)-400) // 超过90% { MyPIR_Obj.pir_status = 1; // 有人 }else{ MyPIR_Obj.pir_status = 0; // 无人 } MyPIR_Obj.pir_value = (float)(100 * (MAX_PIR - g_readVal) / MAX_PIR); // 百分化处理 char temp_data[10] = {0}; sprintf((char *)temp_data, "%.1f", MyPIR_Obj.pir_value); // 格式化输出 浮点数转成字符串 lv_label_set_text_fmt(ui_pirValue, "%s", (const char *)temp_data); // 刷新UI } // 主要任务函数 static void PIR_motion_unit(void) { g_adcHandle = AdcOpen(ADC_DEVICE_NUM); if (g_adcHandle == NULL) { HDF_LOGE("AdcOpen1 fail!\r\n"); return; } for (;;) { // 红外值,判断是否有人 adc_pir(); LOS_TaskDelay(20); /* 0.05 Seconds */ } } // PIR_motion_unit任务入口 void PIR_Task(void) { HDF_LOGE("into PIR_Task example!\n"); .... g_taskID = osThreadNew((osThreadFunc_t)PIR_motion_unit, NULL, &attr); if (g_taskID == NULL) { HDF_LOGE("Falied to create PIR_motion_unit task thread!\n"); } } ``` # 6 GPS定位开发教程 ## 案例说明: 人体红外需要M5core2或M5Core、TypeC数据线、GPS模块。实现功能:能自动根据卫星定位,支持北斗(BDS)、GPS定位,可以获取当前的经纬度并显示到屏幕上。 ![image-20240619040601268](./images/gps_unit_pro.png) ## 学习目标: A. 熟悉OpenHarmony开发,新建工程 B. 学会M5Core2+OH的串口的使用 ## 6.1 添加GPS定位工程 **步骤1**:在VScode最下面选择TERMINAL,进入我们的工程目录下,例如: 在该目录下执行如下指令选择编译工程,选择m5core2 ```sh #进入工程目录,hispark用户: cd /home/hispark/m5core2 hb set 【root用户: (hispark用户不需要执行下面这条指令) cd /root/hispark/m5core2 hb set 】 ``` **步骤2**:在VScode最下面选择TERMINAL,终端里输入: ```sh cd /home/hispark/m5core2/kernel/liteos_m/ 【root用户: (hispark用户不需要执行下面这条指令) cd /root/hispark/m5core2/kernel/liteos_m/ 】 ``` **然后在进入的liteos_m**目录下的终端输入: ```sh make menuconfig ``` 选择**gps_unit** ![image-20240619034246632](./images/gps_unit.png) ## 6.2 GPS定位工程 在工程路径下执行指令编译工程: ```sh cd /home/hispark/m5core2 hb build -f 【root用户: (hispark用户不需要执行下面2条指令) cd /root/hispark/m5core2 hb build -f 】 ``` ## 6.3 烧录验证GPS定位工程 需要注意,GPS定位工程需要烧录自生路径下的执行文件! 需要注意,GPS定位工程需要烧录自生路径下的执行文件! 需要注意,GPS定位工程需要烧录自生路径下的执行文件! ![image-20240621235624828](./images/image-20240621235624828.png) 将GPS Unit接到M5Core2或M5Core的**PortC**,烧录后按下复位键,即可看到如下图所示效果。可尝试将设备拿到阳台或者户外场地,等待一段时间后(需要耐心等待),可获取到经纬度坐标。 ![image-20240621235654671](./images/image-20240621235654671.png) 定位成功后,倒数第四行的末尾是当前获取到的GPS时间,时间前面是坐标。有个技巧,在国内,数值小的是纬度,大的是经度。坐标数值可以在改网址([根据经纬度定位 (baidu.com)](https://lbsyun.baidu.com/jsdemo/demo/yLngLatLocation.htm))进行转换。 ![image-20240619034857548](./images/gps_my.png) ## 6.4 GPS定位程序解析 主要的开发流程: 开启串口通道---> 读取GPS数值--->解析数据,获取$GNRMC精简数据--->刷新UI。 需要注意的是,GPS模块会一直输出非常多的log,但我们只需要关心如下内容段(当定位成功后才会出现) ``` $GNRMC,084852.000,A,2236.9453,N,11408.4790,E,0.53,292.44,141216,A*75 ``` 该数据需要进行转换,解析成需要的经纬度坐标,数据格式分析如下: 纬度:ddmm.mmmm 经度:dddmm.mmmm 度分格式,换算成百度、谷歌地图的格式 ```sh 北纬 2236.9453 22+(36.9453/60)= 22.615755 东经 11408.4790 114+(08.4790/60)=114.141317 ``` 转换成度、分、秒的格式 ```sh 北纬 2236.9453 = 22 度 36 分 0.9453x60 秒 = 22 度 36 分 56.718 秒 东经 11408.4790 = 114 度 8 分 0.4790x60 秒 = 114 度 8 分 28.74 秒 ``` 主要核心代码在vendor/m5stack/m5core/app/gps_unit/display_gps_data/ui_control.c,解释如下: ```C typedef struct SaveData //存放接收数据的结构体 { char GPS_Buffer[GPS_Buffer_Length]; char isGetData; //是否获取到GPS数据 char isParseData; //是否解析完成 char UTCTime[UTCTime_Length]; //UTC时间 char latitude[latitude_Length]; //纬度 char N_S[N_S_Length]; //N/S char longitude[longitude_Length]; //经度 char E_W[E_W_Length]; //E/W char isUsefull; //定位信息是否有效 } _SaveData; // 接收数据 void recvGPS() { Res = //读取接收到的数据 if (Res == '$') { point1 = 0; } USART_RX_BUF[point1++] = Res; if (USART_RX_BUF[0] == '$' && USART_RX_BUF[4] == 'M' && USART_RX_BUF[5] == 'C') //确定是否收到"$GPRMC/$GNRMC"这一帧数据 { if (Res == '\n') { memset(Save_Data.GPS_Buffer, 0, GPS_Buffer_Length); //清空 memcpy(Save_Data.GPS_Buffer, USART_RX_BUF, point1); //保存数据 Save_Data.isGetData = true; point1 = 0; memset(USART_RX_BUF, 0, USART_REC_LEN); //清空 } } if (point1 >= USART_REC_LEN) { point1 = USART_REC_LEN; } } // 清空数据 void clrStruct() { Save_Data.isGetData = false; Save_Data.isParseData = false; Save_Data.isUsefull = false; memset(Save_Data.GPS_Buffer, 0, GPS_Buffer_Length); //清空 memset(Save_Data.UTCTime, 0, UTCTime_Length); memset(Save_Data.latitude, 0, latitude_Length); memset(Save_Data.N_S, 0, N_S_Length); memset(Save_Data.longitude, 0, longitude_Length); memset(Save_Data.E_W, 0, E_W_Length); } // 解析GPS数据 void parseGpsBuffer() { char *subString; char *subStringNext; char i = 0; if (Save_Data.isGetData) { Save_Data.isGetData = false; //printf("**************\r\n"); //printf(Save_Data.GPS_Buffer); for (i = 0 ; i <= 6 ; i++) { if (i == 0) { if ((subString = strstr(Save_Data.GPS_Buffer, ",")) == NULL) errorLog(1); //解析错误 } else { subString++; if ((subStringNext = strstr(subString, ",")) != NULL) { char usefullBuffer[2]; switch(i) { case 1:memcpy(Save_Data.UTCTime, subString, subStringNext - subString);break; //获取UTC时间 case 2:memcpy(usefullBuffer, subString, subStringNext - subString);break; //获取UTC时间 case 3:memcpy(Save_Data.latitude, subString, subStringNext - subString);break; //获取纬度信息 case 4:memcpy(Save_Data.N_S, subString, subStringNext - subString);break; //获取N/S case 5:memcpy(Save_Data.longitude, subString, subStringNext - subString);break; //获取经度信息 case 6:memcpy(Save_Data.E_W, subString, subStringNext - subString);break; //获取E/W default:break; } subString = subStringNext; Save_Data.isParseData = true; if(usefullBuffer[0] == 'A') Save_Data.isUsefull = true; else if(usefullBuffer[0] == 'V') Save_Data.isUsefull = false; } else { errorLog(2); //解析错误 } } } } } // 输出解析后的GPS信息 void printGpsBuffer() { if (Save_Data.isParseData) { Save_Data.isParseData = false; printf("Save_Data.UTCTime = "); printf(Save_Data.UTCTime); printf("\r\n"); if(Save_Data.isUsefull) { Save_Data.isUsefull = false; printf("Save_Data.latitude = "); printf(Save_Data.latitude); printf("\r\n"); printf("Save_Data.N_S = "); printf(Save_Data.N_S); printf("\r\n"); printf("Save_Data.longitude = "); printf(Save_Data.longitude); printf("\r\n"); printf("Save_Data.E_W = "); printf(Save_Data.E_W); printf("\r\n"); } else { printf("GPS DATA is not usefull!\r\n"); } } } ```