SysConfig Generated Code: When to Trust It and When to Override It

Reviewing TI SysConfig generated code in a CC23xx embedded development workflow

Last month, an engineer on a CC23xx project spent three days chasing a UART bug. The peripheral was dropping bytes at 115200 baud, but only when the board ran on its external 32 MHz crystal instead of the default 48 MHz HFXTAL. Every line of application code checked out. The ISR was clean. The wiring was right. The root cause? SysConfig’s generated baud rate divisor was calculated for a clock source the board wasn’t using. The fix was two lines of code.

A different engineer on a different team had the opposite problem. She’d been burned by generated code before, so she hand-wrote all her GPIO initialization. Bare metal, register by register. It worked, until she added a second SPI peripheral and hit a pin mux conflict that bricked her prototype. SysConfig would have caught that conflict at build time, before it ever reached hardware.

These are the two failure modes: blind trust and irrational fear. Both cost real time and real money. You need a framework, not a gut feeling.

What SysConfig Actually Generates (and What It Doesn’t)

Your .syscfg file (the choices you made in the GUI) feeds into the SysConfig engine, which runs JavaScript-based templates and produces C files:

┌─────────────┐      ┌──────────────┐      ┌─────────────────────────┐
│  .syscfg    │ ──── │  SysConfig   │ ──── │  ti_drivers_config.c/h  │
│  (your GUI  │      │  Engine +    │      │  ti_devices_config.c    │
│   choices)  │      │  Templates   │      │  ti_radio_config.c      │
└─────────────┘      └──────────────┘      └─────────────────────────┘
     INPUT              PROCESS                   OUTPUT (generated)

Those generated files contain: pin mux assignments, driver handle declarations, peripheral initialization structs, and power/clock dependency tables.

What’s not in there: your application logic, ISR bodies, runtime reconfiguration, advanced DMA chaining, or custom power state transitions. SysConfig generates initialization-time configuration. Everything that happens after Board_init() returns is your responsibility.

This distinction matters. If you’re debugging a runtime behavior problem, the generated code probably isn’t your culprit. If you’re debugging a “peripheral won’t start” or “wrong speed” or “pin conflict” problem, it might be.

Where SysConfig Earns Your Trust

For a significant chunk of peripheral setup, the generated code is genuinely good.

Pin muxing and GPIO assignment is SysConfig’s strongest feature. It knows which pins can map to which peripherals on your specific CC23xx package variant and catches conflicts at generation time. On 48-pin parts where the mux options get dense, doing this by hand invites mistakes.

Standard peripheral init. UART at 115200, SPI at 1 MHz, I2C at 100 kHz or 400 kHz with default settings: the generated structs match TI’s driver API expectations precisely. The handle arrays and open/close boilerplate are tedious to write by hand and easy to mess up. Let SysConfig do it.

For typical BLE or simple proprietary RF, the generated power policy code handles active/standby transitions correctly. If you’re building something close to TI’s example projects, the defaults work.

If the SysConfig GUI exposed the exact parameter you needed, and you set it to the value you wanted, trust the output. The templates are well-tested for the happy path.

Where Generated Code Gets Suspicious

The trouble starts at the edges. SysConfig’s GUI can’t expose every register bit for every peripheral mode, and its templates make assumptions that may not match your hardware.

Non-standard clock sources or frequencies. Your board might use a 32 MHz crystal instead of the expected 48 MHz. Or maybe you need 250000 baud for a specific sensor protocol. Either way, SysConfig calculates the divisor based on the clock source it thinks you’re using. If your board design diverges from that assumption, verify the math yourself against the CC23xx Technical Reference Manual.

Advanced peripheral modes. SysConfig may not surface all the configuration bits you need. Specific SPI phase/polarity combinations with DMA, UART hardware flow control edge cases, or timer capture modes with unusual prescaler settings can fall outside what the GUI can express. The generated code won’t be wrong exactly; it’ll be incomplete.

Custom power policies. If your device needs to wake from standby on a GPIO edge, transition through a custom low-power state, or coordinate sleep with multiple RF protocols, the generated power policy code probably doesn’t match your requirements. Power management is where SysConfig’s one-size-fits-most approach breaks down fastest.

Multi-peripheral timing dependencies. SysConfig configures each peripheral in isolation. It doesn’t know that UART TX must complete before SPI CS asserts. It doesn’t know that ADC sampling must synchronize to timer overflow. Sequencing and coordination are invisible to the generation engine.

Radio configurations at the edges. Custom PHY settings, non-standard TX power levels, or frequency offsets don’t always round-trip cleanly through the GUI. If you’re doing anything beyond stock BLE 1M/2M PHY or standard proprietary modes, read the generated ti_radio_config.c carefully.

Quick reference:

