├── tests ├── .gitignore ├── Kconfig ├── src │ ├── tests.h │ ├── daq_stub.h │ ├── main.cpp │ ├── tests_half_bridge.cpp │ ├── tests_power_port.cpp │ └── tests_device_status.cpp ├── prj.conf ├── CMakeLists.txt └── boards │ └── native_posix.overlay ├── docs ├── src │ ├── api │ │ ├── load.rst │ │ ├── daq.rst │ │ ├── dcdc.rst │ │ ├── pwm_switch.rst │ │ ├── bat_charger.rst │ │ ├── device_status.rst │ │ ├── half_bridge.rst │ │ ├── power_port.rst │ │ ├── data_objects.rst │ │ └── misc.rst │ ├── overview │ │ ├── charging-stages.png │ │ ├── features.rst │ │ ├── supported_hardware.rst │ │ └── charger_concepts.rst │ └── dev │ │ ├── unit_tests.rst │ │ ├── troubleshooting.rst │ │ ├── building_flashing.rst │ │ ├── workspace_setup.rst │ │ └── customization.rst ├── requirements.txt ├── static │ └── images │ │ ├── logo.png │ │ ├── favicon.ico │ │ └── cc-by-sa-centered.png ├── Doxyfile ├── README.md ├── Makefile ├── conf.py └── index.rst ├── boards └── arm │ ├── mppt_2420_hc │ ├── revision.cmake │ ├── mppt_2420_hc_0_1_0.conf │ ├── Kconfig.board │ ├── Kconfig.defconfig │ ├── mppt_2420_hc.yaml │ ├── mppt_2420_hc_0_2_0.conf │ ├── board.cmake │ ├── mppt_2420_hc_defconfig │ ├── mppt_2420_hc_0_1_0.overlay │ ├── mppt_2420_hc_0_2_0.overlay │ └── mppt_2420_hc.dts │ ├── mppt_1210_hus │ ├── mppt_1210_hus_0_4_0.conf │ ├── mppt_1210_hus_0_7_0.conf │ ├── revision.cmake │ ├── Kconfig.board │ ├── board.cmake │ ├── mppt_1210_hus_0_7_0.overlay │ ├── mppt_1210_hus.yaml │ ├── mppt_1210_hus_defconfig │ ├── Kconfig.defconfig │ ├── support │ │ └── openocd.cfg │ ├── mppt_1210_hus_0_4_0.overlay │ └── mppt_1210_hus.dts │ ├── pwm_2420_lus │ ├── pwm_2420_lus_0_2_0.conf │ ├── pwm_2420_lus_0_3_0.conf │ ├── revision.cmake │ ├── Kconfig.board │ ├── pwm_2420_lus_0_3_0.overlay │ ├── board.cmake │ ├── pwm_2420_lus.yaml │ ├── pwm_2420_lus_defconfig │ ├── Kconfig.defconfig │ ├── pwm_2420_lus_0_2_0.overlay │ ├── support │ │ └── openocd.cfg │ └── pwm_2420_lus.dts │ ├── nucleo_g474re │ ├── support │ │ └── openocd.cfg │ ├── Kconfig.board │ ├── Kconfig.defconfig │ ├── nucleo_g474re.yaml │ ├── board.cmake │ └── nucleo_g474re_defconfig │ ├── mppt_2420_hpx │ ├── Kconfig.board │ ├── mppt_2420_hpx.yaml │ ├── board.cmake │ ├── Kconfig.defconfig │ ├── mppt_2420_hpx_defconfig │ └── mppt_2420_hpx.dts │ ├── mppt_2420_lc │ ├── Kconfig.board │ ├── board.cmake │ ├── mppt_2420_lc.yaml │ ├── support │ │ └── openocd.cfg │ ├── Kconfig.defconfig │ ├── mppt_2420_lc_defconfig │ └── mppt_2420_lc.dts │ └── mppt_2420_rc │ ├── Kconfig.board │ ├── mppt_2420_rc.yaml │ ├── board.cmake │ ├── Kconfig.defconfig │ ├── mppt_2420_rc_defconfig │ └── mppt_2420_rc.dts ├── app ├── src │ ├── ext │ │ ├── CMakeLists.txt │ │ ├── README.md │ │ └── serial.cpp │ ├── hardware.h │ ├── data_storage.h │ ├── board.h │ ├── CMakeLists.txt │ ├── setup.h │ ├── power_port.cpp │ ├── data_objects.h │ ├── helper.h │ ├── half_bridge.h │ ├── mcu.h │ ├── hardware.cpp │ ├── setup.cpp │ ├── leds.h │ ├── pwm_switch.h │ ├── daq.h │ ├── pwm_switch_driver.c │ ├── device_status.h │ ├── pwm_switch.cpp │ ├── main.cpp │ ├── device_status.cpp │ ├── load.cpp │ └── load.h ├── .gitignore ├── CMakeLists.txt └── prj.conf ├── .gitignore ├── dts └── bindings │ ├── zephyr,dummy-adc.yaml │ ├── zephyr,dummy-pwm.yaml │ ├── half-bridge.yaml │ ├── charlieplexing-leds.yaml │ ├── outputs.yaml │ ├── adc-inputs.yaml │ └── charge-controller.yaml ├── .gitattributes ├── .clang-format ├── .github └── workflows │ ├── docs.yml │ └── zephyr.yml ├── west.yml ├── scripts ├── check.sh ├── check-style.sh ├── check.bat └── data_objects_parser.py ├── README.md ├── .vscode └── c_cpp_properties.json ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md /tests/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /tests/Kconfig: -------------------------------------------------------------------------------- 1 | 2 | rsource "../app/Kconfig" 3 | -------------------------------------------------------------------------------- /docs/src/api/load.rst: -------------------------------------------------------------------------------- 1 | Load Output 2 | =========== 3 | 4 | .. doxygenfile:: load.h 5 | :project: app 6 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | breathe 2 | sphinx~=5.3 3 | sphinx-notfound-page 4 | sphinx-rtd-theme 5 | sphinx-tabs 6 | -------------------------------------------------------------------------------- /docs/src/api/daq.rst: -------------------------------------------------------------------------------- 1 | Data Acquisition 2 | ================ 3 | 4 | .. doxygenfile:: daq.h 5 | :project: app 6 | -------------------------------------------------------------------------------- /docs/src/api/dcdc.rst: -------------------------------------------------------------------------------- 1 | DC/DC converter 2 | =============== 3 | 4 | .. doxygenfile:: dcdc.h 5 | :project: app 6 | -------------------------------------------------------------------------------- /docs/src/api/pwm_switch.rst: -------------------------------------------------------------------------------- 1 | PWM Switch 2 | ========== 3 | 4 | .. doxygenfile:: pwm_switch.h 5 | :project: app 6 | -------------------------------------------------------------------------------- /docs/static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibreSolar/charge-controller-firmware/HEAD/docs/static/images/logo.png -------------------------------------------------------------------------------- /docs/src/api/bat_charger.rst: -------------------------------------------------------------------------------- 1 | Battery Charger 2 | =============== 3 | 4 | .. doxygenfile:: bat_charger.h 5 | :project: app 6 | -------------------------------------------------------------------------------- /docs/src/api/device_status.rst: -------------------------------------------------------------------------------- 1 | Device Status 2 | ============= 3 | 4 | .. doxygenfile:: device_status.h 5 | :project: app 6 | -------------------------------------------------------------------------------- /docs/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibreSolar/charge-controller-firmware/HEAD/docs/static/images/favicon.ico -------------------------------------------------------------------------------- /boards/arm/mppt_2420_hc/revision.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | board_check_revision(FORMAT MAJOR.MINOR.PATCH) 4 | -------------------------------------------------------------------------------- /docs/src/api/half_bridge.rst: -------------------------------------------------------------------------------- 1 | Half Bridge Driver 2 | ================== 3 | 4 | .. doxygenfile:: half_bridge.h 5 | :project: app 6 | -------------------------------------------------------------------------------- /docs/src/api/power_port.rst: -------------------------------------------------------------------------------- 1 | DC Bus and Power Port 2 | ===================== 3 | 4 | .. doxygenfile:: power_port.h 5 | :project: app 6 | -------------------------------------------------------------------------------- /boards/arm/mppt_1210_hus/mppt_1210_hus_0_4_0.conf: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # empty (using defaults from _defconfig file) 4 | -------------------------------------------------------------------------------- /boards/arm/mppt_1210_hus/mppt_1210_hus_0_7_0.conf: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # empty (using defaults from _defconfig file) 4 | -------------------------------------------------------------------------------- /boards/arm/pwm_2420_lus/pwm_2420_lus_0_2_0.conf: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # empty (using defaults from _defconfig file) 4 | -------------------------------------------------------------------------------- /boards/arm/pwm_2420_lus/pwm_2420_lus_0_3_0.conf: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # empty (using defaults from _defconfig file) 4 | -------------------------------------------------------------------------------- /docs/src/overview/charging-stages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibreSolar/charge-controller-firmware/HEAD/docs/src/overview/charging-stages.png -------------------------------------------------------------------------------- /docs/static/images/cc-by-sa-centered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LibreSolar/charge-controller-firmware/HEAD/docs/static/images/cc-by-sa-centered.png -------------------------------------------------------------------------------- /boards/arm/mppt_2420_hc/mppt_2420_hc_0_1_0.conf: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | CONFIG_I2C=y 4 | CONFIG_EEPROM=y 5 | CONFIG_EEPROM_AT24=y 6 | -------------------------------------------------------------------------------- /app/src/ext/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | target_sources(app PRIVATE 4 | oled.cpp 5 | serial.cpp 6 | can.cpp 7 | ) 8 | -------------------------------------------------------------------------------- /boards/arm/pwm_2420_lus/revision.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | board_check_revision( 4 | FORMAT MAJOR.MINOR.PATCH 5 | DEFAULT_REVISION 0.3 6 | ) 7 | -------------------------------------------------------------------------------- /boards/arm/mppt_1210_hus/revision.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | board_check_revision( 4 | FORMAT MAJOR.MINOR.PATCH 5 | DEFAULT_REVISION 0.7.1 6 | ) 7 | -------------------------------------------------------------------------------- /boards/arm/nucleo_g474re/support/openocd.cfg: -------------------------------------------------------------------------------- 1 | source [find interface/stlink.cfg] 2 | 3 | transport select hla_swd 4 | 5 | source [find target/stm32g4x.cfg] 6 | 7 | reset_config srst_only 8 | -------------------------------------------------------------------------------- /app/src/ext/README.md: -------------------------------------------------------------------------------- 1 | # Folder src/ext 2 | 3 | This folder contains code for all **ext**ensions (displays etc. connected to UEXT port) and **ext**ernal communication interfaces (UART or CAN). 4 | -------------------------------------------------------------------------------- /boards/arm/mppt_1210_hus/Kconfig.board: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | config BOARD_MPPT_1210_HUS 5 | bool "Libre Solar MPPT 1210 HUS" 6 | depends on SOC_STM32L072XX 7 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_hc/Kconfig.board: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | config BOARD_MPPT_2420_HC 5 | bool "Libre Solar MPPT 2420 HC" 6 | depends on SOC_STM32G431XX 7 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_hpx/Kconfig.board: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | config BOARD_MPPT_2420_HPX 5 | bool "Libre Solar MPPT 2420 HPX" 6 | depends on SOC_STM32G431XX 7 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_lc/Kconfig.board: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | config BOARD_MPPT_2420_LC 5 | bool "Libre Solar MPPT 2420 LC" 6 | depends on SOC_STM32F072XB 7 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_rc/Kconfig.board: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | config BOARD_MPPT_2420_RC 5 | bool "Libre Solar MPPT 2420 RC" 6 | depends on SOC_STM32G431XX 7 | -------------------------------------------------------------------------------- /boards/arm/pwm_2420_lus/Kconfig.board: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | config BOARD_PWM_2420_LUS 5 | bool "Libre Solar PWM 2420 LUS" 6 | depends on SOC_STM32L072XX 7 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # custom DTS overlays 3 | *.overlay 4 | 5 | # custom project configuration per board 6 | prj_*.conf 7 | 8 | # alternative location for custom DTS overlays or conf files 9 | boards* 10 | 11 | # folders generated by west 12 | build 13 | -------------------------------------------------------------------------------- /boards/arm/pwm_2420_lus/pwm_2420_lus_0_3_0.overlay: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | / { 8 | pcb { 9 | version-str = "v0.3"; 10 | version-num = <3>; 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # backup files 2 | *.bak 3 | *_bak 4 | 5 | # folders generated by west 6 | build 7 | 8 | # editors 9 | .vscode/* 10 | !.vscode/c_cpp_properties.json 11 | *.swp 12 | *~ 13 | 14 | # others 15 | .clang_complete 16 | .gcc-flags.json 17 | .pio 18 | bin 19 | html 20 | -------------------------------------------------------------------------------- /boards/arm/nucleo_g474re/Kconfig.board: -------------------------------------------------------------------------------- 1 | # STM32G474RE Nucleo board configuration 2 | 3 | # Copyright (c) 2019 STMicroelectronics. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | config BOARD_NUCLEO_G474RE 7 | bool "Nucleo G474RE Development Board" 8 | depends on SOC_STM32G474XX 9 | -------------------------------------------------------------------------------- /boards/arm/mppt_1210_hus/board.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | board_runner_args(jlink "--device=STM32L072RZ" "--speed=4000" "--reset-after-load") 4 | 5 | include(${ZEPHYR_BASE}/boards/common/openocd.board.cmake) 6 | include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) 7 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_lc/board.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | board_runner_args(jlink "--device=STM32F072CB" "--speed=4000" "--reset-after-load") 4 | 5 | include(${ZEPHYR_BASE}/boards/common/openocd.board.cmake) 6 | include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) 7 | -------------------------------------------------------------------------------- /boards/arm/pwm_2420_lus/board.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | board_runner_args(jlink "--device=STM32L072RZ" "--speed=4000" "--reset-after-load") 4 | 5 | include(${ZEPHYR_BASE}/boards/common/openocd.board.cmake) 6 | include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) 7 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_hc/Kconfig.defconfig: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | if BOARD_MPPT_2420_HC 5 | 6 | config BOARD 7 | default "mppt_2420_hc" 8 | 9 | config LIBRE_SOLAR_TYPE_ID 10 | default 8 11 | 12 | endif # BOARD_MPPT_2420_HC 13 | -------------------------------------------------------------------------------- /boards/arm/mppt_1210_hus/mppt_1210_hus_0_7_0.overlay: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | / { 8 | pcb { 9 | version-str = "v0.7.1"; 10 | version-num = <7>; // minor version .1 includes no HW change 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_lc/mppt_2420_lc.yaml: -------------------------------------------------------------------------------- 1 | identifier: mppt_2420_lc 2 | name: MPPT-2420-LC 3 | type: mcu 4 | arch: arm 5 | toolchain: 6 | - zephyr 7 | - gnuarmemb 8 | - xtools 9 | ram: 16 10 | flash: 128 11 | supported: 12 | - arduino_i2c 13 | - gpio 14 | - i2c 15 | - spi 16 | - watchdog 17 | - adc 18 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_hc/mppt_2420_hc.yaml: -------------------------------------------------------------------------------- 1 | identifier: mppt_2420_hc 2 | name: MPPT-2420-HC 3 | type: mcu 4 | arch: arm 5 | toolchain: 6 | - zephyr 7 | - gnuarmemb 8 | - xtools 9 | ram: 32 10 | flash: 128 11 | supported: 12 | - nvs 13 | - pwm 14 | - i2c 15 | - gpio 16 | - usb device 17 | - counter 18 | - spi 19 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_rc/mppt_2420_rc.yaml: -------------------------------------------------------------------------------- 1 | identifier: mppt_2420_rc 2 | name: MPPT-2420-RC 3 | type: mcu 4 | arch: arm 5 | toolchain: 6 | - zephyr 7 | - gnuarmemb 8 | - xtools 9 | ram: 32 10 | flash: 128 11 | supported: 12 | - nvs 13 | - pwm 14 | - i2c 15 | - gpio 16 | - usb device 17 | - counter 18 | - spi 19 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_hpx/mppt_2420_hpx.yaml: -------------------------------------------------------------------------------- 1 | identifier: mppt_2420_hpx 2 | name: MPPT-2420-HPX 3 | type: mcu 4 | arch: arm 5 | toolchain: 6 | - zephyr 7 | - gnuarmemb 8 | - xtools 9 | ram: 32 10 | flash: 128 11 | supported: 12 | - nvs 13 | - pwm 14 | - i2c 15 | - gpio 16 | - usb device 17 | - counter 18 | - spi 19 | -------------------------------------------------------------------------------- /boards/arm/pwm_2420_lus/pwm_2420_lus.yaml: -------------------------------------------------------------------------------- 1 | identifier: pwm_2420_lus 2 | name: PWM-2420-LUS 3 | type: mcu 4 | arch: arm 5 | toolchain: 6 | - zephyr 7 | - gnuarmemb 8 | - xtools 9 | ram: 20 10 | flash: 192 11 | supported: 12 | - arduino_i2c 13 | - gpio 14 | - i2c 15 | - spi 16 | - watchdog 17 | - adc 18 | - eeprom 19 | -------------------------------------------------------------------------------- /boards/arm/mppt_1210_hus/mppt_1210_hus.yaml: -------------------------------------------------------------------------------- 1 | identifier: mppt_1210_hus 2 | name: MPPT-1210-HUS 3 | type: mcu 4 | arch: arm 5 | toolchain: 6 | - zephyr 7 | - gnuarmemb 8 | - xtools 9 | ram: 20 10 | flash: 192 11 | supported: 12 | - arduino_i2c 13 | - gpio 14 | - i2c 15 | - spi 16 | - watchdog 17 | - adc 18 | - eeprom 19 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_hpx/board.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | board_runner_args(pyocd "--target=stm32g431rbtx") 4 | board_runner_args(jlink "--device=STM32G431RB" "--speed=4000" "--reset-after-load") 5 | 6 | include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake) 7 | include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) 8 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_rc/board.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | board_runner_args(pyocd "--target=stm32g431rbtx") 4 | board_runner_args(jlink "--device=STM32G431RB" "--speed=4000" "--reset-after-load") 5 | 6 | include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake) 7 | include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) 8 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_hc/mppt_2420_hc_0_2_0.conf: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | CONFIG_FLASH=y 4 | CONFIG_FLASH_PAGE_LAYOUT=y 5 | CONFIG_FLASH_LOG_LEVEL_WRN=y 6 | 7 | CONFIG_SPI=y 8 | CONFIG_SPI_NOR=y 9 | CONFIG_SPI_NOR_SFDP_RUNTIME=y 10 | CONFIG_SPI_NOR_FLASH_LAYOUT_PAGE_SIZE=4096 11 | 12 | CONFIG_NVS=y 13 | CONFIG_NVS_LOG_LEVEL_WRN=y 14 | -------------------------------------------------------------------------------- /boards/arm/nucleo_g474re/Kconfig.defconfig: -------------------------------------------------------------------------------- 1 | # STM32G474RE Nucleo board configuration 2 | 3 | # Copyright (c) 2019 STMicroelectronics. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | if BOARD_NUCLEO_G474RE 7 | 8 | config BOARD 9 | default "nucleo_g474re" 10 | 11 | config LIBRE_SOLAR_TYPE_ID 12 | default 0 13 | 14 | endif # BOARD_NUCLEO_G431RB 15 | -------------------------------------------------------------------------------- /docs/src/api/data_objects.rst: -------------------------------------------------------------------------------- 1 | Data Objects 2 | ============ 3 | 4 | The charge controller uses the [ThingSet protocol](https://thingset.github.io/) for communication 5 | with external devices. Internal data that should be exposed via the protocol is defined in the 6 | Data Objects module. 7 | 8 | .. doxygenfile:: data_objects.h 9 | :project: app 10 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_lc/support/openocd.cfg: -------------------------------------------------------------------------------- 1 | source [find board/st_nucleo_f0.cfg] 2 | 3 | $_TARGETNAME configure -event gdb-attach { 4 | echo "Debugger attaching: halting execution" 5 | reset halt 6 | gdb_breakpoint_override hard 7 | } 8 | 9 | $_TARGETNAME configure -event gdb-detach { 10 | echo "Debugger detaching: resuming execution" 11 | resume 12 | } 13 | -------------------------------------------------------------------------------- /boards/arm/pwm_2420_lus/pwm_2420_lus_defconfig: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | CONFIG_SOC_SERIES_STM32L0X=y 4 | CONFIG_SOC_STM32L072XX=y 5 | 6 | CONFIG_CLOCK_CONTROL=y 7 | 8 | CONFIG_PINCTRL=y 9 | 10 | CONFIG_GPIO=y 11 | 12 | CONFIG_SERIAL=y 13 | 14 | CONFIG_CONSOLE=y 15 | CONFIG_UART_CONSOLE=y 16 | 17 | CONFIG_EEPROM=y 18 | CONFIG_EEPROM_STM32=y 19 | -------------------------------------------------------------------------------- /boards/arm/mppt_1210_hus/mppt_1210_hus_defconfig: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | CONFIG_SOC_SERIES_STM32L0X=y 4 | CONFIG_SOC_STM32L072XX=y 5 | 6 | CONFIG_CLOCK_CONTROL=y 7 | 8 | CONFIG_PINCTRL=y 9 | 10 | CONFIG_GPIO=y 11 | 12 | CONFIG_SERIAL=y 13 | 14 | CONFIG_CONSOLE=y 15 | CONFIG_UART_CONSOLE=y 16 | 17 | CONFIG_EEPROM=y 18 | CONFIG_EEPROM_STM32=y 19 | -------------------------------------------------------------------------------- /docs/Doxyfile: -------------------------------------------------------------------------------- 1 | # Doxygen configuration 2 | # 3 | # Only contains most important options, as HTML rendering is done by Sphinx 4 | 5 | INPUT = ../app/src 6 | OUTPUT_DIRECTORY = build/doxygen 7 | RECURSIVE = YES 8 | GENERATE_HTML = NO 9 | GENERATE_LATEX = NO 10 | GENERATE_XML = YES 11 | GENERATE_TREEVIEW = YES 12 | ENABLE_PREPROCESSING = NO 13 | -------------------------------------------------------------------------------- /dts/bindings/zephyr,dummy-adc.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | description: ADC similar to STM32 for unit tests 5 | 6 | compatible: "zephyr,dummy-adc" 7 | 8 | include: adc-controller.yaml 9 | 10 | properties: 11 | "#io-channel-cells": 12 | const: 1 13 | 14 | io-channel-cells: 15 | - input 16 | -------------------------------------------------------------------------------- /boards/arm/nucleo_g474re/nucleo_g474re.yaml: -------------------------------------------------------------------------------- 1 | identifier: nucleo_g474re 2 | name: ST Nucleo G474RE 3 | type: mcu 4 | arch: arm 5 | toolchain: 6 | - zephyr 7 | - gnuarmemb 8 | - xtools 9 | ram: 128 10 | flash: 512 11 | supported: 12 | - nvs 13 | - pwm 14 | - i2c 15 | - gpio 16 | - usb device 17 | - counter 18 | - spi 19 | - watchdog 20 | - adc 21 | - can 22 | - canfd 23 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_hc/board.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | board_runner_args(pyocd "--target=stm32g431rbtx") 4 | board_runner_args(jlink "--device=STM32G431RB" "--speed=4000" "--reset-after-load") 5 | 6 | include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake) 7 | include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake) 8 | include(${ZEPHYR_BASE}/boards/common/openocd.board.cmake) 9 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_hpx/Kconfig.defconfig: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | if BOARD_MPPT_2420_HPX 5 | 6 | config BOARD 7 | default "mppt_2420_hpx" 8 | 9 | config LIBRE_SOLAR_TYPE_ID 10 | default 7 11 | 12 | if SPI 13 | 14 | config SPI_STM32_INTERRUPT 15 | default y 16 | 17 | endif # SPI 18 | 19 | endif # BOARD_MPPT_2420_HPX 20 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_lc/Kconfig.defconfig: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | if BOARD_MPPT_2420_LC 5 | 6 | config BOARD 7 | default "mppt_2420_lc" 8 | 9 | config LIBRE_SOLAR_TYPE_ID 10 | default 1 11 | 12 | if SPI 13 | 14 | config SPI_STM32_INTERRUPT 15 | default y 16 | 17 | endif # SPI 18 | 19 | endif # BOARD_MPPT_2420_LC_0V10 20 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_rc/Kconfig.defconfig: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | if BOARD_MPPT_2420_RC 5 | 6 | config BOARD 7 | default "mppt_2420_rc" 8 | 9 | config LIBRE_SOLAR_TYPE_ID 10 | default 0 11 | 12 | 13 | if SPI 14 | 15 | config SPI_STM32_INTERRUPT 16 | default y 17 | 18 | endif # SPI 19 | 20 | endif # BOARD_MPPT_2420_RC 21 | -------------------------------------------------------------------------------- /boards/arm/pwm_2420_lus/Kconfig.defconfig: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | if BOARD_PWM_2420_LUS 5 | 6 | config BOARD 7 | default "pwm_2420_lus" 8 | 9 | config LIBRE_SOLAR_TYPE_ID 10 | default 5 11 | 12 | if SPI 13 | 14 | config SPI_STM32_INTERRUPT 15 | default y 16 | 17 | endif # SPI 18 | 19 | 20 | endif # BOARD_PWM_2420_LUS 21 | -------------------------------------------------------------------------------- /boards/arm/mppt_1210_hus/Kconfig.defconfig: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | if BOARD_MPPT_1210_HUS 5 | 6 | config BOARD 7 | default "mppt_1210_hus" 8 | 9 | config LIBRE_SOLAR_TYPE_ID 10 | default 4 11 | 12 | if SPI 13 | 14 | config SPI_STM32_INTERRUPT 15 | default y 16 | 17 | endif # SPI 18 | 19 | endif # BOARD_MPPT_1210_HUS_0V7 20 | -------------------------------------------------------------------------------- /dts/bindings/zephyr,dummy-pwm.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | description: PWM similar to STM32 for unit tests 5 | 6 | compatible: "zephyr,dummy-pwm" 7 | 8 | include: [pwm-controller.yaml, base.yaml] 9 | 10 | properties: 11 | "#pwm-cells": 12 | const: 3 13 | 14 | pwm-cells: 15 | - channel 16 | # period in terms of nanoseconds 17 | - period 18 | - flags 19 | -------------------------------------------------------------------------------- /boards/arm/nucleo_g474re/board.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | # use target=stm32g474rbtx instead of stm32g474retx 4 | # to allow board re-flashing (see PR #23230) 5 | board_runner_args(pyocd "--target=stm32g474rbtx") 6 | 7 | board_runner_args(jlink "--device=STM32G474RE" "--speed=4000" "--reset-after-load") 8 | 9 | include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake) 10 | include(${ZEPHYR_BASE}/boards/common/openocd.board.cmake) 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_hpx/mppt_2420_hpx_defconfig: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | CONFIG_SOC_SERIES_STM32G4X=y 4 | CONFIG_SOC_STM32G431XX=y 5 | 6 | CONFIG_ARM_MPU=y 7 | CONFIG_HW_STACK_PROTECTION=y 8 | 9 | CONFIG_CLOCK_CONTROL=y 10 | 11 | CONFIG_PINCTRL=y 12 | 13 | CONFIG_GPIO=y 14 | 15 | CONFIG_SERIAL=y 16 | 17 | CONFIG_CONSOLE=y 18 | CONFIG_UART_CONSOLE=y 19 | 20 | CONFIG_PWM=y 21 | 22 | CONFIG_I2C=y 23 | CONFIG_EEPROM=y 24 | CONFIG_EEPROM_AT24=y 25 | -------------------------------------------------------------------------------- /docs/src/api/misc.rst: -------------------------------------------------------------------------------- 1 | Misc 2 | ==== 3 | 4 | Data Storage 5 | ------------ 6 | 7 | .. doxygenfile:: data_storage.h 8 | :project: app 9 | 10 | Hardware 11 | -------- 12 | 13 | .. doxygenfile:: hardware.h 14 | :project: app 15 | 16 | Helper 17 | ------ 18 | 19 | .. doxygenfile:: helper.h 20 | :project: app 21 | 22 | LEDs 23 | ---- 24 | 25 | .. doxygenfile:: leds.h 26 | :project: app 27 | 28 | Setup 29 | ----- 30 | 31 | .. doxygenfile:: setup.h 32 | :project: app 33 | -------------------------------------------------------------------------------- /tests/src/tests.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | int bat_charger_tests(); 11 | 12 | int daq_tests(); 13 | 14 | int power_port_tests(); 15 | 16 | int half_bridge_tests(); 17 | 18 | int dcdc_tests(); 19 | 20 | int device_status_tests(); 21 | 22 | int load_tests(); 23 | 24 | #ifdef CONFIG_CUSTOM_TESTS 25 | int custom_tests(); 26 | #endif 27 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_lc/mppt_2420_lc_defconfig: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | CONFIG_SOC_SERIES_STM32F0X=y 4 | CONFIG_SOC_STM32F072XB=y 5 | 6 | CONFIG_CLOCK_CONTROL=y 7 | 8 | CONFIG_PINCTRL=y 9 | 10 | CONFIG_GPIO=y 11 | 12 | CONFIG_SERIAL=y 13 | 14 | CONFIG_CONSOLE=y 15 | CONFIG_UART_CONSOLE=y 16 | 17 | CONFIG_I2C=y 18 | CONFIG_EEPROM=y 19 | CONFIG_EEPROM_AT24=y 20 | 21 | # too little RAM, can only be enabled if serial is disabled 22 | #CONFIG_CAN=y 23 | #CONFIG_THINGSET_CAN=y 24 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_hc/mppt_2420_hc_defconfig: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | CONFIG_SOC_SERIES_STM32G4X=y 4 | CONFIG_SOC_STM32G431XX=y 5 | 6 | CONFIG_ARM_MPU=y 7 | CONFIG_HW_STACK_PROTECTION=y 8 | CONFIG_MPU_ALLOW_FLASH_WRITE=y 9 | 10 | CONFIG_CLOCK_CONTROL=y 11 | 12 | CONFIG_PINCTRL=y 13 | 14 | CONFIG_GPIO=y 15 | 16 | CONFIG_SERIAL=y 17 | 18 | CONFIG_CONSOLE=y 19 | CONFIG_UART_CONSOLE=y 20 | 21 | CONFIG_PWM=y 22 | 23 | CONFIG_CAN=y 24 | CONFIG_THINGSET_CAN=y 25 | 26 | CONFIG_ISOTP=y 27 | -------------------------------------------------------------------------------- /boards/arm/pwm_2420_lus/pwm_2420_lus_0_2_0.overlay: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | / { 8 | pcb { 9 | version-str = "v0.2"; 10 | version-num = <2>; 11 | }; 12 | 13 | adc-inputs { 14 | compatible = "adc-inputs"; 15 | 16 | i-load { 17 | io-channels = <&adc1 5>; 18 | // amp gain: 68/2.2, resistor: 2 mOhm 19 | multiplier = <1000>; 20 | divider = <62>; // 2*68/2.2 = 61.8181 21 | }; 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_rc/mppt_2420_rc_defconfig: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | CONFIG_SOC_SERIES_STM32G4X=y 4 | CONFIG_SOC_STM32G431XX=y 5 | 6 | CONFIG_ARM_MPU=y 7 | CONFIG_HW_STACK_PROTECTION=y 8 | 9 | CONFIG_CLOCK_CONTROL=y 10 | 11 | CONFIG_PINCTRL=y 12 | 13 | CONFIG_GPIO=y 14 | 15 | CONFIG_SERIAL=y 16 | 17 | CONFIG_CONSOLE=y 18 | CONFIG_UART_CONSOLE=y 19 | 20 | CONFIG_I2C=y 21 | CONFIG_EEPROM=y 22 | CONFIG_EEPROM_AT24=y 23 | 24 | CONFIG_CAN=y 25 | CONFIG_THINGSET_CAN=y 26 | 27 | CONFIG_ISOTP=y 28 | -------------------------------------------------------------------------------- /dts/bindings/half-bridge.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | description: Half bridge, consisting of two coupled PWM outputs 5 | 6 | compatible: "half-bridge" 7 | 8 | include: [base.yaml, pinctrl-device.yaml] 9 | 10 | properties: 11 | frequency: 12 | type: int 13 | required: true 14 | description: PWM frequency in Hz 15 | 16 | deadtime: 17 | type: int 18 | required: true 19 | description: Dead time for synchronous PWM generation in nanoseconds 20 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Libre Solar Charge Controller Firmware 2 | 3 | ## Charge controller basic concepts 4 | 5 | [charger-concepts.md](charger-concepts.md) 6 | 7 | ## Firmware module overview 8 | 9 | [module-overview.md](module-overview.md) 10 | 11 | ## API documentation 12 | 13 | The documentation auto-generated by Doxygen can be found [here](https://libre.solar/charge-controller-firmware/). 14 | 15 | ## Further information 16 | 17 | [Zephyr Project documentation](https://docs.zephyrproject.org/latest/) 18 | 19 | [Zephyr / West cheatsheet](zephyr-cheatsheet.md) 20 | -------------------------------------------------------------------------------- /boards/arm/nucleo_g474re/nucleo_g474re_defconfig: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | CONFIG_SOC_SERIES_STM32G4X=y 4 | CONFIG_SOC_STM32G474XX=y 5 | 6 | # enable uart driver 7 | CONFIG_SERIAL=y 8 | 9 | # enable pinmux 10 | CONFIG_PINCTRL=y 11 | 12 | # enable GPIO 13 | CONFIG_GPIO=y 14 | 15 | # Enable Clocks 16 | CONFIG_CLOCK_CONTROL=y 17 | 18 | # Console 19 | CONFIG_CONSOLE=y 20 | CONFIG_UART_CONSOLE=y 21 | 22 | # Enable MPU 23 | CONFIG_ARM_MPU=y 24 | 25 | # Enable HW stack protection 26 | CONFIG_HW_STACK_PROTECTION=y 27 | 28 | CONFIG_CAN=y 29 | CONFIG_THINGSET_CAN=y 30 | 31 | CONFIG_ISOTP=y 32 | -------------------------------------------------------------------------------- /dts/bindings/charlieplexing-leds.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | description: Description for an ADC input including gain (multiplier/divider) and offset 5 | 6 | compatible: "charlieplexing-leds" 7 | 8 | properties: 9 | gpios: 10 | type: phandle-array 11 | description: GPIO handle of pin 12 | 13 | child-binding: 14 | description: GPIO child node 15 | properties: 16 | states: 17 | type: array 18 | description: | 19 | Pin states to switch on LED identified by child node. 1=on, 0=off, 2=floating 20 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | # 3 | # Makefile to build documentation using Doxygen, Sphinx and Breathe 4 | 5 | BUILDDIR = build 6 | SPHINXBUILD = sphinx-build 7 | SPHINXOPTS = -j auto 8 | 9 | SPHINX_INSTALLED := $(shell command -v $(SPHINXBUILD) 2> /dev/null) 10 | 11 | ifndef SPHINX_INSTALLED 12 | $(error '$(SPHINXBUILD)' executable was not found. Please install from https://www.sphinx-doc.org) 13 | endif 14 | 15 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . 16 | 17 | .PHONY: clean html 18 | 19 | html: 20 | mkdir -p build/doxygen 21 | doxygen 22 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 23 | 24 | clean: 25 | rm -rf $(BUILDDIR)/* 26 | -------------------------------------------------------------------------------- /docs/src/overview/features.rst: -------------------------------------------------------------------------------- 1 | Features 2 | ======== 3 | 4 | - Running on `Zephyr RTOS`_ 5 | 6 | - STM32F0, STM32L0 and STM32G4 series MCUs 7 | 8 | - Different battery types supported (lead-acid, Li-ion, LiFePO4) 9 | 10 | - Maximum power point tracking (MPPT) algorithm based on Perturb and Observe (P&O) 11 | 12 | - PWM solar input (for certain hardware) 13 | 14 | - Load output with deep-discharge protection 15 | 16 | - Monitoring and configuration using the `ThingSet`_ protocol (mapping to MQTT, CoAP and HTTP 17 | possible) 18 | 19 | - Serial interface 20 | - CAN bus 21 | 22 | - OLED display support 23 | 24 | .. _Zephyr RTOS: https://docs.zephyrproject.org 25 | .. _ThingSet: https://thingset.io 26 | -------------------------------------------------------------------------------- /docs/src/dev/unit_tests.rst: -------------------------------------------------------------------------------- 1 | Unit Tests 2 | ========== 3 | 4 | Core functions of the firmware are covered by unit-tests (located in ``tests`` directory). Build 5 | and run the tests with the following command: 6 | 7 | .. code-block:: bash 8 | 9 | cd tests 10 | west build -p -b native_posix -t run 11 | 12 | 13 | Conformance testing 14 | ------------------- 15 | 16 | We are using Continuous Integration to perform several checks for each commit or pull-request. In 17 | order to run the same tests locally (recommended before pushing to the server) use the script 18 | ``check.sh`` or ``check.bat`` in the ``scripts`` folder: 19 | 20 | .. code-block:: bash 21 | 22 | ./scripts/check.sh # use check.bat on Windows 23 | -------------------------------------------------------------------------------- /tests/src/daq_stub.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef DAQ_STUB_H_ 8 | #define DAQ_STUB_H_ 9 | 10 | /** Data to fill adc_filtered array during unit-tests 11 | */ 12 | typedef struct 13 | { 14 | float solar_voltage; 15 | float battery_voltage; 16 | float dcdc_current; 17 | float load_current; 18 | float bat_temperature; 19 | float internal_temperature; 20 | } AdcValues; 21 | 22 | void prepare_adc_readings(AdcValues values); 23 | 24 | void prepare_adc_filtered(); 25 | void clear_adc_filtered(); 26 | uint32_t get_adc_filtered(uint32_t channel); 27 | uint16_t adc_raw_clamp(float scale, float limit); 28 | 29 | #endif 30 | -------------------------------------------------------------------------------- /boards/arm/pwm_2420_lus/support/openocd.cfg: -------------------------------------------------------------------------------- 1 | 2 | source [find interface/stlink.cfg] 3 | 4 | transport select hla_swd 5 | 6 | set WORKAREASIZE 0x2000 7 | 8 | source [find target/stm32l0.cfg] 9 | 10 | # Add the second flash bank. 11 | set _FLASHNAME $_CHIPNAME.flash1 12 | flash bank $_FLASHNAME stm32lx 0 0 0 0 $_TARGETNAME 13 | 14 | # There is only system reset line and JTAG/SWD command can be issued when SRST 15 | reset_config srst_only 16 | 17 | $_TARGETNAME configure -event gdb-attach { 18 | echo "Debugger attaching: halting execution" 19 | reset halt 20 | gdb_breakpoint_override hard 21 | } 22 | 23 | $_TARGETNAME configure -event gdb-detach { 24 | echo "Debugger detaching: resuming execution" 25 | resume 26 | } 27 | -------------------------------------------------------------------------------- /app/src/hardware.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef HARDWARE_H 8 | #define HARDWARE_H 9 | 10 | /** @file 11 | * 12 | * @brief 13 | * Hardware-specific functions like timers, watchdog, bootloader 14 | */ 15 | 16 | /** DC/DC or PWM control loop (implemented in main.cpp) 17 | */ 18 | void system_control(); 19 | 20 | /** Reset device and start STM32 internal bootloader 21 | */ 22 | void start_stm32_bootloader(); 23 | 24 | /** 25 | * Reset device 26 | */ 27 | void reset_device(); 28 | 29 | /** 30 | * Callback for task watchdogs used in multiple threads 31 | */ 32 | void task_wdt_callback(int channel_id, void *user_data); 33 | 34 | #endif /* HARDWARE_H */ -------------------------------------------------------------------------------- /boards/arm/mppt_1210_hus/support/openocd.cfg: -------------------------------------------------------------------------------- 1 | # STM32L072 MCU 2 | 3 | source [find interface/stlink.cfg] 4 | 5 | transport select hla_swd 6 | 7 | set WORKAREASIZE 0x2000 8 | 9 | source [find target/stm32l0.cfg] 10 | 11 | # Add the second flash bank. 12 | set _FLASHNAME $_CHIPNAME.flash1 13 | flash bank $_FLASHNAME stm32lx 0 0 0 0 $_TARGETNAME 14 | 15 | # There is only system reset line and JTAG/SWD command can be issued when SRST 16 | reset_config srst_only 17 | 18 | $_TARGETNAME configure -event gdb-attach { 19 | echo "Debugger attaching: halting execution" 20 | reset halt 21 | gdb_breakpoint_override hard 22 | } 23 | 24 | $_TARGETNAME configure -event gdb-detach { 25 | echo "Debugger detaching: resuming execution" 26 | resume 27 | } 28 | -------------------------------------------------------------------------------- /app/src/data_storage.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef DATA_STORAGE_H_ 8 | #define DATA_STORAGE_H_ 9 | 10 | #define DATA_UPDATE_INTERVAL (6 * 60 * 60) // update every 6 hours 11 | 12 | /** 13 | * @file 14 | * 15 | * @brief Handling of internal or external EEPROM to store device configuration 16 | */ 17 | 18 | /** 19 | * Store current charge controller data to EEPROM 20 | */ 21 | void data_storage_write(); 22 | 23 | /** 24 | * Restore charge controller data from EEPROM and write to variables in RAM 25 | */ 26 | void data_storage_read(); 27 | 28 | /** 29 | * Stores data to EEPROM every 6 hours (can be called regularly) 30 | */ 31 | void data_storage_update(); 32 | 33 | #endif /* DATA_STORAGE_H_ */ 34 | -------------------------------------------------------------------------------- /app/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | set(BOARD_ROOT ${CMAKE_SOURCE_DIR}/..) 5 | set(DTS_ROOT ${CMAKE_SOURCE_DIR}/..) 6 | 7 | cmake_minimum_required(VERSION 3.13.1) 8 | find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) 9 | 10 | project(libre_solar_charge_controller) 11 | 12 | add_subdirectory(src) 13 | 14 | # determine git tag and commit hash for automatic firmware versioning 15 | find_package(Git) 16 | if(GIT_FOUND) 17 | execute_process( 18 | COMMAND ${GIT_EXECUTABLE} describe --long --dirty --tags 19 | OUTPUT_VARIABLE FIRMWARE_VERSION_ID 20 | OUTPUT_STRIP_TRAILING_WHITESPACE) 21 | else() 22 | set(FIRMWARE_VERSION_ID "unknown") 23 | endif() 24 | add_definitions(-DFIRMWARE_VERSION_ID="${FIRMWARE_VERSION_ID}") 25 | -------------------------------------------------------------------------------- /app/src/board.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef BOARD_H_ 8 | #define BOARD_H_ 9 | 10 | /** 11 | * @file 12 | * 13 | * @brief Defines for conditional compiling based on existing board features 14 | */ 15 | 16 | #include 17 | 18 | #define BOARD_HAS_DCDC DT_HAS_COMPAT_STATUS_OKAY(half_bridge) 19 | #define BOARD_HAS_PWM_PORT DT_NODE_EXISTS(DT_CHILD(DT_PATH(outputs), pwm_switch)) 20 | #define BOARD_HAS_LOAD_OUTPUT DT_NODE_EXISTS(DT_CHILD(DT_PATH(outputs), load)) 21 | #define BOARD_HAS_USB_OUTPUT DT_NODE_EXISTS(DT_CHILD(DT_PATH(outputs), usb_pwr)) 22 | #define BOARD_HAS_TEMP_FETS DT_NODE_EXISTS(DT_CHILD(DT_PATH(adc_inputs), temp_fets)) 23 | #define BOARD_HAS_TEMP_BAT DT_NODE_EXISTS(DT_CHILD(DT_PATH(adc_inputs), temp_bat)) 24 | 25 | #endif /* BOARD_H_ */ 26 | -------------------------------------------------------------------------------- /dts/bindings/outputs.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | description: GPIOs used for digital output (either on/off type or PWM signal) 5 | 6 | compatible: "outputs" 7 | 8 | child-binding: 9 | description: GPIO digital outputs child node 10 | properties: 11 | gpios: 12 | type: phandle-array 13 | description: GPIO handle for on/off type output 14 | pwms: 15 | type: phandle-array 16 | description: PWM handle for PWM type output 17 | latching-pgood: 18 | type: boolean 19 | description: Driver for latching power good pin (optional) 20 | current-max: 21 | type: int 22 | description: Maximum current of driven power switch (A) 23 | kconfig-flag: 24 | type: boolean 25 | description: This value can be checked by Kconfig parser using dt_node_has_bool_prop function 26 | -------------------------------------------------------------------------------- /tests/src/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include "unity.h" 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #ifdef CONFIG_ARCH_POSIX 14 | #include "posix_board_if.h" 15 | #endif 16 | 17 | #include "tests.h" 18 | 19 | extern "C" { 20 | 21 | void setUp(void) {} 22 | void tearDown(void) {} 23 | 24 | } /* extern "C" */ 25 | 26 | void main() 27 | { 28 | int err = 0; 29 | 30 | err += daq_tests(); 31 | err += bat_charger_tests(); 32 | err += power_port_tests(); 33 | err += half_bridge_tests(); 34 | err += dcdc_tests(); 35 | err += device_status_tests(); 36 | err += load_tests(); 37 | 38 | #ifdef CONFIG_CUSTOM_TESTS 39 | err += custom_tests(); 40 | #endif 41 | 42 | #ifdef CONFIG_ARCH_POSIX 43 | posix_exit(err); 44 | #endif 45 | } 46 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | # Format definition for Libre Solar firmware 2 | # 3 | # Use Clang-Format plugin in VS Code and add "editor.formatOnSave": true to settings 4 | 5 | # Generic format 6 | BasedOnStyle: LLVM 7 | UseTab: Never 8 | IndentWidth: 4 9 | TabWidth: 4 10 | ColumnLimit: 100 11 | AccessModifierOffset: -4 12 | 13 | # Indentation and alignment 14 | AllowShortEnumsOnASingleLine: false 15 | AllowShortFunctionsOnASingleLine: false 16 | AllowShortIfStatementsOnASingleLine: false 17 | AlignConsecutiveAssignments: false 18 | AlignConsecutiveMacros: true 19 | AlignEscapedNewlines: DontAlign 20 | BreakBeforeBinaryOperators: NonAssignment 21 | IndentCaseLabels: true 22 | 23 | # Braces 24 | BreakBeforeBraces: Custom 25 | BraceWrapping: 26 | AfterClass: true 27 | AfterControlStatement: MultiLine 28 | AfterEnum: true 29 | AfterStruct: true 30 | AfterFunction: true 31 | BeforeElse: true 32 | SplitEmptyFunction: false 33 | Cpp11BracedListStyle: false 34 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 # necessary to get tags 15 | 16 | - name: Install dependencies 17 | run: | 18 | sudo apt install -y git make python3 python3-pip doxygen graphviz 19 | pip3 install -r docs/requirements.txt 20 | 21 | - name: Build documentation 22 | run: | 23 | cd docs 24 | make html 25 | 26 | - name: Deploy to gh-pages 27 | uses: peaceiris/actions-gh-pages@v3 28 | with: 29 | github_token: ${{ secrets.GITHUB_TOKEN }} 30 | publish_dir: docs/build/html/ 31 | enable_jekyll: false 32 | allow_empty_commit: false 33 | force_orphan: true 34 | publish_branch: gh-pages 35 | -------------------------------------------------------------------------------- /app/src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 2 | 3 | zephyr_include_directories(.) 4 | 5 | target_sources(app PRIVATE 6 | bat_charger.cpp 7 | data_objects.cpp 8 | data_storage.cpp 9 | daq.cpp 10 | daq_driver.c 11 | device_status.cpp 12 | dcdc.cpp 13 | half_bridge.c 14 | hardware.cpp 15 | leds.cpp 16 | load.cpp 17 | load_driver.c 18 | main.cpp 19 | power_port.cpp 20 | pwm_switch_driver.c 21 | pwm_switch.cpp 22 | setup.cpp 23 | ) 24 | 25 | add_subdirectory(ext) 26 | 27 | if(${CONFIG_CUSTOM_DATA_OBJECTS_FILE}) 28 | target_sources(app PRIVATE data_objects_custom.cpp) 29 | endif() 30 | 31 | # Using this option enables user-specific customization of the firmware. The 32 | # custom subfolder should have its own CMakeLists.txt file. 33 | if(${CONFIG_CUSTOMIZATION}) 34 | add_subdirectory(custom build/custom) 35 | endif() 36 | -------------------------------------------------------------------------------- /docs/src/dev/troubleshooting.rst: -------------------------------------------------------------------------------- 1 | Troubleshoting 2 | ============== 3 | 4 | This page contains a collection of often encountered issues and solutions. 5 | 6 | Errors with STM32L072 MCU using OpenOCD 7 | --------------------------------------- 8 | 9 | The standard OpenOCD settings sometimes fail to flash firmware to this MCU, which is used in the 10 | MPPT 1210 HUS and the PWM charge controller. 11 | 12 | Try one of these workarounds: 13 | 14 | 1. Change OpenOCD settings to ``adapter_khz 500`` in ``openocd-path/scripts/target/stm32l0.cfg``. 15 | 16 | 2. Use other tools or debug probes such as `STM32CubeProgrammer`_ or `Segger J-Link`_. 17 | 18 | 3. Change OpenOCD setting from ``reset_config srst_only`` to ``reset_config srst_open_drain`` or ``reset_config none`` in ``boards/arm//support/openocd.cfg``. 19 | 20 | .. _STM32CubeProgrammer: https://www.st.com/en/development-tools/stm32cubeprog.html 21 | .. _Segger J-Link: https://www.segger.com/products/debug-probes/j-link/ 22 | -------------------------------------------------------------------------------- /west.yml: -------------------------------------------------------------------------------- 1 | manifest: 2 | remotes: 3 | - name: zephyrproject 4 | url-base: https://github.com/zephyrproject-rtos 5 | - name: libresolar 6 | url-base: https://github.com/LibreSolar 7 | - name: thingset 8 | url-base: https://github.com/ThingSet 9 | - name: throwtheswitch 10 | url-base: https://github.com/ThrowTheSwitch 11 | projects: 12 | - name: zephyr 13 | remote: libresolar 14 | revision: v3.2-branch 15 | import: 16 | name-allowlist: 17 | - cmsis 18 | - edtt 19 | - hal_stm32 20 | - name: oled-display-library 21 | remote: libresolar 22 | revision: 523b26c103de56714fcda655702d600870a671bc 23 | path: modules/oled-display 24 | - name: thingset-device-library 25 | remote: thingset 26 | revision: 9630a357de071ac63bfbbaf9d91c6c937663601f 27 | path: modules/thingset 28 | - name: unity 29 | remote: throwtheswitch 30 | revision: 74cde089e65c3435ce9aa87d5c69f4f16b7f6ade 31 | path: modules/unity 32 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from pathlib import Path 3 | import os 4 | 5 | extensions = [ 6 | "breathe", 7 | ] 8 | 9 | # Project 10 | 11 | project = "Charge Controller" 12 | author = "The Libre Solar Project" 13 | year = datetime.now().year 14 | copyright = f"{year} {author}" 15 | 16 | version = os.popen("git describe --tags --abbrev=0").read() 17 | 18 | # HTML output 19 | 20 | html_theme = "sphinx_rtd_theme" 21 | html_theme_options = { 22 | "collapse_navigation": False, 23 | "style_nav_header_background": "#005e83", 24 | } 25 | html_static_path = ["static"] 26 | html_logo = "static/images/logo.png" 27 | html_context = { 28 | "display_github": True, 29 | "github_user": "LibreSolar", 30 | "github_repo": "charge-controller-firmware", 31 | "github_version": "main/docs/", 32 | } 33 | html_favicon = "static/images/favicon.ico" 34 | 35 | # Breathe 36 | 37 | breathe_projects = {"app": "build/doxygen/xml"} 38 | breathe_default_project = "app" 39 | breathe_domain_by_extension = {"h": "cpp", "c": "c"} 40 | breathe_default_members = ("members", ) 41 | -------------------------------------------------------------------------------- /scripts/check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This file performs continuous integration checks and should be run before pushing new commits 4 | # 5 | 6 | # exit when any command fails 7 | set -e 8 | 9 | # use board-specific directory 10 | west config build.dir-fmt "build/{board}" 11 | 12 | echo "---------- Trailing whitespace check -------------" 13 | 14 | # check uncommitted changes 15 | git diff --check HEAD 16 | 17 | # check all commits starting from initial commit 18 | git diff --check `git rev-list --max-parents=0 HEAD`..HEAD 19 | 20 | echo "---------- Running compile check -------------" 21 | 22 | cd zephyr 23 | west build -b mppt_2420_hc@0.2 24 | west build -b mppt_2420_lc 25 | west build -b mppt_1210_hus@0.7 26 | west build -b pwm_2420_lus@0.3 27 | cd .. 28 | 29 | echo "---------- Running unit-tests -------------" 30 | 31 | cd tests 32 | west build -b native_posix -t run 33 | cd .. 34 | 35 | #echo "---------- Running static code checks -------------" 36 | 37 | #platformio check -e mppt_1210_hus -e pwm_2420_lus --skip-packages --fail-on-defect high 38 | 39 | echo "---------- All tests passed -------------" 40 | -------------------------------------------------------------------------------- /docs/src/dev/building_flashing.rst: -------------------------------------------------------------------------------- 1 | Building and Flashing 2 | ===================== 3 | 4 | As the ``main`` branch may contain unstable code, make sure to select the desired release branch 5 | (see GitHub for a list of releases and branches): 6 | 7 | .. code-block:: bash 8 | 9 | git switch -branch 10 | west update 11 | 12 | The ``app`` subfolder contains all application source files and the CMake entry point to build the 13 | firmware, so we go into that directory first. 14 | 15 | .. code-block:: bash 16 | 17 | cd app 18 | 19 | Initial board selection (see ``boards`` subfolder for correct names): 20 | 21 | .. code-block:: bash 22 | 23 | west build -b @ 24 | 25 | The appended ``@`` specifies the board version according to the above table. It can be 26 | omitted if only a single board revision is available or if the default (most recent) version should 27 | be used. See also 28 | `here `_ 29 | for more details regarding board revision handling in Zephyr. 30 | 31 | Flash with specific debug probe (runner), e.g. J-Link: 32 | 33 | .. code-block:: bash 34 | 35 | west flash -r jlink 36 | -------------------------------------------------------------------------------- /tests/prj.conf: -------------------------------------------------------------------------------- 1 | # Main application configuration (overrides board-specific settings) 2 | 3 | # only very small heap necessary for malloc in printf statements with %f 4 | CONFIG_HEAP_MEM_POOL_SIZE=256 5 | 6 | CONFIG_CPLUSPLUS=y 7 | CONFIG_LIB_CPLUSPLUS=y 8 | 9 | #CONFIG_WATCHDOG=y 10 | #CONFIG_WDT_DISABLE_AT_BOOT=y 11 | CONFIG_TASK_WDT=y 12 | CONFIG_THREAD_NAME=y 13 | 14 | CONFIG_REBOOT=y 15 | 16 | CONFIG_HWINFO=y 17 | 18 | CONFIG_CONSOLE_SUBSYS=y 19 | 20 | CONFIG_LOG=y 21 | CONFIG_LOG_MODE_MINIMAL=y 22 | CONFIG_CBPRINTF_FP_SUPPORT=y 23 | 24 | CONFIG_THINGSET=y 25 | 26 | # ThingSet protocol interface via UART serial 27 | CONFIG_THINGSET_SERIAL=n 28 | 29 | # Uncomment to disable regular data publication on ThingSet serial at startup 30 | CONFIG_THINGSET_SERIAL_PUB_DEFAULT=n 31 | 32 | # Uncomment to use LS.one or debug RX/TX connector instead of UEXT serial for ThingSet 33 | CONFIG_UEXT_SERIAL_THINGSET=n 34 | 35 | # Uncomment to enable I2C OLED display in UEXT connector 36 | #CONFIG_UEXT_OLED_DISPLAY=y 37 | 38 | # Select initial battery configuration: GEL (default), FLOODED, AGM, LFP, NMC or NMC_HV 39 | #CONFIG_BAT_TYPE_???=y 40 | 41 | # Change number of cells considering selected type (12V/24V battery by default if not set) 42 | #CONFIG_BAT_NUM_CELLS=? 43 | -------------------------------------------------------------------------------- /app/src/setup.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include 8 | 9 | #include 10 | 11 | #include "bat_charger.h" 12 | #include "board.h" 13 | #include "device_status.h" 14 | #include "load.h" 15 | #include "power_port.h" 16 | #include "pwm_switch.h" 17 | #include "thingset.h" 18 | 19 | extern DcBus lv_bus; 20 | extern PowerPort lv_terminal; 21 | 22 | #if BOARD_HAS_DCDC 23 | extern DcBus hv_bus; 24 | extern PowerPort hv_terminal; 25 | extern Dcdc dcdc; 26 | #endif 27 | 28 | #if BOARD_HAS_PWM_PORT 29 | extern PwmSwitch pwm_switch; 30 | #endif 31 | 32 | extern PowerPort &bat_terminal; 33 | extern PowerPort &solar_terminal; 34 | extern PowerPort &grid_terminal; 35 | 36 | extern DeviceStatus dev_stat; 37 | extern Charger charger; 38 | extern BatConf bat_conf; 39 | extern BatConf bat_conf_user; 40 | 41 | #if BOARD_HAS_LOAD_OUTPUT 42 | extern LoadOutput load; 43 | #endif 44 | 45 | #if BOARD_HAS_USB_OUTPUT 46 | extern LoadOutput usb_pwr; 47 | #endif 48 | 49 | extern ThingSet ts; // defined in data_objects.cpp 50 | 51 | extern uint32_t timestamp; 52 | 53 | /** 54 | * Perform some device setup tasks (currently only used in Zephyr) 55 | */ 56 | void setup(); 57 | -------------------------------------------------------------------------------- /scripts/check-style.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (c) The Libre Solar Project Contributors 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | DEFAULT_BRANCH="origin/main" 7 | 8 | if [ -x "$(command -v clang-format-diff)" ]; then 9 | CLANG_FORMAT_DIFF="clang-format-diff" 10 | elif [ -x "$(command -v clang-format-diff-12)" ]; then 11 | CLANG_FORMAT_DIFF="clang-format-diff-12" 12 | elif [ -x "$(command -v /usr/share/clang/clang-format-diff.py)" ]; then 13 | CLANG_FORMAT_DIFF="/usr/share/clang/clang-format-diff.py -v" 14 | fi 15 | 16 | echo "Style check for diff between branch $DEFAULT_BRANCH" 17 | 18 | STYLE_ERROR=0 19 | 20 | echo "Checking trailing whitespaces with git diff --check" 21 | git diff --check --color=always $DEFAULT_BRANCH 22 | if [[ $? -ne 0 ]]; then 23 | STYLE_ERROR=1 24 | else 25 | echo "No trailing whitespaces found." 26 | fi 27 | 28 | echo "Checking coding style with clang-format" 29 | 30 | # clang-format-diff returns 0 even for style differences, so we have to check the length of the 31 | # response 32 | CLANG_FORMAT_DIFF=`git diff $DEFAULT_BRANCH | $CLANG_FORMAT_DIFF -p1 | colordiff` 33 | if [[ "$(echo -n $CLANG_FORMAT_DIFF | wc -c)" -ne 0 ]]; then 34 | echo "${CLANG_FORMAT_DIFF}" 35 | STYLE_ERROR=1 36 | else 37 | echo "Coding style valid." 38 | fi 39 | 40 | exit $STYLE_ERROR 41 | -------------------------------------------------------------------------------- /docs/src/overview/supported_hardware.rst: -------------------------------------------------------------------------------- 1 | Supported Hardware 2 | ================== 3 | 4 | The software is configurable to support different charge controller PCBs with STM32F072, low-power 5 | STM32L072 or most recent STM32G431 MCUs: 6 | 7 | +-------------------------------+-----------+---------+-------+ 8 | | | | Revisions | 9 | | | +---------+-------+ 10 | | Board | MCU | Default | Older | 11 | +===============================+===========+=========+=======+ 12 | | Libre Solar `MPPT 2420 HC`_ | STM32G431 | 0.2 | 0.1 | 13 | +-------------------------------+-----------+---------+-------+ 14 | | Libre Solar `MPPT 2420 LC`_ | STM32F072 | 0.10 | | 15 | +-------------------------------+-----------+---------+-------+ 16 | | Libre Solar `MPPT 1210 HUS`_ | STM32L072 | 0.7 | 0.4 | 17 | +-------------------------------+-----------+---------+-------+ 18 | | Libre Solar `PWM 2420 LUS`_ | STM32L072 | 0.3 | 0.2 | 19 | +-------------------------------+-----------+---------+-------+ 20 | 21 | .. _MPPT 2420 HC: https://github.com/LibreSolar/mppt-2420-hc 22 | .. _MPPT 2420 LC: https://github.com/LibreSolar/mppt-2420-lc 23 | .. _MPPT 1210 HUS: https://github.com/LibreSolar/mppt-1210-hus 24 | .. _PWM 2420 LUS: https://github.com/LibreSolar/pwm-2420-lus 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Libre Solar Charge Controller Firmware 2 | 3 | ![build badge](https://github.com/LibreSolar/charge-controller-firmware/actions/workflows/zephyr.yml/badge.svg) 4 | 5 | This repository contains the firmware for the different Libre Solar Charge Controllers based on [Zephyr RTOS](https://www.zephyrproject.org/). 6 | 7 | Coding style is described [here](https://github.com/LibreSolar/coding-style). 8 | 9 | ## Development and release model 10 | 11 | The `main` branch is used for ongoing development of the firmware. 12 | 13 | Releases are created from `main` after significant updates have been introduced to the firmware. Each release has to pass tests with multiple boards. 14 | 15 | A release is tagged with a version number consisting of the release year and a release count for that year (starting at zero). For back-porting of bug-fixes, a branch named after the release followed by `-branch` is created, e.g. `v21.0-branch`. 16 | 17 | ## Documentation 18 | 19 | The firmware documentation including build instructions and API reference can be found under [libre.solar/charge-controller-firmware](https://libre.solar/charge-controller-firmware/). 20 | 21 | In order to build the documentation locally you need to install Doxygen, Sphinx and Breathe and run `make html` in the `docs` folder. 22 | 23 | ## License 24 | 25 | This firmware is released under the [Apache-2.0 License](LICENSE). 26 | -------------------------------------------------------------------------------- /.github/workflows/zephyr.yml: -------------------------------------------------------------------------------- 1 | name: Build with Zephyr toolchain 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | container: zephyrprojectrtos/ci:latest 9 | env: 10 | CMAKE_PREFIX_PATH: /opt/toolchains 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | path: charge-controller-firmware 16 | fetch-depth: 0 17 | 18 | - name: Initialize 19 | working-directory: charge-controller-firmware 20 | run: | 21 | west init -l . 22 | west update --narrow 23 | 24 | - name: Coding style check 25 | working-directory: charge-controller-firmware 26 | run: | 27 | apt-get update 28 | apt-get install -y clang-format-12 colordiff 29 | git status 30 | bash scripts/check-style.sh 31 | 32 | - name: Build firmware 33 | working-directory: charge-controller-firmware 34 | run: | 35 | cd app 36 | west build -b mppt_2420_hc@0.2 -p 37 | west build -b mppt_2420_lc -p 38 | west build -b mppt_1210_hus@0.7 -p 39 | west build -b pwm_2420_lus@0.3 -p 40 | 41 | - name: Run unit-tests 42 | working-directory: charge-controller-firmware 43 | run: | 44 | cd tests 45 | west build -b native_posix -t run 46 | -------------------------------------------------------------------------------- /dts/bindings/adc-inputs.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | description: Description for an ADC input including gain (multiplier/divider) and offset 5 | 6 | compatible: "adc-inputs" 7 | 8 | child-binding: 9 | description: ADC inputs child node 10 | properties: 11 | io-channels: 12 | type: phandle-array 13 | required: true 14 | description: | 15 | ADC channel for this input. 16 | 17 | multiplier: 18 | type: int 19 | required: true 20 | description: | 21 | Resistance of the lower leg of the voltage divider. 22 | 23 | divider: 24 | type: int 25 | required: true 26 | description: | 27 | Resistance of the full path through the voltage divider. 28 | 29 | offset: 30 | type: int 31 | required: false 32 | description: | 33 | Offset to be substracted from ADC raw reading before applying the gain 34 | 35 | filter-const: 36 | type: int 37 | default: 5 38 | description: Low-pass filter multiplier 1/(2^filter-const) 39 | 40 | enable-gpios: 41 | type: phandle-array 42 | required: false 43 | description: | 44 | Enable the voltage divider input or amplifier. 45 | 46 | If present the corresponding GPIO must be set to an active level 47 | to enable the divider input. 48 | -------------------------------------------------------------------------------- /app/prj.conf: -------------------------------------------------------------------------------- 1 | # Main application configuration (overrides board-specific settings) 2 | 3 | # only very small heap necessary for malloc in printf statements with %f 4 | CONFIG_HEAP_MEM_POOL_SIZE=256 5 | 6 | CONFIG_CPLUSPLUS=y 7 | CONFIG_LIB_CPLUSPLUS=y 8 | 9 | CONFIG_NEWLIB_LIBC=y 10 | CONFIG_NEWLIB_LIBC_NANO=y 11 | CONFIG_NEWLIB_LIBC_FLOAT_PRINTF=y 12 | 13 | CONFIG_WATCHDOG=y 14 | CONFIG_WDT_DISABLE_AT_BOOT=y 15 | CONFIG_TASK_WDT=y 16 | CONFIG_THREAD_NAME=y 17 | 18 | CONFIG_REBOOT=y 19 | 20 | CONFIG_HWINFO=y 21 | 22 | CONFIG_CONSOLE_SUBSYS=y 23 | CONFIG_CONSOLE_GETCHAR=y 24 | 25 | # Use minimal logging by default to save memory for STM32F0 with little RAM 26 | CONFIG_LOG=y 27 | CONFIG_LOG_MODE_MINIMAL=y 28 | 29 | CONFIG_THINGSET=y 30 | CONFIG_THINGSET_NESTED_JSON=y 31 | 32 | # ThingSet protocol interface via UART serial 33 | CONFIG_THINGSET_SERIAL=y 34 | 35 | # Uncomment to disable regular data publication on ThingSet serial at startup 36 | #CONFIG_THINGSET_SERIAL_PUB_DEFAULT=n 37 | 38 | # Uncomment to use LS.one or debug RX/TX connector instead of UEXT serial for ThingSet 39 | #CONFIG_UEXT_SERIAL_THINGSET=n 40 | 41 | # Uncomment to enable I2C OLED display in UEXT connector 42 | #CONFIG_UEXT_OLED_DISPLAY=y 43 | 44 | # Select initial battery configuration: GEL (default), FLOODED, AGM, LFP, NMC or NMC_HV 45 | #CONFIG_BAT_TYPE_???=y 46 | 47 | # Change number of cells considering selected type (12V/24V battery by default if not set) 48 | #CONFIG_BAT_NUM_CELLS=? 49 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_hc/mppt_2420_hc_0_1_0.overlay: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | / { 8 | pcb { 9 | version-str = "v0.1"; 10 | version-num = <1>; 11 | }; 12 | 13 | /* temporary solution w/o using RGB LED */ 14 | leds { 15 | compatible = "charlieplexing-leds"; 16 | gpios = <&gpiob 14 GPIO_ACTIVE_LOW>, /* red */ 17 | <&gpiob 15 GPIO_ACTIVE_LOW>, /* green */ 18 | <&gpiob 9 GPIO_ACTIVE_LOW>; /* blue */ 19 | 20 | pwr { 21 | states = <1 0 0>; 22 | }; 23 | 24 | load { 25 | states = <0 1 0>; 26 | }; 27 | 28 | rxtx { 29 | states = <0 0 1>; 30 | }; 31 | }; 32 | }; 33 | 34 | &i2c1 { 35 | eeprom@50 { 36 | // Microchip 24AA32A 37 | compatible = "atmel,at24"; 38 | reg = <0x50>; 39 | label = "EEPROM_0"; 40 | size = <32768>; 41 | pagesize = <32>; 42 | address-width = <16>; 43 | /* 44 | * timeout of 5 ms as suggested in datasheet seems too optimistic 45 | * (several write errors occured during testing) 46 | */ 47 | timeout = <7>; 48 | }; 49 | }; 50 | 51 | &spi1 { 52 | pinctrl-0 = <&spi1_sck_pb3 &spi1_miso_pb4 &spi1_mosi_pb5>; 53 | cs-gpios = <&gpiob 6 GPIO_ACTIVE_LOW>; 54 | status = "okay"; 55 | }; 56 | 57 | &timers1 { 58 | halfbridge: halfbridge { 59 | pinctrl-0 = <&tim1_ch1_pa8 &tim1_ch1n_pb13>; 60 | }; 61 | }; 62 | 63 | &spi2 { 64 | status = "disabled"; 65 | 66 | /delete-node/ pinctrl-0; 67 | /delete-node/ pinctrl-names; 68 | /delete-node/ cs-gpios; 69 | }; 70 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ==================================================== 2 | Libre Solar Charge Controller Firmware Documentation 3 | ==================================================== 4 | 5 | This documentation describes the firmware based on `Zephyr RTOS`_ for different Libre Solar MPPT/PWM 6 | Charge Controllers. 7 | 8 | The firmware is under ongoing development. Most recent version can be found on 9 | `GitHub `_. 10 | 11 | This documentation is licensed under the Creative Commons Attribution-ShareAlike 4.0 International 12 | (CC BY-SA 4.0) License. 13 | 14 | .. image:: static/images/cc-by-sa-centered.png 15 | 16 | The full license text is available at ``_. 17 | 18 | .. _Zephyr RTOS: https://zephyrproject.org 19 | 20 | .. toctree:: 21 | :caption: Overview 22 | :hidden: 23 | 24 | src/overview/features 25 | src/overview/supported_hardware 26 | src/overview/charger_concepts 27 | 28 | .. toctree:: 29 | :caption: Development 30 | :hidden: 31 | 32 | src/dev/workspace_setup 33 | src/dev/building_flashing 34 | src/dev/customization 35 | src/dev/unit_tests 36 | src/dev/troubleshooting 37 | 38 | .. toctree:: 39 | :caption: API Reference 40 | :hidden: 41 | 42 | src/api/bat_charger 43 | src/api/daq 44 | src/api/data_objects 45 | src/api/dcdc 46 | src/api/device_status 47 | src/api/half_bridge 48 | src/api/load 49 | src/api/power_port 50 | src/api/pwm_switch 51 | src/api/misc 52 | -------------------------------------------------------------------------------- /boards/arm/mppt_1210_hus/mppt_1210_hus_0_4_0.overlay: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | / { 8 | pcb { 9 | version-str = "v0.4"; 10 | version-num = <4>; 11 | }; 12 | 13 | leds { 14 | compatible = "charlieplexing-leds"; 15 | gpios = <&gpiob 14 GPIO_ACTIVE_HIGH>, /* GND */ 16 | <&gpiob 13 GPIO_ACTIVE_HIGH>, /* SOC_12 */ 17 | <&gpiob 2 GPIO_ACTIVE_HIGH>, /* SOC_3 */ 18 | <&gpiob 11 GPIO_ACTIVE_HIGH>, /* RXTX */ 19 | <&gpiob 10 GPIO_ACTIVE_HIGH>; /* LOAD */ 20 | 21 | soc_1 { 22 | states = <1 0 0 0 0>; 23 | }; 24 | 25 | soc_2 { 26 | states = <0 1 0 0 0>; 27 | }; 28 | 29 | soc_3 { 30 | states = <0 0 1 0 0>; 31 | }; 32 | 33 | rxtx { 34 | states = <0 0 0 1 0>; 35 | }; 36 | 37 | load { 38 | states = <0 0 0 0 1>; 39 | }; 40 | }; 41 | 42 | outputs { 43 | compatible = "outputs"; 44 | 45 | load { 46 | gpios = <&gpioc 13 GPIO_ACTIVE_HIGH>; 47 | }; 48 | 49 | usb-pwr { 50 | gpios = <&gpiob 12 GPIO_ACTIVE_HIGH>; 51 | }; 52 | 53 | /delete-node/ v-solar; 54 | /delete-node/ boot0; 55 | }; 56 | 57 | adc-inputs { 58 | compatible = "adc-inputs"; 59 | 60 | i-load { 61 | io-channels = <&adc1 6>; 62 | // amp gain: 50, resistor: 4 mOhm 63 | multiplier = <1000>; 64 | divider = <200>; // 4*50 65 | }; 66 | 67 | i-dcdc { 68 | io-channels = <&adc1 7>; 69 | // amp gain: 50, resistor: 4 mOhm 70 | multiplier = <1000>; 71 | divider = <200>; // 4*50 72 | }; 73 | 74 | /delete-node/ temp-bat; 75 | }; 76 | }; 77 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Zephyr", 5 | "includePath": [ 6 | "${workspaceFolder}/app/src", 7 | "${workspaceFolder}/app/build/zephyr/include/generated", 8 | "${workspaceFolder}/../modules/hal/cmsis/CMSIS/Core/Include", 9 | "${workspaceFolder}/../modules/hal/stm32/stm32cube/stm32g0xx/soc", 10 | "${workspaceFolder}/../modules/oled-display/src", 11 | "${workspaceFolder}/../modules/thingset/src", 12 | "${workspaceFolder}/../modules/unity/src", 13 | "${workspaceFolder}/../zephyr/include", 14 | "${workspaceFolder}/../zephyr/soc/arm/st_stm32/common", 15 | "${workspaceFolder}/../zephyr/soc/arm/st_stm32/stm32g0", 16 | "" 17 | ], 18 | "browse": { 19 | "path": [ 20 | "${workspaceFolder}/app/src", 21 | "${workspaceFolder}/../modules/thingset/src", 22 | "${workspaceFolder}/../modules/unity/src" 23 | ], 24 | "limitSymbolsToIncludedHeaders": true 25 | }, 26 | "defines": [ 27 | "FIRMWARE_VERSION_ID=\"v21.0-21-ga535fa8\"" 28 | ], 29 | "cStandard": "c99", 30 | "cppStandard": "c++14", 31 | "forcedInclude": [ 32 | "${workspaceFolder}/app/build/zephyr/include/generated/autoconf.h" 33 | ] 34 | } 35 | ], 36 | "version": 4 37 | } 38 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | set(BOARD_ROOT ${CMAKE_SOURCE_DIR}/..) 5 | set(DTS_ROOT ${CMAKE_SOURCE_DIR}/..) 6 | 7 | #set(BOARD "native_posix") 8 | #set(ZEPHYR_TOOLCHAIN_VARIANT "host") 9 | 10 | cmake_minimum_required(VERSION 3.13.1) 11 | find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) 12 | 13 | project(libre_solar_charge_controller) 14 | add_definitions(-DUNIT_TEST=1) 15 | add_definitions(-DUNITY_OUTPUT_COLOR) 16 | add_definitions(-DUNITY_EXCLUDE_SETJMP) 17 | 18 | add_subdirectory(../app/src build/app) 19 | 20 | zephyr_include_directories(./src) 21 | zephyr_include_directories(../app/src) 22 | 23 | zephyr_include_directories(../../modules/thingset/src) 24 | zephyr_include_directories(../../modules/unity/src) 25 | 26 | target_sources(app PRIVATE 27 | ../../modules/unity/src/unity.c 28 | src/main.cpp 29 | src/tests_bat_charger.cpp 30 | src/tests_daq.cpp 31 | src/tests_dcdc.cpp 32 | src/tests_device_status.cpp 33 | src/tests_half_bridge.cpp 34 | src/tests_load.cpp 35 | src/tests_power_port.cpp 36 | ) 37 | 38 | # determine git tag and commit hash for automatic firmware versioning 39 | find_package(Git) 40 | if(GIT_FOUND) 41 | execute_process( 42 | COMMAND ${GIT_EXECUTABLE} describe --long --dirty --tags 43 | OUTPUT_VARIABLE FIRMWARE_VERSION_ID 44 | OUTPUT_STRIP_TRAILING_WHITESPACE) 45 | else() 46 | set(FIRMWARE_VERSION_ID "unknown") 47 | endif() 48 | add_definitions(-DFIRMWARE_VERSION_ID="${FIRMWARE_VERSION_ID}") 49 | -------------------------------------------------------------------------------- /app/src/power_port.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include "power_port.h" 8 | 9 | void PowerPort::init_solar() 10 | { 11 | neg_current_limit = -50; // derating based on max. DC/DC or PWM switch current only 12 | pos_current_limit = 0; // no current towards solar panel allowed 13 | } 14 | 15 | void PowerPort::init_nanogrid() 16 | { 17 | pos_current_limit = 5.0; 18 | neg_current_limit = -5.0; 19 | 20 | // 1 Ohm means 1V change of target voltage per amp, same droop for both current directions. 21 | bus->sink_droop_res = 0.1; 22 | bus->src_droop_res = 0.1; 23 | 24 | // also initialize the connected bus 25 | bus->src_voltage_intercept = 30.0; // starting buck mode above this point 26 | bus->sink_voltage_intercept = 28.0; // boost mode until this voltage is reached 27 | } 28 | 29 | void PowerPort::energy_balance() 30 | { 31 | // remark: timespan = 1s, so no multiplication with time necessary for energy calculation 32 | if (current >= 0.0F) { 33 | pos_energy_Wh += bus->voltage * current / 3600.0F; 34 | } 35 | else { 36 | neg_energy_Wh -= bus->voltage * current / 3600.0F; 37 | } 38 | } 39 | 40 | void PowerPort::update_bus_current_margins() const 41 | { 42 | // charging direction of battery 43 | bus->sink_current_margin = pos_current_limit - current; 44 | 45 | // discharging direction of battery 46 | bus->src_current_margin = neg_current_limit - current; 47 | 48 | // printf("pos: %.3f neg: %.3f current: %.3f\n", pos_current_limit, neg_current_limit, current); 49 | } 50 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_hc/mppt_2420_hc_0_2_0.overlay: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | / { 8 | pcb { 9 | version-str = "v0.2"; 10 | version-num = <2>; 11 | }; 12 | 13 | chosen { 14 | zephyr,code-partition = &slot0_partition; 15 | }; 16 | }; 17 | 18 | &spi2 { 19 | w25q80dv: w25q80dv@0 { 20 | compatible = "jedec,spi-nor"; 21 | reg = <0>; 22 | spi-max-frequency = <4000000>; 23 | size = <0x800000>; /* bits */ 24 | has-dpd; 25 | t-enter-dpd = <4000>; 26 | t-exit-dpd = <25000>; 27 | jedec-id = [ef 40 14]; 28 | }; 29 | }; 30 | 31 | /* 32 | * Below partition layout allows to use MCUboot if CONFIG_BOOTLOADER_MCUBOOT is enabled. Otherwise, 33 | * the code will just use the entire available flash of the MCU. 34 | */ 35 | &flash0 { 36 | partitions { 37 | compatible = "fixed-partitions"; 38 | #address-cells = <1>; 39 | #size-cells = <1>; 40 | 41 | /* bootloader slot: 32 KiB */ 42 | boot_partition: partition@0 { 43 | reg = <0x00000000 0x8000>; 44 | read-only; 45 | }; 46 | 47 | /* application image slot: 96 KiB */ 48 | slot0_partition: partition@8000 { 49 | reg = <0x00008000 0x18000>; 50 | }; 51 | }; 52 | }; 53 | 54 | &w25q80dv { 55 | partitions { 56 | compatible = "fixed-partitions"; 57 | #address-cells = <1>; 58 | #size-cells = <1>; 59 | 60 | /* backup slot: 96 KiB */ 61 | slot1_partition: partition@0 { 62 | reg = <0x00000000 0x18000>; 63 | }; 64 | 65 | /* swap slot: 96 KiB */ 66 | scratch_partition: partition@18000 { 67 | reg = <0x00018000 0x00018000>; 68 | }; 69 | 70 | /* remaining 832 KiB of SPI flash used as storage */ 71 | storage_partition: partition@30000 { 72 | reg = <0x00030000 0x000D0000>; 73 | }; 74 | }; 75 | }; 76 | -------------------------------------------------------------------------------- /scripts/check.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | :: This file performs continuous integration checks and should be run before pushing new commits 3 | 4 | echo "---------- Trailing whitespace check -------------" 5 | 6 | :: check uncommitted changes 7 | git diff --check HEAD 8 | if errorlevel 1 ( 9 | echo Failure Reason is %errorlevel% 10 | exit /b %errorlevel% 11 | ) 12 | 13 | :: check all commits starting from initial commit 14 | for /F %i in ('"git rev-list --max-parents=0 HEAD"') do git diff --check %i..HEAD 15 | if errorlevel 1 ( 16 | echo Failure Reason is %errorlevel% 17 | exit /b %errorlevel% 18 | ) 19 | 20 | echo "---------- Running compile check -------------" 21 | 22 | cd zephyr 23 | 24 | west build -b mppt_2420_hc@0.2 25 | if errorlevel 1 ( 26 | echo Failure Reason is %errorlevel% 27 | exit /b %errorlevel% 28 | ) 29 | 30 | west build -b mppt_2420_lc 31 | if errorlevel 1 ( 32 | echo Failure Reason is %errorlevel% 33 | exit /b %errorlevel% 34 | ) 35 | 36 | west build -b mppt_1210_hus@0.7 37 | if errorlevel 1 ( 38 | echo Failure Reason is %errorlevel% 39 | exit /b %errorlevel% 40 | ) 41 | 42 | west build -b pwm_2420_lus@0.3 43 | if errorlevel 1 ( 44 | echo Failure Reason is %errorlevel% 45 | exit /b %errorlevel% 46 | ) 47 | 48 | cd .. 49 | 50 | echo "---------- Running unit-tests -------------" 51 | 52 | cd tests 53 | west build -b native_posix -t run 54 | if errorlevel 1 ( 55 | echo Failure Reason is %errorlevel% 56 | exit /b %errorlevel% 57 | ) 58 | cd .. 59 | 60 | :: echo "---------- Running static code checks -------------" 61 | 62 | :: platformio check -e mppt_1210_hus -e pwm_2420_lus --skip-packages --fail-on-defect high 63 | :: if errorlevel 1 ( 64 | :: echo Failure Reason is %errorlevel% 65 | :: exit /b %errorlevel% 66 | :: ) 67 | 68 | echo "---------- All tests passed -------------" 69 | -------------------------------------------------------------------------------- /docs/src/dev/workspace_setup.rst: -------------------------------------------------------------------------------- 1 | 2 | Workspace Setup 3 | =============== 4 | 5 | This guide assumes you have already installed the Zephyr SDK and the ``west`` tool according to the 6 | `Zephyr documentation `_. 7 | 8 | Below commands initialize a new workspace and pull all required source files: 9 | 10 | .. code-block:: bash 11 | 12 | # create a new west workspace and pull the charge controller firmware 13 | west init -m https://github.com/LibreSolar/charge-controller-firmware west-workspace 14 | cd west-workspace/charge-controller-firmware 15 | 16 | # pull Zephyr upstream repository and modules (may take a while) 17 | west update 18 | 19 | Afterwards, most important folders in your workspace will look like this: 20 | 21 | .. code-block:: bash 22 | 23 | west-workspace/ # contains .west/config 24 | │ 25 | ├── charge-controller-firmware/ # application firmware repository 26 | │ ├── app/ # application source files 27 | │ ├── boards/ # board specifications 28 | │ ├── tests/ # unit test source files 29 | │ └── west.yml # main manifest file 30 | │ 31 | ├── modules/ # modules imported by Zephyr and CC firmware 32 | | 33 | ├── tools/ # tools used by Zephyr 34 | │ 35 | └── zephyr/ # upstream Zephyr repository 36 | 37 | If you already have a west workspace set up (e.g. for other Libre Solar firmware), you can also 38 | re-use it to avoid having many copies of upstream Zephyr and modules: 39 | 40 | .. code-block:: bash 41 | 42 | # go to your workspace directory 43 | cd your-zephyr-workspace 44 | 45 | # pull the charge controller firmware 46 | git clone https://github.com/LibreSolar/charge-controller-firmware 47 | cd charge-controller-firmware 48 | 49 | # re-configure and update the workspace 50 | # (to be done each time you switch between applications in same workspace) 51 | west config manifest.path charge-controller-firmware 52 | west update 53 | -------------------------------------------------------------------------------- /app/src/data_objects.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef DATA_OBJECTS_H_ 8 | #define DATA_OBJECTS_H_ 9 | 10 | /** @file 11 | * 12 | * @brief Handling of ThingSet data objects 13 | */ 14 | 15 | #include 16 | #include 17 | 18 | /* 19 | * Groups / first layer data object IDs 20 | */ 21 | #define ID_ROOT 0x00 22 | #define ID_DEVICE 0x01 23 | #define ID_BATTERY 0x02 24 | #define ID_CHARGER 0x03 25 | #define ID_SOLAR 0x04 26 | #define ID_LOAD 0x05 27 | #define ID_USB 0x06 28 | #define ID_NANOGRID 0x07 29 | #define ID_DFU 0x0F 30 | #define ID_PUB 0x100 31 | #define ID_CTRL 0x8000 32 | 33 | /* 34 | * Subset definitions for statements and publish/subscribe 35 | */ 36 | #define SUBSET_SER (1U << 0) // UART serial 37 | #define SUBSET_CAN (1U << 1) // CAN bus 38 | #define SUBSET_NVM (1U << 2) // data that should be stored in EEPROM 39 | #define SUBSET_CTRL (1U << 3) // control data sent and received via CAN 40 | 41 | /* 42 | * Data object versioning for EEPROM 43 | * 44 | * Increment the version number each time any data object IDs stored in NVM are changed. Otherwise 45 | * data might get corrupted. 46 | */ 47 | #define DATA_OBJECTS_VERSION 05 48 | 49 | extern bool pub_serial_enable; 50 | extern bool pub_can_enable; 51 | 52 | /** 53 | * Callback function to be called when conf values were changed 54 | */ 55 | void data_objects_update_conf(); 56 | 57 | /** 58 | * Initializes and reads data objects from EEPROM 59 | */ 60 | void data_objects_init(); 61 | 62 | /** 63 | * Callback to provide authentication mechanism via ThingSet 64 | */ 65 | void thingset_auth(); 66 | 67 | /** 68 | * Alphabet used for base32 encoding 69 | * 70 | * https://en.wikipedia.org/wiki/Base32#Crockford's_Base32 71 | */ 72 | const char alphabet_crockford[] = "0123456789abcdefghjkmnpqrstvwxyz"; 73 | 74 | /** 75 | * Convert numeric device ID to base32 string 76 | */ 77 | void uint64_to_base32(uint64_t in, char *out, size_t size, const char *alphabet); 78 | 79 | /** 80 | * Update control values received via CAN 81 | */ 82 | void update_control(); 83 | 84 | #endif /* DATA_OBJECTS_H */ 85 | -------------------------------------------------------------------------------- /dts/bindings/charge-controller.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) The Libre Solar Project Contributors 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | description: Generic charge controller device setup 5 | 6 | compatible: "charge-controller" 7 | 8 | properties: 9 | type: 10 | type: string 11 | required: true 12 | description: Device type 13 | version-str: 14 | type: string 15 | required: true 16 | description: Hardware version number (human-readable) 17 | version-num: 18 | type: int 19 | required: true 20 | description: Hardware version number (usable in preprocessor defines) 21 | hs-voltage-max: 22 | type: int 23 | required: true 24 | description: Maximum voltage at high-side (typically PV input) in volts 25 | ls-voltage-max: 26 | type: int 27 | required: true 28 | description: Maximum voltage at low-side (typically battery) in volts 29 | dcdc-current-max: 30 | type: int 31 | description: Maximum inductor current in amps (if existing in the charge controller) 32 | internal-tref-max: 33 | type: int 34 | default: 50 35 | description: | 36 | Internal reference temperature at full load (°C) 37 | 38 | This value is used for model-based control of overcurrent protection. It represents 39 | the steady-state internal temperature for max. continuous current at ambient 40 | temperature of 25°C. Default value is conservative and can be overwritten in PCB 41 | configuration. 42 | mosfets-tj-max: 43 | type: int 44 | default: 120 45 | description: | 46 | Maximum Tj of MOSFETs (°C) 47 | 48 | This value is used for model-based control of overcurrent protection. It represents 49 | the steady-state junction temperature for max. continuous current at ambient 50 | temperature of 25°C. 51 | mosfets-tau-ja: 52 | type: int 53 | default: 5 54 | description: | 55 | Thermal time constant junction to ambient (s) 56 | 57 | This value is used for model-based control of overcurrent protection. It does not reflect 58 | the much lower MOSFET-internal time constant junction to case, but includes thermal 59 | inertia of the board. 60 | 61 | Around 5s seems to be a good conservative estimation for 5x6 type SMD MOSFETs. Take larger 62 | values for big heat sinks. 63 | -------------------------------------------------------------------------------- /app/src/helper.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef HELPER_H 8 | #define HELPER_H 9 | 10 | /** @file 11 | * 12 | * @brief 13 | * General helper functions 14 | */ 15 | 16 | #include 17 | #include 18 | #include 19 | 20 | #ifdef __INTELLISENSE__ 21 | /* 22 | * VS Code intellisense can't cope with all the Zephyr macro layers for logging, so provide it 23 | * with something more simple and make it silent. 24 | */ 25 | 26 | #define LOG_DBG(...) printf(__VA_ARGS__) 27 | 28 | #define LOG_INF(...) printf(__VA_ARGS__) 29 | 30 | #define LOG_WRN(...) printf(__VA_ARGS__) 31 | 32 | #define LOG_ERR(...) printf(__VA_ARGS__) 33 | 34 | #define LOG_MODULE_REGISTER(...) 35 | 36 | #else 37 | 38 | #include 39 | 40 | #endif /* VSCODE_INTELLISENSE_HACK */ 41 | 42 | #include 43 | 44 | #ifdef __cplusplus 45 | extern "C" { 46 | #endif 47 | 48 | /** 49 | * Framework-independent system uptime 50 | * 51 | * @returns seconds since the system booted 52 | */ 53 | static inline uint32_t uptime() 54 | { 55 | #ifndef UNIT_TEST 56 | return k_uptime_get() / 1000; 57 | #else 58 | return time(NULL); 59 | #endif 60 | } 61 | 62 | /** 63 | * Sets one or more flags in given field 64 | * 65 | * @param field pointer to the field that will be manipulated 66 | * @param mask a single flag or bitwise OR-ed flags 67 | */ 68 | inline void flags_set(uint32_t *field, uint32_t mask) 69 | { 70 | *field |= mask; 71 | } 72 | 73 | /** 74 | * Clears one or more flags in the bit field 75 | * 76 | * @param field pointer to the field that will be manipulated 77 | * @param mask a single flag or bitwise OR-ed flags 78 | */ 79 | inline void flags_clear(uint32_t *field, uint32_t mask) 80 | { 81 | *field &= ~mask; 82 | } 83 | 84 | /** 85 | * Queries one or more flags in the bit field 86 | * 87 | * @param field pointer to the field that will be manipulated 88 | * @param mask a single flag or bitwise OR-ed 89 | * @returns true if any of the flags given in mask are set in the bit field 90 | */ 91 | inline bool flags_check(uint32_t *field, uint32_t mask) 92 | { 93 | return (*field & mask) != 0; 94 | } 95 | 96 | #ifdef __cplusplus 97 | } 98 | #endif 99 | 100 | #endif /* HELPER_H */ 101 | -------------------------------------------------------------------------------- /app/src/half_bridge.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef HALF_BRIDGE_H_ 8 | #define HALF_BRIDGE_H_ 9 | 10 | #ifdef __cplusplus 11 | extern "C" { 12 | #endif 13 | 14 | #include 15 | #include 16 | 17 | /** 18 | * @file 19 | * 20 | * @brief PWM timer functions for half bridge of DC/DC converter 21 | * 22 | * Generates the synchronous PWM signal for the half bridge in the DC/DC converter. Depending on 23 | * the MCU, either the advanced timer TIM1 or the basic timer TIM3 is used. 24 | */ 25 | 26 | /** 27 | * Initiatializes the registers to generate the PWM signal and sets duty 28 | * cycle limits 29 | * 30 | * @param freq_kHz Switching frequency in kHz 31 | * @param deadtime_ns Deadtime in ns between switching the two FETs on/off 32 | * @param min_duty Minimum duty cycle (e.g. 0.5 for limiting input voltage) 33 | * @param max_duty Maximum duty cycle (e.g. 0.97 for charge pump) 34 | */ 35 | void half_bridge_init(int freq_kHz, int deadtime_ns, float min_duty, float max_duty); 36 | 37 | /** 38 | * Set raw timer capture/compare register 39 | * 40 | * This function allows to change the PWM with minimum step size. 41 | * 42 | * @param ccr Timer CCR value (between 0 and ARR) 43 | */ 44 | void half_bridge_set_ccr(uint16_t ccr); 45 | 46 | /** 47 | * Get raw timer capture/compare register 48 | * 49 | * @returns Timer CCR value (between 0 and ARR) 50 | */ 51 | uint16_t half_bridge_get_ccr(); 52 | 53 | /** 54 | * Get raw timer auto-reload register 55 | * 56 | * @returns Timer ARR value 57 | */ 58 | uint16_t half_bridge_get_arr(); 59 | 60 | /** 61 | * Set the duty cycle of the PWM signal 62 | * 63 | * @param duty Duty cycle between 0.0 and 1.0 64 | */ 65 | void half_bridge_set_duty_cycle(float duty); 66 | 67 | /** 68 | * Read the currently set duty cycle 69 | * 70 | * @returns Duty cycle between 0.0 and 1.0 71 | */ 72 | float half_bridge_get_duty_cycle(); 73 | 74 | /** 75 | * Start the PWM generation 76 | * 77 | * Important: Valid duty cycle / CCR has to be set before starting 78 | */ 79 | void half_bridge_start(); 80 | 81 | /** 82 | * Stop the PWM generation 83 | */ 84 | void half_bridge_stop(); 85 | 86 | /** 87 | * Get status of the PWM output 88 | * 89 | * @returns True if PWM output enabled 90 | */ 91 | bool half_bridge_enabled(); 92 | 93 | #ifdef __cplusplus 94 | } 95 | #endif 96 | 97 | #endif /* HALF_BRIDGE_H_ */ 98 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | We welcome every sort of contribution, from bug reports to improvements of the code or new features. If you are not yet 4 | familiar with contributions to open source projects, don't worry. It's not that difficult and most important steps are 5 | explained below. 6 | 7 | There are mainly two ways of contribution: 8 | 9 | 1. If you find a bug or have an idea for a good new feature, just add an **issue** in the GitHub web interface. 10 | 2. If you already know how to implement a solution, follow the guide to contribute code below using a **pull-request**. 11 | In case you are not sure about the implementation, raise an issue first to discuss ideas with others. 12 | 13 | Please note also our [Code of Conduct](CODE_OF_CONDUCT.md) 14 | 15 | ## How can I contribute code? 16 | 17 | Here is a quick summary how contributions work on GitHub: 18 | 19 | 1. Fork the repository to your local GitHub account 20 | 2. Clone the repository to your local computer using `git clone https://github.com/your-github-username/repository-name.git` 21 | 3. Create a new branch where you will later add your changes using `git switch -c branch-name` 22 | 4. Update the files to fix the issue or add new features. Please add your copyright notice in a new line if you made significant changes to the file. 23 | 5. Use `git add your-changed-files` to add the file contents of the changed files to the so-called staging area (means 24 | they are prepared to be committed) 25 | 6. Run `git commit -m "Short message to describe the changes"` to add the staged files to the respository 26 | 7. Upload the changes to the remote repository on GitHub using `git push origin branch-name` 27 | 8. Submit a pull request via the GitHub web interface and wait for the response of the maintainers. 28 | 29 | ### Important remarks: 30 | 31 | - Better make several small [atomic commits](https://en.wikipedia.org/wiki/Atomic_commit#Atomic_commit_convention) than 32 | a single big one 33 | - Use meaningful commit messages and pull request topics. Tell *what* was changed and not that *something* was changed. 34 | "ADC measurements: Improved performance of temperature sensor reading" is much better than "Updates". You can also 35 | reference an issue with its numer (e.g. #123) in the commit message 36 | - Use the [Libre Solar coding style](https://libre.solar/docs/coding_style/) 37 | 38 | ## License 39 | 40 | All contributions must be licensed under the [Apache License 2.0](LICENSE) as the rest of the repository. 41 | -------------------------------------------------------------------------------- /app/src/mcu.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef MCU_H_ 8 | #define MCU_H_ 9 | 10 | #ifdef CONFIG_SOC_STM32F072XB 11 | #include "stm32f072xb.h" 12 | #endif 13 | 14 | #ifdef CONFIG_SOC_STM32L072XX 15 | #include "stm32l072xx.h" 16 | #endif 17 | 18 | #if defined(CONFIG_SOC_STM32G431XX) 19 | #include "stm32g431xx.h" 20 | #elif defined(CONFIG_SOC_STM32G474XX) 21 | #include "stm32g474xx.h" 22 | #endif 23 | 24 | // factory calibration values for internal voltage reference and temperature sensor 25 | // (see MCU datasheet, not Reference Manual) 26 | #if defined(CONFIG_SOC_SERIES_STM32F0X) 27 | 28 | #define VREFINT_CAL (*((uint16_t *)0x1FFFF7BA)) // VREFINT @3.3V/30°C 29 | #define VREFINT_VALUE 3300 // mV 30 | #define TSENSE_CAL1 (*((uint16_t *)0x1FFFF7B8)) 31 | #define TSENSE_CAL2 (*((uint16_t *)0x1FFFF7C2)) 32 | #define TSENSE_CAL1_VALUE 30.0 // temperature of first calibration point 33 | #define TSENSE_CAL2_VALUE 110.0 // temperature of second calibration point 34 | 35 | #elif defined(CONFIG_SOC_SERIES_STM32L0X) 36 | 37 | #define VREFINT_CAL (*((uint16_t *)0x1FF80078)) // VREFINT @3.0V/25°C 38 | #define VREFINT_VALUE 3000 // mV 39 | #define TSENSE_CAL1 (*((uint16_t *)0x1FF8007A)) 40 | #define TSENSE_CAL2 (*((uint16_t *)0x1FF8007E)) 41 | #define TSENSE_CAL1_VALUE 30.0 // temperature of first calibration point 42 | #define TSENSE_CAL2_VALUE 130.0 // temperature of second calibration point 43 | 44 | #elif defined(CONFIG_SOC_SERIES_STM32G4X) 45 | 46 | #define VREFINT_CAL (*((uint16_t *)0x1FFF75AA)) // VREFINT @3.0V/30°C 47 | #define VREFINT_VALUE 3000 // mV 48 | #define TSENSE_CAL1 (*((uint16_t *)0x1FFF75A8)) 49 | #define TSENSE_CAL2 (*((uint16_t *)0x1FFF75CA)) 50 | #define TSENSE_CAL1_VALUE 30.0 // temperature of first calibration point 51 | #define TSENSE_CAL2_VALUE 110.0 // temperature of second calibration point 52 | 53 | #elif !defined(CONFIG_SOC_FAMILY_STM32) 54 | 55 | #define VREFINT_CAL (4096 * 1.224 / 3.0) // VREFINT @3.0V/25°C 56 | #define VREFINT_VALUE 3000 // mV 57 | #define TSENSE_CAL1 (4096.0 * (670 - 161) / 3300) // datasheet: slope 1.61 mV/°C 58 | #define TSENSE_CAL2 (4096.0 * 670 / 3300) // datasheet: 670 mV 59 | #define TSENSE_CAL1_VALUE 30.0 // temperature of first calibration point 60 | #define TSENSE_CAL2_VALUE 130.0 // temperature of second calibration point 61 | 62 | #endif 63 | 64 | #endif // MCU_H_ 65 | -------------------------------------------------------------------------------- /tests/src/tests_half_bridge.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include "half_bridge.h" 8 | #include "tests.h" 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #define MAX_PWM_DUTY 0.97 15 | #define MIN_PWM_DUTY 0.1 16 | #define MID_PWM_DUTY ((MIN_PWM_DUTY + MAX_PWM_DUTY) / 2) 17 | #define PWM_F_KHZ 70 18 | #define PWM_DEADTIME_NS 300 19 | 20 | const float duty_epsilon = 0.006; 21 | // calculated duty cycle float may deviate +/- duty_epsilon from test target value 22 | 23 | static void init_structs() 24 | { 25 | half_bridge_init(PWM_F_KHZ, PWM_DEADTIME_NS, MIN_PWM_DUTY, MAX_PWM_DUTY); 26 | half_bridge_stop(); 27 | } 28 | 29 | void half_bridge_set_duty_cycle_works() 30 | { 31 | half_bridge_set_duty_cycle(MID_PWM_DUTY); 32 | TEST_ASSERT_FLOAT_WITHIN(duty_epsilon, MID_PWM_DUTY, half_bridge_get_duty_cycle()); 33 | } 34 | 35 | void half_bridge_starts_up() 36 | { 37 | half_bridge_set_duty_cycle(MID_PWM_DUTY); 38 | half_bridge_start(); 39 | TEST_ASSERT_EQUAL(true, half_bridge_enabled()); 40 | } 41 | 42 | void half_bridge_stops() 43 | { 44 | half_bridge_set_duty_cycle(MID_PWM_DUTY); 45 | half_bridge_start(); 46 | half_bridge_stop(); 47 | TEST_ASSERT_EQUAL(false, half_bridge_enabled()); 48 | } 49 | 50 | void half_bridge_duty_limits_not_violated() 51 | { 52 | // maximum limit 53 | half_bridge_set_duty_cycle(1.0); 54 | TEST_ASSERT_FLOAT_WITHIN(duty_epsilon, MAX_PWM_DUTY, half_bridge_get_duty_cycle()); 55 | 56 | // minimum limit 57 | half_bridge_set_duty_cycle(0.0); 58 | TEST_ASSERT_FLOAT_WITHIN(duty_epsilon, MIN_PWM_DUTY, half_bridge_get_duty_cycle()); 59 | } 60 | 61 | void half_bridge_ccr_limits_not_violated() 62 | { 63 | // maximum limit 64 | half_bridge_set_duty_cycle(MAX_PWM_DUTY); 65 | half_bridge_set_ccr(half_bridge_get_ccr() + 1); 66 | TEST_ASSERT_FLOAT_WITHIN(duty_epsilon, MAX_PWM_DUTY, half_bridge_get_duty_cycle()); 67 | 68 | // minimum limit 69 | half_bridge_set_duty_cycle(MIN_PWM_DUTY); 70 | half_bridge_set_ccr(half_bridge_get_ccr() - 1); 71 | TEST_ASSERT_FLOAT_WITHIN(duty_epsilon, MIN_PWM_DUTY, half_bridge_get_duty_cycle()); 72 | } 73 | 74 | int half_bridge_tests() 75 | { 76 | init_structs(); 77 | 78 | UNITY_BEGIN(); 79 | 80 | RUN_TEST(half_bridge_set_duty_cycle_works); 81 | RUN_TEST(half_bridge_starts_up); 82 | RUN_TEST(half_bridge_stops); 83 | 84 | RUN_TEST(half_bridge_duty_limits_not_violated); 85 | RUN_TEST(half_bridge_ccr_limits_not_violated); 86 | 87 | return UNITY_END(); 88 | } 89 | -------------------------------------------------------------------------------- /app/src/hardware.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include "hardware.h" 8 | 9 | #include "half_bridge.h" 10 | #include "helper.h" 11 | #include "leds.h" 12 | #include "load.h" 13 | #include "mcu.h" 14 | #include "setup.h" 15 | 16 | #ifndef UNIT_TEST 17 | 18 | #if DT_NODE_EXISTS(DT_CHILD(DT_PATH(outputs), boot0)) 19 | #define BOOT0_NODE DT_CHILD(DT_PATH(outputs), boot0) 20 | #endif 21 | 22 | #include 23 | #include 24 | 25 | LOG_MODULE_REGISTER(hardware, CONFIG_HW_LOG_LEVEL); 26 | 27 | void start_stm32_bootloader() 28 | { 29 | #if DT_NODE_EXISTS(DT_CHILD(DT_PATH(outputs), boot0)) 30 | // pin is connected to BOOT0 via resistor and capacitor 31 | const struct gpio_dt_spec boot0 = GPIO_DT_SPEC_GET(BOOT0_NODE, gpios); 32 | if (device_is_ready(boot0.port)) { 33 | gpio_pin_configure_dt(&boot0, GPIO_OUTPUT_ACTIVE); 34 | k_sleep(K_MSEC(100)); // wait for capacitor at BOOT0 pin to charge up 35 | reset_device(); 36 | } 37 | #elif defined(CONFIG_SOC_SERIES_STM32G4X) 38 | if ((FLASH->CR & FLASH_CR_OPTLOCK) != 0U) { 39 | /* Authorizes the Option Byte register programming */ 40 | FLASH->OPTKEYR = FLASH_OPTKEY1; 41 | FLASH->OPTKEYR = FLASH_OPTKEY2; 42 | } 43 | 44 | // Set proper bits for booting the embedded bootloader (see table 45 | // 5 in section 2.6.1 in document RM0440) 46 | 47 | // nBOOT0: nBOOT0 option bit (equivalent to the BOOT0 pin) 48 | // nSWBOOT0: 0: BOOT0 taken from the option bit nBOOT0 49 | // nSWBOOT0: 1: BOOT0 taken from PB8/BOOT0 pin 50 | // nBOOT1: Together with the BOOT0 pin, this bit selects boot mode 51 | // from the Flash main memory, SRAM1 or the System memory. 52 | 53 | FLASH->OPTR &= ~(FLASH_OPTR_nSWBOOT0 | FLASH_OPTR_nBOOT0); 54 | FLASH->OPTR |= FLASH_OPTR_nBOOT1; 55 | 56 | // Save the current registers in flash, to be reloaded at reset 57 | FLASH->CR |= FLASH_CR_OPTSTRT; 58 | k_sleep(K_MSEC(20)); 59 | 60 | // Reload the option registers from flash; should trigger a system 61 | // reset. 62 | FLASH->CR |= FLASH_CR_OBL_LAUNCH; 63 | 64 | // If OBL_LAUNCH did not reset (it should), we'll force it by 65 | // first locking back the flash registers and going for a reboot. 66 | FLASH->CR |= FLASH_CR_OPTLOCK; 67 | reset_device(); 68 | #endif 69 | } 70 | 71 | void reset_device() 72 | { 73 | sys_reboot(SYS_REBOOT_COLD); 74 | } 75 | 76 | void task_wdt_callback(int channel_id, void *user_data) 77 | { 78 | printk("Task watchdog callback (channel: %d, thread: %s)\n", channel_id, 79 | k_thread_name_get((k_tid_t)user_data)); 80 | 81 | printk("Resetting device...\n"); 82 | 83 | sys_reboot(SYS_REBOOT_COLD); 84 | } 85 | 86 | #else 87 | 88 | // dummy functions for unit tests 89 | void start_stm32_bootloader() {} 90 | void reset_device() {} 91 | void task_wdt_callback(int channel_id, void *user_data) {} 92 | 93 | #endif /* UNIT_TEST */ 94 | -------------------------------------------------------------------------------- /tests/src/tests_power_port.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include "daq.h" 8 | #include "daq_stub.h" 9 | #include "tests.h" 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #include "setup.h" 16 | 17 | static AdcValues adcval; 18 | 19 | const float dcdc_current_sun = 3; 20 | const float load_current = 1; 21 | const int sun_hours = 1; 22 | const int night_hours = 3; 23 | 24 | void energy_calculation_init() 25 | { 26 | dev_stat.solar_in_total_Wh = 0; 27 | dev_stat.load_out_total_Wh = 0; 28 | dev_stat.bat_chg_total_Wh = 0; 29 | dev_stat.bat_dis_total_Wh = 0; 30 | hv_terminal.neg_energy_Wh = 0.0; 31 | load.pos_energy_Wh = 0.0; 32 | lv_terminal.pos_energy_Wh = 0.0; 33 | lv_terminal.neg_energy_Wh = 0.0; 34 | 35 | // set desired measurement values 36 | adcval.bat_temperature = 25; 37 | adcval.battery_voltage = 12; 38 | adcval.dcdc_current = dcdc_current_sun; 39 | adcval.internal_temperature = 25; 40 | adcval.load_current = load_current; 41 | adcval.internal_temperature = 25; 42 | adcval.solar_voltage = 30; 43 | 44 | // insert values into ADC functions 45 | prepare_adc_readings(adcval); 46 | prepare_adc_filtered(); 47 | daq_update(); 48 | 49 | for (int i = 0; i < 60 * 60 * sun_hours; i++) { 50 | hv_terminal.energy_balance(); 51 | lv_terminal.energy_balance(); 52 | load.energy_balance(); 53 | } 54 | 55 | // disable DC/DC = solar charging 56 | adcval.dcdc_current = 0; 57 | prepare_adc_readings(adcval); 58 | prepare_adc_filtered(); 59 | daq_update(); 60 | 61 | for (int i = 0; i < 60 * 60 * night_hours; i++) { 62 | hv_terminal.energy_balance(); 63 | lv_terminal.energy_balance(); 64 | load.energy_balance(); 65 | } 66 | } 67 | 68 | void charging_energy_calculation_valid() 69 | { 70 | energy_calculation_init(); 71 | // charging only during sun hours 72 | TEST_ASSERT_EQUAL_FLOAT( 73 | round(sun_hours * lv_terminal.bus->voltage * (dcdc_current_sun - load_current)), 74 | round(lv_terminal.pos_energy_Wh)); 75 | } 76 | 77 | void discharging_energy_calculation_valid() 78 | { 79 | energy_calculation_init(); 80 | // discharging (sum of current) only during dis hours 81 | TEST_ASSERT_EQUAL_FLOAT(round(night_hours * lv_terminal.bus->voltage * adcval.load_current), 82 | round(lv_terminal.neg_energy_Wh)); 83 | } 84 | 85 | void solar_input_energy_calculation_valid() 86 | { 87 | energy_calculation_init(); 88 | TEST_ASSERT_EQUAL_FLOAT(round(sun_hours * lv_terminal.bus->voltage * dcdc_current_sun), 89 | round(hv_terminal.neg_energy_Wh)); 90 | } 91 | 92 | void load_output_energy_calculation_valid() 93 | { 94 | energy_calculation_init(); 95 | TEST_ASSERT_EQUAL_FLOAT( 96 | round((sun_hours + night_hours) * lv_terminal.bus->voltage * adcval.load_current), 97 | round(load.pos_energy_Wh)); 98 | } 99 | 100 | int power_port_tests() 101 | { 102 | energy_calculation_init(); 103 | 104 | UNITY_BEGIN(); 105 | 106 | // energy calculation 107 | RUN_TEST(charging_energy_calculation_valid); 108 | RUN_TEST(discharging_energy_calculation_valid); 109 | RUN_TEST(solar_input_energy_calculation_valid); 110 | RUN_TEST(load_output_energy_calculation_valid); 111 | 112 | return UNITY_END(); 113 | } 114 | -------------------------------------------------------------------------------- /docs/src/overview/charger_concepts.rst: -------------------------------------------------------------------------------- 1 | Charger Concepts 2 | ================ 3 | 4 | Bus and port structure 5 | ---------------------- 6 | 7 | In order to handle multiple different types of charge controllers, the power flow in the charge controller is abstracted using the two base components DC bus and Power Port: 8 | 9 | - A **DC bus**, which is a connection of two or more electrical circuits such that the electric potential at each connection point can be considered equal. This can be achieved either by connecting each participant very close to each other or by using large copper cross-section such that voltage drop introduced by current flow is negligible. The voltage control targets of the bus can only be set by one of the connected ports. 10 | 11 | - A **Power port**, which connects a DC bus with an internal or external device or sub-circuit. A port usually contains a current measurement. Its voltage is determined by the DC bus it is connected to. A port can also contain a switching element. 12 | 13 | The following image gives an overview of the maximum set of features for a charge controller currently considered in the firmware. It contains two DC buses: High-voltage side (typically solar panel or wind turbine) and low-voltage side (typically determined by battery voltage). Most of the power ports are connected at the low voltage side (12V or 24V). 14 | 15 | .. image:: charge-controller-structure.svg 16 | :alt: Charger controller bus and port structure 17 | 18 | The image also shows the direction of positive current flow. 19 | 20 | Charger state machine 21 | --------------------- 22 | 23 | For general information about battery charging, please check out the [battery chapter at learn.libre.solar](https://learn.libre.solar/system/battery.html#charge-methods). 24 | 25 | The charger module in the firmware has the following states: 26 | 27 | 0. Standby 28 | """""""""" 29 | 30 | Initial state of the charge controller. If the solar voltage is high enough and the battery is not full, charging in CC mode is started. 31 | 32 | 1. Bulk stage 33 | """"""""""""" 34 | 35 | This first charging stage adds about 70 percent of the stored energy. The battery is charged with a constant current (CC), typically the maximum current the charger can supply. In case of a solar charge controller, this is where the Maximum Power Point Tracking (MPPT) takes place. As a result, the terminal voltage increases until the peak charge voltage limit is reached and topping phase is entered. 36 | 37 | 2. Topping stage 38 | """""""""""""""" 39 | 40 | During the topping (sometimes also called boost or absorption) the remaining 30 percent of the energy is continued being charged at a constant voltage (CV) while gradually decreasing the charge current until the battery is fully charged. 41 | 42 | 3. Equalization stage 43 | """"""""""""""""""""" 44 | 45 | An additional equalization charging is beneficial for flooded lead-acid batteries only. It can be considered as a periodic controlled overcharge that brings the cells to the same charge level and removes the sulphation by increasing the voltage to a higher value than the peak charge voltage limit. The current during equlization is controlled at a very low value. 46 | 47 | 4. Float stage 48 | """""""""""""" 49 | 50 | The float or trickle charging stage is the final phase upon the completion of the absorption or equalization phase which maintains the battery at full charge. During this stage, the charge voltage is reduced and held constant. The current reduces to a very low value. 51 | 52 | .. image:: charging-stages.png 53 | :alt: Flow-chart of the MPPT charge controller state machine 54 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at info at libre.solar. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /tests/boards/native_posix.overlay: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | / { 8 | /* delete conflicting nodes from Zephyr native_posix board */ 9 | /delete-node/ leds; 10 | /delete-node/ aliases; 11 | 12 | pcb { 13 | compatible = "charge-controller"; 14 | 15 | type = "Native POSIX dummy"; 16 | version-str = "v0.1"; 17 | version-num = <1>; 18 | 19 | hs-voltage-max = <80>; 20 | ls-voltage-max = <32>; 21 | dcdc-current-max = <20>; 22 | }; 23 | 24 | chosen { 25 | zephyr,console = &uart0; 26 | zephyr,shell-uart = &uart0; 27 | zephyr,flash = &flash0; 28 | }; 29 | 30 | leds { 31 | compatible = "charlieplexing-leds"; 32 | gpios = <&gpio0 6 GPIO_ACTIVE_LOW>, /* chg red */ 33 | <&gpio0 14 GPIO_ACTIVE_LOW>, /* chg green */ 34 | <&gpio0 9 GPIO_ACTIVE_LOW>, /* dis red */ 35 | <&gpio0 15 GPIO_ACTIVE_LOW>; /* dis green */ 36 | 37 | pwr { 38 | states = <0 1 0 0>; 39 | }; 40 | 41 | load { 42 | states = <0 0 0 1>; 43 | }; 44 | }; 45 | 46 | outputs { 47 | compatible = "outputs"; 48 | 49 | pwm_switch { 50 | pwms = <&pwm3 2 (20*1000*1000) 0>; /* 20 ms period = 50 Hz */ 51 | current-max = <20>; 52 | kconfig-flag; /* make node visible in Kconfig */ 53 | }; 54 | 55 | load { 56 | gpios = <&gpio0 2 GPIO_ACTIVE_LOW>; 57 | current-max = <20>; 58 | kconfig-flag; /* make node visible in Kconfig */ 59 | }; 60 | 61 | can_en { 62 | gpios = <&gpio0 11 GPIO_ACTIVE_LOW>; 63 | }; 64 | }; 65 | 66 | adc1: adc1 { 67 | compatible = "zephyr,dummy-adc"; 68 | #io-channel-cells = <1>; 69 | }; 70 | 71 | adc-inputs { 72 | compatible = "adc-inputs"; 73 | 74 | v-low { 75 | io-channels = <&adc1 12>; 76 | multiplier = <105600>; 77 | divider = <5600>; 78 | }; 79 | 80 | v-high { 81 | io-channels = <&adc1 15>; 82 | multiplier = <102200>; 83 | divider = <2200>; 84 | }; 85 | 86 | v-pwm { 87 | io-channels = <&adc1 4>; 88 | multiplier = <25224>; // (12*8.2 + 120*8.2 + 120*12) * 10 89 | divider = <984>; // (8.2*12) * 10 90 | offset = <37414>; // 65536 / (1 + 8.2/120 + 8.2/12) 91 | }; 92 | 93 | temp-fets { 94 | io-channels = <&adc1 11>; 95 | multiplier = <10000>; // 10k NTC series resistor 96 | divider = <1>; 97 | }; 98 | 99 | vref-mcu { 100 | io-channels = <&adc1 18>; 101 | multiplier = <1>; 102 | divider = <1>; 103 | }; 104 | 105 | temp-mcu { 106 | io-channels = <&adc1 16>; 107 | multiplier = <1>; 108 | divider = <1>; 109 | }; 110 | 111 | i-dcdc { 112 | io-channels = <&adc1 1>; 113 | // amp gain: 25, resistor: 2 mOhm 114 | multiplier = <1000>; // 1000 115 | divider = <50>; // 2*25 116 | }; 117 | 118 | i-load { 119 | io-channels = <&adc1 2>; 120 | // amp gain: 25, resistor: 2 mOhm 121 | multiplier = <1000>; // 1000 122 | divider = <50>; // 2*25 123 | }; 124 | 125 | i-pwm { 126 | io-channels = <&adc1 5>; 127 | // amp gain: 68/2.2, resistor: 2 mOhm 128 | multiplier = <2200>; // 1000*2.2 129 | divider = <136>; // 2*68 130 | }; 131 | }; 132 | 133 | timers1: timers1 { 134 | status = "okay"; 135 | 136 | halfbridge: halfbridge { 137 | compatible = "half-bridge"; 138 | frequency = <70000>; 139 | deadtime = <300>; 140 | }; 141 | }; 142 | 143 | timers3: timers { 144 | status = "okay"; 145 | 146 | pwm3: pwm3 { 147 | compatible = "zephyr,dummy-pwm"; 148 | #pwm-cells = <3>; 149 | }; 150 | }; 151 | 152 | aliases { 153 | uart-dbg = &uart0; 154 | uart-uext = &uart0; 155 | // spi-uext = &spi1; 156 | // i2c-uext = &i2c1; 157 | }; 158 | }; 159 | -------------------------------------------------------------------------------- /docs/src/dev/customization.rst: -------------------------------------------------------------------------------- 1 | Customization 2 | ============= 3 | 4 | This firmware is developed to allow easy customization for individual use-cases. Below options 5 | based on the Zephyr devicetree and Kconfig systems try to allow customization without changing 6 | any of the files tracked by git, so that a `git pull` does not lead to conflicts with local 7 | changes. 8 | 9 | Hardware-specific changes 10 | ------------------------- 11 | 12 | In Zephyr, all hardware-specific configuration is described in the 13 | `Devicetree `_. 14 | 15 | The file ``boards/arm/board_name/board_name.dts`` contains the default devicetree specification 16 | (DTS) for a board. It is based on the DTS of the used MCU, which is included from the main Zephyr 17 | repository. 18 | 19 | In order to overwrite the default devicetree specification, so-called overlays can be used. An 20 | overlay file can be specified via the west command line. If it is stored as ``board_name.overlay`` 21 | in the ``app`` subfolder, it will be recognized automatically when building the firmware for that 22 | board. 23 | 24 | Application firmware configuration 25 | ---------------------------------- 26 | 27 | For configuration of the application-specific features, Zephyr uses the 28 | `Kconfig system `_. 29 | 30 | The configuration can be changed using ``west build -t menuconfig`` command or manually by changing 31 | the prj.conf file (see ``Kconfig`` file for possible options). 32 | 33 | Similar to DTS overlays, Kconfig can also be customized per board. Create a folder ``app/boards`` 34 | and a file ``board_name.conf`` in that folder. The configuration from this file will be merged with 35 | the ``prj.conf`` automatically. 36 | 37 | Change the battery type 38 | """"""""""""""""""""""" 39 | 40 | By default, the charge controller is configured for maintainance-free VRLA gel battery 41 | (``CONFIG_BAT_TYPE_GEL``). Possible other pre-defined options are ``CONFIG_BAT_TYPE_FLOODED``, 42 | ``CONFIG_BAT_TYPE_AGM``, ``CONFIG_BAT_TYPE_LFP``, ``CONFIG_BAT_TYPE_NMC`` and 43 | ``CONFIG_BAT_TYPE_NMC_HV``. 44 | 45 | The number of cells is automatically selected by Kconfig to get 12V nominal voltage. It can also 46 | be manually specified via ``CONFIG_BAT_NUM_CELLS``. 47 | 48 | To compile the firmware with default settings for 12V LiFePO4 batteries, add the following to 49 | ``prj.conf`` or the board-specific ``.conf`` file: 50 | 51 | .. code-block:: bash 52 | 53 | CONFIG_BAT_TYPE_LFP=y 54 | CONFIG_BAT_NUM_CELLS=4 55 | 56 | Configure serial for ThingSet protocol 57 | """""""""""""""""""""""""""""""""""""" 58 | 59 | By default, the charge controller uses the serial interface in the UEXT connector for the 60 | `ThingSet protocol `_. This allows to use WiFi modules with ESP32 61 | without any firmware change. 62 | 63 | Add the following configuration if you prefer to use the serial of the additional debug RX/TX pins 64 | present on many boards: 65 | 66 | .. code-block:: bash 67 | 68 | CONFIG_UEXT_SERIAL_THINGSET=n 69 | 70 | To disable regular data publication in one-second interval on ThingSet serial at startup add the 71 | following configuration: 72 | 73 | .. code-block:: bash 74 | 75 | CONFIG_THINGSET_SERIAL_PUB_DEFAULT=n 76 | 77 | 78 | Custom functions (separate C/C++ files) 79 | --------------------------------------- 80 | 81 | If you want experiment with some completely new functions for the charge controller, new files 82 | should be stored in a subfolder ``src/custom``. 83 | 84 | To add the files to the firmware build, create an own CMakeLists.txt in the folder and enable the 85 | Kconfig option ``CONFIG_CUSTOMIZATION``. 86 | 87 | If you think the customization could be relevant for others, please consider sending a pull request. 88 | Integrating features into the upstream charge controller firmware guarantees that the feature stays 89 | up-to-date and that it will not accidentally break after changes in the original firmware. 90 | -------------------------------------------------------------------------------- /app/src/setup.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** @file 8 | * 9 | * @brief Setup of ports and other essential charge controller objects 10 | */ 11 | 12 | #include 13 | 14 | #include "thingset.h" // handles access to internal data via communication interfaces 15 | 16 | #include "bat_charger.h" // battery settings and charger state machine 17 | #include "board.h" 18 | #include "daq.h" // ADC using DMA and conversion to measurement values 19 | #include "data_objects.h" // for access to internal data via ThingSet 20 | #include "data_storage.h" // external I2C EEPROM 21 | #include "dcdc.h" // DC/DC converter control (hardware independent) 22 | #include "device_status.h" // log data (error memory, min/max measurements, etc.) 23 | #include "half_bridge.h" // PWM generation for DC/DC converter 24 | #include "hardware.h" // hardware-related functions like load switch, LED control, watchdog, etc. 25 | #include "leds.h" // LED switching using charlieplexing 26 | #include "load.h" // load and USB output management 27 | #include "pwm_switch.h" // PWM charge controller 28 | 29 | DcBus lv_bus; 30 | PowerPort lv_terminal(&lv_bus, true); // low voltage terminal (battery for typical MPPT) 31 | 32 | #if BOARD_HAS_DCDC 33 | DcBus hv_bus; 34 | PowerPort hv_terminal(&hv_bus, true); // high voltage terminal (solar for typical MPPT) 35 | #if CONFIG_HV_TERMINAL_NANOGRID 36 | Dcdc dcdc(&hv_bus, &lv_bus, DCDC_MODE_AUTO); 37 | #elif CONFIG_HV_TERMINAL_BATTERY 38 | Dcdc dcdc(&hv_bus, &lv_bus, DCDC_MODE_BOOST); 39 | #else 40 | Dcdc dcdc(&hv_bus, &lv_bus, DCDC_MODE_BUCK); 41 | #endif // CONFIG_HV_TERMINAL 42 | #endif 43 | 44 | #if BOARD_HAS_PWM_PORT 45 | PwmSwitch pwm_switch(&lv_bus); 46 | #endif 47 | 48 | #if BOARD_HAS_LOAD_OUTPUT 49 | LoadOutput load(&lv_bus, &load_out_set, &load_out_init, NULL); 50 | #endif 51 | 52 | #if BOARD_HAS_USB_OUTPUT 53 | LoadOutput usb_pwr(&lv_bus, &usb_out_set, &usb_out_init, &pgood_check); 54 | #endif 55 | 56 | #if CONFIG_HV_TERMINAL_SOLAR 57 | PowerPort &solar_terminal = hv_terminal; 58 | #elif CONFIG_LV_TERMINAL_SOLAR 59 | PowerPort &solar_terminal = lv_terminal; 60 | #elif CONFIG_PWM_TERMINAL_SOLAR 61 | PowerPort &solar_terminal = pwm_switch; 62 | #endif 63 | 64 | #if CONFIG_HV_TERMINAL_NANOGRID 65 | PowerPort &grid_terminal = hv_terminal; 66 | #endif 67 | 68 | #if CONFIG_LV_TERMINAL_BATTERY 69 | PowerPort &bat_terminal = lv_terminal; 70 | #elif CONFIG_HV_TERMINAL_BATTERY 71 | PowerPort &bat_terminal = hv_terminal; 72 | #endif 73 | 74 | Charger charger(&bat_terminal); 75 | 76 | BatConf bat_conf; // actual (used) battery configuration 77 | BatConf bat_conf_user; // temporary storage where the user can write to 78 | 79 | DeviceStatus dev_stat; 80 | 81 | // current unix timestamp (independent of time(NULL), as it is user-configurable) 82 | // uint32_t considered large enough, so we avoid 64-bit math (overflow in year 2106) 83 | uint32_t timestamp; 84 | 85 | #ifndef UNIT_TEST 86 | 87 | #include 88 | #include 89 | 90 | static inline void timestamp_inc(struct k_timer *timer_id) 91 | { 92 | ARG_UNUSED(timer_id); 93 | timestamp++; 94 | } 95 | 96 | void setup() 97 | { 98 | static struct k_timer timestamp_timer; 99 | k_timer_init(×tamp_timer, timestamp_inc, NULL); 100 | k_timer_start(×tamp_timer, K_MSEC(1000), K_MSEC(1000)); 101 | 102 | /* 103 | * printf from newlib-nano requires malloc, but Zephyr garbage-collects heap management if it 104 | * is not used anywhere in the code. Below dummy calls force Zephyr to build with heap support. 105 | */ 106 | void *temp = k_malloc(4); 107 | k_free(temp); 108 | 109 | #ifdef CONFIG_SOC_SERIES_STM32G4X 110 | // disable 5k pull-down required for USB-C PD on PB4 and PB6 so that they can be used as inputs 111 | PWR->CR3 |= PWR_CR3_UCPD_DBDIS; 112 | #endif 113 | } 114 | 115 | #endif 116 | -------------------------------------------------------------------------------- /app/src/leds.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef LEDS_H 8 | #define LEDS_H 9 | 10 | /** 11 | * @file 12 | * 13 | * @brief 14 | * Control of status LEDs with charlieplexing 15 | * 16 | * Provides higher-level access to different LEDs (e.g. blinking and flickering). Internally, 17 | * charlieplexing is used for some boards to optimize power consumption and needed pin numbers. 18 | */ 19 | 20 | #ifdef __cplusplus 21 | extern "C" { 22 | #endif 23 | 24 | #include 25 | 26 | /* 27 | * Find out if LED was specified in board devicetree file 28 | */ 29 | #define LED_EXISTS(node) DT_NODE_EXISTS(DT_CHILD(DT_PATH(leds), node)) 30 | 31 | #define LED_TIMEOUT_INFINITE (-1) 32 | 33 | /* 34 | * Find out the position of an LED in the LED states array (using below enum) 35 | */ 36 | #define LED_POS(node) DT_N_S_leds_S_##node##_LED_POS 37 | 38 | /* 39 | * Creates a unique name used by LED_POS macro for below enum 40 | */ 41 | #define LED_ENUM(node) node##_LED_POS, 42 | 43 | /* 44 | * Enum for numbering of LEDs in the order specified in board.dts 45 | */ 46 | // cppcheck-suppress syntaxError 47 | enum 48 | { 49 | DT_FOREACH_CHILD(DT_PATH(leds), LED_ENUM) // actual LED nodes 50 | NUM_LEDS // trick to get the number of nodes 51 | }; 52 | 53 | /** 54 | * LED states 55 | */ 56 | enum LedState 57 | { 58 | LED_STATE_OFF = 0, 59 | LED_STATE_ON, 60 | LED_STATE_BLINK, 61 | LED_STATE_FLICKER 62 | }; 63 | 64 | /** 65 | * Pin states 66 | */ 67 | enum PinState 68 | { 69 | PIN_LOW = 0, 70 | PIN_HIGH, 71 | PIN_FLOAT 72 | }; 73 | 74 | /** 75 | * Initialize LEDs (called at the beginning of leds_update_thread) 76 | */ 77 | void leds_init(bool enabled); 78 | 79 | /** 80 | * Main thread for LED control. Performs pin initializations and charlieplexing at 60 Hz 81 | */ 82 | void leds_update_thread(); 83 | 84 | /** 85 | * Enables/disables dedicated charging LED if existing or blinks SOC LED when solar power is coming 86 | * in. 87 | */ 88 | void leds_set_charging(bool enabled); 89 | 90 | /** 91 | * Enables/disables LED 92 | * 93 | * @param led Number of LED in array defined in PCB configuration 94 | * @param enabled LED is switched on if enabled is set to true 95 | * @param timeout Defines for how long this state should be set (-1 for permanent setting) 96 | */ 97 | void leds_set(unsigned int led, bool enabled, int timeout); 98 | 99 | /** 100 | * Enable LED 101 | * 102 | * @param led Number of LED in array defined in PCB configuration 103 | * @param timeout Defines for how long this state should be set (-1 for permanent setting) 104 | */ 105 | void leds_on(unsigned int led, int timeout); 106 | 107 | /** 108 | * Disable LED 109 | * 110 | * @param led Number of LED in array defined in PCB configuration 111 | */ 112 | void leds_off(unsigned int led); 113 | 114 | /** 115 | * Blink LED 116 | * 117 | * @param led Number of LED in array defined in PCB configuration 118 | * @param timeout Defines for how long this state should be set (-1 for permanent setting) 119 | */ 120 | void leds_blink(unsigned int led, int timeout); 121 | 122 | /** 123 | * Flicker LED 124 | * 125 | * @param led Number of LED in array defined in PCB configuration 126 | * @param timeout Defines for how long this state should be set (-1 for permanent setting) 127 | */ 128 | void leds_flicker(unsigned int led, int timeout); 129 | 130 | /** Updates LED blink and timeout states, must be called every second 131 | */ 132 | void leds_update_1s(); 133 | 134 | /** 135 | * Update SOC LED bar (if existing) 136 | * 137 | * @param soc SOC in percent 138 | * @param load_off_low_soc Prevents showing two SOC LEDs if load is switched off because of low SOC 139 | */ 140 | void leds_update_soc(int soc, bool load_off_low_soc); 141 | 142 | /** 143 | * Toggle between even and uneven LEDs switched on/off to create 144 | * annoying flashing in case of an error. 145 | */ 146 | void leds_toggle_error(); 147 | 148 | #ifdef __cplusplus 149 | } 150 | #endif 151 | 152 | #endif /* LEDS_H */ 153 | -------------------------------------------------------------------------------- /tests/src/tests_device_status.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include "tests.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include "setup.h" 14 | 15 | void reset_counters_at_start_of_day() 16 | { 17 | solar_terminal.bus->voltage = bat_terminal.bus->voltage - 1; 18 | 19 | dev_stat.day_counter = 0; 20 | 21 | solar_terminal.neg_energy_Wh = 10.0; 22 | bat_terminal.neg_energy_Wh = 3.0; 23 | bat_terminal.pos_energy_Wh = 4.0; 24 | load.pos_energy_Wh = 9.0; 25 | 26 | // 5 hours without sun 27 | for (int i = 0; i <= 5 * 60 * 60; i++) { 28 | dev_stat.update_energy(); 29 | } 30 | 31 | TEST_ASSERT_EQUAL(10, dev_stat.solar_in_total_Wh); 32 | TEST_ASSERT_EQUAL(3, dev_stat.bat_dis_total_Wh); 33 | TEST_ASSERT_EQUAL(4, dev_stat.bat_chg_total_Wh); 34 | TEST_ASSERT_EQUAL(9, dev_stat.load_out_total_Wh); 35 | 36 | TEST_ASSERT_EQUAL(10, solar_terminal.neg_energy_Wh); 37 | TEST_ASSERT_EQUAL(3, bat_terminal.neg_energy_Wh); 38 | TEST_ASSERT_EQUAL(4, bat_terminal.pos_energy_Wh); 39 | TEST_ASSERT_EQUAL(9, load.pos_energy_Wh); 40 | 41 | // solar didn't come back yet 42 | TEST_ASSERT_EQUAL(0, dev_stat.day_counter); 43 | 44 | // now solar power comes back 45 | solar_terminal.bus->voltage = bat_terminal.bus->voltage + 1; 46 | dev_stat.update_energy(); 47 | 48 | // day counter should be increased and daily energy counters reset 49 | TEST_ASSERT_EQUAL(1, dev_stat.day_counter); 50 | TEST_ASSERT_EQUAL(0, solar_terminal.neg_energy_Wh); 51 | TEST_ASSERT_EQUAL(0, bat_terminal.neg_energy_Wh); 52 | TEST_ASSERT_EQUAL(0, bat_terminal.pos_energy_Wh); 53 | TEST_ASSERT_EQUAL(0, load.pos_energy_Wh); 54 | } 55 | 56 | void dev_stat_new_solar_voltage_max() 57 | { 58 | solar_terminal.bus->voltage = 40; 59 | dev_stat.update_min_max_values(); 60 | TEST_ASSERT_EQUAL(40, dev_stat.solar_voltage_max); 61 | } 62 | 63 | void dev_stat_new_bat_voltage_max() 64 | { 65 | bat_terminal.bus->voltage = 31; 66 | dev_stat.update_min_max_values(); 67 | TEST_ASSERT_EQUAL(31, dev_stat.battery_voltage_max); 68 | } 69 | 70 | void dev_stat_new_dcdc_current_max() 71 | { 72 | dcdc.inductor_current = 21; 73 | dev_stat.update_min_max_values(); 74 | TEST_ASSERT_EQUAL(21, dev_stat.dcdc_current_max); 75 | } 76 | 77 | void dev_stat_new_load_current_max() 78 | { 79 | load.current = 21; 80 | dev_stat.update_min_max_values(); 81 | TEST_ASSERT_EQUAL(21, dev_stat.load_current_max); 82 | } 83 | 84 | void dev_stat_solar_power_max() 85 | { 86 | solar_terminal.power = -50; 87 | dev_stat.update_min_max_values(); 88 | TEST_ASSERT_EQUAL(50, dev_stat.solar_power_max_day); 89 | TEST_ASSERT_EQUAL(50, dev_stat.solar_power_max_total); 90 | } 91 | 92 | void dev_stat_load_power_max() 93 | { 94 | load.power = 50; 95 | dev_stat.update_min_max_values(); 96 | TEST_ASSERT_EQUAL(50, dev_stat.load_power_max_day); 97 | TEST_ASSERT_EQUAL(50, dev_stat.load_power_max_total); 98 | } 99 | 100 | void dev_stat_new_mosfet_temp_max() 101 | { 102 | dcdc.temp_mosfets = 80; 103 | dev_stat.update_min_max_values(); 104 | TEST_ASSERT_EQUAL(80, dev_stat.mosfet_temp_max); 105 | } 106 | 107 | void dev_stat_new_bat_temp_max() 108 | { 109 | charger.bat_temperature = 45; 110 | dev_stat.update_min_max_values(); 111 | TEST_ASSERT_EQUAL(45, dev_stat.bat_temp_max); 112 | } 113 | 114 | void dev_stat_new_int_temp_max() 115 | { 116 | dev_stat.int_temp_max = 20; 117 | dev_stat.internal_temp = 22; 118 | dev_stat.update_min_max_values(); 119 | TEST_ASSERT_EQUAL(22, dev_stat.int_temp_max); 120 | } 121 | 122 | int device_status_tests() 123 | { 124 | UNITY_BEGIN(); 125 | 126 | RUN_TEST(reset_counters_at_start_of_day); 127 | 128 | RUN_TEST(dev_stat_new_solar_voltage_max); 129 | RUN_TEST(dev_stat_new_bat_voltage_max); 130 | RUN_TEST(dev_stat_new_dcdc_current_max); 131 | RUN_TEST(dev_stat_new_load_current_max); 132 | RUN_TEST(dev_stat_solar_power_max); 133 | RUN_TEST(dev_stat_load_power_max); 134 | RUN_TEST(dev_stat_new_mosfet_temp_max); 135 | RUN_TEST(dev_stat_new_bat_temp_max); 136 | RUN_TEST(dev_stat_new_int_temp_max); 137 | 138 | return UNITY_END(); 139 | } 140 | -------------------------------------------------------------------------------- /scripts/data_objects_parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Parser to create JSON file containing data object metadata 5 | # 6 | # TODO 7 | # - Parse nesting in _pub/serial/Enable properly (needs to take parent IDs into account) 8 | # - Also add metadata to groups (using "self" key) 9 | 10 | import json 11 | 12 | output_path = "cc-05.json" # version has to match DATA_OBJECTS_VERSION 13 | 14 | data_objects = {} 15 | id_macros = {} 16 | 17 | # used to make units provided by ThingSet object names nicer 18 | unit_fix = { 19 | 'degC': '°C', 20 | 'degF': '°F', 21 | 'pct': '%', 22 | } 23 | 24 | # get IDs which are defined as macros 25 | with open("app/src/data_objects.h", 'r') as fd: 26 | for (num, line) in enumerate(fd, 1): 27 | if "#define ID_" in line: 28 | id_macros[line.split()[1]] = int(line.split()[2], base=16) 29 | continue 30 | 31 | # get actual metadata from data_objects file 32 | with open("app/src/data_objects.cpp", 'r') as fd: 33 | json_str = "" 34 | group_name = "" 35 | save_lines = False 36 | save_name = False 37 | beginning_line = 0 38 | for (num, line) in enumerate(fd, 1): 39 | if "TS_GROUP" in line: 40 | group_name = line.strip().split(",")[1].strip(' "') 41 | data_objects[group_name] = {} 42 | continue 43 | 44 | if (line.strip(" \n") == "/*{"): 45 | save_lines = True 46 | beginning_line = num 47 | json_str += "{" 48 | continue 49 | elif (line.strip(" \n") == "}*/"): 50 | json_str += "}" 51 | save_lines = False 52 | save_name = True 53 | continue 54 | 55 | if save_lines: 56 | json_str += line 57 | 58 | if save_name: 59 | try: 60 | name = line.strip().split(",")[1].strip(' "') 61 | id_parsed = line.split("(")[1].split(",")[0] 62 | 63 | try: 64 | obj_id = int(id_parsed, base=16) 65 | obj_idx = str(id_parsed) 66 | except ValueError: 67 | obj_id = id_macros[id_parsed] 68 | obj_idx = hex(id_macros[id_parsed]) 69 | 70 | if group_name == "": 71 | data_objects[name] = {} 72 | data_objects[name]["id"] = obj_id 73 | data_objects[name]["idx"] = obj_idx 74 | data_objects[name].update(json.loads(json_str)) 75 | else: 76 | data_objects[group_name][name] = {} 77 | data_objects[group_name][name]["id"] = obj_id 78 | data_objects[group_name][name]["idx"] = obj_idx 79 | data_objects[group_name][name].update(json.loads(json_str)) 80 | 81 | except Exception as e: 82 | print("Error while parsing block between lines {} and {}".format(beginning_line, num)) 83 | print(e) 84 | exit(-1) 85 | 86 | save_name = False 87 | json_str = "" 88 | 89 | def add_missing_fields(obj, name): 90 | if not 'unit' in obj: 91 | chunks = name.split('_') 92 | if len(chunks) > 2 and name[0] != '_': 93 | obj['unit'] = chunks[1] + '/' + chunks[2] 94 | elif len(chunks) > 1 and name[0] != '_': 95 | obj['unit'] = chunks[1] 96 | else: 97 | obj['unit'] = None 98 | if obj['unit'] in unit_fix: 99 | obj['unit'] = unit_fix[obj['unit']] 100 | if not 'min' in obj: 101 | obj['min'] = None 102 | if not 'max' in obj: 103 | obj['max'] = None 104 | return obj 105 | 106 | # add missing fields 107 | for obj_name in data_objects: 108 | if obj_name[0].islower(): 109 | # data item at root level 110 | data_objects[obj_name] = add_missing_fields(data_objects[obj_name], obj_name) 111 | else: 112 | # group 113 | for item in data_objects[obj_name]: 114 | data_objects[obj_name][item] = add_missing_fields(data_objects[obj_name][item], item) 115 | 116 | # save result 117 | with open(output_path, "w", encoding='utf8') as filename: 118 | json.dump(data_objects, filename, indent=4, ensure_ascii=False) 119 | 120 | print("Saved JSON output as: " + output_path) 121 | -------------------------------------------------------------------------------- /app/src/pwm_switch.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef PWM_SWITCH_H 8 | #define PWM_SWITCH_H 9 | 10 | /** 11 | * @file 12 | * 13 | * @brief PWM charger MOSFET switch control functions 14 | * 15 | * Only used for PWM solar charge controllers. 16 | */ 17 | 18 | #include 19 | #include 20 | 21 | #ifdef __cplusplus 22 | 23 | #include "power_port.h" 24 | 25 | /** 26 | * PWM charger type 27 | * 28 | * Contains all data belonging to the PWM switching sub-component. 29 | */ 30 | class PwmSwitch : public PowerPort 31 | { 32 | public: 33 | PwmSwitch(DcBus *dc_bus); 34 | 35 | /** 36 | * Main control function for the PWM switching algorithm 37 | */ 38 | void control(); 39 | 40 | /** 41 | * Test mode for PWM switch 42 | * 43 | * Sets duty cycle to 90% and listens to enable/disable signal 44 | */ 45 | void test(); 46 | 47 | /** 48 | * Fast stop function (bypassing control loop) 49 | * 50 | * May be called from an ISR which detected overvoltage / overcurrent conditions. 51 | * 52 | * PWM port will be restarted automatically from control function if condtions are valid. 53 | */ 54 | void stop(); 55 | 56 | /** 57 | * Read the general on/off status of PWM switching 58 | * 59 | * @returns true if on 60 | */ 61 | bool active(); 62 | 63 | /** 64 | * Read the current high or low state of the PWM signal 65 | * 66 | * @returns true if high, false if low 67 | */ 68 | bool signal_high(); 69 | 70 | /** 71 | * Read the currently set duty cycle 72 | * 73 | * @returns Duty cycle between 0.0 and 1.0 74 | */ 75 | float get_duty_cycle(); 76 | 77 | /** 78 | * Voltage measurement at terminal (external, usually solar panel voltage) 79 | */ 80 | float ext_voltage; 81 | 82 | /** 83 | * Enable switch, true by default. Can be used to completely disable the PWM power stage. 84 | */ 85 | bool enable = true; 86 | 87 | /** 88 | * Offset voltage of solar panel vs. battery to start charging (V) 89 | */ 90 | float offset_voltage_start = 2.0F; 91 | 92 | /** 93 | * Interval to wait before retrying charging after low solar power cut-off or overvoltage 94 | * event (s) 95 | */ 96 | uint32_t restart_interval = 60; 97 | 98 | /** 99 | * Time when charger was switched off last time 100 | * 101 | * Initialized with large negative value to start immediately after reset. 102 | */ 103 | time_t off_timestamp = -10000; 104 | 105 | /** 106 | * Last time the current through the switch was above minimum 107 | */ 108 | time_t power_good_timestamp; 109 | }; 110 | #endif 111 | 112 | #ifdef __cplusplus 113 | extern "C" { 114 | #endif 115 | 116 | /** 117 | * Read the currently set duty cycle 118 | * 119 | * @returns Duty cycle between 0.0 and 1.0 120 | */ 121 | float pwm_signal_get_duty_cycle(); 122 | 123 | /** 124 | * Set the duty cycle of the PWM signal 125 | * 126 | * @param duty Duty cycle between 0.0 and 1.0 127 | */ 128 | void pwm_signal_set_duty_cycle(float duty); 129 | 130 | /** 131 | * Change raw timer capture/compare register by defined step 132 | * 133 | * @param delta Steps to be added/substracted from current CCR value 134 | */ 135 | void pwm_signal_duty_cycle_step(int delta); 136 | 137 | /** 138 | * Initiatializes the registers to generate the PWM signal and sets duty 139 | * cycle limits 140 | * 141 | * @param freq_Hz Switching frequency in Hz 142 | */ 143 | void pwm_signal_init_registers(int freq_Hz); 144 | 145 | /** 146 | * Start the PWM generation 147 | * 148 | * @param duty Duty cycle between 0.0 and 1.0 149 | */ 150 | void pwm_signal_start(float pwm_duty); 151 | 152 | /** 153 | * Stop the PWM generation 154 | */ 155 | void pwm_signal_stop(); 156 | 157 | /** 158 | * Check if the PWM pin voltage level is high (on-state) 159 | * 160 | * @returns true if pin is high, false if pin is low 161 | */ 162 | bool pwm_signal_high(); 163 | 164 | /** 165 | * Check if the PWM generation is active 166 | * 167 | * @returns true if PWM signal generation is active 168 | */ 169 | bool pwm_active(); 170 | 171 | #ifdef __cplusplus 172 | } 173 | #endif 174 | 175 | #endif /* PWM_SWITCH_H */ 176 | -------------------------------------------------------------------------------- /boards/arm/mppt_1210_hus/mppt_1210_hus.dts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /dts-v1/; 8 | #include 9 | #include 10 | 11 | / { 12 | model = "Libre Solar MPPT 1210 HUS"; 13 | compatible = "st,stm32l072"; 14 | 15 | pcb { 16 | compatible = "charge-controller"; 17 | 18 | type = "MPPT 1210 HUS"; 19 | 20 | hs-voltage-max = <40>; 21 | ls-voltage-max = <16>; 22 | dcdc-current-max = <10>; 23 | }; 24 | 25 | chosen { 26 | zephyr,console = &usart1; 27 | zephyr,shell-uart = &usart1; 28 | zephyr,sram = &sram0; 29 | zephyr,flash = &flash0; 30 | }; 31 | 32 | leds { 33 | compatible = "charlieplexing-leds"; 34 | gpios = <&gpiob 13 GPIO_ACTIVE_HIGH>, /* Pin A */ 35 | <&gpiob 14 GPIO_ACTIVE_HIGH>, /* Pin B */ 36 | <&gpiob 2 GPIO_ACTIVE_HIGH>; /* Pin C */ 37 | 38 | soc_1 { 39 | states = <1 2 0>; /* LED1 */ 40 | }; 41 | 42 | soc_2 { 43 | states = <0 2 1>; /* LED2 */ 44 | }; 45 | 46 | soc_3 { 47 | states = <1 0 2>; /* LED3 */ 48 | }; 49 | 50 | load { 51 | states = <2 0 1>; /* LED4 */ 52 | }; 53 | 54 | rxtx { 55 | states = <2 1 0>; /* LED5 */ 56 | }; 57 | }; 58 | 59 | outputs { 60 | compatible = "outputs"; 61 | 62 | load { 63 | gpios = <&gpioc 13 GPIO_ACTIVE_HIGH>; 64 | current-max = <10>; 65 | kconfig-flag; /* make node visible in Kconfig */ 66 | }; 67 | 68 | usb-pwr { 69 | gpios = <&gpiob 10 GPIO_ACTIVE_HIGH>; 70 | }; 71 | 72 | boot0 { 73 | gpios = <&gpiob 12 GPIO_ACTIVE_HIGH>; 74 | }; 75 | }; 76 | 77 | adc-inputs { 78 | compatible = "adc-inputs"; 79 | 80 | v-low { 81 | io-channels = <&adc1 0>; 82 | multiplier = <105600>; 83 | divider = <5600>; 84 | }; 85 | 86 | v-high { 87 | io-channels = <&adc1 1>; 88 | multiplier = <105600>; 89 | divider = <5600>; 90 | enable-gpios = <&gpioc 14 GPIO_ACTIVE_HIGH>; 91 | }; 92 | 93 | i-load { 94 | io-channels = <&adc1 5>; 95 | // amp gain: 50, resistor: 3 mOhm 96 | multiplier = <1000>; 97 | divider = <150>; // 3*50 98 | }; 99 | 100 | i-dcdc { 101 | io-channels = <&adc1 6>; 102 | // amp gain: 50, resistor: 3 mOhm 103 | multiplier = <1000>; 104 | divider = <150>; // 3*50 105 | }; 106 | 107 | temp-bat { 108 | io-channels = <&adc1 7>; 109 | multiplier = <10000>; 110 | divider = <1>; 111 | }; 112 | 113 | vref-mcu { 114 | io-channels = <&adc1 17>; 115 | multiplier = <1>; 116 | divider = <1>; 117 | }; 118 | 119 | temp-mcu { 120 | io-channels = <&adc1 18>; 121 | multiplier = <10000>; 122 | divider = <1>; 123 | }; 124 | }; 125 | 126 | aliases { 127 | uart-dbg = &usart1; 128 | uart-uext = &usart2; 129 | spi-uext = &spi1; 130 | i2c-uext = &i2c1; 131 | }; 132 | }; 133 | 134 | &clk_hsi { 135 | status = "okay"; 136 | }; 137 | 138 | &pll { 139 | div = <2>; 140 | mul = <4>; 141 | clocks = <&clk_hsi>; 142 | status = "okay"; 143 | }; 144 | 145 | &rcc { 146 | clocks = <&pll>; 147 | clock-frequency = ; 148 | ahb-prescaler = <1>; 149 | apb1-prescaler = <1>; 150 | apb2-prescaler = <1>; 151 | }; 152 | 153 | &usart1 { 154 | pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>; 155 | pinctrl-names = "default"; 156 | current-speed = <115200>; 157 | status = "okay"; 158 | }; 159 | 160 | &usart1_rx_pa10 { 161 | bias-pull-up; // avoid junk characters if pin is left floating 162 | }; 163 | 164 | &usart2 { 165 | pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3>; 166 | pinctrl-names = "default"; 167 | current-speed = <115200>; 168 | status = "okay"; 169 | }; 170 | 171 | &usart2_rx_pa3 { 172 | bias-pull-up; // avoid junk characters if pin is left floating 173 | }; 174 | 175 | &i2c1 { 176 | pinctrl-0 = <&i2c1_scl_pb6 &i2c1_sda_pb7>; 177 | pinctrl-names = "default"; 178 | clock-frequency = ; 179 | status = "okay"; 180 | }; 181 | 182 | &spi1 { 183 | pinctrl-0 = <&spi1_sck_pb3 &spi1_miso_pb4 &spi1_mosi_pb5>; 184 | pinctrl-names = "default"; 185 | cs-gpios = <&gpioa 15 GPIO_ACTIVE_LOW>; 186 | status = "okay"; 187 | }; 188 | 189 | &iwdg { 190 | status = "okay"; 191 | }; 192 | 193 | &timers3 { 194 | status = "okay"; 195 | 196 | halfbridge: halfbridge { 197 | compatible = "half-bridge"; 198 | pinctrl-0 = <&tim3_ch3_pb0 &tim3_ch4_pb1>; 199 | pinctrl-names = "default"; 200 | frequency = <50000>; 201 | deadtime = <230>; 202 | }; 203 | }; 204 | 205 | &eeprom { 206 | status = "okay"; 207 | }; 208 | -------------------------------------------------------------------------------- /app/src/daq.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /** 8 | * @file 9 | * @brief Data Acquisition (DAQ) 10 | * 11 | * Measurements are taken using ADC with DMA. The DAC is used to generate reference voltages 12 | * for bi-directional current measurement. Data is stored in structs, which are afterwards 13 | * used by the control algorithms 14 | * 15 | * All calculations assume a 16-bit ADC, even though the STM32 ADCs natively support 12-bit. 16 | * Using 16-bit for calculation allows to increase resolution by oversampling. Left-aligned 17 | * 12-bit ADC readings can be treated as 16-bit ADC readings. 18 | */ 19 | 20 | #ifndef DAQ_H_ 21 | #define DAQ_H_ 22 | 23 | #ifdef __cplusplus 24 | extern "C" { 25 | #endif 26 | 27 | #include 28 | 29 | #include 30 | 31 | #define ADC_SCALE_FLOAT 65536.0F // 16-bit full scale 32 | 33 | #ifdef CONFIG_SOC_SERIES_STM32G4X 34 | // Using internal reference buffer at VREF+ pin, set to 2048 mV 35 | #define VREF (2048) 36 | #elif defined(UNIT_TEST) 37 | #define VREF (3300) 38 | #else 39 | // internal STM reference voltage (calibrated using 12-bit right-aligned readings) 40 | #define VREF (VREFINT_VALUE * VREFINT_CAL / (adc_raw_filtered(ADC_POS(vref_mcu)) >> 4)) 41 | #endif 42 | 43 | #define ADC_GAIN(name) \ 44 | ((float)DT_PROP(DT_CHILD(DT_PATH(adc_inputs), name), multiplier) \ 45 | / DT_PROP(DT_CHILD(DT_PATH(adc_inputs), name), divider)) 46 | 47 | #define ADC_OFFSET(name) (DT_PROP(DT_CHILD(DT_PATH(adc_inputs), name), offset)) 48 | 49 | /* 50 | * Find out the position in the ADC reading array for a channel identified by its Devicetree node 51 | */ 52 | #define ADC_POS(node) DT_N_S_adc_inputs_S_##node##_ADC_POS 53 | 54 | /* 55 | * Creates a unique name for below enum 56 | */ 57 | #define ADC_ENUM(node) node##_ADC_POS, 58 | 59 | /* 60 | * Enum for numbering of ADC channels as they are written by the DMA controller 61 | * 62 | * The channels must be specified in ascending order in the board.dts file. 63 | */ 64 | // cppcheck-suppress syntaxError 65 | enum 66 | { 67 | DT_FOREACH_CHILD(DT_PATH(adc_inputs), ADC_ENUM) 68 | NUM_ADC_CH // trick to get the number of elements 69 | }; 70 | 71 | /** 72 | * Struct to define upper and lower limit alerts for any ADC channel 73 | */ 74 | typedef struct 75 | { 76 | void (*callback)(); ///< Function to be called when limits are exceeded 77 | uint16_t limit; ///< ADC reading for lower limit 78 | int16_t debounce_ms; ///< Milliseconds delay for triggering alert 79 | } AdcAlert; 80 | 81 | /** 82 | * Convert 16-bit raw ADC reading to voltage 83 | * 84 | * @param raw 16-bit ADC reading 85 | * @param vref_mV Reference voltage in millivolts 86 | * 87 | * @return Voltage in volts 88 | */ 89 | static inline float adc_raw_to_voltage(int32_t raw, int32_t vref_mV) 90 | { 91 | return (raw * vref_mV) / (ADC_SCALE_FLOAT * 1000); 92 | } 93 | 94 | /** 95 | * Convert voltage to 16-bit raw ADC reading 96 | * 97 | * @param voltage Voltage in volts 98 | * @param vref_mV Reference voltage in millivolts 99 | * 100 | * @return 16-bit ADC reading 101 | */ 102 | static inline int32_t adc_voltage_to_raw(float voltage, int32_t vref_mV) 103 | { 104 | return voltage / vref_mV * ADC_SCALE_FLOAT * 1000; 105 | } 106 | 107 | /** 108 | * Set offset vs. actual measured value, i.e. sets zero current point. 109 | * 110 | * All input/output switches and consumers should be switched off before calling this function 111 | */ 112 | void calibrate_current_sensors(void); 113 | 114 | /** 115 | * Updates structures with data read from ADC 116 | */ 117 | void daq_update(void); 118 | 119 | /** 120 | * Initializes ADC, DAC and DMA 121 | */ 122 | void daq_setup(void); 123 | 124 | /** 125 | * Read, filter and check raw ADC readings stored by DMA controller 126 | */ 127 | void adc_update_value(unsigned int pos); 128 | 129 | /** 130 | * Set lv side (battery) voltage limits where an alert should be triggered 131 | * 132 | * @param lv_overvoltage Upper voltage limit 133 | * @param lv_undervoltage Lower voltage limit 134 | */ 135 | void daq_set_lv_limits(float lv_overvoltage, float lv_undervoltage); 136 | 137 | /** 138 | * Set hv side (grid/solar) voltage limit where an alert should be triggered 139 | * 140 | * @param hv_overvoltage Upper voltage limit 141 | */ 142 | void daq_set_hv_limit(float hv_overvoltage); 143 | 144 | /** 145 | * Add an inhibit delay to the alerts to disable it temporarily 146 | * 147 | * @param adc_pos The position of the ADC measurement channel 148 | * @param timeout_ms Timeout in milliseconds 149 | */ 150 | void adc_upper_alert_inhibit(int adc_pos, int timeout_ms); 151 | 152 | #ifdef __cplusplus 153 | } 154 | #endif 155 | 156 | #endif /* DAQ_H_ */ 157 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_hc/mppt_2420_hc.dts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /dts-v1/; 8 | #include 9 | #include 10 | 11 | / { 12 | model = "Libre Solar MPPT 2420 HC"; 13 | compatible = "st,stm32g431"; 14 | 15 | pcb { 16 | compatible = "charge-controller"; 17 | 18 | type = "MPPT 2420 HC"; 19 | 20 | hs-voltage-max = <90>; 21 | ls-voltage-max = <32>; 22 | dcdc-current-max = <20>; 23 | }; 24 | 25 | chosen { 26 | zephyr,console = &usart1; 27 | zephyr,shell-uart = &usart1; 28 | zephyr,sram = &sram0; 29 | zephyr,flash = &flash0; 30 | }; 31 | 32 | /* temporary solution only using green color LED */ 33 | leds { 34 | compatible = "charlieplexing-leds"; 35 | gpios = <&gpioa 6 GPIO_ACTIVE_LOW>, /* chg red */ 36 | <&gpioc 14 GPIO_ACTIVE_LOW>, /* chg green */ 37 | <&gpiob 9 GPIO_ACTIVE_LOW>, /* dis red */ 38 | <&gpioc 15 GPIO_ACTIVE_LOW>; /* dis green */ 39 | 40 | pwr { 41 | states = <0 1 0 0>; 42 | }; 43 | 44 | load { 45 | states = <0 0 0 1>; 46 | }; 47 | }; 48 | 49 | outputs { 50 | compatible = "outputs"; 51 | 52 | load { 53 | gpios = <&gpiob 2 GPIO_ACTIVE_LOW>; 54 | current-max = <20>; 55 | kconfig-flag; /* make node visible in Kconfig */ 56 | }; 57 | 58 | charge_pump { 59 | pwms = <&pwm8 2 200000 PWM_POLARITY_NORMAL>; /* 200 us period = 5 kHz */ 60 | }; 61 | 62 | can_en { 63 | gpios = <&gpiob 11 GPIO_ACTIVE_LOW>; 64 | }; 65 | }; 66 | 67 | adc-inputs { 68 | compatible = "adc-inputs"; 69 | 70 | v-low { 71 | io-channels = <&adc1 12>; 72 | multiplier = <105600>; 73 | divider = <5600>; 74 | }; 75 | 76 | v-high { 77 | io-channels = <&adc1 15>; 78 | multiplier = <102200>; 79 | divider = <2200>; 80 | }; 81 | 82 | temp-fets { 83 | io-channels = <&adc1 11>; 84 | multiplier = <10000>; // 10k NTC series resistor 85 | divider = <1>; 86 | }; 87 | 88 | vref-mcu { 89 | io-channels = <&adc1 18>; 90 | multiplier = <1>; 91 | divider = <1>; 92 | }; 93 | 94 | temp-mcu { 95 | io-channels = <&adc1 16>; 96 | multiplier = <1>; 97 | divider = <1>; 98 | }; 99 | 100 | i-dcdc { 101 | io-channels = <&adc2 1>; 102 | // amp gain: 25, resistor: 2 mOhm 103 | multiplier = <1000>; // 1000 104 | divider = <50>; // 2*25 105 | }; 106 | 107 | i-load { 108 | io-channels = <&adc2 2>; 109 | // amp gain: 25, resistor: 2 mOhm 110 | multiplier = <1000>; // 1000 111 | divider = <50>; // 2*25 112 | }; 113 | }; 114 | 115 | aliases { 116 | uart-dbg = &usart1; 117 | uart-uext = &usart2; 118 | spi-uext = &spi1; 119 | i2c-uext = &i2c1; 120 | }; 121 | }; 122 | 123 | &clk_hsi { 124 | status = "okay"; 125 | }; 126 | 127 | &pll { 128 | div-m = <4>; 129 | mul-n = <75>; 130 | div-p = <7>; 131 | div-q = <2>; 132 | div-r = <2>; 133 | clocks = <&clk_hsi>; 134 | status = "okay"; 135 | }; 136 | 137 | &rcc { 138 | clocks = <&pll>; 139 | clock-frequency = ; 140 | ahb-prescaler = <1>; 141 | apb1-prescaler = <1>; 142 | apb2-prescaler = <1>; 143 | }; 144 | 145 | &usart1 { 146 | pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>; 147 | pinctrl-names = "default"; 148 | current-speed = <115200>; 149 | status = "okay"; 150 | }; 151 | 152 | &usart1_rx_pa10 { 153 | bias-pull-up; // avoid junk characters if pin is left floating 154 | }; 155 | 156 | &usart2 { 157 | pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3>; 158 | pinctrl-names = "default"; 159 | current-speed = <115200>; 160 | status = "okay"; 161 | }; 162 | 163 | &usart2_rx_pa3 { 164 | bias-pull-up; // avoid junk characters if pin is left floating 165 | }; 166 | 167 | &i2c1 { 168 | pinctrl-0 = <&i2c1_scl_pa15 &i2c1_sda_pb7>; 169 | pinctrl-names = "default"; 170 | clock-frequency = ; 171 | status = "okay"; 172 | }; 173 | 174 | &spi1 { 175 | pinctrl-0 = <&spi1_sck_pb3 &spi1_miso_pb4 &spi1_mosi_pb5>; 176 | pinctrl-names = "default"; 177 | cs-gpios = <&gpiob 6 GPIO_ACTIVE_LOW>; 178 | status = "okay"; 179 | }; 180 | 181 | &spi2 { 182 | pinctrl-0 = <&spi2_sck_pb13 &spi2_miso_pb14 &spi2_mosi_pb15>; 183 | pinctrl-names = "default"; 184 | cs-gpios = <&gpioa 5 GPIO_ACTIVE_LOW>; 185 | status = "okay"; 186 | }; 187 | 188 | &iwdg { 189 | status = "okay"; 190 | }; 191 | 192 | &timers1 { 193 | status = "okay"; 194 | 195 | halfbridge: halfbridge { 196 | compatible = "half-bridge"; 197 | pinctrl-0 = <&tim1_ch1_pa8 &tim1_ch1n_pc13>; 198 | pinctrl-names = "default"; 199 | frequency = <70000>; 200 | deadtime = <300>; 201 | }; 202 | }; 203 | 204 | /* TIM8 (CH2) used for charge pump */ 205 | &timers8 { 206 | status = "okay"; 207 | st,prescaler = <149>; /* reduce counter clock frequency to 1 MHz */ 208 | pwm8: pwm { 209 | status = "okay"; 210 | pinctrl-0 = <&tim8_ch2_pb8>; 211 | pinctrl-names = "default"; 212 | }; 213 | }; 214 | 215 | &can1 { 216 | pinctrl-0 = <&fdcan1_rx_pa11 &fdcan1_tx_pa12>; 217 | pinctrl-names = "default"; 218 | bus-speed = <500000>; 219 | bus-speed-data = <500000>; 220 | sjw = <1>; 221 | sjw-data = <1>; 222 | sample-point = <875>; 223 | sample-point-data = <875>; 224 | status = "okay"; 225 | }; 226 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_rc/mppt_2420_rc.dts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /dts-v1/; 8 | #include 9 | #include 10 | 11 | / { 12 | model = "Libre Solar MPPT 2420 RC"; 13 | compatible = "st,stm32g431"; 14 | 15 | pcb { 16 | compatible = "charge-controller"; 17 | 18 | type = "MPPT 2420 RC"; 19 | version-str = "v0.1"; 20 | version-num = <1>; 21 | 22 | hs-voltage-max = <80>; 23 | ls-voltage-max = <32>; 24 | dcdc-current-max = <20>; 25 | }; 26 | 27 | chosen { 28 | zephyr,console = &usart1; 29 | zephyr,shell-uart = &usart1; 30 | zephyr,sram = &sram0; 31 | zephyr,flash = &flash0; 32 | }; 33 | 34 | leds { 35 | compatible = "charlieplexing-leds"; 36 | gpios = <&gpiob 15 GPIO_ACTIVE_HIGH>; 37 | 38 | pwr { 39 | states = <1>; 40 | }; 41 | }; 42 | 43 | outputs { 44 | compatible = "outputs"; 45 | 46 | can_en { 47 | gpios = <&gpiob 14 GPIO_ACTIVE_LOW>; 48 | }; 49 | 50 | hv-out { 51 | gpios = <&gpiob 12 GPIO_ACTIVE_LOW>; 52 | }; 53 | }; 54 | 55 | adc-inputs { 56 | compatible = "adc-inputs"; 57 | 58 | v-low { 59 | io-channels = <&adc1 12>; 60 | multiplier = <105600>; 61 | divider = <5600>; 62 | }; 63 | 64 | v-high { 65 | io-channels = <&adc1 15>; 66 | multiplier = <102200>; 67 | divider = <2200>; 68 | }; 69 | 70 | hv-ext-sense { 71 | io-channels = <&adc1 11>; 72 | multiplier = <1>; 73 | divider = <1>; 74 | }; 75 | 76 | temp-fets { 77 | io-channels = <&adc1 2>; 78 | multiplier = <10000>; // 10k NTC series resistor 79 | divider = <1>; 80 | }; 81 | 82 | temp-bat { 83 | io-channels = <&adc1 14>; 84 | multiplier = <10000>; // 10k NTC series resistor 85 | divider = <1>; 86 | }; 87 | 88 | vref-mcu { 89 | io-channels = <&adc1 18>; 90 | multiplier = <1>; 91 | divider = <1>; 92 | }; 93 | 94 | temp-mcu { 95 | io-channels = <&adc1 16>; 96 | multiplier = <1>; 97 | divider = <1>; 98 | }; 99 | 100 | i-dcdc { 101 | io-channels = <&adc2 1>; 102 | // amp gain: 68/2.2, resistor: 2 mOhm 103 | multiplier = <2200>; // 1000*2.2 104 | divider = <136>; // 2*68 105 | }; 106 | }; 107 | 108 | aliases { 109 | uart-dbg = &usart1; 110 | uart-uext = &usart2; 111 | spi-uext = &spi1; 112 | i2c-uext = &i2c1; 113 | }; 114 | }; 115 | 116 | &clk_hsi{ 117 | status = "okay"; 118 | }; 119 | 120 | &pll { 121 | div-m = <4>; 122 | mul-n = <75>; 123 | div-p = <7>; 124 | div-q = <2>; 125 | div-r = <2>; 126 | clocks = <&clk_hsi>; 127 | status = "okay"; 128 | }; 129 | 130 | &rcc { 131 | clocks = <&pll>; 132 | clock-frequency = ; 133 | ahb-prescaler = <1>; 134 | apb1-prescaler = <1>; 135 | apb2-prescaler = <1>; 136 | }; 137 | 138 | &usart1 { 139 | pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>; 140 | pinctrl-names = "default"; 141 | current-speed = <115200>; 142 | status = "okay"; 143 | }; 144 | 145 | &usart1_rx_pa10 { 146 | bias-pull-up; // avoid junk characters if pin is left floating 147 | }; 148 | 149 | &usart2 { 150 | pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3>; 151 | pinctrl-names = "default"; 152 | current-speed = <115200>; 153 | status = "okay"; 154 | }; 155 | 156 | &usart2_rx_pa3 { 157 | bias-pull-up; // avoid junk characters if pin is left floating 158 | }; 159 | 160 | &spi1 { 161 | pinctrl-0 = <&spi1_sck_pb3 &spi1_miso_pb4 &spi1_mosi_pb5>; 162 | pinctrl-names = "default"; 163 | cs-gpios = <&gpiob 6 GPIO_ACTIVE_LOW>; 164 | status = "okay"; 165 | }; 166 | 167 | &timers1 { 168 | status = "okay"; 169 | 170 | halfbridge: halfbridge { 171 | compatible = "half-bridge"; 172 | pinctrl-0 = <&tim1_ch1_pa8 &tim1_ch1n_pb13>; 173 | pinctrl-names = "default"; 174 | frequency = <70000>; 175 | deadtime = <300>; 176 | }; 177 | }; 178 | 179 | &flash0 { 180 | /* 181 | * For more information, see: 182 | * http://docs.zephyrproject.org/latest/guides/dts/index.html#flash-partitions 183 | */ 184 | partitions { 185 | compatible = "fixed-partitions"; 186 | #address-cells = <1>; 187 | #size-cells = <1>; 188 | 189 | /* Set 4Kb of storage at the end of the 128Kb of flash */ 190 | storage_partition: partition@1f000 { 191 | reg = <0x0001f000 0x00001000>; 192 | }; 193 | }; 194 | }; 195 | 196 | &iwdg { 197 | status = "okay"; 198 | }; 199 | 200 | &i2c1 { 201 | /* shared between UEXT and EEPROM */ 202 | status = "okay"; 203 | pinctrl-0 = <&i2c1_scl_pa15 &i2c1_sda_pb7>; 204 | pinctrl-names = "default"; 205 | clock-frequency = ; 206 | 207 | eeprom: eeprom@50 { 208 | // Microchip 24AA32A 209 | compatible = "atmel,at24"; 210 | reg = <0x50>; 211 | size = <32768>; 212 | pagesize = <32>; 213 | address-width = <16>; 214 | /* 215 | * timeout of 5 ms as suggested in datasheet seems too optimistic 216 | * (several write errors occured during testing) 217 | */ 218 | timeout = <7>; 219 | }; 220 | }; 221 | 222 | &can1 { 223 | pinctrl-0 = <&fdcan1_rx_pa11 &fdcan1_tx_pa12>; 224 | pinctrl-names = "default"; 225 | bus-speed = <500000>; 226 | bus-speed-data = <500000>; 227 | sjw = <1>; 228 | sjw-data = <1>; 229 | sample-point = <875>; 230 | sample-point-data = <875>; 231 | status = "okay"; 232 | }; 233 | -------------------------------------------------------------------------------- /boards/arm/pwm_2420_lus/pwm_2420_lus.dts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /dts-v1/; 8 | #include 9 | #include 10 | 11 | / { 12 | model = "Libre Solar PWM 2420 LUS"; 13 | compatible = "st,stm32l072"; 14 | 15 | pcb { 16 | compatible = "charge-controller"; 17 | 18 | type = "PWM 2420 LUS"; 19 | 20 | hs-voltage-max = <55>; 21 | ls-voltage-max = <32>; 22 | // The MCU, where internal temperature is measured, is close to the MOSFETs. Tests 23 | // showed that temperature at heat sink is only 10-20°C above measured internal 24 | // temp. As PWM CC doesn't use electrolytic cap for core charging function, 25 | // internal temperature of 70°C can be allowed (instead of default 50°C). 26 | internal-tref-max = <70>; 27 | }; 28 | 29 | chosen { 30 | zephyr,console = &usart1; 31 | zephyr,shell-uart = &usart1; 32 | zephyr,sram = &sram0; 33 | zephyr,flash = &flash0; 34 | }; 35 | 36 | leds { 37 | compatible = "charlieplexing-leds"; 38 | gpios = <&gpiob 13 GPIO_ACTIVE_HIGH>, /* Pin A */ 39 | <&gpiob 14 GPIO_ACTIVE_HIGH>, /* Pin B */ 40 | <&gpiob 15 GPIO_ACTIVE_HIGH>; /* Pin C */ 41 | 42 | soc_1 { 43 | states = <1 0 2>; 44 | }; 45 | 46 | soc_2 { 47 | states = <0 1 2>; 48 | }; 49 | 50 | soc_3 { 51 | states = <1 2 0>; 52 | }; 53 | 54 | load { 55 | states = <2 1 0>; 56 | }; 57 | 58 | rxtx { 59 | states = <2 0 1>; 60 | }; 61 | }; 62 | 63 | outputs { 64 | compatible = "outputs"; 65 | 66 | pwm_switch { 67 | pwms = <&pwm3 4 (20*1000*1000) PWM_POLARITY_NORMAL>; /* 20 ms period = 50 Hz */ 68 | // Current reduced to 15A. Increase to 20A PCB max values 69 | // only if attached to a big heat sink. 70 | current-max = <15>; 71 | kconfig-flag; /* make node visible in Kconfig */ 72 | }; 73 | 74 | load { 75 | gpios = <&gpiob 2 GPIO_ACTIVE_LOW>; 76 | current-max = <15>; 77 | kconfig-flag; /* make node visible in Kconfig */ 78 | }; 79 | 80 | usb-pwr { 81 | gpios = <&gpiob 5 GPIO_ACTIVE_LOW>; 82 | }; 83 | 84 | boot0 { 85 | gpios = <&gpiob 12 GPIO_ACTIVE_HIGH>; 86 | }; 87 | }; 88 | 89 | adc-inputs { 90 | compatible = "adc-inputs"; 91 | 92 | v-low { 93 | io-channels = <&adc1 0>; 94 | multiplier = <132000>; 95 | divider = <12000>; 96 | }; 97 | 98 | v-pwm { 99 | io-channels = <&adc1 1>; 100 | multiplier = <25224>; // (12*8.2 + 120*8.2 + 120*12) * 10 101 | divider = <984>; // (8.2*12) * 10 102 | offset = <37414>; // 65536 / (1 + 8.2/120 + 8.2/12) 103 | }; 104 | 105 | i-load { 106 | io-channels = <&adc1 5>; 107 | // amp gain: 68/2.2, resistor: 2 mOhm 108 | // fix for hardware bug in overcurrent comparator voltage divider wiring 109 | multiplier = <2931>; // 1000 * (39+12+8.2) / (12+8.2) = 2930.69 110 | divider = <62>; // 2*68/2.2 = 61.8181 111 | }; 112 | 113 | i-pwm { 114 | io-channels = <&adc1 6>; 115 | // amp gain: 68/2.2, resistor: 2 mOhm 116 | multiplier = <2200>; // 1000*2.2 117 | divider = <136>; // 2*68 118 | }; 119 | 120 | temp-bat { 121 | io-channels = <&adc1 7>; 122 | multiplier = <8200>; // 8.2k NTC series resistor 123 | divider = <1>; 124 | }; 125 | 126 | vref-mcu { 127 | io-channels = <&adc1 17>; 128 | multiplier = <1>; 129 | divider = <1>; 130 | }; 131 | 132 | temp-mcu { 133 | io-channels = <&adc1 18>; 134 | multiplier = <1>; 135 | divider = <1>; 136 | }; 137 | }; 138 | 139 | aliases { 140 | uart-dbg = &usart1; 141 | uart-uext = &usart2; 142 | spi-uext = &spi1; 143 | i2c-uext = &i2c1; 144 | }; 145 | }; 146 | 147 | &clk_hsi { 148 | status = "okay"; 149 | }; 150 | 151 | &pll { 152 | div = <2>; 153 | mul = <4>; 154 | clocks = <&clk_hsi>; 155 | status = "okay"; 156 | }; 157 | 158 | &rcc { 159 | clocks = <&pll>; 160 | clock-frequency = ; 161 | ahb-prescaler = <1>; 162 | apb1-prescaler = <1>; 163 | apb2-prescaler = <1>; 164 | }; 165 | 166 | &usart1 { 167 | pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>; 168 | pinctrl-names = "default"; 169 | current-speed = <115200>; 170 | status = "okay"; 171 | }; 172 | 173 | &usart1_rx_pa10 { 174 | bias-pull-up; // avoid junk characters if pin is left floating 175 | }; 176 | 177 | 178 | &usart2 { 179 | pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3>; 180 | pinctrl-names = "default"; 181 | current-speed = <115200>; 182 | status = "okay"; 183 | }; 184 | 185 | &usart2_rx_pa3 { 186 | bias-pull-up; // avoid junk characters if pin is left floating 187 | }; 188 | 189 | &i2c1 { 190 | pinctrl-0 = <&i2c1_scl_pb6 &i2c1_sda_pb7>; 191 | pinctrl-names = "default"; 192 | status = "okay"; 193 | clock-frequency = ; 194 | }; 195 | 196 | &spi1 { 197 | pinctrl-0 = <&spi1_sck_pb3 &spi1_miso_pa11 &spi1_mosi_pa12>; 198 | pinctrl-names = "default"; 199 | cs-gpios = <&gpioa 15 GPIO_ACTIVE_LOW>; 200 | status = "okay"; 201 | }; 202 | 203 | &iwdg { 204 | status = "okay"; 205 | }; 206 | 207 | &timers3 { 208 | status = "okay"; 209 | st,prescaler = <10000>; 210 | pwm3: pwm { 211 | status = "okay"; 212 | pinctrl-0 = <&tim3_ch4_pb1>; 213 | pinctrl-names = "default"; 214 | }; 215 | }; 216 | 217 | &eeprom { 218 | status = "okay"; 219 | }; 220 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_lc/mppt_2420_lc.dts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /dts-v1/; 8 | #include 9 | #include 10 | 11 | / { 12 | model = "Libre Solar MPPT 2420 LC"; 13 | compatible = "st,stm32f072"; 14 | 15 | pcb { 16 | compatible = "charge-controller"; 17 | 18 | type = "MPPT 2420 LC"; 19 | version-str = "v0.10"; 20 | version-num = <10>; 21 | 22 | hs-voltage-max = <55>; 23 | ls-voltage-max = <32>; 24 | dcdc-current-max = <20>; 25 | }; 26 | 27 | chosen { 28 | zephyr,console = &usart1; 29 | zephyr,shell-uart = &usart1; 30 | zephyr,sram = &sram0; 31 | zephyr,flash = &flash0; 32 | }; 33 | 34 | leds { 35 | compatible = "charlieplexing-leds"; 36 | gpios = <&gpiob 14 GPIO_ACTIVE_HIGH>, 37 | <&gpiob 15 GPIO_ACTIVE_HIGH>; 38 | 39 | pwr { 40 | states = <1 0>; 41 | }; 42 | 43 | load { 44 | states = <0 1>; 45 | }; 46 | }; 47 | 48 | outputs { 49 | compatible = "outputs"; 50 | 51 | load { 52 | gpios = <&gpiob 2 GPIO_ACTIVE_LOW>; 53 | current-max = <20>; 54 | kconfig-flag; /* make node visible in Kconfig */ 55 | }; 56 | 57 | uext_en { 58 | gpios = <&gpioc 14 GPIO_ACTIVE_LOW>; 59 | }; 60 | 61 | can_en { 62 | gpios = <&gpioa 15 GPIO_ACTIVE_LOW>; 63 | }; 64 | 65 | aux_en { 66 | gpios = <&gpiob 12 GPIO_ACTIVE_HIGH>; 67 | }; 68 | 69 | vbus_en { 70 | gpios = <&gpioc 15 GPIO_ACTIVE_LOW>; 71 | }; 72 | }; 73 | 74 | adc-inputs { 75 | compatible = "adc-inputs"; 76 | 77 | temp-bat { 78 | io-channels = <&adc1 0>; 79 | multiplier = <10000>; // 10k NTC series resistor 80 | divider = <1>; 81 | }; 82 | 83 | temp-fets { 84 | io-channels = <&adc1 1>; 85 | multiplier = <10000>; // 10k NTC series resistor 86 | divider = <1>; 87 | }; 88 | 89 | vref { 90 | io-channels = <&adc1 5>; 91 | multiplier = <1>; 92 | divider = <1>; 93 | }; 94 | 95 | v-low { 96 | io-channels = <&adc1 6>; 97 | multiplier = <110000>; 98 | divider = <10000>; 99 | }; 100 | 101 | v-high { 102 | io-channels = <&adc1 7>; 103 | multiplier = <105600>; 104 | divider = <5600>; 105 | }; 106 | 107 | i-load { 108 | io-channels = <&adc1 8>; 109 | // amp gain: 150/2.2, resistor: 2 mOhm 110 | multiplier = <2200>; // 1000*2.2 111 | divider = <300>; // 2*150 112 | }; 113 | 114 | i-dcdc { 115 | io-channels = <&adc1 9>; 116 | // amp gain: 150/2.2, resistor: 2 mOhm 117 | multiplier = <2200>; // 1000*2.2 118 | divider = <300>; // 2*150 119 | }; 120 | 121 | temp-mcu { 122 | io-channels = <&adc1 16>; 123 | multiplier = <1>; 124 | divider = <1>; 125 | }; 126 | 127 | vref-mcu { 128 | io-channels = <&adc1 17>; 129 | multiplier = <1>; 130 | divider = <1>; 131 | }; 132 | }; 133 | 134 | aliases { 135 | uart-dbg = &usart1; 136 | uart-uext = &usart2; 137 | spi-uext = &spi1; 138 | i2c-uext = &i2c1; 139 | }; 140 | }; 141 | 142 | &clk_hse { 143 | clock-frequency = ; 144 | status = "okay"; 145 | }; 146 | 147 | &pll { 148 | clocks = <&clk_hse>; 149 | prediv = <1>; 150 | mul = <6>; 151 | status = "okay"; 152 | }; 153 | 154 | &rcc { 155 | clocks = <&pll>; 156 | clock-frequency = ; 157 | ahb-prescaler = <1>; 158 | apb1-prescaler = <2>; 159 | }; 160 | 161 | &usart1 { 162 | pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>; 163 | pinctrl-names = "default"; 164 | current-speed = <115200>; 165 | status = "okay"; 166 | }; 167 | 168 | &usart1_rx_pa10 { 169 | bias-pull-up; // avoid junk characters if pin is left floating 170 | }; 171 | 172 | &usart2 { 173 | pinctrl-0 = <&usart2_tx_pa2 &usart2_rx_pa3>; 174 | pinctrl-names = "default"; 175 | current-speed = <115200>; 176 | status = "okay"; 177 | }; 178 | 179 | &usart2_rx_pa3 { 180 | bias-pull-up; // avoid junk characters if pin is left floating 181 | }; 182 | 183 | &i2c1 { 184 | status = "okay"; 185 | pinctrl-0 = <&i2c1_scl_pb6 &i2c1_sda_pb7>; 186 | pinctrl-names = "default"; 187 | clock-frequency = ; 188 | }; 189 | 190 | &i2c2 { 191 | status = "okay"; 192 | pinctrl-0 = <&i2c2_scl_pb10 &i2c2_sda_pb11>; 193 | pinctrl-names = "default"; 194 | clock-frequency = ; 195 | 196 | eeprom: eeprom@50 { 197 | // Microchip 24AA32A 198 | compatible = "atmel,at24"; 199 | reg = <0x50>; 200 | size = <32768>; 201 | pagesize = <32>; // 24AA01: 8 bytes 202 | address-width = <16>; // 24AA01: 8 bit 203 | /* 204 | * timeout of 5 ms as suggested in datasheet seems too optimistic 205 | * (several write errors occured during testing) 206 | */ 207 | timeout = <7>; 208 | }; 209 | }; 210 | 211 | &spi1 { 212 | pinctrl-0 = <&spi1_sck_pb3 &spi1_miso_pb4 &spi1_mosi_pb5>; 213 | pinctrl-names = "default"; 214 | cs-gpios = <&gpioc 13 GPIO_ACTIVE_LOW>; 215 | status = "okay"; 216 | }; 217 | 218 | &iwdg { 219 | status = "okay"; 220 | }; 221 | 222 | &timers1 { 223 | status = "okay"; 224 | 225 | halfbridge: halfbridge { 226 | compatible = "half-bridge"; 227 | pinctrl-0 = <&tim1_ch1_pa8 &tim1_ch1n_pb13>; 228 | pinctrl-names = "default"; 229 | frequency = <70000>; 230 | deadtime = <300>; 231 | }; 232 | }; 233 | 234 | &can1 { 235 | pinctrl-0 = <&can_rx_pb8 &can_tx_pb9>; 236 | pinctrl-names = "default"; 237 | bus-speed = <500000>; 238 | status = "okay"; 239 | }; 240 | -------------------------------------------------------------------------------- /app/src/pwm_switch_driver.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include "pwm_switch.h" 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include "board.h" 15 | #include "daq.h" 16 | #include "helper.h" 17 | #include "mcu.h" 18 | 19 | #if BOARD_HAS_PWM_PORT 20 | 21 | #ifndef UNIT_TEST 22 | 23 | #include 24 | #include 25 | #include 26 | 27 | #include 28 | #include 29 | 30 | // all PWM charge controllers use TIM3 at the moment 31 | static TIM_TypeDef *tim = TIM3; 32 | 33 | #if DT_PWMS_CHANNEL(DT_CHILD(DT_PATH(outputs), pwm_switch)) == 1 34 | #define LL_TIM_CHANNEL LL_TIM_CHANNEL_CH1 35 | #define LL_TIM_OC_SetCompare LL_TIM_OC_SetCompareCH1 36 | #define LL_TIM_OC_GetCompare LL_TIM_OC_GetCompareCH1 37 | #elif DT_PWMS_CHANNEL(DT_CHILD(DT_PATH(outputs), pwm_switch)) == 2 38 | #define LL_TIM_CHANNEL LL_TIM_CHANNEL_CH2 39 | #define LL_TIM_OC_SetCompare LL_TIM_OC_SetCompareCH2 40 | #define LL_TIM_OC_GetCompare LL_TIM_OC_GetCompareCH2 41 | #elif DT_PWMS_CHANNEL(DT_CHILD(DT_PATH(outputs), pwm_switch)) == 3 42 | #define LL_TIM_CHANNEL LL_TIM_CHANNEL_CH3 43 | #define LL_TIM_OC_SetCompare LL_TIM_OC_SetCompareCH3 44 | #define LL_TIM_OC_GetCompare LL_TIM_OC_GetCompareCH3 45 | #elif DT_PWMS_CHANNEL(DT_CHILD(DT_PATH(outputs), pwm_switch)) == 4 46 | #define LL_TIM_CHANNEL LL_TIM_CHANNEL_CH4 47 | #define LL_TIM_OC_SetCompare LL_TIM_OC_SetCompareCH4 48 | #define LL_TIM_OC_GetCompare LL_TIM_OC_GetCompareCH4 49 | #else 50 | #error "PWM Switch channel not defined properly!" 51 | #endif 52 | 53 | // to check if PWM signal is high or low (not sure how to get pin config from devicetree...) 54 | #if defined(CONFIG_BOARD_PWM_2420_LUS) 55 | #define PWM_GPIO_PIN_HIGH (GPIOB->IDR & GPIO_IDR_ID1) 56 | #elif defined(CONFIG_BOARD_MPPT_2420_HPX) 57 | #define PWM_GPIO_PIN_HIGH (GPIOC->IDR & GPIO_IDR_ID7) 58 | #endif 59 | 60 | #define PINCTRL_NODE DT_NODELABEL(pwm3) 61 | 62 | PINCTRL_DT_DEFINE(PINCTRL_NODE); 63 | 64 | static const struct pinctrl_dev_config *pincfg = PINCTRL_DT_DEV_CONFIG_GET(PINCTRL_NODE); 65 | 66 | static bool _pwm_active; 67 | static int _pwm_resolution; 68 | 69 | static void TIM3_IRQHandler(void *args) 70 | { 71 | LL_TIM_ClearFlag_UPDATE(tim); 72 | 73 | if ((int)LL_TIM_OC_GetCompare(tim) < _pwm_resolution) { 74 | // turning the PWM switch on creates a short voltage rise, so inhibit alerts by 10 ms 75 | // at each rising edge if switch is not continuously on 76 | adc_upper_alert_inhibit(ADC_POS(v_low), 10); 77 | } 78 | } 79 | 80 | void pwm_signal_init_registers(int freq_Hz) 81 | { 82 | pinctrl_apply_state(pincfg, PINCTRL_STATE_DEFAULT); 83 | 84 | LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_TIM3); 85 | 86 | // Set timer clock to 10 kHz 87 | LL_TIM_SetPrescaler(TIM3, SystemCoreClock / 10000 - 1); 88 | 89 | LL_TIM_OC_SetMode(tim, LL_TIM_CHANNEL, LL_TIM_OCMODE_PWM1); 90 | LL_TIM_OC_EnablePreload(tim, LL_TIM_CHANNEL); 91 | LL_TIM_OC_SetPolarity(tim, LL_TIM_CHANNEL, LL_TIM_OCPOLARITY_HIGH); 92 | 93 | // Interrupt on timer update 94 | LL_TIM_EnableIT_UPDATE(tim); 95 | 96 | // Force update generation (UG = 1) 97 | LL_TIM_GenerateEvent_UPDATE(tim); 98 | 99 | // set PWM frequency and resolution 100 | _pwm_resolution = 10000 / freq_Hz; 101 | 102 | // Period goes from 0 to ARR (including ARR value), so substract 1 clock cycle 103 | LL_TIM_SetAutoReload(tim, _pwm_resolution - 1); 104 | 105 | // 1 = second-highest priority of STM32L0/F0 106 | IRQ_CONNECT(TIM3_IRQn, 1, TIM3_IRQHandler, 0, 0); 107 | irq_enable(TIM3_IRQn); 108 | 109 | LL_TIM_EnableCounter(tim); 110 | } 111 | 112 | void pwm_signal_set_duty_cycle(float duty) 113 | { 114 | LL_TIM_OC_SetCompare(tim, _pwm_resolution * duty); 115 | } 116 | 117 | void pwm_signal_duty_cycle_step(int delta) 118 | { 119 | uint32_t ccr_new = (int)LL_TIM_OC_GetCompare(tim) + delta; 120 | if (ccr_new <= _pwm_resolution && ccr_new >= 0) { 121 | LL_TIM_OC_SetCompare(tim, ccr_new); 122 | } 123 | } 124 | 125 | float pwm_signal_get_duty_cycle() 126 | { 127 | return (float)(LL_TIM_OC_GetCompare(tim)) / _pwm_resolution; 128 | } 129 | 130 | void pwm_signal_start(float pwm_duty) 131 | { 132 | pwm_signal_set_duty_cycle(pwm_duty); 133 | LL_TIM_CC_EnableChannel(tim, LL_TIM_CHANNEL); 134 | _pwm_active = true; 135 | } 136 | 137 | void pwm_signal_stop() 138 | { 139 | LL_TIM_CC_DisableChannel(tim, LL_TIM_CHANNEL); 140 | _pwm_active = false; 141 | } 142 | 143 | bool pwm_signal_high() 144 | { 145 | return PWM_GPIO_PIN_HIGH; 146 | } 147 | 148 | bool pwm_active() 149 | { 150 | return _pwm_active; 151 | } 152 | 153 | #else 154 | 155 | // dummy functions for unit tests 156 | float pwm_signal_get_duty_cycle() 157 | { 158 | return 0; 159 | } 160 | 161 | void pwm_signal_set_duty_cycle(float duty) 162 | {} 163 | 164 | void pwm_signal_duty_cycle_step(int delta) 165 | {} 166 | 167 | void pwm_signal_init_registers(int freq_Hz) 168 | {} 169 | 170 | void pwm_signal_start(float pwm_duty) 171 | {} 172 | 173 | void pwm_signal_stop() 174 | {} 175 | 176 | bool pwm_signal_high() 177 | { 178 | return false; 179 | } 180 | 181 | bool pwm_active() 182 | { 183 | return false; 184 | } 185 | 186 | #endif 187 | 188 | #endif /* BOARD_HAS_PWM_PORT */ 189 | -------------------------------------------------------------------------------- /app/src/ext/serial.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #if CONFIG_THINGSET_SERIAL 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include "data_objects.h" 18 | #include "hardware.h" 19 | #include "thingset.h" 20 | 21 | #if CONFIG_UEXT_SERIAL_THINGSET 22 | #define UART_DEVICE_NODE DT_ALIAS(uart_uext) 23 | #elif DT_NODE_EXISTS(DT_ALIAS(uart_dbg)) 24 | #define UART_DEVICE_NODE DT_ALIAS(uart_dbg) 25 | #else 26 | // cppcheck-suppress preprocessorErrorDirective 27 | #error "No UART for ThingSet serial defined." 28 | #endif 29 | 30 | static const struct device *uart_dev = DEVICE_DT_GET(UART_DEVICE_NODE); 31 | 32 | static char tx_buf[CONFIG_THINGSET_SERIAL_TX_BUF_SIZE]; 33 | static char rx_buf[CONFIG_THINGSET_SERIAL_RX_BUF_SIZE]; 34 | 35 | static volatile size_t rx_buf_pos = 0; 36 | static bool discard_buffer; 37 | 38 | static struct k_sem command_flag; // used as an event to signal a received command 39 | static struct k_sem rx_buf_mutex; // binary semaphore used as mutex in ISR context 40 | 41 | extern ThingSet ts; 42 | 43 | const char serial_subset_path[] = "mSerial"; 44 | static ThingSetDataObject *serial_subset; 45 | 46 | void serial_pub_msg() 47 | { 48 | if (pub_serial_enable) { 49 | int len = ts.txt_statement(tx_buf, sizeof(tx_buf), serial_subset); 50 | for (int i = 0; i < len; i++) { 51 | uart_poll_out(uart_dev, tx_buf[i]); 52 | } 53 | uart_poll_out(uart_dev, '\n'); 54 | } 55 | } 56 | 57 | void serial_process_command() 58 | { 59 | // commands must have 2 or more characters 60 | if (rx_buf_pos > 0) { 61 | printf("Received Request (%d bytes): %s\n", strlen(rx_buf), rx_buf); 62 | 63 | int len = ts.process((uint8_t *)rx_buf, strlen(rx_buf), (uint8_t *)tx_buf, sizeof(tx_buf)); 64 | 65 | for (int i = 0; i < len; i++) { 66 | uart_poll_out(uart_dev, tx_buf[i]); 67 | } 68 | uart_poll_out(uart_dev, '\n'); 69 | } 70 | 71 | // release buffer and start waiting for new commands 72 | rx_buf_pos = 0; 73 | k_sem_give(&rx_buf_mutex); 74 | } 75 | 76 | /* 77 | * Read characters from stream until line end \n is detected, afterwards signal available command. 78 | */ 79 | void serial_cb(const struct device *dev, void *user_data) 80 | { 81 | uint8_t c; 82 | 83 | if (!uart_irq_update(uart_dev)) { 84 | return; 85 | } 86 | 87 | while (uart_irq_rx_ready(uart_dev)) { 88 | 89 | uart_fifo_read(uart_dev, &c, 1); 90 | 91 | if (k_sem_take(&rx_buf_mutex, K_NO_WAIT) != 0) { 92 | // buffer not available: drop character 93 | discard_buffer = true; 94 | continue; 95 | } 96 | 97 | // \r\n and \n are markers for line end, i.e. command end 98 | // we accept this at any time, even if the buffer is 'full', since 99 | // there is always one last character left for the \0 100 | if (c == '\n') { 101 | if (rx_buf_pos > 0 && rx_buf[rx_buf_pos - 1] == '\r') { 102 | rx_buf[rx_buf_pos - 1] = '\0'; 103 | } 104 | else { 105 | rx_buf[rx_buf_pos] = '\0'; 106 | } 107 | if (discard_buffer) { 108 | rx_buf_pos = 0; 109 | discard_buffer = false; 110 | k_sem_give(&rx_buf_mutex); 111 | } 112 | else { 113 | // start processing command and keep the rx_buf_mutex locked 114 | k_sem_give(&command_flag); 115 | } 116 | return; 117 | } 118 | // backspace allowed if there is something in the buffer already 119 | else if (rx_buf_pos > 0 && c == '\b') { 120 | rx_buf_pos--; 121 | } 122 | // Fill the buffer up to all but 1 character (the last character is reserved for '\0') 123 | // Characters beyond the size of the buffer are dropped. 124 | else if (rx_buf_pos < (sizeof(rx_buf) - 1)) { 125 | rx_buf[rx_buf_pos++] = c; 126 | } 127 | 128 | k_sem_give(&rx_buf_mutex); 129 | } 130 | } 131 | 132 | void serial_thread() 133 | { 134 | k_sem_init(&command_flag, 0, 1); 135 | k_sem_init(&rx_buf_mutex, 1, 1); 136 | 137 | __ASSERT_NO_MSG(device_is_ready(uart_dev)); 138 | 139 | uart_irq_callback_user_data_set(uart_dev, serial_cb, NULL); 140 | uart_irq_rx_enable(uart_dev); 141 | 142 | serial_subset = ts.get_endpoint(serial_subset_path, strlen(serial_subset_path)); 143 | 144 | // below process loop should be run at least once per sec, setting watchdog timeout to 1.5s 145 | int wdt_channel = task_wdt_add(1500, task_wdt_callback, (void *)k_current_get()); 146 | 147 | int64_t t_start = k_uptime_get(); 148 | 149 | while (true) { 150 | if (k_sem_take(&command_flag, K_TIMEOUT_ABS_MS(t_start)) == 0) { 151 | serial_process_command(); 152 | } 153 | else { 154 | // semaphore timed out (should happen exactly every 1 second) 155 | t_start += 1000; 156 | serial_pub_msg(); 157 | } 158 | task_wdt_feed(wdt_channel); 159 | } 160 | } 161 | 162 | K_THREAD_DEFINE(serial_thread_id, 1280, serial_thread, NULL, NULL, NULL, 6, 0, 1000); 163 | 164 | #endif /* CONFIG_THINGSET_SERIAL */ 165 | -------------------------------------------------------------------------------- /app/src/device_status.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef DEVICE_STATUS_H 8 | #define DEVICE_STATUS_H 9 | 10 | /** @file 11 | * 12 | * @brief 13 | * Device-level data storage and functions (like max/min values, error flags, etc.) 14 | */ 15 | 16 | #include "bat_charger.h" 17 | #include "dcdc.h" 18 | #include "load.h" 19 | #include "power_port.h" 20 | #include 21 | #include 22 | 23 | /** Error Flags 24 | * 25 | * When adding new flags, please make sure to use only up to 32 errors 26 | * Each enum must represent a unique power of 2 number 27 | */ 28 | enum ErrorFlag 29 | { 30 | /** Battery voltage too low 31 | * 32 | * Set and cleared in Charger::discharge_control() if the battery voltage dropped to lower than 33 | * absolute minimum. Load outputs should have switched off earlier. 34 | */ 35 | ERR_BAT_UNDERVOLTAGE = 1U << 0, 36 | 37 | /** Battery voltage too high 38 | * 39 | * Set directly in ISR after ADC conversion finished, cleared in Charger::charge_control() when 40 | * voltage reached lower level again. 41 | */ 42 | ERR_BAT_OVERVOLTAGE = 1U << 1, 43 | 44 | /** Battery discharge overcurrent 45 | * 46 | * Not used yet, reserved for future. 47 | */ 48 | ERR_BAT_DIS_OVERCURRENT = 1U << 2, 49 | 50 | /** Battery charge overcurrent 51 | * 52 | * Not used yet, reserved for future. 53 | */ 54 | ERR_BAT_CHG_OVERCURRENT = 1U << 3, 55 | 56 | /** Temperature below discharge minimum limit 57 | * 58 | * Set and cleared in Charger::discharge_control (with 2°C hysteresis) 59 | */ 60 | ERR_BAT_DIS_UNDERTEMP = 1U << 4, 61 | 62 | /** Temperature above discharge maximum limit 63 | * 64 | * Set and cleared in Charger::discharge_control (with 2°C hysteresis) 65 | */ 66 | ERR_BAT_DIS_OVERTEMP = 1U << 5, 67 | 68 | /** Temperature below charge minimum limit 69 | * 70 | * Set and cleared in Charger::charge_control (with 2°C hysteresis) 71 | */ 72 | ERR_BAT_CHG_UNDERTEMP = 1U << 6, 73 | 74 | /** Temperature above charge maximum limit 75 | * 76 | * Set and cleared in Charger::charge_control (with 2°C hysteresis) 77 | */ 78 | ERR_BAT_CHG_OVERTEMP = 1U << 7, 79 | 80 | /** Charge controller internal temperature too high 81 | * 82 | * Set and cleared by daq_update() 83 | */ 84 | ERR_INT_OVERTEMP = 1U << 13, 85 | 86 | /** Short-circuit in HS MOSFET 87 | * 88 | * Set in Dcdc::control() and never cleared 89 | */ 90 | ERR_DCDC_HS_MOSFET_SHORT = 1U << 14, 91 | 92 | /** Overvoltage in HS MOSFET 93 | * 94 | * Set in DAQ alert and never cleared 95 | */ 96 | ERR_DCDC_HS_OVERVOLTAGE = 1U << 15, 97 | 98 | /** Overvoltage caused by PWM switch 99 | * 100 | * Set and cleared in PwmSwitch::control() 101 | */ 102 | ERR_PWM_SWITCH_OVERVOLTAGE = 1U << 16, 103 | 104 | /** Mask to catch all error flags (up to 32 errors) 105 | */ 106 | ERR_ANY_ERROR = UINT32_MAX, 107 | }; 108 | 109 | /** Device Status data 110 | * 111 | * Stores error counters and some maximum ever measured values to EEPROM 112 | */ 113 | class DeviceStatus 114 | { 115 | public: 116 | /** Updates the total energy counters for solar, battery and load bus 117 | */ 118 | void update_energy(); 119 | 120 | /** Updates the logged min/max values for voltages, power, temperatures etc. 121 | */ 122 | void update_min_max_values(); 123 | 124 | // total energy 125 | uint32_t bat_chg_total_Wh; 126 | uint32_t bat_dis_total_Wh; 127 | uint32_t solar_in_total_Wh; 128 | uint32_t load_out_total_Wh; 129 | #if CONFIG_HV_TERMINAL_NANOGRID 130 | uint32_t grid_import_total_Wh; 131 | uint32_t grid_export_total_Wh; 132 | #endif 133 | 134 | // maximum/minimum values 135 | uint16_t solar_power_max_day; 136 | uint16_t load_power_max_day; 137 | uint16_t solar_power_max_total; 138 | uint16_t load_power_max_total; 139 | float battery_voltage_max; 140 | float solar_voltage_max; 141 | float dcdc_current_max; 142 | float load_current_max; 143 | int16_t bat_temp_max; // °C 144 | int16_t int_temp_max; // °C 145 | int16_t mosfet_temp_max; 146 | 147 | uint32_t day_counter; 148 | 149 | // instantaneous device-level data 150 | uint32_t error_flags; ///< Currently detected errors 151 | float internal_temp; ///< Internal temperature (measured in MCU) 152 | 153 | /** 154 | * @brief sets one or more error flags in device state 155 | * @param e a single ErrorFlag or "bitwise ORed" ERR_XXX | ERR_YYY 156 | */ 157 | inline void set_error(uint32_t e) 158 | { 159 | error_flags |= e; 160 | } 161 | 162 | /** 163 | * @brief clears one or more error flags in device state 164 | * @param e a single ErrorFlag or "bitwise ORed" ERR_XXX | ERR_YYY 165 | */ 166 | void clear_error(uint32_t e) 167 | { 168 | error_flags &= ~e; 169 | } 170 | 171 | /** 172 | * @brief queries one or more error flags in device state 173 | * @param e a single ErrorFlag or "bitwise ORed" ERR_XXX | ERR_YYY 174 | * @return true if any of the error flags given in e are set in device state 175 | */ 176 | bool has_error(uint32_t e) 177 | { 178 | return (error_flags & e) != 0; 179 | } 180 | }; 181 | 182 | #endif /* DEVICE_STATUS_H */ 183 | -------------------------------------------------------------------------------- /app/src/pwm_switch.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include "pwm_switch.h" 8 | 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include "daq.h" 15 | #include "helper.h" 16 | #include "mcu.h" 17 | #include "setup.h" 18 | 19 | LOG_MODULE_REGISTER(pwm_switch, CONFIG_PWM_LOG_LEVEL); 20 | 21 | #if BOARD_HAS_PWM_PORT 22 | 23 | #define PWM_CURRENT_MAX (DT_PROP(DT_CHILD(DT_PATH(outputs), pwm_switch), current_max)) 24 | #define PWM_PERIOD (DT_PHA(DT_CHILD(DT_PATH(outputs), pwm_switch), pwms, period)) 25 | 26 | bool PwmSwitch::active() 27 | { 28 | return pwm_active(); 29 | } 30 | 31 | bool PwmSwitch::signal_high() 32 | { 33 | return pwm_signal_high(); 34 | } 35 | 36 | PwmSwitch::PwmSwitch(DcBus *dc_bus) : PowerPort(dc_bus) 37 | { 38 | // period stored in nanoseconds 39 | pwm_signal_init_registers(1000 * 1000 * 1000 / PWM_PERIOD); 40 | } 41 | 42 | void PwmSwitch::test() 43 | { 44 | if (pwm_active() && enable == false) { 45 | pwm_signal_stop(); 46 | off_timestamp = uptime(); 47 | LOG_INF("PWM test mode stop."); 48 | } 49 | else if (!pwm_active() && enable == true) { 50 | // turning the PWM switch on creates a short voltage rise, so inhibit alerts by 50 ms 51 | adc_upper_alert_inhibit(ADC_POS(v_low), 50); 52 | pwm_signal_start(0.9F); 53 | LOG_INF("PWM test mode start."); 54 | } 55 | } 56 | 57 | void PwmSwitch::control() 58 | { 59 | if (pwm_active()) { 60 | if (current < -0.1F) { 61 | power_good_timestamp = uptime(); // reset the time 62 | } 63 | 64 | if (neg_current_limit == 0 || (uptime() - power_good_timestamp > 10) // low power since 10s 65 | || current > 0.5F // discharging battery into solar panel --> stop 66 | || bus->voltage < 9.0F // not enough voltage for MOSFET drivers anymore 67 | || enable == false) 68 | { 69 | pwm_signal_stop(); 70 | off_timestamp = uptime(); 71 | LOG_INF("PWM charger stop, current = %d mA", (int)(current * 1000.0F)); 72 | } 73 | else if (bus->voltage > bus->sink_control_voltage() + 0.3F) { 74 | pwm_signal_stop(); 75 | off_timestamp = uptime(); 76 | dev_stat.set_error(ERR_PWM_SWITCH_OVERVOLTAGE); 77 | LOG_INF("PWM charger stop, overvoltage."); 78 | } 79 | else if (bus->voltage > bus->sink_control_voltage() // bus voltage above target 80 | || current < neg_current_limit // port current limit exceeded 81 | || current < -PWM_CURRENT_MAX) // PCB current limit exceeded 82 | { 83 | // decrease power, as limits were reached 84 | 85 | // the gate driver switch-off time is quite high (fall time around 1ms), so very short 86 | // on or off periods (duty cycle close to 0 and 1) should be avoided 87 | if (pwm_signal_get_duty_cycle() > 0.95F) { 88 | // prevent very short off periods 89 | pwm_signal_set_duty_cycle(0.95F); 90 | } 91 | else if (pwm_signal_get_duty_cycle() < 0.05F) { 92 | // prevent very short on periods and switch completely off instead 93 | pwm_signal_stop(); 94 | off_timestamp = uptime(); 95 | // consider this as overvoltage in order to start again with 5% duty cycle 96 | dev_stat.set_error(ERR_PWM_SWITCH_OVERVOLTAGE); 97 | LOG_INF("PWM charger stop, no further derating possible."); 98 | } 99 | else { 100 | // decrease the power with large steps to prevent long-term overvoltages if 101 | // the PWM switch was started with full battery and high solar irradiation 102 | pwm_signal_duty_cycle_step(-10); 103 | } 104 | } 105 | else { 106 | // increase power (if not yet at 100% duty cycle) 107 | 108 | if (pwm_signal_get_duty_cycle() > 0.95F) { 109 | // prevent very short off periods and switch completely on instead 110 | pwm_signal_set_duty_cycle(1); 111 | } 112 | else { 113 | pwm_signal_duty_cycle_step(1); 114 | } 115 | } 116 | 117 | if (dev_stat.has_error(ERR_PWM_SWITCH_OVERVOLTAGE) 118 | && bus->voltage < bus->sink_control_voltage() - 0.5F) 119 | { 120 | dev_stat.clear_error(ERR_PWM_SWITCH_OVERVOLTAGE); 121 | } 122 | } 123 | else { 124 | if (bus->sink_current_margin > 0 && // charging allowed 125 | bus->voltage < bus->sink_control_voltage() 126 | && ext_voltage > bus->voltage + offset_voltage_start 127 | && uptime() > (off_timestamp + restart_interval) && enable == true) 128 | { 129 | // turning the PWM switch on creates a short voltage rise, so inhibit alerts by 50 ms 130 | adc_upper_alert_inhibit(ADC_POS(v_low), 50); 131 | 132 | if (dev_stat.has_error(ERR_PWM_SWITCH_OVERVOLTAGE)) { 133 | // start with minimum duty cycle in order to prevent another overvoltage event 134 | pwm_signal_start(0.05F); 135 | } 136 | else { 137 | pwm_signal_start(1.0F); 138 | } 139 | 140 | power_good_timestamp = uptime(); 141 | LOG_INF("PWM charger start."); 142 | } 143 | } 144 | } 145 | 146 | void PwmSwitch::stop() 147 | { 148 | pwm_signal_stop(); 149 | off_timestamp = uptime(); 150 | } 151 | 152 | float PwmSwitch::get_duty_cycle() 153 | { 154 | return pwm_signal_get_duty_cycle(); 155 | } 156 | 157 | #endif /* BOARD_HAS_PWM_PORT */ 158 | -------------------------------------------------------------------------------- /app/src/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef UNIT_TEST 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "helper.h" 16 | #include "setup.h" 17 | #include "thingset.h" // handles access to internal data via communication interfaces 18 | 19 | #include "bat_charger.h" // battery settings and charger state machine 20 | #include "daq.h" // ADC using DMA and conversion to measurement values 21 | #include "data_objects.h" // for access to internal data via ThingSet 22 | #include "data_storage.h" // non-volatile data storage (e.g. EEPROM) 23 | #include "dcdc.h" // DC/DC converter control (hardware independent) 24 | #include "device_status.h" // log data (error memory, min/max measurements, etc.) 25 | #include "half_bridge.h" // PWM generation for DC/DC converter 26 | #include "hardware.h" // hardware-related functions like load switch, LED control, watchdog, etc. 27 | #include "leds.h" // LED switching using charlieplexing 28 | #include "load.h" // load and USB output management 29 | #include "pwm_switch.h" // PWM charge controller 30 | 31 | int main(void) 32 | { 33 | printf("Hardware: Libre Solar %s (%s)\n", DT_PROP(DT_PATH(pcb), type), 34 | DT_PROP(DT_PATH(pcb), version_str)); 35 | printf("Firmware: %s\n", FIRMWARE_VERSION_ID); 36 | 37 | task_wdt_init(DEVICE_DT_GET_OR_NULL(DT_NODELABEL(iwdg))); 38 | 39 | setup(); 40 | 41 | battery_conf_init(&bat_conf, CONFIG_BAT_TYPE, CONFIG_BAT_NUM_CELLS, CONFIG_BAT_CAPACITY_AH); 42 | battery_conf_overwrite(&bat_conf, &bat_conf_user); // initialize conf_user with same values 43 | 44 | #if BOARD_HAS_DCDC 45 | daq_set_hv_limit(DT_PROP(DT_PATH(pcb), hs_voltage_max)); 46 | #endif 47 | 48 | #if CONFIG_HV_TERMINAL_SOLAR || CONFIG_LV_TERMINAL_SOLAR || CONFIG_PWM_TERMINAL_SOLAR 49 | solar_terminal.init_solar(); 50 | #endif 51 | 52 | #if CONFIG_HV_TERMINAL_NANOGRID 53 | grid_terminal.init_nanogrid(); 54 | #endif 55 | 56 | // read custom configuration from EEPROM 57 | data_objects_init(); 58 | 59 | // Data Acquisition (DAQ) setup 60 | daq_setup(); 61 | 62 | charger.detect_num_batteries(&bat_conf); // check if we have 24V instead of 12V system 63 | charger.init_terminal(&bat_conf); 64 | 65 | #if BOARD_HAS_LOAD_OUTPUT 66 | load.set_voltage_limits(bat_conf.load_disconnect_voltage, bat_conf.load_reconnect_voltage, 67 | bat_conf.absolute_max_voltage); 68 | #endif 69 | 70 | #if BOARD_HAS_USB_OUTPUT 71 | usb_pwr.set_voltage_limits(bat_conf.load_disconnect_voltage - 0.1, // keep on longer than load 72 | bat_conf.load_reconnect_voltage, bat_conf.absolute_max_voltage); 73 | #endif 74 | 75 | // wait until all threads are spawned before activating the watchdog 76 | k_sleep(K_MSEC(2500)); 77 | 78 | int64_t t_start = k_uptime_get(); 79 | 80 | while (true) { 81 | // loop runs exactly once per second and includes slow control tasks and energy calculation 82 | 83 | charger.discharge_control(&bat_conf); 84 | charger.charge_control(&bat_conf); 85 | 86 | // energy + soc calculation must be called exactly once per second 87 | #if BOARD_HAS_DCDC 88 | if (dcdc.state != DCDC_CONTROL_OFF) { 89 | hv_terminal.energy_balance(); 90 | } 91 | #endif 92 | 93 | #if BOARD_HAS_PWM_PORT 94 | if (pwm_switch.active() == 1) { 95 | pwm_switch.energy_balance(); 96 | } 97 | #endif 98 | 99 | lv_terminal.energy_balance(); 100 | 101 | #if BOARD_HAS_LOAD_OUTPUT 102 | if (load.state == 1) { 103 | load.energy_balance(); 104 | } 105 | #endif 106 | 107 | dev_stat.update_energy(); 108 | dev_stat.update_min_max_values(); 109 | charger.update_soc(&bat_conf); 110 | 111 | #if CONFIG_HS_MOSFET_FAIL_SAFE_PROTECTION && BOARD_HAS_DCDC 112 | if (dev_stat.has_error(ERR_DCDC_HS_MOSFET_SHORT)) { 113 | dcdc.fuse_destruction(); 114 | } 115 | #endif 116 | 117 | leds_update_1s(); 118 | 119 | #if BOARD_HAS_LOAD_OUTPUT 120 | leds_update_soc(charger.soc, flags_check(&load.error_flags, ERR_LOAD_SHEDDING)); 121 | #else 122 | leds_update_soc(charger.soc, false); 123 | #endif 124 | 125 | data_storage_update(); 126 | 127 | t_start += 1000; 128 | k_sleep(K_TIMEOUT_ABS_MS(t_start)); 129 | } 130 | } 131 | 132 | void control_thread() 133 | { 134 | int wdt_channel = task_wdt_add(200, task_wdt_callback, (void *)k_current_get()); 135 | 136 | while (true) { 137 | // control loop runs at approx. 10 Hz 138 | 139 | bool charging = false; 140 | 141 | task_wdt_feed(wdt_channel); 142 | 143 | // convert ADC readings to meaningful measurement values 144 | daq_update(); 145 | 146 | // alerts should trigger only for transients, so update based on actual voltage 147 | daq_set_lv_limits(lv_terminal.bus->voltage * 1.2F, lv_terminal.bus->voltage * 0.8F); 148 | 149 | lv_terminal.update_bus_current_margins(); 150 | 151 | #if BOARD_HAS_PWM_PORT 152 | pwm_switch.control(); 153 | charging |= pwm_switch.active(); 154 | #endif 155 | 156 | #if BOARD_HAS_DCDC 157 | hv_terminal.update_bus_current_margins(); 158 | dcdc.control(); // control of DC/DC including MPPT algorithm 159 | charging |= half_bridge_enabled(); 160 | #endif 161 | 162 | leds_set_charging(charging); 163 | 164 | #if BOARD_HAS_LOAD_OUTPUT 165 | load.control(); 166 | #endif 167 | 168 | #if BOARD_HAS_USB_OUTPUT 169 | usb_pwr.control(); 170 | #endif 171 | 172 | k_sleep(K_MSEC(100)); 173 | } 174 | } 175 | 176 | // 2s delay for control thread as a safety feature: be able to re-flash before starting 177 | // priority is set to -1 (=cooperative) to have higher priority than other threads incl. main 178 | K_THREAD_DEFINE(control_thread_id, 1024, control_thread, NULL, NULL, NULL, -1, 0, 2000); 179 | 180 | #endif // UNIT_TEST 181 | -------------------------------------------------------------------------------- /app/src/device_status.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include "device_status.h" 8 | 9 | #include 10 | 11 | #include // for fabs function 12 | #include 13 | 14 | #include "helper.h" 15 | #include "setup.h" 16 | 17 | //---------------------------------------------------------------------------- 18 | // must be called exactly once per second, otherwise energy calculation gets wrong 19 | void DeviceStatus::update_energy() 20 | { 21 | // static variables so that it is not reset for each function call 22 | static int seconds_zero_solar = 0; 23 | 24 | // stores the input/output energy status of previous day and to add 25 | // xxx_day_Wh only once per day and increase accuracy 26 | static uint32_t solar_in_total_Wh_prev; 27 | static uint32_t load_out_total_Wh_prev; 28 | static uint32_t bat_chg_total_Wh_prev; 29 | static uint32_t bat_dis_total_Wh_prev; 30 | #if CONFIG_HV_TERMINAL_NANOGRID 31 | static uint32_t grid_import_total_Wh_prev; 32 | static uint32_t grid_export_total_Wh_prev; 33 | #endif 34 | 35 | static bool first_call = true; 36 | if (first_call) { 37 | // initialize values with values we got from EEPROM 38 | solar_in_total_Wh_prev = solar_in_total_Wh; 39 | load_out_total_Wh_prev = load_out_total_Wh; 40 | bat_chg_total_Wh_prev = bat_chg_total_Wh; 41 | bat_dis_total_Wh_prev = bat_dis_total_Wh; 42 | #if CONFIG_HV_TERMINAL_NANOGRID 43 | grid_import_total_Wh_prev = grid_import_total_Wh; 44 | grid_export_total_Wh_prev = grid_export_total_Wh; 45 | #endif 46 | first_call = false; 47 | } 48 | 49 | #if CONFIG_HV_TERMINAL_SOLAR || CONFIG_LV_TERMINAL_SOLAR || CONFIG_PWM_TERMINAL_SOLAR 50 | #if CONFIG_HV_TERMINAL_SOLAR || CONFIG_LV_TERMINAL_SOLAR 51 | if (solar_terminal.bus->voltage < bat_terminal.bus->voltage) { 52 | #else 53 | if (pwm_switch.ext_voltage < bat_terminal.bus->voltage) { 54 | #endif 55 | seconds_zero_solar += 1; 56 | } 57 | else { 58 | // solar voltage > battery voltage after 5 hours of night time means sunrise in the morning 59 | // --> reset daily energy counters 60 | if (seconds_zero_solar > 60 * 60 * 5) { 61 | day_counter++; 62 | solar_in_total_Wh_prev = solar_in_total_Wh; 63 | load_out_total_Wh_prev = load_out_total_Wh; 64 | bat_chg_total_Wh_prev = bat_chg_total_Wh; 65 | bat_dis_total_Wh_prev = bat_dis_total_Wh; 66 | #if CONFIG_HV_TERMINAL_NANOGRID 67 | grid_import_total_Wh_prev = grid_import_total_Wh; 68 | grid_export_total_Wh_prev = grid_export_total_Wh; 69 | #endif 70 | solar_terminal.neg_energy_Wh = 0.0; 71 | #if BOARD_HAS_LOAD_OUTPUT 72 | load.pos_energy_Wh = 0.0; 73 | #endif 74 | bat_terminal.pos_energy_Wh = 0.0; 75 | bat_terminal.neg_energy_Wh = 0.0; 76 | #if CONFIG_HV_TERMINAL_NANOGRID 77 | grid_terminal.pos_energy_Wh = 0.0; 78 | grid_terminal.neg_energy_Wh = 0.0; 79 | #endif 80 | } 81 | seconds_zero_solar = 0; 82 | } 83 | #endif 84 | 85 | bat_chg_total_Wh = 86 | bat_chg_total_Wh_prev + (bat_terminal.pos_energy_Wh > 0 ? bat_terminal.pos_energy_Wh : 0); 87 | bat_dis_total_Wh = 88 | bat_dis_total_Wh_prev + (bat_terminal.neg_energy_Wh > 0 ? bat_terminal.neg_energy_Wh : 0); 89 | 90 | #if CONFIG_HV_TERMINAL_SOLAR || CONFIG_LV_TERMINAL_SOLAR || CONFIG_PWM_TERMINAL_SOLAR 91 | solar_in_total_Wh = solar_in_total_Wh_prev 92 | + (solar_terminal.neg_energy_Wh > 0 ? solar_terminal.neg_energy_Wh : 0); 93 | #endif 94 | 95 | #if BOARD_HAS_LOAD_OUTPUT 96 | load_out_total_Wh = load_out_total_Wh_prev + (load.pos_energy_Wh > 0 ? load.pos_energy_Wh : 0); 97 | #endif 98 | 99 | #if CONFIG_HV_TERMINAL_NANOGRID 100 | grid_import_total_Wh = grid_import_total_Wh_prev 101 | + (grid_terminal.neg_energy_Wh > 0 ? grid_terminal.neg_energy_Wh : 0); 102 | grid_export_total_Wh = grid_export_total_Wh_prev 103 | + (grid_terminal.pos_energy_Wh > 0 ? grid_terminal.pos_energy_Wh : 0); 104 | #endif 105 | } 106 | 107 | void DeviceStatus::update_min_max_values() 108 | { 109 | if (bat_terminal.bus->voltage > battery_voltage_max) { 110 | battery_voltage_max = bat_terminal.bus->voltage; 111 | } 112 | 113 | #if CONFIG_HV_TERMINAL_SOLAR || CONFIG_LV_TERMINAL_SOLAR 114 | if (solar_terminal.bus->voltage > solar_voltage_max) { 115 | solar_voltage_max = solar_terminal.bus->voltage; 116 | } 117 | #elif CONFIG_PWM_TERMINAL_SOLAR 118 | if (pwm_switch.ext_voltage > solar_voltage_max) { 119 | solar_voltage_max = pwm_switch.ext_voltage; 120 | } 121 | #endif 122 | 123 | #if BOARD_HAS_DCDC 124 | if (dcdc.inductor_current > dcdc_current_max) { 125 | dcdc_current_max = dcdc.inductor_current; 126 | } 127 | 128 | if (dcdc.temp_mosfets > mosfet_temp_max) { 129 | mosfet_temp_max = dcdc.temp_mosfets; 130 | } 131 | #endif 132 | 133 | #if BOARD_HAS_LOAD_OUTPUT 134 | if (load.current > load_current_max) { 135 | load_current_max = load.current; 136 | } 137 | #endif 138 | 139 | #if CONFIG_HV_TERMINAL_SOLAR || CONFIG_LV_TERMINAL_SOLAR || CONFIG_PWM_TERMINAL_SOLAR 140 | if (-solar_terminal.power > solar_power_max_day) { 141 | solar_power_max_day = -solar_terminal.power; 142 | if (solar_power_max_day > solar_power_max_total) { 143 | solar_power_max_total = solar_power_max_day; 144 | } 145 | } 146 | #endif 147 | 148 | #if BOARD_HAS_LOAD_OUTPUT 149 | if (load.power > load_power_max_day) { 150 | load_power_max_day = load.power; 151 | if (load_power_max_day > load_power_max_total) { 152 | load_power_max_total = load_power_max_day; 153 | } 154 | } 155 | #endif 156 | 157 | if (charger.bat_temperature > static_cast(bat_temp_max)) { 158 | bat_temp_max = charger.bat_temperature; 159 | } 160 | 161 | if (internal_temp > static_cast(int_temp_max)) { 162 | int_temp_max = internal_temp; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /app/src/load.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #include "load.h" 8 | 9 | #include "board.h" 10 | #include 11 | 12 | #if BOARD_HAS_LOAD_OUTPUT || UNIT_TEST 13 | 14 | #include "device_status.h" 15 | #include "helper.h" 16 | #include "leds.h" 17 | 18 | #define LOAD_CURRENT_MAX DT_PROP(DT_CHILD(DT_PATH(outputs), load), current_max) 19 | 20 | #define PCB_LS_VOLTAGE_MAX DT_PROP(DT_PATH(pcb), ls_voltage_max) 21 | #define PCB_MOSFETS_TJ_MAX DT_PROP(DT_PATH(pcb), mosfets_tj_max) 22 | #define PCB_MOSFETS_TAU_JA DT_PROP(DT_PATH(pcb), mosfets_tau_ja) 23 | #define PCB_INTERNAL_TREF_MAX DT_PROP(DT_PATH(pcb), internal_tref_max) 24 | 25 | extern DeviceStatus dev_stat; 26 | 27 | LoadOutput::LoadOutput(DcBus *dc_bus, void (*switch_fn)(bool), void (*init_fn)(), 28 | bool (*pgood_fn)()) 29 | : PowerPort(dc_bus), switch_set(switch_fn), pgood_check(pgood_fn) 30 | { 31 | state = LOAD_STATE_OFF; 32 | 33 | // call driver initialization function 34 | init_fn(); 35 | 36 | switch_set(false); 37 | junction_temperature = 25; // starting point: 25°C 38 | 39 | oc_recovery_delay = CONFIG_LOAD_OC_RECOVERY_DELAY; 40 | lvd_recovery_delay = CONFIG_LOAD_LVD_RECOVERY_DELAY; 41 | 42 | ov_hysteresis = 0.3; 43 | 44 | enable = true; // switch on in next control() call if everything is fine 45 | } 46 | 47 | // this function is called more often than the state machine 48 | void LoadOutput::control() 49 | { 50 | if (state == LOAD_STATE_ON) { 51 | 52 | // junction temperature calculation model for overcurrent detection 53 | junction_temperature = junction_temperature 54 | + (dev_stat.internal_temp - junction_temperature 55 | + current * current / (LOAD_CURRENT_MAX * LOAD_CURRENT_MAX) 56 | * (PCB_MOSFETS_TJ_MAX - PCB_INTERNAL_TREF_MAX)) 57 | / (PCB_MOSFETS_TAU_JA * CONFIG_CONTROL_FREQUENCY); 58 | 59 | if (junction_temperature > PCB_MOSFETS_TJ_MAX || current > LOAD_CURRENT_MAX * 2) { 60 | flags_set(&error_flags, ERR_LOAD_OVERCURRENT); 61 | oc_timestamp = uptime(); 62 | } 63 | 64 | if (pgood_check != NULL && !pgood_check()) { 65 | flags_set(&error_flags, ERR_LOAD_OVERCURRENT); 66 | oc_timestamp = uptime(); 67 | } 68 | 69 | // negative margin means sourcing current from bus is allowed 70 | if (bus->src_current_margin > -0.1F) { 71 | flags_set(&error_flags, ERR_LOAD_BUS_SRC_CURRENT); 72 | } 73 | 74 | if (bus->voltage < bus->src_control_voltage(disconnect_voltage)) { 75 | flags_set(&error_flags, ERR_LOAD_SHEDDING); 76 | lvd_timestamp = uptime(); 77 | } 78 | 79 | // long-term overvoltage (overvoltage transients are detected as an ADC alert and switch 80 | // off the solar input instead of the load output) 81 | if (bus->voltage > bus->series_voltage(overvoltage) || bus->voltage > PCB_LS_VOLTAGE_MAX) { 82 | ov_debounce_counter++; 83 | if (ov_debounce_counter > CONFIG_CONTROL_FREQUENCY) { 84 | // waited 1s before setting the flag 85 | flags_set(&error_flags, ERR_LOAD_OVERVOLTAGE); 86 | } 87 | } 88 | else { 89 | ov_debounce_counter = 0; 90 | } 91 | 92 | if (error_flags) { 93 | stop(); 94 | } 95 | 96 | if (enable == false) { 97 | switch_set(false); 98 | state = LOAD_STATE_OFF; 99 | } 100 | } 101 | else { 102 | // load is off: check if errors are resolved and if load can be switched on 103 | 104 | if (flags_check(&error_flags, ERR_LOAD_SHEDDING) 105 | && bus->voltage > bus->src_control_voltage(reconnect_voltage) 106 | && uptime() - lvd_timestamp > lvd_recovery_delay) 107 | { 108 | flags_clear(&error_flags, ERR_LOAD_SHEDDING); 109 | } 110 | 111 | if (flags_check(&error_flags, ERR_LOAD_OVERCURRENT | ERR_LOAD_VOLTAGE_DIP) 112 | && uptime() - oc_timestamp > oc_recovery_delay) 113 | { 114 | flags_clear(&error_flags, ERR_LOAD_OVERCURRENT | ERR_LOAD_VOLTAGE_DIP); 115 | } 116 | 117 | if (flags_check(&error_flags, ERR_LOAD_OVERVOLTAGE) 118 | && bus->voltage < (bus->series_voltage(overvoltage) - ov_hysteresis) 119 | && bus->voltage < (PCB_LS_VOLTAGE_MAX - ov_hysteresis)) 120 | { 121 | flags_clear(&error_flags, ERR_LOAD_OVERVOLTAGE); 122 | } 123 | 124 | if (flags_check(&error_flags, ERR_LOAD_BUS_SRC_CURRENT) && bus->src_current_margin < 0) { 125 | flags_clear(&error_flags, ERR_LOAD_BUS_SRC_CURRENT); 126 | } 127 | 128 | if (flags_check(&error_flags, ERR_LOAD_SHORT_CIRCUIT) && enable == false) { 129 | // stay here until the charge controller is reset or load is manually switched off 130 | flags_clear(&error_flags, ERR_LOAD_SHORT_CIRCUIT); 131 | } 132 | 133 | // finally switch on if all errors were resolved and at least 1A src current is available 134 | if (enable == true && !error_flags && bus->src_current_margin < -1.0F) { 135 | switch_set(true); 136 | state = LOAD_STATE_ON; 137 | } 138 | } 139 | 140 | info = error_flags > 0 ? -error_flags : state; 141 | } 142 | 143 | void LoadOutput::stop(uint32_t flag) 144 | { 145 | switch_set(false); 146 | state = LOAD_STATE_OFF; 147 | flags_set(&error_flags, flag); 148 | 149 | // flicker the load LED if failure was most probably caused by the user 150 | if (flags_check(&error_flags, 151 | ERR_LOAD_OVERCURRENT | ERR_LOAD_VOLTAGE_DIP | ERR_LOAD_SHORT_CIRCUIT)) { 152 | #if LED_EXISTS(load) 153 | leds_flicker(LED_POS(load), LED_TIMEOUT_INFINITE); 154 | #endif 155 | oc_timestamp = uptime(); 156 | } 157 | } 158 | 159 | void LoadOutput::set_voltage_limits(float lvd, float lvr, float ov) 160 | { 161 | disconnect_voltage = lvd; 162 | reconnect_voltage = lvr; 163 | 164 | overvoltage = ov; 165 | } 166 | 167 | extern LoadOutput load; 168 | 169 | void load_short_circuit_stop() 170 | { 171 | load.stop(ERR_LOAD_SHORT_CIRCUIT); 172 | } 173 | 174 | #endif /* BOARD_HAS_LOAD_OUTPUT */ 175 | -------------------------------------------------------------------------------- /boards/arm/mppt_2420_hpx/mppt_2420_hpx.dts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | /dts-v1/; 8 | #include 9 | #include 10 | 11 | / { 12 | model = "Libre Solar MPPT 2420 HPX"; 13 | compatible = "st,stm32g431"; 14 | 15 | pcb { 16 | compatible = "charge-controller"; 17 | 18 | type = "MPPT 2420 HPX"; 19 | version-str = "v0.1"; 20 | version-num = <1>; 21 | 22 | hs-voltage-max = <80>; 23 | ls-voltage-max = <32>; 24 | dcdc-current-max = <20>; 25 | }; 26 | 27 | chosen { 28 | zephyr,console = &usart1; 29 | zephyr,shell-uart = &usart1; 30 | zephyr,sram = &sram0; 31 | zephyr,flash = &flash0; 32 | }; 33 | 34 | leds { 35 | compatible = "charlieplexing-leds"; 36 | gpios = <&gpioc 15 GPIO_ACTIVE_HIGH>; 37 | 38 | pwr { 39 | states = <1>; 40 | }; 41 | }; 42 | 43 | outputs { 44 | compatible = "outputs"; 45 | 46 | pwm_switch { 47 | pwms = <&pwm3 2 (20*1000*1000) PWM_POLARITY_NORMAL>; /* 20 ms period = 50 Hz */ 48 | current-max = <20>; 49 | kconfig-flag; /* make node visible in Kconfig */ 50 | }; 51 | 52 | load { 53 | gpios = <&gpiob 15 GPIO_ACTIVE_LOW>; 54 | current-max = <20>; 55 | kconfig-flag; /* make node visible in Kconfig */ 56 | }; 57 | 58 | charge_pump { 59 | pwms = <&pwm8 1 200000 PWM_POLARITY_NORMAL>; /* 200 us period = 5 kHz */ 60 | }; 61 | 62 | hv-out { 63 | gpios = <&gpiob 12 GPIO_ACTIVE_LOW>; 64 | }; 65 | }; 66 | 67 | adc-inputs { 68 | compatible = "adc-inputs"; 69 | 70 | v-low { 71 | io-channels = <&adc1 12>; 72 | multiplier = <105600>; 73 | divider = <5600>; 74 | }; 75 | 76 | v-high { 77 | io-channels = <&adc1 15>; 78 | multiplier = <102200>; 79 | divider = <2200>; 80 | }; 81 | 82 | hv-ext-sense { 83 | io-channels = <&adc1 11>; 84 | multiplier = <1>; 85 | divider = <1>; 86 | }; 87 | 88 | v-pwm { 89 | io-channels = <&adc1 4>; 90 | multiplier = <25224>; // (12*8.2 + 120*8.2 + 120*12) * 10 91 | divider = <984>; // (8.2*12) * 10 92 | offset = <37414>; // 65536 / (1 + 8.2/120 + 8.2/12) 93 | }; 94 | 95 | temp-fets { 96 | io-channels = <&adc1 6>; 97 | multiplier = <10000>; // 10k NTC series resistor 98 | divider = <1>; 99 | }; 100 | 101 | vref-mcu { 102 | io-channels = <&adc1 18>; 103 | multiplier = <1>; 104 | divider = <1>; 105 | }; 106 | 107 | temp-mcu { 108 | io-channels = <&adc1 16>; 109 | multiplier = <1>; 110 | divider = <1>; 111 | }; 112 | 113 | i-dcdc { 114 | io-channels = <&adc2 1>; 115 | // amp gain: 20, resistor: 2 mOhm 116 | multiplier = <1000>; // 1000 117 | divider = <40>; // 2*20 118 | }; 119 | 120 | i-load { 121 | io-channels = <&adc2 2>; 122 | // amp gain: 68/2.2, resistor: 2 mOhm 123 | multiplier = <2200>; // 1000*2.2 124 | divider = <136>; // 2*68 125 | }; 126 | 127 | i-pwm { 128 | io-channels = <&adc2 5>; 129 | // amp gain: 68/2.2, resistor: 2 mOhm 130 | multiplier = <2200>; // 1000*2.2 131 | divider = <136>; // 2*68 132 | }; 133 | }; 134 | 135 | aliases { 136 | uart-dbg = &usart1; 137 | uart-uext = &usart3; 138 | spi-uext = &spi1; 139 | i2c-uext = &i2c1; 140 | }; 141 | }; 142 | 143 | &clk_hsi{ 144 | status = "okay"; 145 | }; 146 | 147 | &pll { 148 | div-m = <4>; 149 | mul-n = <75>; 150 | div-p = <7>; 151 | div-q = <2>; 152 | div-r = <2>; 153 | clocks = <&clk_hsi>; 154 | status = "okay"; 155 | }; 156 | 157 | &rcc { 158 | clocks = <&pll>; 159 | clock-frequency = ; 160 | ahb-prescaler = <1>; 161 | apb1-prescaler = <1>; 162 | apb2-prescaler = <1>; 163 | }; 164 | 165 | &usart1 { 166 | pinctrl-0 = <&usart1_tx_pa9 &usart1_rx_pa10>; 167 | pinctrl-names = "default"; 168 | current-speed = <115200>; 169 | status = "okay"; 170 | }; 171 | 172 | &usart1_rx_pa10 { 173 | bias-pull-up; // avoid junk characters if pin is left floating 174 | }; 175 | 176 | &usart3 { 177 | pinctrl-0 = <&usart3_tx_pc10 &usart3_rx_pc11>; 178 | pinctrl-names = "default"; 179 | current-speed = <115200>; 180 | status = "okay"; 181 | }; 182 | 183 | &usart3_rx_pc11 { 184 | bias-pull-up; // avoid junk characters if pin is left floating 185 | }; 186 | 187 | &i2c1 { 188 | pinctrl-0 = <&i2c1_scl_pa15 &i2c1_sda_pb7>; 189 | pinctrl-names = "default"; 190 | status = "okay"; 191 | }; 192 | 193 | &spi1 { 194 | pinctrl-0 = <&spi1_sck_pb3 &spi1_miso_pb4 &spi1_mosi_pb5>; 195 | pinctrl-names = "default"; 196 | cs-gpios = <&gpiod 2 GPIO_ACTIVE_LOW>; 197 | status = "okay"; 198 | }; 199 | 200 | &rtc { 201 | status = "okay"; 202 | }; 203 | 204 | &flash0 { 205 | /* 206 | * For more information, see: 207 | * http://docs.zephyrproject.org/latest/guides/dts/index.html#flash-partitions 208 | */ 209 | partitions { 210 | compatible = "fixed-partitions"; 211 | #address-cells = <1>; 212 | #size-cells = <1>; 213 | 214 | /* Set 4Kb of storage at the end of the 128Kb of flash */ 215 | storage_partition: partition@1f000 { 216 | reg = <0x0001f000 0x00001000>; 217 | }; 218 | }; 219 | }; 220 | 221 | &iwdg { 222 | status = "okay"; 223 | }; 224 | 225 | &timers1 { 226 | status = "okay"; 227 | 228 | halfbridge: halfbridge { 229 | compatible = "half-bridge"; 230 | pinctrl-0 = <&tim1_ch1_pa8 &tim1_ch1n_pb13>; 231 | pinctrl-names = "default"; 232 | frequency = <70000>; 233 | deadtime = <300>; 234 | }; 235 | }; 236 | 237 | /* TIM3 (CH2) used for PWM power switch */ 238 | &timers3 { 239 | status = "okay"; 240 | pwm3: pwm { 241 | status = "okay"; 242 | pinctrl-0 = <&tim3_ch2_pc7>; 243 | pinctrl-names = "default"; 244 | }; 245 | }; 246 | 247 | /* TIM8 (CH1) used for charge pump */ 248 | &timers8 { 249 | status = "okay"; 250 | st,prescaler = <149>; /* reduce counter clock frequency to 1 MHz */ 251 | pwm8: pwm { 252 | status = "okay"; 253 | pinctrl-0 = <&tim8_ch1_pc6>; 254 | pinctrl-names = "default"; 255 | }; 256 | }; 257 | 258 | &i2c3 { 259 | pinctrl-0 = <&i2c3_scl_pc8 &i2c3_sda_pc9>; 260 | pinctrl-names = "default"; 261 | status = "okay"; 262 | clock-frequency = ; 263 | 264 | eeprom: eeprom@50 { 265 | // Microchip 24AA32A 266 | compatible = "atmel,at24"; 267 | reg = <0x50>; 268 | size = <32768>; 269 | pagesize = <32>; 270 | address-width = <16>; 271 | /* 272 | * timeout of 5 ms as suggested in datasheet seems too optimistic 273 | * (several write errors occured during testing) 274 | */ 275 | timeout = <7>; 276 | }; 277 | }; 278 | -------------------------------------------------------------------------------- /app/src/load.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) The Libre Solar Project Contributors 3 | * 4 | * SPDX-License-Identifier: Apache-2.0 5 | */ 6 | 7 | #ifndef LOAD_H 8 | #define LOAD_H 9 | 10 | /** @file 11 | * 12 | * @brief 13 | * Load/USB output functions and data types 14 | */ 15 | 16 | #include 17 | #include 18 | #include 19 | 20 | #ifdef __cplusplus 21 | 22 | #include "power_port.h" 23 | 24 | /** 25 | * Load/USB output states 26 | */ 27 | enum LoadState 28 | { 29 | LOAD_STATE_OFF = 0, ///< Actively disabled 30 | LOAD_STATE_ON = 1, ///< Normal state: On 31 | }; 32 | 33 | /** Load error flags 34 | * 35 | * When adding new flags, please make sure to use only up to 32 errors 36 | * Each enum must represent a unique power of 2 number 37 | */ 38 | enum LoadErrorFlag 39 | { 40 | /** 41 | * Available energy or power too low 42 | * 43 | * Switching off the load can be triggered either by a low battery voltage or by low state of 44 | * charge (SOC) in case of more advanced battery management. 45 | * 46 | * Set in LoadOutput::control() and cleared after reconnect delay passed and voltage is above 47 | * reconnect threshold again. 48 | */ 49 | ERR_LOAD_SHEDDING = 1U << 0, 50 | 51 | /** 52 | * Too high voltage for load 53 | * 54 | * Set and cleared in LoadOutput::control() 55 | */ 56 | ERR_LOAD_OVERVOLTAGE = 1U << 1, 57 | 58 | /** 59 | * Long-term overcurrent at load port 60 | * 61 | * Set in LoadOutput::control() and cleared after configurable delay. 62 | */ 63 | ERR_LOAD_OVERCURRENT = 1U << 2, 64 | 65 | /** 66 | * Short circuit detected at load port 67 | * 68 | * Set by LoadOutput::control() after overcurrent comparator triggered, cleared only if 69 | * load output is manually disabled and enabled again. 70 | */ 71 | ERR_LOAD_SHORT_CIRCUIT = 1U << 3, 72 | 73 | /** 74 | * Overcurrent identified via voltage dip (may be caused by too small battery) 75 | * 76 | * Set and cleared in LoadOutput::control(). Treated same as load overcurrent. 77 | */ 78 | ERR_LOAD_VOLTAGE_DIP = 1U << 4, 79 | 80 | /** 81 | * The bus the load is connected to disabled sourcing current from it 82 | * 83 | * Reasons can be that battery temperature limits were exceeded. Voltage limits should be 84 | * covered by the load directly. 85 | */ 86 | ERR_LOAD_BUS_SRC_CURRENT = 1U << 5 87 | }; 88 | 89 | /** Load output type 90 | * 91 | * Stores status of load output incl. 5V USB output (if existing on PCB) 92 | */ 93 | class LoadOutput : public PowerPort 94 | { 95 | public: 96 | /** 97 | * Initialize LoadOutput struct and overcurrent / short circuit protection comparator (if 98 | * existing) 99 | * 100 | * @param dc_bus DC bus the load is connected to 101 | * @param switch_fn Pointer to function for enabling/disabling load switch 102 | * @param init_fn Pointer to function for load driver initialization 103 | * @param pgood_fn Pointer to pgood check 104 | */ 105 | LoadOutput(DcBus *dc_bus, void (*switch_fn)(bool), void (*init_fn)(), bool (*pgood_fn)()); 106 | 107 | /** Main load control function, should be called by control timer 108 | * 109 | * This function includes the load state machine 110 | */ 111 | void control(); 112 | 113 | /** Fast emergency stop function 114 | * 115 | * May be called from an ISR which detected overvoltage / overcurrent conditions 116 | * 117 | * @param error_flag Optional error flag that should be set 118 | */ 119 | void stop(uint32_t error_flag = 0); 120 | 121 | /** 122 | * Update of load voltage limits (typically based on battery configuration) 123 | * 124 | * @param lvd Low voltage disconnect setpoint 125 | * @param lvr Low voltage reconnect setpoint 126 | * @param ov Overvoltage setpoint 127 | */ 128 | void set_voltage_limits(float lvd, float lvr, float ov); 129 | 130 | uint32_t state; ///< Current state of load output switch 131 | 132 | uint32_t error_flags = 0; ///< Stores error flags as bits according to LoadErrorFlag enum 133 | 134 | int32_t info; ///< Contains either the state or negative value of error_flags 135 | ///< in case of error_flags > 0. This allows to have a single 136 | ///< variable for load state diagnosis. 137 | 138 | bool enable = false; ///< Target on state set via communication port (overruled if 139 | ///< battery is empty or any errors occured) 140 | 141 | time_t oc_timestamp; ///< Time when last overcurrent event occured 142 | uint32_t oc_recovery_delay; ///< Seconds before we attempt to re-enable the load 143 | ///< after an overcurrent event 144 | 145 | float disconnect_voltage = 0; ///< Low voltage disconnect (LVD) setpoint 146 | float reconnect_voltage = 0; ///< Low voltage reconnect (LVD) setpoint 147 | 148 | time_t lvd_timestamp; ///< Time when last low voltage disconnect happened 149 | uint32_t lvd_recovery_delay; ///< Seconds before we re-enable the load after a low voltage 150 | ///< disconnect 151 | 152 | float junction_temperature; ///< calculated using thermal model based on current and ambient 153 | ///< temperature measurement (unit: °C) 154 | 155 | float overvoltage = 0; ///< Upper voltage limit 156 | float ov_hysteresis; ///< Hysteresis to switch back on after an overvoltage event 157 | 158 | private: 159 | /** 160 | * Pointer to the load switch function 161 | */ 162 | void (*switch_set)(bool); 163 | 164 | /** 165 | * Pointer to the function to check the power output status 166 | */ 167 | bool (*pgood_check)(void); 168 | 169 | /** 170 | * Used to prevent switching of because of very short voltage dip 171 | */ 172 | int uv_debounce_counter = 0; 173 | 174 | /** 175 | * Used to prevent switching off because of short voltage spike 176 | */ 177 | int ov_debounce_counter = 0; 178 | }; 179 | #endif 180 | 181 | #ifdef __cplusplus 182 | extern "C" { 183 | #endif 184 | 185 | void load_out_init(); 186 | void usb_out_init(); 187 | 188 | void load_out_set(bool); 189 | void usb_out_set(bool); 190 | 191 | void load_short_circuit_stop(); 192 | 193 | bool pgood_check(); 194 | 195 | #ifdef __cplusplus 196 | } 197 | #endif 198 | 199 | #endif /* LOAD_H */ 200 | --------------------------------------------------------------------------------