MSPM0 Code Generation
The CodeGenerator's libxr mspm0 setup command brings a TI SysConfig project to LibXR: it parses the .syscfg file at the project root and generates C++ initialization code. SysConfig configures the clocks, pins, DMA channels and peripherals, which main.c initializes with SYSCFG_DL_init(); the generated code constructs the LibXR objects from the macros SysConfig generated, and an XRobot project also gets the XR_REGISTER registrations.
Quick Start
Run in the SysConfig project root:
libxr mspm0 setup -d .
The project root holds one .syscfg file. The command performs these steps:
- When the project directory has no
.gitignoreyet, create one that ignoresbuild,.history,.cache,CMakeFilesand.config.yaml; an existing.gitignorestays unchanged; - parse the
.syscfginto.config.yaml; - generate
app_main.cpp,app_main.handlibxr_config.yamlunderUser/.
When neither --xrobot nor --no-xrobot is given, the output file keeps its choice and a new file does not use XRobot; with XRobot the command generates the XR_REGISTER registrations and XROBOT_MAIN();, and before it ends it suggests running xrobot gen to bring User/xrobot_main.hpp up to date. When the existing app_main.cpp was not generated by libxr gen, setup prints a warning that lists the lines to be replaced; everything outside the User Code regions is regenerated.
Where the SysConfig Output Comes From
Parsing reads two sources: the .syscfg itself (peripheral instances, parameters and pin assignments) and the ti_msp_dl_config.h/.c that SysConfig generates (the macro names actually emitted, CPUCLK_FREQ, the ULPCLK divider and the clock configuration of each instance).
When the environment variables SYSCONFIG_TOOL and MSPM0_SDK_INSTALL_DIR are set, parsing runs SysConfig once on the .syscfg and writes the output to a temporary directory; these are the same variables the BSP's cmake/MSPM0SysConfig.cmake uses. Without them, parsing reuses a generated output newer than the .syscfg under build*, cmake-build* or out in the project. Reuse compares modification times only: a .syscfg replaced by one with an older modification time still reuses the old output, so build once more, or set the two variables and run again.
The BSP's CMake runs SysConfig on the .syscfg at configure time and writes the output to the syscfg/ directory of the build tree; a change to the .syscfg triggers regeneration, see MSPM0 Environment Setup.
Example Output
The following is the first run on a copy of bsp-mspm0g3507-mini with the three generated files under User/ removed, with the two environment variables set:
$ libxr mspm0 setup -d . --xrobot
[INFO] Processing mspm0g3507_minidb48.syscfg...
[INFO] Running SysConfig on mspm0g3507_minidb48.syscfg
[INFO] Configuration exported to: .\.config.yaml
[INFO] System: bare metal
[INFO] .\User\libxr_config.yaml does not exist; creating it with the default settings
[INFO] terminal_source is not set; using uart0
[INFO] Generated User: wrote app_main.cpp, app_main.h, libxr_config.yaml
[INFO] Next: run `xrobot gen` to bring User/xrobot_main.hpp up to date; libxr setup does not update it
===== [Configuration Summary] =====
MCU: MSPM0 MSPM0G3507
GPIO (4 pins):
LED1: PB8, OUTPUT
LED2: PA16, OUTPUT
KEY1: PB24, INPUT, FALL interrupt
KEY2: PB20, INPUT, FALL interrupt
Active Peripherals:
UART: 2 instance(s)
UART_0
UART_1
I2C: 2 instance(s)
I2C_0
I2C_1
SPI: 1 instance(s)
SPI_1
PWM: 1 instance(s)
PWM_TIMA1
Other: 5 instance(s)
ADC12_0
ADC12_1
DAC0
MCAN_0
QEI_TIMG8
Later runs rewrite only the files whose content changes. Other in the summary lists the configurations LibXR has no driver for, see Generated Objects and Naming.
Output Layout
After the run, the project gains or has these files modified:
.
├── .config.yaml # the configuration file written by parsing
├── main.c # BSP-provided: calls app_main() after SYSCFG_DL_init()
├── User/
│ ├── app_main.cpp # the entry source file
│ ├── app_main.h # the header of the entry source file
│ └── libxr_config.yaml # the LibXR configuration file
└── libxr/ # Git submodule: LibXR itself
app_main.cpp
app_main.cpp contains the LibXR initialization function app_main(). Code between /* User Code Begin N */ and /* User Code End N */ survives regeneration; the rest is regenerated every time. The generated peripheral objects are static and stay valid after app_main() returns; long-lived local objects are declared static. A peripheral object can be passed to a thread or a module as a base-class pointer, see Integrate with XRobot.
The following is an excerpt of the app_main.cpp of that first run, with // ... marking omissions:
// Generated by `libxr gen`; do not edit by hand.
// Generated from mspm0g3507_minidb48.syscfg (MSPM0G3507); SysConfig configures the
// clocks, pins, DMA channels and peripherals (SYSCFG_DL_init() in main.c). Put
// application code between the "User Code Begin" and "User Code End" markers.
#include "app_main.h"
#include "libxr.hpp"
#include "mspm0_gpio.hpp"
#include "mspm0_i2c.hpp"
#include "mspm0_power.hpp"
#include "mspm0_pwm.hpp"
#include "mspm0_spi.hpp"
#include "mspm0_timebase.hpp"
#include "mspm0_uart.hpp"
#include "ti_msp_dl_config.h"
#include "xrobot_main.hpp"
using namespace LibXR;
/* User Code Begin 1 */
/* User Code End 1 */
// Input clocks: the BUSCLK of the power domain. PD0 from ULPCLK = CPUCLK_FREQ / 2
// (DL_SYSCTL_setULPCLKDivider) and PD1 from MCLK (CPUCLK_FREQ). The I2C driver resets
// the clock configuration of its peripherals, so the BUSCLK of the domain is the right
// argument.
static constexpr uint32_t PD0_BUSCLK_FREQ = (CPUCLK_FREQ / 2);
static constexpr uint32_t PD1_BUSCLK_FREQ = CPUCLK_FREQ;
// Ownership of the DMA channels that serve a UART transmitter;
// MSPM0_UART_MAIN_INIT checks these names against the ones SysConfig generated.
#define DMA_CH_UART0_TX_LIBXR_UART_IRQN UART_0_INST_INT_IRQN
#define DMA_CH_UART0_TX_LIBXR_UART_TX 1
// ...
// DMA buffers. A UART gets 2 x N bytes for its two transmit halves; the receive side
// runs on byte interrupts. The SPI DMA buffers are split in two halves as well; the
// I2C driver uses polling, its buffer is the staging area for DMA transfers.
alignas(size_t) static uint8_t uart0_tx_buf[2 * 128];
// ...
alignas(4) static uint8_t i2c0_buf[32];
// ...
extern "C" void app_main(void)
{
/* User Code Begin 2 */
/* User Code End 2 */
// Timebase and platform
static MSPM0Timebase timebase;
PlatformInit();
static MSPM0PowerManager power_manager;
// GPIO: SysConfig configured LED1 (PB8), LED2 (PA16) as output pins and KEY1 (PB24),
// KEY2 (PB20) as input pins. The interrupt edge comes from SysConfig; RegisterCallback
// and EnableInterrupt belong to the consumer (a Module or the User Code).
static MSPM0GPIO LED1(GPIO_LEDS_PIN_LED1_PORT, GPIO_LEDS_PIN_LED1_PIN,
GPIO_LEDS_PIN_LED1_IOMUX);
// ...
static MSPM0GPIO KEY1(GPIO_KEYS_PORT, GPIO_KEYS_PIN_KEY1_PIN, GPIO_KEYS_PIN_KEY1_IOMUX);
static MSPM0GPIO KEY2(GPIO_KEYS_PORT, GPIO_KEYS_PIN_KEY2_PIN, GPIO_KEYS_PIN_KEY2_IOMUX);
// UART: TX with DMA; the receive side runs on byte interrupts.
static MSPM0UART uart0(MSPM0_UART_MAIN_INIT(UART_0, DMA_CH_UART0_TX, uart0_tx_buf,
sizeof(uart0_tx_buf), 5, 128));
// ...
// I2C: polling; the buffer stages DMA transfers.
static MSPM0I2C i2c0(MSPM0_I2C_INIT(I2C_0, PD0_BUSCLK_FREQ, i2c0_buf, sizeof(i2c0_buf),
8), {I2C_0_BUS_SPEED_HZ});
// ...
// SPI: DMA for transfers longer than the threshold.
static MSPM0SPI spi1(MSPM0_SPI_INIT(SPI_1, PD1_BUSCLK_FREQ, DMA_CH_SPI1_RX,
DMA_CH_SPI1_TX, spi1_rx_buf, sizeof(spi1_rx_buf), spi1_tx_buf,
sizeof(spi1_tx_buf), 3));
// PWM: the channels of one timer share one period, which SysConfig configures.
static MSPM0PWM pwm_tima1_c0(MSPM0_PWM_CH(PWM_TIMA1, 0));
static MSPM0PWM pwm_tima1_c1(MSPM0_PWM_CH(PWM_TIMA1, 1));
// Terminal on uart0
STDIO::read_ = uart0.read_port_;
STDIO::write_ = uart0.write_port_;
static RamFS ramfs("XRobot");
static Terminal<32, 32, 5, 5> terminal(ramfs);
static auto terminal_task = Timer::CreateTask(terminal.TaskFun, &terminal, 10);
Timer::Add(terminal_task);
Timer::Start(terminal_task);
// Hardware registration
XR_REGISTER(power_manager, LibXR::PowerManager);
XR_REGISTER(LED1, LibXR::GPIO);
// ...
XR_REGISTER(pwm_tima1_c0, LibXR::PWM);
// ...
XR_REGISTER(spi1, LibXR::SPI);
XR_REGISTER(uart0, LibXR::UART);
// ...
XR_REGISTER(i2c0, LibXR::I2C);
// ...
XR_REGISTER(ramfs, LibXR::RamFS);
XR_REGISTER(terminal, LibXR::Terminal<32, 32, 5, 5>);
/* User Code Begin 3 */
/* User Code End 3 */
XROBOT_MAIN();
}
Every peripheral object is constructed from macros SysConfig generated, such as GPIO_LEDS_PIN_LED1_PORT for a GPIO or UART_0 for a UART. The interrupts these objects use are enabled and prioritized by the LibXR drivers at construction, overriding the interrupt settings SysConfig made for the same IRQs. The edge of a pin interrupt comes from SysConfig, and the consumer registers the callback (RegisterCallback and EnableInterrupt) in a Module or a User Code region, for example to count the interrupts of KEY1:
/* User Code Begin 1 */
#include <atomic>
static std::atomic<uint32_t> key1_irq_count{0};
static void OnKeyInterrupt(bool in_isr, std::atomic<uint32_t>* count)
{
(void)in_isr;
count->fetch_add(1, std::memory_order_relaxed);
}
/* User Code End 1 */
// ...
/* User Code Begin 3 */
KEY1.SetConfig({GPIO::Direction::FALL_INTERRUPT, GPIO::Pull::NONE});
KEY1.RegisterCallback(GPIO::Callback::Create(OnKeyInterrupt, &key1_irq_count));
KEY1.EnableInterrupt();
/* User Code End 3 */
Generated Objects and Naming
Objects are generated for the configurations LibXR has drivers for: UART, I2C and SPI in controller mode, and PWM; every GPIO pin gets an object. The remaining configurations (ADC12, DAC12, MCAN, QEI, an I2C in target mode, an SPI in peripheral mode and so on) are listed under Other in the summary for display only.
The output always contains static MSPM0PowerManager power_manager; after PlatformInit(); with XRobot it is registered as PowerManager, ahead of the other objects. power_manager is a reserved name, so a GPIO label that uses it makes generation fail with a message to rename the pin in SysConfig. The generated code needs a LibXR that carries PowerManager, that is v2.0.1 or newer. The power reset|shutdown|bootloader commands of the terminal are registered by user code through power_manager.RegisterCommand(ramfs); the generator does not emit that call.
The objects are named:
| Configuration | Object name | Example |
|---|---|---|
| GPIO | the label of the pin in SysConfig (the $name without its PIN_ prefix) | LED1, KEY1 |
| UART, I2C, SPI | the lower-case name of the peripheral the instance uses (UART_0 uses UART0) | uart0, i2c1, spi1 |
| PWM | pwm_<timer>_c<channel> | pwm_tima1_c0 |
When the $name of a GPIO pin without its PIN_ prefix is not a valid identifier (the SysConfig default PIN_0 leaves 0), the object takes the lower-case group plus pin name instead (such as gpio_btn_pin_0), and a warning asks for naming the pin in SysConfig.
Every CCP channel of a timer gets a PWM object; the channels of one timer share one period, which SysConfig configures. When libxr_config.yaml sets different frequencies for the channels of one timer, the generator prints a warning and the last SetConfig wins.
The generation requirements of each configuration:
- A UART needs a transmit DMA channel; the receive side runs on byte interrupts by default (
MSPM0_UART_MAIN_INIT). A UART Extend instance with a receive DMA receives through a circular DMA (MSPM0_UART_EXTEND_INIT, the bytes of the receive ring inrx_dma_buffer_size), and its receive channel must be a FULL-DMA channel; the receive DMA channel of a UART Main instance is unused and warned about. A UART without transmit DMA makes generation fail. - An SPI needs a pair of DMA channels, otherwise generation fails; the receive channel must be triggered by RX (in SysConfig's SPI > DMA Configuration, pick
DL_SPI_DMA_INTERRUPT_RXas the DMA Event trigger), an RX timeout trigger makes generation fail. - An I2C works by polling; its buffer stages DMA transfers.
Clocks
The generated code defines a BUSCLK constant for every power domain in use: the BUSCLK of PD1 is CPUCLK_FREQ, that of PD0 is CPUCLK_FREQ divided by the ULPCLK divider (the DL_SYSCTL_setULPCLKDivider of the generated code, 1 by default). The I2C driver resets the clock configuration of its peripherals itself (BUSCLK, divider 1), so its construction argument is the BUSCLK of the power domain; the SPI driver keeps the clock configuration SysConfig wrote, so its argument is the clock source SysConfig chose (BUSCLK, MFCLK at 4 MHz or LFCLK at 32.768 kHz) divided by its divider, and any other source makes generation fail. The macros SysConfig exports for an I2C or an SPI carry no clock frequency, and generation fails when the project has no power domain or CPUCLK_FREQ. A UART references the INST_FREQUENCY macro of SysConfig directly, and the source clock of a PWM is derived by the driver from INST_CLK_FREQ.
libxr_config.yaml
User/libxr_config.yaml stores the settings of code generation. A missing file is created with the defaults; later runs fill in the missing settings and keep the values already present. A peripheral's settings section is written only when the project has that peripheral, one entry per object keyed by the object name (uart0, pwm_tima1_c0). A null value (a key without a value in the YAML) takes the default; buffers and queues must be positive integers.
The settings sections:
| Section | Keys and defaults | Notes |
|---|---|---|
UART | tx_buffer_size: 128, rx_buffer_size: 128, tx_queue_size: 5 | A UART Extend instance also has rx_dma_buffer_size: 128 (even) |
I2C | buffer_size: 32, dma_enable_min_size: 8 | |
SPI | tx_buffer_size: 32, rx_buffer_size: 32, dma_enable_min_size: 3 | |
PWM | frequency (default null) | null calls no SetConfig, see Generated Objects and Naming |
Terminal | read_buff_size: 32, max_line_size: 32, max_arg_number: 5, max_history_number: 5 | template parameters of the terminal, see Serial Ports and Terminal |
terminal_source follows the STM32 convention: an empty value or none generates no terminal; when the file has no such key yet (the first generation), the first UART is chosen. tx_buffer_size must also satisfy the driver's assertions: the transmit buffer is 2 × N bytes, 2 × N is a multiple of 2 * alignof(size_t) (so N is a multiple of 4 on the Cortex-M0+), and N is at most the 0xFFFF bytes of one DMA transfer. Settings keys match without regard to case, and the spelling already in the file is kept.
The following is the User/libxr_config.yaml generated on bsp-mspm0g3507-mini the first time:
generator: 6.0.1
terminal_source: uart0
SPI:
spi1:
tx_buffer_size: 32
rx_buffer_size: 32
dma_enable_min_size: 3
I2C:
i2c0:
buffer_size: 32
dma_enable_min_size: 8
i2c1:
buffer_size: 32
dma_enable_min_size: 8
Terminal:
read_buff_size: 32
max_line_size: 32
max_arg_number: 5
max_history_number: 5
UART:
uart0:
tx_buffer_size: 128
rx_buffer_size: 128
tx_queue_size: 5
uart1:
tx_buffer_size: 128
rx_buffer_size: 128
tx_queue_size: 5
PWM:
pwm_tima1_c0:
frequency:
pwm_tima1_c1:
frequency:
The I2C bus speed and the UART baud rate come from the SysConfig project (such as I2C_0_BUS_SPEED_HZ); libxr_config.yaml has no setting for them.
Regenerating Code
After changing the .syscfg or User/libxr_config.yaml, run libxr mspm0 setup -d . again at the project root (the SysConfig output comes from the same sources as above), or run the two steps:
libxr parse -d .
libxr gen -i .config.yaml -o User/app_main.cpp
.config.yaml is written by libxr parse; a freshly cloned project does not have it, so run libxr parse first. Without --xrobot and --no-xrobot, libxr gen keeps the choice of the output file; afterwards xrobot gen brings User/xrobot_main.hpp up to date. The Generate LibXR Code action of the VS Code extension runs the two commands in sequence, see VS Code Extension.
Options
The options of libxr mspm0 setup are listed below; libxr mspm0 setup --help lists the same.
| Option | Description |
|---|---|
-d, --directory | the project directory: one .syscfg file at its root (default: the current directory) |
-o, --output | the output C++ file (default: User/app_main.cpp in DIRECTORY) |
--xrobot / --no-xrobot | generate or skip the XR_REGISTER registrations and XROBOT_MAIN();, see Integrate with XRobot; when neither is given the output file keeps its choice, and a new file does not use XRobot |
--libxr-config | path or URL of libxr_config.yaml (default: the libxr_config.yaml next to the output) |
--verbose | enable debug logging |
Commands
| Command | Description |
|---|---|
libxr parse | parse the .syscfg into .config.yaml |
libxr gen | generate the entry source file and app_main.h from .config.yaml, and update libxr_config.yaml |
libxr pins | print the package and pin layout of a model, see Pin Layout |
The options and defaults of every command are listed by libxr <command> --help.