TRUST when:                          VERIFY/OVERRIDE when:
─────────────                        ────────────────────
Pin mux assignments                  Non-default clock sources
Standard baud/speed configs          Fractional baud rates
Default power policies               Custom sleep/wake schemes
Driver handle boilerplate            Advanced DMA configurations
Common RF settings (BLE default)     Custom PHY / TX power edges

Three Safe Override Patterns

You’ve found a problem in the generated code. How do you fix it without creating a maintenance disaster?

Never edit the generated files directly. Use one of these three patterns instead.

Pattern 1: Post-Init Override in Application Code

This is the right choice for 1 or 2 targeted fixes. After Board_init() or after a specific Driver_open() call, write directly to the registers or struct fields you need to correct.

#include <ti/drivers/UART2.h>
#include <ti/devices/cc23x0/inc/hw_uart.h>

/* SysConfig inits UART0 with default divisor. Override for 32 MHz crystal. */
UART2_Handle uart = UART2_open(CONFIG_UART2_0, &uartParams);

/* Correct the baud rate divisor for our actual clock source */
HWREG(UART0_BASE + UART_O_IBRD) = 17;   /* Integer divisor for 115200 @ 32MHz */
HWREG(UART0_BASE + UART_O_FBRD) = 23;   /* Fractional divisor */

This survives regeneration because it lives in your code, not the generated files. Add a comment explaining why you’re overriding, not just what you’re changing.

Pattern 2: Custom Board File

When you’ve accumulated 3 or more overrides, or when the generated structure is fundamentally wrong for your hardware, copy ti_drivers_config.c into your own source tree. Rename it (say, board_config.c). Exclude the generated version from your build.

You own that file. You can modify it freely.

The tradeoff: you’re responsible for synchronizing with SysConfig updates. When you upgrade your SDK or change a SysConfig setting, diff the newly generated file against your custom copy. Merge deliberately. A block comment at the top listing your divergences will save your future self (or your teammate) hours.

Pattern 3: SysConfig Scripting and Template Modification

SysConfig supports custom modules and template overrides. You can write JavaScript that hooks into the generation pipeline and alters the output programmatically.

This makes sense when you’re building a reusable board support package for a product family, where the same overrides apply across 5+ boards. For a single product, it’s overkill. TI’s SysConfig documentation covers the scripting API if you go this route.

OVERRIDE DECISION FLOW:

  How many things need changing?
         │
    ┌────┴────┐
    │  1-2    │──── Pattern 1: Post-init override
    └─────────┘
         │
    ┌────┴────┐
    │  3+     │──── Pattern 2: Custom Board file
    └─────────┘
         │
    ┌────┴──────┐
    │ Reusable  │── Pattern 3: SysConfig scripting
    │ across    │
    │ products? │
    └───────────┘

Why Editing Generated Files Is Always Wrong

The build system regenerates those files on every build (or should, if your project is configured correctly). Any edits to ti_drivers_config.c will be silently overwritten the next time a teammate builds, or the next time you clean and rebuild.

Here’s how this plays out: a developer edits the generated file on a Thursday afternoon. It works. They commit. On Monday, a colleague pulls, does a clean build, and the bug reappears. Nobody connects the two events because the diff shows no changes to application code. Two days later, someone notices the generated file no longer contains the fix.

Use Patterns 1 through 3.

Verification Habits That Catch Problems Early

A few practices that save disproportionate debugging time:

Diff generated files after SysConfig changes. Run git diff after any SysConfig modification. Read the changes. If something shifted that you didn’t expect, investigate before you test. This takes 2 minutes and can save 2 days.

Read the generated code at least once per peripheral. It’s C. It’s readable. You don’t need to memorize it, but you should understand the initialization sequence for every peripheral your product depends on. If you’re integrating a BLE device with Hubble’s network, for instance, understanding how your BLE advertising packet configuration interacts with the generated radio config is especially important.

Compare register values against the TRM. For critical peripherals (anything where wrong configuration means data corruption or hardware damage), pull up the CC23xx Technical Reference Manual and verify that the generated register writes match your expectations. This is especially true for clock dividers and power control registers.

Add startup self-tests. A few assert() calls that read back key register values after Board_init() cost almost nothing and catch generated-code regressions instantly. If UART0’s baud rate divisor isn’t what you expect, you’ll know on the first boot, not after three days of chasing dropped bytes.

The Trust-Verify-Override Checklist

For every peripheral in your design, ask three questions: Is this a standard configuration that SysConfig handles well? Does my hardware deviate from TI’s reference design in any way that affects this peripheral? Do I need runtime behavior that goes beyond what init-time configuration can provide?

Trust for the standard cases. Verify for the edge cases. Override with the right pattern when you need to.


Hubble Network enables Bluetooth connectivity from any chip to any satellite—no extra hardware, no line of sight. See how it works →