Saturday, May 13, 2023

MODBUS communication with Solis 4G-US inverters

Solis single-phase inverters have a circular RS485 connector supporting MODBUS communication.  The connectors can be difficult to find for sale outside China, so using a wifi data logger stick is a more straightforward way of communicating with the inverters.  IGEN Tech is the OEM for the Solis wifi data loggers, which IGEN also sells under the SOLARMAN brand.  While the same circular connector is used by many other inverter manufacturers such as Solax, RENAC, and KSTAR, the logger firmware is customized to read and report the MODBUS registers for a particular manufacturer.

The LSW-3 series of wifi loggers allow external programs to perform MODBUS queries via a TCP connection port 8899.  I believe this is a variant of the MODBUS/TCP protocol that is assigned TCP port 502.

To perform MODBUS queries, I used pysolarmanv5.  To connect to the logger pysolarmanv5 requires the logger serial number and IP address.  Initially I read the serial number off the label of the logger, and looked up the IP address from the admin page of my router.  Later I noticed, which sends a broadcast UDP packet which the data logger replies to.  I sometimes had to run it more than once before the logger responded to the scan packet.

The Solis 4G-US series inverters are Sunspec MODBUS certified, and have the well-known 32-bit ‘SunS’ identifier (0x53756e53) at address 40001.  This means it should be possible to read the registers by decoding the SunSpec information models and inverter device IDs.  SunSpec shares some example code, however I haven't been able to figure it all out.

I couldn't figure out the Solis registers using SunSpec, but I was able to find the register documentation from Ginlong.  The AC output power and DC input power are 32-bit registers at address 3005 using MODBUS function code 4 (input registers).  I wrote a python program to read the output and input power and calculate the efficiency.  It also reads the inverter temperature, and outputs the data every 5 minutes.  I also wrote a small AWK program to calculate the weighted average of multiple samples.  The code can be found in my github repo.

Over multiple days of output in the spring of 2023 including sunny and cloudy days, I observed an overall efficiency of 94% for a Solis 1P4K-4G-US.  For a Solis 1P6K-4G-US I observed an average efficiency of 95%.  This compares to respective advertised CEC weighted efficiencies of 97.5% and 97.0%.

Monday, April 10, 2023

KSTAR Single Phase String Inverters


KSTAR New Energy makes single phase grid-tied inverters ranging from 1 kW to 10 kW.  I tested a 3000S, a 5000D, and a 6000D that were produced in KSTAR's factory outside of Shenzhen.  Their single phase inverters are marketed for locations with a 230 V line to neutral (L-N) grid.  They also work with the split phase 240 V line to line grid that is typical in the US and Canada.  They do not have UL 1741 certification, so they would require special engineering approval to be used for permanent installations with most US and Canada power utilities.

Residential inverters used in the US and Canada usually have an attached junction box with terminal connections for DC and AC wiring.  In the rest of the world, inverters usually have MC4 connectors for the DC string input, and a watertight three-pin plug connection for the AC output.  It is much more convenient having the plug connections when testing inverters and PV panels.  It also avoids potential electrical code concerns when DC wiring up to 600 V and 240 Vac are in the same junction box.


The KSTAR inverters all included MC4 crimp connectors for terminating the DC strings.  The AC connector will accept SOOW or SJOW cable with a outside diameter of up to 16 mm.  I used 3-wire 12 AWG SOOW cable that is rated for up to 25 Amps.

The 3000S has a single string input, and a "nominal" output power of 3 kW.  It is a light inverter, with a stated weight of 8 kg.  Out of the box, the measured weight was 7.3 kg.  The light weight makes it very easy for a single person to install.  When hooked up to a test string of 10 72-cell panels, the efficiency was 85-86%. This is much lower than the spec efficiency of 97% or the 96% efficiency at nominal 380 V listed on the inspection and test sheet that was included with the inverter.  With input power of 3070 W and input voltage of 367.7 V, the output power was 2620 W, for an efficiency of 85.3%.  KSTAR sales and engineering were unable to explain the low efficiency.

The 5000D and 6000D have the same external dimensions and connections on the bottom.  The weight of the 5000D is 11.74 kg, while the 6000D weighs 12.48 kg.   This suggests the 6000D has different internal circuitry, likely larger inductors and capacitors, to support the higher power rating.

