first commit
This commit is contained in:
124
ESP32_XY6020L_BUS/ESP32_XY6020L_BUS.ino
Normal file
124
ESP32_XY6020L_BUS/ESP32_XY6020L_BUS.ino
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* @file dcdcmbus.ino
|
||||
* @brief usage of XY6020L DCDC for simple max power point tracking of
|
||||
* a solar module driving a electrolytic cell
|
||||
* Cells voltage start from ~3 V and current will rise up to ~3A at 4 V.
|
||||
* At ~19 V the 20V pannel has its maximum power.
|
||||
* Simple I-controler increases the cell voltage till the power consumption
|
||||
* from the solar panel drives its voltage below 19 V.
|
||||
Hardware: Arduino Pro Micro Clone from China
|
||||
*
|
||||
* @author Jens Gleissberg
|
||||
* @date 2024
|
||||
* @license GNU Lesser General Public License v3.0 or later
|
||||
*/
|
||||
|
||||
#include "xy6020l.h"
|
||||
#define PIN_SOFTWARE_SERIAL_RX2 25
|
||||
#define PIN_SOFTWARE_SERIAL_TX2 26
|
||||
// dcdc's MBus is connected to Serial2 of Arduino
|
||||
xy6020l xy(Serial2, 1);
|
||||
|
||||
|
||||
long ts;
|
||||
bool boActive;
|
||||
// solar panel voltage
|
||||
word vIn;
|
||||
// cell voltage setpoint
|
||||
word vOut, vOutMin, vOutMax;
|
||||
|
||||
void setup() {
|
||||
// debug messages via USB
|
||||
Serial.begin( 115200);
|
||||
// MBus serial
|
||||
// Serial2.begin( 115200);
|
||||
Serial2.begin(115200, SERIAL_8N1, PIN_SOFTWARE_SERIAL_RX2, PIN_SOFTWARE_SERIAL_TX2); // Rx = 4, Tx = 5 will work for ESP32, S2, S3 and C3
|
||||
|
||||
ts = millis();
|
||||
|
||||
vOutMin= 300; // start with 3,0 V
|
||||
vOutMax= 450; // max voltage
|
||||
vIn = 1700;
|
||||
boActive = false;
|
||||
|
||||
xy.setPreset(01);
|
||||
while(!xy.TxBufEmpty())
|
||||
xy.task();
|
||||
|
||||
xy.setOutput(false);
|
||||
while(!xy.TxBufEmpty())
|
||||
xy.task();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
int vDiff;
|
||||
char tmpBuf[30]; // text buffer for serial messages
|
||||
|
||||
xy.task();
|
||||
if(xy.HRegUpdated())
|
||||
{
|
||||
|
||||
vIn = xy.getInV();
|
||||
// 15 V -> undervoltage of solar panel
|
||||
if(vIn < 1300 )
|
||||
{
|
||||
// output off
|
||||
xy.setOutput(false);
|
||||
boActive = false;
|
||||
// reset cell voltage to its min value
|
||||
vOut= vOutMin;
|
||||
}
|
||||
else
|
||||
{
|
||||
// use a hyseressis for switch on to avoid to short on pulses
|
||||
if(vIn > 2100 )
|
||||
{
|
||||
boActive= true;
|
||||
// output on
|
||||
if(!xy.getOutputOn() )
|
||||
xy.setOutput(true);
|
||||
}
|
||||
}
|
||||
|
||||
if(xy.getLockOn() )
|
||||
xy.setLockOn(false);
|
||||
if(xy.getProtect()>0)
|
||||
{
|
||||
xy.setProtect(0);
|
||||
}
|
||||
|
||||
if(boActive)
|
||||
{
|
||||
// target:
|
||||
// the input voltage must be kept at 18..19 V
|
||||
// @todo I part depents on voltage difference and slope
|
||||
vDiff= (int)vIn - (int)1900;
|
||||
// dead band -> no change
|
||||
if(vDiff < 70 && vDiff > -100)
|
||||
vDiff=0;
|
||||
else
|
||||
{
|
||||
if(vDiff > 200)
|
||||
vDiff=200;
|
||||
if(vDiff < -200)
|
||||
vDiff=-200;
|
||||
|
||||
if(vDiff > 0)
|
||||
vOut+= vDiff /10;
|
||||
else
|
||||
vOut+= vDiff ;
|
||||
|
||||
if(vOut > vOutMax)
|
||||
vOut = vOutMax;
|
||||
if(vOut < vOutMin)
|
||||
vOut = vOutMin;
|
||||
xy.setCV( vOut );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// print control results: - switched to save runtime -
|
||||
//sprintf( tmpBuf, "%d: %d Out=%d\n", vIn, vOut, boActive);
|
||||
//Serial.print(tmpBuf);
|
||||
}
|
||||
}
|
||||
617
ESP32_XY6020L_BUS/xy6020l.cpp
Normal file
617
ESP32_XY6020L_BUS/xy6020l.cpp
Normal file
@@ -0,0 +1,617 @@
|
||||
// #include <type_traits>
|
||||
/**
|
||||
* @file xy6020l.cpp
|
||||
* @brief UART control access to XY6020L DCDC
|
||||
*
|
||||
* This library provides an embedded and simplified ModBus implementation.
|
||||
* The data type of interfaces to physical values are in decimal, scaled almost in 0.01.
|
||||
*
|
||||
* Tested only on a Arduino Pro Micro clone from China
|
||||
*
|
||||
* @author Jens Gleissberg
|
||||
* @date 2024
|
||||
* @license GNU Lesser General Public License v3.0 or later
|
||||
*/
|
||||
|
||||
#include "Arduino.h"
|
||||
#include "xy6020l.h"
|
||||
|
||||
/**
|
||||
* @brief: debug message level on serial
|
||||
* > 1 - timeout comment
|
||||
* > 2 - HRegs contents
|
||||
* - rx bytes and HReg of written HReg
|
||||
* > 9 - rx bytes (basic RS232 communication)
|
||||
*/
|
||||
#define __debug__ 0
|
||||
|
||||
/** @brief period for reading content of all hold regs, in msec */
|
||||
// #define PERIOD_READ_ALL_HREGS 100
|
||||
/** @brief answer timeout of tx message: 4 x 10 ms */
|
||||
#define PERIOD_TIMEOUT_RESPONSE 4
|
||||
|
||||
TxRingBuffer::TxRingBuffer()
|
||||
{
|
||||
mpIn = &(mTxBuf[0]);
|
||||
mpOut= &(mTxBuf[0]);
|
||||
mIn=0;
|
||||
}
|
||||
|
||||
bool TxRingBuffer::AddTx(txRingEle* pTxEle)
|
||||
{
|
||||
bool retVal=true;
|
||||
char tmpBuf[20];
|
||||
|
||||
if( IsFull())
|
||||
retVal= false;
|
||||
else
|
||||
{
|
||||
mpIn->mHregIdx = pTxEle->mHregIdx;
|
||||
mpIn->mValue = pTxEle->mValue;
|
||||
mIn++;
|
||||
mpIn++;
|
||||
// wrap around
|
||||
if(mpIn == &(mTxBuf[TX_RING_BUFFER_SIZE]))
|
||||
mpIn = &(mTxBuf[0]);
|
||||
|
||||
#if __debug__ > 2
|
||||
sprintf( tmpBuf, "\nRing In: %d\n", mIn);
|
||||
Serial.print(tmpBuf);
|
||||
#endif
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
bool TxRingBuffer::AddTx(byte hRegIdx, word value)
|
||||
{
|
||||
txRingEle TxEle;
|
||||
TxEle.mHregIdx= hRegIdx;
|
||||
TxEle.mValue = value;
|
||||
return AddTx( &TxEle );
|
||||
}
|
||||
|
||||
bool TxRingBuffer::GetTx(txRingEle& TxEle)
|
||||
{
|
||||
bool retVal=true;
|
||||
if( IsEmpty() )
|
||||
retVal= false;
|
||||
else
|
||||
{
|
||||
TxEle.mHregIdx = mpOut->mHregIdx;
|
||||
TxEle.mValue = mpOut->mValue;
|
||||
mIn--;
|
||||
mpOut++;
|
||||
// wrap around
|
||||
if(mpOut == &(mTxBuf[TX_RING_BUFFER_SIZE]))
|
||||
mpOut = &(mTxBuf[0]);
|
||||
};
|
||||
return retVal;
|
||||
}
|
||||
|
||||
|
||||
xy6020l::xy6020l(Stream& serial, byte adr, byte txPeriod, byte options )
|
||||
{
|
||||
mSerial= &serial;
|
||||
mAdr=adr;
|
||||
mOptions = options;
|
||||
mMemory = 255;
|
||||
mMemoryState= Send;
|
||||
mRxBufIdx = 0;
|
||||
mRxFrameCnt=0;
|
||||
mRxFrameCntLast=0;
|
||||
mTxBufIdx = 0;
|
||||
mTxPeriod = txPeriod;
|
||||
mResponse = None;
|
||||
mTs = millis();
|
||||
mTO = mTs;
|
||||
mTLastTx = mTs;
|
||||
};
|
||||
|
||||
bool xy6020l::ReadAllHRegs(void)
|
||||
{
|
||||
bool retValue=false;
|
||||
if( mTxBufIdx == 0 )
|
||||
{
|
||||
SendReadHReg(0, NB_HREGS-1 );
|
||||
retValue= true;
|
||||
}
|
||||
return retValue;
|
||||
}
|
||||
|
||||
bool xy6020l::HRegUpdated(void)
|
||||
{
|
||||
bool retValue=false;
|
||||
if( mRxFrameCnt != mRxFrameCntLast)
|
||||
{
|
||||
mRxFrameCntLast= mRxFrameCnt;
|
||||
retValue=true;
|
||||
}
|
||||
return retValue;
|
||||
};
|
||||
|
||||
/** @brief: read register reply, must be decoded to Hregister content */
|
||||
bool xy6020l::RxDecode03( byte cnt)
|
||||
{
|
||||
bool RxOk= true;
|
||||
|
||||
char tmpBuf[30];
|
||||
|
||||
mRxThis = mRxBuf[0]==mAdr? true:false;
|
||||
mRxSize = mRxBuf[2];
|
||||
if (mRxSize > NB_HREGS*2+5)
|
||||
{
|
||||
RxOk= false;
|
||||
}
|
||||
else
|
||||
{
|
||||
for(int i=0; i< mRxSize/2; i++)
|
||||
{
|
||||
if(mMemory > 10 )
|
||||
{
|
||||
hRegs[i]= (word)mRxBuf[3+2*i] * 256 + (word)mRxBuf[4+2*i];
|
||||
}
|
||||
else
|
||||
{
|
||||
mMem[i]= (word)mRxBuf[3+2*i] * 256 + (word)mRxBuf[4+2*i];
|
||||
}
|
||||
}
|
||||
};
|
||||
// ignore CRC
|
||||
mRxFrameCnt++;
|
||||
|
||||
#if __debug__ > 2
|
||||
sprintf( tmpBuf, "\nDec03:%d: ", mRxBuf[1]);
|
||||
Serial.print(tmpBuf);
|
||||
#endif
|
||||
#if __debug__ > 2
|
||||
if(mMemory > 10 )
|
||||
{
|
||||
for(int i=0; i < NB_HREGS; i++)
|
||||
{
|
||||
sprintf( tmpBuf, "%X(%4d) ", i, hRegs[i]);
|
||||
Serial.print(tmpBuf);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for(int i=0; i < NB_MEMREGS; i++)
|
||||
{
|
||||
sprintf( tmpBuf, "%X(%4d) ", i, mMem[i]);
|
||||
Serial.print(tmpBuf);
|
||||
}
|
||||
}
|
||||
Serial.print("\n");
|
||||
#endif
|
||||
|
||||
// reset memory redirection
|
||||
mMemory=255;
|
||||
|
||||
mResponse = None;
|
||||
return RxOk;
|
||||
}
|
||||
|
||||
bool xy6020l::RxDecode06( byte cnt)
|
||||
{
|
||||
bool RxOk= true;
|
||||
word RegNr;
|
||||
char tmpBuf[30];
|
||||
|
||||
mRxThis = mRxBuf[0]==mAdr? true:false;
|
||||
if(mRxBuf[1]==0x06)
|
||||
{
|
||||
RegNr = (word)mRxBuf[2] *256 + mRxBuf[3];
|
||||
if (RegNr < NB_HREGS)
|
||||
{
|
||||
hRegs[RegNr] = (word)mRxBuf[4] *256 + mRxBuf[5];
|
||||
}
|
||||
};
|
||||
|
||||
#if __debug__ > 2
|
||||
sprintf( tmpBuf, "Dec06: %d %d:=%4d\n", mRxBuf[1], RegNr, hRegs[RegNr]);
|
||||
Serial.print(tmpBuf);
|
||||
#endif
|
||||
|
||||
mResponse = None;
|
||||
return RxOk;
|
||||
}
|
||||
|
||||
bool xy6020l::RxDecode16( byte cnt)
|
||||
{
|
||||
bool RxOk= true;
|
||||
word RegNr;
|
||||
char tmpBuf[30];
|
||||
|
||||
mRxThis = mRxBuf[0]==mAdr? true:false;
|
||||
if(mRxBuf[1]==0x10)
|
||||
{
|
||||
RegNr = (word)mRxBuf[2] *256 + mRxBuf[3];
|
||||
};
|
||||
|
||||
#if __debug__ > 2
|
||||
sprintf( tmpBuf, "\nDec16: %d RegStart: 0x%X\n", mRxBuf[1], RegNr);
|
||||
Serial.print(tmpBuf);
|
||||
#endif
|
||||
|
||||
mResponse = None;
|
||||
return RxOk;
|
||||
}
|
||||
|
||||
void xy6020l::RxDecodeExceptions(byte cnt)
|
||||
{
|
||||
char tmpBuf[30];
|
||||
|
||||
if(cnt >= 5)
|
||||
{
|
||||
mLastExceptionCode = mRxBuf[2];
|
||||
// reset memory redirection
|
||||
mMemory=255;
|
||||
mResponse = None;
|
||||
|
||||
#if __debug__ > 2
|
||||
sprintf( tmpBuf, "\nException to fct code %d", ( mRxBuf[1] & 0x0F) );
|
||||
Serial.print(tmpBuf);
|
||||
switch( mRxBuf[2])
|
||||
{
|
||||
case 1: Serial.print(F("\nIllegal Function")); break;
|
||||
case 2: Serial.print(F("\nIllegal Data Address")); break;
|
||||
case 3: Serial.print(F("\nIllegal Data Value")); break;
|
||||
case 4: Serial.print(F("\nSlave Device Failure")); break;
|
||||
case 5: Serial.print(F("\nAcknowledge")); break;
|
||||
case 6: Serial.print(F("\nSlave Device Busy")); break;
|
||||
case 7: Serial.print(F("\nNegative Acknowledge")); break;
|
||||
case 8: Serial.print(F("\nMemory Parity Error")); break;
|
||||
case 10:Serial.print(F("\nGateway Path Unavailable")); break;
|
||||
case 11:Serial.print(F("\nGateway Target Device Failed to Respond")); break;
|
||||
default:Serial.print(F("\nUnknown Exception Code")); break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void xy6020l::task()
|
||||
{
|
||||
char tmpBuf[30];
|
||||
|
||||
mRxBufIdx=0;
|
||||
// check rx buffer
|
||||
#if __debug__ > 9
|
||||
if(mSerial->available() > 0)
|
||||
Serial.print("\nRX: ");
|
||||
#endif
|
||||
|
||||
while ( (mSerial->available() > 0) && (mRxBufIdx < sizeof(mRxBuf)) )
|
||||
{
|
||||
mRxBuf[ mRxBufIdx] = mSerial->read();
|
||||
|
||||
#if __debug__ > 9
|
||||
sprintf( tmpBuf, "%02X ", mRxBuf[ mRxBufIdx]);
|
||||
Serial.print(tmpBuf);
|
||||
#endif
|
||||
|
||||
mRxBufIdx++;
|
||||
// @todo optimize delay time in dependency from baudrate
|
||||
delayMicroseconds(1000);
|
||||
}
|
||||
if(mRxBufIdx > 7)
|
||||
{
|
||||
if(mRxBuf[1] & 0x80)
|
||||
RxDecodeExceptions(mRxBufIdx);
|
||||
else
|
||||
{
|
||||
// mbus as different answer layouts
|
||||
switch(mRxBuf[1] )
|
||||
{
|
||||
case 0x3: RxDecode03(mRxBufIdx); break;
|
||||
case 0x6: RxDecode06(mRxBufIdx); break;
|
||||
case 0x10:RxDecode16(mRxBufIdx); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// transmits pending ?
|
||||
if(mResponse== None)
|
||||
{
|
||||
// response received -> tx next after pause time
|
||||
// transmit pause time: mTxPeriod ms !
|
||||
if (millis() > mTLastTx + mTxPeriod)
|
||||
{
|
||||
// something in the txbuffer ? -> send Tx data out
|
||||
if(mTxBufIdx > 0)
|
||||
{
|
||||
|
||||
#if __debug__ > 9
|
||||
Serial.print("Send bytes: ");
|
||||
for(int i=0; i < mTxBufIdx; i++)
|
||||
{
|
||||
sprintf( tmpBuf, "%02X ",mTxBuf[i] );
|
||||
Serial.print(tmpBuf);
|
||||
}
|
||||
Serial.print("\n");
|
||||
#endif
|
||||
|
||||
mSerial->write( mTxBuf, mTxBufIdx);
|
||||
mTxBufIdx=0;
|
||||
mResponse = Data;
|
||||
mTLastTx = millis(); // wait from here PERIOD_TX_PAUSE for next transmits
|
||||
#if __debug__ > 2
|
||||
Serial.print("Tx Buf send\n");
|
||||
#endif
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// prioritize queued register writes against updating Hregs
|
||||
|
||||
// any command queued ?
|
||||
if( !mTxRingBuffer.IsEmpty() )
|
||||
{
|
||||
setHRegFromBuf();
|
||||
}
|
||||
else
|
||||
{
|
||||
// update all HReg
|
||||
if(!(mOptions & XY6020_OPT_NO_HREG_UPDATE))
|
||||
SendReadHReg(0, NB_HREGS-1 );
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// answer timeout detection
|
||||
if (millis() > mTO + 10)
|
||||
{
|
||||
mTO = millis();
|
||||
if(mResponse == None)
|
||||
mCntTO= PERIOD_TIMEOUT_RESPONSE;
|
||||
else
|
||||
{
|
||||
if(mCntTO>0)
|
||||
mCntTO--;
|
||||
else
|
||||
{
|
||||
// TIME OUT
|
||||
#if __debug__ > 1
|
||||
Serial.print(F("\n\n - - TIMEOUT - - *********************************************************************************************************\n"));
|
||||
//delay(10000);
|
||||
#endif
|
||||
mResponse= None;
|
||||
// reset memory redirection
|
||||
mMemory=255;
|
||||
// dummy increment frame counter to release any blocked waiting loop
|
||||
mRxFrameCnt++;
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void xy6020l::SendReadHReg( word startReg, word nbRegs)
|
||||
{
|
||||
// tx buffer free?
|
||||
if( mTxBufIdx == 0 )
|
||||
{
|
||||
mTxBuf[0]= mAdr;
|
||||
mTxBuf[1]= 0x03;
|
||||
mTxBuf[2]= startReg >> 8;
|
||||
mTxBuf[3]= startReg & 0xFF;
|
||||
mTxBuf[4]= nbRegs >> 8;
|
||||
mTxBuf[5]= nbRegs & 0xFF;
|
||||
CRCModBus(6);
|
||||
mTxBufIdx=8;
|
||||
}
|
||||
}
|
||||
|
||||
void xy6020l::SetMemory(tMemory& mem )
|
||||
{
|
||||
if( mem.Nr<10)
|
||||
{
|
||||
mMemory= mem.Nr;
|
||||
/** @todo: check memcpy for fast copy */
|
||||
mMem[HREG_IDX_M_VSET] = mem.VSet;
|
||||
mMem[HREG_IDX_M_ISET] = mem.ISet;
|
||||
mMem[HREG_IDX_M_SLVP] = mem.sLVP;
|
||||
mMem[HREG_IDX_M_SOVP] = mem.sOVP;
|
||||
mMem[HREG_IDX_M_SOCP] = mem.sOCP;
|
||||
mMem[HREG_IDX_M_SOPP] = mem.sOPP;
|
||||
mMem[HREG_IDX_M_SOHPH]= mem.sOHPh;
|
||||
mMem[HREG_IDX_M_SOHPM]= mem.sOHPm;
|
||||
mMem[HREG_IDX_M_SOAHL]= (word)(mem.sOAH & 0xFFFF);
|
||||
mMem[HREG_IDX_M_SOAHH]= (word)(mem.sOAH >> 16);
|
||||
mMem[HREG_IDX_M_SOWHL]= (word)(mem.sOWH & 0xFFFF);
|
||||
mMem[HREG_IDX_M_SOWHH]= (word)(mem.sOWH >> 16);
|
||||
mMem[HREG_IDX_M_SOTP]= mem.sOTP;
|
||||
mMem[HREG_IDX_M_SINI]= mem.sINI;
|
||||
// queue cmd for memory write
|
||||
// only 1 memory write at 1 time !
|
||||
mTxRingBuffer.AddTx( HREG_IDX_M0 + mem.Nr * HREG_IDX_M_OFFSET, 0 );
|
||||
}
|
||||
}
|
||||
|
||||
bool xy6020l::GetMemory(tMemory* pMem)
|
||||
{
|
||||
bool retVal= false;
|
||||
switch(mMemoryState)
|
||||
{
|
||||
case Send:
|
||||
if(pMem!= nullptr && (pMem->Nr < 10) )
|
||||
{
|
||||
mMemory= pMem->Nr;
|
||||
SendReadHReg( HREG_IDX_M0 + pMem->Nr * HREG_IDX_M_OFFSET, NB_MEMREGS);
|
||||
mMemoryState = Wait;
|
||||
mMemoryLastFrame= mRxFrameCnt;
|
||||
}
|
||||
break;
|
||||
case Wait:
|
||||
if( mMemoryLastFrame != mRxFrameCnt)
|
||||
{
|
||||
//
|
||||
pMem->VSet = mMem[HREG_IDX_M_VSET];
|
||||
pMem->ISet = mMem[HREG_IDX_M_ISET];
|
||||
pMem->sLVP = mMem[HREG_IDX_M_SLVP];
|
||||
pMem->sOVP = mMem[HREG_IDX_M_SOVP];
|
||||
pMem->sOCP = mMem[HREG_IDX_M_SOCP];
|
||||
pMem->sOPP = mMem[HREG_IDX_M_SOPP];
|
||||
pMem->sOHPh= mMem[HREG_IDX_M_SOHPH];
|
||||
pMem->sOHPm= mMem[HREG_IDX_M_SOHPM];
|
||||
pMem->sOAH = mMem[HREG_IDX_M_SOAHL] | ((unsigned long)mMem[HREG_IDX_M_SOAHH])<<16;
|
||||
pMem->sOWH = mMem[HREG_IDX_M_SOWHL] | ((unsigned long)mMem[HREG_IDX_M_SOWHH])<<16;
|
||||
pMem->sOTP = mMem[HREG_IDX_M_SOTP];
|
||||
pMem->sINI = mMem[HREG_IDX_M_SINI];
|
||||
|
||||
mMemory = 255;
|
||||
mMemoryState= Send;
|
||||
retVal = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
|
||||
bool xy6020l::setHReg(byte nr, word value)
|
||||
{
|
||||
bool retVal=false;
|
||||
|
||||
// tx buffer free?
|
||||
if(mTxBufIdx == 0)
|
||||
{
|
||||
mTxBuf[0]= mAdr;
|
||||
mTxBuf[1]= 0x06;
|
||||
mTxBuf[2]= 0;
|
||||
mTxBuf[3]= nr;
|
||||
mTxBuf[4]= value >> 8;
|
||||
mTxBuf[5]= value & 0xFF;
|
||||
CRCModBus(6);
|
||||
mTxBufIdx=8;
|
||||
retVal= true;
|
||||
}
|
||||
return (retVal);
|
||||
}
|
||||
|
||||
bool xy6020l::setHRegFromBuf()
|
||||
{
|
||||
bool retVal=false;
|
||||
txRingEle txEle;
|
||||
int iMem;
|
||||
|
||||
// buffer filled ?
|
||||
if( !mTxRingBuffer.IsEmpty())
|
||||
{
|
||||
if(mTxRingBuffer.GetTx(txEle))
|
||||
{
|
||||
// check if register needs to be updated at all -> skip transfer to reduce update period of HRegs
|
||||
if( !( mOptions & XY6020_OPT_SKIP_SAME_HREG_VALUE ) ||
|
||||
( hRegs[txEle.mHregIdx] != txEle.mValue ) )
|
||||
{
|
||||
mTxBuf[0]= mAdr;
|
||||
mTxBuf[1]= 0x06;
|
||||
mTxBuf[2]= 0;
|
||||
mTxBuf[3]= txEle.mHregIdx;
|
||||
if(txEle.mHregIdx < HREG_IDX_M0)
|
||||
{
|
||||
// "normal" hregs
|
||||
mTxBuf[4]= txEle.mValue >> 8;
|
||||
mTxBuf[5]= txEle.mValue & 0xFF;
|
||||
CRCModBus(6);
|
||||
mTxBufIdx=8;
|
||||
}
|
||||
else {
|
||||
// memory set HRegs
|
||||
setMemoryRegs(txEle.mHregIdx);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
#if __debug__ > 2
|
||||
Serial.print(F("\nSkip HReg Update!\n"));
|
||||
#endif
|
||||
}
|
||||
// else { send nothing };
|
||||
retVal= true;
|
||||
}
|
||||
}
|
||||
return (retVal);
|
||||
}
|
||||
|
||||
void xy6020l::CRCModBus(int datalen)
|
||||
{
|
||||
word crc = 0xFFFF;
|
||||
for (int pos = 0; pos < datalen; pos++)
|
||||
{
|
||||
crc ^= mTxBuf[pos];
|
||||
|
||||
for (int i = 8; i != 0; i--)
|
||||
{
|
||||
if ((crc & 0x0001) != 0)
|
||||
{
|
||||
crc >>= 1;
|
||||
crc ^= 0xA001;
|
||||
}
|
||||
else
|
||||
crc >>= 1;
|
||||
}
|
||||
}
|
||||
mTxBuf[datalen] = (byte)(crc & 0xFF);
|
||||
mTxBuf[datalen + 1] = (byte)((crc >> 8) & 0xFF);
|
||||
}
|
||||
|
||||
bool xy6020l::setSlaveAdd( word add)
|
||||
{
|
||||
bool retVal= true;
|
||||
if( setHReg(HREG_IDX_SLAVE_ADD, add & (word)0x00FF ) )
|
||||
{
|
||||
// change address only if command could be places in tx buffer !
|
||||
//mAdr= add;
|
||||
}
|
||||
else
|
||||
retVal=false;
|
||||
|
||||
return retVal;
|
||||
};
|
||||
|
||||
void xy6020l::setMemoryRegs(byte HRegIdx)
|
||||
{
|
||||
int iMem;
|
||||
|
||||
// buffer filled ?
|
||||
mTxBuf[0]= mAdr;
|
||||
mTxBuf[1]= 16;
|
||||
// start address
|
||||
mTxBuf[2]= 0;
|
||||
mTxBuf[3]= HRegIdx;
|
||||
// number of regs to write
|
||||
mTxBuf[4]= 0;
|
||||
mTxBuf[5]= 14;
|
||||
// bytes to write
|
||||
mTxBuf[6]= 2* 14;
|
||||
// memory set HRegs
|
||||
// register are cached in mMem[] array !
|
||||
for(iMem=0; iMem<NB_MEMREGS; iMem++)
|
||||
{
|
||||
mTxBuf[7+iMem*2]= mMem[iMem] >> 8;
|
||||
mTxBuf[8+iMem*2]= mMem[iMem] & 0xFF;
|
||||
}
|
||||
CRCModBus(9+14*2);
|
||||
mTxBufIdx=11+14*2;
|
||||
}
|
||||
|
||||
void xy6020l::PrintMemory(tMemory& mem)
|
||||
{
|
||||
char tmpBuf[50];
|
||||
Serial.print(F("\nList Memory Content:"));
|
||||
|
||||
sprintf( tmpBuf, "\nNr: %d ", mem.Nr ); Serial.print(tmpBuf);
|
||||
sprintf( tmpBuf, "\nV-SET = %d (Voltage setting)", mem.VSet ); Serial.print(tmpBuf);
|
||||
sprintf( tmpBuf, "\nI-SET = %d (Current setting)", mem.ISet ); Serial.print(tmpBuf);
|
||||
sprintf( tmpBuf, "\nS-LVP = %d (Low voltage protection value)", mem.sLVP ); Serial.print(tmpBuf);
|
||||
sprintf( tmpBuf, "\nS-OVP = %d (Overvoltage protection value)", mem.sOVP ); Serial.print(tmpBuf);
|
||||
sprintf( tmpBuf, "\nS-OCP = %d (Overcurrent protection value)", mem.sOCP ); Serial.print(tmpBuf);
|
||||
sprintf( tmpBuf, "\nS-OPP = %d (Over power protection value)", mem.sOPP ); Serial.print(tmpBuf);
|
||||
sprintf( tmpBuf, "\nS-OHP_H = %d (Maximum output time - hours)", mem.sOHPh ); Serial.print(tmpBuf);
|
||||
sprintf( tmpBuf, "\nS-OHP_M = %d (Maximum output time - minutes)", mem.sOHPm ); Serial.print(tmpBuf);
|
||||
sprintf( tmpBuf, "\nS-OAH = %d (Maximum output charge Ah)", mem.sOAH ); Serial.print(tmpBuf);
|
||||
sprintf( tmpBuf, "\nS-OWH = %d (Maximum output energy Wh)", mem.sOWH ); Serial.print(tmpBuf);
|
||||
sprintf( tmpBuf, "\nS-OTP = %d (Over temperature protection)", mem.sOTP ); Serial.print(tmpBuf);
|
||||
sprintf( tmpBuf, "\nS-INI = %d (Power-on output switch)", mem.sINI ); Serial.print(tmpBuf);
|
||||
|
||||
Serial.print(F("\n"));
|
||||
}
|
||||
296
ESP32_XY6020L_BUS/xy6020l.h
Normal file
296
ESP32_XY6020L_BUS/xy6020l.h
Normal file
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* @file xy6020l.h
|
||||
* @brief UART control access to XY6020L DCDC
|
||||
*
|
||||
* This library provides an embedded and simplified ModBus implementation.
|
||||
* The data type of interfaces to physical values are in decimal, scaled almost in 0.01.
|
||||
*
|
||||
* Tested only on a Arduino Pro Micro clone from China
|
||||
*
|
||||
* Special thanks to user g-radmac for his discovery of the UART protocol!
|
||||
* References:
|
||||
* https://forum.allaboutcircuits.com/threads/exploring-programming-a-xy6020l-power-supply-via-modbus.197022/
|
||||
* https://www.simplymodbus.ca/FAQ.htm
|
||||
*
|
||||
*
|
||||
* @author Jens Gleissberg
|
||||
* @date 2024
|
||||
* @license GNU Lesser General Public License v3.0 or later
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef xy6020l_h
|
||||
#define xy6020l_h
|
||||
|
||||
#include "Arduino.h"
|
||||
|
||||
// the XY6020 provides 31 holding registers
|
||||
#define NB_HREGS 31
|
||||
#define NB_MEMREGS 14
|
||||
|
||||
// Holding Register index
|
||||
// set voltage
|
||||
#define HREG_IDX_CV 0
|
||||
// set current
|
||||
#define HREG_IDX_CC 1
|
||||
// actual voltage 0,01 V
|
||||
#define HREG_IDX_ACT_V 2
|
||||
// actual current 0,01 A
|
||||
#define HREG_IDX_ACT_C 3
|
||||
// actual output power 0,1 W
|
||||
#define HREG_IDX_ACT_P 4
|
||||
// input voltage 0,01 V
|
||||
#define HREG_IDX_IN_V 5
|
||||
// output charge 0,001 Ah
|
||||
#define HREG_IDX_OUT_CHRG 6
|
||||
#define HREG_IDX_OUT_CHRG_HIGH 7
|
||||
// output energy 0,001 Wh
|
||||
#define HREG_IDX_OUT_ENERGY 8
|
||||
#define HREG_IDX_OUT_ENERGY_HIGH 9
|
||||
// on time [h] ??
|
||||
#define HREG_IDX_ON_HOUR 0x0A
|
||||
// on time [min]
|
||||
#define HREG_IDX_ON_MIN 0x0B
|
||||
// on time [s]
|
||||
#define HREG_IDX_ON_SEC 0x0C
|
||||
// temperature 0,1 °C / Fahrenheit ?
|
||||
#define HREG_IDX_TEMP 0x0D
|
||||
#define HREG_IDX_TEMP_EXD 0x0E
|
||||
// key lock changes
|
||||
#define HREG_IDX_LOCK 0x0F
|
||||
#define HREG_IDX_PROTECT 0x10
|
||||
#define HREG_IDX_CVCC 0x11
|
||||
// output on
|
||||
#define HREG_IDX_OUTPUT_ON 0x12
|
||||
#define HREG_IDX_FC 0x13
|
||||
#define HREG_IDX_MODEL 0x16
|
||||
#define HREG_IDX_VERSION 0x17
|
||||
#define HREG_IDX_SLAVE_ADD 0x18
|
||||
#define HREG_IDX_BAUDRATE 0x19
|
||||
#define HREG_IDX_TEMP_OFS 0x1A
|
||||
#define HREG_IDX_TEMP_EXT_OFS 0x1B
|
||||
#define HREG_IDX_MEMORY 0x1D
|
||||
// Memory register
|
||||
#define HREG_IDX_M0 0x50
|
||||
#define HREG_IDX_M_OFFSET 0x10
|
||||
#define HREG_IDX_M_VSET 0
|
||||
#define HREG_IDX_M_ISET 1
|
||||
#define HREG_IDX_M_SLVP 2
|
||||
#define HREG_IDX_M_SOVP 3
|
||||
#define HREG_IDX_M_SOCP 4
|
||||
#define HREG_IDX_M_SOPP 5
|
||||
#define HREG_IDX_M_SOHPH 6
|
||||
#define HREG_IDX_M_SOHPM 7
|
||||
#define HREG_IDX_M_SOAHL 8
|
||||
#define HREG_IDX_M_SOAHH 9
|
||||
#define HREG_IDX_M_SOWHL 10
|
||||
#define HREG_IDX_M_SOWHH 11
|
||||
#define HREG_IDX_M_SOTP 12
|
||||
#define HREG_IDX_M_SINI 13
|
||||
|
||||
|
||||
#define TX_RING_BUFFER_SIZE 16
|
||||
typedef struct {
|
||||
byte mHregIdx;
|
||||
word mValue;
|
||||
} txRingEle;
|
||||
|
||||
class TxRingBuffer
|
||||
{
|
||||
private:
|
||||
txRingEle* mpIn;
|
||||
txRingEle* mpOut;
|
||||
int mIn;
|
||||
txRingEle mTxBuf[TX_RING_BUFFER_SIZE];
|
||||
public:
|
||||
TxRingBuffer();
|
||||
bool IsEmpty() { return (mIn<1);};
|
||||
bool IsFull() { return (mIn>=TX_RING_BUFFER_SIZE);}
|
||||
bool AddTx(txRingEle* pTxEle);
|
||||
bool AddTx(byte hRegIdx, word value);
|
||||
bool GetTx(txRingEle& pTxEle);
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
byte Nr;
|
||||
word VSet;
|
||||
word ISet;
|
||||
word sLVP;
|
||||
word sOVP;
|
||||
word sOCP;
|
||||
word sOPP;
|
||||
word sOHPh;
|
||||
word sOHPm;
|
||||
unsigned long sOAH;
|
||||
unsigned long sOWH;
|
||||
word sOTP;
|
||||
word sINI;
|
||||
} tMemory;
|
||||
|
||||
/**
|
||||
* @class xy6020l
|
||||
* @brief Class for controlling the XY6020L DCDC converter
|
||||
*/
|
||||
/** @brief option flags */
|
||||
#define XY6020_OPT_SKIP_SAME_HREG_VALUE 1
|
||||
#define XY6020_OPT_NO_HREG_UPDATE 2
|
||||
|
||||
class xy6020l
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor requires an interface to serial port
|
||||
* @param serial Stream object reference (i.e., )
|
||||
* @param adr slave address of the xy device, can be change by setSlaveAdd command
|
||||
* @param txPeriod minimum period to wait for next tx message, at times < 50 ms the XY6020 does not send answers
|
||||
*/
|
||||
xy6020l(Stream& serial, byte adr=1, byte txPeriod=50, byte options=XY6020_OPT_SKIP_SAME_HREG_VALUE );
|
||||
/**
|
||||
* @brief Task method that must be called in loop() function of the main program cyclically.
|
||||
* It automatically triggers the reading of the Holding Registers each PERIOD_READ_ALL_HREGS ms.
|
||||
*/
|
||||
void task(void);
|
||||
|
||||
/** @brief requests to read all Hold Registers, reception from XY6020 can be checked via HRegUpdated and data access via read/get methods
|
||||
@return false if tx buffer is full
|
||||
*/
|
||||
bool ReadAllHRegs(void);
|
||||
/** @brief true if the Hold Regs are read after read all register command, asynchron access */
|
||||
bool HRegUpdated(void);
|
||||
|
||||
/// @name XY6020L application layer: HReg register access
|
||||
/// @{
|
||||
//
|
||||
/** @brief voltage setpoint, LSB: 0.01 V , R/W */
|
||||
word getCV(void) { return (word)hRegs[ HREG_IDX_CV]; };
|
||||
bool setCV( word cv) { return mTxRingBuffer.AddTx(HREG_IDX_CV, cv);};
|
||||
|
||||
/** @brief constant current setpoint, LSB: 0.01 A , R/W */
|
||||
word getCC() { return (word)hRegs[ HREG_IDX_CC]; };
|
||||
bool setCC( word cc) { return mTxRingBuffer.AddTx(HREG_IDX_CC, cc);};
|
||||
|
||||
/** @brief actual input voltage , LSB: 0.01 V, readonly */
|
||||
word getInV() { return (word)hRegs[ HREG_IDX_IN_V ]; };
|
||||
/** @brief actual voltage at output, LSB: 0.01 V, readonly */
|
||||
word getActV() { return (word)hRegs[ HREG_IDX_ACT_V]; };
|
||||
/** @brief actual current at output, LSB: 0.01 A, readonly */
|
||||
word getActC() { return (word)hRegs[ HREG_IDX_ACT_C]; };
|
||||
/** @brief actual power at output, LSB: 0.01 W, readonly */
|
||||
word getActP() { return (word)hRegs[ HREG_IDX_ACT_P]; };
|
||||
/** @brief actual charge from output, LSB: 0.001 Ah, readonly, with high word because not tested yet */
|
||||
word getCharge() { return (word)hRegs[ HREG_IDX_OUT_CHRG ] ; };
|
||||
/** @brief actual energy provided from output, LSB: 0.001 Wh, readonly, with high word because not tested yet */
|
||||
word getEnergy() { return (word)hRegs[ HREG_IDX_OUT_ENERGY ] ; };
|
||||
/** @brief actual output time, LSB: 1 h, readonly */
|
||||
word getHour() { return (word)hRegs[ HREG_IDX_ON_HOUR ] ; };
|
||||
/** @brief actual output time, LSB: 1 min, readonly */
|
||||
word getMin() { return (word)hRegs[ HREG_IDX_ON_MIN ] ; };
|
||||
/** @brief actual output time, LSB: 1 secs, readonly */
|
||||
word getSec() { return (word)hRegs[ HREG_IDX_ON_SEC ] ; };
|
||||
|
||||
/** @brief dcdc temperature, LSB: 0.1°C/F, readonly */
|
||||
word getTemp() { return (word)hRegs[ HREG_IDX_TEMP ] ; };
|
||||
/** @brief external temperature, LSB: 0.1°C/F, readonly */
|
||||
word getTempExt() { return (word)hRegs[ HREG_IDX_TEMP_EXD ]; };
|
||||
|
||||
/** @brief lock switch, true = on, R/W */
|
||||
bool getLockOn() { return hRegs[ HREG_IDX_LOCK]>0?true:false; };
|
||||
bool setLockOn(bool onState) { return mTxRingBuffer.AddTx(HREG_IDX_LOCK, onState?1:0);};
|
||||
|
||||
/** @brief lock switch, true = on, R/W */
|
||||
word getProtect() { return hRegs[ HREG_IDX_PROTECT]; };
|
||||
bool setProtect(word state) { return setHReg(HREG_IDX_PROTECT, state );};
|
||||
|
||||
/** @brief returns if CC is active , true = on, read only */
|
||||
bool isCC() { return hRegs[ HREG_IDX_CVCC]>0?true:false; };
|
||||
/** @brief returns if CV is active , true = on, read only */
|
||||
bool isCV() { return hRegs[ HREG_IDX_CVCC]<1?true:false; };
|
||||
|
||||
/** @brief output switch, true = on, R/W */
|
||||
bool getOutputOn() { return hRegs[ HREG_IDX_OUTPUT_ON]>0?true:false; };
|
||||
bool setOutput(bool onState) { return mTxRingBuffer.AddTx(HREG_IDX_OUTPUT_ON, onState?1:0);};
|
||||
|
||||
/** @brief set the temperature unit to °C, read not implemended because no use */
|
||||
bool setTempAsCelsius(void) { return setHReg(HREG_IDX_FC, 0);};
|
||||
/** @brief set the temperature unit to Fahrenheit, read not implemended because no use */
|
||||
bool setTempAsFahrenheit(void) { return setHReg(HREG_IDX_FC, 1);};
|
||||
|
||||
/** @brief returns the product number, readonly */
|
||||
word getModel(void) { return (word)hRegs[ HREG_IDX_MODEL ] ; };
|
||||
/** @brief returns the version number, readonly */
|
||||
word getVersion(void) { return (word)hRegs[ HREG_IDX_VERSION ] ; };
|
||||
|
||||
/** @brief slave address, R/W, take effect after reset of XY6020L ! */
|
||||
word getSlaveAdd(void) { return (word)hRegs[ HREG_IDX_SLAVE_ADD]; };
|
||||
bool setSlaveAdd( word add);
|
||||
|
||||
/** @brief baud rate , W, no read option because on use
|
||||
@todo: provide enum for rate number to avoid random/unsupported number */
|
||||
bool setBaudrate( word rate) { return setHReg(HREG_IDX_BAUDRATE, rate);};
|
||||
|
||||
/** @brief internal temperature offset, R/W */
|
||||
word getTempOfs(void) { return (word)hRegs[ HREG_IDX_TEMP_OFS]; };
|
||||
bool setTempOfs( word tempOfs) { return setHReg(HREG_IDX_TEMP_OFS, tempOfs );};
|
||||
|
||||
/** @brief external temperature offset, R/W */
|
||||
word getTempExtOfs(void) { return (word)hRegs[ HREG_IDX_TEMP_EXT_OFS]; };
|
||||
bool setTempExtOfs( word tempOfs) { return setHReg(HREG_IDX_TEMP_EXT_OFS, tempOfs );};
|
||||
|
||||
/** @brief Presets, R/W */
|
||||
word getPreset(void) { return (word)hRegs[ HREG_IDX_MEMORY]; };
|
||||
bool setPreset( word preset) { return setHReg(HREG_IDX_MEMORY, preset );};
|
||||
/// @}
|
||||
|
||||
bool TxBufEmpty(void) { return ((mTxBufIdx<=0)&&(mTxRingBuffer.IsEmpty()));};
|
||||
void SetMemory(tMemory& mem);
|
||||
bool GetMemory(tMemory* pMem);
|
||||
void PrintMemory(tMemory& mem);
|
||||
|
||||
private:
|
||||
byte mAdr;
|
||||
byte mOptions;
|
||||
Stream* mSerial;
|
||||
byte mRxBufIdx;
|
||||
unsigned char mRxBuf[60];
|
||||
byte mRxState;
|
||||
bool mRxThis;
|
||||
byte mRxSize;
|
||||
word mRxFrameCnt;
|
||||
word mRxFrameCntLast;
|
||||
byte mLastExceptionCode;
|
||||
|
||||
enum Response { None, Confirm, Data };
|
||||
Response mResponse;
|
||||
/** @brief rx answer belongs to memory request data
|
||||
* M0..M9 -> 0..9 ; 255 no memory */
|
||||
byte mMemory;
|
||||
long mTs;
|
||||
long mTO;
|
||||
long mTLastTx;
|
||||
byte mCntTO;
|
||||
byte mTxPeriod;
|
||||
|
||||
int mTxBufIdx;
|
||||
unsigned char mTxBuf[40];
|
||||
TxRingBuffer mTxRingBuffer;
|
||||
|
||||
/** @brief buffer to cache hold regs after reading them at once and to check if update needed for writting regs */
|
||||
word hRegs[NB_HREGS];
|
||||
/** @brief 1 cache for memory register */
|
||||
word mMem[NB_MEMREGS];
|
||||
enum MemoryState { Send, Wait };
|
||||
MemoryState mMemoryState;
|
||||
word mMemoryLastFrame;
|
||||
|
||||
bool setHReg(byte nr, word value);
|
||||
bool setHRegFromBuf(void);
|
||||
|
||||
void CRCModBus(int datalen);
|
||||
void RxDecodeExceptions(byte cnt);
|
||||
bool RxDecode03( byte cnt);
|
||||
bool RxDecode06( byte cnt);
|
||||
bool RxDecode16( byte cnt);
|
||||
void SendReadHReg( word startReg, word nbRegs);
|
||||
void setMemoryRegs(byte HRegIdx);
|
||||
};
|
||||
#endif
|
||||
Reference in New Issue
Block a user