# 34970A-style SCPI firmware for the Eletechsup UD85C08 (MindMotion MM32F0010A1T)
#
#   make test            build and run the host unit tests (needs only a C compiler)
#   make coverage        line/branch coverage of core/ by the unit tests (gcc, gcov)
#   make fuzz            run every fuzzer for FUZZ_TIME seconds (clang, libFuzzer)
#   make                 build the firmware (needs arm-none-eabi-gcc)
#   make size            flash/RAM usage
#   make stack           worst-case stack depth against the free RAM
#   make info            read-only: show chip ID and read-protection state
#   make dump            try to read the existing firmware over SWD (OpenOCD)
#   make erase CONFIRM_ERASE=1
#                        erase the whole chip; also removes read protection
#                        (then power cycle the board)
#   make flash           program the board (this ERASES what's on it)
#   make hw-check        board registers over SWD, and the measured core clock
#   make hw-test         firmware behaviour over the serial port (HW_PORT)
#   make rig-test        a session with rigkit's HP34970A driver
#   make hw-watchdog     hang the firmware on purpose: the watchdog must reset it
#
# Build-time options (UART_BAUD=9600, FAST_INPUT=1, ...): see BUILDING.md.

.DEFAULT_GOAL := all
TARGET  ?= ud85c08
BUILD   := build

# ------------------------------------------------------------ host unit tests
HOSTCC     ?= cc
# Warnings for every build, firmware and host: all clean, kept that way by -Werror.
WARNINGS   := -Wall -Wextra -Werror -Wshadow -Wconversion -Wsign-conversion -Wcast-qual \
              -Wcast-align -Wstrict-prototypes -Wmissing-prototypes -Wmissing-declarations \
              -Wundef -Wdouble-promotion -Wformat=2 -Wpointer-arith -Wvla -Wnull-dereference \
              -Wredundant-decls -Wswitch-default -Wswitch-enum
