Chapter 205: DALI Master Controller Implementation

Chapter Objectives

Upon completing this chapter, students will be able to:

  • Design and implement a DALI master controller application on an ESP32.
  • Structure DALI master controller code for clarity and reusability.
  • Implement functions for sending various DALI command types, including direct arc power, standard commands, group commands, and broadcast commands.
  • Understand and apply correct timing for DALI command sequences, including inter-command delays and response windows for query commands.
  • Manage basic DALI network interactions, such as controlling individual devices and groups.
  • Develop strategies for handling DALI communication within a FreeRTOS multitasking environment.
  • Identify and troubleshoot common issues encountered during DALI master controller development.

Introduction

In Chapter 204, we introduced the Digital Addressable Lighting Interface (DALI) as a powerful protocol for controlling lighting systems. We explored its physical layer, frame structures, addressing, and basic commands. Now, we shift our focus to the heart of any DALI system: the DALI Master Controller. The master is the intelligent entity that orchestrates the behavior of all connected DALI control gear, sending commands, managing configurations, and potentially processing feedback.

Building a custom DALI master controller using an ESP32 offers tremendous flexibility. It allows for tailored lighting control logic, seamless integration with other building automation systems, IoT connectivity for remote control and monitoring, and can be a cost-effective solution for specialized applications. While commercial DALI controllers offer rich feature sets, an ESP32-based master empowers you to create deeply customized solutions.

This chapter will guide you through the practical aspects of implementing a DALI master controller. We will discuss the master’s core responsibilities, architectural considerations for your ESP32 application, and provide more advanced practical examples using the ESP32’s RMT peripheral to communicate on the DALI bus. By the end of this chapter, you will be equipped to develop sophisticated DALI control applications.

Theory

Responsibilities of a DALI Master Controller

The DALI master controller is the sole initiator of communication on a standard DALI bus (in a single-master system). Its primary responsibilities include:

  1. Initiating Communication: All DALI forward frames originate from the master. Slaves (control gear) only transmit backward frames in direct response to a query command from the master.
  2. Sending Commands: This is the master’s core function. It includes:
    • Direct Lighting Control: Setting specific light levels (e.g., using Direct Arc Power Control – DAPC), turning lights ON/OFF, recalling predefined MIN/MAX levels.
    • Scene Control: Instructing control gear to go to pre-programmed scene levels.
    • Group Control: Sending commands to logical groups of luminaires.
    • Broadcast Control: Addressing all devices on the DALI line.
  3. Managing Addresses: While full commissioning is often done with dedicated tools, a master may need to:
    • Send commands to specific short addresses.
    • Send commands to group addresses.
    • (Advanced) Participate in or initiate parts of the commissioning process, like assigning short addresses or group memberships (though this is complex and beyond basic master control).
  4. Querying Control Gear: Requesting status information, actual light levels, diagnostic data, etc., from specific devices or groups.
  5. Processing Responses: Listening for and decoding backward frames sent by control gear in response to query commands. This requires the master to manage the DALI bus timing carefully to open a “listening window.”
  6. Managing DALI Bus Timing: Adhering to strict DALI timing parameters is crucial for reliable communication. This includes:
    • Bit Timing: Ensuring correct Manchester encoding at 1200 bps (handled by RMT and proper symbol generation).
    • Inter-Command Delay: Providing sufficient time between consecutive forward frames. The DALI standard specifies minimum settling times. Typically, after a forward frame that might elicit a backward frame, the master must wait at least 7 Te (half-bit times) before the backward frame starts and up to 22 Te for it to complete. Thus, a common practice is to wait at least ~22ms before sending another command if a response was possible. If no response is expected, the master can send another command sooner, after at least two forward frame times (~32ms) from the start of the previous command, or by observing bus idle.
    • Response Window: After sending a query, the master must cease transmitting and prepare to receive the backward frame within a defined window (max 22Te or ~9.17ms for the backward frame itself, plus the settling time).
  7. Error Handling: Detecting and potentially reacting to communication errors, such as no response to a query, or bus collisions (if multiple devices try to respond simultaneously to a broadcast query that expects a response).

Architecting a DALI Master Application on ESP32

When developing a DALI master on the ESP32, a structured approach is recommended:

  1. Modular Design:
    • DALI Protocol Layer: A set of functions or a “component” that handles the low-level DALI communication. This layer would be responsible for:
      • Initializing the RMT peripheral for DALI TX (and potentially RX).
      • Formatting DALI address and data bytes.
      • Generating RMT symbols for Manchester encoding.
      • Transmitting frames via RMT.
      • (If RX is implemented) Receiving and decoding backward frames.
      • Managing DALI timings.
    • Application Logic Layer: This layer contains the specific control strategy (e.g., lighting schedules, sensor-based control, user interface interactions). It calls functions from the DALI protocol layer to interact with the lighting system.
  2. State Management: For more complex interactions, especially involving query-response cycles or commissioning sequences, a state machine within the DALI protocol layer can be beneficial. States might include:
    • IDLE
    • SENDING_COMMAND
    • WAITING_FOR_RESPONSE
    • PROCESSING_RESPONSE
    • BUSY (e.g. during commissioning sequence)
  3. Task-Based Operation: Using FreeRTOS tasks is essential for non-blocking operation.
    • A dedicated DALI communication task can manage sending commands and handling responses, potentially using queues to receive command requests from other application tasks.
    • Other tasks can handle user input, sensor readings, network communication, etc.
  4. Configuration Management: Storing DALI device addresses, group assignments, scene definitions, etc., often using NVS (Non-Volatile Storage) as discussed in Chapter 22.
