Skip to content

Conversation

@SvenHaedrich
Copy link

@SvenHaedrich SvenHaedrich commented Mar 31, 2025

Add a DALI driver to Zephyr

DALI is the digital addressable lighting interface, a standard for professional lighting solutions. In its core the standard is based on Manchester encoded exchange of frames. Because specific behavior for the interface is required by the DALI-Alliance a dedicated driver is needed to pass the mandatory tests.

Driver Status

Here are two implementations for a DALI driver.

lpcxpresso11u68

This driver is very specific for this chip. There was no counter or pwm implementation available at the time of the implementation. So, the driver accesess these peripherals directly. This makes this implementation hardly transferable to other platforms. The reason to include this driver in this PR is the fact that it passes all the relevant DALI tests. Hence,m it can serve as a source of inspiration how to achieve full compatibility. You can select this implementation with

export BOARD=lpcxpresso11u68

nucleo_f091rc

This is a more generic implementation of a DALI driver. Though, it requires a PWM peripheral that can capture edge events on gpio pins. This is true for a variety of STM32 controllers. Though, this driver is only tested for the f091.

export BOARD=nucleo_f091rc

Sample program

This code includes a sample application that sends DALI frames. These result in a blink action on LEDs (or device type 6 control gears, as we call it in the business) that are connected to the DALI bus. The device tree overlays are optimized for a DALI Click Adapter providing the physical interface to the DALI bus. Refer to the devicetree files to find details about the connection of Rx and Tx lines.
You can build the sample code:

west build --board $BOARD samples/drivers/dali

LPC11U6x Dali Test Status

This low-level driver passes the following tests from the DALI-Alliance. (DiiA V2 2.6.0.0 - February 2024) This list will
not track tests that relate to the hardware of the DALI interface.

  • 2.1 Test preamble
  • 3.7 Transmitter bit timing
  • 3.8 Transmitter frame timing
  • 3.9 Receiver start-up behavior
  • 3.11 Receiver bit timing
  • 3.12 Extended receiver bit timing
  • 3.13 Receiver forward frame violation
  • 3.14 Receiver settling time
  • 3.15 Receiver frame timing FF-FF send twice
  • 3.16 Transmitter collision avoidance by priority
  • 3.17 Transmitter collision detection for truncated idle phase
  • 3.18 Transmitter collision detection for extended active phase
  • 3.19 Frame size violation
  • 3.20 Backward frame collision detection

STM PWM Dali Test Status

This low-level driver passes the following tests from the DALI-Alliance. (DiiA V2 2.6.0.0 - February 2024) This list will
not track tests that relate to the hardware of the DALI interface.

  • 2.1 Test preamble
  • 3.7 Transmitter bit timing
  • 3.8 Transmitter frame timing
  • 3.9 Receiver start-up behavior
  • 3.11 Receiver bit timing
  • 3.12 Extended receiver bit timing
  • 3.13 Receiver forward frame violation
  • 3.14 Receiver settling time
  • 3.15 Receiver frame timing FF-FF send twice
  • 3.16 Transmitter collision avoidance by priority
  • 3.17 Transmitter collision detection for truncated idle phase
  • 3.18 Transmitter collision detection for extended active phase
  • 3.19 Frame size violation
  • 3.20 Backward frame collision detection

Design Considerations for DALI Low Level Driver

API-Design

The API for the DALI bus needs to provide the following functionalities:

  1. Send DALI frames
  2. Receive DALI frames
  3. Report changes of the bus status (e.g. bus failure)
  4. Abort pending transmissions

Further requirements

  1. The low-level-driver should be as simple as possible
  2. All timings and time critical tasks are handled within the low-level-driver
  3. Forward frames are looped back into the driver

Frame Types

Currently, supported events and frame types are:

ID description read write
DALI_EVENT_NONE no event * *
DALI_FRAME_CORRUPT corrupt frame * *
DALI_FRAME_BACKWARD backward frame * *
DALI_FRAME_GEAR forward 16bit gear frame * *
DALI_FRAME_GEAR_TWICE forward 16bit gear frame, received twice * -
DALI_FRAME_DEVICE forward 24bit device frame * *
DALI_FRAME_DEVICE_TWICE forward 24bit device frame, received twice * -
DALI_FRAME_FIRMWARE forward 32bit firmware frame * *
DALI_FRAME_FIRMWARE_TWICE forward 32bit firmware frame * -
DALI_NO_ANSWER received no answer to query * -
DALI_EVENT_BUS_FAILURE detected a bus failure * -
DALI_EVENT_BUS_IDLE detected that bus is idle again after failure * -

