Skip to content

drivers: Add DALI drivers to Zephyr #88128

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

SvenHaedrich
Copy link

@SvenHaedrich SvenHaedrich commented Apr 3, 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 2 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.

Copy link
Collaborator

@jeppenodgaard jeppenodgaard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to see a DALI PR! I skimmed the PR and have a few initial comments.

help
How many frames should be buffered in the recv queue

rsource "Kconfig.lpc11u6x"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

source should suffice

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to source.

/* 4.4 Register description*/
/* clang-format off */
typedef struct { /*!< (@ 0x40048000) SYSCON Structure */
__IO uint32_t SYSMEMREMAP; /*!< (@ 0x40048000) System memory remap */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use snake_case for variable names

Copy link
Author

@SvenHaedrich SvenHaedrich Apr 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of work in progress. I opened another PR, so that I can use clock_control for the syscon stuff. For the rest I would like to leave this conversation open, as a reminder of work to be done.

#define LPC_PINT_BASE 0xA0004000UL

/* 4.4 Register description*/
/* clang-format off */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why turn it off?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the clang-format off.

#define LPC_GPIO_PORT ((LPC_GPIO_PORT_Type *)LPC_GPIO_PORT_BASE)

/* 18.6.1 configuration register */
#define SCT_CONFIG_UNIFY (1U << 0U)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using bit macro


/* get the DALI device */
const struct device *dali_dev = DEVICE_DT_GET(DALI_NODE);
if (!dali_dev) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use device_is_ready. It also checks if device is null.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use devcie_is_ready now.

@SvenHaedrich SvenHaedrich changed the title [Drivers:] Add DALI drivers to Zephyr drivers: Add DALI drivers to Zephyr Apr 5, 2025
@SvenHaedrich SvenHaedrich force-pushed the add-dali-drivers branch 3 times, most recently from 5d932a6 to 43613e7 Compare April 5, 2025 11:55
Copy link
Collaborator

@pdgendt pdgendt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial comments, but besides that as it introduces a new API, I think this should be presented at the Architecture working group.
CC @carlescufi @henrikbrixandersen

@henrikbrixandersen henrikbrixandersen added the Architecture Review Discussion in the Architecture WG required label Apr 7, 2025
Copy link
Collaborator

@pdgendt pdgendt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description of this PR is quite elaborate, maybe this could be used as a documentation starting point too?

Next to that, it would be nice to have some tests in place, and shell commands.

@SvenHaedrich SvenHaedrich force-pushed the add-dali-drivers branch 3 times, most recently from 66f7806 to cd16b54 Compare April 7, 2025 16:10
@SvenHaedrich SvenHaedrich force-pushed the add-dali-drivers branch 5 times, most recently from 7334e5b to 607233c Compare April 13, 2025 18:28
@carlescufi
Copy link
Member

@SvenHaedrich would you be available to present this topic at the Architecture WG meeting in the near future?

@SvenHaedrich
Copy link
Author

@SvenHaedrich would you be available to present this topic at the Architecture WG meeting in the near future?

Sure, I will be happy to present my PR to the Working-Group. You can reach out to me at [email protected]

@pdgendt
Copy link
Collaborator

pdgendt commented Apr 16, 2025

@SvenHaedrich I see you squashed everything into a single commit, it think it would be better to split these into some smaller chunks:

  • Introduce driver API
  • Add driver A
  • Add driver B
  • Add sample

@SvenHaedrich SvenHaedrich force-pushed the add-dali-drivers branch 3 times, most recently from c54a16a to 4514d74 Compare April 22, 2025 16:58
Comment on lines +2 to +4
description: DALI interface for nxp lpc11u6x

compatible: "nxp,dali-lpc11u6x"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is very odd that there would need to be a binding aiming at just one single SoC?

Copy link
Author

@SvenHaedrich SvenHaedrich Apr 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. It is just that I have this mature driver for this specific chip. The driver is tailored around the counter peripherals of that chip. I guess there are more NXP cores using these peripherals but I never investigated this. As of now there are no Zephyr drivers for these counters. So, the driver treats the counters as "DALI peripherals" and the abstraction is the DALI API.

I don't want to be the one that decides whether this driver implementation is too specific to be part of Zephyr.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Architecture Review Discussion in the Architecture WG required area: Devicetree area: Samples Samples platform: NXP Drivers NXP Semiconductors, drivers
Projects
Status: Todo
Development

Successfully merging this pull request may close these issues.

6 participants