Add the Seeed SenseCAP Indicator D1L as a new touch target. This step builds and boots to a blank screen; the panel, touch and radio drivers follow in their own commits so each can be reviewed against the hardware it drives. The SX1262's NSS, RESET, BUSY and DIO1, the LCD CS/reset and the touch reset all sit on a TCA9535 at 0x20 on the SDA39/SCL40 bus, not on ESP32 GPIOs, so everything after this commit depends on the expander. The env gives the radio lines virtual pin numbers 0x40..0x43 for a radio HAL to map onto expander bits; until that HAL exists the radio is a plain Module that links but cannot drive the chip. The expander driver is header-only so a host test can cover it: - its output and config shadows are seeded from the chip, because a warm reset or the previous firmware may have left pins driven and writing the power-on defaults back would glitch the radio, panel and RP2040 resets; - a failed write rolls both shadows back to the last state written in full, and the next write brings the chip back in line with them; - polarity inversion is cleared when a previous firmware left it set, since it would invert BUSY, DIO1 and the strap; it is written only when it is not already 0; - every read of input port 0 is reported to an observer, so a DIO1 edge is not lost when another reader clears /INT. A five-sample majority of the P1.3 strap selects the TCXO (2.4 V) or crystal radio at run time, so one noisy read cannot pick the wrong oscillator. The expander, the touch driver and the display init serialise on one recursive mutex over the shared Wire bus (IndicatorIo::BusLock). It is a function-local static, so the compiler guards its creation when two tasks reach the first lock together. IndicatorIo::begin() logs the registers it inherited and names the step that failed, since a dead expander takes the panel and radio down with it and is otherwise silent. The board has 8 MB of flash. The partition table has one 4.5 MB factory app slot, a 3.375 MB SPIFFS (as on the 16 MB boards) and a 64 KB coredump, with no map tile partition. The factory slot is larger than the 0x3E0000 OTA slots of the 16 MB boards, so an image that fits those fits here. CAP_OTA is off: with one app slot there is no in-place update. CAP_BATTERY is 0: no battery is fitted. Montserrat 18/20/24 for the UI-size presets are the generated faces; the board joins their gate in ui_font_18/20/24.c, gen-touch-fonts.sh, hwFitFont and luaHostFigureFont. LVGL is built at -O2 with the Arabic and newlib-memcpy settings the other envs use. run-host-tests.sh builds every test/test_*.cpp, so the new expander test gets its include path there in the same commit. build-upload-monitor.sh gets --sensecap-indicator. Signed-off-by: Confituurke <9595028+Confituurke@users.noreply.github.com>
230 lines
7.3 KiB
C++
230 lines
7.3 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
//
|
|
// Host test for the SenseCAP Indicator TCA9535 driver (IndicatorExpander.h): the
|
|
// shadow registers, the write order, the rollback on a failed write and the
|
|
// radio-strap vote.
|
|
//
|
|
// c++ -std=c++17 -Wall -Wextra -I src -I variants/sensecap_indicator test/test_indicator_expander.cpp -o /tmp/ind_exp && /tmp/ind_exp
|
|
|
|
#include <assert.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "IndicatorExpander.h"
|
|
|
|
using namespace indicator;
|
|
|
|
// Eight registers of a TCA9535 and a log of every write that reached it.
|
|
struct FakeBus : ExpanderBus {
|
|
struct Write { uint8_t reg; uint8_t bytes[2]; size_t n; };
|
|
|
|
uint8_t regs[8] = {};
|
|
bool reads[8] = {};
|
|
Write log[32] = {};
|
|
size_t writes = 0;
|
|
int fail_write_at = 0; // 0: never fail; N: the Nth write from now fails
|
|
|
|
bool read(uint8_t reg, uint8_t* data, size_t n) override {
|
|
for (size_t i = 0; i < n; ++i) {
|
|
data[i] = regs[reg + i];
|
|
reads[reg + i] = true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool write(uint8_t reg, const uint8_t* data, size_t n) override {
|
|
if (fail_write_at > 0 && --fail_write_at == 0) return false;
|
|
Write& w = log[writes++];
|
|
w.reg = reg;
|
|
w.n = n;
|
|
for (size_t i = 0; i < n; ++i) { w.bytes[i] = data[i]; regs[reg + i] = data[i]; }
|
|
return true;
|
|
}
|
|
};
|
|
|
|
static void seedChip(FakeBus& bus, uint8_t out0, uint8_t out1, uint8_t cfg0, uint8_t cfg1) {
|
|
bus.regs[0x02] = out0; bus.regs[0x03] = out1;
|
|
bus.regs[0x06] = cfg0; bus.regs[0x07] = cfg1;
|
|
}
|
|
|
|
static void seeds_shadow_from_chip_not_defaults() {
|
|
FakeBus bus;
|
|
seedChip(bus, 0x5A, 0x0F, 0x4C, 0xFF);
|
|
Tca9535 chip(bus);
|
|
assert(chip.begin());
|
|
assert(chip.output(0) == 0x5A);
|
|
assert(chip.output(1) == 0x0F);
|
|
assert(chip.config(0) == 0x4C);
|
|
assert(chip.config(1) == 0xFF);
|
|
assert(bus.reads[0x00]); // /INT released
|
|
assert(bus.writes == 0);
|
|
}
|
|
|
|
static void latch_before_direction() {
|
|
FakeBus bus;
|
|
seedChip(bus, 0x00, 0x00, 0xFF, 0xFF);
|
|
Tca9535 chip(bus);
|
|
assert(chip.begin());
|
|
assert(chip.setOutput(pins::kLcdCs, true));
|
|
assert(bus.writes == 2);
|
|
assert(bus.log[0].reg == 0x02);
|
|
assert(bus.log[1].reg == 0x06);
|
|
assert(chip.output(0) & 0x10);
|
|
assert(!(chip.config(0) & 0x10));
|
|
}
|
|
|
|
static void output_pin_writes_level_only() {
|
|
FakeBus bus;
|
|
seedChip(bus, 0x00, 0x00, 0xFF, 0xFF);
|
|
Tca9535 chip(bus);
|
|
assert(chip.begin());
|
|
assert(chip.setOutput(pins::kLoraNss, true));
|
|
assert(bus.writes == 2);
|
|
// Already an output: the level changes, the config register is left alone.
|
|
assert(chip.setOutput(pins::kLoraNss, false));
|
|
assert(chip.setOutput(pins::kLoraNss, true));
|
|
assert(bus.writes == 4);
|
|
assert(bus.log[2].reg == 0x02 && bus.log[3].reg == 0x02);
|
|
assert(bus.regs[0x02] & 0x01);
|
|
assert(!(bus.regs[0x06] & 0x01));
|
|
}
|
|
|
|
static void port1_untouched_except_written_pin() {
|
|
FakeBus bus;
|
|
seedChip(bus, 0x00, 0xA4, 0xFF, 0xF7);
|
|
Tca9535 chip(bus);
|
|
assert(chip.begin());
|
|
assert(chip.setOutput(pins::kRp2040Reset, true));
|
|
assert(chip.output(1) == 0xA5);
|
|
assert(chip.config(1) == 0xF6);
|
|
assert(bus.regs[0x03] == 0xA5);
|
|
assert(bus.regs[0x07] == 0xF6);
|
|
assert((bus.regs[0x03] & 0xFE) == (0xA4 & 0xFE));
|
|
assert((bus.regs[0x07] & 0xFE) == (0xF7 & 0xFE));
|
|
}
|
|
|
|
static void failed_write_rolls_back_shadow() {
|
|
FakeBus bus;
|
|
seedChip(bus, 0x01, 0x00, 0xFF, 0xFF);
|
|
Tca9535 chip(bus);
|
|
assert(chip.begin());
|
|
const uint8_t out0 = chip.output(0), out1 = chip.output(1);
|
|
const uint8_t cfg0 = chip.config(0), cfg1 = chip.config(1);
|
|
|
|
bus.fail_write_at = 1;
|
|
assert(!chip.setOutput(pins::kLoraNss, false));
|
|
assert(chip.output(0) == out0 && chip.output(1) == out1);
|
|
assert(chip.config(0) == cfg0 && chip.config(1) == cfg1);
|
|
|
|
// The next write starts from the restored shadow, not from the half-applied one.
|
|
assert(chip.setOutput(pins::kLcdCs, true));
|
|
assert(bus.writes == 2);
|
|
assert(bus.log[0].reg == 0x02 && bus.log[0].bytes[0] == (out0 | 0x10));
|
|
assert(bus.log[1].reg == 0x06 && bus.log[1].bytes[0] == (cfg0 & (uint8_t)~0x10));
|
|
}
|
|
|
|
static void failed_config_write_rolls_back_both_shadows() {
|
|
FakeBus bus;
|
|
seedChip(bus, 0x01, 0x00, 0xFF, 0xFF);
|
|
Tca9535 chip(bus);
|
|
assert(chip.begin());
|
|
const uint8_t out0 = chip.output(0), out1 = chip.output(1);
|
|
const uint8_t cfg0 = chip.config(0), cfg1 = chip.config(1);
|
|
|
|
// The output write (0x02) lands, the config write (0x06) fails.
|
|
bus.fail_write_at = 2;
|
|
assert(!chip.setOutput(pins::kLcdReset, true));
|
|
assert(bus.writes == 1 && bus.log[0].reg == 0x02);
|
|
assert(chip.output(0) == out0 && chip.output(1) == out1);
|
|
assert(chip.config(0) == cfg0 && chip.config(1) == cfg1);
|
|
|
|
// The next write starts from the restored shadow: LCD RESET is not carried
|
|
// over from the half-applied call, and the chip's output latch is rewritten.
|
|
assert(chip.setOutput(pins::kLcdCs, true));
|
|
assert(bus.writes == 3);
|
|
assert(bus.log[1].reg == 0x02 && bus.log[1].bytes[0] == (out0 | 0x10));
|
|
assert(bus.log[1].bytes[1] == out1);
|
|
assert(bus.log[2].reg == 0x06 && bus.log[2].bytes[0] == (cfg0 & (uint8_t)~0x10));
|
|
assert(bus.log[2].bytes[1] == cfg1);
|
|
assert(bus.regs[0x02] == (out0 | 0x10));
|
|
}
|
|
|
|
static uint8_t s_seen = 0;
|
|
static int s_calls = 0;
|
|
static void* s_ctx = nullptr;
|
|
|
|
static void observer(uint8_t value, void* ctx) {
|
|
s_seen = value;
|
|
s_ctx = ctx;
|
|
++s_calls;
|
|
}
|
|
|
|
static void port0_observer_sees_every_read() {
|
|
FakeBus bus;
|
|
seedChip(bus, 0x00, 0x00, 0xFF, 0xFF);
|
|
Tca9535 chip(bus);
|
|
assert(chip.begin());
|
|
int token = 0;
|
|
chip.setPort0Observer(observer, &token);
|
|
|
|
bus.regs[0x00] = 0x0C; // BUSY and DIO1 high
|
|
bool high = false;
|
|
assert(chip.readPin(pins::kLoraBusy, high));
|
|
assert(high);
|
|
assert(s_calls == 1 && s_seen == 0x0C && s_ctx == &token);
|
|
|
|
bus.regs[0x00] = 0x08;
|
|
uint8_t value = 0;
|
|
assert(chip.readPort0(value));
|
|
assert(value == 0x08);
|
|
assert(s_calls == 2 && s_seen == 0x08);
|
|
|
|
// A port-1 pin read covers both input bytes, so port 0 is reported too.
|
|
bus.regs[0x00] = 0x48;
|
|
bus.regs[0x01] = 0x08;
|
|
assert(chip.readPin(pins::kRadioStrap, high));
|
|
assert(high);
|
|
assert(s_calls == 3 && s_seen == 0x48);
|
|
}
|
|
|
|
static void strap_majority_vote() {
|
|
const bool a[5] = {true, true, false, false, true};
|
|
const bool b[5] = {true, false, false, false, true};
|
|
const bool c[5] = {true, true, true, false, false};
|
|
assert(strapMajority(a));
|
|
assert(!strapMajority(b));
|
|
assert(strapMajority(c));
|
|
}
|
|
|
|
static void clears_polarity_inversion_only_when_set() {
|
|
FakeBus clean;
|
|
seedChip(clean, 0xFF, 0xFF, 0x4C, 0xFE);
|
|
Tca9535 a(clean);
|
|
assert(a.begin());
|
|
assert(clean.writes == 0); // already 0: nothing written
|
|
|
|
FakeBus inverted;
|
|
seedChip(inverted, 0xFF, 0xFF, 0x4C, 0xFE);
|
|
inverted.regs[0x04] = 0x0C; inverted.regs[0x05] = 0x08;
|
|
Tca9535 b(inverted);
|
|
assert(b.begin());
|
|
assert(inverted.writes == 1);
|
|
assert(inverted.log[0].reg == 0x04 && inverted.log[0].n == 2);
|
|
assert(inverted.regs[0x04] == 0x00 && inverted.regs[0x05] == 0x00);
|
|
assert(b.output(0) == 0xFF && b.config(0) == 0x4C); // shadows still from the chip
|
|
}
|
|
|
|
int main() {
|
|
seeds_shadow_from_chip_not_defaults();
|
|
clears_polarity_inversion_only_when_set();
|
|
latch_before_direction();
|
|
output_pin_writes_level_only();
|
|
port1_untouched_except_written_pin();
|
|
failed_write_rolls_back_shadow();
|
|
failed_config_write_rolls_back_both_shadows();
|
|
port0_observer_sees_every_read();
|
|
strap_majority_vote();
|
|
printf("all indicator expander tests passed\n");
|
|
return 0;
|
|
}
|