Receive

int dali_receive(const struct device *dev, struct dali_frame *rx_frame, k_timeout_t timeout);

dev is a pointer to a DALI device
rx_frame is a buffer for a DALI frame or an event
timeout timeout period

dali_receive will return the next frame received from the DALI bus. The function has an input queue, to ensure that no frame or event is lost.

return codes

code meaning
0 success, valid data in *rx_frame
-ENOMSG returned without waiting, or waiting period timed out, *rx_frame is invalid

Send

int dali_send(const struct device *dev, struct dali_tx_frame *rx_frame)

dev is a pointer to a DALI device
tx_frame is a buffer holding a single DALI frame, the buffer is copied into data structures of the low-level driver and may be discarded after return

This function supports async operation. Any frame is stored into an internal send slot and the dali_send returns immediately. dali_send maintains two send slots. One slot is reserved for backward frames. The other slot is used for all kind of forward frames. In case of a forward frame in its slot that is pending for transmission, it is still possible to provide a backward frame. That backward frame will be transmitted before the pending forward frame, whenever possible. There is a strict timing limit from the DALI standard (see IEC 62386-101:2022 8.1.2 Table 17) for the timing of backward frames. When these restrictions can not be fulfilled, the backward frame may be dropped and an error code returned.

return codes

code meaning
0 success, added frame to the output queue
-EINVAL invalid input parameters
-EBUSY input slot is busy - data is rejected
-ETIMEDOUT backward frame is too late, backward frame is dropped

Abort

void dali_abort(const struct device *dev)

dev is a pointer to a DALI device

dali_abort will abort all pending or ongoing forward frame transmissions. Transmission will be aborted, regardless of bit timings, at the shortest possible time. This can result in corrupt a frame.

Reasoning

Firstly, I think an async send is the way to go. I can not think of a scenario where it is favorable to block execution until the frame is sent. Arguments for that:

  1. DALI is a slow protocol, lots of useful actions can be executed while a DALI frame is sent.
  2. There is no action required from the DALI protocol at the end of a frame transmission, so there is no benefit from using the flow-control to signal that point in time.
  3. If blocking is required it is easy to implement a blocking wrapper function.

Secondly, on using a send queue. I feel my stomach ache about that. But as good practitioners we need to grow above feelings. Here is some reasoning. To build a sound API for DALI we need to consider what we require from the higher stack levels.

There a basically two kind of DALI controls:

  1. Those with an application controller (or external event sources)
  2. Those without.

Control Gears

Let's start with the case 2 as it's much easier. All messages follow a simple sequence.

sequenceDiagram
   DALI-Bus->>Low-Level-Driver: Forward Frame
   Low-Level-Driver->>DALI-Stack: Forward Frame
   DALI-Stack-->>Low-Level-Driver: Backward frame (or NO)
   Low-Level-Driver-->>DALI-Bus: Backward Frame (or NO)
Loading

Actually, just a best effort to transmit the backward frame is required from the low-level-driver. The backward frame is send regardless of collisions, and I would say even a bus down condition does not matter as the transmission of the backward frame is bound to strict timing limits.

The higher levels of the DALI protocol ensure that there is only a single frame processed at a time. Hence, it is sufficient to have a single input slot for forward and backward frames.

/* main loop for a control gear */
for(;;) {
   struct dali_frame frame_received;
   if (dali_receive(&dev, &frame_received, timeout) >= 0) {
      if (frame_received.event_type == DALI_FRAME_GEAR ||
          frame_received.event_type == DALI_FRAME_GEAR_TWICE ||
          frame_received.event_type == DALI_EVENT_FAILURE ) {
         struct dali_tx_frame back_frame = { 0 };
         dali_process_frame(&bus, frame_received, &back_frame);
         dali_send(&dev, &back_frame);
      }
   }
}

Optionally, the low-level-driver can track whether the timing requirements for a backframe are meet. In case the backframe is overdue, the low-level-driver can decide to drop the backframe completely as it will be disregarded anyhow.

Control Devices

Now look at e.g. control devices with an application controller. Firstly, we have exactly the sequence pattern from above, where forward frames are received and require processing and an optional response from the DALI-stack. Parallel to that there might be an event that requires signalling (or it triggers sending of a forward frame typically something like a DAPC command).