graph TD
    subgraph "ESP32 Application"
        direction TB
        
        subgraph "Application Logic Layer (High-Level)"
            UI_Task["User Input Task<br>(e.g., Buttons, Serial)"]
            Sensor_Task["Sensor Task<br>(e.g., Light, Occupancy)"]
            Network_Task["Network Task<br>(e.g., MQTT, HTTP)"]
        end

        subgraph "FreeRTOS"
            CmdQueue([DALI Command Queue])
        end

        subgraph "DALI Protocol Layer (Low-Level)"
            DaliTask[DALI Communication Task]
            subgraph "DALI Master Utilities"
                direction LR
                Init["dali_master_init()"]
                Send["dali_master_send_...()"]
                Query["dali_master_query_...()"]
            end
        end

        subgraph "ESP-IDF Drivers"
            RMT["RMT Peripheral Driver"]
            NVS["NVS Driver<br><i>(for configuration)</i>"]
        end
        
        Hardware[(ESP32 Hardware)]
    end

    %% Links
    UI_Task -- "Places Command in Queue" --> CmdQueue
    Sensor_Task -- "Places Command in Queue" --> CmdQueue
    Network_Task -- "Places Command in Queue" --> CmdQueue
    CmdQueue -- "Receives Command" --> DaliTask
    DaliTask -- "Uses Utility Functions" --> Send
    DaliTask -- "Uses Utility Functions" --> Query
    Init -- "Configures" --> RMT
    Send -- "Uses" --> RMT
    Query -- "Uses" --> RMT
    RMT -- "Controls" --> Hardware
    DaliTask -- "Stores/Retrieves Config" --> NVS

    %% Styling
    classDef appLogic fill:#DBEAFE,stroke:#2563EB,color:#1E40AF;
    classDef daliProtocol fill:#EDE9FE,stroke:#5B21B6,color:#5B21B6;
    classDef rtos fill:#FEF3C7,stroke:#D97706,color:#92400E;
    classDef drivers fill:#D1FAE5,stroke:#059669,color:#065F46;
    classDef hardware fill:#FEE2E2,stroke:#DC2626,color:#991B1B;

    class UI_Task,Sensor_Task,Network_Task appLogic;
    class DaliTask,Init,Send,Query daliProtocol;
    class CmdQueue rtos;
    class RMT,NVS drivers;
    class Hardware hardware;

Key DALI Master Operations in Detail

Sending Commands

As covered in Chapter 204, forward frames consist of a start bit, an address byte, a data byte, and two stop bits. The master’s role is to construct these frames and transmit them.

  • Direct Arc Power Control (DAPC):
    • This is the most common way to set a specific light level.
    • The address byte format for DAPC to a short address AAAAAA is 0AAAAAA1 (Y=0, S=1).
    • The address byte format for DAPC to a group address GGGGGG (where only G0-G3 are used for groups 0-15, so GGGGGG = 00GGGG) is 100GGGG1 (Y=1, S=1).
    • The data byte contains the desired arc power level (0-254). 0xFF (255) means MASK (ignore this command, no change to light level).
  • Standard Commands (S=0):
    • The address byte format for a short address AAAAAA is 0AAAAAA0 (Y=0, S=0).
    • The address byte format for a group address GGGGGG is 100GGGG0 (Y=1, S=0).
    • The address byte for broadcast is typically 11111110 (Y=1, AAAAAA=111111, S=0).
    • The data byte contains the opcode for commands like OFF (0x00), RECALL MAX LEVEL (0x05), RECALL MIN LEVEL (0x06), etc.
  • Repeating Commands: For commands like UP (0x01) or DOWN (0x02) to have a continuous dimming effect, the master must send them repeatedly (e.g., every 200ms as per DALI spec for continuous dimming steps) until a STOP DIMMING (0x0A) command or another lighting level command is sent.
Command Type Target Address Byte Format (YAAAAAAS) Data Byte Content Expect Response?
Direct Arc Power (DAPC) Short Address (SA) 0AAAAAA1 Power level (0-254) No
Standard Command Short Address (SA) 0AAAAAA0 Command Opcode (e.g., 0x00 for OFF) No
Standard Command Group Address (GA) 10GGGG00 Command Opcode (e.g., 0x05 for MAX) No
Standard Command Broadcast 11111110 Command Opcode (e.g., 0x00 for OFF) No
Query Command Short Address (SA) 0AAAAAA1 Query Opcode (e.g., 0xA0 for QUERY LEVEL) Yes (Listen for backward frame)

Querying Control Gear and Handling Responses

