I create test programs that each contain a set of unit tests. It gets flashed and run, and watched by a scripted GDB session which collects results and outputs a report. The harness that runs the tests is a simple python program that generates GDB scripts and runs them via openocd, then tallies up the reports for progress/pass-fail/etc. All output is captured (openocd, GDB, SWO, and serial port) to help narrow down failures.
Also, and this is not limited to embedded/firmware, but also large software systems I've worked on: massive GDB invariant scans that can walk memory and check EVERYTHING. Every heap allocation, pointer, data, values -- all state. Since it's GDB and Unix it can also operate against a core dump, making it exceedingly handy in that it can tell you up front what state is corrupt so provides extremely useful clues on a segfault or bus error, or even hamfisted kills of stuck processes. (Most likely deadlocked or they closed their listen port or something.) At one place we had a hard rule: you're not done until you have a unit test, documentation, reviewed code, AND a GDB TCL script to invariant check your state (or updates to existing checkers) against a live system or core dump.