The processing of a forward frame needs to have a higher priority than sending an event as it might require a backward frame that obeys the strict timing requirements. Actually, an event has to wait until the bus is idle for the time defined by the event´s inter frame timing priority.

sequenceDiagram
    DALI-Bus->>Low-Level-Driver: Forward Frame
    Event-Context-->>Low-Level-Driver: Event Frame
    Low-Level-Driver->>DALI-Stack: Forward Frame
    DALI-Stack-->>Low-Level-Driver: Backward Frame
    Low-Level-Driver-->>DALI-Bus: Backward Frame
    Low-Level-Driver-->>DALI-Bus: Event-Frame 
Loading

How should this be expressed in code? The main loop of the DALI stack will be identical to the one for control gears.
Ideally events are generated in a different context and dali_send will keep the event frame in a buffer and wait for the specified inter frame timing while back frames might sneak by.

Collisions

The concepts of collision avoidance, detection, and recovery is found in IEC 62386-101:2022 9.2. These concepts apply for forward frames only. Collision of backward frames are neither detected, nor is it necessary or possible to re-send these. When the low-level driver detects a collision it will destroy the ongoing frame transmission by application of a break signal (see IEC 62386-101:2022 9.2.4 Table 25). After the recovery time the low-level-driver will automatically re-try to send the frame. It is the task of the application layer to control and tame the re-sending if necessary.

So, sending a forward frame from the application layer needs to look like this:

const struct dali_tx_frame cmd_dapc = {
   .frame = { .event_type = DALI_FRAME_GEAR, .data = .. },
   .priority = 2, /* or 3,4,5 */
   .is_query = false,
};

/* empty the receive buffer */
while (dali_receive(&dev, &frame_result, 0)!=0);

/* send forward frame */
int result = dali_send(&dev, &cmd_dapc);
if (result < 0) {
   return result; /* delegate error handling to caller */
}

uint8_t retry = 0;
struct dali_frame frame_result = {0};

while (frame_result != cmd_dapc.dali_frame && retry++ < MAX_ATTEMPTS) {   
   /* wait for result */
   if (dali_receive(&dev, &frame_result, timeout) >= 0) {
      error ("this should not fail\n");
      return -NOT_EXPECTED;
   }
}
if (retry == MAX_ATTEMPTS) {
   dali_abort(&dev);
   error ("could not send, though I tried\n")
   return -NOT_SEND;
}

Transactions

The concept of transactions is found in IEC 62386-101:2022 9.3: The purpose of transactions is to ensure that a sequence of commands send by one control device cannot be interrupted by another control device.

Look, for instance, at the retrieval of a memory bank cell for a control gear. The required command sequence is something like:

DTR0 #00
DTR1 #00
READ MEMORY LOCATION (DTR1,DTR0)

There are two commands that prepare the query, and a query command that expects backward frames
from the addressed gears. The code to transmit these frames can look like this:

const struct dali_tx_frame cmd_dtr0 = {
   .frame = { .event_type = DALI_FRAME_GEAR, .data = .. },
   .priority = 2, /* or 3,4,5 */
   .is_query = false,
};
const struct dali_tx_frame cmd_dtr1 = {
   .frame = { .event_type = DALI_FRAME_GEAR, .data = .. },
   .priority = 1, /* do not intercept */
   .is_query = false,
};
const struct dali_tx_frame cmd_read = {
   .frame = { .event_type = DALI_FRAME_GEAR, .data = .. },
   .priority = 1, /* do not intercept */
   .is_query = true,
};

struct dali_frame frame_result;
uint8_t attempt = 0;

