/*###########################################################################
copyright qqqlab.com / github.com/qqqlab
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
----------------------------------------------------------------------------
Changelog:
2020-11-14 Rewrite with sampling instead of pinchange
2020-11-10 Split off hardware specific code into separate class
2020-11-08 Created & tested on ATMega328 @ 8Mhz
###########################################################################*/
//#define DALI_DEBUG
//=================================================================
// LOW LEVEL DRIVER
//=================================================================
#include "qqqDALI.h"
#ifdef DALI_DEBUG
#include "arduino.h"
#endif
//timing
#define BEFORE_CMD_IDLE_MS 13 //require 13ms idle time before sending a cmd()
//busstate
#define IDLE 0
#define RX 1
#define COLLISION_RX 2
#define TX 3
#define COLLISION_TX 4
void Dali::begin(uint8_t (*bus_is_high)(), void (*bus_set_low)(), void (*bus_set_high)())
{
this->bus_is_high = bus_is_high;
this->bus_set_low = bus_set_low;
this->bus_set_high = bus_set_high;
_init();
}
void Dali::_set_busstate_idle() {
bus_set_high();
idlecnt = 0;
busstate = IDLE;
}
void Dali::_init() {
_set_busstate_idle();
rxstate = EMPTY;
txcollision = 0;
}
uint16_t Dali::milli() {
while(ticks==0xFF); //wait for _millis update to finish
return _milli;
}
// timer interrupt service routine, called 9600 times per second
void Dali::timer() {
//get bus sample
uint8_t busishigh = (bus_is_high() ? 1 : 0); //bus_high is 1 on high (non-asserted), 0 on low (asserted)
//millis update
ticks++;
if(ticks==10) {
ticks = 0xff; //signal _millis is updating
_milli++;
ticks = 0;
}
switch(busstate) {
case IDLE:
if(busishigh) {
if(idlecnt != 0xff) idlecnt++;
break;
}
//set busstate = RX
rxpos = 0;
rxbitcnt = 0;
rxidle = 0;
rxstate = RECEIVING;
busstate = RX;
//fall-thru to RX
case RX:
//store sample
rxbyte = (rxbyte << 1) | busishigh;
rxbitcnt++;
if(rxbitcnt == 8) {
rxdata[rxpos] = rxbyte;
rxpos++;
if(rxpos > DALI_RX_BUF_SIZE - 1) rxpos = DALI_RX_BUF_SIZE - 1;
rxbitcnt = 0;
}
//check for reception of 2 stop bits
if(busishigh) {
rxidle++;
if(rxidle >= 16) {
rxdata[rxpos] = 0xFF;
rxpos++;
rxstate = COMPLETED;
_set_busstate_idle();
break;
}
}else{
rxidle = 0;
}
break;
case TX:
if(txhbcnt >= txhblen) {
//all bits transmitted, go back to IDLE
_set_busstate_idle();
}else{
//check for collisions (transmitting high but bus is low)
if( (
txcollisionhandling == DALI_TX_COLLISSION_ON //handle all
|| (txcollisionhandling == DALI_TX_COLLISSION_AUTO && txhblen != 2+8+4) //handle only if not transmitting 8 bits (2+8+4 half bits)
) && (txhigh && !busishigh) //transmitting high, but bus is low
&& (txspcnt==1 || txspcnt==2) ) // in middle of transmitting low period
{
if(txcollision != 0xFF) txcollision++;
txspcnt = 0;
busstate = COLLISION_TX;
return;
}
//send data bits (MSB first) to bus every 4th sample time
if(txspcnt == 0) {
//send bit
uint8_t pos = txhbcnt >> 3;
uint8_t bitmask = 1 << (7 - (txhbcnt & 0x7));
if( txhbdata[pos] & bitmask ) {
bus_set_low();
txhigh = 0;
}else{
bus_set_high();
txhigh = 1;
}
//update half bit counter
txhbcnt++;
//next transmit in 4 sample times
txspcnt = 4;
}
txspcnt--;
}
break;
case COLLISION_TX:
//keep bus low for 16 samples = 4 TE
bus_set_low();
txspcnt++;
if(txspcnt >= 16) _set_busstate_idle();
break;
}
}
//push 2 half bits into the half bit transmit buffer, MSB first, 0x0=stop, 0x1= bit value 0, 0x2= bit value 1/start
void Dali::_tx_push_2hb(uint8_t hb) {
uint8_t pos = txhblen>>3;
uint8_t shift = 6 - (txhblen & 0x7);
txhbdata[pos] |= hb << shift;
txhblen += 2;
}
//non-blocking transmit
//transmit if bus is IDLE, without checking hold off times, sends start+stop bits
uint8_t Dali::tx(uint8_t *data, uint8_t bitlen) {
if(bitlen > 32) return DALI_RESULT_FRAME_TOO_LONG;
if(busstate != IDLE) return DALI_RESULT_BUS_NOT_IDLE;
//clear data
for(uint8_t i=0; i<9; i++) txhbdata[i]=0;
//push data in transmit buffer
txhblen = 0;
_tx_push_2hb(0x2); //start bit
//data bits MSB first
for(uint8_t i=0; i>3;
uint8_t mask = 1 << (7 - (i & 0x7));
_tx_push_2hb( data[pos] & mask ? 0x2 : 0x1 );
}
_tx_push_2hb(0x0); //stop bit
_tx_push_2hb(0x0); //stop bit
//setup tx vars
txhbcnt = 0;
txspcnt = 0;
txcollision = 0;
rxstate = EMPTY;
busstate = TX;
return DALI_OK;
}
uint8_t Dali::tx_state() {
if(txcollision) {
txcollision = 0;
return DALI_RESULT_COLLISION;
}
if(busstate == TX) return DALI_RESULT_TRANSMITTING;
return DALI_OK;
}
//-------------------------------------------------------------------
//manchester decode
/*
Prefectly matched transmitter and sampling: 8 samples per bit
---------+ +---+ +-------+ +-------+ +------------------------
| | | | | | | |
+---+ +---+ +-------+ +---+
sample-> 012345670123456701234567012345670123456701234567012345670
sync-> ^ ^ ^ ^ ^ ^ ^ ^
decode-> start 1 1 0 1 0 stop stop
slow transmitter: 9 samples per bit
---------+ +----+ +--------+ +--------+ +------------------------
| | | | | | | |
+---+ +----+ +--------+ +---+
sample-> 0123456780123456780123456780123456780123456780123456780123456780
sync-> ^ ^ ^ ^ ^ ^ ^ ^
decode-> start 1 1 0 1 0 stop stop
*/
//compute weight for a 8 bit sample i
uint8_t Dali::_man_weight(uint8_t i) {
int8_t w = 0;
w += ((i>>7) & 1) ? 1 : -1;
w += ((i>>6) & 1) ? 2 : -2; //put more weight in middle
w += ((i>>5) & 1) ? 2 : -2; //put more weight in middle
w += ((i>>4) & 1) ? 1 : -1;
w -= ((i>>3) & 1) ? 1 : -1;
w -= ((i>>2) & 1) ? 2 : -2; //put more weight in middle
w -= ((i>>1) & 1) ? 2 : -2; //put more weight in middle
w -= ((i>>0) & 1) ? 1 : -1;
//w at this point:
//w = -12 perfect manchester encoded value 1
//...
//w = -2 very weak value 1
//w = 0 unknown (all samples high or low)
//...
//w = 12 perfect manchester encoded value 0
w *= 2;
if(w<0) w = -w + 1;
return w;
}
//call with bitpos <= DALI_RX_BUF_SIZE*8-8;
uint8_t Dali::_man_sample(uint8_t *edata, uint16_t bitpos, uint8_t *stop_coll) {
uint8_t pos = bitpos>>3;
uint8_t shift = bitpos & 0x7;
uint8_t sample = (edata[pos] << shift) | (edata[pos+1] >> (8-shift));
//stop bit: received high (non-asserted) bus for last 8 samples
if(sample == 0xFF) *stop_coll = 1;
//collision: received low (asserted) bus for last 8 samples
if(sample == 0x00) *stop_coll = 2;
return sample;
}
//decode 8 times oversampled encoded data
//returns bitlen of decoded data, or 0 on collision
uint8_t Dali::_man_decode(uint8_t *edata, uint8_t ebitlen, uint8_t *ddata) {
uint8_t dbitlen = 0;
uint16_t ebitpos = 1;
while(ebitpos+1 0) { //ignore start bit
uint8_t bytepos = (dbitlen - 1) >> 3;
uint8_t bitpos = (dbitlen - 1) & 0x7;
if(bitpos == 0) ddata[bytepos] = 0; //empty data before storing first bit
ddata[bytepos] = (ddata[bytepos] << 1) | (weightmax & 1); //get databit from bit0 of weight
}
dbitlen++;
ebitpos += pmax; //jump to next mancheter bit, skipping over number of samples with max weight
}
if(dbitlen>1) dbitlen--;
return dbitlen;
}
//non-blocking receive,
//returns 0 empty, 1 if busy receiving, 2 decode error, >2 number of bits received
uint8_t Dali::rx(uint8_t *ddata) {
switch(rxstate) {
case EMPTY: return 0;
case RECEIVING: return 1;
case COMPLETED:
rxstate = EMPTY;
uint8_t dlen = _man_decode(rxdata,rxpos*8,ddata);
#ifdef DALI_DEBUG
if(dlen!=8){
//print received samples
Serial.print("RX: len=");
Serial.print(rxpos*8);
Serial.print(" ");
for(uint8_t i=0;i>=1) {
if(rxdata[i]&m) Serial.print("1"); else Serial.print("0");
}
Serial.print(" ");
}
//print decoded data
Serial.print("decoded: len=");
Serial.print(dlen);
Serial.print(" ");
for(uint8_t i=0;i>3] & (1 << (7 - (i & 0x7))) ) Serial.print("1"); else Serial.print("0");
}
Serial.println();
}
#endif
if(dlen<3) return 2;
return dlen;
}
return 0; //should not get here
}
//=================================================================
// HIGH LEVEL FUNCTIONS
//=================================================================
//blocking send - wait until successful send or timeout
uint8_t Dali::tx_wait(uint8_t* data, uint8_t bitlen, uint16_t timeout_ms) {
if(bitlen>32) return DALI_RESULT_DATA_TOO_LONG;
uint16_t start_ms = milli();
while(1) {
//wait for 10ms idle
while(idlecnt < BEFORE_CMD_IDLE_MS){
//Serial.print('w');
if(milli() - start_ms > timeout_ms) return DALI_RESULT_TIMEOUT;
}
//try transmit
while(tx(data,bitlen) != DALI_OK){
//Serial.print('w');
if(milli() - start_ms > timeout_ms) return DALI_RESULT_TIMEOUT;
}
//wait for completion
uint8_t rv;
while(1) {
rv = tx_state();
if(rv != DALI_RESULT_TRANSMITTING) break;
if(milli() - start_ms > timeout_ms) return DALI_RESULT_TIMEOUT;
}
//exit if transmit was ok
if(rv == DALI_OK) return DALI_OK;
//not ok (for example collision) - retry until timeout
}
return DALI_RESULT_TIMEOUT;
}
//blocking transmit 2 byte command, receive 1 byte reply (if a reply was sent)
//returns >=0 with reply byte
//returns <0 with negative result code
int16_t Dali::tx_wait_rx(uint8_t cmd0, uint8_t cmd1, uint16_t timeout_ms) {
#ifdef DALI_DEBUG
Serial.print("TX");
Serial.print(cmd0>>4,HEX);
Serial.print(cmd0&0xF,HEX);
Serial.print(cmd1>>4,HEX);
Serial.print(cmd1&0xF,HEX);
Serial.print(" ");
#endif
uint8_t data[4];
data[0] = cmd0;
data[1] = cmd1;
int16_t rv = tx_wait(data, 16, timeout_ms);
if(rv) return -rv;;
//wait up to 10 ms for start of reply, additional 15ms for receive to complete
uint16_t rx_start_ms = milli();
uint16_t rx_timeout_ms = 10;
while(1) {
rv = rx(data);
switch( rv ) {
case 0: break; //nothing received yet, wait
case 1: rx_timeout_ms = 25; break; //extend timeout, wait for RX completion
case 2: return -DALI_RESULT_COLLISION; //report collision
default:
if(rv==8)
return data[0];
else
return -DALI_RESULT_INVALID_REPLY;
}
if(milli() - rx_start_ms > rx_timeout_ms) return -DALI_RESULT_NO_REPLY;
}
return -DALI_RESULT_NO_REPLY; //should not get here
}
//check YAAAAAA: 0000 0000 to 0011 1111 adr, 0100 0000 to 0100 1111 group, x111 1111 broadcast
uint8_t Dali::_check_yaaaaaa(uint8_t yaaaaaa) {
return (yaaaaaa<=0b01001111 || yaaaaaa==0b01111111 || yaaaaaa==0b11111111);
}
void Dali::set_level(uint8_t level, uint8_t adr) {
if(_check_yaaaaaa(adr)) tx_wait_rx(adr<<1,level);
}
int16_t Dali::cmd(uint16_t cmd, uint8_t arg) {
//Serial.print("dali_cmd[");Serial.print(cmd,HEX);Serial.print(",");Serial.print(arg,HEX);Serial.print(")");
uint8_t cmd0,cmd1;
if(cmd & 0x0100) {
//special commands: MUST NOT have YAAAAAAX pattern for cmd
//Serial.print(" SPC");
if(!_check_yaaaaaa(cmd>>1)) {
cmd0 = cmd;
cmd1 = arg;
}else{
return DALI_RESULT_INVALID_CMD;
}
}else{
//regular commands: MUST have YAAAAAA pattern for arg
//Serial.print(" REG");
if(_check_yaaaaaa(arg)) {
cmd0 = arg<<1|1;
cmd1 = cmd;
}else{
return DALI_RESULT_INVALID_CMD;
}
}
if(cmd & 0x0200) {
//Serial.print(" REPEAT");
tx_wait_rx(cmd0, cmd1);
}
int16_t rv = tx_wait_rx(cmd0, cmd1);
//Serial.print(" rv=");Serial.println(rv);
return rv;
}
uint8_t Dali::set_operating_mode(uint8_t v, uint8_t adr) {
return _set_value(DALI_SET_OPERATING_MODE, DALI_QUERY_OPERATING_MODE, v, adr);
}
uint8_t Dali::set_max_level(uint8_t v, uint8_t adr) {
return _set_value(DALI_SET_MAX_LEVEL, DALI_QUERY_MAX_LEVEL, v, adr);
}
uint8_t Dali::set_min_level(uint8_t v, uint8_t adr) {
return _set_value(DALI_SET_MIN_LEVEL, DALI_QUERY_MIN_LEVEL, v, adr);
}
uint8_t Dali::set_system_failure_level(uint8_t v, uint8_t adr) {
return _set_value(DALI_SET_SYSTEM_FAILURE_LEVEL, DALI_QUERY_SYSTEM_FAILURE_LEVEL, v, adr);
}
uint8_t Dali::set_power_on_level(uint8_t v, uint8_t adr) {
return _set_value(DALI_SET_POWER_ON_LEVEL, DALI_QUERY_POWER_ON_LEVEL, v, adr);
}
//set a parameter value, returns 0 on success
uint8_t Dali::_set_value(uint16_t setcmd, uint16_t getcmd, uint8_t v, uint8_t adr) {
int16_t current_v = cmd(getcmd,adr); //get current parameter value
if(current_v == v) return 0;
cmd(DALI_DATA_TRANSFER_REGISTER0,v); //store value in DTR
int16_t dtr = cmd(DALI_QUERY_CONTENT_DTR0,adr); //get DTR value
if(dtr != v) return 1;
cmd(setcmd,adr); //set parameter value = DTR
current_v = cmd(getcmd,adr); //get current parameter value
if(current_v != v) return 2;
return 0;
}
//======================================================================
// Commissioning short addresses
//======================================================================
//Sets the slave Note 1 to the INITIALISE status for15 minutes.
//Commands 259 to 270 are enabled only for a slave in this
//status.
//set search address
void Dali::set_searchaddr(uint32_t adr) {
cmd(DALI_SEARCHADDRH,adr>>16);
cmd(DALI_SEARCHADDRM,adr>>8);
cmd(DALI_SEARCHADDRL,adr);
}
//set search address, but set only changed bytes (takes less time)
void Dali::set_searchaddr_diff(uint32_t adr_new,uint32_t adr_current) {
if( (uint8_t)(adr_new>>16) != (uint8_t)(adr_current>>16) ) cmd(DALI_SEARCHADDRH,adr_new>>16);
if( (uint8_t)(adr_new>>8) != (uint8_t)(adr_current>>8) ) cmd(DALI_SEARCHADDRM,adr_new>>8);
if( (uint8_t)(adr_new) != (uint8_t)(adr_current) ) cmd(DALI_SEARCHADDRL,adr_new);
}
//Is the random address smaller or equal to the search address?
//as more than one device can reply, the reply gets garbled
uint8_t Dali::compare() {
uint8_t retry = 2;
while(retry>0) {
//compare is true if we received any activity on the bus as reply.
//sometimes the reply is not registered... so only accept retry times 'no reply' as a real false compare
int16_t rv = cmd(DALI_COMPARE,0x00);
if(rv == -DALI_RESULT_COLLISION) return 1;
if(rv == -DALI_RESULT_INVALID_REPLY) return 1;
if(rv == 0xFF) return 1;
retry--;
}
return 0;
}
//The slave shall store the received 6-bit address (AAAAAA) as a short address if it is selected.
void Dali::program_short_address(uint8_t shortadr) {
cmd(DALI_PROGRAM_SHORT_ADDRESS, (shortadr << 1) | 0x01);
}
//What is the short address of the slave being selected?
uint8_t Dali::query_short_address() {
return cmd(DALI_QUERY_SHORT_ADDRESS, 0x00) >> 1;
}
//find addr with binary search
uint32_t Dali::find_addr() {
uint32_t adr = 0x800000;
uint32_t addsub = 0x400000;
uint32_t adr_last = adr;
set_searchaddr(adr);
while(addsub) {
set_searchaddr_diff(adr,adr_last);
adr_last = adr;
//Serial.print("cmpadr=");
//Serial.print(adr,HEX);
uint8_t cmp = compare(); //returns 1 if searchadr > adr
//Serial.print("cmp ");
//Serial.print(adr,HEX);
//Serial.print(" = ");
//Serial.println(cmp);
if(cmp) adr-=addsub; else adr+=addsub;
addsub >>= 1;
}
set_searchaddr_diff(adr,adr_last);
adr_last = adr;
if(!compare()) {
adr++;
set_searchaddr_diff(adr,adr_last);
}
return adr;
}
//init_arg=11111111 : all without short address
//init_arg=00000000 : all
//init_arg=0AAAAAA1 : only for this shortadr
//returns number of new short addresses assigned
uint8_t Dali::commission(uint8_t init_arg) {
uint8_t cnt = 0;
uint8_t arr[64];
uint8_t sa;
for(sa=0; sa<64; sa++) arr[sa]=0;
//start commissioning
cmd(DALI_INITIALISE,init_arg);
cmd(DALI_RANDOMISE,0x00);
//need 100ms pause after RANDOMISE, scan takes care of this...
//find used short addresses (run always, seems to work better than without...)
for(sa = 0; sa<64; sa++) {
int16_t rv = cmd(DALI_QUERY_STATUS,sa);
if(rv>=0) {
if(init_arg!=0b00000000) arr[sa]=1; //remove address from list if not in "all" mode
}
}
//find random addresses and assign unused short addresses
while(1) {
uint32_t adr = find_addr();
if(adr>0xffffff) break; //no more random addresses found -> exit
//find first unused short address
for(sa=0; sa<64; sa++) {
if(arr[sa]==0) break;
}
if(sa>=64) break; //all 64 short addresses assigned -> exit
//mark short address as used
arr[sa] = 1;
cnt++;
//assign short address
program_short_address(sa);
//Serial.println(query_short_address()); //TODO check read adr, handle if not the same...
//remove the device from the search
cmd(DALI_WITHDRAW,0x00);
}
//terminate the DALI_INITIALISE command
cmd(DALI_TERMINATE,0x00);
return cnt;
}
//======================================================================
// Memory
//======================================================================
uint8_t Dali::set_dtr0(uint8_t value, uint8_t adr) {
uint8_t retry=3;
while(retry) {
cmd(DALI_DATA_TRANSFER_REGISTER0,value); //store value in DTR
int16_t dtr = cmd(DALI_QUERY_CONTENT_DTR0,adr); //get DTR value
if(dtr == value) return 0;
retry--;
}
return 1;
}
uint8_t Dali::set_dtr1(uint8_t value, uint8_t adr) {
uint8_t retry=3;
while(retry) {
cmd(DALI_DATA_TRANSFER_REGISTER1,value); //store value in DTR
int16_t dtr = cmd(DALI_QUERY_CONTENT_DTR1,adr); //get DTR value
if(dtr == value) return 0;
retry--;
}
return 1;
}
uint8_t Dali::set_dtr2(uint8_t value, uint8_t adr) {
uint8_t retry=3;
while(retry) {
cmd(DALI_DATA_TRANSFER_REGISTER2,value); //store value in DTR
int16_t dtr = cmd(DALI_QUERY_CONTENT_DTR2,adr); //get DTR value
if(dtr == value) return 0;
retry--;
}
return 1;
}
uint8_t Dali::read_memory_bank(uint8_t bank, uint8_t adr) {
uint16_t rv;
if(set_dtr0(0, adr)) return 1;
if(set_dtr1(bank, adr)) return 2;
//uint8_t data[255];
uint16_t len = cmd(DALI_READ_MEMORY_LOCATION, adr);
#ifdef DALI_DEBUG
Serial.print("memlen=");
Serial.println(len);
for(uint8_t i=0;i=0) {
//data[i] = mem;
Serial.print(i,HEX);
Serial.print(":");
Serial.print(mem);
Serial.print(" 0x");
Serial.print(mem,HEX);
Serial.print(" ");
if(mem>=32 && mem <127) Serial.print((char)mem);
Serial.println();
}else if(mem!=-DALI_RESULT_NO_REPLY) {
Serial.print(i,HEX);
Serial.print(":err=");
Serial.println(mem);
}
//delay(10);
}
#endif
uint16_t dtr0 = cmd(DALI_QUERY_CONTENT_DTR0,adr); //get DTR value
if(dtr0 != 255) return 4;
}
//======================================================================