diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a8c2003 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python-envs.defaultEnvManager": "ms-python.python:conda", + "python-envs.defaultPackageManager": "ms-python.python:conda", + "python-envs.pythonProjects": [] +} \ No newline at end of file diff --git a/3D/外壳上部.STL b/3D/外壳上部.STL new file mode 100644 index 0000000..e2c5385 Binary files /dev/null and b/3D/外壳上部.STL differ diff --git a/3D/外壳下部.STL b/3D/外壳下部.STL new file mode 100644 index 0000000..7f2a554 Binary files /dev/null and b/3D/外壳下部.STL differ diff --git a/Keil_5/ble_app_peripheral.uvoptx b/Keil_5/ble_app_peripheral.uvoptx index abb79ea..b081216 100644 --- a/Keil_5/ble_app_peripheral.uvoptx +++ b/Keil_5/ble_app_peripheral.uvoptx @@ -140,7 +140,7 @@ 0 JL2CM3 - -U480066255 -O78 -S2 -ZTIFSpeedSel5000 -A0 -C0 -JU1 -JI127.0.0.1 -JP0 -RST0 -N00("ARM CoreSight SW-DP") -D00(0BB11477) -L00(0) -TO18 -TC10000000 -TP21 -TDS8007 -TDT0 -TDC1F -TIEFFFFFFFF -TIP8 -TB1 -TFE0 -FO7 -FD20000000 -FC1000 -FN1 -FF0NEW_DEVICE.FLM -FS00 -FL040000 -FP0($$Device:ARMCM0$Device\ARM\Flash\NEW_DEVICE.FLM) + -U20090928 -O78 -S2 -ZTIFSpeedSel5000 -A0 -C0 -JU1 -JI127.0.0.1 -JP0 -RST0 -N00("ARM CoreSight SW-DP") -D00(0BB11477) -L00(0) -TO18 -TC10000000 -TP21 -TDS8007 -TDT0 -TDC1F -TIEFFFFFFFF -TIP8 -TB1 -TFE0 -FO7 -FD20000000 -FC1000 -FN1 -FF0NEW_DEVICE.FLM -FS00 -FL040000 -FP0($$Device:ARMCM0$Device\ARM\Flash\NEW_DEVICE.FLM) 0 diff --git a/Keil_5/ble_app_peripheral.uvprojx b/Keil_5/ble_app_peripheral.uvprojx index 917ae15..244e3d6 100644 --- a/Keil_5/ble_app_peripheral.uvprojx +++ b/Keil_5/ble_app_peripheral.uvprojx @@ -10,7 +10,7 @@ DA14585 0x4 ARM-ADS - 6130001::V6.13.1::.\ARMCLANG + 6220000::V6.22::ARMCLANG 1 diff --git a/Readme.MD b/Readme.MD new file mode 100644 index 0000000..0b39040 --- /dev/null +++ b/Readme.MD @@ -0,0 +1,105 @@ +# 盒马时钟 + +--- + +![设备展示](./images/device.png) + +## 项目简介 + +本项目利用超市淘汰下来的 **盒马价签** 硬件,实现一个功能简单但实用的电子时钟,具有如下功能: + +* ✅ 显示时间与日期 +* ✅ 显示农历、节气与节假日 +* ✅ 显示电池电量 +* ✅ 蓝牙对时 +* ✅ 蓝牙 OTA 更新 + +--- + +## 编译与烧写 + +请先下载 DA14585 的 [SDK](https://www.renesas.com/en/document/swo/sdk60221401-da1453x-da145856 "SDK") 包,目前使用 **6.0.22.1401** 版本。 + +1. 将本项目放置在: + + ``` + SDK_PATH/projects/target_apps/ble_examples + ``` + +2. 使用Keil打开项目进行编译。 + +3. 编译完成后: + + * **调试模式运行一次**,固件会自动写入 Flash; + * 或者使用 **SmartSnippets Toolbox** 将固件下载到 RAM 运行一次也可。 + +--- + +## 蓝牙对时 + +项目内置页面提供 **Web Bluetooth** 功能,可用于手动对时: + +* 固件每隔 **整十分钟** 广播一次,持续 **30秒**; +* 广播状态下,屏幕显示蓝牙图标与设备名后缀; +* 打开页面点击 “连接”,选中设备后,按 “对时” 即可完成同步。 + +![控制台](./images/ctrl.png) + +首次上电会打开配对页面,扫码跳转网页可以进行配对。 + +![配对提示](./images/peiwang.png) + +--- + +## 外壳装配 + +外壳文件存储在3D目录文件下,采用推拉盖结构,推荐使用树脂打印。 + +--- + +## 关于盒马价签 + +本项目使用到的盒马价签屏幕类型如下: + +| 尺寸 | 颜色 | 屏幕连接方式 | 型号 | 主控芯片 | 分辨率 | 拆解难度 | LED | 测试点文件 | +| ------ | --- | ------ | ----------------- | ----------------- | ------- | ---- | --- | --------------- | +| 2.13 寸 | 黑白 | 焊接 | HINK-E0213A41/A55 | IL3897 / SSD1675B | 212×104 | 困难 | 有 | pinout_1/0.xlsx | +| 2.13 寸 | 黑白 | 插座 | OPM021B1 | IL3895 / SSD1673A | 250×122 | 困难 | 有 | pinout_0.xlsx | +| 2.13 寸 | 黑白红 | 焊接 | HINK-E0213A67 | IL3897 | 250×122 | 困难 | 三色 | pinout_0.xlsx | +| 2.9 寸 | 黑白红 | 插座 | HINK-E029A10 | IL3897 | 296×128 | 容易 | 有 | pinout_0.xlsx | + + +--- + +### Flash存储信息 + +#### 屏幕引脚配置(地址 `0x39000`) + +示例: + +``` +09 01 FF FF FF FF FF FF 21 22 10 01 20 07 11 23 + CS ?? RST CLK SDI DC BUSY PWR +``` + +* 第一个字节 `09`:屏幕类型 +* 第二个字节 `01`:引脚配置启用标志(非 `01` 表示无效) + +#### 屏幕分辨率等信息(地址 `0x3a000`) + +示例: + +``` +00 25 00 00 92 fa a8 fe 00 01 80 00 28 01 04 00 + 0080 0128 128x296 BWR +``` + +--- + +## 其他说明 + +* 原版价签固件存放在 Flash 的 SUOTA 区域; +* 大多数价签使用 OTP 启动器,但会从 Flash 继续加载固件; +* Flash 中包含屏幕和引脚配置,因此新固件无需硬编码这些信息; +* 原生固件无法被蓝牙扫描到,因此难以通过 OTA 更新; +* 价签多数电池电量不足,因此建议拆机替换供电。 diff --git a/Readme.txt b/Readme.txt deleted file mode 100644 index dc5f250..0000000 --- a/Readme.txt +++ /dev/null @@ -1,81 +0,0 @@ - -盒马时钟 --------- - -此项目利用超市淘汰下来的价签的硬件,实现一个简单的时钟: - 显示时间与日期 - 显示农历与节气和节假日 - 显示电池电量 - 蓝牙对时 - 蓝牙OTA - - -编译与烧写 ----------- - -请先下载DA14585的SDK包。目前使用的版本是6.0.22.1401。 -将本项目放置在SDK_PATH/projects/target_apps/ble_examples下面,然后打开项目编译即可。 -编译完成后以调试模式运行一次,固件会自动写入Flash中。 -或者使用SmartSnippets Toolbox将固件下载到RAM中运行一次即可。 - - -蓝牙对时 --------- - -这里使用web bluetooth实现了一个简单的网页来设置时间。 -为了省电,固件每隔整十分钟广播一次,持续半分钟。广播时,屏幕会显示蓝牙图标和设备名的后缀。 -此时点击页面上的"连接"按钮,在弹出的页面选择对应的设备即可连接上。再点"对时"按钮完成对时。 - - -关于盒马价签 ------------- - -我用过的有三种: - 2.13寸黑白 - 第一种: - 屏是直接焊接到主板上的,型号: HINK-E0213A41/A55, 主控IL3897,分辨率212x104。 - 另外,这种屏有少数用的主控是SSD1675B。这两种主控的LUT格式是不一样的。 - 很难无损拆解,需从后盖处拆起。带一个LED空位。 - 此种型号有两种电路板: - 5个测试点: pinout_1.xlsx - 6个测试点: pinout_0.xlsx - - 第二种: - 屏通过插座连接到主板上,型号: OPM021B1, 主控IL3895/SSD1673A,分辨率250x122。 - 这种主控貌似没有内部OTP,需要写入LUT才能工作。 - 很难无损拆解,需从后盖处拆起。带一个LED空位。 - 此种型号的电路板: - 6个测试点: pinout_0.xlsx - - 2.13寸黑白红: - 屏是直接焊接到主板上的,型号: HINK-E0213A67,主控IL3897,分辨率250x122。 - 很难无损拆解,需从面板处拆起。自带一个三色LED。 - 6个测试点: pinout_0.xlsx - - 2.9 寸黑白红: pinout_0.xlsx - 屏通过插座连接到主板上,型号: HINK-E029A10, 主控IL3897,分辨率296x128。 - 这个尺寸的价签比较好拆,卡扣结构。带一个LED空位。 - 6个测试点: pinout_0.xlsx - -这批价签看来都是从OTP启动的。但OTP里面放的只是一个二级BootLoader,还是会从Flash加载APP启动的。 -Flash中的固件符合SUOTA格式。 - -Flash的0x39000处存放有墨水屏所使用的IO的信息: - - 09 01 FF FF FF FF FF FF 21 22 10 01 20 07 11 23 - CS ?? RST CLK SDI DC BUSY PWR - 第一个字节"09"是所用屏的类别。固件内置了十几种屏的驱动,根据这里的类别来选择。 - 第二个字节"01"指示后面有IO的配置。非01的值则忽略后面的配置。 - - -Flash的0x3a000处存放有墨水屏的分辨率等信息: - - 00 25 00 00 92 fa a8 fe 00 01 80 00 28 01 04 00 - 0080 0128 128x296 BWR - 40 1f 00 00 f0 70 18 01 00 01 7a 00 fa 00 fc 07 - 007a 00fa 122x250 BWR - c4 0a 00 00 1a 4f ae 5a 00 00 68 00 d4 00 04 00 - 0068 00d4 104x212 BW - -原版的固件,不知道什么原因,无法用蓝牙搜索到。否则可以无损更新固件了(但大多数价签的电池都是没电的,还是得拆开)。 - diff --git a/images/ctrl.png b/images/ctrl.png new file mode 100644 index 0000000..2a257ad Binary files /dev/null and b/images/ctrl.png differ diff --git a/images/device.png b/images/device.png new file mode 100644 index 0000000..1cb81f9 Binary files /dev/null and b/images/device.png differ diff --git a/images/peiwang.png b/images/peiwang.png new file mode 100644 index 0000000..d07c2d0 Binary files /dev/null and b/images/peiwang.png differ diff --git a/src/epd/epd.h b/src/epd/epd.h index e11eba0..24424d5 100644 --- a/src/epd/epd.h +++ b/src/epd/epd.h @@ -74,6 +74,13 @@ int fb_draw_font_info(int x, int y, const u8 *font_data, int color); int fb_draw_font(int x, int y, int ucs, int color); void fb_test(void); +void draw_qr_code( + int start_x, + int start_y, + int pix_size, + const unsigned char img[31][4] +); + void select_layout(int xres, int yres); diff --git a/src/epd/epd_gui.c b/src/epd/epd_gui.c index b5d707a..f8fff37 100644 --- a/src/epd/epd_gui.c +++ b/src/epd/epd_gui.c @@ -14,7 +14,6 @@ int fb_h; u8 fb_bw[FB_SIZE]; u8 fb_rr[FB_SIZE]; - /******************************************************************************/ @@ -100,6 +99,41 @@ void draw_box(int x1, int y1, int x2, int y2, int color) } } + + +/** + * 绘制二维码到墨水屏 + * + * @param start_x 绘制起始X位置 + * @param start_y 绘制起始Y位置 + * @param pix_size 每个二维码像素点的宽高(单位像素) + * @param img 二维码C数组,尺寸31x4,每行4字节 + */ +void draw_qr_code( + int start_x, + int start_y, + int pix_size, + const unsigned char img[31][4] +) { + for (int y = 0; y < 31; y++) { + for (int x = 0; x < 31; x++) { + + int byte_idx = x / 8; // 每4字节一行,8bit一个字节 + int bit_idx = 7 - (x % 8); // 图像从高位到低位存储 + int bit = (img[y][byte_idx] >> bit_idx) & 1; + int color = (bit == 0) ? WHITE : BLACK; + + // 放大绘制 + int x1 = start_x + x * pix_size; + int y1 = start_y + y * pix_size; + int x2 = x1 + pix_size - 1; + int y2 = y1 + pix_size - 1; + + draw_box(x1, y1, x2, y2, color); + } + } +} + /******************************************************************************/ diff --git a/src/user_custs1_impl.c b/src/user_custs1_impl.c index d6e68d6..4594c7b 100644 --- a/src/user_custs1_impl.c +++ b/src/user_custs1_impl.c @@ -69,6 +69,7 @@ static uint8_t h24_format = 1; // 24小时制标志 static void get_holiday(void); +extern int adv_state; /* * FUNCTION DEFINITIONS **************************************************************************************** @@ -191,6 +192,77 @@ int hour=0, minute=0, second=0; int cal_minute=-1; +//GUIQRLB +// 来自 pic.py 自动生成的 C 数组 +const unsigned char QR_31x31[31][4] = { + {0x00, 0x00, 0x00, 0x00}, + {0x7F, 0x1E, 0x99, 0xFC}, + {0x41, 0x3B, 0xC5, 0x04}, + {0x5D, 0x5D, 0x6D, 0x74}, + {0x5D, 0x11, 0x9D, 0x74}, + {0x5D, 0x63, 0xD1, 0x74}, + {0x41, 0x43, 0x59, 0x04}, + {0x7F, 0x55, 0x55, 0xFC}, + {0x00, 0x06, 0xB0, 0x00}, + {0x25, 0x43, 0x96, 0xD0}, + {0x74, 0x55, 0x2C, 0x74}, + {0x7B, 0xB1, 0x22, 0xC4}, + {0x5E, 0xB4, 0xE2, 0xE4}, + {0x53, 0xBA, 0x6E, 0x98}, + {0x72, 0x54, 0xE1, 0xF4}, + {0x0B, 0xC8, 0xD5, 0x1C}, + {0x32, 0xEA, 0xCD, 0x20}, + {0x7B, 0x13, 0xCC, 0x4C}, + {0x4A, 0xC0, 0x1A, 0x9C}, + {0x1B, 0x55, 0xB5, 0x7C}, + {0x1A, 0x8E, 0xF5, 0x54}, + {0x77, 0xBD, 0x27, 0xE0}, + {0x00, 0x6B, 0xDC, 0x74}, + {0x7F, 0x0A, 0xED, 0x54}, + {0x41, 0x1B, 0x3C, 0x64}, + {0x5D, 0x55, 0x9F, 0xE4}, + {0x5D, 0x3E, 0x48, 0x38}, + {0x5D, 0x2B, 0x4E, 0x0C}, + {0x41, 0x60, 0x20, 0x2C}, + {0x7F, 0x0D, 0xB0, 0xC8}, + {0x00, 0x00, 0x00, 0x00}, +}; + +const unsigned char LB_31x31[31][4] = { + {0x00, 0x00, 0x00, 0x00}, + {0x00, 0x00, 0x00, 0x00}, + {0x00, 0x07, 0xC0, 0x00}, + {0x00, 0x04, 0x40, 0x00}, + {0x00, 0xFF, 0xFE, 0x00}, + {0x00, 0x80, 0x02, 0x00}, + {0x01, 0x80, 0x03, 0x00}, + {0x01, 0x00, 0x01, 0x00}, + {0x01, 0x00, 0x01, 0x00}, + {0x01, 0x03, 0x81, 0x00}, + {0x01, 0x03, 0x81, 0x00}, + {0x01, 0x03, 0x81, 0x00}, + {0x01, 0x03, 0x81, 0x00}, + {0x01, 0x03, 0x81, 0x00}, + {0x01, 0x03, 0x81, 0x00}, + {0x01, 0x03, 0x81, 0x00}, + {0x01, 0x03, 0x81, 0x00}, + {0x01, 0x03, 0x81, 0x00}, + {0x01, 0x00, 0x01, 0x00}, + {0x01, 0x00, 0x01, 0x00}, + {0x01, 0x03, 0x81, 0x00}, + {0x01, 0x03, 0x81, 0x00}, + {0x01, 0x03, 0x81, 0x00}, + {0x01, 0x00, 0x01, 0x00}, + {0x01, 0x00, 0x01, 0x00}, + {0x01, 0xC0, 0x07, 0x00}, + {0x00, 0x40, 0x04, 0x00}, + {0x00, 0x7F, 0xFC, 0x00}, + {0x00, 0x00, 0x00, 0x00}, + {0x00, 0x00, 0x00, 0x00}, + {0x00, 0x00, 0x00, 0x00}, +}; + + /** * 获取农历月份的天数 * @@ -593,6 +665,7 @@ static uint8_t batt_cal(uint16_t adc_sample) return batt_lvl; } + /** * 绘制电池电量图标 * @@ -722,6 +795,56 @@ static void epd_wait_timer(void) } } + +void QR_draw() +{ + // 此处添加QR码绘制逻辑 + epd_hw_open(); + + epd_update_mode(UPDATE_FULL); + + memset(fb_bw, 0xff, scr_h*line_bytes); + memset(fb_rr, 0x00, scr_h*line_bytes); + + draw_qr_code(5, 5, 3, QR_31x31); + draw_text(100, 5,"Bluetooth", BLACK); + draw_text(100, 20, "DLG-CLOCK ", BLACK); + draw_text(170,20,bt_id,BLACK); + + draw_text(110,40,"-------------",BLACK); + + draw_text(100, 60, "Scan the QR code", BLACK); + draw_text(100, 75, "with your browser", BLACK); + // 墨水屏更新显示 + epd_init(); + epd_screen_update(); + epd_update(); + // 更新时如果深度休眠,会花屏。 这里暂时关闭休眠。 + arch_set_sleep_mode(ARCH_SLEEP_OFF); + epd_wait_hnd = app_easy_timer(40, epd_wait_timer); +} + +void LB_draw() +{ + // 此处添加低电压码的绘制逻辑 + epd_hw_open(); + + epd_update_mode(UPDATE_FULL); + + memset(fb_bw, 0xff, scr_h*line_bytes); + memset(fb_rr, 0x00, scr_h*line_bytes); + + draw_qr_code(60, 10, 4, LB_31x31); + + // 墨水屏更新显示 + epd_init(); + epd_screen_update(); + epd_update(); + // 更新时如果深度休眠,会花屏。 这里暂时关闭休眠。 + arch_set_sleep_mode(ARCH_SLEEP_OFF); + epd_wait_hnd = app_easy_timer(40, epd_wait_timer); +} + /** * 绘制时钟界面 * diff --git a/src/user_custs1_impl.h b/src/user_custs1_impl.h index d98267f..8b3e0d7 100644 --- a/src/user_custs1_impl.h +++ b/src/user_custs1_impl.h @@ -84,7 +84,8 @@ void clock_print(void); void clock_set(uint8_t *buf); void clock_push(void); void clock_draw(int full); - +void QR_draw(void); +void LB_draw(void); /** **************************************************************************************** diff --git a/src/user_peripheral.c b/src/user_peripheral.c index 42d4136..1d61e21 100644 --- a/src/user_peripheral.c +++ b/src/user_peripheral.c @@ -3,30 +3,11 @@ * * @file user_peripheral.c * - * @brief Peripheral project source code. + * @brief 外设项目源代码文件,主要实现BLE外设功能、时钟管理、EPD屏幕显示及OTP数据读取等功能 * * Copyright (C) 2015-2023 Renesas Electronics Corporation and/or its affiliates. * All rights reserved. Confidential Information. * - * This software ("Software") is supplied by Renesas Electronics Corporation and/or its - * affiliates ("Renesas"). Renesas grants you a personal, non-exclusive, non-transferable, - * revocable, non-sub-licensable right and license to use the Software, solely if used in - * or together with Renesas products. You may make copies of this Software, provided this - * copyright notice and disclaimer ("Notice") is included in all such copies. Renesas - * reserves the right to change or discontinue the Software at any time without notice. - * - * THE SOFTWARE IS PROVIDED "AS IS". RENESAS DISCLAIMS ALL WARRANTIES OF ANY KIND, - * WHETHER EXPRESS, IMPLIED, OR STATUTORY, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. TO THE - * MAXIMUM EXTENT PERMITTED UNDER LAW, IN NO EVENT SHALL RENESAS BE LIABLE FOR ANY DIRECT, - * INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE, EVEN IF RENESAS HAS BEEN ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGES. USE OF THIS SOFTWARE MAY BE SUBJECT TO TERMS AND CONDITIONS CONTAINED IN - * AN ADDITIONAL AGREEMENT BETWEEN YOU AND RENESAS. IN CASE OF CONFLICT BETWEEN THE TERMS - * OF THIS NOTICE AND ANY SUCH ADDITIONAL LICENSE AGREEMENT, THE TERMS OF THE AGREEMENT - * SHALL TAKE PRECEDENCE. BY CONTINUING TO USE THIS SOFTWARE, YOU AGREE TO THE TERMS OF - * THIS NOTICE.IF YOU DO NOT AGREE TO THESE TERMS, YOU ARE NOT PERMITTED TO USE THIS - * SOFTWARE. * **************************************************************************************** */ @@ -39,90 +20,86 @@ */ /* - * INCLUDE FILES + * 包含头文件 **************************************************************************************** */ -#include "rwip_config.h" // SW configuration -#include "gattc_task.h" -#include "gap.h" -#include "app_easy_timer.h" -#include "user_peripheral.h" -#include "user_custs1_impl.h" -#include "user_custs1_def.h" -#include "co_bt.h" -#include "hw_otpc.h" +#include "rwip_config.h" // 软件配置 +#include "gattc_task.h" // GATT客户端任务相关定义 +#include "gap.h" // GAP层相关定义 +#include "app_easy_timer.h" // 应用层定时器功能 +#include "user_peripheral.h" // 本文件接口声明 +#include "user_custs1_impl.h" // 自定义服务1实现 +#include "user_custs1_def.h" // 自定义服务1定义 +#include "co_bt.h" // 蓝牙协议协议相关定义 +#include "hw_otpc.h" // OTP控制器硬件接口 -#include "epd.h" +#include "epd.h" // EPD电子纸屏幕驱动 /* - * TYPE DEFINITIONS + * 类型定义 **************************************************************************************** */ /* - * GLOBAL VARIABLE DEFINITIONS + * 全局变量定义 **************************************************************************************** */ -int app_connection_idx __SECTION_ZERO("retention_mem_area0"); -timer_hnd app_clock_timer_used __SECTION_ZERO("retention_mem_area0"); //@RETENTION MEMORY -timer_hnd app_param_update_request_timer_used __SECTION_ZERO("retention_mem_area0"); +int app_connection_idx __SECTION_ZERO("retention_mem_area0"); // 连接索引,使用 retention 内存区域保存 +timer_hnd app_clock_timer_used __SECTION_ZERO("retention_mem_area0"); // 时钟定时器句柄,retention内存保存 +timer_hnd app_param_update_request_timer_used __SECTION_ZERO("retention_mem_area0"); // 参数更新请求定时器句柄,retention内存保存 -static int adv_state; -static int otp_btaddr[2]; -static int otp_boot; -static char adv_name[20]; -char *bt_id = adv_name+12; -int clock_interval; -int clock_fixup_value; -int clock_fixup_count; +int adv_state = 0; // 广播状态:0-未广播,1-正在广播 +static int otp_btaddr[2]; // 从OTP读取的蓝牙地址 +static int otp_boot; // 从OTP读取的启动相关数据 +static char adv_name[20]; // 广播名称缓冲区 +char *bt_id = adv_name+12; // 蓝牙ID在广播名称中的起始位置 +int clock_interval; // 时钟更新间隔(秒) +int clock_fixup_value; // 时钟修正值 +int clock_fixup_count; // 时钟修正计数器 +// EPD版本信息(volatile确保不被优化,用于版本检测) const volatile u32 epd_version[3] = {0xF9A51379, ~0xF9A51379, EPD_VERSION}; +extern int year,month; // 当前时间变量 /* - * FUNCTION DEFINITIONS + * 函数定义 **************************************************************************************** */ /** **************************************************************************************** - * @brief Add an AD structure in the Advertising or Scan Response Data of the - * GAPM_START_ADVERTISE_CMD parameter struct. - * @param[in] cmd GAPM_START_ADVERTISE_CMD parameter struct - * @param[in] ad_struct_data AD structure buffer - * @param[in] ad_struct_len AD structure length - * @param[in] adv_connectable Connectable advertising event or not. It controls whether - * the advertising data use the full 31 bytes length or only - * 28 bytes (Document CCSv6 - Part 1.3 Flags). + * @brief 在GAPM_START_ADVERTISE_CMD参数结构的广播或扫描响应数据中添加AD结构 + * @param[in] cmd GAPM_START_ADVERTISE_CMD参数结构 + * @param[in] ad_struct_data AD结构数据缓冲区 + * @param[in] ad_struct_len AD结构长度 + * @param[in] adv_connectable 是否为可连接广播事件,控制广播数据最大长度(可连接时28字节,否则31字节) **************************************************************************************** */ static void app_add_ad_struct(struct gapm_start_advertise_cmd *cmd, void *ad_struct_data, uint8_t ad_struct_len, uint8_t adv_connectable) { + // 根据是否可连接确定广播数据最大长度 uint8_t adv_data_max_size = (adv_connectable) ? (ADV_DATA_LEN - 3) : (ADV_DATA_LEN); + // 优先添加到广播数据 if ((adv_data_max_size - cmd->info.host.adv_data_len) >= ad_struct_len) { - // Append manufacturer data to advertising data memcpy(&cmd->info.host.adv_data[cmd->info.host.adv_data_len], ad_struct_data, ad_struct_len); - // Update Advertising Data Length cmd->info.host.adv_data_len += ad_struct_len; - } + // 广播数据空间不足时添加到扫描响应数据 else if ((SCAN_RSP_DATA_LEN - cmd->info.host.scan_rsp_data_len) >= ad_struct_len) { - // Append manufacturer data to scan response data memcpy(&cmd->info.host.scan_rsp_data[cmd->info.host.scan_rsp_data_len], ad_struct_data, ad_struct_len); - // Update Scan Response Data Length cmd->info.host.scan_rsp_data_len += ad_struct_len; - } + // 空间不足时触发断言警告 else { - // Manufacturer Specific Data do not fit in either Advertising Data or Scan Response Data ASSERT_WARNING(0); } } @@ -130,27 +107,36 @@ static void app_add_ad_struct(struct gapm_start_advertise_cmd *cmd, void *ad_str /** **************************************************************************************** - * @brief Parameter update request timer callback function. + * @brief 参数更新请求定时器回调函数 + * 当定时器超时,发起连接参数更新请求 **************************************************************************************** */ static void param_update_request_timer_cb() { - app_easy_gap_param_update_start(app_connection_idx); - app_param_update_request_timer_used = EASY_TIMER_INVALID_TIMER; + app_easy_gap_param_update_start(app_connection_idx); // 发起参数更新 + app_param_update_request_timer_used = EASY_TIMER_INVALID_TIMER; // 重置定时器句柄 } +/** + **************************************************************************************** + * @brief 读取OTP(一次性可编程)存储器中的值 + * 主要读取蓝牙地址和启动信息,并生成广播名称 + **************************************************************************************** + */ static void read_otp_value(void) { - hw_otpc_init(); - hw_otpc_manual_read_on(false); + hw_otpc_init(); // 初始化OTP控制器 + hw_otpc_manual_read_on(false); // 关闭手动读取模式 - otp_boot = *(u32*)(0x07f8fe00); - otp_btaddr[0] = *(u32*)(0x07f8ffa8); - otp_btaddr[1] = *(u32*)(0x07f8ffac); + // 从OTP特定地址读取数据 + otp_boot = *(u32*)(0x07f8fe00); // 读取启动相关数据 + otp_btaddr[0] = *(u32*)(0x07f8ffa8); // 读取蓝牙地址低32位 + otp_btaddr[1] = *(u32*)(0x07f8ffac); // 读取蓝牙地址高32位 - hw_otpc_disable(); + hw_otpc_disable(); // 禁用OTP控制器 + // 处理蓝牙地址,生成设备唯一标识 u32 ba0 = otp_btaddr[0]; u32 ba1 = otp_btaddr[1]; @@ -158,221 +144,344 @@ static void read_otp_value(void) ba0 &= 0x00ffffff; ba0 ^= ba1; + // 生成广播名称(格式:DLG-CLOCK-XXYYZZ,XXYYZZ为蓝牙地址后三段) u8 *ba = (u8*)&ba0; sprintf(adv_name+2, "DLG-CLOCK-%02x%02x%02x", ba[2], ba[1], ba[0]); int name_len = strlen(adv_name+2); + // 如果设备名称未设置,则使用生成的名称 if(device_info.dev_name.length==0){ device_info.dev_name.length = name_len; memcpy(device_info.dev_name.name, adv_name+2, name_len); } + // 构造AD结构:第一个字节为长度,第二个字节为AD类型(完整名称) adv_name[0] = name_len+1; adv_name[1] = GAP_AD_TYPE_COMPLETE_NAME; } +// 外部声明的区域表基地址(用于内存相关操作) extern int Region$$Table$$Base; +/** + **************************************************************************************** + * @brief 应用初始化函数 + * 初始化OTP数据、定时器、屏幕、蓝牙等模块 + **************************************************************************************** + */ void user_app_init(void) { - read_otp_value(); + read_otp_value(); // 读取OTP数据,初始化广播名称 printk("\n\nuser_app_init! %s %08x\n", __TIME__, epd_version[2]); - app_param_update_request_timer_used = EASY_TIMER_INVALID_TIMER; - app_clock_timer_used = EASY_TIMER_INVALID_TIMER; + app_param_update_request_timer_used = EASY_TIMER_INVALID_TIMER; // 初始化参数更新定时器 + app_clock_timer_used = EASY_TIMER_INVALID_TIMER; // 初始化时钟定时器 - clock_interval = 60; // 60s - clock_fixup_value = 0; - clock_fixup_count = 0; + clock_interval = 60; // 时钟更新间隔设置为60秒 + clock_fixup_value = 0; // 初始化时钟修正值 + clock_fixup_count = 0; // 初始化时钟修正计数器 - adv_state = 0; - fspi_config(0x00030605); + adv_state = 0; // 初始化为未广播状态 + fspi_config(0x00030605); // 配置FSPI接口 - selflash(otp_boot); + selflash(otp_boot); // 根据OTP启动数据执行自闪存操作 - epd_hw_init(0x23200700, 0x05210006, detect_w, detect_h, detect_mode | ROTATE_3); // 2.13黑白屏,6个测试点 - if(epd_detect()==0){ - epd_hw_init(0x23111000, 0x07210120, detect_w, detect_h, detect_mode | ROTATE_3); // 2.13黑白屏,5个测试点 + // 初始化EPD屏幕(2.13黑白屏,6个测试点) + epd_hw_init(0x23200700, 0x05210006, detect_w, detect_h, detect_mode | ROTATE_3); + if(epd_detect()==0){ // 如果检测不到屏幕,尝试另一种配置(5个测试点) + epd_hw_init(0x23111000, 0x07210120, detect_w, detect_h, detect_mode | ROTATE_3); epd_detect(); } - - app_connection_idx = -1; - default_app_on_init(); + app_connection_idx = -1; // 初始化连接索引为无效值 + default_app_on_init(); // 执行默认应用初始化 } -// 时钟快慢修正 -// minutes : 自上次对时后,经过的分钟数 -// diff_sec: 自上次对时后,误差的秒数 -// 误差为正数,说明时钟快了。将定时器加上一个修正值,让它慢一点。 -// 误差为负数,说明时钟慢了。将定时器减去一个修正值,让它快一点。 +/** + **************************************************************************************** + * @brief 时钟快慢修正函数 + * @param[in] diff_sec 自上次对时后的误差秒数(正数表示快了,负数表示慢了) + * @param[in] minutes 自上次对时后经过的分钟数 + * @note 计算并累积时钟修正值,用于调整定时器间隔,补偿时钟误差 + **************************************************************************************** + */ void clock_fixup_set(int diff_sec, int minutes) { + // 计算新的修正值(基于4096精度的分数计算) int new_fixup_value = diff_sec*100*4096/minutes; - clock_fixup_value += new_fixup_value; + clock_fixup_value += new_fixup_value; // 累积修正值 } +/** + **************************************************************************************** + * @brief 应用时钟修正值 + * @return 本次需要调整的毫秒数 + * @note 从累积的修正计数器中提取整数部分作为本次调整值,保留余数 + **************************************************************************************** + */ static int clock_fixup(void) { int value; - clock_fixup_count += clock_fixup_value; + clock_fixup_count += clock_fixup_value; // 累积修正计数 - value = clock_fixup_count>>12; - clock_fixup_count &= 0xfff; + value = clock_fixup_count>>12; // 右移12位(除以4096)得到整数部分 + clock_fixup_count &= 0xfff; // 保留低12位作为余数 - return value; + return value; // 返回本次调整的毫秒数 } - +extern int adcval; // ADC电压值变量 +/** + **************************************************************************************** + * @brief 应用时钟定时器回调函数 + * 定时更新时钟、推送时钟数据、处理屏幕显示,并根据需要重启广播 + **************************************************************************************** + */ static void app_clock_timer_cb(void) { - int adj = clock_fixup(); + int adj = clock_fixup(); // 获取时钟修正值 + // 重启定时器,应用修正后的间隔(单位:10ms,故乘以100) app_clock_timer_used = app_easy_timer(clock_interval*100+adj, app_clock_timer_cb); + // 确定屏幕更新标志(根据时钟状态) + int flags = UPDATE_FLY; // 默认快速更新 + // 更新时钟并打印 int stat = clock_update(clock_interval); clock_print(); + // 如果已连接,则推送时钟数据 if(app_connection_idx!=-1){ clock_push(); } + + //未进行初始化,则始终展示二维码 + if(year==2025 && month<=5){ + // 在2024年2月执行特定操作(占位符) + QR_draw(); + user_app_adv_start();//持续开启广播 + return; + } - int flags = UPDATE_FLY; - if(stat>=3){ - flags = DRAW_BT | UPDATE_FULL; - }else if(stat>=2){ - flags = DRAW_BT | UPDATE_FAST; - } - + // 如果是快速更新,更新ADC数据,若电量不足则不继续执行任务 if(flags==4){ adc1_update(); + //ADC电压小于2.6V + if(adcval<1360){ + //绘制低电量图标 + LB_draw(); + //清除定时器唤醒任务 + app_easy_timer_cancel(app_clock_timer_used); + return; + } } + if(stat>=3){ + flags = DRAW_BT | UPDATE_FULL; // 需要蓝牙图标+全量更新 + }else if(stat>=2){ + flags = DRAW_BT | UPDATE_FAST; // 需要蓝牙图标+快速更新 + } + + // 如果需要显示蓝牙图标,启动广播 if(flags&DRAW_BT){ user_app_adv_start(); } + // 根据状态或标志更新屏幕显示 if(stat>0 || flags&DRAW_BT){ clock_draw(flags); } } +/** + **************************************************************************************** + * @brief 重启应用时钟定时器 + **************************************************************************************** + */ void app_clock_timer_restart(void) { - app_easy_timer_cancel(app_clock_timer_used); + app_easy_timer_cancel(app_clock_timer_used); // 取消当前定时器 + // 以默认间隔重启定时器 app_clock_timer_used = app_easy_timer(clock_interval*100, app_clock_timer_cb); } +/** + **************************************************************************************** + * @brief 数据库初始化完成回调函数 + * 当GATT数据库初始化完成后调用,初始化ADC、显示时钟并启动广播 + **************************************************************************************** + */ void user_app_on_db_init_complete( void ) { printk("\nuser_app_on_db_init_complete!\n"); + // 更新ADC值并打印电压 int adcval = adc1_update(); printk("Voltage: %d\n", adcval); + // 打印并推送时钟数据 clock_print(); clock_push(); - clock_draw(DRAW_BT|UPDATE_FULL); + // 绘制时钟(带蓝牙图标+全量更新)并启动广播 + //clock_draw(DRAW_BT|UPDATE_FULL); + QR_draw(); user_app_adv_start(); + // 启动时钟定时器 app_clock_timer_used = app_easy_timer(clock_interval*100, app_clock_timer_cb); } +/** + **************************************************************************************** + * @brief 启动应用广播 + * 构造广播数据(包含设备名称和EPD版本),并启动带超时的无向广播 + **************************************************************************************** + */ void user_app_adv_start(void) { - u8 vbuf[4]; + u8 vbuf[4]; // 版本信息AD结构缓冲区 + // 如果已在广播状态,直接返回 if(adv_state) return; - adv_state = 1; + adv_state = 1; // 标记为正在广播 - struct gapm_start_advertise_cmd* cmd = app_easy_gap_undirected_advertise_get_active(); + // 获取广播命令结构 + struct gapm_start_advertise_cmd* cmd = app_easy_gap_undirected_advertise_get_active(); + // 添加设备名称AD结构 app_add_ad_struct(cmd, adv_name, adv_name[0]+1, 1); + // 构造版本信息AD结构(长度+类型+版本号低两位) vbuf[0] = 0x03; vbuf[1] = GAP_AD_TYPE_MANU_SPECIFIC_DATA; vbuf[2] = EPD_VERSION&0xff; vbuf[3] = (EPD_VERSION>>8)&0xff; app_add_ad_struct(cmd, vbuf, vbuf[0]+1, 1); - - //default_advertise_operation(); - //app_easy_gap_undirected_advertise_start(); + // 启动带超时的无向广播 app_easy_gap_undirected_advertise_with_timeout_start(user_default_hnd_conf.advertise_period, NULL); printk("\nuser_app_adv_start! %s\n", adv_name+2); } +/** + **************************************************************************************** + * @brief 连接事件回调函数 + * 当收到连接请求时调用,更新连接索引,检查连接参数并在需要时请求参数更新 + * @param[in] connection_idx 连接索引 + * @param[in] param 连接请求参数 + **************************************************************************************** + */ void user_app_connection(uint8_t connection_idx, struct gapc_connection_req_ind const *param) { printk("user_app_connection: %d\n", connection_idx); + // 检查连接是否有效 if (app_env[connection_idx].conidx != GAP_INVALID_CONIDX) { - app_connection_idx = connection_idx; + app_connection_idx = connection_idx; // 更新连接索引 + // 打印连接参数 printk(" interval: %d\n", param->con_interval); printk(" latency : %d\n", param->con_latency); printk(" sup_to : %d\n", param->sup_to); - // Check if the parameters of the established connection are the preferred ones. - // If not then schedule a connection parameter update request. + + // 检查连接参数是否符合预期,不符合则调度参数更新请求 if ((param->con_interval < user_connection_param_conf.intv_min) || (param->con_interval > user_connection_param_conf.intv_max) || (param->con_latency != user_connection_param_conf.latency) || (param->sup_to != user_connection_param_conf.time_out)) { - // Connection params are not these that we expect app_param_update_request_timer_used = app_easy_timer(APP_PARAM_UPDATE_REQUEST_TO, param_update_request_timer_cb); } + // 推送时钟数据到客户端 clock_push(); } else { - adv_state = 0; + adv_state = 0; // 连接无效时,标记为未广播 } + // 执行默认连接处理 default_app_on_connection(connection_idx, param); } +/** + **************************************************************************************** + * @brief 无向广播完成回调函数 + * 当广播超时或异常结束时调用,更新广播状态并刷新屏幕 + * @param[in] status 广播结束状态码 + **************************************************************************************** + */ void user_app_adv_undirect_complete(uint8_t status) { printk("user_app_adv_undirect_complete: %02x\n", status); + // 状态非0表示异常结束,更新广播状态并刷新屏幕 if(status!=0){ adv_state = 0; + //未进行初始化,则始终展示二维码 + if(year==2025 && month<=5){ + // 在2024年2月执行特定操作(占位符) + QR_draw(); + } + else clock_draw(UPDATE_FLY); } } +/** + **************************************************************************************** + * @brief 断开连接回调函数 + * 当连接断开时调用,清理定时器,更新连接状态,并根据断开原因决定是否重启广播 + * @param[in] param 断开连接参数(包含断开原因) + **************************************************************************************** + */ void user_app_disconnect(struct gapc_disconnect_ind const *param) { printk("user_app_disconnect! reason=%02x\n", param->reason); - // Cancel the parameter update request timer + // 取消参数更新请求定时器 if (app_param_update_request_timer_used != EASY_TIMER_INVALID_TIMER) { app_easy_timer_cancel(app_param_update_request_timer_used); app_param_update_request_timer_used = EASY_TIMER_INVALID_TIMER; } - app_connection_idx = -1; - adv_state = 0; + app_connection_idx = -1; // 重置连接索引为无效值 + adv_state = 0; // 标记为未广播 + // 非远程用户主动断开时,重启广播;否则仅刷新屏幕 if(param->reason!=CO_ERROR_REMOTE_USER_TERM_CON){ - // 非主动断开连接时, 重新广播. user_app_adv_start(); }else{ + //未进行初始化,则始终展示二维码 + if(year==2025 && month<=5){ + // 在2024年2月执行特定操作(占位符) + QR_draw(); + } + else clock_draw(UPDATE_FLY); } } +/** + **************************************************************************************** + * @brief 未处理消息的捕获处理函数 + * 处理各类未被默认处理的消息,包括特征值读写、参数更新、MTU变更等事件 + * @param[in] msgid 消息ID + * @param[in] param 消息参数 + * @param[in] dest_id 目标任务ID + * @param[in] src_id 源任务ID + **************************************************************************************** + */ void user_catch_rest_hndl(ke_msg_id_t const msgid, void const *param, ke_task_id_t const dest_id, @@ -380,12 +489,12 @@ void user_catch_rest_hndl(ke_msg_id_t const msgid, { switch(msgid) { + // 特征值写入通知(值已写入数据库) case CUSTS1_VAL_WRITE_IND: { - /* 写特征值通知. 值已经写入Database中了. */ - //printk("CUSTS1_VAL_WRITE_IND!\n"); struct custs1_val_write_ind const *msg_param = (struct custs1_val_write_ind const *)(param); + // 根据句柄分发到对应的处理函数 switch (msg_param->handle) { case SVC1_IDX_CONTROL_POINT_VAL: @@ -401,22 +510,22 @@ void user_catch_rest_hndl(ke_msg_id_t const msgid, } } break; + // Notification确认(请求已发出) case CUSTS1_VAL_NTF_CFM: { - /* Notification确认. 请求已经发出. */ } break; + // Indication确认(请求已发出) case CUSTS1_VAL_IND_CFM: { - /* Indication确认. 请求已经发出. */ } break; + // 读ATT_INFO请求(需要返回数据) case CUSTS1_ATT_INFO_REQ: { - /* 读ATT_INFO请求. 需要返回数据. */ - //printk("CUSTS1_ATT_INFO_REQ!\n"); struct custs1_att_info_req const *msg_param = (struct custs1_att_info_req const *)param; + // 根据属性索引分发处理 switch (msg_param->att_idx) { case SVC1_IDX_LONG_VALUE_VAL: @@ -429,16 +538,17 @@ void user_catch_rest_hndl(ke_msg_id_t const msgid, } } break; + // 连接参数更新通知 case GAPC_PARAM_UPDATED_IND: { - // Cast the "param" pointer to the appropriate message structure struct gapc_param_updated_ind const *msg_param = (struct gapc_param_updated_ind const *)(param); printk("GAPC_PARAM_UPDATED_IND!\n"); + // 打印更新后的参数 printk(" interval: %d\n", msg_param->con_interval); printk(" latency : %d\n", msg_param->con_latency); printk(" sup_to : %d\n", msg_param->sup_to); - // Check if updated Conn Params filled to preferred ones + // 检查更新后的参数是否符合预期 if ((msg_param->con_interval >= user_connection_param_conf.intv_min) && (msg_param->con_interval <= user_connection_param_conf.intv_max) && (msg_param->con_latency == user_connection_param_conf.latency) && @@ -448,51 +558,48 @@ void user_catch_rest_hndl(ke_msg_id_t const msgid, } } break; + // 特征值读取请求 case CUSTS1_VALUE_REQ_IND: { - /* 读特征值. 什么时候会有这个事件? */ printk("CUSTS1_VALUE_REQ_IND!\n"); struct custs1_value_req_ind const *msg_param = (struct custs1_value_req_ind const *) param; + // 处理未定义的读取请求,返回错误 switch (msg_param->att_idx) { default: { - // Send Error message struct custs1_value_req_rsp *rsp = KE_MSG_ALLOC(CUSTS1_VALUE_REQ_RSP, src_id, dest_id, custs1_value_req_rsp); - // Provide the connection index. rsp->conidx = app_env[msg_param->conidx].conidx; - // Provide the attribute index. rsp->att_idx = msg_param->att_idx; - // Force current length to zero. rsp->length = 0; - // Set Error status rsp->status = ATT_ERR_APP_ERROR; - // Send message KE_MSG_SEND(rsp); } break; } } break; + // GATT事件请求指示(确认未处理的指示以避免超时) case GATTC_EVENT_REQ_IND: { - // Confirm unhandled indication to avoid GATT timeout struct gattc_event_ind const *ind = (struct gattc_event_ind const *) param; struct gattc_event_cfm *cfm = KE_MSG_ALLOC(GATTC_EVENT_CFM, src_id, dest_id, gattc_event_cfm); cfm->handle = ind->handle; KE_MSG_SEND(cfm); } break; + // MTU(最大传输单元)变更指示 case GATTC_MTU_CHANGED_IND: { struct gattc_mtu_changed_ind *ind = (struct gattc_mtu_changed_ind *) param; printk("GATTC_MTU_CHANGED_IND: %d\n", ind->mtu); } break; + // 未处理的消息 default: { printk("Unhandled msgid=%08x\n", msgid); @@ -500,4 +607,4 @@ void user_catch_rest_hndl(ke_msg_id_t const msgid, } } -/// @} APP +/// @} APP \ No newline at end of file