drivers: display: Add support for LED matrix driven by nRF SoC GPIOs

Add a display driver and the corresponding devicetree binding for a LED
matrix with rows and columns driven by nRF SoCs GPIOs. Such matrix can
be found, for example, in the BBC micro:bit boards.

Signed-off-by: Andrzej Głąbek <andrzej.glabek@nordicsemi.no>
diff --git a/drivers/display/CMakeLists.txt b/drivers/display/CMakeLists.txt
index 2c72d93..492ebad 100644
--- a/drivers/display/CMakeLists.txt
+++ b/drivers/display/CMakeLists.txt
@@ -2,6 +2,7 @@
 
 zephyr_library()
 zephyr_library_sources_ifdef(CONFIG_DISPLAY_MCUX_ELCDIF	display_mcux_elcdif.c)
+zephyr_library_sources_ifdef(CONFIG_DISPLAY_NRF_LED_MATRIX display_nrf_led_matrix.c)
 zephyr_library_sources_ifdef(CONFIG_DUMMY_DISPLAY	display_dummy.c)
 zephyr_library_sources_ifdef(CONFIG_FRAMEBUF_DISPLAY	display_framebuf.c)
 zephyr_library_sources_ifdef(CONFIG_GD7965		gd7965.c)
diff --git a/drivers/display/Kconfig b/drivers/display/Kconfig
index 674bebd..c9a9f95 100644
--- a/drivers/display/Kconfig
+++ b/drivers/display/Kconfig
@@ -23,6 +23,7 @@
 source "drivers/display/Kconfig.grove"
 source "drivers/display/Kconfig.mcux_elcdif"
 source "drivers/display/Kconfig.microbit"
+source "drivers/display/Kconfig.nrf_led_matrix"
 source "drivers/display/Kconfig.ili9xxx"
 source "drivers/display/Kconfig.sdl"
 source "drivers/display/Kconfig.ssd1306"
