Initial Commit
This commit is contained in:
commit
9825b9b4f8
15 files changed
+1439
No files matched your search
Binary file not shown.
@@ -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.
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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) */
|
||||
Reference in new issue
Block a user