do {
   /* empty the receive buffer */
   while (dali_receive(&dev, &frame_result, 0)!=0);

   /* transmit first frame */
   int result = dali_send(&dev, &cmd_dtr0);
   if (result < 0) {
      return result; /* delegate error handling to caller */
   }
   /* wait for successful end of transmission */
   if (dali_receive(&dev, &frame_result, timeout) >= 0) {
      error ("this should not fail\n");
      return -NOT_EXPECTED;
   }

   if (frame_result == cmd_dtr0.dali_frame) {
      /* transmit second frame */
      int result = dali_send(&dev, &cmd_dtr1);
      if (result < 0) {
         return result; /* delegate error handling to caller */
      }
      /* wait for successful end of transmission */
      if (dali_receive(&dev, &frame_result, timeout) >= 0) {
         error ("this should not fail\n");
         return -NOT_EXPECTED;
      }
   }
   else {
      dali_abort(&dev);
   }

   if (frame_result == cmd_dtr1.dali_frame) {
      /* transmit query */
      int result = dali_send(&dev, &cmd_read);
      if (result < 0) {
         return result; /* delegate error handling to caller */
      }
      /* wait for successful end of transmission */
      if (dali_receive(&dev, &frame_result, timeout) >= 0) {
         error ("this should not fail\n");
         return -NOT_EXPECTED;
      }
   }
   else {
      dali_abort(&dev);
   }
   if (frame_result == cmd_read.dali_frame) {
      printf("success: read value %d from gear\n", frame_result.data);
      break;
   }
   if (frame_result.event_type == DALI_NO_ANSWER) {
      print("success: the answer is NO\n");
      break;
   }
} while (attempt++ < MAX_ATTEMPT)

Obviously, there are a lot of code repetitions which can be saved when defining a function

int dali_send_array(dev, frame_array, size);

Probably, this will be a blocking function. This should not hurt too much, as we most likely wait for a backframe anyhow.

We have to wait until the transmission of the preceding frame has finished before we call for the next frame to be send to the DALI bus. Otherwise, we can expect to see a EBUSY error code.

@SvenHaedrich SvenHaedrich force-pushed the add-dali-drivers branch 8 times, most recently from 6f0d150 to 579f2e9 Compare April 3, 2025 16:35
@SvenHaedrich SvenHaedrich changed the title [Drivers:] [Drivers:] Add a DALI driver to Zephyr Apr 3, 2025
@SvenHaedrich SvenHaedrich force-pushed the add-dali-drivers branch 13 times, most recently from 35e78a5 to dff527d Compare April 9, 2025 16:03
@SvenHaedrich SvenHaedrich force-pushed the add-dali-drivers branch 2 times, most recently from bb8ec94 to c9e8506 Compare April 15, 2025 16:38
@SvenHaedrich SvenHaedrich force-pushed the add-dali-drivers branch 5 times, most recently from 07c9a2c to 1447272 Compare April 23, 2025 16:04
@SvenHaedrich SvenHaedrich force-pushed the add-dali-drivers branch 7 times, most recently from 1983d32 to da8c7a8 Compare October 29, 2025 18:08
@SvenHaedrich SvenHaedrich force-pushed the add-dali-drivers branch 7 times, most recently from 25bbe02 to 2a71ea5 Compare November 3, 2025 14:17
@SvenHaedrich SvenHaedrich force-pushed the add-dali-drivers branch 4 times, most recently from 4ad07f6 to 1e76677 Compare November 19, 2025 10:28
SvenHaedrich pushed a commit that referenced this pull request Dec 5, 2025
Map all pins of RT685's Flexcomm #1 peripheral.

Needed for opration of Flexcomm #1 in I2S mode or Flexcomm zephyrproject-rtos#3 in I2S
mode with shared signals.

Signed-off-by: Vit Stanicek <vit.stanicek@nxp.com>
SvenHaedrich pushed a commit that referenced this pull request Dec 5, 2025
Add SCK and WS signals to a shared signal set between Flexcomm #1 and
Flexcomm zephyrproject-rtos#3 for the mimxrt685_evk/mimxrt685s/cm33.

This enables the board to both transmit and receive audio, as the BCK
and WS signals produced by the WM8904 codec are only connected to the
Flexcomm #1 peripheral.

Signed-off-by: Vit Stanicek <vit.stanicek@nxp.com>
@SvenHaedrich SvenHaedrich force-pushed the add-dali-drivers branch 4 times, most recently from 3fd3a68 to 180773d Compare December 9, 2025 12:16
Defines the generic interface for the
DALI driver.

Signed-off-by: Sven Hädrich <sven.haedrich@sevenlab.de>
Provide a basic example how the DALI
driver can be used.

Signed-off-by: Sven Hädrich <sven.haedrich@sevenlab.de>
Implementation of a DALI driver for a cpu
that provide a PWM interface for zephyr.
The code is tested for a STM32F091 cpu and
for the nRF52840 cpu.

Signed-off-by: Sven Hädrich <sven.haedrich@sevenlab.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants