|
| 1 | +--- |
| 2 | +name: c-style |
| 3 | +description: Enforce and review this project's C/C++ coding style for RP2040/RP2350 Pico SDK firmware. Use this skill whenever writing, editing, or reviewing .c, .h, .cpp, .hpp, CMakeLists, Pico SDK module code, FreeRTOS task/queue/semaphore code, u8g2/MUI display code, lwIP/CYW43 wireless or REST endpoint code, EEPROM configuration code, PIO/UART/I2C/SPI/PWM driver code, or embedded target configuration in this repository. Apply it proactively for C/C++ refactors, bug fixes, feature work, and code-review requests; do not wait for the user to ask for style explicitly. |
| 4 | +--- |
| 5 | + |
| 6 | +# C/C++ Style Guide |
| 7 | + |
| 8 | +This skill captures the C/C++ style used in this repository. Apply it proactively when writing or reviewing firmware code. |
| 9 | + |
| 10 | +This project is Pico SDK firmware for RP2040/RP2350-class boards, with FreeRTOS, u8g2/MUI, lwIP/CYW43 networking, EEPROM-backed configuration, PIO stepper control, and small C++ bridge islands. The strongest style signal is tracked project code under `src/` and target hardware configuration under `targets/`. |
| 11 | + |
| 12 | +Treat `library/` code as third-party unless the user explicitly asks to modify it. Generated files under `src/generated/` should normally be regenerated rather than hand edited. |
| 13 | + |
| 14 | +The style is direct embedded C with pragmatic C++ where it helps integrate C++ helpers or device libraries: explicit initialization, visible hardware control, clear task/queue flow, and small abstractions only where the surrounding module already supports them. |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## Rule 1 - Match the local module shape |
| 19 | + |
| 20 | +Keep code organized as small module pairs in `src/`: `feature.c` / `feature.h`, or `feature.cpp` / `feature.h` when C++ is required. Public APIs are declared in the header and implemented in the source file. File-local helpers, callbacks, buffers, task handles, and state should be `static` unless existing code deliberately shares them. |
| 21 | + |
| 22 | +Prefer existing module vocabulary and prefixes: |
| 23 | + |
| 24 | +- `*_init`, `*_config_init`, `*_config_save` |
| 25 | +- `*_task`, `*_render_task`, `*_wait_for_*` |
| 26 | +- `http_rest_*`, `http_get_*`, `populate_rest_*`, `apply_rest_*` |
| 27 | +- `*_set_*`, `get_*`, `*_enable` |
| 28 | +- hardware names such as `motor_*`, `scale_*`, `servo_gate_*`, `wireless_*`, `neopixel_led_*` |
| 29 | + |
| 30 | +**Wrong:** |
| 31 | +```c |
| 32 | +void update(void) { |
| 33 | + ... |
| 34 | +} |
| 35 | +``` |
| 36 | +
|
| 37 | +**Right:** |
| 38 | +```c |
| 39 | +static void wireless_update_state(void) { |
| 40 | + ... |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +When reviewing: check whether a new symbol should be public. If not, make it `static` and name it with the module's existing vocabulary. |
| 45 | + |
| 46 | +--- |
| 47 | + |
| 48 | +## Rule 2 - Use the project's C naming conventions |
| 49 | + |
| 50 | +Use `snake_case` for project-owned functions, variables, fields, files, callbacks, and tasks. Use `*_t` typedef names for structs, enums, and function pointer types where the surrounding code does. Use `ALL_CAPS` for macros and enum values. |
| 51 | + |
| 52 | +Keep Pico SDK, FreeRTOS, lwIP, u8g2, CYW43, and vendor-library names as-is. Do not wrap library types just to make names prettier. |
| 53 | + |
| 54 | +**Wrong:** |
| 55 | +```c |
| 56 | +typedef struct { |
| 57 | + int TimeoutMs; |
| 58 | +} TimerConfig; |
| 59 | + |
| 60 | +void StartTimer(TimerConfig *config); |
| 61 | +``` |
| 62 | +
|
| 63 | +**Right:** |
| 64 | +```c |
| 65 | +typedef struct { |
| 66 | + int timeout_ms; |
| 67 | +} timer_config_t; |
| 68 | +
|
| 69 | +void start_timer(timer_config_t *config); |
| 70 | +``` |
| 71 | + |
| 72 | +When reviewing: flag CamelCase in project-owned C APIs unless it comes from an external type, constant, generated code, or local legacy API that the edit is intentionally preserving. |
| 73 | + |
| 74 | +--- |
| 75 | + |
| 76 | +## Rule 3 - Match formatting in the current file |
| 77 | + |
| 78 | +Use four-space indentation. Most source uses K&R braces for functions and control blocks: |
| 79 | + |
| 80 | +```c |
| 81 | +bool motor_config_init(void) { |
| 82 | + bool is_ok = true; |
| 83 | + |
| 84 | + if (!is_ok) { |
| 85 | + return false; |
| 86 | + } |
| 87 | + |
| 88 | + return true; |
| 89 | +} |
| 90 | +``` |
| 91 | +
|
| 92 | +Pointer spacing varies in the existing code, with both `char * s` and `char *s` present. Match the surrounding file instead of imposing a repo-wide rewrite. Avoid formatting-only churn outside the edited area. |
| 93 | +
|
| 94 | +Keep designated initializers for config/default structures, with short inline comments where they describe hardware, EEPROM layout, units, or user-visible behavior. |
| 95 | +
|
| 96 | +**Right:** |
| 97 | +```c |
| 98 | +const eeprom_wireless_metadata_t default_eeprom_wireless_metadata = { |
| 99 | + .wireless_data_rev = 0, |
| 100 | + .ssid = "", |
| 101 | + .pw = "", |
| 102 | + .auth = AUTH_WPA2_MIXED_PSK, |
| 103 | + .timeout_ms = 30000, // 30s |
| 104 | + .enable = false, |
| 105 | +}; |
| 106 | +``` |
| 107 | + |
| 108 | +When reviewing: distinguish style consistency from broad cleanup. Prefer focused fixes over reformatting whole files. |
| 109 | + |
| 110 | +--- |
| 111 | + |
| 112 | +## Rule 4 - Use local error and return contracts |
| 113 | + |
| 114 | +This project usually returns `bool` for success/failure and small project enums for richer initialization failures. Preserve that style unless the surrounding module already uses another contract. |
| 115 | + |
| 116 | +Use early returns for invalid state, failed allocation, failed hardware initialization, failed EEPROM reads/writes, and task/queue creation failures. Diagnostics are commonly `printf(...)`; keep messages actionable and include hardware/config context when available. |
| 117 | + |
| 118 | +**Wrong:** |
| 119 | +```c |
| 120 | +int init_sensor(sensor_t *ctx) { |
| 121 | + if (ctx == NULL) { |
| 122 | + return -1; |
| 123 | + } |
| 124 | + |
| 125 | + return sensor_open(ctx); |
| 126 | +} |
| 127 | +``` |
| 128 | +
|
| 129 | +**Right:** |
| 130 | +```c |
| 131 | +bool sensor_init(sensor_t *ctx) { |
| 132 | + if (ctx == NULL) { |
| 133 | + printf("Sensor context is NULL\n"); |
| 134 | + return false; |
| 135 | + } |
| 136 | +
|
| 137 | + if (!sensor_open(ctx)) { |
| 138 | + printf("Unable to initialize sensor\n"); |
| 139 | + return false; |
| 140 | + } |
| 141 | +
|
| 142 | + return true; |
| 143 | +} |
| 144 | +``` |
| 145 | + |
| 146 | +For multi-stage hardware initialization, use a module enum when the caller needs to distinguish failures: |
| 147 | + |
| 148 | +```c |
| 149 | +typedef enum { |
| 150 | + MOTOR_INIT_OK = 0, |
| 151 | + MOTOR_INIT_CFG_ERR = 2, |
| 152 | + MOTOR_INIT_COARSE_DRV_ERR = 3, |
| 153 | + MOTOR_INIT_FINE_DRV_ERR = 4, |
| 154 | + MOTOR_INIT_PIO_ERR = 5, |
| 155 | +} motor_init_err_t; |
| 156 | +``` |
| 157 | + |
| 158 | +When reviewing: flag silent failures, ignored allocation/task creation failures in new code, and ad-hoc integer return codes when a local enum or `bool` contract already exists. |
| 159 | + |
| 160 | +--- |
| 161 | + |
| 162 | +## Rule 5 - Follow the EEPROM configuration pattern |
| 163 | + |
| 164 | +Persistent configuration lives in EEPROM-backed structs with defaults and CRC validation through shared helpers in `common.c`. |
| 165 | + |
| 166 | +The usual pattern is: |
| 167 | + |
| 168 | +- A persistent config struct in the module header, often named `eeprom_*_data_t` or `eeprom_*_metadata_t` |
| 169 | +- A global live module config object in the source file |
| 170 | +- A `const ... default_*` initializer with `.revision = 0` or similar legacy revision field |
| 171 | +- `*_config_init()` calls `load_config(...)` |
| 172 | +- `*_config_save()` calls `save_config(...)` |
| 173 | +- Successful init registers the save handler with `eeprom_register_handler(...)` |
| 174 | +- REST handlers update live config and save only when the `ee` parameter is true |
| 175 | + |
| 176 | +**Right:** |
| 177 | +```c |
| 178 | +bool wireless_config_save() { |
| 179 | + bool is_ok = save_config(EEPROM_WIRELESS_CONFIG_BASE_ADDR, |
| 180 | + &wireless_config.eeprom_wireless_metadata, |
| 181 | + sizeof(eeprom_wireless_metadata_t)); |
| 182 | + return is_ok; |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +When writing a new config feature, preserve EEPROM layout compatibility. Add fields carefully, keep defaults explicit, and update the relevant EEPROM address/revision policy only when required. |
| 187 | + |
| 188 | +--- |
| 189 | + |
| 190 | +## Rule 6 - Keep target hardware configuration in `targets/` |
| 191 | + |
| 192 | +Board-specific pin and peripheral mappings live under `targets/`, for example `targets/raspberrypi_pico_w_config.h`. Use macros for fixed hardware resources: |
| 193 | + |
| 194 | +```c |
| 195 | +#define MOTOR_UART uart1 |
| 196 | +#define MOTOR_UART_TX 4 |
| 197 | +#define MOTOR_UART_RX 5 |
| 198 | +#define MOTOR_PIO pio0 |
| 199 | +``` |
| 200 | +
|
| 201 | +Project modules should consume these macros via `configuration.h` rather than hard-coding pins or peripheral instances locally. |
| 202 | +
|
| 203 | +When adding RP2040/RP2350 board support, prefer a new target config header and CMake board selection updates over conditional clutter in feature modules. |
| 204 | +
|
| 205 | +--- |
| 206 | +
|
| 207 | +## Rule 7 - Use Pico SDK hardware APIs directly and visibly |
| 208 | +
|
| 209 | +Hardware control should be explicit and close to the module that owns it. Prefer Pico SDK calls already used in the codebase: |
| 210 | +
|
| 211 | +- GPIO: `gpio_init`, `gpio_set_dir`, `gpio_put`, `gpio_set_function` |
| 212 | +- UART: `uart_init`, `uart_write_blocking`, `uart_read_blocking`, `uart_is_readable_within_us` |
| 213 | +- PIO: `pio_claim_unused_sm`, `pio_add_program`, `pio_sm_set_enabled`, `pio_sm_put_blocking` |
| 214 | +- Clocks/time: `clock_get_hz`, `time_us_32`, `busy_wait_us`, `busy_wait_ms` |
| 215 | +- CYW43/lwIP: protect lwIP calls with `cyw43_arch_lwip_begin()` / `cyw43_arch_lwip_end()` where the surrounding code does |
| 216 | +
|
| 217 | +Check return values for resource-claiming APIs and report failures: |
| 218 | +
|
| 219 | +```c |
| 220 | +int sm = pio_claim_unused_sm(pio, true); |
| 221 | +if (sm < 0) { |
| 222 | + printf("Unable to claim state machine, err: %d\n", sm); |
| 223 | + return false; |
| 224 | +} |
| 225 | +``` |
| 226 | + |
| 227 | +When reviewing: flag hard-coded pin numbers, hidden hardware side effects, unchecked PIO/UART resource setup, and lwIP calls that violate the local CYW43 locking pattern. |
| 228 | + |
| 229 | +--- |
| 230 | + |
| 231 | +## Rule 8 - Treat FreeRTOS tasks, queues, and semaphores explicitly |
| 232 | + |
| 233 | +Task code favors visible loops, explicit queue commands, and simple state machines. Task functions use `void *p` or `void *args`, cast near the top or at use sites, then loop. |
| 234 | + |
| 235 | +Creation commonly uses `xTaskCreate(...)` with literal task names, stack sizes such as `configMINIMAL_STACK_SIZE` or module-specific values, and explicit priorities. New code should check queue/semaphore allocation and task creation when practical. |
| 236 | + |
| 237 | +**Right:** |
| 238 | +```c |
| 239 | +wireless_ctrl_queue = xQueueCreate(5, sizeof(wireless_ctrl_t)); |
| 240 | +if (!wireless_ctrl_queue) { |
| 241 | + printf("Unable to create wireless control queue\n"); |
| 242 | + return; |
| 243 | +} |
| 244 | +``` |
| 245 | + |
| 246 | +Use `vTaskDelayUntil(...)` for periodic loops that should keep cadence, and `vTaskDelay(pdMS_TO_TICKS(...))` for simple sleeps. |
| 247 | + |
| 248 | +When reviewing: flag hidden background behavior, unbounded busy loops after the scheduler starts, queue item-size mismatches, blocking calls in high-priority paths without a clear reason, and missing synchronization around shared display buffers or shared hardware. |
| 249 | + |
| 250 | +--- |
| 251 | + |
| 252 | +## Rule 9 - Build u8g2 and MUI views directly |
| 253 | + |
| 254 | +Display code is intentionally imperative: clear the buffer, set font, draw strings/lines/shapes, send the buffer, and delay at the required cadence. Keep layout calculations local and readable. |
| 255 | + |
| 256 | +Use the display access helpers where shared buffer access matters: |
| 257 | + |
| 258 | +- `get_display_handler()` |
| 259 | +- `acquire_display_buffer_access()` |
| 260 | +- `release_display_buffer_access()` |
| 261 | + |
| 262 | +**Right:** |
| 263 | +```c |
| 264 | +u8g2_t *display_handler = get_display_handler(); |
| 265 | + |
| 266 | +u8g2_ClearBuffer(display_handler); |
| 267 | +u8g2_SetFont(display_handler, u8g2_font_helvB08_tr); |
| 268 | +u8g2_DrawStr(display_handler, 5, 10, title_string); |
| 269 | +u8g2_SendBuffer(display_handler); |
| 270 | +``` |
| 271 | +
|
| 272 | +When reviewing: flag display updates from multiple tasks without a clear locking or ownership story, oversized UI abstractions, and text drawing that can exceed fixed display buffers without bounds checks. |
| 273 | +
|
| 274 | +--- |
| 275 | +
|
| 276 | +## Rule 10 - Keep REST handlers small and static-buffer based |
| 277 | +
|
| 278 | +REST endpoint handlers usually accept `struct fs_file *file, int num_params, char *params[], char *values[]`, update live settings from compact parameter names, and return static response buffers with `FS_FILE_FLAGS_HEADER_INCLUDED`. |
| 279 | +
|
| 280 | +Prefer bounded formatting: |
| 281 | +
|
| 282 | +```c |
| 283 | +static char json_buffer[256]; |
| 284 | +
|
| 285 | +snprintf(json_buffer, |
| 286 | + sizeof(json_buffer), |
| 287 | + "%s{\"enabled\":%s}", |
| 288 | + http_json_header, |
| 289 | + boolean_to_string(enabled)); |
| 290 | +``` |
| 291 | + |
| 292 | +Do not expose secrets such as WiFi passwords in responses. Preserve the existing compact parameter mapping style (`w0`, `m1`, `c13`, `ee`) unless the user asks for an API redesign. |
| 293 | + |
| 294 | +When reviewing: flag response buffer overflow risk, returning stack memory through `file->data`, unsanitized secret output, and failing to set `file->len`, `file->index`, and `file->flags`. |
| 295 | + |
| 296 | +--- |
| 297 | + |
| 298 | +## Rule 11 - Use memory allocation conservatively |
| 299 | + |
| 300 | +This firmware uses standard C allocation (`malloc`, `free`) in project code and C++ objects in selected C++ modules. Keep allocations bounded, check for `NULL`, and free on every failure path. |
| 301 | + |
| 302 | +Prefer static buffers for small HTTP responses, display strings, and fixed hardware data where the existing module does. Avoid long-lived heap allocations in fast control loops unless there is a clear ownership model. |
| 303 | + |
| 304 | +**Right:** |
| 305 | +```c |
| 306 | +uint8_t *buf = malloc(write_size); |
| 307 | +if (!buf) { |
| 308 | + printf("Unable to allocate buffer with size: %d\n", write_size); |
| 309 | + return false; |
| 310 | +} |
| 311 | + |
| 312 | +... |
| 313 | + |
| 314 | +free(buf); |
| 315 | +``` |
| 316 | +
|
| 317 | +When reviewing: flag unchecked allocation, allocator mismatches, stack buffers that are too large for FreeRTOS task stacks, and heap use inside timing-sensitive loops. |
| 318 | +
|
| 319 | +--- |
| 320 | +
|
| 321 | +## Rule 12 - Comments explain hardware intent, units, and compatibility |
| 322 | +
|
| 323 | +Comments should explain hardware intent, timing assumptions, EEPROM compatibility, units, REST parameter mappings, and concurrency assumptions. Keep comments short and practical. Preserve useful `TODO`, `FIXME`, and `NOTE` markers when they describe real incomplete work or constraints. |
| 324 | +
|
| 325 | +**Wrong:** |
| 326 | +```c |
| 327 | +// Set value to true |
| 328 | +enabled = true; |
| 329 | +``` |
| 330 | + |
| 331 | +**Right:** |
| 332 | +```c |
| 333 | +// At 250k baud rate the transmit time is about 320us. Need to wait long enough to not read the echo message back. |
| 334 | +busy_wait_us(20); |
| 335 | +``` |
| 336 | +
|
| 337 | +When reviewing: flag misleading comments and empty narration, but do not demand Doxygen for every internal helper. Public headers may use brief comments where the surrounding header already does. |
| 338 | +
|
| 339 | +--- |
| 340 | +
|
| 341 | +## Rule 13 - Keep C++ as a bridge, not a second architecture |
| 342 | +
|
| 343 | +Use C++ where it is already needed for helper classes or modules such as `FloatRingBuffer` and selected mode implementations. Keep public headers C-callable with `extern "C"` guards when C modules need to call them. |
| 344 | +
|
| 345 | +**Right:** |
| 346 | +```c |
| 347 | +#ifdef __cplusplus |
| 348 | +extern "C" { |
| 349 | +#endif |
| 350 | +
|
| 351 | +bool charge_mode_config_init(void); |
| 352 | +
|
| 353 | +#ifdef __cplusplus |
| 354 | +} |
| 355 | +#endif |
| 356 | +``` |
| 357 | + |
| 358 | +Avoid introducing classes, templates, exceptions, RTTI-heavy patterns, STL containers, or broad C++ architecture into firmware modules unless the surrounding module already depends on them and the user explicitly wants that direction. |
| 359 | + |
| 360 | +When reviewing: flag C++ abstractions that make firmware control flow harder to inspect or call from C. |
| 361 | + |
| 362 | +--- |
| 363 | + |
| 364 | +## Rule 14 - Keep CMake changes local and Pico SDK-native |
| 365 | + |
| 366 | +Top-level CMake uses Pico SDK initialization, FreeRTOS import, u8g2, Trinamic interface sources, and `src/CMakeLists.txt` for generated/support targets. Preserve this structure. |
| 367 | + |
| 368 | +Prefer: |
| 369 | + |
| 370 | +- `target_sources(...)` for explicit generated or library-owned source additions |
| 371 | +- `target_link_libraries(...)` for Pico SDK hardware libraries such as `hardware_pio`, `hardware_spi`, `hardware_i2c`, `hardware_pwm` |
| 372 | +- target config include paths through `PICO_BOARD_HEADER_DIRS` and `targets/` |
| 373 | + |
| 374 | +Do not vendor new dependencies into `library/` or alter SDK import behavior unless the user asks for that scope. |
| 375 | + |
| 376 | +--- |
| 377 | + |
| 378 | +## Review Workflow |
| 379 | + |
| 380 | +When asked to review C/C++ code: |
| 381 | + |
| 382 | +1. Read the file or diff in full, plus the nearby header/source pair when relevant. |
| 383 | +2. Prioritize behavioral risks first: hardware resource claims, task/queue/semaphore safety, display buffer concurrency, EEPROM compatibility, REST buffer lifetime/size, PIO/UART timing, CYW43/lwIP locking, and memory ownership. |
| 384 | +3. Then list style violations against these rules with file and line references. |
| 385 | +4. Suggest the smallest correction that matches the surrounding module. |
| 386 | +5. If no issues are found, say so and mention any residual test or hardware-verification gap. |
| 387 | + |
| 388 | +When writing C/C++ code: |
| 389 | + |
| 390 | +Apply these rules before editing. Match the current file's local conventions, keep edits scoped, and prefer existing helpers and patterns over new abstractions. |
0 commit comments