# Continuity Test Feature — Change Log

Adds a continuity beeper to the HP 3478A TFT retrofit firmware: sounds an active
buzzer (and shows an on-screen **CONT** indicator) when the meter is in
**resistance mode**, the **SHIFT annunciator is lit**, and the reading is **at or
below a threshold** (default 10 Ω).

- **Date:** 2026-06-08 — **reworked 2026-07-20** (see *Rework* below)
- **Target:** STM32F103C8 (Blue Pill)
- **Beeper:** two outputs side by side, pick per buzzer type (both low when idle):
  **PB9** carries a 1 kHz 50 %-duty square wave while below threshold for a
  *passive* buzzer/piezo (TIM4_CH4 hardware PWM, no ISR); **PB8** goes static
  high for an *active* self-oscillating buzzer
- **Beeper pins:** **PB9** (= TIM4_CH4, AF push-pull; sits next to GND/+5V on the
  header, so the buzzer plugs straight in) and **PB8** (plain GPIO, next door)
- **Arming:** SHIFT key on the meter — its annunciator latches, so it doubles as an
  on/off switch for the whole feature
- **On-screen indicator:** the lit 2ohm/4ohm annunciator turns into an inverted
  **CONT** (black text in a green box) while beeping
- **Threshold:** `CONTINUITY_THRESHOLD_OHMS` = 10.0 Ω (configurable)
- **Flash cost:** ~1.8 KB total (text 16,972 → 18,764 bytes)

---

## Rework (2026-07-20)

Two changes from the original 2026-06-08 version:

1. **Beeper moved PB13 → PB9.** Purely mechanical: PB9 is adjacent to GND and +5 V
   on the header, so a 3-wire buzzer lead plugs in without flying leads. PB13 is
   free again.
2. **Gating changed from "2-wire ohms" to "ohms + SHIFT".** The original inferred
   2-wire as *ohm unit present AND 4-wire annunciator off* (`Annunc[5] == 0`), which
   was never verified on hardware and silently excluded 4-wire mode. It now triggers
   in **either** 2- or 4-wire resistance mode, gated on the **SHIFT annunciator**
   (`Annunc[1]`). Because SHIFT latches on the meter, pressing it arms continuity and
   pressing it again disarms it — no more unwanted beeping during ordinary
   low-resistance measurements.

The Live-Watch variable `contIs2Wire` was accordingly replaced by `contShift`.

---

## How it works

The firmware already sniffs and decodes the 3478A's internal display bus into a
text string + annunciator bits (in `timer.c`, updated from an ISR). The new
feature reads those existing globals — it does **not** touch the capture/decode
path.

Once per main-loop pass, `ContinuityCheck()`:

1. Snapshots `displayWithPunct[]` (the reading as text incl. unit) and the
   annunciator bits, briefly disabling IRQs to avoid a torn read.
2. Detects **resistance mode** by finding the unit text (`OHM`/`KOHM`/`MOHM`/`GOHM`)
   and sets the matching multiplier (×1 / ×1e3 / ×1e6 / ×1e9). 2- and 4-wire both
   qualify.
3. Checks the **SHIFT annunciator** (`Annunc[1]`) — the arm/disarm gate.
4. Parses the numeric value with a small hand-rolled fixed-point parser
   (avoids newlib `strtod`, which would add ~15 KB of float-formatting code).
   The parser translates the 3478A's letter-digits back to digits — the meter's
   character ROM shares one code between 0/'O' and 5/'S', so a 0.1885 Ω short
   arrives in the raw decode as `" OO.188S  OHM"` (found via SWD RAM dump during
   hardware bring-up; without the translation nothing containing a 0 or 5 ever
   parsed, which is why CONT initially never appeared).
5. If `value × multiplier ≤ CONTINUITY_THRESHOLD_OHMS`, sets TIM4_CH4's compare
   register to 500 → **1 kHz square wave on PB9** and drives **PB8 high**;
   otherwise compare = 0 and PB8 low → both pins low. TIM4 free-runs from boot
   (`MX_TIM4_Init()` in `timer.c`), so gating the tone is a single register
   write — no ISR, no CPU load.