The efficiency of the 5000D and 6000D inverters ranged between 89 and 91%.  The screenshot of monitor data below shows a total input power of 6240 W with AC output power of 5570 W, for an efficiency of 89.3%.  This test was done with a large difference between the PV1 and PV2 voltages to represent typical residential PV installations which are not optimized for the inverter's 380 nominal string voltage.

The KStar inverters are reasonably priced and easy to install, but the low efficiency makes them unattractive compared to Growatt and Ginlong Solis inverters.

Wednesday, October 5, 2022

DC Wiring Losses in String and Microinverter Solar PV Arrays

There are two common ways of wiring solar PV arrays.  Each panel can be connected to a microinverter, with each microinverter connected in parallel to an AC bus.  Alternatively, panels can be connected in series, with one or more DC strings connected to an inverter.  Although there is debate over which design is best, at Solar Si, we prefer string inverters.  This is an analysis of DC wiring losses with an array of 8 72-cell LONGi PV modules of about 450 Watts each.

There are two sources of wiring resistance in the array.  The first is from the wire itself, and the second is from the connectors.  The 12 AWG wire used for the panel output cables has a resistance of 5.2 mOhm/m.  The MC4 connectors are specified to have a contact resistance of less than 0.5 mOhm.  While this may be the resistance when tested in a clean and dry factory, test results in warm and humid conditions show a much higher resistance.  Reliability Model Development for Photovoltaic Connector Lifetime Prediction Capabilities indicate resistance in the field is likely to be around 2.5 mOhm.

For the string array, the panels are arranged in the portrait configuration, with the inverter situated 1m from the array.  The panels are about 1.06 m wide, making the length of the array 8.5 m.  Each panel has a 20cm and a 40cm negative and positive output cable.  Unlike the 12 AWG wire used for the PV panel output cables, in Canada, field wiring for PV strings is almost always done with 10 AWG RPVU wire.  This has a resistance of 3.28 mOhm/m, and a total of 10.5 m are used for the array.

With 8 panels, there are 7 connections between panels, plus two connections at the ends mating with the RPVU wire.  The DC connections on the inverter are usually not MC4, but for simplicity the resistance is assumed to be the same.  Adding the positive and negative connections connections to the inverter, the total comes to 11.  Here's the calculations for the total resistance:

10.5 m * 3.28 mOhm/m = 34.4 mOhm
12 AWG 0.6 m panel cables * 8 = 4.8m, * 5.2 = 25 mOhm
11 contacts/string * 2.5 mOhm = 27.5 mOhm
total: 86.9 mOhm

For the microinverter array, the optional 1.4 m PV panel output cables will be needed in order for the cables to reach the corresponding microinverter.  This increased the total length of 12 AWG wire to 22.4 m.  Here's the calculations for the total resistance:

12 AWG 2.8 m panel cables * 8 = 22.4 m, * 5.2 = 116 mOhm
16 contacts * 2.5 mOhm = 40 mOhm
total: 156 mOhm

Although the microinverter configuration higher resistance losses, they are not significant.  During peak power output, DC current is about 10 Amps.  Using P = I^2 * R, power losses are around 0.5%.  Most of the time the array output current is much less than 10 Amps, so the average power loss is much lower.  There are additional losses from the AC bus connectors, which are also not significant.

In conclusion, power losses are higher with microinverters than string inverters, but they are not significant.  The justification for choosing string inverters lies more with the cost savings in material and labor.  For an array with 16 panels, the cost of a 6 kW inverter with 2 string inputs is less than half the cost of 16 Enphase IQ7A microinverters.

Friday, April 16, 2021

Pi ethernet gadget with reverse SSH proxy


I love my Pi Zeros.  I think every hacker should have one in their toolbox.  When I got my firs Pi Zero several years ago, I used a USB-TTL serial adapter to connect to the console UART on pins 8 and 10 of the Pi header.  Once I learned how to setup the Zero as an ethernet gadget, things were a bit easier.  However updating software was still a cumbersome process of downloading files to the host computer and then using scp to transfer them to the Pi.  This blog post documents how to setup the Pi to use a SSH reverse proxy so utilities like git and apt work.

