Project Structure
The firmware is organised as a set of ESP-IDF components — the framework's equivalent of
packages in a monorepo. Each component owns a hardware subsystem or feature and exposes
a clean public API via its include/ directory.
Directory Layout
alarm-clock/
├── CMakeLists.txt # top-level CMake; just lists components dir
├── sdkconfig # board/feature config (generated by idf.py menuconfig)
├── main/
│ ├── CMakeLists.txt # registers main component, lists its deps
│ └── main.c # app_main() entry point
└── components/
├── rtc/
│ ├── CMakeLists.txt
│ ├── include/rtc.h # public API
│ └── rtc.c
├── display/
│ ├── CMakeLists.txt
│ ├── include/display.h
│ └── display.c
├── audio/
│ ├── CMakeLists.txt
│ ├── include/audio.h
│ └── audio.c
├── lfs/
│ ├── CMakeLists.txt
│ ├── include/lfs.h
│ └── lfs.c
├── buttons/
│ ├── CMakeLists.txt
│ ├── include/buttons.h
│ └── buttons.c
├── ntp/
│ ├── CMakeLists.txt
│ ├── include/ntp.h
│ └── ntp.c
└── alarm_manager/
├── CMakeLists.txt
├── include/alarm_manager.h
└── alarm_manager.c
Component CMakeLists
Each component has a minimal CMakeLists.txt:
# components/rtc/CMakeLists.txt
idf_component_register(
SRCS "rtc.c"
INCLUDE_DIRS "include"
REQUIRES "driver" "esp_timer"
)
REQUIRES lists ESP-IDF libraries or other components this one depends on. The build
system resolves include paths and link order automatically.
Main Component
The main component ties everything together:
# main/CMakeLists.txt
idf_component_register(
SRCS "main.c"
INCLUDE_DIRS "."
REQUIRES "rtc" "display" "audio" "lfs" "buttons" "ntp" "alarm_manager"
)
Public APIs (Header Design)
Each component's include/ header exposes only what the main task needs. Example for
the RTC component:
// components/rtc/include/rtc.h
#pragma once
#include <time.h>
#include "esp_err.h"
esp_err_t rtc_init(void);
esp_err_t rtc_get_time(struct tm *out);
esp_err_t rtc_set_time(const struct tm *in);
float rtc_get_temperature(void);
Implementation details (I2C handle, register constants) stay in rtc.c — not exposed.
This mirrors the interface/implementation separation you would use in any other codebase.
sdkconfig as "Environment Variables"
sdkconfig stores compile-time configuration values. Custom config items can be added
via a Kconfig file in any component:
# components/ntp/Kconfig
menu "NTP Configuration"
config NTP_SERVER
string "NTP server hostname"
default "pool.ntp.org"
config NTP_SYNC_INTERVAL_SEC
int "NTP sync interval (seconds)"
default 3600
endmenu
Access in C code:
#include "sdkconfig.h"
sntp_setservername(0, CONFIG_NTP_SERVER);
This keeps user-configurable values out of the source code, analogous to environment variables in a server application.