leoribg/pulse_generator

1.0.1

Latest
uploaded 3 hours ago
RMT-backed pulse train generator for ESP32. Configurable pulse width, period and count, with push-pull or open-drain (open-collector) output for buses that already have an external pull-up.

Readme

# ESP 32 Pulse Generator Component

RMT-backed pulse train generator for ESP32. Configurable pulse width, period and
count, with push-pull or open-drain output for buses that already have an
external pull-up.

- Timing produced entirely by the RMT peripheral — no RTOS jitter
- Finite trains of N pulses, or endless trains looped in hardware
- Open-drain mode emulates an open-collector sensor output
- Phases longer than one RMT symbol field are split automatically
- Completion callback from ISR context
- Configurable defaults through `menuconfig`

Requires ESP-IDF 5.3 or newer.

---

## Install

Into an existing project:

```bash
idf.py add-dependency "leoribg/pulse_generator^1.0.1"
idf.py reconfigure
```

Or declare it in `main/idf_component.yml`:

```yaml
dependencies:
  leoribg/pulse_generator: "^1.0.1"
```

Start from the bundled example instead:

```bash
idf.py create-project-from-example "leoribg/pulse_generator^1.0.1:basic"
```

Nothing else is needed. The manager downloads the component into
`managed_components/` and the build system picks it up automatically.

---

## Quick start

```c
#include "pulse_generator.h"

pulse_gen_config_t config =
    PULSE_GEN_CONFIG_DEFAULT(25, PULSE_GEN_DRIVE_OPEN_DRAIN, 1);

pulse_gen_handle_t gen;
ESP_ERROR_CHECK(pulse_gen_new(&config, &gen));

const pulse_gen_train_t train = {
    .width_us  = 500,
    .period_us = 1000,
    .count     = 100,
};

ESP_ERROR_CHECK(pulse_gen_start(gen, &train));
ESP_ERROR_CHECK(pulse_gen_wait_done(gen, 1000));
ESP_ERROR_CHECK(pulse_gen_del(gen));
```

Endless train, stopped explicitly:

```c
const pulse_gen_train_t endless = {
    .width_us  = 500,
    .period_us = 1000,
    .count     = PULSE_GEN_COUNT_INFINITE,
};

pulse_gen_start(gen, &endless);
vTaskDelay(pdMS_TO_TICKS(5000));
pulse_gen_stop(gen);
```

---

## Picking the output mode

| Wiring | `drive_mode` | `idle_level` |
| --- | --- | --- |
| GPIO straight onto a line pulled up to 3.3 V or less | `OPEN_DRAIN` | `1` |
| External NPN / N-channel MOSFET, pull-up at 5–24 V | `PUSH_PULL` | `0` |
| Dedicated line, no pull-up, receiver idles low | `PUSH_PULL` | `0` |

- **ESP32 pads are not 5 V tolerant.** In open-drain mode the pad goes
  high-impedance and then sees the full bus voltage, which destroys it above
  ~3.6 V. Use an external transistor or opto-coupler for anything higher.
- An external NPN or N-channel MOSFET **inverts** the signal: a high on the GPIO
  pulls the line low. That is why the second row idles at 0.
- The rising edge in open-drain mode is passive, set by the pull-up and the line
  capacitance (`t ≈ 2.2 · R · C`). It bounds the usable frequency and can
  stretch the perceived pulse width.

---

## API

| Function | Purpose |
| --- | --- |
| `pulse_gen_new()` | Allocate an RMT channel and encoder |
| `pulse_gen_start()` | Start a finite or endless train |
| `pulse_gen_wait_done()` | Block until a finite train completes |
| `pulse_gen_stop()` | Abort the current train, return the line to idle |
| `pulse_gen_is_busy()` | Query whether a train is in flight |
| `pulse_gen_register_done_callback()` | ISR callback on completion |
| `pulse_gen_del()` | Release everything |

Full Doxygen comments live in `include/pulse_generator.h`.

---

## Configuration

`menuconfig` → `Component config` → `Pulse Generator`

| Symbol | Default | Effect |
| --- | --- | --- |
| `PULSE_GEN_DEFAULT_RESOLUTION_HZ` | `1000000` | RMT tick rate when `resolution_hz` is 0 |
| `PULSE_GEN_DEFAULT_MEM_BLOCK_SYMBOLS` | `64` | Symbols per block; must match the SoC granularity |
| `PULSE_GEN_DEFAULT_QUEUE_DEPTH` | `4` | RMT transaction queue depth |
| `PULSE_GEN_ENABLE_DMA` | `n` | DMA backend, where the target supports it |
| `PULSE_GEN_ISR_IRAM_SAFE` | `n` | Encoder and ISR in IRAM; needs an IRAM callback |
| `PULSE_GEN_ENABLE_ARG_CHECK` | `y` | Runtime argument validation |
| `PULSE_GEN_ENABLE_DEBUG_LOG` | `n` | DEBUG logging plus a symbol dump per start |

- These are **defaults only**. Any field filled in explicitly in
  `pulse_gen_config_t` wins over the Kconfig value.
- `PULSE_GEN_CONFIG_DEFAULT()` reads all of them, so it is the simplest way to
  honour the menuconfig settings.
- Pin them per project in `sdkconfig.defaults` rather than editing `sdkconfig`
  by hand.

---

## Limits and gotchas

- One RMT symbol field holds **32767 ticks**. Longer phases are split across
  several symbols automatically.
- At the default 1 MHz, a phase spans 2 µs to ~32.7 ms per slice. For
  second-long periods, drop `resolution_hz` to 100 kHz so each pulse costs 10x
  fewer symbols.
- **Endless trains must fit in one RMT memory block**, because the hardware
  replays the block without re-entering the encoder. Exceeding it returns
  `ESP_ERR_INVALID_SIZE`; lower `resolution_hz` or raise `mem_block_symbols`.
- Only **one train at a time** per generator. Starting a second returns
  `ESP_ERR_INVALID_STATE` — the symbol buffer is shared and the RMT driver holds
  a pointer to it for the whole transaction.
- `pulse_gen_wait_done()` returns `ESP_ERR_INVALID_STATE` for endless trains.
- `mem_block_symbols` must be a multiple of `SOC_RMT_MEM_WORDS_PER_CHANNEL`
  (64 on ESP32 and ESP32-S2, 48 elsewhere), or channel allocation fails.
- Avoid strapping pins (0, 2, 12, 15) and the boot UART pins (1, 3): they toggle
  during reset, before your idle level is ever applied.

---

## Supported targets

ESP32 · ESP32-S2 · ESP32-S3 · ESP32-C3 · ESP32-C6 · ESP32-H2

---

## License

MIT. See `LICENSE`.

Links

To add this component to your project, run:

idf.py add-dependency "leoribg/pulse_generator^1.0.1"

download archive

Stats

  • Archive size
    Archive size ~ 19.73 KB
  • Downloaded in total
    Downloaded in total 0 times
  • Downloaded this version
    This version: 0 times

Badge

leoribg/pulse_generator version: 1.0.1
|