diff --git a/LYWSD03MMC-BTHome.bin b/LYWSD03MMC-BTHome.bin index 762a078..0a6dc9b 100644 Binary files a/LYWSD03MMC-BTHome.bin and b/LYWSD03MMC-BTHome.bin differ diff --git a/README.md b/README.md index 52cbc13..747e913 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # LYWSD03MMC 2025 B1.5 (pvvx B1.1) BTHome v2 beacon + LCD + OTA Port of the nRF52840/Zephyr SHT41 beacon to the Xiaomi LYWSD03MMC -(Telink TLSR8251). Same behaviour: +(Telink TLSR8251), built on the official Telink BLE SDK +([tc_ble_single_sdk](https://github.com/telink-semi/tc_ble_single_sdk) +V3.4.3.0). Same behaviour: - every 60 s: measure temperature/humidity, read the battery right after the sensor load, update the advertisement @@ -12,8 +14,9 @@ Port of the nRF52840/Zephyr SHT41 beacon to the Xiaomi LYWSD03MMC it in the scan response only, as pvvx does) - a failed sensor read keeps advertising the last good T/RH; the voltage is updated on every measurement -- Telink OTA service for over-the-air firmware updates, reachable only - during the first 2 minutes after the battery is inserted (see Flashing) +- Telink OTA service (the SDK's OTA server) for over-the-air firmware + updates, reachable only during the first 2 minutes after the battery is + inserted (see Flashing) plus the display: temperature (0.1 °C, `°C`), humidity (`%`), battery icon below 2.4 V, `-- --` until the first good reading. The LCD controller holds @@ -60,19 +63,53 @@ pvvx/ATC_MiThermometer. - The sensor is wired straight to the coin cell, so there is no power gating; the SHTV3 is put to sleep explicitly after every measurement. - Voltage is VDD (= CR2032), measured by driving the unbonded PB0 pad high - and sampling it with the ADC. Uses the typical 1175 mV reference, so expect - a few % absolute error; adjust `ADC_VREF_MV` against a meter if needed. + and sampling it with the ADC. The ADC reference is the chip's factory + calibration, which the SDK loads from the calibration sector or eFuse + (typical 1175 mV when there is none). To trim against a meter, set + `ADC_VREF_MV` to a fixed value in mV. ## Build +Builds on Linux x86-64 only (the SDK's post-build tool is a Linux x86-64 +binary). Official Telink SDK, used unmodified, plus the TC32 toolchain: + ```sh -git clone https://github.com/pvvx/ATC_MiThermometer ../ATC_MiThermometer +git clone -b release/V3.4.3.0 https://github.com/telink-semi/tc_ble_single_sdk ../tc_ble_single_sdk +# apply the official patches as tc_ble_single_sdk_v3.4.3.0_Patch/patch_*/Patch_Guide.txt +# says (copy each patch folder over tc_ble_single_sdk/, 0001 then 0002) # TC32 toolchain (Linux): e.g. https://github.com/flyskywhy/tc32 branch "linux" -ln -s /path/to/tc32 ../ATC_MiThermometer/SDK/tools/linux/tc32 -make +make TC32_PATH=/path/to/tc32/bin/ ``` -Produces `LYWSD03MMC-BTHome.bin` (with the Telink CRC appended). +`SDK_PATH` defaults to `../tc_ble_single_sdk/tc_ble_single_sdk`. The +makefile uses the same compiler flags, startup code +(`boot/B85/cstartup_825x.S`, `MCU_STARTUP_8251`: 32 KiB SRAM), linker +script (`project/tlsr_tc32/B85/boot.link`), libraries (`lt_825x`, +`lt_general_stack`) and post-build tool (`script/tl_check_fw/check_fw`) +as the SDK's own `825x_ble_sample` project. Because the SDK only +looks up `app_config.h` for its own demo projects, `src/app_config.h` is +force-included (`-include`) into every file, SDK sources included. + +Produces `LYWSD03MMC-BTHome.bin` (with the Telink CRC appended) and prints +the memory use: about 54 KiB of image, 17.8 KiB of RAM code + retained data +(so 32 KiB deep-sleep retention, as before), 21 KiB of the 32 KiB SRAM. + +### Changes from the pvvx-SDK build + +- RF/clock bring-up, calibration loading + (`blc_app_loadCustomizedParameters_normal()` / `_deepRetn()`), retention + size selection and the OTA server API are the official ones. +- The power-on marker moved from `DEEP_ANA_REG0` to `DEEP_ANA_REG1`: the + official SDK reserves bit 1 of `DEEP_ANA_REG0` for its crystal start-up + failure flag. +- OTA: inactivity timeout 16 s (`OTA_PACKET_TIMEOUT_S`), whole upload at + most 1000 s (`OTA_PROCESS_TIMEOUT_S`; the SDK default would abort after + 30 s); images up to 124 KiB (SDK limit). The OTA characteristic gained + NOTIFY and a CCC descriptor, as in the SDK sample, because the official + server answers the extended OTA protocol on it. +- Flash write protection is still cleared once per power-on (status + register := 0, as pvvx), now with the SDK's flash driver and only when a + protection bit is set. ## Flashing @@ -81,7 +118,7 @@ Produces `LYWSD03MMC-BTHome.bin` (with the Telink CRC appended). "Select Firmware" → custom file. Units running the previous build of this firmware (no OTA) need one wired flash: USB-UART adapter on the SWS pad, `TlsrComProg`/`ATCtelink.py` from the pvvx repo. -- **Updates** (this firmware → any Telink image ≤ 128 KiB, including back to +- **Updates** (this firmware → any Telink image ≤ 124 KiB, including back to pvvx or stock), with https://pvvx.github.io/ATC_MiThermometer/TelinkOTA.html: 1. Take the battery out, wait ~5 s, put it back. The BLE symbol on the LCD means the OTA window is open: for 2 minutes the device accepts @@ -97,12 +134,14 @@ Produces `LYWSD03MMC-BTHome.bin` (with the Telink CRC appended). The new image is written to the inactive flash bank and only activated after its CRC checks out, so an interrupted upload leaves the current - firmware running. The transfer aborts after 16 s without data. + firmware running. The transfer aborts after 16 s without data or after + 1000 s in total. The window opens only on a real power cycle. Reboots without one - (low-battery sleep, OTA reboot, crash) keep it closed. After installing - this firmware by OTA from pvvx firmware, power-cycle once before the next - update. + (low-battery sleep, OTA reboot, crash) keep it closed. Exception: the + first boot after installing this build by OTA from other firmware (stock, + pvvx, or the earlier pvvx-SDK build of this firmware) opens the window + once, because the marker register was never set. **OTA itself is unauthenticated**: anyone in Bluetooth range can upload firmware while the window is open, so it is limited to 2 minutes after a diff --git a/makefile b/makefile index 3551867..54feddd 100644 --- a/makefile +++ b/makefile @@ -1,87 +1,103 @@ -# LYWSD03MMC B1.5 BTHome v2 beacon + LCD +# LYWSD03MMC B1.5 BTHome v2 beacon + LCD + OTA # -# Builds against the Telink TLSR825x BLE SDK as shipped in -# pvvx/ATC_MiThermometer, reusing its startup code, linker script and -# post-build scripts: -# git clone https://github.com/pvvx/ATC_MiThermometer ../ATC_MiThermometer -# make ATC_PATH=../ATC_MiThermometer -# TC32 toolchain: $(TEL_PATH)/tools/linux/tc32/bin (or set TC32_PATH) +# Builds against the official Telink BLE SDK for TLSR825x (B85m): +# git clone -b release/V3.4.3.0 https://github.com/telink-semi/tc_ble_single_sdk ../tc_ble_single_sdk +# (apply the patches in tc_ble_single_sdk_v3.4.3.0_Patch/ as Patch_Guide.txt says) +# make TC32_PATH=/path/to/tc32/bin/ +# +# Same compiler flags, startup code, linker script, libraries and post-build +# step as the SDK's own 825x_ble_sample project (project/tlsr_tc32/B85/.cproject). +# The SDK itself is used unmodified. PROJECT_NAME ?= LYWSD03MMC-BTHome -ATC_PATH ?= ../ATC_MiThermometer -TEL_PATH ?= $(ATC_PATH)/SDK -TC32_PATH ?= $(TEL_PATH)/tools/linux/tc32/bin/ -PYTHON ?= python3 +SDK_PATH ?= ../tc_ble_single_sdk/tc_ble_single_sdk +TC32_PATH ?= SRC_PATH := ./src OUT_PATH := ./out -ATC_SRC := $(ATC_PATH)/src -ifeq ($(wildcard $(TEL_PATH)/components/drivers/8258/gpio_8258.c),) -$(error SDK not found: set ATC_PATH or TEL_PATH) +ifneq ($(MAKECMDGOALS),clean) +ifeq ($(wildcard $(SDK_PATH)/boot/B85/cstartup_825x.S),) +$(error Telink SDK not found: set SDK_PATH to the tc_ble_single_sdk/tc_ble_single_sdk directory) +endif endif -CC := $(TC32_PATH)tc32-elf-gcc -LD := $(TC32_PATH)tc32-elf-ld +CC := $(TC32_PATH)tc32-elf-gcc +LD := $(TC32_PATH)tc32-elf-ld OBJCOPY := $(TC32_PATH)tc32-elf-objcopy -NM := $(TC32_PATH)tc32-elf-nm +NM := $(TC32_PATH)tc32-elf-nm -CFLAGS := -DCHIP_TYPE=CHIP_TYPE_8258 -DMCU_STARTUP_825X \ +# TLSR8251: 32 KiB SRAM (stack top 0x848000) +STARTUP := MCU_STARTUP_8251 + +INCLUDES := -I$(SDK_PATH) -I$(SDK_PATH)/vendor/common -I$(SDK_PATH)/common \ + -I$(SDK_PATH)/drivers/B85 -I$(SRC_PATH) + +# -include: the SDK selects app_config.h only for its own demo projects +# (vendor/common/user_config.h), so ours is forced into every file +CFLAGS := -DCHIP_TYPE=CHIP_TYPE_825x -D$(STARTUP)=1 \ -ffunction-sections -fdata-sections -Wall -O2 -fpack-struct \ - -fshort-enums -finline-small-functions -std=gnu99 -funsigned-char \ + -fshort-enums -finline-small-functions -std=gnu99 \ -fshort-wchar -fms-extensions \ - -I$(TEL_PATH)/components -I$(SRC_PATH) + $(INCLUDES) -include $(SRC_PATH)/app_config.h -APP_SRCS := main.c ble.c i2c.c sensor.c battery.c lcd.c -DRV_SRCS := adc.c analog.c bsp.c clock.c flash.c gpio_8258.c i2c.c lpc.c rf_pa.c timer.c uart.c -COM_SRCS := string.c utility.c +ASFLAGS := -D$(STARTUP)=1 $(INCLUDES) -OBJS := \ - $(OUT_PATH)/boot/cstartup_825x.o \ - $(OUT_PATH)/boot/div_mod.o \ - $(addprefix $(OUT_PATH)/app/,$(APP_SRCS:.c=.o)) \ - $(addprefix $(OUT_PATH)/drv/,$(DRV_SRCS:.c=.o)) \ - $(addprefix $(OUT_PATH)/common/,$(COM_SRCS:.c=.o)) \ - $(OUT_PATH)/vendor/blt_common.o +APP_SRCS := $(wildcard $(SRC_PATH)/*.c) + +# Everything the SDK's ble_sample compiles from the SDK (minus USB/audio +# code, which this device has no use for); --gc-sections keeps only what +# is referenced +SDK_DIRS := drivers/B85 drivers/B85/driver_ext drivers/B85/flash common vendor/common +SDK_SRCS := $(foreach d,$(SDK_DIRS),$(wildcard $(SDK_PATH)/$(d)/*.c)) +SDK_ASMS := $(SDK_PATH)/boot/B85/cstartup_825x.S $(SDK_PATH)/common/div_mod.S + +OBJS := $(patsubst $(SRC_PATH)/%.c,$(OUT_PATH)/app/%.o,$(APP_SRCS)) \ + $(patsubst $(SDK_PATH)/%.c,$(OUT_PATH)/sdk/%.o,$(SDK_SRCS)) \ + $(patsubst $(SDK_PATH)/%.S,$(OUT_PATH)/sdk/%.o,$(SDK_ASMS)) + +LDSCRIPT := $(SDK_PATH)/project/tlsr_tc32/B85/boot.link +LIBS := -L$(SDK_PATH)/proj_lib -llt_825x -llt_general_stack ELF := $(OUT_PATH)/$(PROJECT_NAME).elf BIN := $(PROJECT_NAME).bin +# Post-build: the SDK's check_fw (Linux x86-64 binary) pads the image, sets +# the size and marker in the header and appends the CRC the OTA server +# checks. Copied to out/ because the SDK checkout does not mark it executable. +CHECK_FW = cp $(SDK_PATH)/script/tl_check_fw/check_fw $(OUT_PATH)/check_fw && \ + chmod +x $(OUT_PATH)/check_fw && $(OUT_PATH)/check_fw $(BIN) + all: $(BIN) $(OUT_PATH)/app/%.o: $(SRC_PATH)/%.c $(wildcard $(SRC_PATH)/*.h) @mkdir -p $(dir $@) $(CC) $(CFLAGS) -c -o $@ $< -$(OUT_PATH)/drv/%.o: $(TEL_PATH)/components/drivers/8258/%.c +$(OUT_PATH)/sdk/%.o: $(SDK_PATH)/%.c $(SRC_PATH)/app_config.h @mkdir -p $(dir $@) - $(CC) $(CFLAGS) -c -o $@ $< + @echo CC $< + @$(CC) $(CFLAGS) -c -o $@ $< -$(OUT_PATH)/common/%.o: $(TEL_PATH)/components/common/%.c +$(OUT_PATH)/sdk/%.o: $(SDK_PATH)/%.S @mkdir -p $(dir $@) - $(CC) $(CFLAGS) -c -o $@ $< + $(CC) $(ASFLAGS) -c -o $@ $< -$(OUT_PATH)/vendor/%.o: $(TEL_PATH)/components/vendor/common/%.c - @mkdir -p $(dir $@) - $(CC) $(CFLAGS) -c -o $@ $< - -$(OUT_PATH)/boot/%.o: $(ATC_SRC)/%.S - @mkdir -p $(dir $@) - $(CC) $(CFLAGS) -c -o $@ $< - -# Two-pass link: the second pass places code after the retention RAM area $(ELF): $(OBJS) - $(LD) --gc-sections -L $(TEL_PATH)/components/proj_lib -T $(ATC_SRC)/boot.link \ - -o $@ $(OBJS) -llt_8258 - $(LD) --gc-sections -Ttext `$(PYTHON) $(ATC_SRC)/TlsrRetMemAddr.py -e $@ -t $(NM)` \ - -L $(TEL_PATH)/components/proj_lib -T $(ATC_SRC)/boot.link \ - -o $@ $(OBJS) -llt_8258 - @$(PYTHON) $(ATC_SRC)/TlsrMemInfo.py -s 32768 -t $(NM) $@ + $(LD) --gc-sections -T $(LDSCRIPT) -o $@ $(OBJS) $(LIBS) + @$(NM) $@ | awk ' \ + function hex(s, i, n) { n = 0; for (i = 1; i <= length(s); i++) \ + n = n * 16 + index("0123456789abcdef", substr(tolower(s), i, 1)) - 1; return n } \ + $$3 == "_rstored_" { r = hex($$1) } \ + $$3 == "_retention_size_" { t = hex($$1) } \ + $$3 == "_bin_size_" { b = hex($$1) } \ + $$3 == "_ram_use_end_" { e = hex($$1) - 8650752 } \ + END { printf "RAM code %d B, retention (code + data) %d B -> %s deep retention, RAM used %d B of 32768, image %d B\n", \ + r, t, (t < 16384 ? "16K" : "32K"), e, b }' -# Raw image + Telink CRC (required by the OTA bootloader) $(BIN): $(ELF) $(OBJCOPY) -O binary $< $@ - $(PYTHON) $(ATC_PATH)/utils/tl_check_fw.py $@ + $(CHECK_FW) clean: rm -rf $(OUT_PATH) $(BIN) diff --git a/src/app_config.h b/src/app_config.h index bbc9d8f..4f2dac1 100644 --- a/src/app_config.h +++ b/src/app_config.h @@ -11,16 +11,18 @@ * to measure VDD (= CR2032 voltage) * PD7 LCD SPI-CLK * PB7 LCD SPI-SDI + * + * Built against the official Telink tc_ble_single_sdk. The makefile + * force-includes this file into every translation unit (-include), SDK + * sources included, because the SDK's vendor/common/user_config.h only + * knows its own demo projects. That is how gpio_init() in the SDK sees the + * PB0 settings below. */ #pragma once +/* System clock. vendor/common/app_common.h derives SYS_CLK_TYPE and + * CLOCK_SYS_CLOCK_1S/1MS/1US from this; do not define them here. */ #define CLOCK_SYS_CLOCK_HZ 24000000 -#define SYS_CLK_TYPE SYS_CLK_24M_Crystal -enum { - CLOCK_SYS_CLOCK_1S = CLOCK_SYS_CLOCK_HZ, - CLOCK_SYS_CLOCK_1MS = (CLOCK_SYS_CLOCK_1S / 1000), - CLOCK_SYS_CLOCK_1US = (CLOCK_SYS_CLOCK_1S / 1000000), -}; /* Low-power busy wait: CPU stalled, woken by timer0 */ #define pm_wait_us(t) cpu_stall_wakeup_by_timer0((t) * CLOCK_SYS_CLOCK_1US) @@ -29,6 +31,19 @@ enum { /* Variables that must survive deep-sleep retention */ #define RAM _attribute_data_retention_ +/* ---- SDK feature switches ---- */ +#define BLE_APP_PM_ENABLE 1 +#define PM_DEEPSLEEP_RETENTION_ENABLE 1 /* also feeds the boot.link check */ +#define BLE_APP_SECURITY_ENABLE 0 /* no pairing: OTA service only */ +#define APP_FLASH_PROTECTION_ENABLE 0 /* see flash_clear_write_protection() */ +#define UART_PRINT_DEBUG_ENABLE 0 +#define MODULE_WATCHDOG_ENABLE 0 + +/* No Telink evaluation board: a value that matches none of the boards in + * vendor/common/boards, so default_config.h pulls in no board pin map */ +#define BOARD_LYWSD03MMC_B15 0xB150 +#define BOARD_SELECT BOARD_LYWSD03MMC_B15 + /* ---- Application settings (equivalents of the Zephyr build) ---- */ #define DEV_NAME "LYWSD03MMC" /* CONFIG_BT_DEVICE_NAME */ #define ADV_INTERVAL 16000 /* 10000 ms / 0.625 ms */ @@ -52,12 +67,22 @@ enum { #define OTA_WINDOW_S 120 #define OTA_WINDOW_ADV_INTERVAL 1600 /* 1 s while the window is open */ +/* Power-on marker. DEEP_ANA_REG1 keeps its value through deep sleep and + * software reboots and is cleared only by a power cycle. (DEEP_ANA_REG0, + * used by the pvvx build, is reserved by the official SDK: its bit 1 is + * the crystal start-up failure flag.) */ +#define POWER_ON_FLAG_REG DEEP_ANA_REG1 +#define POWER_ON_FLAG 0x55 + /* ---- Connection / OTA (values from pvvx ble.h / app.c) ---- */ #define CONN_INTERVAL 16 /* 16 * 1.25 ms = 20 ms */ #define CONN_LATENCY 49 /* (49 + 1) * 20 ms = 1 s between radio events */ #define CONN_TIMEOUT 400 /* x10 ms = 4 s */ -#define OTA_IDLE_TIMEOUT_US (16 * 1000000) /* abort OTA after 16 s without data */ - +/* Official OTA server timeouts. The packet timeout is an inactivity + * timeout (1..20 s); the process timeout limits the whole upload + * (5..1000 s, SDK default only 30 s). */ +#define OTA_PACKET_TIMEOUT_S 16 /* abort OTA after 16 s without data */ +#define OTA_PROCESS_TIMEOUT_S 1000 /* ---- I2C ---- */ #define I2C_GROUP I2C_GPIO_GROUP_C2C3 @@ -69,7 +94,10 @@ enum { /* ---- Battery ADC ---- */ #define GPIO_VBAT GPIO_PB0 #define ADC_VBAT_CHN B0P -#define ADC_VREF_MV 1175 /* typical internal 1.2 V ref, mV */ +/* 0: use the chip's ADC reference calibration, loaded by the SDK from the + * calibration sector or eFuse (typical 1175 mV if there is none). + * Non-zero: fixed reference in mV, to trim against a meter. */ +#define ADC_VREF_MV 0 /* PB0 (not bonded) is driven high permanently, as in pvvx: no toggling * around each ADC read, and the pad never floats. Applied by gpio_init() * on every wake-up, including from deep-sleep retention. */ @@ -89,9 +117,4 @@ enum { #define GPIO_KEY GPIO_PA5 #define PULL_KEY PM_PIN_PULLUP_1M -#define MODULE_WATCHDOG_ENABLE 0 - -/* Stock Xiaomi flash layout: factory MAC address lives here */ -#define CFG_ADR_MAC 0x76000 - #include "vendor/common/default_config.h" diff --git a/src/battery.c b/src/battery.c index 840f28a..4750dbf 100644 --- a/src/battery.c +++ b/src/battery.c @@ -3,7 +3,9 @@ * kept driven high (= VDD) permanently by gpio_init() (see app_config.h, * same as pvvx) and sampled through the 1/8 prescaler against the * 1.2 V reference. 8 samples are taken, sorted, and the middle 4 averaged. - * ADC sequence follows pvvx/ATC_MiThermometer battery.c. + * ADC sequence follows pvvx/ATC_MiThermometer battery.c; the conversion + * uses the per-chip reference calibration of the official SDK (as its + * adc_sample_and_get_result() does) unless ADC_VREF_MV fixes it. */ #include "tl_common.h" #include "drivers.h" @@ -12,6 +14,11 @@ #define ADC_SAMPLES 8 +/* Set by user_calib_adc_vref() from blc_app_loadCustomizedParameters_normal() + * (drivers/B85/adc.c, retention RAM, default 1175 mV / 0 mV) */ +extern unsigned short adc_gpio_calib_vref; +extern signed char adc_gpio_calib_vref_offset; + /* ADC analog config is not kept across deep-sleep retention: set it up each time */ static void vbat_adc_setup(void) { @@ -61,8 +68,16 @@ u16 battery_read_mv(void) dfifo_disable_dfifo2(); adc_power_on_sar_adc(0); - /* mV = avg/8192 * Vref * 8 = sum4 * Vref / 4096 */ + /* mV = avg/8192 * Vref * 8 + offset = sum4 * Vref / 4096 + offset */ u32 sum = s[2] + s[3] + s[4] + s[5]; + if (!sum) { + return 0; + } +#if ADC_VREF_MV return (u16)((sum * ADC_VREF_MV + 2048) >> 12); +#else + return (u16)((s32)((sum * adc_gpio_calib_vref + 2048) >> 12) + + adc_gpio_calib_vref_offset); +#endif } diff --git a/src/ble.c b/src/ble.c index f69986f..f0374d6 100644 --- a/src/ble.c +++ b/src/ble.c @@ -7,23 +7,23 @@ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. * * OTA itself (packet handling, CRC, flash writes, swapping the boot bank - * and rebooting) is done by the Telink BLE library: otaWrite()/otaRead(). - * The new image is written to the bank that is not running (0x00000 or - * 0x20000, 128 KiB each), so a failed transfer leaves the old firmware - * bootable. + * and rebooting) is done by the OTA server of the official Telink BLE + * library (stack/ble/service/ota/ota_server.h, otaWrite()). It speaks the + * legacy Telink OTA protocol (CMD_OTA_START / data / CMD_OTA_END) used by + * TelinkOTA.html, plus the extended one. The new image is written to the + * bank that is not running (0x00000 or 0x20000, 128 KiB each; at most + * 124 KiB of firmware, the SDK default), so a failed transfer leaves the old + * firmware bootable. */ #include "tl_common.h" #include "drivers.h" #include "stack/ble/ble.h" -#include "stack/ble/service/ble_ll_ota.h" -#include "vendor/common/blt_common.h" #include "app_config.h" #include "ble.h" #include "lcd.h" -void bls_set_advertise_prepare(void *p); /* in the library, no header */ - -/* Peripheral Preferred Connection Parameters (not in this SDK's headers) */ +/* Peripheral Preferred Connection Parameters (not in the SDK headers; + * the SDK's ble_sample defines it locally too) */ typedef struct { u16 intervalMin; u16 intervalMax; @@ -46,9 +46,6 @@ RAM static u32 window_start; static void close_window(void); RAM static u8 conn_param_accepted; -RAM static u8 mac_public[6]; -static u8 mac_random_static[6]; - /* Scan response: complete local name DEV_NAME */ #define NAME_LEN (sizeof(DEV_NAME) - 1) RAM static u8 scan_rsp[2 + NAME_LEN]; @@ -74,6 +71,7 @@ enum { OTA_PS_H, /* Telink OTA */ OTA_CD_H, OTA_DP_H, + OTA_CCC_H, OTA_DESC_H, ATT_END_H, @@ -120,27 +118,21 @@ static const u8 service_change_cd[5] = { U16_LO(GATT_UUID_SERVICE_CHANGE), U16_HI(GATT_UUID_SERVICE_CHANGE) }; -/* Telink OTA */ -static const u8 ota_service_uuid[16] = TELINK_OTA_UUID_SERVICE; -static const u8 ota_data_uuid[16] = TELINK_SPP_DATA_OTA; -static u8 ota_data; +/* Telink OTA, laid out as in the SDK's ble_sample: the official OTA + * server answers on the data characteristic itself (result and version + * responses of the extended protocol), hence NOTIFY and the CCC. */ +static const u8 ota_service_uuid[16] = WRAPPING_BRACES(TELINK_OTA_UUID_SERVICE); +static const u8 ota_data_uuid[16] = WRAPPING_BRACES(TELINK_SPP_DATA_OTA); +RAM static u8 ota_data; +RAM static u8 ota_ccc[2]; static const u8 ota_name[] = { 'O', 'T', 'A' }; static const u8 ota_cd[19] = { - CHAR_PROP_READ | CHAR_PROP_WRITE_WITHOUT_RSP, + CHAR_PROP_READ | CHAR_PROP_WRITE_WITHOUT_RSP | CHAR_PROP_NOTIFY | + CHAR_PROP_WRITE, U16_LO(OTA_DP_H), U16_HI(OTA_DP_H), - 0x12, 0x2B, 0x0d, 0x0c, 0x0b, 0x0a, 0x09, 0x08, - 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00 + TELINK_SPP_DATA_OTA }; - -/* Make the library's OTA timeout an inactivity timeout (pvvx otaWritePre): - * every OTA packet restarts it. */ -static int ota_write_pre(void *p) -{ - blt_ota_start_tick = clock_time() | 1; - return otaWrite(p); -} - /* In RAM, as pvvx: the stack walks this table from interrupt context, * which must not depend on flash reads while OTA is erasing/writing it. */ RAM static attribute_t attributes[] = { @@ -159,9 +151,10 @@ RAM static attribute_t attributes[] = { { 0, ATT_PERMISSIONS_READ, 2, sizeof(service_change_val), (u8 *)&service_change_uuid, (u8 *)&service_change_val, 0, 0 }, { 0, ATT_PERMISSIONS_RDWR, 2, sizeof(service_change_ccc), (u8 *)&ccc_uuid, (u8 *)&service_change_ccc, 0, 0 }, - { 4, ATT_PERMISSIONS_READ, 2, 16, (u8 *)&primary_service_uuid, (u8 *)ota_service_uuid, 0, 0 }, + { 5, ATT_PERMISSIONS_READ, 2, 16, (u8 *)&primary_service_uuid, (u8 *)ota_service_uuid, 0, 0 }, { 0, ATT_PERMISSIONS_READ, 2, sizeof(ota_cd), (u8 *)&character_uuid, (u8 *)ota_cd, 0, 0 }, - { 0, ATT_PERMISSIONS_RDWR, 16, sizeof(ota_data), (u8 *)ota_data_uuid, &ota_data, &ota_write_pre, &otaRead }, + { 0, ATT_PERMISSIONS_RDWR, 16, sizeof(ota_data), (u8 *)ota_data_uuid, &ota_data, &otaWrite, 0 }, + { 0, ATT_PERMISSIONS_RDWR, 2, sizeof(ota_ccc), (u8 *)&ccc_uuid, ota_ccc, 0, 0 }, { 0, ATT_PERMISSIONS_READ, 2, sizeof(ota_name), (u8 *)&user_desc_uuid, (u8 *)ota_name, 0, 0 }, }; @@ -174,7 +167,6 @@ static void ota_start_cb(void) ota_active = 1; conn_param_accepted = 0; bls_pm_setManualLatency(0); - bls_ota_setTimeout(OTA_IDLE_TIMEOUT_US); } /* After a successful OTA the library reboots into the new image itself; @@ -233,12 +225,17 @@ static void suspend_exit_cb(u8 e, u8 *p, int n) void ble_init(const u8 *adv, int adv_len, int (*adv_prepare)(void *), int open_window) { - /* Factory public address (A4:C1:38:...) from the stock flash layout */ - blc_initMacAddress(CFG_ADR_MAC, mac_public, mac_random_static); + u8 mac_public[6]; + u8 mac_random_static[6]; + + /* Factory public address (A4:C1:38:...) from the stock flash layout: + * flash_sector_mac_address = 0x76000 on the 512 KiB flash */ + blc_initMacAddress(flash_sector_mac_address, mac_public, + mac_random_static); memcpy(&scan_rsp[2], DEV_NAME, NAME_LEN); scan_rsp[0] = NAME_LEN + 1; - scan_rsp[1] = GAP_ADTYPE_LOCAL_NAME_COMPLETE; + scan_rsp[1] = DT_COMPLETE_LOCAL_NAME; /* Controller */ blc_ll_initStandby_module(mac_public); @@ -246,23 +243,26 @@ void ble_init(const u8 *adv, int adv_len, int (*adv_prepare)(void *), blc_ll_initConnection_module(); blc_ll_initSlaveRole_module(); - /* Host */ + /* Host (GAP first, as the SDK requires) */ blc_gap_peripheral_init(); - bls_att_setAttributeTable((u8 *)attributes); blc_l2cap_register_handler(blc_l2cap_packet_receive); + bls_att_setAttributeTable((u8 *)attributes); blc_smp_setSecurityLevel(No_Security); + /* OTA server. Its init also erases the inactive bank if it still + * holds an old image. */ + blc_ota_initOtaServer_module(); + blc_ota_setOtaProcessTimeout(OTA_PROCESS_TIMEOUT_S); + blc_ota_setOtaDataPacketTimeout(OTA_PACKET_TIMEOUT_S); + blc_ota_registerOtaStartCmdCb(ota_start_cb); + blc_ota_registerOtaResultIndicationCb(ota_result_cb); + rf_set_power_level_index(RF_TX_POWER); bls_app_registerEventCallback(BLT_EV_FLAG_CONNECT, &connect_cb); bls_app_registerEventCallback(BLT_EV_FLAG_TERMINATE, &disconnect_cb); bls_app_registerEventCallback(BLT_EV_FLAG_SUSPEND_EXIT, &suspend_exit_cb); blc_l2cap_registerConnUpdateRspCb(conn_param_rsp_cb); - /* OTA: erase the inactive bank if it still holds an old image */ - bls_ota_clearNewFwDataArea(); - bls_ota_registerStartCmdCb(ota_start_cb); - bls_ota_registerResultIndicateCb(ota_result_cb); - /* Advertising */ bls_ll_setAdvData((u8 *)adv, adv_len); bls_ll_setScanRspData(scan_rsp, sizeof(scan_rsp)); @@ -281,19 +281,18 @@ void ble_init(const u8 *adv, int adv_len, int (*adv_prepare)(void *), close_window(); } - /* Power management (pvvx init_ble) */ + /* Power management. Thresholds as pvvx init_ble; early wake-up and + * retention size as the SDK's ble_sample: 16 KiB retention when code + * + retained data fit, else 32 KiB. */ blc_ll_initPowerManagement_module(); + blc_app_setDeepsleepRetentionSramSize(); blc_pm_setDeepsleepRetentionThreshold(40, 18); - blc_pm_setDeepsleepRetentionEarlyWakeupTiming(240); - blc_pm_setDeepsleepRetentionType(DEEPSLEEP_MODE_RET_SRAM_LOW32K); + blc_pm_setDeepsleepRetentionEarlyWakeupTiming(270); bls_pm_setSuspendMask(SUSPEND_ADV | DEEPSLEEP_RETENTION_ADV | SUSPEND_CONN | DEEPSLEEP_RETENTION_CONN); bls_pm_setManualLatency(0); } -/* Called every main-loop pass. During OTA: answer every connection event - * (no slave latency) and never use deep-sleep retention, which would drop - * the transfer state. Otherwise allow the deepest sleep everywhere. */ /* Window over: non-connectable advertising at the normal interval. In * this mode the radio does not listen after each packet. */ static void close_window(void) @@ -306,6 +305,9 @@ static void close_window(void) lcd_show_ble(0); } +/* Called every main-loop pass. During OTA: answer every connection event + * (no slave latency) and never use deep-sleep retention, which would drop + * the transfer state. Otherwise allow the deepest sleep everywhere. */ _attribute_ram_code_ int ble_task(void) { @@ -336,5 +338,6 @@ void ble_init_deep_retention(void) { rf_set_power_level_index(RF_TX_POWER); blc_ll_recoverDeepRetention(); - bls_ota_registerStartCmdCb(ota_start_cb); /* as pvvx */ + /* OTA server state and callbacks live in retention RAM in the + * official library: nothing to re-register */ } diff --git a/src/lcd.c b/src/lcd.c index 43ac035..0f80952 100644 --- a/src/lcd.c +++ b/src/lcd.c @@ -25,7 +25,6 @@ */ #include "tl_common.h" #include "drivers.h" -#include "drivers/8258/gpio_8258.h" #include "app_config.h" #include "lcd.h" diff --git a/src/main.c b/src/main.c index 88b0c5a..4a72544 100644 --- a/src/main.c +++ b/src/main.c @@ -1,6 +1,7 @@ /* * BTHome v2 temperature / humidity beacon with LCD and OTA - Xiaomi - * LYWSD03MMC B1.5 (Telink TLSR8251, Telink BLE SDK, no RTOS) + * LYWSD03MMC B1.5 (Telink TLSR8251, official Telink tc_ble_single_sdk, + * no RTOS) * * Every 60 s: measure T/RH (SHTV3) and VDD, update the segment LCD and the * BTHome v2 advertisement sent every 10 s. For OTA_WINDOW_S after the @@ -21,7 +22,6 @@ #include "tl_common.h" #include "drivers.h" #include "stack/ble/ble.h" -#include "vendor/common/blt_common.h" #include "app_config.h" #include "sensor.h" #include "battery.h" @@ -92,6 +92,22 @@ static void low_battery(u16 mv) deep_sleep_s(LOW_VBAT_SLEEP_S); } +/* The OTA server writes flash: clear block protection left by other + * firmware, once per power-on, as pvvx does. Same operation as the pvvx + * SDK's flash_unlock() (status register := 0), done with the official + * driver, but only when a protection bit is actually set. Works on any + * SPI flash, unlike the per-chip flash_prot.c, which only knows the + * flash types listed in the SDK. */ +#define FLASH_SR_PROTECT_BITS 0xFC /* BP0..BP4, SRP0 */ + +static void flash_clear_write_protection(void) +{ + if (flash_read_status(FLASH_READ_STATUS_CMD_LOWBYTE) & + FLASH_SR_PROTECT_BITS) { + flash_write_status(FLASH_TYPE_8BIT_STATUS, 0); + } +} + _attribute_ram_code_ static void store_result(s32 mc, s32 mrh) { @@ -173,6 +189,16 @@ static int adv_prepare_cb(void *p) static void user_init_normal(void) { + /* Must come before any BLE stack initialisation (official order) */ + random_generator_init(); + + /* Flash size -> sector map (MAC 0x76000, calibration 0x77000 on the + * 512 KiB part), then the factory calibration from that sector: + * crystal trim, flash supply and ADC reference. Read-only, so it is + * safe before the battery check, which needs the ADC calibration. */ + blc_readFlashSize_autoConfigCustomFlashSector(); + blc_app_loadCustomizedParameters_normal(); + blc_ll_initBasicMCU(); adc_power_on_sar_adc(0); lpc_power_down(); @@ -194,10 +220,7 @@ static void user_init_normal(void) low_battery(vbat_mv); } - /* The library writes flash for OTA: clear write protection left by - * other firmware (as pvvx, once per power-on) */ - flash_unlock(); - random_generator_init(); + flash_clear_write_protection(); #if ADV_NAME_IN_ADV memcpy(&adv[18], DEV_NAME, NAME_LEN); @@ -217,11 +240,10 @@ static void user_init_normal(void) } publish(); - /* DEEP_ANA_REG0 survives deep sleep and software reboots and is - * cleared only by a power cycle (pvvx uses it the same way) */ - int power_on = analog_read(DEEP_ANA_REG0) != 0x55; + /* Cleared only by a power cycle, see app_config.h */ + int power_on = analog_read(POWER_ON_FLAG_REG) != POWER_ON_FLAG; - analog_write(DEEP_ANA_REG0, 0x55); + analog_write(POWER_ON_FLAG_REG, POWER_ON_FLAG); bls_pm_registerAppWakeupLowPowerCb(measure_done_cb); ble_init(adv, sizeof(adv), adv_prepare_cb, power_on); @@ -231,6 +253,7 @@ static void user_init_normal(void) _attribute_ram_code_ static void user_init_deepRetn(void) { + blc_app_loadCustomizedParameters_deepRetn(); /* crystal trim, VDD_F */ blc_ll_initBasicMCU(); ble_init_deep_retention(); } @@ -280,11 +303,11 @@ int main(void) int deep_ret = pm_is_MCU_deepRetentionWakeup(); + /* Same order as the SDK's ble_sample */ + rf_drv_ble_init(); gpio_init(!deep_ret); clock_init(SYS_CLK_TYPE); - rf_drv_init(RF_MODE_BLE_1M); reg_clk_en0 = FLD_CLK0_SWIRE_EN; /* gate unused peripheral clocks */ - blc_app_loadCustomizedParameters(); /* factory crystal trim */ if (deep_ret) { user_init_deepRetn();