When I got my first Pi Zero, I chose the Pi OS Lite image.  I decided to update to the March 4, 2021 release, and this time I used the Pi OS with desktop because it includes development tools like git.  I followed the ethernet gadget setup instructions, modifying config.txt, cmdline.txt, and creating an empty file called "ssh".  The next step is to configure the multicast DNS component of Zeroconf.  As mentioned in the Adafruit instructions, if you are using Windows, the easiest way to do this is installing Apple's Bonjour service.

To use a reverse proxy over ssh, Windows users can't use putty as that feature is not supported.  OpenSSH supports reverse socks5 proxies as of version 7.6.  For connecting from Windows, I installed MSYS2, including OpenSSH 8.4.  On Windows 10, WSL is probably the easiest option.  To connect to the Pi and enable a reverse socks5 proxy on port 1080, enter, "ssh -R 1080 pi@raspberrypi.local".

Once connected to the Pi, set "http_proxy" to "socks5h://localhost:1080".  The "h" at the end is important as it means the client will do hostname (DNS) resolution through the proxy.  I added the following line to .profile to set it every time I login:

export http_proxy="socks5h://localhost:1080"

Programs such as git and curl will automatically use the socks proxy when the http_proxy environment variable is set.  Note that github defaults to showing https URLs for repositories, which need to be changed to "http://" for the proxy to work.

The last configuration I recommend is setting the current date, since the Pi does not have a battery-backed RTC.  I normally use ntpdate from the ntp project for manually setting the date and time on Linux, but it does not work with a socks proxy.  After some searching I found a suggestion of using the HTTP Date: field from a reliable internet server.  The command I use is:

date -s "`curl -sI | grep "^Date:" | cut -d' ' -f3-7`"

Once the Pi Zero is configured and has the proper date and time set, I recommend running "apt update".  If everything is working properly, it will use the socks5 reverse proxy to connect to the raspbian servers and update the local apt repository cache.

Saturday, April 3, 2021

Honey, I shrunk the Arduino core!


One of my gripes about the Arduino AVR core is that it is not an example of efficient embedded programming.  One of the foundations of C++ (PDF) is zero-overhead abstractions, yet the Arduino core has a very significant overhead.  The Arduino basic blink example compiles to almost 1kB, with most of that space taken up by code that is never used.  Rewriting the AVR core is a task I'm not ready to tackle, but after writing picoCore, I realized I could use many of the same optimization techniques in an Arduino library.  The result is ArduinoShrink, a library that can dramatically reduce the compiled size of Arduino projects.  In this post I'll explain some of the techniques I used to achieve the coding trifecta of faster, better, and smaller.

The Arduino core is actually a static library that is linked with the project code.  As Eli explains in this post on static linking, libraries like libc usually have only one function per .o in order to avoid linking in unnecessary code.  The Arduino doesn't use that kind of modular approach, however by making use of gcc's "-ffunction-sections" option, it does mitigate the amount of code bloat due to the non-modular approach.

With ArduinoShrink, I wrote more modular, self-contained code.  For example, the Arduino delay() function calls micros(), which relies on the 32-bit timer0 interrupt overflow counter.  I simplified the delay function so that it only needs the 8-bit timer value.  If the user code never calls micros() or millis(), the timer0 ISR code never gets linked in.  By using a more efficient algorithm and writing the code in AVR assembler, I reduced the size of the delay function to 12 instructions taking 24 bytes of flash.

In order to minimize code size and maximize speed, almost half of the code is in AVR assembler.  Despite improvements in compiler optimization techniques over the past decades, on architectures like the AVR I can almost always write better assembler code than what the compiler generates.  That's especially true for interrupt service routines, such as the timer0 interrupt used to maintain the counters for millis() and micros().  My assembler version of the interrupt uses only 56 bytes of flash, and is faster than the Arduino ISR written in C.

One part that is still written in C is the digitalWrite() function.  The Arduino core uses a set of tables in flash to map a given pin number to an IO port and bit, making for a lot of code to have digitalWrite(13, LOW) clear PORTB5.  Making use of Bill's discovery that these flash memory table lookups can be resolved at compile time, digitalWrite(13, LOW) compiles to a single instruction: "cbi PORTB, 5".

