Compare commits

...

8 Commits

Author SHA1 Message Date
tpu
57fa8d58b8 更新readme与版本号 2025-11-06 09:46:44 +08:00
tpunix
6d55de390e Merge pull request #6 from 5Breeze/master
增加了上电配对提示以及低电量提示图像,修改了readme的格式,增加了装配外壳的3D模型
2025-11-06 09:13:36 +08:00
5breeze
fd8defe77a add readme 2025-11-05 23:49:25 +08:00
bitshen
82d3b47111 Update Readme.MD 2025-11-05 23:42:23 +08:00
bitshen
e75505dd4a Rename Readme.txt to Readme.MD 2025-11-05 23:42:02 +08:00
bitshen
ec1fd3d3f4 Update Readme.txt 2025-11-05 23:41:27 +08:00
5breeze
a364d530ac 增加了蓝牙配置提示以及低电量提醒 2025-11-05 23:26:21 +08:00
5breeze
db808f006c add qr and low bat 2025-11-05 13:24:00 +08:00
16 changed files with 533 additions and 223 deletions

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"python-envs.defaultEnvManager": "ms-python.python:conda",
"python-envs.defaultPackageManager": "ms-python.python:conda",
"python-envs.pythonProjects": []
}

BIN
3D/外壳上部.STL Normal file

Binary file not shown.

BIN
3D/外壳下部.STL Normal file

Binary file not shown.

View File

@@ -140,7 +140,7 @@
<SetRegEntry>
<Number>0</Number>
<Key>JL2CM3</Key>
<Name>-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)</Name>
<Name>-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)</Name>
</SetRegEntry>
<SetRegEntry>
<Number>0</Number>

View File

@@ -10,7 +10,7 @@
<TargetName>DA14585</TargetName>
<ToolsetNumber>0x4</ToolsetNumber>
<ToolsetName>ARM-ADS</ToolsetName>
<pCCUsed>6130001::V6.13.1::.\ARMCLANG</pCCUsed>
<pCCUsed>6220000::V6.22::ARMCLANG</pCCUsed>
<uAC6>1</uAC6>
<TargetOption>
<TargetCommonOption>

114
Readme.MD Normal file
View File

@@ -0,0 +1,114 @@
# 盒马时钟
---
![设备展示](./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 更新;
* 价签多数电池电量不足,因此建议拆机替换供电。

View File

@@ -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
原版的固件,不知道什么原因,无法用蓝牙搜索到。否则可以无损更新固件了(但大多数价签的电池都是没电的,还是得拆开)。

BIN
images/ctrl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

BIN
images/device.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

BIN
images/peiwang.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

View File

@@ -55,7 +55,7 @@
****************************************************************************************
*/
#define EPD_VERSION 0xA50f0006
#define EPD_VERSION 0xA50f0007
/*

View File

@@ -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);

View File

@@ -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);
}
}
}
/******************************************************************************/

View File

@@ -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);
}
/**
* 绘制时钟界面
*

View File

@@ -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);
/**
****************************************************************************************

View File

@@ -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-XXYYZZXXYYZZ为蓝牙地址后三段
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();
}
int flags = UPDATE_FLY;
if(stat>=3){
flags = DRAW_BT | UPDATE_FULL;
}else if(stat>=2){
flags = DRAW_BT | UPDATE_FAST;
//未进行初始化,则始终展示二维码
if(year==2025 && month<=5){
// 在2024年2月执行特定操作占位符
QR_draw();
user_app_adv_start();//持续开启广播
return;
}
// 如果是快速更新更新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();
// 添加设备名称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);