**Guards:**
- Ignores corrupted frames (decoder marks unknown cells with `?`) — buzzer holds
  its previous state until a clean frame arrives.
- Never beeps on `OVLD` / open circuit. The OVLD and unit matches run on the
  punctuation-free `displayStr`, not `displayWithPunct`: in overload the range's
  DP segment stays lit, so the punctuated string reads `"O.VLD"` — which defeats
  a naive `strstr` and which the letter-digit translation would then parse as
  0.0 Ω, latching a phantom beep on open leads (found and fixed on hardware,
  2026-07-20).
- Debounces the ON transition by one frame (a single bad decode can't blip the
  buzzer); releases immediately when the condition clears.

The on-screen indicator lives inside `DisplayAnnunciators()`: while `contBeeping`
is set, the lit 2ohm/4ohm annunciator is drawn as inverted **CONT** (black text
in a green box) instead of its name. Ohm mode is a precondition of the beep, so
one of the two slots is always lit when it fires; the cell is the same 4-character
size, so the normal draw restores the name cleanly the moment the beep stops.
(Standalone-label attempts failed on hardware: inside the reading field the label
was overdrawn every pass, and the strip below the annunciator row is hidden by
the bezel.)

---

## Files changed

### 1. `Core/Inc/main.h` — pin + threshold defines
Added after the LCD SPI pin defines:

```c
//**************************************************************************************************
// Continuity beeper (added feature)
//
// Sounds a buzzer when the 3478A is in resistance mode, the SHIFT annunciator
// is lit (press SHIFT on the meter to arm continuity), and the measured value is
// at or below CONTINUITY_THRESHOLD_OHMS. Two outputs side by side, pick per
// buzzer type (both low when idle):
//   PB9 - 1 kHz 50%-duty square wave while active (TIM4_CH4 PWM, MX_TIM4_Init
//         in timer.c) - for a PASSIVE buzzer/piezo directly between pin and 0V.
//   PB8 - plain GPIO, static high while active - for an ACTIVE (self-
//         oscillating) buzzer between pin and 0V.
// Use a transistor/NPN if the transducer draws more than a few mA.
#define BEEPER_Pin             GPIO_PIN_9    // PB9 = TIM4_CH4 - free pin next to GND/+5V
#define BEEPER_DC_Pin          GPIO_PIN_8    // PB8 - DC gate for an active buzzer
#define BEEPER_GPIO_Port       GPIOB
#define CONTINUITY_THRESHOLD_OHMS   10.0f    // beep when |R| <= this many ohms
```

### 2. `Core/Src/gpio.c` — initialize PB9 (TIM4_CH4) and PB8 (GPIO) outputs
Added in `MX_GPIO_Init()`, after the LK1 (PB0) input config: PB9 as
`GPIO_MODE_AF_PP` (the 1 kHz tone comes from TIM4, `MX_TIM4_Init` in `timer.c`),
and PB8 as a plain `GPIO_MODE_OUTPUT_PP` driven low at init:

```c
    GPIO_InitStruct.Pin = BEEPER_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    ...
    HAL_GPIO_WritePin(BEEPER_GPIO_Port, BEEPER_DC_Pin, GPIO_PIN_RESET);  // start silent
    GPIO_InitStruct.Pin = BEEPER_DC_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    ...
```

`MX_TIM4_Init()` (added to `Core/Src/timer.c`, called from `main()` right after
`MX_TIM3_Init()`) configures TIM4 for 1 kHz PWM (prescaler 71, period 999 at the
72 MHz APB1 timer clock) with CCR4 = 0 (silent) and starts it free-running.

### 3. `Core/Inc/display.h` — prototype and status vars
Added after the `FixUnitText` prototype:

```c
// Continuity beeper (added feature). The on-screen indicator is drawn by
// DisplayAnnunciators(): the lit 2ohm/4ohm annunciator becomes an inverted
// "CONT" (black on green) while contBeeping is set.
void ContinuityCheck(void);
extern volatile float   contLastOhms;     // last parsed resistance (ohms) - Live Watch
extern volatile uint8_t contIsResistance; // 1 = ohm unit detected
extern volatile uint8_t contShift;        // 1 = SHIFT annunciator lit (continuity armed)
extern volatile uint8_t contBeeping;      // 1 = buzzer currently on
```