ArduinoShrink is also designed to significantly reduce interrupt latency.  The original timer0 interrupt takes around 5us to run, during which time any other interrupts are delayed.  The first instruction in my ISR is 'sei', which allows other interrupts to run, reducing the latency impact to a few cycles more than the hardware minimum.  The official Arduino core disables interrupts in several places, such as when reading the millis counter.  My solution is to detect if the millis counter has been updated and re-read it, thereby avoiding any interrupt latency impact.

The only limitation compared to the official AVR core is that the compiler must be able to resolve the pin number for the digital IO functions at compile time.  Although the pin may hard-coded, even with LTO enabled, avr-gcc is not always able to recognize the pin is a compile-time constant.  Since AVR is not a priority target for GCC optimizations, I can't rely on compiler improvements to resolve this limitation.  Therefore I plan to write a version of digitalWrite that is much smaller and faster, even when avr-gcc can't figure out the pin at compile time.

Although ArduinoShrink should be compatible with any Arduino sketch, given some of the compiler tricks I've used it's not unlikely I've missed a potential error.  If you do find what you think is a bug, open an issue in the github repository.

Tuesday, March 2, 2021

Writing USB firmware on the CH55x MCUs

Over the last several months, I've been familiarizing myself with the CH552 and CH551 MCUs.  Most recently, I've been learning how to program the USB serial interface engine on these devices.  The USB interface is powerful and flexible enough to implement many different kinds of USB devices, from HID to CDC serial.  The highlights are:

  • support for endpoints 0 through 4, both IN and OUT
  • 64-byte maximum packet size
  • DMA to/from xram only
  • multiple USB interrupt triggers
One of the first requirements for writing USB firmware is writing the descriptors.  The examples from WCH are difficult to use as a template due to the descriptors being uint8_t arrays instead of structures.  There are USB structure and constant definitions in ch554_usb.h, which I recommend using instead of arrays.  For instance, I changed the CDC serial example from :