# gcc only: clang doesn't know these
GCC_WARNINGS := -Wjump-misses-init -Wlogical-op -Wduplicated-cond -Wduplicated-branches
HOST_WARNINGS = $(WARNINGS) $(if $(findstring clang,$(HOSTCC)),,$(GCC_WARNINGS))
TEST_FLAGS  = -std=c11 $(HOST_WARNINGS) -g -fsanitize=address,undefined -fno-sanitize-recover=all -Icore -Itests
CORE_SRCS  := core/scpi.c core/chlist.c core/relay_dev.c core/rxfifo.c core/counters.c core/store.c core/led.c
TEST_SRCS  := $(filter-out tests/fuzz_%.c,$(wildcard tests/*.c))

.PHONY: test
test: $(BUILD)/unit_tests
	./$(BUILD)/unit_tests

$(BUILD)/unit_tests: $(CORE_SRCS) $(TEST_SRCS) $(wildcard core/*.h tests/*.h) | $(BUILD)
	$(HOSTCC) $(TEST_FLAGS) $(CORE_SRCS) $(TEST_SRCS) -o $@

# Line and branch coverage of core/ by the unit tests (gcc + gcov). Reports
# per file; the annotated sources end up in build/cov/*.gcov.
COV_DIR := $(BUILD)/cov

.PHONY: coverage
coverage: | $(BUILD)
	rm -rf $(COV_DIR) && mkdir -p $(COV_DIR)
	for f in $(CORE_SRCS); do \
	  $(HOSTCC) -std=c11 $(HOST_WARNINGS) -g -O0 --coverage -Icore \
	    -c $$f -o $(COV_DIR)/$$(basename $$f .c).o || exit 1; done
	$(HOSTCC) -std=c11 $(HOST_WARNINGS) -g -O0 --coverage -Icore -Itests \
	  $(COV_DIR)/*.o $(TEST_SRCS) -o $(COV_DIR)/unit_tests
	cd $(COV_DIR) && ./unit_tests > /dev/null
	gcov -b -o $(COV_DIR) $(CORE_SRCS) | grep -A4 "^File '.*core/"
	mv *.gcov $(COV_DIR)/
	@! grep -n -E 'branch +[0-9]+ (taken 0%|never executed)|^ +#####' $(COV_DIR)/*.c.gcov \
	  || { echo "^^^ not covered"; exit 1; }

# Coverage-guided fuzzing (clang + libFuzzer, with ASan and UBSan): every
# tests/fuzz_*.c, FUZZ_TIME seconds each. Corpora are kept in build/fuzz/;
# a crash leaves its input there as crash-*.
#   fuzz_relay_dev  bytes into the whole device: invariants and recovery
#   fuzz_chlist     channel lists against an independent reference parser
#   fuzz_rxfifo     put/lost/get sequences against a reference queue
#   fuzz_store      arbitrary flash contents must load, and save-then-load round-trip
FUZZ_CC   ?= clang
FUZZ_TIME ?= 60
FUZZ_DIR  := $(BUILD)/fuzz
FUZZERS   := $(patsubst tests/%.c,%,$(wildcard tests/fuzz_*.c))
FUZZ_FLAGS = -std=c11 -g -O1 -fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all \
             $(WARNINGS) -Wno-missing-prototypes -Icore -Itests

.PHONY: fuzz fuzz-build
fuzz-build: $(FUZZERS:%=$(FUZZ_DIR)/%)

$(FUZZ_DIR)/fuzz_%: tests/fuzz_%.c $(CORE_SRCS) tests/fixture.c $(wildcard core/*.h tests/*.h)
	@mkdir -p $(FUZZ_DIR)
	$(FUZZ_CC) $(FUZZ_FLAGS) $(CORE_SRCS) tests/fixture.c $< -o $@

fuzz: fuzz-build
	@for f in $(FUZZERS); do \
	  mkdir -p $(FUZZ_DIR)/corpus-$$f; \
	  echo "== $$f, $(FUZZ_TIME) s"; \
	  $(FUZZ_DIR)/$$f -max_total_time=$(FUZZ_TIME) -max_len=2048 -print_final_stats=1 \
	    -artifact_prefix=$(FUZZ_DIR)/$$f- $(FUZZ_DIR)/corpus-$$f 2>&1 \
	    | grep -E 'stat::number_of_executed_units|ERROR|SUMMARY|crash|Done' || true; \
	  failed=$$(find $(FUZZ_DIR) -maxdepth 1 -name "$$f-crash-*" -o -name "$$f-leak-*" -o -name "$$f-timeout-*"); \
	  [ -z "$$failed" ] || { echo "$$failed"; echo "$$f: failing input saved"; exit 1; }; \
	done

# ------------------------------------------------------------------ firmware
# Build-time options, all optional (BUILDING.md): `make NAME=value`.
# Strings, without quotes: make SERIAL_NUMBER=RIG3-DIO1
STRING_OPTIONS := MANUFACTURER MODEL SERIAL_NUMBER FW_VERSION
# Numbers
NUMBER_OPTIONS := NUM_RELAYS UART_BAUD OPERATE_MS RELEASE_MS SAVE_INTERVAL_S SAVE_IDLE_MS WATCHDOG_MS

# Reset clock is HSI/6 = 8 MHz (measured on the board with SysTick).
F_CPU   ?= 8000000

CROSS   ?= arm-none-eabi-
CC      := $(CROSS)gcc
OBJCOPY := $(CROSS)objcopy
SIZE    := $(CROSS)size

CPU     := -mcpu=cortex-m0 -mthumb
CFLAGS  := $(CPU) -Os -g -std=c11 $(WARNINGS) $(GCC_WARNINGS) \
           -ffreestanding -ffunction-sections -fdata-sections -fcallgraph-info=su \
           -Icore -Imcu -DF_CPU=$(F_CPU)u \
           $(foreach v,$(STRING_OPTIONS),$(if $($(v)),-D$(v)='"$($(v))"')) \
           $(foreach v,$(NUMBER_OPTIONS),$(if $($(v)),-D$(v)=$($(v))u)) \
           $(if $(filter 1,$(FAST_INPUT)),-DFAST_INPUT) \
           $(EXTRA_CFLAGS)
LDFLAGS := $(CPU) -nostartfiles -nostdlib -T link.ld \
           -Wl,--gc-sections -Wl,-Map=$(BUILD)/$(TARGET).map

FW_SRCS := mcu/startup.c mcu/hal_mm32f0010.c mcu/main.c mcu/libc_min.c $(CORE_SRCS)
FW_OBJS := $(FW_SRCS:%.c=$(BUILD)/fw/%.o)

# ------------------------------------------------------- programmer (OpenOCD)
# Pick the interface config for your probe, e.g. interface/jlink.cfg,
# interface/stlink.cfg or interface/cmsis-dap.cfg. See README.md.
OPENOCD           ?= openocd
OPENOCD_INTERFACE ?= interface/jlink.cfg
OPENOCD_SPEED     ?= 1000
OPENOCD_FLAGS     := -f $(OPENOCD_INTERFACE) -f mm32f0010.cfg \
                     -c "adapter speed $(OPENOCD_SPEED)"

.PHONY: all size info flash dump erase clean
all: $(BUILD)/$(TARGET).elf $(BUILD)/$(TARGET).bin $(BUILD)/$(TARGET).hex size

$(BUILD):
	mkdir -p $@

$(BUILD)/fw/%.o: %.c $(wildcard core/*.h mcu/*.h)
	@mkdir -p $(dir $@)
	$(CC) $(CFLAGS) -c $< -o $@

$(BUILD)/$(TARGET).elf: $(FW_OBJS) link.ld
	$(CC) $(LDFLAGS) $(FW_OBJS) -lgcc -o $@

$(BUILD)/$(TARGET).bin: $(BUILD)/$(TARGET).elf
	$(OBJCOPY) -O binary $< $@

$(BUILD)/$(TARGET).hex: $(BUILD)/$(TARGET).elf
	$(OBJCOPY) -O ihex $< $@

size: $(BUILD)/$(TARGET).elf
	$(SIZE) $<

# Worst-case stack depth from gcc's call graph (-fcallgraph-info), against
# the RAM left after .data and .bss; see tools/stack_usage.py.
.PHONY: stack
stack: $(BUILD)/$(TARGET).elf
	python3 tools/stack_usage.py $< $(FW_OBJS:.o=.ci)

info:
	$(OPENOCD) $(OPENOCD_FLAGS) -c init -c mm32_info -c shutdown

# Reads 16 KB from flash. A read-protected chip gives garbage or an error.
dump: | $(BUILD)
	$(OPENOCD) $(OPENOCD_FLAGS) -c init -c "mm32_dump $(BUILD)/dump.bin" -c shutdown

erase:
	@test "$(CONFIRM_ERASE)" = "1" || { \
	  echo "Refusing to erase: this wipes the whole chip, including the stock firmware."; \
	  echo "Run: make erase CONFIRM_ERASE=1"; exit 1; }
	$(OPENOCD) $(OPENOCD_FLAGS) -c init -c mm32_erase -c shutdown

flash: all
	$(OPENOCD) $(OPENOCD_FLAGS) -c init -c "mm32_flash $(BUILD)/$(TARGET).bin" -c shutdown

# ------------------------------------------------------- hardware checks
# hw-check: registers of the running firmware, over SWD (read-only), and the
#   core clock, measured with SysTick (resets the board afterwards).
# hw-test:  the firmware's behaviour over its serial port (switches relays).
# hw-watchdog: hangs the firmware on purpose; the watchdog must reset it.
# rig-test: a session with rigkit's HP34970A driver; RIGKIT is its src/.
HW_PORT ?= /dev/ttyUSB0
HW_BAUD ?= $(or $(UART_BAUD),57600)
RIGKIT  ?= ../../rigkit/rigkit/src

.PHONY: hw-check hw-test rig-test hw-watchdog
hw-check:
	$(OPENOCD) $(OPENOCD_FLAGS) -c init -c "mm32_check $(F_CPU) $(HW_BAUD)" -c mm32_clock -c shutdown

hw-test:
	python3 tools/hw_test.py --port $(HW_PORT) --baud $(HW_BAUD)

# Hangs the firmware through SWD and checks over serial that the watchdog
# reset the board (-310 in the error queue).
hw-watchdog: $(BUILD)/$(TARGET).elf
	$(OPENOCD) $(OPENOCD_FLAGS) -c init \
	  -c "mm32_hang 0x$$($(CROSS)nm $< | awk '/ Default_Handler$$/ {print $$1}')" -c shutdown
	python3 tools/hw_test.py --port $(HW_PORT) --baud $(HW_BAUD) --watchdog

rig-test:
	python3 tools/rig_test.py --port $(HW_PORT) --baud $(HW_BAUD) --rigkit $(RIGKIT)

clean:
	rm -rf $(BUILD)
