SX126x Load Capacitance Calibration
Background
Why a calibration value is needed
The SX126x radio IC from Semtech uses a crystal oscillator (XO) as its frequency reference. To compensate for component tolerances in the crystal and its load capacitors, Semtech introduced a software-configurable trim register that adjusts the effective load capacitance seen by the crystal. Setting this register correctly ensures the radio operates at the intended center frequency, which is critical for LoRaWAN conformance and reliable communication.
In earlier SX127x-based designs, this compensation was handled entirely in hardware. The SX126x moved it into firmware, which means every application that uses the radio must explicitly write the correct trim value during initialization.
Crystal change due to EOL and sourcing constraints
The original crystal specified for Miromico SX126x-based LoRaWAN modules reached end-of-life (EOL) status. To maintain supply continuity, a replacement crystal from an alternate source was qualified and introduced into production.
The replacement crystal has a different nominal load capacitance requirement. As a result, modules produced after the crystal change require a different trim value compared to older modules. Using the wrong value causes a frequency offset that can degrade link budget, increase packet error rate, or prevent network join entirely in tight radio environments.
The trim value is referred to throughout this documentation as the LoadCAP value.
LoadCAP value by module generation
| Module series | LoadCAP value | Notes |
|---|---|---|
| Legacy (original crystal) | 18 (0x12) | Default fallback if no value is found |
| Current production | see Loadcap Vault | Individual value per module, stored in flash and database |
If you are unsure which series your module belongs to, look up the serial number in the Loadcap Vault. If no entry is found, use the default value of 18 (0x12).
Options for obtaining the LoadCAP value
Option 1: Read from module flash (recommended)
Each module stores its LoadCAP value in the last flash page (page 255), protected by a magic word and a CRC32 checksum. This can be read before the flash is erased for the first time.
Warning
This is a one-time opportunity. Once the flash page is erased (for example, during initial firmware programming), the value can no longer be recovered from the module itself. Read and persist the value before erasing.
Flash layout
The structure is stored at ADDR_FLASH_PAGE_255 and occupies 12 bytes.
| Offset | Size | Field | Example value | Description |
|---|---|---|---|---|
0x0000 |
4 bytes | magic_word |
0xCAFEBABE |
Fixed validation marker. If this does not match, the page has not been written or has been erased. |
0x0004 |
4 bytes | tuning_value |
0x00000019 (25) |
LoadCAP trim value as a little-endian uint32_t. The relevant range fits in the lower byte. |
0x0008 |
4 bytes | crc32 |
0x83779B14 |
CRC32 over the preceding 8 bytes (magic_word + tuning_value), stored little-endian. |
Note
The CRC32 is calculated over the raw little-endian byte representation of the
struct. For the example above: input bytes are BE BA FE CA 19 00 00 00,
result is 0x83779B14.
Validation procedure
- Read 12 bytes from
ADDR_FLASH_PAGE_255. - Verify
magic_word == 0xCAFEBABE. If not, the page has not been written or has been erased. - Calculate CRC32 over the first 8 bytes and compare against the stored
crc32field. If mismatched, the data is corrupt. - If both checks pass, use the lower byte of
tuning_valueas the LoadCAP value.
If either check fails, fall back to the Loadcap Vault (Option 2) or the default value for your module series (Option 3).
Option 2: Loadcap Vault
Miromico operates a lookup service at loadcap.miromico.ch where you can retrieve the individual LoadCAP value for any current-generation module by Serial number or MCU ID.
The service is also available as a REST API for automated use during production or commissioning. See API reference below.
Option 3: Use the default value
If you cannot obtain the individual value through Options 1 or 2, use the default value appropriate for your module series (see table above). For legacy modules the default is 18 (0x12). For current-generation modules, using the default introduces a small frequency offset. Whether this is acceptable depends on your deployment environment and link budget.
Applying the LoadCAP value in firmware
The LoadCAP value must be written to the SX126x before the LoRaWAN stack is started and before any radio transmission or reception takes place.
The exact API or register to use depends on your radio driver or LoRaWAN stack. The general pattern is:
- Read the LoadCAP value from your persistent configuration storage (NVM, EEPROM, or equivalent).
- Pass the value to your SX126x driver initialization function that sets the XO trim / load capacitance register.
- Proceed with LoRaWAN stack initialization.
If the value is missing or invalid at startup, your firmware should fall back to the appropriate default value for your module series and log a warning.
Example (pseudocode)
uint8_t load_cap = config_read_load_cap();
if (!load_cap_is_valid(load_cap)) {
load_cap = LOAD_CAP_DEFAULT; /* 18 (0x12) for legacy, or series-specific default */
log_warning("LoadCAP not set, using default");
}
sx126x_set_xo_trim(load_cap);
lorawan_stack_init();
The specific function name (sx126x_set_xo_trim in this example) depends on the driver you are using. Refer to your driver or stack documentation for the correct call.
Persisting the value
We recommend the following approach for first-time firmware programming:
- Before erasing the module flash, read the LoadCAP value from the last flash page and validate the magic number.
- Write the value to your application's configuration partition or NVM alongside other commissioning data such as LoRaWAN keys.
- On every subsequent boot, read from configuration storage and apply to the SX126x.
Alternatively, retrieve the value from the Loadcap Vault API at commissioning time and write it directly to configuration storage without needing to access module flash.
API reference
The Loadcap Vault API is a simple HTTP GET interface. Exactly one query parameter must be provided per request.
Endpoint
Parameters
| Parameter | Type | Description |
|---|---|---|
serial |
string | Serial number from the module label |
mcu_id |
string | MCU ID readable from firmware |
Both parameters are case-insensitive. Exactly one must be provided.
Responses
| HTTP status | Meaning |
|---|---|
200 OK |
Value found, returned in data object |
404 Not Found |
No entry for the given identifier |
400 Bad Request |
Neither or both parameters were provided |
Success response body
{
"data": {
"serial": "MM-123456",
"mcu_id": "AABBCCDD00112233",
"load_cap_value": 47,
"updated_at": "2025-01-15"
}
}
Error response body
curl examples
Lookup by serial number:
Lookup by MCU ID:
Scripted use with jq to extract just the integer value:
Integration in production scripts
The API is suitable for automated use during device commissioning. A typical production flow:
- Read MCU ID from the module via your test interface.
- Query the API:
GET /api/load-cap?mcu_id=<MCU_ID>. - On
200, extractload_cap_valueand write it to the device configuration alongside LoRaWAN keys. - On
404, fall back to the default value for the module series and flag the device for review.
Frequently asked questions
What happens if I use the wrong LoadCAP value?
The radio will operate at a slightly incorrect center frequency. In practice this means a reduced link budget and potentially higher packet error rate. In environments with strong signal the effect may not be noticeable. In marginal coverage conditions, or on networks with tight frequency filters, it can prevent successful communication.
How often do I need to apply the value?
On every device startup, before the LoRaWAN stack initializes. The value does not need to be reapplied during normal operation.
The Loadcap Vault returns no result for my module. What should I do?
Use the default value 18 (0x12). Your module is likely from the legacy series that used the original crystal. If you believe the module is from current production and the value should be available, contact support@miromico.ch with the serial number.
Can I read the value from flash after the module has already been programmed?
Only if the last flash page was not erased during programming. If it was erased, the value can no longer be recovered from the module. Use the Loadcap Vault or the default value.
Is the Loadcap Vault API rate-limited?
Retrieve the value once per module during commissioning and persist it to your device's configuration storage. Do not query the API at device runtime.