Hi—sorry for my English, and I’m not sure this is the right category.
I’ve been experimenting with controlling the YL-UV3-2406 inverter (with the limiter port) (lcd firmware: STC08-2403 / cpu ver-3.1-2403). the blue one from chinese famous website “TINGEN GTN”
Below is an Arduino/ESP32 sketch and notes for simulating the limiter over the RS485 port using an ESP32 + MAX485 transceiver.
Mods: please feel free to move or edit the post if needed.
In my tests the program works from 0 to ~900 W.
/*
===============================================================================
TECHNICAL NOTICE + CODE (ESP32 + MAX485)
Control a YL-UV3-2406 inverter via “Limiter” RS485 — 0..~900 W
===============================================================================
1) PURPOSE
Reverse-engineered, practical framing to set the inverter power setpoint via the
Limiter RS485 port using an ESP32 and a MAX485. Includes wiring, frame format,
checksum rule, command mapping (0x22 / 0x21), ready-to-use sketch, and notes.
2) HARDWARE
- ESP32 (e.g., WROOM-32 DevKitC)
- RS485 transceiver MAX485 (or SN75176 equivalent)
- Twisted pair for RS485 A/B, 120 Ω termination if needed
- Stable power (ESP32 5V USB; MAX485 VCC per module, 3V3/5V)
- Common ground between ESP32/MAX485/inverter
3) WIRING (TX to inverter)
ESP32 -> MAX485 -> YL-UV3-2406 (Limiter RS485)
------------------------------------------------------------------------------
GPIO17 (TX2) -> DI
GPIO16 (RX2) -> RO (optional; not used here)
GPIO4 -> DE & RE (bridged together)
3V3 or 5V -> VCC (per module)
GND -> GND -> GND (common)
A ---------------------------> A (RS485+)
B ---------------------------> B (RS485−)
DE/RE: HIGH=TX, LOW=RX. If no reaction, try swapping A/B.
Line: 4800 baud, 8N1. Console (USB): 115200 baud.
4) FRAME FORMAT (8 bytes)
[0] [1] [2] [3] [4] [5] [6] [7]
24 56 00 CMD G X M CS
- Prefix: 24 56 00
- CMD: 0x22 → direct 0..255 W | 0x21 → tiered >~255 W
- G: tier/gain (depends on CMD)
- X: value (depends on CMD/G)
- M: 0x80 (observed)
- CS: checksum (see #5)
5) CHECKSUM RULE (MANDATORY)
Sum of all bytes modulo 256 must equal 0x23.
CS = (0x23 − ((byte0 + … + byte6) & 0xFF)) & 0xFF
Example (0x22, 100 W): 24 56 00 22 00 64 80 A3
6) COMMANDS & MAPPING (confirmed by measurements)
A) CMD 0x22 — direct 0..255 W
24 56 00 22 00 [PW] 80 [CS]
Examples: 0W→… 00 80 07 | 25W→… 19 80 EE | 50W→… 32 80 D5
100W→… 64 80 A3 | 150W→… 96 80 71 | 200W→… C8 80 3F | 255W→… FF 80 08
B) CMD 0x21 — tiered ranges for >~255 W
24 56 00 21 [G] [X] 80 [CS]
Observed:
- G=0x01 → ~256..~499 W, linear: W ≈ 256 + 0.93 * X
- G=0x02 → ~500..~640 W, base≈500, slope≈1.00 W/step
- G=0x03 → ~650..~850 W, base≈750, slope≈1.20 W/step
- G=0x04 → ~860..~900 W, base≈880, slope≈1.00 W/step
- G=0x05 → saturates ≈900 W
Example (300 W): X≈(300−256)/0.93≈47 → 24 56 00 21 01 2C 80 DB
Notes: Display varies slightly with mains (e.g., 226–233 V). For 300–499 W keep G=0x01.
Above ~900 W a cap is observed.
7) SAFETY / DISCLAIMER
Use at your own risk. Ensure electrical safety and ratings compliance.
This information is provided for interoperability. Trademarks remain with owners.
===============================================================================
CODE (Arduino / ESP32)
Commands (serial @115200):
p <watts 0..1000> -> set target power
g <gain 1..5> <x> -> send CMD 0x21 directly (debug)
raw <hex bytes...> -> send raw 8-byte frame (auto checksum if length=8)
@<ms> -> period
start | stop | once
===============================================================================
*/
#define TXD2 17
#define MAX485_DE_RE 4
#define BAUD_RS485 4800
// Empirical calibration (based on field measurements)
static const float BASE_G1 = 256.0f; // CMD 0x21, G=1 — W @ X=0
static const float SLOPE_G1 = 0.93f; // CMD 0x21, G=1 — W/step (~256..499)
static const float BASE_G2 = 500.0f; // ~500..~640
static const float SLOPE_G2 = 1.00f;
static const float BASE_G3 = 750.0f; // ~650..~850
static const float SLOPE_G3 = 1.20f;
static const float BASE_G4 = 880.0f; // ~860..~900
static const float SLOPE_G4 = 1.00f;
// G=5 ≈ cap ~900 W (not effective)
static const float MAX_G5 = 900.0f;
uint8_t frame[64] = {0x24,0x56,0x00,0x22,0x00,0x00,0x80,0x07};
size_t frameLen = 8;
unsigned long periodMs = 500;
bool txEnabled = true;
void setDriver(bool tx){ pinMode(MAX485_DE_RE, OUTPUT); digitalWrite(MAX485_DE_RE, tx?HIGH:LOW); }
void printHex(const uint8_t* b, size_t n){
for(size_t i=0;i<n;i++){ if(b[i]<0x10) Serial.print("0"); Serial.print(b[i],HEX); if(i+1<n) Serial.print(" "); }
}
uint8_t cs23(const uint8_t* b, size_t nNoCS){
uint16_t s=0; for(size_t i=0;i<nNoCS;i++) s+=b[i];
return (uint8_t)((0x23 - (s & 0xFF)) & 0xFF);
}
void sendFrame(){
setDriver(true);
Serial2.write(frame,frameLen);
Serial2.flush();
setDriver(false);
Serial.print("TX "); printHex(frame,frameLen); Serial.println();
}
// CMD 0x22: direct 0..255 W -> 24 56 00 22 00 [PW] 80 [CS]
void buildCmd22(uint8_t watts){
frame[0]=0x24; frame[1]=0x56; frame[2]=0x00; frame[3]=0x22;
frame[4]=0x00; frame[5]=watts; frame[6]=0x80;
frame[7]=cs23(frame,7); frameLen=8;
}
// CMD 0x21: tiers (>~255 W) -> 24 56 00 21 [G] [X] 80 [CS]
void buildCmd21(uint8_t g, uint8_t x){
frame[0]=0x24; frame[1]=0x56; frame[2]=0x00; frame[3]=0x21;
frame[4]=g; frame[5]=x; frame[6]=0x80;
frame[7]=cs23(frame,7); frameLen=8;
}
// Map W -> frame (0..~900 W)
void buildPower(uint16_t W){
if (W <= 255) { buildCmd22((uint8_t)W); return; }
if (W <= 499) { // 0x21 G=1 (linear)
float xf = (W - BASE_G1) / SLOPE_G1;
if (xf < 0) xf = 0; if (xf > 255) xf = 255;
buildCmd21(1, (uint8_t)lroundf(xf));
return;
}
if (W < 650){ // 0x21 G=2
float xf = (W - BASE_G2) / SLOPE_G2;
if (xf < 0) xf = 0; if (xf > 255) xf = 255;
buildCmd21(2, (uint8_t)lroundf(xf));
return;
}
if (W < 860){ // 0x21 G=3
float xf = (W - BASE_G3) / SLOPE_G3;
if (xf < 0) xf = 0; if (xf > 255) xf = 255;
buildCmd21(3, (uint8_t)lroundf(xf));
return;
}
if (W <= 900){ // 0x21 G=4
float xf = (W - BASE_G4) / SLOPE_G4;
if (xf < 0) xf = 0; if (xf > 255) xf = 255;
buildCmd21(4, (uint8_t)lroundf(xf));
return;
}
// >900: observed cap ~900 W (G=5 not very effective)
(void)W; buildCmd21(5, 0);
}
// ---- Console helpers ----
bool parseHexBytes(const String& line, uint8_t* out, size_t& outLen){
outLen=0; int i=0, n=line.length();
while(i<n){
while(i<n && isspace((int)line[i])) i++;
if(i>=n) break;
int j=i; while(j<n && !isspace((int)line[j])) j++;
String tok=line.substring(i,j); tok.trim();
if(tok.length()==0) { i=j; continue; }
if(tok[0]=='#') break;
char* e=nullptr; long v=strtol(tok.c_str(), &e, 16);
if(e==tok.c_str() || v<0 || v>255) return false;
if(outLen>=64) return false;
out[outLen++]=(uint8_t)v; i=j;
}
return outLen>0;
}
void setup(){
Serial.begin(115200);
Serial2.begin(BAUD_RS485, SERIAL_8N1, -1, TXD2);
setDriver(true);
buildPower(0);
Serial.println("YL-UV3-2406 RS485 Limiter | p <0..1000> | g <1..5> <x> | raw <hex...> | @<ms> | start | stop | once");
}
void loop(){
static unsigned long t0=0;
if(txEnabled && millis()-t0>=periodMs){ t0=millis(); sendFrame(); }
if(Serial.available()){
String line=Serial.readStringUntil('\n'); line.trim(); if(!line.length()) return;
if(line.equalsIgnoreCase("stop")){ txEnabled=false; Serial.println("TX stop."); return; }
if(line.equalsIgnoreCase("start")){ txEnabled=true; Serial.println("TX start."); return; }
if(line.equalsIgnoreCase("once")){ sendFrame(); return; }
if(line[0]=='@'){ long v=line.substring(1).toInt(); if(v>0){ periodMs=v; Serial.printf("period=%ld ms\n", v);} return; }
if(line[0]=='p' || line[0]=='P'){
int sp=line.indexOf(' '); if(sp>0){
long w=line.substring(sp+1).toInt(); if(w<0) w=0; if(w>1000) w=1000;
buildPower((uint16_t)w);
Serial.printf("Target %ld W -> ", w); printHex(frame,frameLen); Serial.println();
sendFrame();
}
return;
}
if(line.startsWith("g ") || line.startsWith("G ")){
int sp1=line.indexOf(' '); if(sp1<0) return;
int sp2=line.indexOf(' ', sp1+1); if(sp2<0) return;
int g = line.substring(sp1+1, sp2).toInt();
int x = line.substring(sp2+1).toInt();
if(g<1) g=1; if(g>5) g=5; if(x<0) x=0; if(x>255) x=255;
buildCmd21((uint8_t)g, (uint8_t)x);
Serial.printf("CMD21 g=%d x=%d -> ", g, x); printHex(frame,frameLen); Serial.println();
sendFrame(); return;
}
if(line.startsWith("raw ") || line.startsWith("RAW ")){
String hex=line.substring(4); hex.trim();
uint8_t tmp[64]; size_t n=0;
if(!parseHexBytes(hex,tmp,n)){ Serial.println("ERR raw parse"); return; }
if(n==8){ tmp[7]=cs23(tmp,7); }
memcpy(frame,tmp,n); frameLen=n;
Serial.print("RAW set -> "); printHex(frame,frameLen); Serial.println();
sendFrame(); return;
}
Serial.println("Unknown command.");
}
}