No display coordinates are needed — the indicator reuses the 2ohm/4ohm
annunciator slot. (Two standalone-label positions were tried and abandoned
during hardware bring-up 2026-07-20: inside the reading field the label was
overdrawn every pass, and the strip below the annunciator row is hidden by the
bezel from roughly X 200 down.)

### 4. `Core/Src/display.c` — the implementation
- Removed the temporary `#include <stdlib.h>` / `#include <math.h>` (no longer
  needed after switching to the hand-rolled parser).
- Appended the `ContinuityCheck()` function (and its 4 `volatile` status globals).
  Reads `displayWithPunct[]`, `displayStr[]` and `Annunc[]` (from `timer.c`, via
  `timer.h`, which `display.c` already includes).
- In `DisplayAnnunciators()`, the ON branch swaps the label and colours for the
  2ohm/4ohm slots while `contBeeping` is set: `"CONT"` drawn black-on-green
  instead of the name green-on-black. Same cell size, full-cell repaint, so the
  name is restored cleanly the moment the beep stops.

### 5. `Core/Src/main.c` — call it in the main loop
Added inside `while (1)`: `ContinuityCheck()` right after `DisplayMain()` (so
`contBeeping` is fresh when `DisplayAnnunciators()` runs later in the pass):

```c
		DisplayMain();

		ContinuityCheck();   // Added feature: beep on low resistance in ohms mode with SHIFT lit

		HAL_Delay(10);

		// ... DisplayAnnunciators(), DisplayTrigger() unchanged ...
```

---

## Tuning knobs

| What | Where |
|------|-------|
| Beep threshold (Ω) | `CONTINUITY_THRESHOLD_OHMS` in `Core/Inc/main.h` |
| Beeper pins / port | `BEEPER_Pin` (PB9, PWM) / `BEEPER_DC_Pin` (PB8, DC) / `BEEPER_GPIO_Port` in `Core/Inc/main.h` |
| Arming annunciator | `Annunc[1]` (SHIFT) in `ContinuityCheck()`, `Core/Src/display.c` |
| "CONT" indicator style | ON-branch override in `DisplayAnnunciators()`, `Core/Src/display.c` |
| Debug / Live Watch | `contLastOhms`, `contIsResistance`, `contShift`, `contBeeping` |

---

## Wiring

Pick the pin that matches the buzzer:

- **Passive buzzer / piezo disc** between **PB9** and **0 V** — the pin supplies
  the 1 kHz drive itself. PB9 was chosen because it sits next to GND and +5 V on
  the header.
- **Active (self-oscillating) buzzer** between **PB8** and **0 V** — the pin is
  simply high while the beep is active.

Either pin can source only a few mA, so if the transducer draws more, drive it
through an NPN transistor (base via ~1 kΩ from the pin, emitter to 0 V, buzzer
from +5 V to collector, with a flyback diode across an electromagnetic buzzer).

---

## To verify on real hardware

All verified on hardware 2026-07-20. The annunciator-bit mapping (names
`{SRQ, LSTN, TLK, RMT, MATH, AZOFF, 2ohm, 4ohm, MRNG, STRG, CAL, SHIFT}` against
`Annunc[12]` down to `Annunc[1]`, so **SHIFT = `Annunc[1]`**, 2ohm = `Annunc[6]`)
was confirmed by dumping `Annunc[]` over SWD with SHIFT latched in 2-wire ohm
mode. The full chain — short → CONT + beep, open → `OVLD` → release — was
exercised on the meter.

---

## Build

```bash
make GCC_PATH="$HOME/toolchains/arm-gnu-toolchain-14.2.rel1-darwin-arm64-arm-none-eabi/bin"
```

Produces `build/3478A_VS_Display.{elf,hex,bin}`. Verified: compiles with no new
warnings or errors; `ContinuityCheck`, `contLastOhms`,
`contShift`, `contBeeping` link into the image; size after the rework
text=18,764 data=48 bss=2,616 (fits 64 KB flash / 20 KB RAM).

The current image also carries the power-on splash enabled on 2026-07-20
(text=19,308) — see `CLAUDE.md`. For flashing, see `flash_syntax.md`.