__code uint8_t DevDesc[] = {0x12,0x01,0x10,0x01,0x02,0x00,0x00,DEFAULT_ENDP0_SIZE,

__code USB_DEV_DESCR DevDesc = {
.bLength = 18,
.bDescriptorType = USB_DESCR_TYP_DEVICE,
.bcdUSBH = 0x01, .bcdUSBL = 0x10,
.bDeviceSubClass = 0,
.bDeviceProtocol = 0,
.bMaxPacketSize0 = DEFAULT_ENDP0_SIZE,
.idVendorH = 0x1a, .idVendorL = 0x86,
.idProductH = 0x57, .idProductL = 0x22,
.bcdDeviceH = 0x01, .bcdDeviceL = 0x00,
.iManufacturer = 1, // string descriptors
.iProduct = 2,
.iSerialNumber = 0,
.bNumConfigurations = 1

Once the descriptors are written, the code to handle device enumeration is mostly boilerplate and can be copied from one of the examples.  During the firmware development stage, I recommend adding a call to disconnectUSB() near the start of main().  It's a function I added to debug.h which forces the host to re-enumerate the device.  This way I don't have to unplug and re-connect the USB module after flashing new firmware.

Setting up the DMA buffer pointers requires special attention when multiple IN and OUT endpoints are used.  Even though five endpoints are supported, there are only four DMA buffer pointer registers: UEP[0-3]_DMA.  When the bits bUEP4_RX_EN and bUEP4_TX_EN are set in the UEP4_1_MOD SFR, the EP4 OUT buffer is UEP0_DMA + 64, and the EP4 IN buffer is UEP0_DMA + 128.  Endpoints 1-3 have even more complex buffer configurations, with optional double-buffering for IN and OUT using 256 bytes for four buffers starting from the UEPn_DMA pointer.

When I first started writing USB firmware for the CH551 and CH552, I was concerned that it may be difficult to meet the tight timing requirements, particularly for control and bulk packets that can have multiple in a single 1ms frame.  For example, with small data packets, the time between the end of one OUT transfer and the end of the next OUT transfer can be less than 20uS.  If the USB interrupt handler is too slow, the 2nd OUT transfer could overwrite the DMA buffer before processing of the first has completed.  This situation is avoided by setting bUC_INT_BUSY in the USB_CTRL SFR.  When this bit is set, the SIE will NAK any packets while the UIF_TRANSFER flag is set.  Therefore I recommend setting bUC_INT_BUSY, and clear UIF_TRANSFER at the end of the interrupt handler.

I am currently working on the CMSIS_DAP example.  It implements the DAPv1 (HID) protocol supporting SWD transfers, and works well with OpenOCD and pyOCD.  I'm working on adding CDC/ACM for serial UART communication.  The first step is creating the descriptors for the composite CDC + HID device.  The second step will be integrating the usb_device_cdc code.  The final step, although not absolutely necessary, will be optimizing the CDC code for baud rates up to 1mbps.  The current code uses transmit and receive ring buffers with data copied to and from the IN and OUT DMA buffers.  With double-buffering, the transmit and receive ring buffers can be omitted.  The UART interrupt will copy directly between SBUF and the appropriate USB DMA buffer.

Tuesday, January 26, 2021

Quirks of the CH55x MCUs

Over the past several months, I've been been learning to use the CH551 and CH552 MCUs.  Learning generic 8051 programming was the easy part, as there is lots of old documentation available, with Philips having written some of the best.  The learning curve for WCH's additions to the MCS-51 architecture has been steeper, requiring careful reading of the datasheets, and reading the SDK headers and examples.  I've found that the CH55x chips have some quirks that I've never encountered on any other MCUs.

The GPIO modes are controlled by two registers: MOD_OC and DIR_PU.  The register values are explained in the datasheet and in ch554.h in the SDK.  Figure 10.2.1 in the datasheet shows a schematic diagram for the GPIO.  Modes 0, 1, and 2 are for high-Z input, push-pull, and open-drain respectively.  Mode 3, "standard 8051 mode" is the most complicated.  It's an open drain mode with internal pullup, but with the output driven high for two cycles when the GPIO changes from a 0 to a 1.  This ensures a fast signal rise time.  The part that took me the longest to figure out was the operation of the pullup.  The GPIO diagram shows 70k and 10k, but section 10 of the datasheet does not explain their operation.  Therefore I've highlighted a part of the schematic in green.  When the pin input schmitt trigger output is 1, the inverter in the top right of the diagram will output a low signal to turn on the pFET activating the 10k pullup.  When port input value is 0, only the weak 70k pullup is active.

The pullups aren't actually implemented as resistors on the IC.  They are specially-designed FETs with a high drain-source resistance (RDS).  Since RDS varies with gate-source voltage (Vgs), the pullup resistance will vary inversely with Vcc.  Using a 5V supply, the pullup resistance will be close to the 70k shown in the schematic.  Using a 3.3V supply, the pullup resistance is close to 125k.  Although it is not obvious, this information can be found in section 18 of the datasheet, with the specifications for IUP5 and IUP3.  These numbers are the amount of current a grounded pin will source when the pullup is enabled.

The reset pin has an internal pulldown, which seems to be weak like the GPIO pullups.  At times when working with a CH552 running at 3V3, the chip reset when I inadvertently touched the RST pin with my finger.  This was easily solved by keeping the RST pin shorted to ground.

The last issue I encountered is more of a documentation issue than a quirk.  The maximum reliable clock speed of an IC is depended on the supply voltage.  All of the AVR MCUs I've worked with have a graph in the datasheet showing the voltage required to ensure safe operation at a given speed.  For the CH55x MCUs, there is a subtle difference in the electrical specs at section 18 of the datasheet.  At 5V, total supply current at 24MHz is specified, whereas the specs for 3.3V specify total operating current at 16Mhz.  When I tried running a CH552T at 24MHz with a 3.3V supply, it never worked.  The same part worked perfectly at 16MHz.

Despite the quirks, I think the CH55x MCUs are still a good value.  Current quantity 10 pricing at LCSC is 36c for the CH552T, and 26c for the CH551G.  I recently purchased a small tube of the CH552T, and have plans to test the touch, ADC, PWM, and SPI peripherals.