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:
- 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.
- 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.
- 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).
- Querying Control Gear: Requesting status information, actual light levels, diagnostic data, etc., from specific devices or groups.
- 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.”
- 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).
- 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:
- 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.
- DALI Protocol Layer: A set of functions or a “component” that handles the low-level DALI communication. This layer would be responsible for:
- 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)
- 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.
- 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
is0AAAAAA1
(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) is100GGGG1
(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
is0AAAAAA0
(Y=0, S=0). - The address byte format for a group address
GGGGGG
is100GGGG0
(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.
- The address byte format for a short address
- Repeating Commands: For commands like
UP
(0x01) orDOWN
(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 aSTOP 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.
- 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 addressAAAAAA
). - 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.
- Backward Frame: If the addressed device responds, it sends an 11-bit Manchester-encoded frame containing 8 bits of data.
- 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 (likeQUERY 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:
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
:
idf_component_register(SRCS "dali_controller_main.c dali_master_utils.c"
INCLUDE_DIRS ".")
CMakeLists.txt
(Project root): (Same as Chapter 204)
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(esp32_dali_controller)
main/dali_master_utils.h
:
#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
:
#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(©_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
:
#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
inrmt_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 onexpect_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.
- Defines a
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
- 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.
- Software (VS Code with ESP-IDF Extension):
- Open the
esp32_dali_controller
project. - Set your ESP32 target variant.
- Crucially, verify
DALI_TX_GPIO
indali_master_utils.h
andflags.invert_out
indali_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
).
- Open the
- Observe:
- The serial monitor should show logs from
DALI_UTILS
andDALI_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.
- The serial monitor should show logs from
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 withrmt_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). TheRMT_TX_RESOLUTION_HZ
andDALI_HALF_BIT_US
define the timing based on this. This is generally consistent.
- 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
- 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
- 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)
andset_relax_scene(dali_master_handle_t *handle)
. - Modify the
dali_control_task
to cycle through these scenes.
- 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) orDALI_CMD_STEP_DOWN
(0x03) command to the specified groupsteps
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.)
- Implement a function
- 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
withexpect_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.
- 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.
- (Advanced) Modify the
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
- 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.
- DALI Alliance (DiiA) Resources: Technical guides and application notes.
- Website: https://www.dali-alliance.org/
- ESP-IDF RMT Driver Documentation: For detailed information on RMT TX and RX configuration.
- Refer to the documentation for your specific ESP-IDF version and ESP32 variant (e.g., https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/rmt.html).
- 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.