This is where DALI’s bi-directional nature comes into play.

  1. Sending a Query Command: The master sends a forward frame with a query command in the data byte (e.g., QUERY ACTUAL LEVEL – 0xA0). The address byte’s ‘S’ bit is often ‘1’ for queries (e.g., 0AAAAAA1 for querying short address AAAAAA).
  2. Response Window: After sending the query, the DALI master MUST stop transmitting and listen for a potential backward frame from the addressed control gear.
    • The DALI standard specifies a minimum time (Tsettle_f, ~2.9ms or 7Te) before a slave starts sending a response.
    • The backward frame itself is 11 bits long (~9.17ms).
    • The master should ideally listen for at least Tsettle_f + Tbackward_frame (~12ms) from the end of its forward frame. Some recommendations suggest allowing up to 22ms total from the start of the master’s query for the bus to clear.
  3. Backward Frame: If the addressed device responds, it sends an 11-bit Manchester-encoded frame containing 8 bits of data.
  4. Collision: If a query is sent to a group or broadcast address that could elicit responses from multiple devices simultaneously (e.g., QUERY DEVICE TYPE), a collision will occur, and the backward frame will be corrupted. DALI includes mechanisms for detecting collisions, but handling them (e.g., by systematically querying individual short addresses) is a more advanced commissioning task. Standard DALI queries that return a value (like QUERY ACTUAL LEVEL) should only be sent to individual short addresses to avoid collisions.
flowchart TD
    A["Start: Prepare Query<br><b>(Address Byte, Query Data)</b>"] --> B{Send Forward Frame<br>via RMT TX};
    B --> C{"Stop TX & Start Timer<br><b>(Open ~22ms Response Window)</b>"};
    C --> D{"Bus Activity?<br><i>(Backward Frame Started)</i>"};
    D --"No (Timeout)"--> F[No Response Received<br><b>Proceed to next action</b>];
    D --"Yes (Within Window)"--> E{"Decode Backward Frame<br><b>(Requires RMT RX)</b>"};
    E --> G["Store Response Data<br><b>(e.g., Light Level)</b>"];
    G --> H[End: Query Successful];
    F --> I[End: Query Timeout];

    %% Styling
    classDef startEnd fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef wait fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;
    
    class A,H,I startEnd;
    class B,E,G process;
    class D decision;
    class C wait;

Inter-Command Timing

  • Master-to-Slave then Slave-to-Master: If a forward command expects a backward frame (query), the master must wait for the response window to close (e.g., ~22ms from start of its query) before sending any new forward frame.
  • Master-to-Slave then Master-to-Slave: If a forward command does not expect a backward frame, the master must still wait for a minimum period. The DALI bus must be idle (high) for at least Tidle_f (typically 2Te, ~833µs) before a new forward frame can start. For safety and to ensure the previous command has been processed, waiting longer (e.g., a few milliseconds, or at least two forward frame durations from the start of the previous command which is about 32ms) is common practice.

DALI Interface Circuit Considerations

As detailed in Chapter 204, a DALI interface circuit is essential. For a master controller, this circuit must:

  • Allow the ESP32’s GPIO (via RMT TX) to reliably pull the DALI bus line LOW against the DALI PSU’s pull-up.
  • Allow the ESP32’s GPIO (via RMT RX and appropriate level shifting/isolation like an optocoupler) to sense the DALI bus state (HIGH or LOW).
  • Provide electrical isolation if the ESP32 is powered from a source that is not isolated relative to the DALI bus, especially if the DALI bus or connected lighting is mains-referenced.

Tip: Commercial DALI transceiver ICs can simplify the interface design and often provide better protection and signal integrity compared to simple discrete component solutions, especially if bi-directional communication is critical.

Practical Examples

Building on the RMT TX example from Chapter 204, we’ll now create a more structured DALI master application. We’ll focus on sending various commands and managing basic timing. Full DALI RX is complex; we’ll conceptually address the timing for response windows.

Project Structure and Basic DALI Utilities

Let’s create a utility file for DALI functions.

Project Structure:

Plaintext
esp32_dali_controller/
├── CMakeLists.txt
├── main/
│   ├── dali_controller_main.c
│   ├── dali_master_utils.c
│   ├── dali_master_utils.h
│   └── CMakeLists.txt
└── sdkconfig       (Generated by menuconfig)

main/CMakeLists.txt:

Plaintext
idf_component_register(SRCS "dali_controller_main.c dali_master_utils.c"
                    INCLUDE_DIRS ".")

CMakeLists.txt (Project root): (Same as Chapter 204)

Plaintext
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(esp32_dali_controller)

main/dali_master_utils.h:

C
#ifndef DALI_MASTER_UTILS_H
#define DALI_MASTER_UTILS_H

#include "driver/rmt_tx.h"
#include "driver/gpio.h"

// Configuration (adjust as needed)
#define DALI_TX_GPIO          GPIO_NUM_18
#define RMT_TX_RESOLUTION_HZ  1000000 // 1MHz, 1 tick = 1us
#define DALI_HALF_BIT_US      417     // Te = 1 / (1200 * 2) * 1e6

// Min time to wait after a command before sending another (if no response expected)
#define DALI_INTER_COMMAND_DELAY_MS 20 // A safe delay
// Min time to wait for a response after a query command
#define DALI_RESPONSE_WINDOW_MS     22 // Allows for slave response

typedef struct {
    rmt_channel_handle_t tx_channel;
    rmt_encoder_handle_t copy_encoder;
} dali_master_handle_t;

