Initial Commit

This commit is contained in:
SSAnkur committed 2026-10-17 20:39:01 +06:00
commit 07f75ad6a5
15 files changed
+1439

No files matched your search

Binary file not shown.
+109
View File
@@ -0,0 +1,109 @@
# LYWSD03MMC 2025 B1.5 (HW B1.1) BTHome v2 beacon + LCD + OTA
Port of the nRF52840/Zephyr SHT41 beacon to the Xiaomi LYWSD03MMC
(Telink TLSR8251). Same behaviour:
- every 60 s: measure temperature/humidity, read the battery right after the
sensor load, update the advertisement
- BTHome v2 advertisement every 10 s, public (factory) address,
objects 0x0C voltage (mV), 0x2E humidity (%), 0x45 temperature (0.1 °C)
- name `LYWSD03MMC` in the advertisement and the scan response, so passive
scanners (ESPHome `active: false`) see it too (`ADV_NAME_IN_ADV 0` keeps
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)
plus the display: temperature (0.1 °C, `°C`), humidity (`%`), battery icon
below 2.4 V, `-- --` until the first good reading. The LCD controller holds
the image itself and is only written when a value changes.
## Power saving (same methods as pvvx/ATC_MiThermometer)
- No wake-ups of its own: a measurement starts right after every 6th
advertising event, while the chip is awake anyway.
- The CPU sleeps during the ~12 ms sensor conversion (pvvx
`SENSOR_SLEEP_MEASURE`); an app wake-up timer collects the result.
- VDD is sampled while the sensor converts, i.e. under load.
- Deep-sleep retention between events, also while connected: the device
requests 20 ms interval / latency 49, so it wakes once per second.
- PB0 (battery probe) is driven high permanently; sensor I2C at 700 kHz;
RF power restored after every suspend.
- Battery protection: below 2.2 V at power-on, or below 2.0 V while running,
the LCD shows the voltage and `Lo` and the device deep-sleeps for 3 min
before any flash write or radio use, then retries.
Outside the 2-minute OTA window the advertising is non-connectable, as in
the original build: the radio does not listen after each packet.
## Hardware: 2025 "B1.5" only
This build supports **only the LYWSD03MMC sold since 2025 with "B1.5"
printed on the PCB** (next to the battery holder), whose LCD controller is
driven over bit-banged SPI. pvvx firmware / TelinkMiFlasher report this
board as **HW B1.1**, which is the reliable way to tell it apart.
| Board | Sensor | LCD controller |
|-------|--------------------|-----------------------------------|
| "B1.5" 2025 (pvvx B1.1) | SHTV3 (SHTC3) 0x70 | SPI, PD7 CLK / PB7 SDI, write-only |
There is no auto-detection. On any other unit (original UART-LCD B1.5,
B1.4, B1.6, B1.7, B1.9, B2.0) the display stays blank and/or the sensor read
fails: do not flash it there.
Pinout, segment map and the LCD frame format come from
pvvx/ATC_MiThermometer.
## Differences from the nRF build
- 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.
## Build
```sh
git clone https://github.com/pvvx/ATC_MiThermometer ../ATC_MiThermometer
# TC32 toolchain (Linux): e.g. https://github.com/flyskywhy/tc32 branch "linux"
ln -s /path/to/tc32 ../ATC_MiThermometer/SDK/tools/linux/tc32
make
```
Produces `lywsd03mmc_b15_bthome.bin` (with the Telink CRC appended).
## Flashing
- **First install** (from stock Xiaomi or pvvx firmware, which have OTA):
https://pvvx.github.io/ATC_MiThermometer/TelinkMiFlasher.html → connect →
"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
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
connections and advertises every second.
2. TelinkOTA.html → connect → pick `LYWSD03MMC` → select the `.bin` →
start. The upload must start within the 2 minutes; once started it may
run past them.
3. The device reboots into the new firmware. If the window closes or the
upload fails, start again from step 1.
All units are named `LYWSD03MMC`; only the unit you just power-cycled is
connectable, so it is the only one the page can connect to.
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.
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.
**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
battery swap, which needs physical access.
+89
View File
@@ -0,0 +1,89 @@
# LYWSD03MMC B1.5 BTHome v2 beacon + LCD
#
# 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)
PROJECT_NAME ?= LYWSD03MMC-BTHome
ATC_PATH ?= ../ATC_MiThermometer
TEL_PATH ?= $(ATC_PATH)/SDK
TC32_PATH ?= $(TEL_PATH)/tools/linux/tc32/bin/
PYTHON ?= python3
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)
endif
CC := $(TC32_PATH)tc32-elf-gcc
LD := $(TC32_PATH)tc32-elf-ld
OBJCOPY := $(TC32_PATH)tc32-elf-objcopy
NM := $(TC32_PATH)tc32-elf-nm
CFLAGS := -DCHIP_TYPE=CHIP_TYPE_8258 -DMCU_STARTUP_825X \
-ffunction-sections -fdata-sections -Wall -O2 -fpack-struct \
-fshort-enums -finline-small-functions -std=gnu99 -funsigned-char \
-fshort-wchar -fms-extensions \
-I$(TEL_PATH)/components -I$(SRC_PATH)
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
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
ELF := $(OUT_PATH)/$(PROJECT_NAME).elf
BIN := $(PROJECT_NAME).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
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c -o $@ $<
$(OUT_PATH)/common/%.o: $(TEL_PATH)/components/common/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -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) $@
# Raw image + Telink CRC (required by the OTA bootloader)
$(BIN): $(ELF)
$(OBJCOPY) -O binary $< $@
$(PYTHON) $(ATC_PATH)/utils/tl_check_fw.py $@
clean:
rm -rf $(OUT_PATH) $(BIN)
.PHONY: all clean
+97
View File
@@ -0,0 +1,97 @@
/*
* Board / build configuration: Xiaomi LYWSD03MMC, hardware B1.5 ONLY
* as sold since 2025 (pvvx reports it as HW B1.1): TLSR8251F512ET24,
* SHTV3 sensor, SPI-driven LCD controller
*
* Pin map (from pvvx/ATC_MiThermometer reverse engineering):
* PC2 I2C SCL (SHTV3 sensor, 0x70), 10k pull-up
* PC3 I2C SDA, 10k pull-up
* PB6 LCD controller pin, must be pulled up (10k)
* PB0 not bonded on this package: driven high and sampled by the ADC
* to measure VDD (= CR2032 voltage)
* PD7 LCD SPI-CLK
* PB7 LCD SPI-SDI
*/
#pragma once
#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)
#define pm_wait_ms(t) cpu_stall_wakeup_by_timer0((t) * CLOCK_SYS_CLOCK_1MS)
/* Variables that must survive deep-sleep retention */
#define RAM _attribute_data_retention_
/* ---- Application settings (equivalents of the Zephyr build) ---- */
#define DEV_NAME "LYWSD03MMC" /* CONFIG_BT_DEVICE_NAME */
#define ADV_INTERVAL 16000 /* 10000 ms / 0.625 ms */
#define ADV_INTERVAL_DELAY 10 /* + up to 6.25 ms, as pvvx */
#define MEAS_EVERY_N_ADV 6 /* measure every 6th adv = 60 s */
#define MEAS_INTERVAL_S ((ADV_INTERVAL * MEAS_EVERY_N_ADV) / 1600)
#define RF_TX_POWER RF_POWER_P0p04dBm /* CONFIG_BT_CTLR_TX_PWR_0 */
#define LOW_BATT_MV 2400 /* show battery icon below this */
/* Local name in the advertising packet as well as the scan response.
* 1: passive scanners (e.g. ESPHome with active: false) see the name.
* 0 (pvvx default): name only in the scan response; every advertising
* packet is 12 bytes shorter, but only active scanners see the name. */
#define ADV_NAME_IN_ADV 1
/* ---- OTA window ----
* Connections (and with them OTA) are only accepted for OTA_WINDOW_S after
* the battery is inserted. Then the device switches to non-connectable
* advertising until the next power cycle. A reboot without a power cycle
* (low-battery sleep, OTA reboot, crash) does not reopen the window. */
#define OTA_WINDOW_S 120
#define OTA_WINDOW_ADV_INTERVAL 1600 /* 1 s while the window is open */
/* ---- 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 */
/* ---- I2C ---- */
#define I2C_GROUP I2C_GPIO_GROUP_C2C3
#define I2C_SCL GPIO_PC2
#define I2C_SDA GPIO_PC3
#define I2C_SPEED_HZ 700000 /* as pvvx I2C_MAX_SPEED; SHTC3 allows 1 MHz */
#define PULL_I2C PM_PIN_PULLUP_10K
/* ---- Battery ADC ---- */
#define GPIO_VBAT GPIO_PB0
#define ADC_VBAT_CHN B0P
#define ADC_VREF_MV 1175 /* typical internal 1.2 V ref, mV */
/* 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. */
#define PB0_INPUT_ENABLE 0
#define PB0_OUTPUT_ENABLE 1
#define PB0_DATA_OUT 1
#define PB0_FUNC AS_GPIO
/* ---- LCD ---- */
#define GPIO_LCD_PB6 GPIO_PB6
#define PULL_LCD_PB6 PM_PIN_PULLUP_10K
#define GPIO_LCD_CLK GPIO_PD7 /* SPI clock to the LCD controller */
#define GPIO_LCD_SDI GPIO_PB7 /* SPI data to the LCD controller */
#define PULL_LCD_SER PM_PIN_PULLUP_1M
/* Unused "reset" pad: keep it from floating */
#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"
+68
View File
@@ -0,0 +1,68 @@
/*
* VDD measurement. PB0 is not bonded out on the TLSR8251 package; it is
* 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.
*/
#include "tl_common.h"
#include "drivers.h"
#include "app_config.h"
#include "battery.h"
#define ADC_SAMPLES 8
/* ADC analog config is not kept across deep-sleep retention: set it up each time */
static void vbat_adc_setup(void)
{
adc_power_on_sar_adc(0);
adc_set_sample_clk(5);
adc_set_left_right_gain_bias(GAIN_STAGE_BIAS_PER100, GAIN_STAGE_BIAS_PER100);
adc_set_chn_enable_and_max_state_cnt(ADC_MISC_CHN, 2);
adc_set_state_length(240, 0, 10);
analog_write(anareg_adc_res_m, RES14 | FLD_ADC_EN_DIFF_CHN_M);
adc_set_ain_chn_misc(ADC_VBAT_CHN, GND);
adc_set_ref_voltage(ADC_MISC_CHN, ADC_VREF_1P2V);
adc_set_tsample_cycle_chn_misc(SAMPLING_CYCLES_6);
adc_set_ain_pre_scaler(ADC_PRESCALER_1F8);
}
_attribute_ram_code_
u16 battery_read_mv(void)
{
volatile u32 dat[ADC_SAMPLES];
u16 s[ADC_SAMPLES];
u8 rp = 0;
vbat_adc_setup();
adc_power_on_sar_adc(1);
adc_reset_adc_module();
for (int i = 0; i < ADC_SAMPLES; i++) {
dat[i] = 0;
}
adc_config_misc_channel_buf((u16 *)dat, sizeof(dat));
dfifo_enable_dfifo2();
for (int i = 0; i < ADC_SAMPLES; i++) {
while (rp == reg_dfifo2_wptr) {
}
rp = reg_dfifo2_wptr;
/* 14-bit result, bit 13 = sign in differential mode */
u16 v = (dat[i] & BIT(13)) ? 0 : (dat[i] & 0x1FFF);
int j = i - 1;
while (j >= 0 && s[j] > v) { /* insertion sort */
s[j + 1] = s[j];
j--;
}
s[j + 1] = v;
}
dfifo_disable_dfifo2();
adc_power_on_sar_adc(0);
/* mV = avg/8192 * Vref * 8 = sum4 * Vref / 4096 */
u32 sum = s[2] + s[3] + s[4] + s[5];
return (u16)((sum * ADC_VREF_MV + 2048) >> 12);
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include "tl_common.h"
/* Low-battery protection, thresholds as pvvx (battery.h, CR2032 build):
* below BOOT_VBAT_MV at power-on the device sleeps again before touching
* flash or the radio; below END_VBAT_MV while running it shows "Lo" and
* sleeps for LOW_VBAT_SLEEP_S, then retries. */
#define BOOT_VBAT_MV 2200
#define END_VBAT_MV 2000
#define LOW_VBAT_SLEEP_S 180
u16 battery_read_mv(void); /* VDD (= CR2032) in mV */
+340
View File
@@ -0,0 +1,340 @@
/*
* BLE peripheral: connectable advertising, GATT server with the Telink OTA
* service, and the connection / OTA power handling of
* pvvx/ATC_MiThermometer (ble.c, app_att.c), MIT-style license:
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software ... to deal in the Software without restriction.
* 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.
*/
#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) */
typedef struct {
u16 intervalMin;
u16 intervalMax;
u16 latency;
u16 timeout;
} gap_periConnectParams_t;
/* ----- link-layer buffers (required by the library; sizes as pvvx) ----- */
RAM u8 blt_rxfifo_b[64 * 8] = { 0 };
RAM my_fifo_t blt_rxfifo = { 64, 8, 0, 0, blt_rxfifo_b, };
RAM u8 blt_txfifo_b[40 * 16] = { 0 };
RAM my_fifo_t blt_txfifo = { 40, 16, 0, 0, blt_txfifo_b, };
RAM u8 ble_connected;
RAM u8 ota_active;
RAM u8 ota_window;
RAM static u32 window_start;
#define OTA_WINDOW_TICKS ((u32)OTA_WINDOW_S * CLOCK_16M_SYS_TIMER_CLK_1S)
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];
/* ============================ GATT table ============================ */
enum {
ATT_H_START = 0,
GAP_PS_H, /* 0x1800 Generic Access */
GAP_DEVNAME_CD_H,
GAP_DEVNAME_DP_H,
GAP_APPEAR_CD_H,
GAP_APPEAR_DP_H,
GAP_CONNPAR_CD_H,
GAP_CONNPAR_DP_H,
GATT_PS_H, /* 0x1801 Generic Attribute */
GATT_SC_CD_H,
GATT_SC_DP_H,
GATT_SC_CCC_H,
OTA_PS_H, /* Telink OTA */
OTA_CD_H,
OTA_DP_H,
OTA_DESC_H,
ATT_END_H,
};
static const u16 primary_service_uuid = GATT_UUID_PRIMARY_SERVICE;
static const u16 character_uuid = GATT_UUID_CHARACTER;
static const u16 ccc_uuid = GATT_UUID_CLIENT_CHAR_CFG;
static const u16 user_desc_uuid = GATT_UUID_CHAR_USER_DESC;
/* GAP */
static const u16 gap_service_uuid = SERVICE_UUID_GENERIC_ACCESS;
static const u16 devname_uuid = GATT_UUID_DEVICE_NAME;
static const u16 appearance_uuid = GATT_UUID_APPEARANCE;
static const u16 connpar_uuid = GATT_UUID_PERI_CONN_PARAM;
static const u16 appearance = 768; /* Generic Thermometer */
static const gap_periConnectParams_t periConnParameters = {
CONN_INTERVAL, CONN_INTERVAL, CONN_LATENCY, CONN_TIMEOUT
};
static const u8 devname_cd[5] = {
CHAR_PROP_READ,
U16_LO(GAP_DEVNAME_DP_H), U16_HI(GAP_DEVNAME_DP_H),
U16_LO(GATT_UUID_DEVICE_NAME), U16_HI(GATT_UUID_DEVICE_NAME)
};
static const u8 appear_cd[5] = {
CHAR_PROP_READ,
U16_LO(GAP_APPEAR_DP_H), U16_HI(GAP_APPEAR_DP_H),
U16_LO(GATT_UUID_APPEARANCE), U16_HI(GATT_UUID_APPEARANCE)
};
static const u8 connpar_cd[5] = {
CHAR_PROP_READ,
U16_LO(GAP_CONNPAR_DP_H), U16_HI(GAP_CONNPAR_DP_H),
U16_LO(GATT_UUID_PERI_CONN_PARAM), U16_HI(GATT_UUID_PERI_CONN_PARAM)
};
/* GATT */
static const u16 gatt_service_uuid = SERVICE_UUID_GENERIC_ATTRIBUTE;
static const u16 service_change_uuid = GATT_UUID_SERVICE_CHANGE;
static u32 service_change_val;
RAM static u16 service_change_ccc;
static const u8 service_change_cd[5] = {
CHAR_PROP_INDICATE,
U16_LO(GATT_SC_DP_H), U16_HI(GATT_SC_DP_H),
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;
static const u8 ota_name[] = { 'O', 'T', 'A' };
static const u8 ota_cd[19] = {
CHAR_PROP_READ | CHAR_PROP_WRITE_WITHOUT_RSP,
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
};
/* 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[] = {
{ ATT_END_H - 1, 0, 0, 0, 0, 0, 0, 0 }, /* total number of attributes */
{ 7, ATT_PERMISSIONS_READ, 2, 2, (u8 *)&primary_service_uuid, (u8 *)&gap_service_uuid, 0, 0 },
{ 0, ATT_PERMISSIONS_READ, 2, sizeof(devname_cd), (u8 *)&character_uuid, (u8 *)devname_cd, 0, 0 },
{ 0, ATT_PERMISSIONS_READ, 2, NAME_LEN, (u8 *)&devname_uuid, &scan_rsp[2], 0, 0 },
{ 0, ATT_PERMISSIONS_READ, 2, sizeof(appear_cd), (u8 *)&character_uuid, (u8 *)appear_cd, 0, 0 },
{ 0, ATT_PERMISSIONS_READ, 2, sizeof(appearance), (u8 *)&appearance_uuid, (u8 *)&appearance, 0, 0 },
{ 0, ATT_PERMISSIONS_READ, 2, sizeof(connpar_cd), (u8 *)&character_uuid, (u8 *)connpar_cd, 0, 0 },
{ 0, ATT_PERMISSIONS_READ, 2, sizeof(periConnParameters), (u8 *)&connpar_uuid, (u8 *)&periConnParameters, 0, 0 },
{ 4, ATT_PERMISSIONS_READ, 2, 2, (u8 *)&primary_service_uuid, (u8 *)&gatt_service_uuid, 0, 0 },
{ 0, ATT_PERMISSIONS_READ, 2, sizeof(service_change_cd), (u8 *)&character_uuid, (u8 *)service_change_cd, 0, 0 },
{ 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 },
{ 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_READ, 2, sizeof(ota_name), (u8 *)&user_desc_uuid, (u8 *)ota_name, 0, 0 },
};
/* ======================= connection / OTA events ======================= */
/* OTA start command received: full radio duty until it is done */
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;
* this only matters for failures. */
static void ota_result_cb(int result)
{
(void)result;
ota_active = 0;
}
/* Ask for 20 ms interval + latency 49: the device then only has to wake for
* one connection event per second while idle (pvvx ble_connect_callback). */
static void connect_cb(u8 e, u8 *p, int n)
{
(void)e; (void)p; (void)n;
ble_connected = 1;
conn_param_accepted = 0;
bls_l2cap_setMinimalUpdateReqSendingTime_after_connCreate(1000);
bls_l2cap_requestConnParamUpdate(CONN_INTERVAL, CONN_INTERVAL,
CONN_LATENCY, CONN_TIMEOUT);
lcd_show_ble(1);
}
static void disconnect_cb(u8 e, u8 *p, int n)
{
(void)e; (void)p; (void)n;
ble_connected = 0;
ota_active = 0;
lcd_show_ble(ota_window);
/* the stack resumes advertising by itself; ble_task() switches it to
* non-connectable if the window has expired meanwhile */
}
static int conn_param_rsp_cb(u8 id, u16 result)
{
(void)id;
if (result == CONN_PARAM_UPDATE_ACCEPT) {
conn_param_accepted = 1;
} else if (result == CONN_PARAM_UPDATE_REJECT) {
bls_l2cap_requestConnParamUpdate(CONN_INTERVAL, CONN_INTERVAL,
CONN_LATENCY, CONN_TIMEOUT);
}
return 0;
}
/* Suspend loses the RF power setting (pvvx suspend_exit_cb) */
_attribute_ram_code_
static void suspend_exit_cb(u8 e, u8 *p, int n)
{
(void)e; (void)p; (void)n;
rf_set_power_level_index(RF_TX_POWER);
}
/* ============================== init ============================== */
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);
memcpy(&scan_rsp[2], DEV_NAME, NAME_LEN);
scan_rsp[0] = NAME_LEN + 1;
scan_rsp[1] = GAP_ADTYPE_LOCAL_NAME_COMPLETE;
/* Controller */
blc_ll_initStandby_module(mac_public);
blc_ll_initAdvertising_module(mac_public);
blc_ll_initConnection_module();
blc_ll_initSlaveRole_module();
/* Host */
blc_gap_peripheral_init();
bls_att_setAttributeTable((u8 *)attributes);
blc_l2cap_register_handler(blc_l2cap_packet_receive);
blc_smp_setSecurityLevel(No_Security);
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));
bls_set_advertise_prepare((void *)adv_prepare);
if (open_window) {
/* 1 s interval so the flashing page finds and connects quickly */
ota_window = 1;
window_start = clock_time();
bls_ll_setAdvParam(OTA_WINDOW_ADV_INTERVAL,
OTA_WINDOW_ADV_INTERVAL + ADV_INTERVAL_DELAY,
ADV_TYPE_CONNECTABLE_UNDIRECTED, OWN_ADDRESS_PUBLIC,
0, NULL, BLT_ENABLE_ADV_ALL, ADV_FP_NONE);
bls_ll_setAdvEnable(BLC_ADV_ENABLE);
lcd_show_ble(1); /* BLE symbol = OTA window open */
} else {
close_window();
}
/* Power management (pvvx init_ble) */
blc_ll_initPowerManagement_module();
blc_pm_setDeepsleepRetentionThreshold(40, 18);
blc_pm_setDeepsleepRetentionEarlyWakeupTiming(240);
blc_pm_setDeepsleepRetentionType(DEEPSLEEP_MODE_RET_SRAM_LOW32K);
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)
{
ota_window = 0;
bls_ll_setAdvParam(ADV_INTERVAL, ADV_INTERVAL + ADV_INTERVAL_DELAY,
ADV_TYPE_NONCONNECTABLE_UNDIRECTED, OWN_ADDRESS_PUBLIC,
0, NULL, BLT_ENABLE_ADV_ALL, ADV_FP_NONE);
bls_ll_setAdvEnable(BLC_ADV_ENABLE);
lcd_show_ble(0);
}
_attribute_ram_code_
int ble_task(void)
{
/* An upload that started inside the window may finish after it; any
* other connection is dropped when the window ends. */
if (ota_window && !ota_active &&
clock_time() - window_start > OTA_WINDOW_TICKS) {
if (ble_connected) {
bls_ll_terminateConnection(HCI_ERR_REMOTE_USER_TERM_CONN);
} else {
close_window();
}
}
if (ota_active) {
if (!conn_param_accepted) {
bls_pm_setManualLatency(0);
}
bls_pm_setSuspendMask(SUSPEND_ADV | SUSPEND_CONN);
return 1;
}
bls_pm_setSuspendMask(SUSPEND_ADV | DEEPSLEEP_RETENTION_ADV |
SUSPEND_CONN | DEEPSLEEP_RETENTION_CONN);
return 0;
}
_attribute_ram_code_
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 */
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "tl_common.h"
/* Connectable advertising + GATT server with the Telink OTA service
* (00010203-0405-0607-0809-0a0b0c0d1912), structured like
* pvvx/ATC_MiThermometer ble.c / app_att.c. */
extern u8 ble_connected; /* a central is connected */
extern u8 ota_active; /* OTA transfer in progress */
extern u8 ota_window; /* connectable OTA window open */
/* Called once from user_init_normal(). open_window: battery was just
* inserted, accept connections for OTA_WINDOW_S; otherwise go straight to
* non-connectable advertising. adv_prepare is called by the stack
* right before every advertising event (pvvx app_advertise_prepare_handler):
* the application piggybacks its periodic work on those wake-ups. */
void ble_init(const u8 *adv, int adv_len, int (*adv_prepare)(void *),
int open_window);
/* Every main-loop pass: sets the sleep mode; returns 1 while OTA runs */
int ble_task(void);
/* Restore what deep-sleep retention loses (from user_init_deepRetn) */
void ble_init_deep_retention(void);
+61
View File
@@ -0,0 +1,61 @@
#include "tl_common.h"
#include "drivers.h"
#include "app_config.h"
#include "i2c.h"
#define I2C_WAIT() while (reg_i2c_status & FLD_I2C_CMD_BUSY)
/* The I2C clock and pin mux live in digital registers that are lost in
* deep-sleep retention, so this is called at the start of every bus session.
* Clock stretching is disabled: the sensor is polled, never stretched. */
_attribute_ram_code_
void i2c_bus_init(void)
{
i2c_gpio_set(I2C_GROUP);
reg_i2c_speed = CLOCK_SYS_CLOCK_HZ / (4 * I2C_SPEED_HZ);
reg_i2c_mode |= FLD_I2C_MASTER_EN;
reg_i2c_mode &= ~FLD_I2C_HOLD_MASTER;
reg_clk_en0 |= FLD_CLK0_I2C_EN;
reg_spi_sp &= ~FLD_SPI_ENABLE; /* pads shared with SPI: force I2C */
}
_attribute_ram_code_
int i2c_write(u8 addr, const u8 *buf, u32 len)
{
int err;
reg_i2c_id = addr << 1;
reg_i2c_ctrl = FLD_I2C_CMD_START | FLD_I2C_CMD_ID;
I2C_WAIT();
err = reg_i2c_status & FLD_I2C_NAK;
while (!err && len--) {
reg_i2c_do = *buf++;
reg_i2c_ctrl = FLD_I2C_CMD_DO;
I2C_WAIT();
err = reg_i2c_status & FLD_I2C_NAK;
}
reg_i2c_ctrl = FLD_I2C_CMD_STOP;
I2C_WAIT();
return err;
}
_attribute_ram_code_
int i2c_read(u8 addr, u8 *buf, u32 len)
{
int err;
reg_i2c_id = (addr << 1) | FLD_I2C_WRITE_READ_BIT;
reg_i2c_ctrl = FLD_I2C_CMD_START | FLD_I2C_CMD_ID;
I2C_WAIT();
err = reg_i2c_status & FLD_I2C_NAK;
while (!err && len--) {
/* NAK (ACK flag) the last byte to end the read */
reg_i2c_ctrl = FLD_I2C_CMD_DI | FLD_I2C_CMD_READ_ID |
(len ? 0 : FLD_I2C_CMD_ACK);
I2C_WAIT();
*buf++ = reg_i2c_di;
}
reg_i2c_ctrl = FLD_I2C_CMD_STOP;
I2C_WAIT();
return err;
}
+8
View File
@@ -0,0 +1,8 @@
#pragma once
#include "tl_common.h"
/* Polled I2C master on the TLSR825x hardware block. Addresses are 7-bit.
* All functions return 0 on success, non-zero on NAK. */
void i2c_bus_init(void);
int i2c_write(u8 addr, const u8 *buf, u32 len);
int i2c_read(u8 addr, u8 *buf, u32 len);
+209
View File
@@ -0,0 +1,209 @@
/*
* LYWSD03MMC segment LCD driver for the 2025 "B1.5" board (pvvx reports it
* as HW B1.1): LCD controller on bit-banged SPI, PD7 = CLK, PB7 = SDI.
* Write-only, no acknowledge.
*
* The frame format and segment map are from pvvx/ATC_MiThermometer
* (lcd_lywsd03mmc.c), used under that project's MIT-style license:
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software ... to deal in the Software without restriction.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*
* Display buffer, byte.bit:
*
* --5.4-- --4.4-- --3.4-- BLE 2.4
* | 5.5 5.0 4.5 4.0 3.5 3.0 o 2.5 (temp unit
* 5.3 --5.1-- --4.1-- --3.1-- 2.5..2.7 symbols)
* | 5.6 5.2 4.6 4.2 3.6 3.2
* --5.7-- --4.7-- * --3.7--
* 4.3
* smiley 2.0..2.2 --1.4-- --0.4--
* 1.5 1.0 0.5 0.0
* --1.1-- --0.1--
* 1.6 1.2 0.6 0.2 % 0.3
* BAT 1.3 --1.7-- --0.7--
*/
#include "tl_common.h"
#include "drivers.h"
#include "drivers/8258/gpio_8258.h"
#include "app_config.h"
#include "lcd.h"
#define SEG_MINUS 0x02 /* middle segment */
#define SYM_POINT 0x08 /* 4.3 */
#define SYM_ONE 0x08 /* 5.3: leading "1" for 100..199 */
#define SYM_PERCENT 0x08 /* 0.3 */
#define SYM_BAT 0x08 /* 1.3 */
#define SYM_DEG_C 0xA0 /* 2.5 + 2.7 */
#define SYM_BLE 0x10 /* 2.4: shown while a BLE central is connected */
#define SYM_H 0x67
#define SYM_i 0x40
#define SYM_L 0xE0
#define SYM_o 0xC6
#define SPI_CLK_DELAY_US 32 /* half clock period: ~15 kHz */
static const u8 digits[10] = {
0xF5, 0x05, 0xD3, 0x97, 0x27, 0xB6, 0xF6, 0x15, 0xF7, 0xB7
};
/* Frame: AA d5..d0 xor(d) 55, sent byte by byte, LSB first */
RAM static u8 buf[6];
RAM static u8 sent[6];
RAM static u8 frame[9];
static void frame_fill(void)
{
u8 chk = 0;
frame[0] = 0xAA;
for (int i = 0; i < 6; i++) {
frame[1 + i] = buf[5 - i];
chk ^= buf[i];
}
frame[7] = chk;
frame[8] = 0x55;
}
/* Data changes while CLK is low and is latched on the rising edge */
_attribute_ram_code_
static void spi_send_byte(u8 b)
{
for (int i = 0; i < 8; i++) {
BM_CLR(reg_gpio_out(GPIO_LCD_CLK), GPIO_LCD_CLK & 0xFF);
if (b & 0x01) {
BM_SET(reg_gpio_out(GPIO_LCD_SDI), GPIO_LCD_SDI & 0xFF);
} else {
BM_CLR(reg_gpio_out(GPIO_LCD_SDI), GPIO_LCD_SDI & 0xFF);
}
sleep_us(SPI_CLK_DELAY_US);
BM_SET(reg_gpio_out(GPIO_LCD_CLK), GPIO_LCD_CLK & 0xFF);
sleep_us(SPI_CLK_DELAY_US);
b >>= 1;
}
sleep_us(SPI_CLK_DELAY_US);
}
/* ~5 ms with interrupts off, only when the content changed. gpio_init()
* makes both pins inputs again after every deep-sleep retention wake-up,
* so they are switched to outputs on each send. */
_attribute_ram_code_
static void lcd_send(void)
{
BM_CLR(reg_gpio_oen(GPIO_LCD_CLK), GPIO_LCD_CLK & 0xFF);
BM_CLR(reg_gpio_oen(GPIO_LCD_SDI), GPIO_LCD_SDI & 0xFF);
u8 r = irq_disable();
for (int i = 0; i < (int)sizeof(frame); i++) {
spi_send_byte(frame[i]);
}
irq_restore(r);
}
static void lcd_flush(void)
{
if (!memcmp(sent, buf, sizeof(buf))) {
return; /* nothing changed: no LCD traffic at all */
}
frame_fill();
lcd_send();
memcpy(sent, buf, sizeof(buf));
}
void lcd_init(void)
{
memset(buf, SEG_MINUS, sizeof(buf)); /* "-- --" */
buf[2] = 0;
pm_wait_ms(30); /* controller start-up, as pvvx (>= 30 ms) */
frame_fill();
lcd_send();
memcpy(sent, buf, sizeof(buf));
}
/* Big digits: temperature in 0.1 degC.
* -9.9 .. 199.9 shown with one decimal, -99 .. -10 as integers. */
static void show_big_x10(s16 n)
{
buf[5] = buf[4] = 0;
if (n > 1999) {
buf[3] = 0; buf[4] = SYM_i; buf[5] = SYM_H; /* "Hi" */
return;
}
if (n <= -995) {
buf[3] = 0; buf[4] = SYM_o; buf[5] = SYM_L; /* "Lo" */
return;
}
if (n < 0) {
n = -n;
buf[5] = SEG_MINUS;
}
if (buf[5] && n > 99) {
n = (n + 5) / 10; /* -10 .. -99: integer */
} else {
buf[4] = SYM_POINT;
}
if (n > 999) {
buf[5] |= SYM_ONE;
}
if (n > 99) {
buf[5] |= digits[n / 100 % 10];
}
buf[4] |= digits[n / 10 % 10]; /* always: "0.x" */
buf[3] = digits[n % 10];
}
/* Small digits: humidity in %. */
static void show_small_pct(int n)
{
buf[1] &= SYM_BAT;
buf[0] = SYM_PERCENT;
if (n > 99) {
buf[0] |= SYM_i; buf[1] |= SYM_H;
return;
}
if (n < 0) {
n = 0;
}
if (n > 9) {
buf[1] |= digits[n / 10];
}
buf[0] |= digits[n % 10];
}
void lcd_show(int valid, s16 t10, int rh, int batt_low)
{
if (valid) {
show_big_x10(t10);
show_small_pct(rh);
buf[2] = (buf[2] & SYM_BLE) | SYM_DEG_C;
}
if (batt_low) {
buf[1] |= SYM_BAT;
} else {
buf[1] &= ~SYM_BAT;
}
lcd_flush();
}
void lcd_show_ble(int on)
{
if (on) {
buf[2] |= SYM_BLE;
} else {
buf[2] &= ~SYM_BLE;
}
lcd_flush();
}
/* Dead-battery screen (as pvvx low_vbat()): battery voltage in the big
* digits ("1.9"), "Lo" in the small ones, battery icon on. */
void lcd_show_low_battery(u16 mv)
{
show_big_x10(mv / 100);
buf[1] = SYM_L | SYM_BAT;
buf[0] = SYM_o;
buf[2] = 0;
lcd_flush();
}
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include "tl_common.h"
/* Segment LCD of the LYWSD03MMC 2025 "B1.5" board (pvvx: HW B1.1):
* controller on bit-banged SPI, PD7 CLK / PB7 SDI, write-only. It holds
* the image by itself, so the CPU only talks to it when the content
* changes. */
void lcd_init(void); /* shows "-- --" */
void lcd_show(int valid, s16 t10, int rh, int batt_low);
void lcd_show_ble(int on); /* BLE symbol while connected */
void lcd_show_low_battery(u16 mv); /* "x.x Lo" + battery icon */
+299
View File
@@ -0,0 +1,299 @@
/*
* BTHome v2 temperature / humidity beacon with LCD and OTA - Xiaomi
* LYWSD03MMC B1.5 (Telink TLSR8251, Telink BLE 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
* battery is inserted the advertising is connectable (1 s interval) so the
* Telink OTA service can be reached; after that it is non-connectable.
*
* Power handling follows pvvx/ATC_MiThermometer:
* - no wake-ups of its own: a measurement is started right after every
* 6th advertising event, while the chip is awake anyway
* - the CPU sleeps (suspend) during the ~12 ms sensor conversion instead
* of stalling; the result is collected by an app wake-up timer
* - VDD is sampled while the sensor converts, i.e. under load
* - deep-sleep retention between events, also while connected
* (slave latency 49 -> one connection event per second)
* - battery protection: < 2.2 V at power-on or < 2.0 V while running ->
* "Lo" on the LCD and 3 min deep sleep, before any flash write
*/
#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"
#include "lcd.h"
#include "ble.h"
#define MEAS_INTERVAL_TICKS ((u32)MEAS_INTERVAL_S * CLOCK_16M_SYS_TIMER_CLK_1S)
#define NAME_LEN (sizeof(DEV_NAME) - 1)
/* Advertising payload (objects in ascending ID order: 0x0C, 0x2E, 0x45) */
enum {
OFS_VOLT = 9, /* voltage, 1 mV, uint16 */
OFS_HUM = 12, /* humidity, 1 %, uint8 */
OFS_TEMP = 14, /* temperature, 0.1 degC, sint16 */
};
#if ADV_NAME_IN_ADV
#define ADV_LEN (18 + NAME_LEN)
#else
#define ADV_LEN 16
#endif
RAM static u8 adv[ADV_LEN] = {
0x02, 0x01, 0x06, /* flags: LE general, no BR/EDR */
0x0C, 0x16, /* service data, 16-bit UUID */
0xD2, 0xFC, /* UUID 0xFCD2 */
0x40, /* BTHome v2, unencrypted, regular interval */
0x0C, 0, 0, /* voltage */
0x2E, 0, /* humidity */
0x45, 0, 0, /* temperature */
#if ADV_NAME_IN_ADV
NAME_LEN + 1, 0x09, /* complete local name, filled in at boot */
#endif
};
typedef char adv_fits_31_bytes[(sizeof(adv) <= 31) ? 1 : -1];
/* Measurement state, kept through deep-sleep retention */
RAM static u8 adv_count; /* advertising events since last measurement */
RAM static u8 start_measure; /* measurement due */
RAM static u8 publish_pending; /* new values to put on air / on the LCD */
RAM static u8 valid; /* at least one good sensor reading */
RAM static u32 time_measure; /* != 0: sensor converting since this tick */
RAM static u32 meas_tick; /* last measurement start (used when connected) */
RAM static s16 t10;
RAM static u8 rh_pct;
RAM static u16 vbat_mv;
static inline void put_le16(u8 *p, u16 v)
{
p[0] = v & 0xFF;
p[1] = v >> 8;
}
/* Deep sleep without retention: the next wake-up is a fresh boot */
static void deep_sleep_s(u32 s)
{
cpu_sleep_wakeup(DEEPSLEEP_MODE, PM_WAKEUP_TIMER,
clock_time() + s * CLOCK_16M_SYS_TIMER_CLK_1S);
while (1) {
}
}
/* pvvx low_vbat(): stop everything, show it, retry in 3 minutes */
static void low_battery(u16 mv)
{
sensor_sleep();
lcd_show_low_battery(mv);
deep_sleep_s(LOW_VBAT_SLEEP_S);
}
_attribute_ram_code_
static void store_result(s32 mc, s32 mrh)
{
s32 pct = (mrh + 500) / 1000;
t10 = (mc + (mc < 0 ? -50 : 50)) / 100;
rh_pct = pct < 0 ? 0 : (pct > 100 ? 100 : pct);
valid = 1;
}
/* App wake-up timer: the sensor conversion is finished (pvvx
* WakeupLowPowerCb). Also called from main_loop() if the stack did not
* sleep at all. A failed read keeps the last good T/RH values. */
_attribute_ram_code_
static void measure_done_cb(int unused)
{
s32 mc, mrh;
(void)unused;
if (!time_measure) {
return;
}
time_measure = 0;
bls_pm_setAppWakeupLowPower(0, 0);
if (!sensor_result(&mc, &mrh)) {
store_result(mc, mrh);
}
publish_pending = 1;
}
/* Start a measurement: trigger the sensor, read VDD while it converts,
* then let the stack sleep until the result is ready. */
_attribute_ram_code_
static void measure_start(void)
{
meas_tick = clock_time();
if (!sensor_start()) {
time_measure = clock_time() | 1;
}
vbat_mv = battery_read_mv();
if (vbat_mv < END_VBAT_MV) {
low_battery(vbat_mv);
}
if (time_measure) {
bls_pm_setAppWakeupLowPower(time_measure + SENSOR_MEAS_TICKS, 1);
} else {
publish_pending = 1; /* sensor absent: voltage only */
}
}
_attribute_ram_code_
static void publish(void)
{
publish_pending = 0;
if (valid) {
put_le16(&adv[OFS_TEMP], (u16)t10);
adv[OFS_HUM] = rh_pct;
}
put_le16(&adv[OFS_VOLT], vbat_mv);
bls_ll_setAdvData(adv, sizeof(adv));
lcd_show(valid, t10, rh_pct, vbat_mv < LOW_BATT_MV);
}
/* Called by the stack right before every advertising event (pvvx
* app_advertise_prepare_handler): count events, flag a measurement on
* every MEAS_EVERY_N_ADV-th one. The measurement itself starts in
* main_loop() once the event is done. While the OTA window runs at the
* fast interval, main_loop() uses a timer instead. */
_attribute_ram_code_
static int adv_prepare_cb(void *p)
{
(void)p;
if (!ota_window && ++adv_count >= MEAS_EVERY_N_ADV) {
adv_count = 0;
start_measure = 1;
}
return 1; /* send this advertising packet */
}
static void user_init_normal(void)
{
blc_ll_initBasicMCU();
adc_power_on_sar_adc(0);
lpc_power_down();
/* Analog pull resistors are kept through deep sleep: set once */
gpio_setup_up_down_resistor(I2C_SCL, PULL_I2C);
gpio_setup_up_down_resistor(I2C_SDA, PULL_I2C);
gpio_setup_up_down_resistor(GPIO_LCD_PB6, PULL_LCD_PB6);
gpio_setup_up_down_resistor(GPIO_LCD_CLK, PULL_LCD_SER);
gpio_setup_up_down_resistor(GPIO_LCD_SDI, PULL_LCD_SER);
gpio_setup_up_down_resistor(GPIO_KEY, PULL_KEY);
/* Battery first: nothing below may write flash on a dying cell
* (pvvx start_tst_battery) */
vbat_mv = battery_read_mv();
if (vbat_mv < BOOT_VBAT_MV) {
sensor_init(); /* idle -> sleep */
lcd_init();
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();
#if ADV_NAME_IN_ADV
memcpy(&adv[18], DEV_NAME, NAME_LEN);
#endif
lcd_init(); /* shows "-- --" until the first good reading */
sensor_init(); /* put the SHTV3 to sleep */
/* First measurement, blocking, so the first packet carries data */
if (!sensor_start()) {
s32 mc, mrh;
vbat_mv = battery_read_mv();
pm_wait_us(SENSOR_MEAS_US);
if (!sensor_result(&mc, &mrh)) {
store_result(mc, mrh);
}
}
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;
analog_write(DEEP_ANA_REG0, 0x55);
bls_pm_registerAppWakeupLowPowerCb(measure_done_cb);
ble_init(adv, sizeof(adv), adv_prepare_cb, power_on);
meas_tick = clock_time();
}
_attribute_ram_code_
static void user_init_deepRetn(void)
{
blc_ll_initBasicMCU();
ble_init_deep_retention();
}
_attribute_ram_code_
static void main_loop(void)
{
blt_sdk_main_loop();
if (ble_task()) {
return; /* OTA in progress: no measurements */
}
if (time_measure &&
clock_time() - time_measure > SENSOR_MEAS_TICKS) {
measure_done_cb(0); /* stack did not sleep in between */
}
/* While connected there are no advertising events to count, and in
* the OTA window they come every second */
if ((ble_connected || ota_window) &&
clock_time() - meas_tick >= MEAS_INTERVAL_TICKS) {
start_measure = 1;
}
if (start_measure && !time_measure) {
start_measure = 0;
measure_start();
}
if (publish_pending) {
publish();
}
}
_attribute_ram_code_
void irq_handler(void)
{
irq_blt_sdk_handler();
}
_attribute_ram_code_
int main(void)
{
blc_pm_select_internal_32k_crystal(); /* no 32 kHz crystal fitted */
cpu_wakeup_init();
int deep_ret = pm_is_MCU_deepRetentionWakeup();
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();
} else {
user_init_normal();
}
irq_enable();
for (;;) {
main_loop();
}
}
+94
View File
@@ -0,0 +1,94 @@
#include "tl_common.h"
#include "drivers.h"
#include "app_config.h"
#include "i2c.h"
#include "sensor.h"
/* SHTV3 / SHTC3 on the B1.5 board */
#define SHTC3_ADDR 0x70
#define SHTC3_WAKEUP_US 240
static const u8 shtc3_wakeup[] = { 0x35, 0x17 };
static const u8 shtc3_sleep[] = { 0xB0, 0x98 };
static const u8 shtc3_meas[] = { 0x78, 0x66 }; /* normal mode, T first, no stretching */
_attribute_ram_code_
static u8 crc8(const u8 *p, int len)
{
u8 crc = 0xFF;
while (len--) {
crc ^= *p++;
for (int i = 0; i < 8; i++) {
crc = (crc & 0x80) ? (crc << 1) ^ 0x31 : (crc << 1);
}
}
return crc;
}
/* Wake the sensor once and put it straight back to sleep (~0.3 uA).
* It powers up in idle mode, which draws far more than sleep. */
int sensor_init(void)
{
int err;
i2c_bus_init();
err = i2c_write(SHTC3_ADDR, shtc3_wakeup, sizeof(shtc3_wakeup));
if (!err) {
sleep_us(SHTC3_WAKEUP_US);
i2c_write(SHTC3_ADDR, shtc3_sleep, sizeof(shtc3_sleep));
}
return err;
}
_attribute_ram_code_
int sensor_start(void)
{
int err;
i2c_bus_init();
err = i2c_write(SHTC3_ADDR, shtc3_wakeup, sizeof(shtc3_wakeup));
if (err) {
return err;
}
sleep_us(SHTC3_WAKEUP_US);
err = i2c_write(SHTC3_ADDR, shtc3_meas, sizeof(shtc3_meas));
if (err) {
i2c_write(SHTC3_ADDR, shtc3_sleep, sizeof(shtc3_sleep));
}
return err;
}
/* Raw 16-bit words -> milli-units. The x/65536 scaling is done as
* (x * k) >> 13, which fits in 32 bits:
* 175000 / 8 = 21875, 100000 / 8 = 12500 */
_attribute_ram_code_
int sensor_result(s32 *t_mc, s32 *rh_mpct)
{
u8 buf[6];
int err;
i2c_bus_init(); /* I2C registers do not survive deep-sleep retention */
err = i2c_read(SHTC3_ADDR, buf, sizeof(buf));
i2c_write(SHTC3_ADDR, shtc3_sleep, sizeof(shtc3_sleep));
if (err || crc8(&buf[0], 2) != buf[2] || crc8(&buf[3], 2) != buf[5]) {
return -1;
}
u32 t_raw = (buf[0] << 8) | buf[1];
u32 h_raw = (buf[3] << 8) | buf[4];
*t_mc = (s32)((t_raw * 21875) >> 13) - 45000;
*rh_mpct = (s32)((h_raw * 12500) >> 13);
return 0;
}
/* The sleep command is NAKed while a conversion is running, so allow
* for one to finish first. */
void sensor_sleep(void)
{
i2c_bus_init();
sleep_us(SENSOR_MEAS_US);
i2c_write(SHTC3_ADDR, shtc3_sleep, sizeof(shtc3_sleep));
}
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include "tl_common.h"
/* Temperature/humidity sensor of the LYWSD03MMC B1.5:
* SHTV3 (SHTC3-compatible) at I2C address 0x70.
*
* A measurement is split in two so the CPU can sleep while the sensor
* converts (as pvvx SENSOR_SLEEP_MEASURE does):
* sensor_start() wake the sensor and trigger a conversion
* ... SENSOR_MEAS_TICKS later ...
* sensor_result() read the result and put the sensor back to sleep */
#define SENSOR_MEAS_US 12500 /* SHTC3 normal mode: max 12.1 ms */
#define SENSOR_MEAS_TICKS (SENSOR_MEAS_US * CLOCK_16M_SYS_TIMER_CLK_1US)
int sensor_init(void); /* 0 = sensor answered */
int sensor_start(void); /* 0 = conversion started */
int sensor_result(s32 *t_mc, s32 *rh_mpct); /* milli-degC, milli-%; 0 = ok */
void sensor_sleep(void); /* force sleep (e.g. low battery) */