diff --git a/drivers/display/Kconfig.nrf_led_matrix b/drivers/display/Kconfig.nrf_led_matrix
new file mode 100644
index 0000000..b89b9af
--- /dev/null
+++ b/drivers/display/Kconfig.nrf_led_matrix
@@ -0,0 +1,19 @@
+# Copyright (c) 2021, Nordic Semiconductor ASA
+# SPDX-License-Identifier: Apache-2.0
+
+config DISPLAY_NRF_LED_MATRIX
+	bool "LED matrix driven by GPIOs"
+	depends on SOC_FAMILY_NRF
+	select NRFX_GPIOTE
+	select NRFX_PPI if HAS_HW_NRF_PPI
+	help
+	  Enable driver for a LED matrix with rows and columns driven by
+	  GPIOs. The matrix is refreshed pixel by pixel (only one LED is
+	  turned on in particular time slots) and each pixel can have one
+	  of 256 levels of brightness (0 means off completely).
+	  Assignment of GPIOs to rows and columns and the mapping of those
+	  to pixels are specified in properties of a "nordic,nrf-led-matrix"
+	  compatible node in devicetree.
+	  The driver uses one TIMER instance and, depending on what is set
+	  in devicetree, one PWM instance or one PPI channel and one GPIOTE
+	  channel.
diff --git a/drivers/display/display_nrf_led_matrix.c b/drivers/display/display_nrf_led_matrix.c
new file mode 100644
index 0000000..ac81b5e
--- /dev/null
+++ b/drivers/display/display_nrf_led_matrix.c
@@ -0,0 +1,450 @@
+/*
+ * Copyright (c) 2021, Nordic Semiconductor ASA
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <drivers/display.h>
+#include <devicetree.h>
+#include <dt-bindings/gpio/gpio.h>
+#include <hal/nrf_timer.h>
+#ifdef PWM_PRESENT
+#include <hal/nrf_pwm.h>
+#endif
+#include <nrfx_gpiote.h>
+#ifdef PPI_PRESENT
+#include <nrfx_ppi.h>
+#endif
+
+#define MATRIX_NODE  DT_INST(0, nordic_nrf_led_matrix)
+#define TIMER_NODE   DT_PHANDLE(MATRIX_NODE, timer)
+#define USE_PWM      DT_NODE_HAS_PROP(MATRIX_NODE, pwm)
+#define ROW_COUNT    DT_PROP_LEN(MATRIX_NODE, row_gpios)
+#define COL_COUNT    DT_PROP_LEN(MATRIX_NODE, col_gpios)
+
+#define X_PIXELS     DT_PROP(MATRIX_NODE, width)
+#define Y_PIXELS     DT_PROP(MATRIX_NODE, height)
+#define PIXEL_COUNT  DT_PROP_LEN(MATRIX_NODE, pixel_mapping)
+BUILD_ASSERT(PIXEL_COUNT == (X_PIXELS * Y_PIXELS),
+	     "Invalid length of pixel-mapping");
+
+#define PIXEL_MAPPING(idx)  DT_PROP_BY_IDX(MATRIX_NODE, pixel_mapping, idx)
+#define CHECK_PIXEL(node_id, pha, idx) \
+	BUILD_ASSERT((PIXEL_MAPPING(idx) >> 4) < ROW_COUNT, \
+		     "Invalid row index in pixel-mapping["#idx"]"); \
+	BUILD_ASSERT((PIXEL_MAPPING(idx) & 0xF) < COL_COUNT, \
+		     "Invalid column index in pixel-mapping["#idx"]");
+DT_FOREACH_PROP_ELEM(MATRIX_NODE, pixel_mapping, CHECK_PIXEL)
+
+#define REFRESH_FREQUENCY  DT_PROP(MATRIX_NODE, refresh_frequency)
+#define BASE_FREQUENCY     16000000
+#define TIMER_CLK_CONFIG   NRF_TIMER_FREQ_16MHz
+#define PWM_CLK_CONFIG     NRF_PWM_CLK_16MHz
+#define BRIGHTNESS_MAX     255
+
+#define QUANTUM  (BASE_FREQUENCY / (REFRESH_FREQUENCY * PIXEL_COUNT * \
+				    BRIGHTNESS_MAX))
+#define PIXEL_PERIOD  (BRIGHTNESS_MAX * QUANTUM)
+BUILD_ASSERT(PIXEL_PERIOD <= BIT_MASK(16));
+#if USE_PWM
+BUILD_ASSERT(PIXEL_PERIOD <= PWM_COUNTERTOP_COUNTERTOP_Msk);
+#endif
+
+#define ACTIVE_LOW_MASK  0x80
+#define PSEL_MASK        0x7F
+
+struct display_drv_config {
+	NRF_TIMER_Type *timer;
+#if USE_PWM
+	NRF_PWM_Type *pwm;
+#endif
+	uint8_t rows[ROW_COUNT];
+	uint8_t cols[COL_COUNT];
+	uint8_t pixel_mapping[PIXEL_COUNT];
+};
+
+struct display_drv_data {
+#if USE_PWM
+	uint16_t seq;
+#else
+	uint8_t gpiote_ch;
+#endif
+	uint8_t pixel_idx;
+	uint8_t framebuf[PIXEL_COUNT];
+	uint8_t brightness;
+	bool    blanking;
+};
+
+static void set_pin(uint8_t pin_info, bool active)
+{
+	uint32_t value = active ? 1 : 0;
+
+	if (pin_info & ACTIVE_LOW_MASK) {
+		value = !value;
+	}
+	nrf_gpio_pin_write(pin_info & PSEL_MASK, value);
+}
+
+static int api_blanking_on(const struct device *dev)
+{
+	struct display_drv_data *dev_data = dev->data;
+	const struct display_drv_config *dev_config = dev->config;
+
+	if (!dev_data->blanking) {
+		nrf_timer_task_trigger(dev_config->timer, NRF_TIMER_TASK_STOP);
+		for (uint8_t i = 0; i < ROW_COUNT; ++i) {
+			set_pin(dev_config->rows[i], false);
+		}
+		for (uint8_t i = 0; i < COL_COUNT; ++i) {
+			set_pin(dev_config->cols[i], false);
+		}
+
+		dev_data->blanking = true;
+	}
+
+	return 0;
+}
+
+static int api_blanking_off(const struct device *dev)
+{
+	struct display_drv_data *dev_data = dev->data;
+	const struct display_drv_config *dev_config = dev->config;
+
+	if (dev_data->blanking) {
+		dev_data->pixel_idx = PIXEL_COUNT - 1;
+
+		nrf_timer_task_trigger(dev_config->timer, NRF_TIMER_TASK_CLEAR);
+		nrf_timer_task_trigger(dev_config->timer, NRF_TIMER_TASK_START);
+
+		dev_data->blanking = false;
+	}
+
+	return 0;
+}
+
+static void *api_get_framebuffer(const struct device *dev)
+{
+	struct display_drv_data *dev_data = dev->data;
+
+	return dev_data->framebuf;
+}
+
+static int api_set_brightness(const struct device *dev,
+			      const uint8_t brightness)
+{
+	struct display_drv_data *dev_data = dev->data;
+	uint8_t new_brightness = CLAMP(brightness, 1, BRIGHTNESS_MAX);
+	int16_t delta = (int16_t)new_brightness - dev_data->brightness;
+
+	dev_data->brightness = new_brightness;
+
+	for (uint8_t i = 0; i < PIXEL_COUNT; ++i) {
+		uint8_t old_val = dev_data->framebuf[i];
+
+		if (old_val) {
+			int16_t new_val = old_val + delta;
+
+			dev_data->framebuf[i] =
+				(uint8_t)CLAMP(new_val, 1, BRIGHTNESS_MAX);
+		}
+	}
+
+	return 0;
+}
+
+static int api_set_contrast(const struct device *dev,
+			    const uint8_t contrast)
+{
+	return -ENOTSUP;
+}
+
+static int api_set_pixel_format(const struct device *dev,
+				const enum display_pixel_format format)
+{
+	switch (format) {
+	case PIXEL_FORMAT_MONO01:
+		return 0;
+	default:
+		return -ENOTSUP;
+	}
+}
+
+static int api_set_orientation(const struct device *dev,
+			       const enum display_orientation orientation)
+{
+	switch (orientation) {
+	case DISPLAY_ORIENTATION_NORMAL:
+		return 0;
+	default:
+		return -ENOTSUP;
+	}
+}
+
+static void api_get_capabilities(const struct device *dev,
+				 struct display_capabilities *caps)
+{
+	caps->x_resolution = X_PIXELS;
+	caps->y_resolution = Y_PIXELS;
+	caps->supported_pixel_formats = PIXEL_FORMAT_MONO01;
+	caps->screen_info = 0;
+	caps->current_pixel_format = PIXEL_FORMAT_MONO01;
+	caps->current_orientation = DISPLAY_ORIENTATION_NORMAL;
+}
+
+static inline void move_to_next_pixel(uint8_t *mask, uint8_t *data,
+				      const uint8_t **byte_buf)
+{
+	*mask <<= 1;
+	if (!*mask) {
+		*mask = 0x01;
+		*data = *(*byte_buf)++;
+	}
+}
+
+static int api_write(const struct device *dev,
+		     const uint16_t x, const uint16_t y,
+		     const struct display_buffer_descriptor *desc,
+		     const void *buf)
+{
+	struct display_drv_data *dev_data = dev->data;
+	const uint8_t *byte_buf = buf;
+	uint16_t end_x = x + desc->width;
+	uint16_t end_y = y + desc->height;
+
+	if (x >= X_PIXELS || end_x > X_PIXELS ||
+	    y >= Y_PIXELS || end_y > Y_PIXELS) {
+		return -EINVAL;
+	}
+
+	if (desc->pitch < desc->width) {
+		return -EINVAL;
+	}
+
+	uint16_t to_skip = desc->pitch - desc->width;
+	uint8_t mask = 0;
+	uint8_t data = 0;
+
+	for (uint16_t py = y; py < end_y; ++py) {
+		for (uint16_t px = x; px < end_x; ++px) {
+			move_to_next_pixel(&mask, &data, &byte_buf);
+			dev_data->framebuf[px + (py * X_PIXELS)] =
+				(data & mask) ? dev_data->brightness : 0;
+		}
+
+		if (to_skip) {
+			uint16_t cnt = to_skip;
+
+			do {
+				move_to_next_pixel(&mask, &data, &byte_buf);
+			} while (--cnt);
+		}
+	}
+
+	return 0;
+}
+
+static int api_read(const struct device *dev,
+		    const uint16_t x, const uint16_t y,
+		    const struct display_buffer_descriptor *desc,
+		    void *buf)
+{
+	return -ENOTSUP;
+}
+
+const struct display_driver_api driver_api = {
+	.blanking_on = api_blanking_on,
+	.blanking_off = api_blanking_off,
+	.write = api_write,
+	.read = api_read,
+	.get_framebuffer = api_get_framebuffer,
+	.set_brightness = api_set_brightness,
+	.set_contrast = api_set_contrast,
+	.get_capabilities = api_get_capabilities,
+	.set_pixel_format = api_set_pixel_format,
+	.set_orientation = api_set_orientation,
+};
+
+static void timer_irq_handler(void *arg)
+{
+	const struct device *dev = arg;
+	struct display_drv_data *dev_data = dev->data;
+	const struct display_drv_config *dev_config = dev->config;
+	uint8_t prev_row_idx, pixel_mapping, row_pin_info, col_pin_info;
+	uint16_t pulse;
+
+	/* The timer is automagically stopped and cleared by shortcuts
+	 * on the same event (COMPARE0) that generates this interrupt.
+	 * But the event itself needs to be cleared here.
+	 */
+	nrf_timer_event_clear(dev_config->timer, NRF_TIMER_EVENT_COMPARE0);
+
+	/* Disable the row that contains the previously handled pixel. */
+	prev_row_idx = dev_config->pixel_mapping[dev_data->pixel_idx] >> 4;
+	set_pin(dev_config->rows[prev_row_idx], false);
+	/* Disconnect that pixel column pin from the peripheral driving it. */
+#if USE_PWM
+	nrf_pwm_disable(dev_config->pwm);
+#else
+	NRF_GPIOTE->CONFIG[dev_data->gpiote_ch] = 0;
+#endif
+
+	/* Switch to the next pixel. */
+	++dev_data->pixel_idx;
+	if (dev_data->pixel_idx >= PIXEL_COUNT) {
+		dev_data->pixel_idx = 0;
+	}
+	pixel_mapping = dev_config->pixel_mapping[dev_data->pixel_idx];
+	row_pin_info = dev_config->rows[pixel_mapping >> 4];
+	col_pin_info = dev_config->cols[pixel_mapping & 0xF];
+
+	/* Prepare the low pulse on the column pin for the current pixel. */
+	pulse = dev_data->framebuf[dev_data->pixel_idx] * QUANTUM;
+#if USE_PWM
+	dev_config->pwm->PSEL.OUT[0] = col_pin_info & PSEL_MASK;
+	dev_data->seq = pulse
+		      | ((col_pin_info & ACTIVE_LOW_MASK) ? 0 : BIT(15));
+	nrf_pwm_enable(dev_config->pwm);
+	nrf_pwm_task_trigger(dev_config->pwm, NRF_PWM_TASK_SEQSTART0);
+#else
+	uint32_t gpiote_cfg = GPIOTE_CONFIG_MODE_Task
+		| ((col_pin_info & PSEL_MASK) << GPIOTE_CONFIG_PSEL_Pos);
+
+	if (col_pin_info & ACTIVE_LOW_MASK) {
+		gpiote_cfg |= (GPIOTE_CONFIG_POLARITY_LoToHi
+			       << GPIOTE_CONFIG_POLARITY_Pos)
+			      /* If there should be no pulse at all for a given
+			       * pixel, its column GPIO needs to be configured
+			       * as initially inactive.
+			       */
+			   |  ((pulse == 0 ? GPIOTE_CONFIG_OUTINIT_High
+					   : GPIOTE_CONFIG_OUTINIT_Low)
+			       << GPIOTE_CONFIG_OUTINIT_Pos);
+	} else {
+		gpiote_cfg |= (GPIOTE_CONFIG_POLARITY_HiToLo
+			       << GPIOTE_CONFIG_POLARITY_Pos)
+			   |  ((pulse == 0 ? GPIOTE_CONFIG_OUTINIT_Low
+					   : GPIOTE_CONFIG_OUTINIT_High)
+			       << GPIOTE_CONFIG_OUTINIT_Pos);
+	}
+	nrf_timer_cc_set(dev_config->timer, 1, pulse);
+	NRF_GPIOTE->CONFIG[dev_data->gpiote_ch] = gpiote_cfg;
+#endif
+
+	/* Enable the row drive for the current pixel and restart the timer. */
+	set_pin(row_pin_info, true);
+	nrf_timer_task_trigger(dev_config->timer, NRF_TIMER_TASK_START);
+}
+
+static int instance_init(const struct device *dev)
+{
+	struct display_drv_data *dev_data = dev->data;
+	const struct display_drv_config *dev_config = dev->config;
+
+#if USE_PWM
+	uint32_t out_psels[NRF_PWM_CHANNEL_COUNT] = {
+		NRF_PWM_PIN_NOT_CONNECTED,
+		NRF_PWM_PIN_NOT_CONNECTED,
+		NRF_PWM_PIN_NOT_CONNECTED,
+		NRF_PWM_PIN_NOT_CONNECTED,
+	};
+	nrf_pwm_sequence_t sequence = {
+		.values.p_raw = &dev_data->seq,
+		.length = 1,
+	};
+
+	nrf_pwm_pins_set(dev_config->pwm, out_psels);
+	nrf_pwm_configure(dev_config->pwm,
+		PWM_CLK_CONFIG, NRF_PWM_MODE_UP, PIXEL_PERIOD);
+	nrf_pwm_decoder_set(dev_config->pwm,
+		NRF_PWM_LOAD_COMMON, NRF_PWM_STEP_TRIGGERED);
+	nrf_pwm_sequence_set(dev_config->pwm, 0, &sequence);
+	nrf_pwm_loop_set(dev_config->pwm, 0);
+	nrf_pwm_shorts_set(dev_config->pwm, NRF_PWM_SHORT_SEQEND0_STOP_MASK);
+#else
+	nrfx_err_t err;
+	nrf_ppi_channel_t ppi_ch;
+
+	err = nrfx_ppi_channel_alloc(&ppi_ch);
+	if (err != NRFX_SUCCESS) {
+		return -ENOMEM;
+	}
+
+	err = nrfx_gpiote_channel_alloc(&dev_data->gpiote_ch);
+	if (err != NRFX_SUCCESS) {
+		nrfx_ppi_channel_free(ppi_ch);
+		return -ENOMEM;
+	}
+
+	nrf_ppi_channel_endpoint_setup(NRF_PPI, ppi_ch,
+		nrf_timer_event_address_get(dev_config->timer,
+			nrf_timer_compare_event_get(1)),
+		nrf_gpiote_event_address_get(NRF_GPIOTE,
+			nrf_gpiote_out_task_get(dev_data->gpiote_ch)));
+	nrf_ppi_channel_enable(NRF_PPI, ppi_ch);
+#endif /* USE_PWM */
+
+	for (uint8_t i = 0; i < ROW_COUNT; ++i) {
+		uint8_t row_pin_info = dev_config->rows[i];
+
+		set_pin(row_pin_info, false);
+		nrf_gpio_cfg(row_pin_info & PSEL_MASK,
+			     NRF_GPIO_PIN_DIR_OUTPUT,
+			     NRF_GPIO_PIN_INPUT_DISCONNECT,
+			     NRF_GPIO_PIN_NOPULL,
+			     NRF_GPIO_PIN_S0S1,
+			     NRF_GPIO_PIN_NOSENSE);
+	}
+
+	for (uint8_t i = 0; i < COL_COUNT; ++i) {
+		uint8_t col_pin_info = dev_config->cols[i];
+
+		set_pin(col_pin_info, false);
+		nrf_gpio_cfg(col_pin_info & PSEL_MASK,
+			     NRF_GPIO_PIN_DIR_OUTPUT,
+			     NRF_GPIO_PIN_INPUT_DISCONNECT,
+			     NRF_GPIO_PIN_NOPULL,
+			     NRF_GPIO_PIN_S0S1,
+			     NRF_GPIO_PIN_NOSENSE);
+	}
+
+	nrf_timer_bit_width_set(dev_config->timer, NRF_TIMER_BIT_WIDTH_16);
+	nrf_timer_frequency_set(dev_config->timer, TIMER_CLK_CONFIG);
+	nrf_timer_cc_set(dev_config->timer, 0, PIXEL_PERIOD);
+	nrf_timer_shorts_set(dev_config->timer,
+			     NRF_TIMER_SHORT_COMPARE0_STOP_MASK |
+			     NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK);
+	nrf_timer_event_clear(dev_config->timer, NRF_TIMER_EVENT_COMPARE0);
+	nrf_timer_int_enable(dev_config->timer, NRF_TIMER_INT_COMPARE0_MASK);
+
+	IRQ_CONNECT(DT_IRQN(TIMER_NODE), DT_IRQ(TIMER_NODE, priority),
+		    timer_irq_handler, DEVICE_DT_GET(MATRIX_NODE), 0);
+	irq_enable(DT_IRQN(TIMER_NODE));
+
+	return 0;
+}
+
+static struct display_drv_data instance_data = {
+	.brightness = 0xFF,
+	.blanking   = true,
+};
+
+#define GET_PIN_INFO(node_id, pha, idx) \
+	(DT_GPIO_PIN_BY_IDX(node_id, pha, idx) | \
+	 (DT_PROP_BY_PHANDLE_IDX(node_id, pha, idx, port) << 5) | \
+	 ((DT_GPIO_FLAGS_BY_IDX(node_id, pha, idx) & GPIO_ACTIVE_LOW) ? \
+		ACTIVE_LOW_MASK : 0)),
+
+static const struct display_drv_config instance_config = {
+	.timer = (NRF_TIMER_Type *)DT_REG_ADDR(TIMER_NODE),
+#if USE_PWM
+	.pwm = (NRF_PWM_Type *)DT_REG_ADDR(DT_PHANDLE(MATRIX_NODE, pwm)),
+#endif
+	.rows = { DT_FOREACH_PROP_ELEM(MATRIX_NODE, row_gpios, GET_PIN_INFO) },
+	.cols = { DT_FOREACH_PROP_ELEM(MATRIX_NODE, col_gpios, GET_PIN_INFO) },
+	.pixel_mapping = DT_PROP(MATRIX_NODE, pixel_mapping),
+};
+
+DEVICE_DT_DEFINE(MATRIX_NODE,
+		 instance_init, NULL,
+		 &instance_data, &instance_config,
+		 POST_KERNEL, CONFIG_DISPLAY_INIT_PRIORITY, &driver_api);
diff --git a/dts/bindings/display/nordic,nrf-led-matrix.yaml b/dts/bindings/display/nordic,nrf-led-matrix.yaml
new file mode 100644
index 0000000..a14c772
--- /dev/null
+++ b/dts/bindings/display/nordic,nrf-led-matrix.yaml
@@ -0,0 +1,65 @@
+# Copyright (c) 2021, Nordic Semiconductor ASA
+# SPDX-License-Identifier: Apache-2.0
+
+description: Generic LED matrix driven by nRF SoC GPIOs
+
+compatible: "nordic,nrf-led-matrix"
+
+include: display-controller.yaml
+
+properties:
+    row-gpios:
+      type: phandle-array
+      required: true
+      description: |
+        Array of GPIOs to be used as rows of the matrix.
+
+    col-gpios:
+      type: phandle-array
+      required: true
+      description: |
+        Array of GPIOs to be used as columns of the matrix.
+
+    pixel-mapping:
+      type: uint8-array
+      required: true
+      description: |
+        Array of bytes that specify which rows and columns of the matrix
+        control its particular pixels, line by line. Each byte in this
+        array corresponds to one pixel of the matrix and specifies the row
+        index in the high nibble and the column index in the low nibble.
+
+        For example, the following snippet (from the bbc_microbit board DTS):
+
+          width = <5>;
+          height = <5>;
+          pixel-mapping = [00 13 01 14 02
+                           23 24 25 26 27
+                           ...
+
+        specifies that:
+        - pixel (0,0) is controlled by row 0 and column 0
+        - pixel (1,0) is controlled by row 1 and column 3
+        - pixel (0,1) is controlled by row 2 and column 3
+        - pixel (1,1) is controlled by row 2 and column 4
+        and so on.
+
+    refresh-frequency:
+      type: int
+      required: true
+      description: |
+        Frequency of refreshing the matrix, in Hz.
+
+    timer:
+      type: phandle
+      required: true
+      description: |
+        Reference to a TIMER instance for controlling refreshing of the matrix.
+
+    pwm:
+      type: phandle
+      required: false
+      description: |
+        Reference to a PWM instance for generating pulse signals on column
+        GPIOs. If not provided, one PPI and one GPIOTE channel are allocated
+        and used instead for generating those pulses.