esp_err_t dali_master_init(dali_master_handle_t *handle);
esp_err_t dali_master_send_raw_frame(dali_master_handle_t *handle, uint8_t address_byte, uint8_t data_byte, bool expect_response);
esp_err_t dali_master_send_dapc(dali_master_handle_t *handle, uint8_t short_address, uint8_t power_level);
esp_err_t dali_master_send_cmd(dali_master_handle_t *handle, uint8_t short_address, uint8_t command);
esp_err_t dali_master_broadcast_cmd(dali_master_handle_t *handle, uint8_t command);
esp_err_t dali_master_send_group_cmd(dali_master_handle_t *handle, uint8_t group_address, uint8_t command);
esp_err_t dali_master_query_actual_level(dali_master_handle_t *handle, uint8_t short_address, uint8_t *level_response); // Conceptual response

void dali_master_deinit(dali_master_handle_t *handle);

#endif // DALI_MASTER_UTILS_H

main/dali_master_utils.c:

C
#include "dali_master_utils.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

static const char *TAG_DALI_UTILS = "DALI_UTILS";

// Max RMT symbols for a DALI forward frame: 1 start + 8 addr + 8 data = 17 bits. Each bit is 2 RMT symbols.
// However, rmt_symbol_word_t represents one transition pair, so 17 symbols needed.
#define DALI_FORWARD_FRAME_RMT_SYMBOLS (1 + 8 + 8)

// Helper from Chapter 204 (slightly adapted for clarity)
static size_t rmt_encode_dali_forward_frame(rmt_symbol_word_t *rmt_items, uint8_t address_byte, uint8_t data_byte) {
    size_t num_symbols = 0;

    // Start bit (logic '1': Low-to-High transition on bus)
    rmt_items[num_symbols++] = (rmt_symbol_word_t){.level0 = 0, .duration0 = DALI_HALF_BIT_US, .level1 = 1, .duration1 = DALI_HALF_BIT_US};

    // Address byte
    for (int i = 7; i >= 0; i--) {
        if ((address_byte >> i) & 0x01) { // Bit is '1'
            rmt_items[num_symbols++] = (rmt_symbol_word_t){.level0 = 0, .duration0 = DALI_HALF_BIT_US, .level1 = 1, .duration1 = DALI_HALF_BIT_US};
        } else { // Bit is '0'
            rmt_items[num_symbols++] = (rmt_symbol_word_t){.level0 = 1, .duration0 = DALI_HALF_BIT_US, .level1 = 0, .duration1 = DALI_HALF_BIT_US};
        }
    }

    // Data byte
    for (int i = 7; i >= 0; i--) {
        if ((data_byte >> i) & 0x01) { // Bit is '1'
            rmt_items[num_symbols++] = (rmt_symbol_word_t){.level0 = 0, .duration0 = DALI_HALF_BIT_US, .level1 = 1, .duration1 = DALI_HALF_BIT_US};
        } else { // Bit is '0'
            rmt_items[num_symbols++] = (rmt_symbol_word_t){.level0 = 1, .duration0 = DALI_HALF_BIT_US, .level1 = 0, .duration1 = DALI_HALF_BIT_US};
        }
    }
    // Stop bits (2 idle bits, bus high) are implicitly handled by RMT returning to idle state.
    // The RMT channel should be configured so its idle output level is HIGH.
    return num_symbols;
}

esp_err_t dali_master_init(dali_master_handle_t *handle) {
    if (!handle) return ESP_ERR_INVALID_ARG;

    ESP_LOGI(TAG_DALI_UTILS, "Initializing DALI Master on GPIO %d", DALI_TX_GPIO);
    rmt_tx_channel_config_t tx_chan_config = {
        .gpio_num = DALI_TX_GPIO,
        .clk_src = RMT_CLK_SRC_DEFAULT,
        .resolution_hz = RMT_TX_RESOLUTION_HZ,
        .mem_block_symbols = 64, 
        .trans_queue_depth = 4,
        .flags.invert_out = false, // Assuming level0=LOW on bus, level1=HIGH on bus. Adjust if your interface inverts.
                                   // DALI bus idle level is HIGH.
        .flags.io_od_mode = false, // Set to true if your interface needs open-drain output
    };
    ESP_RETURN_ON_ERROR(rmt_new_tx_channel(&tx_chan_config, &handle->tx_channel), TAG_DALI_UTILS, "Create RMT TX channel failed");

    rmt_copy_encoder_config_t copy_encoder_config = {};
    ESP_RETURN_ON_ERROR(rmt_new_copy_encoder(&copy_encoder_config, &handle->copy_encoder), TAG_DALI_UTILS, "Create RMT copy encoder failed");

    ESP_RETURN_ON_ERROR(rmt_enable(handle->tx_channel), TAG_DALI_UTILS, "Enable RMT TX channel failed");
    ESP_LOGI(TAG_DALI_UTILS, "DALI Master RMT TX Initialized");
    return ESP_OK;
}

