summaryrefslogtreecommitdiff
path: root/src/oracle/ledc_ref.c
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-25 12:40:53 -0300
committerGabriel Schneider <[email protected]>2026-08-25 12:46:51 -0300
commitf5f8068fac59b4f16046c2022c2fc7c7e447ef4c (patch)
tree2731a3ed4e51cae09e184e25778eded5fc37d1f5 /src/oracle/ledc_ref.c
downloadesp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.tar.gz
esp32p4-f5f8068fac59b4f16046c2022c2fc7c7e447ef4c.zip
zig-p4: pure-Zig ESP32-P4 toolchain
build.zig generates the linker script and drives Zig's own LLD; tools/image.zig turns the ELF into a flashable image and tools/{rom,serial}.zig speak the mask ROM loader over the UART. No CMake, ninja, idf.py, esptool, or external linker. src/soc.zig is a comptime register model over ESP-IDF's own *_reg.h headers; src/hal/ adds peripheral sequences; src/io/ implements std.Io for the chip; src/oracle/ diffs this HAL against ESP-IDF's on the die.
Diffstat (limited to 'src/oracle/ledc_ref.c')
-rw-r--r--src/oracle/ledc_ref.c210
1 files changed, 210 insertions, 0 deletions
diff --git a/src/oracle/ledc_ref.c b/src/oracle/ledc_ref.c
new file mode 100644
index 0000000..6b1403a
--- /dev/null
+++ b/src/oracle/ledc_ref.c
@@ -0,0 +1,210 @@
+/* LEDC's reference implementation: ESP-IDF's own LL, given external linkage.
+ *
+ * There is no logic here except where a comment says otherwise, and there is exactly one such
+ * place - `oracle_ledc_divisor` - because the divider arithmetic lives in a `static inline` inside
+ * `esp_driver_ledc/src/ledc.c` and is therefore unreachable from a header. It is transcribed
+ * character for character, with the line number, so that the on-die comparison covers the
+ * arithmetic and not only the store that follows it.
+ */
+
+/* IDF's clock and reset LL functions are shadowed by a wrapper macro that references
+ * `__DECLARE_RCC_ATOMIC_ENV`, an identifier IDF never defines anywhere; its purpose is to make an
+ * unguarded call fail to compile, because the only legal caller holds a spinlock. There is no
+ * FreeRTOS here and core 1 is held in reset at power-on, so declaring the name is exactly as safe
+ * as the spinlock would be - and it is what IDF's own bootloader does. */
+static int __DECLARE_RCC_ATOMIC_ENV __attribute__((unused));
+
+/* `ledc_ll_set_slow_clk_sel` and `ledc_ll_get_slow_clk_sel` call `abort()` in the default arm of
+ * their switch (ledc_ll.h:238, :273). Freestanding, nothing declares it; the arms below are all
+ * reached with constants, so the call folds away and no definition is needed. */
+void abort(void);
+
+#include <stdint.h>
+
+#include "hal/ledc_ll.h"
+#include "hal/gpio_ll.h"
+#include "soc/gpio_struct.h"
+#include "soc/gpio_sig_map.h"
+
+/* P4 has one speed mode: low. `ledc_ll.h` still takes the parameter because the LL is shared with
+ * parts that have two. */
+#define MODE LEDC_LOW_SPEED_MODE
+
+/* ---------------------------------------------------------------- clocks, outside the LEDC block */
+
+void oracle_ledc_enable_bus_clock(int enable)
+{
+ ledc_ll_enable_bus_clock(enable != 0);
+}
+
+void oracle_ledc_enable_function_clock(int enable)
+{
+ ledc_ll_enable_clock(LEDC_LL_GET_HW(), enable != 0);
+}
+
+/* `sel` is this project's `ClockSource` enum, which is HP_SYS_CLKRST's own encoding: 0 XTAL,
+ * 1 RC_FAST, 2 PLL_DIV. Split into three constant calls so that IDF's switch folds and its
+ * `abort()` arm never reaches the linker. */
+void oracle_ledc_set_clock_source(unsigned sel)
+{
+ switch (sel) {
+ case 0:
+ ledc_ll_set_slow_clk_sel(LEDC_LL_GET_HW(), LEDC_SLOW_CLK_XTAL);
+ break;
+ case 1:
+ ledc_ll_set_slow_clk_sel(LEDC_LL_GET_HW(), LEDC_SLOW_CLK_RC_FAST);
+ break;
+ case 2:
+ ledc_ll_set_slow_clk_sel(LEDC_LL_GET_HW(), LEDC_SLOW_CLK_PLL_DIV);
+ break;
+ default:
+ break;
+ }
+}
+
+/* ---------------------------------------------------------------------------- divider arithmetic */
+
+/* Verbatim from esp_driver_ledc/src/ledc.c:468-497 (v6.0.2), which is `static inline` in a .c file
+ * and so cannot be called. The 32-bit wrap of `freq_hz * precision` and the truncation of the
+ * 64-bit quotient into `uint32_t` are IDF's, and are the whole reason this exists: they are what
+ * src/hal/ledc.zig's `divisor` has to reproduce. */
+uint32_t oracle_ledc_divisor(uint32_t src_clk_freq, int freq_hz, uint32_t precision)
+{
+ return (((uint64_t) src_clk_freq << LEDC_LL_FRACTIONAL_BITS) + freq_hz * precision / 2)
+ / (freq_hz * precision);
+}
+
+/* ------------------------------------------------------------------------------------- timers */
+
+void oracle_ledc_set_clock_divider(unsigned timer, uint32_t div)
+{
+ ledc_ll_set_clock_divider(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer, div);
+}
+
+void oracle_ledc_set_duty_resolution(unsigned timer, uint32_t bits)
+{
+ ledc_ll_set_duty_resolution(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer, bits);
+}
+
+void oracle_ledc_commit_timer(unsigned timer)
+{
+ ledc_ll_ls_timer_update(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer);
+}
+
+void oracle_ledc_reset_timer(unsigned timer)
+{
+ ledc_ll_timer_rst(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer);
+}
+
+void oracle_ledc_pause_timer(unsigned timer)
+{
+ ledc_ll_timer_pause(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer);
+}
+
+void oracle_ledc_resume_timer(unsigned timer)
+{
+ ledc_ll_timer_resume(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer);
+}
+
+/* `ledc_set_timer_params` (ledc.c:244-261) followed by the resume/reset pair `ledc_timer_config`
+ * does on success (ledc.c:816-818). The clock-source step of `ledc_set_timer_params` is absent on
+ * purpose: on the P4 there is no timer-specific mux (SOC_LEDC_HAS_TIMER_SPECIFIC_MUX is unset), so
+ * that step compiles out of IDF too. */
+void oracle_ledc_configure_timer(unsigned timer, uint32_t src_hz, int freq_hz, uint32_t resolution)
+{
+ uint32_t div = oracle_ledc_divisor(src_hz, freq_hz, 1u << resolution);
+ ledc_ll_set_clock_divider(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer, div);
+ ledc_ll_set_duty_resolution(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer, resolution);
+ ledc_ll_ls_timer_update(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer);
+ ledc_ll_timer_resume(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer);
+ ledc_ll_timer_rst(LEDC_LL_GET_HW(), MODE, (ledc_timer_t)timer);
+}
+
+/* ------------------------------------------------------------------------------------ channels */
+
+void oracle_ledc_bind_timer(unsigned channel, unsigned timer)
+{
+ ledc_ll_bind_channel_timer(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, (ledc_timer_t)timer);
+}
+
+void oracle_ledc_set_hpoint(unsigned channel, uint32_t hpoint)
+{
+ ledc_ll_set_hpoint(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, hpoint);
+}
+
+/* `ledc_duty_config` (ledc.c:263-280) with `hpoint_val` left alone: the duty integer part, then the
+ * degenerate one-step fade in gamma RAM entry 0 that a constant duty needs on this die, then the
+ * range count. `ledc_hal_clear_left_off_fade_param` is deliberately not called - it zeroes ranges
+ * 1..15, which only matters once real fades are in scope. */
+void oracle_ledc_set_duty(unsigned channel, uint32_t duty)
+{
+ ledc_ll_set_duty_int_part(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, duty);
+ ledc_ll_set_fade_param_range(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, 0, 1, 1, 0, 1);
+ ledc_ll_set_range_number(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, 1);
+}
+
+void oracle_ledc_set_output_enabled(unsigned channel, int enable)
+{
+ ledc_ll_set_sig_out_en(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, enable != 0);
+}
+
+void oracle_ledc_set_idle_level(unsigned channel, uint32_t level)
+{
+ ledc_ll_set_idle_level(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, level);
+}
+
+void oracle_ledc_start_fade(unsigned channel)
+{
+ ledc_ll_set_duty_start(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel);
+}
+
+void oracle_ledc_commit_channel(unsigned channel)
+{
+ ledc_ll_ls_channel_update(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel);
+}
+
+/* `_ledc_update_duty`, ledc.c:1021-1026. */
+void oracle_ledc_start(unsigned channel)
+{
+ ledc_ll_set_sig_out_en(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, true);
+ ledc_ll_set_duty_start(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel);
+ ledc_ll_ls_channel_update(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel);
+}
+
+/* `ledc_stop`, ledc.c:1039-1050: idle level staged before the output is disabled, one commit. */
+void oracle_ledc_stop(unsigned channel, uint32_t idle_level)
+{
+ ledc_ll_set_idle_level(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, idle_level);
+ ledc_ll_set_sig_out_en(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, false);
+ ledc_ll_ls_channel_update(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel);
+}
+
+/* The register half of `ledc_channel_config` (ledc.c:869-1019): stage timer, hpoint, duty, idle
+ * level and output enable, hand the duty over, commit once. */
+void oracle_ledc_configure_channel(unsigned channel, unsigned timer, uint32_t duty, uint32_t hpoint,
+ uint32_t idle_level, int output_enabled)
+{
+ ledc_ll_bind_channel_timer(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, (ledc_timer_t)timer);
+ ledc_ll_set_hpoint(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, hpoint);
+ ledc_ll_set_duty_int_part(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, duty);
+ ledc_ll_set_fade_param_range(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, 0, 1, 1, 0, 1);
+ ledc_ll_set_range_number(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, 1);
+ ledc_ll_set_idle_level(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, idle_level);
+ ledc_ll_set_sig_out_en(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel, output_enabled != 0);
+ ledc_ll_set_duty_start(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel);
+ ledc_ll_ls_channel_update(LEDC_LL_GET_HW(), MODE, (ledc_channel_t)channel);
+}
+
+/* ---------------------------------------------------------------------------------- pin routing */
+
+/* The hardware effect of `ledc_set_pin` (ledc.c:823-836). `gpio_matrix_output` is
+ * `gpio_hal_matrix_out` (gpio_hal.c:60-69): pad function, matrix source, then the output-enable
+ * control last "to avoid undesired level change". The signal index is
+ * `ledc_periph_signal[0].sig_out0_idx + channel`, and that field is initialised to
+ * `LEDC_LS_SIG_OUT_PAD_OUT0_IDX` in esp_hal_ledc/esp32p4/ledc_periph.c:14-18. */
+void oracle_ledc_set_pin(unsigned pin, unsigned channel)
+{
+ gpio_ll_func_sel(&GPIO, pin, PIN_FUNC_GPIO);
+ gpio_ll_set_output_signal_matrix_source(&GPIO, pin, LEDC_LS_SIG_OUT_PAD_OUT0_IDX + channel, false);
+ gpio_ll_set_output_enable_ctrl(&GPIO, pin, true, false);
+}