esp_err_t dali_master_send_raw_frame(dali_master_handle_t *handle, uint8_t address_byte, uint8_t data_byte, bool expect_response) {
    if (!handle || !handle->tx_channel || !handle->copy_encoder) return ESP_ERR_INVALID_STATE;

    rmt_symbol_word_t rmt_items[DALI_FORWARD_FRAME_RMT_SYMBOLS];
    size_t num_symbols = rmt_encode_dali_forward_frame(rmt_items, address_byte, data_byte);

    rmt_transmit_config_t transmit_config = {
        .loop_count = 0,
        // .flags.eot_level = 1 // Ensure output is high after transmission (RMT default idle state should handle this if configured)
    };
    
    ESP_LOGD(TAG_DALI_UTILS, "Sending DALI Frame: ADDR=0x%02X, DATA=0x%02X", address_byte, data_byte);
    esp_err_t ret = rmt_transmit(handle->tx_channel, handle->copy_encoder, rmt_items, num_symbols * sizeof(rmt_symbol_word_t), &transmit_config);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG_DALI_UTILS, "RMT transmit failed: %s", esp_err_to_name(ret));
        return ret;
    }

    // Wait for transmission to complete
    ret = rmt_tx_wait_all_done(handle->tx_channel, pdMS_TO_TICKS(100)); // Timeout 100ms
    if (ret != ESP_OK) {
        ESP_LOGE(TAG_DALI_UTILS, "RMT wait all done failed: %s", esp_err_to_name(ret));
        // Potentially disable and re-enable channel or other recovery
        return ret;
    }
    ESP_LOGD(TAG_DALI_UTILS, "DALI Frame Sent. ADDR=0x%02X, DATA=0x%02X", address_byte, data_byte);

    // Post-transmission delay based on whether a response is expected
    if (expect_response) {
        ESP_LOGD(TAG_DALI_UTILS, "Expecting response, delaying for %d ms", DALI_RESPONSE_WINDOW_MS);
        vTaskDelay(pdMS_TO_TICKS(DALI_RESPONSE_WINDOW_MS));
        // This is where RX logic would be active
    } else {
        ESP_LOGD(TAG_DALI_UTILS, "No response expected, short delay %d ms", DALI_INTER_COMMAND_DELAY_MS);
        vTaskDelay(pdMS_TO_TICKS(DALI_INTER_COMMAND_DELAY_MS));
    }
    return ESP_OK;
}

// Send Direct Arc Power Control command to a short address
esp_err_t dali_master_send_dapc(dali_master_handle_t *handle, uint8_t short_address, uint8_t power_level) {
    if (short_address > 63) return ESP_ERR_INVALID_ARG;
    // Address byte for DAPC to short address: 0AAAAAA1
    uint8_t address_byte = (short_address << 1) | 0x01;
    ESP_LOGI(TAG_DALI_UTILS, "DAPC: SA=%d, Level=%d", short_address, power_level);
    return dali_master_send_raw_frame(handle, address_byte, power_level, false);
}

// Send a standard command to a short address
esp_err_t dali_master_send_cmd(dali_master_handle_t *handle, uint8_t short_address, uint8_t command) {
    if (short_address > 63) return ESP_ERR_INVALID_ARG;
    // Address byte for standard command to short address: 0AAAAAA0
    uint8_t address_byte = (short_address << 1) & 0xFE;
     ESP_LOGI(TAG_DALI_UTILS, "CMD: SA=%d, CMD=0x%02X", short_address, command);
    return dali_master_send_raw_frame(handle, address_byte, command, false); // Most standard commands don't elicit immediate response
}

// Send a standard command to a group address
esp_err_t dali_master_send_group_cmd(dali_master_handle_t *handle, uint8_t group_address, uint8_t command) {
    if (group_address > 15) return ESP_ERR_INVALID_ARG;
    // Address byte for command to group address: 10GGGG0
    // GGGG is the 4-bit group address (0-15)
    uint8_t address_byte = 0x80 | (group_address << 1); // Y=1, S=0
    ESP_LOGI(TAG_DALI_UTILS, "CMD: Group=%d, CMD=0x%02X", group_address, command);
    return dali_master_send_raw_frame(handle, address_byte, command, false);
}

// Send a broadcast command (no response expected for most common broadcast commands)
esp_err_t dali_master_broadcast_cmd(dali_master_handle_t *handle, uint8_t command) {
    // Address byte for broadcast command: 11111110
    uint8_t address_byte = 0xFE;
    ESP_LOGI(TAG_DALI_UTILS, "CMD: Broadcast, CMD=0x%02X", command);
    return dali_master_send_raw_frame(handle, address_byte, command, false);
}

// Query actual level from a short address (conceptual response handling)
esp_err_t dali_master_query_actual_level(dali_master_handle_t *handle, uint8_t short_address, uint8_t *level_response) {
    if (short_address > 63) return ESP_ERR_INVALID_ARG;
    // Address byte for query to short address: 0AAAAAA1 (S=1 indicates special/query command)
    uint8_t address_byte = (short_address << 1) | 0x01;
    uint8_t query_command = 0xA0; // QUERY ACTUAL LEVEL

    ESP_LOGI(TAG_DALI_UTILS, "QUERY ACTUAL LEVEL: SA=%d", short_address);
    esp_err_t ret = dali_master_send_raw_frame(handle, address_byte, query_command, true); // Expect response
    
    if (ret == ESP_OK) {
        // ---- CONCEPTUAL RESPONSE HANDLING ----
        // In a real implementation with RMT RX configured:
        // 1. RMT RX would have been started before or during the DALI_RESPONSE_WINDOW_MS delay.
        // 2. After the delay, check if RMT RX received data.
        // 3. Decode the Manchester encoded backward frame.
        // 4. If successful, populate *level_response.
        ESP_LOGI(TAG_DALI_UTILS, "Query sent. Conceptual response window passed.");
        if (level_response) {
            // For this example, we don't have live RX data.
            // Simulate by returning a placeholder or error.
            // *level_response = 0; // Or some indicator of no actual data
            ESP_LOGW(TAG_DALI_UTILS, "Actual RX not implemented in this example. Response not captured.");
        }
        // ---- END CONCEPTUAL ----
    }
    return ret;
}

void dali_master_deinit(dali_master_handle_t *handle) {
    if (handle && handle->tx_channel) {
        rmt_disable(handle->tx_channel);
        rmt_del_channel(handle->tx_channel);
        handle->tx_channel = NULL;
    }
    if (handle && handle->copy_encoder) {
        rmt_del_encoder(handle->copy_encoder);
        handle->copy_encoder = NULL;
    }
    ESP_LOGI(TAG_DALI_UTILS, "DALI Master Deinitialized");
}

Code Snippet 1: DALI Control Task Example

This example demonstrates a FreeRTOS task that uses the utility functions to send various DALI commands.

sequenceDiagram
    participant Main as app_main()
    participant Task as dali_control_task
    participant Utils as dali_master_utils
    participant RMT as ESP32 RMT HW

    Main->>Utils: dali_master_init()
    activate Utils
    Utils->>RMT: Configure RMT TX Channel
    RMT-->>Utils: OK
    deactivate Utils
    Utils-->>Main: dali_handle
    Main->>Task: xTaskCreate(dali_control_task)
    
    loop Control Cycle
        Task->>Utils: dali_master_send_dapc(SA=0, Level=128)
        activate Utils
        Utils->>RMT: Transmit DAPC Frame
        activate RMT
        RMT-->>Utils: TX Done
        deactivate RMT
        Utils-->>Task: OK
        deactivate Utils
        Task->>Task: vTaskDelay(2000)

        Task->>Utils: dali_master_send_group_cmd(GA=0, MAX_LEVEL)
        activate Utils
        Utils->>RMT: Transmit Group Command Frame
        activate RMT
        RMT-->>Utils: TX Done
        deactivate RMT
        Utils-->>Task: OK
        deactivate Utils
        Task->>Task: vTaskDelay(2000)

        Task->>Utils: dali_master_query_actual_level(SA=0)
        activate Utils
        Utils->>RMT: Transmit Query Frame
        activate RMT
        RMT-->>Utils: TX Done
        deactivate RMT
        alt Response Window (~22ms)
            Utils->>Utils: vTaskDelay(DALI_RESPONSE_WINDOW_MS)
            note right of Utils: Conceptual: This is where<br>RMT RX would be active
        end
        Utils-->>Task: OK (No actual data)
        deactivate Utils
        Task->>Task: vTaskDelay(2000)
    end

main/dali_controller_main.c:

C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "dali_master_utils.h" // Our DALI utilities

static const char *TAG_MAIN = "DALI_CONTROLLER_MAIN";
static dali_master_handle_t dali_handle;

// DALI Commands (from DALI Standard IEC 62386-102)
#define DALI_CMD_OFF                    0x00
#define DALI_CMD_RECALL_MAX_LEVEL       0x05
#define DALI_CMD_RECALL_MIN_LEVEL       0x06
#define DALI_CMD_STEP_UP                0x02 // Step up (and start fade if appropriate)
#define DALI_CMD_STEP_DOWN              0x03 // Step down (and start fade if appropriate)
#define DALI_CMD_QUERY_STATUS           0x90 
#define DALI_CMD_QUERY_ACTUAL_LEVEL     0xA0

void dali_control_task(void *pvParameters) {
    ESP_LOGI(TAG_MAIN, "DALI Control Task Started");

    // Example: Control Short Address 0 and Group 0
    uint8_t target_short_address = 0;
    uint8_t target_group_address = 0;
    uint8_t current_power_level = 128; // Start at mid-level

    while (1) {
        ESP_LOGI(TAG_MAIN, "Sending DAPC to SA %d, Level %d", target_short_address, current_power_level);
        if (dali_master_send_dapc(&dali_handle, target_short_address, current_power_level) != ESP_OK) {
            ESP_LOGE(TAG_MAIN, "Failed to send DAPC");
        }
        vTaskDelay(pdMS_TO_TICKS(2000)); // Wait 2 seconds

        current_power_level = (current_power_level == 254) ? 50 : 254; // Toggle between 50 and 254

        ESP_LOGI(TAG_MAIN, "Sending RECALL_MAX_LEVEL to Group %d", target_group_address);
        if (dali_master_send_group_cmd(&dali_handle, target_group_address, DALI_CMD_RECALL_MAX_LEVEL) != ESP_OK) {
            ESP_LOGE(TAG_MAIN, "Failed to send Group RECALL_MAX_LEVEL");
        }
        vTaskDelay(pdMS_TO_TICKS(2000));

        ESP_LOGI(TAG_MAIN, "Sending OFF to Group %d", target_group_address);
        if (dali_master_send_group_cmd(&dali_handle, target_group_address, DALI_CMD_OFF) != ESP_OK) {
            ESP_LOGE(TAG_MAIN, "Failed to send Group OFF");
        }
        vTaskDelay(pdMS_TO_TICKS(2000));

        uint8_t actual_level_response;
        ESP_LOGI(TAG_MAIN, "Querying actual level for SA %d", target_short_address);
        if (dali_master_query_actual_level(&dali_handle, target_short_address, &actual_level_response) == ESP_OK) {
            // Note: actual_level_response will not be populated in this example due to conceptual RX
            ESP_LOGI(TAG_MAIN, "Query for SA %d sent. (Conceptual response: %d)", target_short_address, actual_level_response);
        } else {
            ESP_LOGE(TAG_MAIN, "Failed to send Query Actual Level");
        }
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

void app_main(void) {
    ESP_LOGI(TAG_MAIN, "Application Start");

    esp_err_t ret = dali_master_init(&dali_handle);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG_MAIN, "Failed to initialize DALI Master: %s", esp_err_to_name(ret));
        return;
    }

    ESP_LOGI(TAG_MAIN, "DALI Master Initialized Successfully.");

    // Create the DALI control task
    xTaskCreate(dali_control_task, "dali_control_task", 4096, NULL, 5, NULL);

    // app_main can now exit or do other things. The DALI task will run independently.
    ESP_LOGI(TAG_MAIN, "app_main finished. DALI task running.");
}

Explanation:

  • dali_master_utils.h/.c:
    • Defines a dali_master_handle_t to hold RMT resources.
    • dali_master_init(): Sets up the RMT TX channel and copy encoder. flags.invert_out in rmt_tx_channel_config_t is critical and depends on your specific DALI interface hardware (whether it inverts the ESP32 signal or not). The idle level of DALI is high; the RMT output pin should reflect this when idle.
    • rmt_encode_dali_forward_frame(): Generates RMT symbols for a DALI forward frame (start bit, address byte, data byte). Stop bits are handled by the RMT returning to its idle state (which should be configured to HIGH for DALI).
    • dali_master_send_raw_frame(): The core sending function. It takes address and data bytes, encodes them into RMT symbols, transmits them, and then waits for RMT completion. It also includes a delay based on expect_response for inter-command timing or response windows.
    • Helper functions like dali_master_send_dapc(), dali_master_send_cmd(), etc., simplify sending common DALI commands by constructing the correct address byte format.
    • dali_master_query_actual_level(): Demonstrates sending a query and waiting for the response window. The actual capturing and decoding of the response is marked as conceptual because a full RMT RX implementation is more involved and device-specific.
  • dali_controller_main.c:
    • Initializes the DALI master utilities.
    • Creates dali_control_task which periodically sends a sequence of commands: DAPC to a short address, group commands (RECALL MAX, OFF), and a query command.
    • This demonstrates a basic application loop controlling DALI devices.

Build, Flash, and Observe

  1. Hardware Setup:
    • Connect your ESP32 to the DALI interface circuit (as discussed in Chapter 204 and this chapter).
    • Connect the DALI interface circuit, DALI PSU, and your DALI control gear (e.g., an LED driver with an LED) to the two-wire DALI bus.
    • Ensure the DALI PSU is powered on.
  2. Software (VS Code with ESP-IDF Extension):
    • Open the esp32_dali_controller project.
    • Set your ESP32 target variant.
    • Crucially, verify DALI_TX_GPIO in dali_master_utils.h and flags.invert_out in dali_master_utils.c based on your DALI interface circuit design.
    • Build the project (idf.py build).
    • Flash the project to your ESP32 (idf.py -p (YOUR_PORT) flash).
    • Monitor the output (idf.py -p (YOUR_PORT) monitor).
  3. Observe:
    • The serial monitor should show logs from DALI_UTILS and DALI_CONTROLLER_MAIN indicating commands being sent.
    • The DALI-connected lamp should respond to the commands (e.g., change brightness, turn on/off).
    • Note that dali_master_query_actual_level will log that RX is conceptual; you won’t see an actual level read back without implementing DALI RX.

Variant Notes

The DALI master implementation using the RMT peripheral is largely consistent across ESP32 variants that include RMT.

  • ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2: All these feature the RMT peripheral.
    • RMT Channels: The number of available RMT TX/RX channels varies (e.g., ESP32 has 8 TX, ESP32-S3 has 4 TX, ESP32-C6 has 4 TX). Ensure RMT_CHANNEL_0 (or the channel index you use implicitly with rmt_new_tx_channel) is available and suitable. The new API manages channel allocation more dynamically.
    • GPIO Selection: DALI_TX_GPIO must be a valid GPIO for digital output on your chosen ESP32 variant. Consult the variant’s datasheet.
    • RMT Clock Source: RMT_CLK_SRC_DEFAULT typically uses the APB clock (e.g., 80MHz). The RMT_TX_RESOLUTION_HZ and DALI_HALF_BIT_US define the timing based on this. This is generally consistent.
  • Performance: The CPU load for DALI master operations is minimal, as the RMT peripheral handles the precise signal generation. All listed variants are more than capable.
  • API Consistency: The ESP-IDF v5.x RMT driver API is used, which aims for consistency across supported chips.

The primary consideration for variants is GPIO choice and ensuring the RMT peripheral is available and configured correctly.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Incorrect flags.invert_out DALI bus signal is stuck high/low or the waveform is inverted. Commands fail completely. 1. Use an Oscilloscope: Probe both the ESP32 GPIO pin and the DALI bus line.
2. Verify Logic: A DALI ‘1’ should be a Low-to-High transition on the bus. Ensure your RMT symbols and hardware produce this.
3. Toggle and Test: Change the boolean value of flags.invert_out in the RMT config and re-flash.
Insufficient Inter-Command Delay The first command may work, but subsequent commands are missed or cause erratic behavior on the DALI gear. 1. Check Delays: Ensure you call vTaskDelay() after each command.
2. Use Correct Timing: Wait at least 20ms (DALI_INTER_COMMAND_DELAY_MS) for normal commands and 22ms (DALI_RESPONSE_WINDOW_MS) for queries.
DALI Command Logic Error The wrong light responds, or a specific command type (like DAPC) fails while others (like OFF) work. 1. Review Address Bytes: Double-check the bit-shifting and masking for the YAAAAAAS format. DAPC/Query to SA needs S=1 ((sa_addr << 1) | 1).
2. Log a Lot: Before sending, use ESP_LOGI to print the final hex values of your address and data bytes to confirm they are correct.
RMT mem_block_symbols Too Small rmt_new_tx_channel may succeed, but rmt_transmit fails with an invalid state error. Ensure mem_block_symbols in rmt_tx_channel_config_t is at least 17. Using a safe default like 64 is recommended to avoid this issue.
FreeRTOS Task Stack Overflow The ESP32 crashes and reboots with a “Stack canary” or “Guru Meditation” error when the DALI task is running. The DALI task, with its function calls and logging, needs sufficient stack. Increase the stack size in the xTaskCreate call (e.g., from 2048 to 4096).

Exercises

  1. Scene Controller Implementation:
    • Define two scenes (e.g., “Work Scene,” “Relax Scene”). Each scene should set specific DAPC levels for 2-3 different DALI short addresses.
    • Create functions set_work_scene(dali_master_handle_t *handle) and set_relax_scene(dali_master_handle_t *handle).
    • Modify the dali_control_task to cycle through these scenes.
  2. Group Dimmer with STEP_UP/STEP_DOWN:
    • Implement a function dim_group(dali_master_handle_t *handle, uint8_t group_address, bool direction_up, uint8_t steps).
    • This function should send the DALI_CMD_STEP_UP (0x02) or DALI_CMD_STEP_DOWN (0x03) command to the specified group steps times.
    • Remember DALI requires these commands to be sent repeatedly (e.g., every 100-200ms) for continuous dimming. Add appropriate delays. (Note: True continuous dimming also involves fade time settings in the ballast, but repeated step commands will work.)
  3. Conceptual DALI Device Scanner:
    • Create a task that iterates through short addresses 0 to 63.
    • For each address, send a DALI_CMD_QUERY_STATUS (0x90, S-bit in address usually 1 for queries: (short_address << 1) | 0x01).
    • Use dali_master_send_raw_frame with expect_response = true.
    • Log which addresses are being queried. Although you can’t get the actual response data without RX, this simulates part of a discovery process.
  4. Command Queue with Priority:
    • (Advanced) Modify the dali_control_task to receive DALI command requests from a FreeRTOS queue (xQueueReceive).
    • Define a struct for command requests (e.g., target address, address type, data byte, if response expected).
    • Allow other tasks to send commands to this queue. Consider how you might handle command priorities if multiple tasks can queue commands.

Summary

  • A DALI Master Controller is the central intelligence on a DALI bus, responsible for initiating all communication, sending commands, and managing the bus.
  • Implementing a DALI master on ESP32 involves using the RMT peripheral for precise Manchester-encoded signal generation (TX) and, conceptually, for RX.
  • A structured application with a dedicated DALI protocol layer and an application logic layer improves code organization and reusability.
  • Key master operations include sending DAPC commands, standard commands (ON/OFF, scenes), group commands, broadcast commands, and query commands.
  • Correct DALI timing is critical: inter-command delays and response windows must be respected for reliable communication.
  • The ESP32 RMT API (v5.x) provides the tools needed for DALI TX. While DALI RX is more complex, understanding its timing requirements is important for a master.
  • Careful hardware interface design and correct RMT configuration (especially flags.invert_out) are essential for successful DALI communication.
  • ESP32 variants with the RMT peripheral can all serve as DALI masters, with considerations for GPIO availability and RMT channel count.

Further Reading

  1. IEC 62386 Standard: Particularly Parts 101 (General requirements – System components) and 102 (General requirements – Control gear) and 103 (General requirements – Control devices). Purchase from IEC Webstore.
  2. DALI Alliance (DiiA) Resources: Technical guides and application notes.
  3. ESP-IDF RMT Driver Documentation: For detailed information on RMT TX and RX configuration.
  4. Application Notes on DALI Master Design: Search for “DALI master controller design,” “DALI with microcontrollers.” Many semiconductor manufacturers and lighting component suppliers provide valuable insights.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top